aboutsummaryrefslogtreecommitdiff
path: root/weechat/python
diff options
context:
space:
mode:
Diffstat (limited to 'weechat/python')
-rw-r--r--weechat/python/autoconnect.py95
-rwxr-xr-xweechat/python/autojoin.py194
-rw-r--r--weechat/python/autojoin_on_invite.py119
l---------weechat/python/autoload/autoconnect.py1
l---------weechat/python/autoload/autojoin.py1
l---------weechat/python/autoload/autojoin_on_invite.py1
l---------weechat/python/autoload/autosort.py1
l---------weechat/python/autoload/bufsave.py1
l---------weechat/python/autoload/clone_scanner.py1
l---------weechat/python/autoload/confversion.py1
l---------weechat/python/autoload/country.py1
l---------weechat/python/autoload/emoji_aliases.py1
l---------weechat/python/autoload/grep.py1
l---------weechat/python/autoload/histman.py1
l---------weechat/python/autoload/irssi_awaylog.py1
l---------weechat/python/autoload/otr.py1
l---------weechat/python/autoload/pyrnotify.py1
l---------weechat/python/autoload/queryman.py1
l---------weechat/python/autoload/sshnotify.py1
-rw-r--r--weechat/python/autoload/wee_slack.py2557
-rwxr-xr-xweechat/python/autosort.py865
-rwxr-xr-xweechat/python/bufsave.py113
-rw-r--r--weechat/python/clone_scanner.py514
-rw-r--r--weechat/python/confversion.py124
-rw-r--r--weechat/python/country.py577
-rw-r--r--weechat/python/emoji_aliases.py1481
-rw-r--r--weechat/python/grep.py1738
-rw-r--r--weechat/python/histman.py429
-rw-r--r--weechat/python/irssi_awaylog.py88
-rw-r--r--weechat/python/otr.py2062
-rw-r--r--weechat/python/pyrnotify.py158
-rw-r--r--weechat/python/queryman.py130
-rwxr-xr-xweechat/python/sshnotify.py308
-rwxr-xr-xweechat/python/theme.py1282
34 files changed, 12850 insertions, 0 deletions
diff --git a/weechat/python/autoconnect.py b/weechat/python/autoconnect.py
new file mode 100644
index 0000000..2bd179a
--- /dev/null
+++ b/weechat/python/autoconnect.py
@@ -0,0 +1,95 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011 Arnaud Renevier <arno@renevier.net>
+#
+# > updated by kbdkode <kbdkode@protonmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+SCRIPT_NAME = "autoconnect"
+SCRIPT_AUTHOR = "arno <arno@renevier.net>"
+SCRIPT_VERSION = "0.3.1"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "reopens servers and channels opened last time weechat closed"
+SCRIPT_COMMAND = "autoconnect"
+
+try:
+ import weechat
+except:
+ print "This script must be run under WeeChat."
+ print "Get WeeChat now at: http://www.weechat.org/"
+ quit()
+
+
+weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", "")
+
+quitting = False
+
+
+def get_autojoin_channels(server):
+ channels = weechat.config_string(weechat.config_get("irc.server.%s.autojoin" % (server,)))
+ return set(channels.split(',')) if channels else set()
+
+
+def update_autojoin_channels(server, channels):
+ channels = ','.join(channels) if channels else "null"
+ weechat.command("", "/mute /set irc.server.%s.autojoin %s" % (server, channels))
+
+
+def joinpart_cb(data, signal, signal_data):
+ server = signal.split(',')[0]
+ if weechat.info_get("irc_nick_from_host", signal_data) != weechat.info_get("irc_nick", server):
+ # nick which has joined is not our current nick
+ return weechat.WEECHAT_RC_OK
+
+ autojoin_channels = get_autojoin_channels(server)
+
+ if signal.endswith("irc_in2_JOIN"):
+ weechat.command("", "/mute /set irc.server.%s.autoconnect on" % (server,))
+
+ # get all channels joined (without passphrases)
+ chans = [j.split()[0].strip() for j in signal_data.split(None, 2)[2].split(',')]
+ autojoin_channels.add(','.join(chans))
+
+ elif signal.endswith("irc_in2_PART"):
+ channel = signal_data.split(' PART ')[1].split()[0]
+ autojoin_channels.discard(channel)
+
+ update_autojoin_channels(server, autojoin_channels)
+ weechat.command("", "/save irc")
+ return weechat.WEECHAT_RC_OK
+
+
+def disconnect_cb(data, signal, signal_data):
+ global quitting
+ if not quitting:
+ server = signal_data.split(',')[0]
+
+ weechat.command("", "/mute /set irc.server.%s.autoconnect null" % (server,))
+ weechat.command("", "/mute /set irc.server.%s.autojoin null" % (server,))
+
+ weechat.command("", "/mute /save irc")
+ return weechat.WEECHAT_RC_OK
+
+
+def quit_cb(data, signal, signal_data):
+ global quitting
+ quitting = True
+ return weechat.WEECHAT_RC_OK
+
+
+weechat.hook_signal("quit", "quit_cb", "")
+weechat.hook_signal("*,irc_in2_join", "joinpart_cb", "")
+weechat.hook_signal("*,irc_in2_part", "joinpart_cb", "")
+weechat.hook_signal("irc_server_disconnected", "disconnect_cb", "")
diff --git a/weechat/python/autojoin.py b/weechat/python/autojoin.py
new file mode 100755
index 0000000..61449ff
--- /dev/null
+++ b/weechat/python/autojoin.py
@@ -0,0 +1,194 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2009 by xt <xt@bash.no>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# (this script requires WeeChat 0.3.0 or newer)
+#
+# History:
+# 2009-06-18, xt <xt@bash.no>
+# version 0.1: initial release
+#
+# 2009-10-18, LBo <leon@tim-online.nl>
+# version 0.2: added autosaving of join channels
+# /set plugins.var.python.autojoin.autosave 'on'
+#
+# 2009-10-19, LBo <leon@tim-online.nl>
+# version 0.2.1: now only responds to part messages from self
+# find_channels() only returns join'ed channels, not all the buffers
+# updated description and docs
+#
+# 2009-10-20, LBo <leon@tim-online.nl>
+# version 0.2.2: fixed quit callback
+# removed the callbacks on part & join messages
+#
+# 2012-04-14, Filip H.F. "FiXato" Slagter <fixato+weechat+autojoin@gmail.com>
+# version 0.2.3: fixed bug with buffers of which short names were changed.
+# Now using 'name' from irc_channel infolist.
+# version 0.2.4: Added support for key-protected channels
+#
+# 2014-05-22, Nathaniel Wesley Filardo <PADEBR2M2JIQN02N9OO5JM0CTN8K689P@cmx.ietfng.org>
+# version 0.2.5: Fix keyed channel support
+#
+# 2016-01-13, The fox in the shell <KellerFuchs@hashbang.sh>
+# version 0.2.6: Support keeping chan list as secured data
+#
+# @TODO: add options to ignore certain buffers
+# @TODO: maybe add an option to enable autosaving on part/join messages
+
+import weechat as w
+import re
+
+SCRIPT_NAME = "autojoin"
+SCRIPT_AUTHOR = "xt <xt@bash.no>"
+SCRIPT_VERSION = "0.2.6"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Configure autojoin for all servers according to currently joined channels"
+SCRIPT_COMMAND = "autojoin"
+
+# script options
+settings = {
+ "autosave": "off",
+}
+
+if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
+
+ w.hook_command(SCRIPT_COMMAND,
+ SCRIPT_DESC,
+ "[--run]",
+ " --run: actually run the commands instead of displaying\n",
+ "--run",
+ "autojoin_cb",
+ "")
+
+ #w.hook_signal('*,irc_in2_join', 'autosave_channels_on_activity', '')
+ #w.hook_signal('*,irc_in2_part', 'autosave_channels_on_activity', '')
+ w.hook_signal('quit', 'autosave_channels_on_quit', '')
+
+# Init everything
+for option, default_value in settings.items():
+ if w.config_get_plugin(option) == "":
+ w.config_set_plugin(option, default_value)
+
+def autosave_channels_on_quit(signal, callback, callback_data):
+ ''' Autojoin current channels '''
+ if w.config_get_plugin(option) != "on":
+ return w.WEECHAT_RC_OK
+
+ items = find_channels()
+
+ # print/execute commands
+ for server, channels in items.iteritems():
+ process_server(server, channels)
+
+ return w.WEECHAT_RC_OK
+
+
+def autosave_channels_on_activity(signal, callback, callback_data):
+ ''' Autojoin current channels '''
+ if w.config_get_plugin(option) != "on":
+ return w.WEECHAT_RC_OK
+
+ items = find_channels()
+
+ # print/execute commands
+ for server, channels in items.iteritems():
+ nick = w.info_get('irc_nick', server)
+
+ pattern = "^:%s!.*(JOIN|PART) :?(#[^ ]*)( :.*$)?" % nick
+ match = re.match(pattern, callback_data)
+
+ if match: # check if nick is my nick. In that case: save
+ process_server(server, channels)
+ else: # someone else: ignore
+ continue
+
+ return w.WEECHAT_RC_OK
+
+def autojoin_cb(data, buffer, args):
+ """Old behaviour: doesn't save empty channel list"""
+ """In fact should also save open buffers with a /part'ed channel"""
+ """But I can't believe somebody would want that behaviour"""
+ items = find_channels()
+
+ if args == '--run':
+ run = True
+ else:
+ run = False
+
+ # print/execute commands
+ for server, channels in items.iteritems():
+ process_server(server, channels, run)
+
+ return w.WEECHAT_RC_OK
+
+def process_server(server, channels, run=True):
+ option = "irc.server.%s.autojoin" % server
+ channels = channels.rstrip(',')
+ oldchans = w.config_string(w.config_get(option))
+
+ if not channels: # empty channel list
+ return
+
+ # Note: re already caches the result of regexp compilation
+ sec = re.match('^\${sec\.data\.(.*)}$', oldchans)
+ if sec:
+ secvar = sec.group(1)
+ command = "/secure set %s %s" % (secvar, channels)
+ else:
+ command = "/set irc.server.%s.autojoin '%s'" % (server, channels)
+
+ if run:
+ w.command('', command)
+ else:
+ w.prnt('', command)
+
+def find_channels():
+ """Return list of servers and channels"""
+ #@TODO: make it return a dict with more options like "nicks_count etc."
+ items = {}
+ infolist = w.infolist_get('irc_server', '', '')
+ # populate servers
+ while w.infolist_next(infolist):
+ items[w.infolist_string(infolist, 'name')] = ''
+
+ w.infolist_free(infolist)
+
+ # populate channels per server
+ for server in items.keys():
+ keys = []
+ keyed_channels = []
+ unkeyed_channels = []
+ items[server] = '' #init if connected but no channels
+ infolist = w.infolist_get('irc_channel', '', server)
+ while w.infolist_next(infolist):
+ if w.infolist_integer(infolist, 'nicks_count') == 0:
+ #parted but still open in a buffer: bit hackish
+ continue
+ if w.infolist_integer(infolist, 'type') == 0:
+ key = w.infolist_string(infolist, "key")
+ if len(key) > 0:
+ keys.append(key)
+ keyed_channels.append(w.infolist_string(infolist, "name"))
+ else :
+ unkeyed_channels.append(w.infolist_string(infolist, "name"))
+ items[server] = ','.join(keyed_channels + unkeyed_channels)
+ if len(keys) > 0:
+ items[server] += ' %s' % ','.join(keys)
+ w.infolist_free(infolist)
+
+ return items
+
diff --git a/weechat/python/autojoin_on_invite.py b/weechat/python/autojoin_on_invite.py
new file mode 100644
index 0000000..45adab5
--- /dev/null
+++ b/weechat/python/autojoin_on_invite.py
@@ -0,0 +1,119 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2009 by xt <xt@bash.no>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+#
+# (this script requires WeeChat 0.3.0 or newer)
+#
+# History:
+# 2015-10-11, Simmo Saan <simmo.saan@gmail.com>
+# version 0.6: allow joining channels with keys in autojoin
+# 2013-12-21, Sebastien Helleu <flashcode@flashtux.org>
+# version 0.5: fix parsing of INVITE message
+# 2013-11-28, sakkemo <scajanus@gmail.com>
+# version 0.4: add whitelist for nicks/channels
+# 2009-11-09, xt <xt@bash.no>
+# version 0.3: add ignore option for channels
+# 2009-10-29, xt <xt@bash.no>
+# version 0.2: add ignore option
+# 2009-10-28, xt <xt@bash.no>
+# version 0.1: initial release
+
+import weechat as w
+import re
+
+SCRIPT_NAME = "autojoin_on_invite"
+SCRIPT_AUTHOR = "xt <xt@bash.no>"
+SCRIPT_VERSION = "0.6"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Auto joins channels when invited"
+
+# script options
+settings = {
+ 'whitelist_nicks': '', # comma separated list of nicks,
+ # overrides ignore_nicks
+ 'whitelist_channels': '', # comma separated list of channels,
+ # overrides ignore_channels
+ 'ignore_nicks': '', # comma separated list of nicks
+ #that we will not accept auto invite from
+ 'ignore_channels': '', # comma separated list of channels to not join
+ 'autojoin_key': 'on', # use channel keys from server's autojoin list
+}
+
+if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
+ SCRIPT_DESC, "", ""):
+ for option, default_value in settings.iteritems():
+ if not w.config_is_set_plugin(option):
+ w.config_set_plugin(option, default_value)
+
+ w.hook_signal('*,irc_in2_invite', 'invite_cb', '')
+
+def join(server, channel):
+ key = None
+
+ if w.config_string_to_boolean(w.config_get_plugin('autojoin_key')):
+ autojoin = w.config_string(w.config_get('irc.server.%s.autojoin' % server)).split(' ', 1)
+
+ if len(autojoin) > 1: # any keys specified
+ autojoin_keys = dict(zip(autojoin[0].split(','), autojoin[1].split(',')))
+ key = autojoin_keys.get(channel) # defaults to None when not set
+
+ if key:
+ w.command('', '/quote -server %s JOIN %s %s' % (server, channel, key))
+ else:
+ w.command('', '/quote -server %s JOIN %s' % (server, channel))
+
+def invite_cb(data, signal, signal_data):
+ server = signal.split(',')[0] # EFNet,irc_in_INVITE
+ channel = signal_data.split()[-1].lstrip(':') # :nick!ident@host.name INVITE yournick :#channel
+ from_nick = re.match(':(?P<nick>.+)!', signal_data).groups()[0]
+
+ if len(w.config_get_plugin('whitelist_nicks')) > 0 and len(w.config_get_plugin('whitelist_channels')) > 0: # if there's two whitelists, accept both
+ if from_nick in w.config_get_plugin('whitelist_nicks').split(',') or channel in w.config_get_plugin('whitelist_channels').split(','):
+ w.prnt('', 'Automatically joining %s on server %s, invitation from %s (whitelist).' \
+ %(channel, server, from_nick))
+ join(server, channel)
+ else:
+ w.prnt('', 'Ignoring invite from %s to channel %s. Neither inviter nor channel in whitelist.' %(from_nick, channel))
+
+ elif len(w.config_get_plugin('whitelist_nicks')) > 0: # if there's a whitelist, accept nicks in it
+ if from_nick in w.config_get_plugin('whitelist_nicks').split(','):
+ w.prnt('', 'Automatically joining %s on server %s, invitation from %s (whitelist).' \
+ %(channel, server, from_nick))
+ join(server, channel)
+ else:
+ w.prnt('', 'Ignoring invite from %s to channel %s. Inviter not in whitelist.' %(from_nick, channel))
+
+ elif len(w.config_get_plugin('whitelist_channels')) > 0: # if there's a whitelist, accept channels in it
+ if channel in w.config_get_plugin('whitelist_channels').split(','):
+ w.prnt('', 'Automatically joining %s on server %s, invitation from %s (whitelist).' \
+ %(channel, server, from_nick))
+ join(server, channel)
+ else:
+ w.prnt('', 'Ignoring invite from %s to channel %s. Channel not in whitelist.' %(from_nick, channel))
+
+ else: # use the ignore lists to make the decision
+ if from_nick in w.config_get_plugin('ignore_nicks').split(','):
+ w.prnt('', 'Ignoring invite from %s to channel %s. Invite from ignored inviter.' %(from_nick, channel))
+ elif channel in w.config_get_plugin('ignore_channels').split(','):
+ w.prnt('', 'Ignoring invite from %s to channel %s. Invite to ignored channel.' %(from_nick, channel))
+ else:
+ w.prnt('', 'Automatically joining %s on server %s, invitation from %s.' \
+ %(channel, server, from_nick))
+ join(server, channel)
+
+ return w.WEECHAT_RC_OK
diff --git a/weechat/python/autoload/autoconnect.py b/weechat/python/autoload/autoconnect.py
new file mode 120000
index 0000000..7e541b6
--- /dev/null
+++ b/weechat/python/autoload/autoconnect.py
@@ -0,0 +1 @@
+../autoconnect.py \ No newline at end of file
diff --git a/weechat/python/autoload/autojoin.py b/weechat/python/autoload/autojoin.py
new file mode 120000
index 0000000..6fceded
--- /dev/null
+++ b/weechat/python/autoload/autojoin.py
@@ -0,0 +1 @@
+../autojoin.py \ No newline at end of file
diff --git a/weechat/python/autoload/autojoin_on_invite.py b/weechat/python/autoload/autojoin_on_invite.py
new file mode 120000
index 0000000..5a64340
--- /dev/null
+++ b/weechat/python/autoload/autojoin_on_invite.py
@@ -0,0 +1 @@
+../autojoin_on_invite.py \ No newline at end of file
diff --git a/weechat/python/autoload/autosort.py b/weechat/python/autoload/autosort.py
new file mode 120000
index 0000000..1850897
--- /dev/null
+++ b/weechat/python/autoload/autosort.py
@@ -0,0 +1 @@
+../autosort.py \ No newline at end of file
diff --git a/weechat/python/autoload/bufsave.py b/weechat/python/autoload/bufsave.py
new file mode 120000
index 0000000..1aaad6f
--- /dev/null
+++ b/weechat/python/autoload/bufsave.py
@@ -0,0 +1 @@
+../bufsave.py \ No newline at end of file
diff --git a/weechat/python/autoload/clone_scanner.py b/weechat/python/autoload/clone_scanner.py
new file mode 120000
index 0000000..bd46088
--- /dev/null
+++ b/weechat/python/autoload/clone_scanner.py
@@ -0,0 +1 @@
+../clone_scanner.py \ No newline at end of file
diff --git a/weechat/python/autoload/confversion.py b/weechat/python/autoload/confversion.py
new file mode 120000
index 0000000..67733b0
--- /dev/null
+++ b/weechat/python/autoload/confversion.py
@@ -0,0 +1 @@
+../confversion.py \ No newline at end of file
diff --git a/weechat/python/autoload/country.py b/weechat/python/autoload/country.py
new file mode 120000
index 0000000..003dc34
--- /dev/null
+++ b/weechat/python/autoload/country.py
@@ -0,0 +1 @@
+../country.py \ No newline at end of file
diff --git a/weechat/python/autoload/emoji_aliases.py b/weechat/python/autoload/emoji_aliases.py
new file mode 120000
index 0000000..d116468
--- /dev/null
+++ b/weechat/python/autoload/emoji_aliases.py
@@ -0,0 +1 @@
+../emoji_aliases.py \ No newline at end of file
diff --git a/weechat/python/autoload/grep.py b/weechat/python/autoload/grep.py
new file mode 120000
index 0000000..87d4ca7
--- /dev/null
+++ b/weechat/python/autoload/grep.py
@@ -0,0 +1 @@
+../grep.py \ No newline at end of file
diff --git a/weechat/python/autoload/histman.py b/weechat/python/autoload/histman.py
new file mode 120000
index 0000000..ef8c6bf
--- /dev/null
+++ b/weechat/python/autoload/histman.py
@@ -0,0 +1 @@
+../histman.py \ No newline at end of file
diff --git a/weechat/python/autoload/irssi_awaylog.py b/weechat/python/autoload/irssi_awaylog.py
new file mode 120000
index 0000000..3f68867
--- /dev/null
+++ b/weechat/python/autoload/irssi_awaylog.py
@@ -0,0 +1 @@
+../irssi_awaylog.py \ No newline at end of file
diff --git a/weechat/python/autoload/otr.py b/weechat/python/autoload/otr.py
new file mode 120000
index 0000000..61d1b18
--- /dev/null
+++ b/weechat/python/autoload/otr.py
@@ -0,0 +1 @@
+../otr.py \ No newline at end of file
diff --git a/weechat/python/autoload/pyrnotify.py b/weechat/python/autoload/pyrnotify.py
new file mode 120000
index 0000000..2cfbc04
--- /dev/null
+++ b/weechat/python/autoload/pyrnotify.py
@@ -0,0 +1 @@
+../pyrnotify.py \ No newline at end of file
diff --git a/weechat/python/autoload/queryman.py b/weechat/python/autoload/queryman.py
new file mode 120000
index 0000000..a98d92b
--- /dev/null
+++ b/weechat/python/autoload/queryman.py
@@ -0,0 +1 @@
+../queryman.py \ No newline at end of file
diff --git a/weechat/python/autoload/sshnotify.py b/weechat/python/autoload/sshnotify.py
new file mode 120000
index 0000000..ae69e64
--- /dev/null
+++ b/weechat/python/autoload/sshnotify.py
@@ -0,0 +1 @@
+../sshnotify.py \ No newline at end of file
diff --git a/weechat/python/autoload/wee_slack.py b/weechat/python/autoload/wee_slack.py
new file mode 100644
index 0000000..5f4833a
--- /dev/null
+++ b/weechat/python/autoload/wee_slack.py
@@ -0,0 +1,2557 @@
+# -*- coding: utf-8 -*-
+#
+
+from functools import wraps
+
+import time
+import json
+import os
+import pickle
+import sha
+import re
+import urllib
+import HTMLParser
+import sys
+import traceback
+import collections
+import ssl
+
+from websocket import create_connection, WebSocketConnectionClosedException
+
+# hack to make tests possible.. better way?
+try:
+ import weechat as w
+except:
+ pass
+
+SCRIPT_NAME = "slack_extension"
+SCRIPT_AUTHOR = "Ryan Huber <rhuber@gmail.com>"
+SCRIPT_VERSION = "0.99.9"
+SCRIPT_LICENSE = "MIT"
+SCRIPT_DESC = "Extends weechat for typing notification/search/etc on slack.com"
+
+BACKLOG_SIZE = 200
+SCROLLBACK_SIZE = 500
+
+CACHE_VERSION = "4"
+
+SLACK_API_TRANSLATOR = {
+ "channel": {
+ "history": "channels.history",
+ "join": "channels.join",
+ "leave": "channels.leave",
+ "mark": "channels.mark",
+ "info": "channels.info",
+ },
+ "im": {
+ "history": "im.history",
+ "join": "im.open",
+ "leave": "im.close",
+ "mark": "im.mark",
+ },
+ "group": {
+ "history": "groups.history",
+ "join": "channels.join",
+ "leave": "groups.leave",
+ "mark": "groups.mark",
+ }
+
+}
+
+NICK_GROUP_HERE = "0|Here"
+NICK_GROUP_AWAY = "1|Away"
+
+sslopt_ca_certs = {}
+if hasattr(ssl, "get_default_verify_paths") and callable(ssl.get_default_verify_paths):
+ ssl_defaults = ssl.get_default_verify_paths()
+ if ssl_defaults.cafile is not None:
+ sslopt_ca_certs = {'ca_certs': ssl_defaults.cafile}
+
+
+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:
+ file('/tmp/debug.log', 'a+').writelines(message + '\n')
+ if main_buffer:
+ w.prnt("", "slack: " + message)
+ else:
+ if slack_debug is not None:
+ w.prnt(slack_debug, message)
+
+
+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):
+ if name in self.hashtable:
+ return self.hashtable[name]
+ # this is a fallback to __eq__ if the item isn't in the hashtable already
+ if name in self:
+ self.update_hashtable()
+ return self[self.index(name)]
+
+ def append(self, item, aliases=[]):
+ super(SearchList, self).append(item)
+ self.update_hashtable(item)
+
+ def update_hashtable(self, item=None):
+ if item is not None:
+ try:
+ for alias in item.get_aliases():
+ if alias is not None:
+ self.hashtable[alias] = item
+ except AttributeError:
+ pass
+ else:
+ for child in self:
+ try:
+ for alias in child.get_aliases():
+ if alias is not None:
+ self.hashtable[alias] = child
+ except AttributeError:
+ pass
+
+ def find_by_class(self, class_name):
+ items = []
+ for child in self:
+ if child.__class__ == class_name:
+ items.append(child)
+ return items
+
+ def find_by_class_deep(self, class_name, attribute):
+ items = []
+ for child in self:
+ if child.__class__ == self.__class__:
+ items += child.find_by_class_deep(class_name, attribute)
+ else:
+ items += (eval('child.' + attribute).find_by_class(class_name))
+ return items
+
+
+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
+ self.team = None
+ self.domain = None
+ self.server_buffer_name = None
+ self.login_data = None
+ self.buffer = None
+ self.token = token
+ self.ws = None
+ self.ws_hook = None
+ self.users = SearchList()
+ self.bots = SearchList()
+ self.channels = SearchList()
+ self.connecting = False
+ self.connected = False
+ self.connection_attempt_time = 0
+ self.communication_counter = 0
+ self.message_buffer = {}
+ self.ping_hook = None
+ self.alias = None
+
+ self.identifier = None
+ self.connect_to_slack()
+
+ def __eq__(self, compare_str):
+ if compare_str == self.identifier or compare_str == self.token or compare_str == self.buffer:
+ return True
+ else:
+ return False
+
+ def __str__(self):
+ return "{}".format(self.identifier)
+
+ 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_bot(self, bot):
+ self.bots.append(bot)
+
+ def add_channel(self, channel):
+ self.channels.append(channel, channel.get_aliases())
+ channels.append(channel, channel.get_aliases())
+
+ def get_aliases(self):
+ aliases = filter(None, [self.identifier, self.token, self.buffer, self.alias])
+ return aliases
+
+ def find(self, name, attribute):
+ attribute = eval("self." + attribute)
+ return attribute.find(name)
+
+ def get_communication_id(self):
+ if self.communication_counter > 999:
+ self.communication_counter = 0
+ self.communication_counter += 1
+ return self.communication_counter
+
+ def send_to_websocket(self, data, expect_reply=True):
+ data["id"] = self.get_communication_id()
+ message = json.dumps(data)
+ try:
+ if expect_reply:
+ self.message_buffer[data["id"]] = data
+ self.ws.send(message)
+ dbg("Sent {}...".format(message[:100]))
+ except:
+ dbg("Unexpected error: {}\nSent: {}".format(sys.exc_info()[0], data))
+ self.connected = False
+
+ def ping(self):
+ request = {"type": "ping"}
+ self.send_to_websocket(request)
+
+ def should_connect(self):
+ """
+ If we haven't tried to connect OR we tried and never heard back and it
+ has been 125 seconds consider the attempt dead and try again
+ """
+ if self.connection_attempt_time == 0 or self.connection_attempt_time + 125 < int(time.time()):
+ return True
+ else:
+ return False
+
+ def connect_to_slack(self):
+ t = time.time()
+ # Double check that we haven't exceeded a long wait to connect and try again.
+ if self.connecting and self.should_connect():
+ self.connecting = False
+ if not self.connecting:
+ async_slack_api_request("slack.com", self.token, "rtm.start", {"ts": t})
+ self.connection_attempt_time = int(time.time())
+ self.connecting = True
+
+ def connected_to_slack(self, login_data):
+ if login_data["ok"]:
+ self.team = login_data["team"]["domain"]
+ self.domain = login_data["team"]["domain"] + ".slack.com"
+ dbg("connected to {}".format(self.domain))
+ self.identifier = self.domain
+
+ alias = w.config_get_plugin("server_alias.{}".format(login_data["team"]["domain"]))
+ if alias:
+ self.server_buffer_name = alias
+ self.alias = alias
+ else:
+ self.server_buffer_name = self.domain
+
+ self.nick = login_data["self"]["name"]
+ self.create_local_buffer()
+
+ if self.create_slack_websocket(login_data):
+ if self.ping_hook:
+ w.unhook(self.ping_hook)
+ self.communication_counter = 0
+ self.ping_hook = w.hook_timer(1000 * 5, 0, 0, "slack_ping_cb", self.domain)
+ if len(self.users) == 0 or len(self.channels) == 0:
+ self.create_slack_mappings(login_data)
+
+ self.connected = True
+ self.connecting = False
+
+ self.print_connection_info(login_data)
+ if len(self.message_buffer) > 0:
+ for message_id in self.message_buffer.keys():
+ if self.message_buffer[message_id]["type"] != 'ping':
+ resend = self.message_buffer.pop(message_id)
+ dbg("Resent failed message.")
+ self.send_to_websocket(resend)
+ # sleep to prevent being disconnected by websocket server
+ time.sleep(1)
+ else:
+ self.message_buffer.pop(message_id)
+ return True
+ else:
+ token_start = self.token[:10]
+ error = """
+!! slack.com login error: {}
+ The problematic token starts with {}
+ Please check your API token with
+ "/set plugins.var.python.slack_extension.slack_api_token (token)"
+
+""".format(login_data["error"], token_start)
+ w.prnt("", error)
+ self.connected = False
+
+ def print_connection_info(self, login_data):
+ self.buffer_prnt('Connected to Slack', backlog=True)
+ self.buffer_prnt('{:<20} {}'.format("Websocket URL", login_data["url"]), backlog=True)
+ self.buffer_prnt('{:<20} {}'.format("User name", login_data["self"]["name"]), backlog=True)
+ self.buffer_prnt('{:<20} {}'.format("User ID", login_data["self"]["id"]), backlog=True)
+ self.buffer_prnt('{:<20} {}'.format("Team name", login_data["team"]["name"]), backlog=True)
+ self.buffer_prnt('{:<20} {}'.format("Team domain", login_data["team"]["domain"]), backlog=True)
+ self.buffer_prnt('{:<20} {}'.format("Team id", login_data["team"]["id"]), backlog=True)
+
+ def create_local_buffer(self):
+ if not w.buffer_search("", self.server_buffer_name):
+ self.buffer = w.buffer_new(self.server_buffer_name, "buffer_input_cb", "", "", "")
+ if w.config_string(w.config_get('irc.look.server_buffer')) == 'merge_with_core':
+ w.buffer_merge(self.buffer, w.buffer_search_main())
+ w.buffer_set(self.buffer, "nicklist", "1")
+
+ def create_slack_websocket(self, data):
+ web_socket_url = data['url']
+ try:
+ self.ws = create_connection(web_socket_url, sslopt=sslopt_ca_certs)
+ 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 Exception as e:
+ print("websocket connection error: {}".format(e))
+ return False
+
+ def create_slack_mappings(self, data):
+
+ for item in data["users"]:
+ self.add_user(User(self, item["name"], item["id"], item["presence"], item["deleted"], is_bot=item.get('is_bot', False)))
+
+ for item in data["bots"]:
+ self.add_bot(Bot(self, item["name"], item["id"], item["deleted"]))
+
+ for item in data["channels"]:
+ if "last_read" not in item:
+ item["last_read"] = 0
+ if "members" not in item:
+ item["members"] = []
+ if "topic" not in item:
+ item["topic"] = {}
+ item["topic"]["value"] = ""
+ if not item["is_archived"]:
+ 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"]:
+ if item["name"].startswith("mpdm-"):
+ self.add_channel(MpdmChannel(self, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"]))
+ else:
+ 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
+ if item["unread_count"] > 0:
+ item["is_open"] = True
+ name = self.users.find(item["user"]).name
+ self.add_channel(DmChannel(self, name, item["id"], item["is_open"], item["last_read"]))
+ for item in data['self']['prefs']['muted_channels'].split(','):
+ if item == '':
+ continue
+ if self.channels.find(item) is not None:
+ self.channels.find(item).muted = True
+
+ for item in self.channels:
+ item.get_history()
+
+ def buffer_prnt(self, message='no message', user="SYSTEM", backlog=False):
+ message = message.encode('ascii', 'ignore')
+ if backlog:
+ tags = "no_highlight,notify_none,logger_backlog_end"
+ else:
+ tags = ""
+ if user == "SYSTEM":
+ user = w.config_string(w.config_get('weechat.look.prefix_network'))
+ if self.buffer:
+ w.prnt_date_tags(self.buffer, 0, tags, "{}\t{}".format(user, message))
+ else:
+ pass
+ # w.prnt("", "%s\t%s" % (user, message))
+
+
+def buffer_input_cb(b, buffer, data):
+ channel = channels.find(buffer)
+ if not channel:
+ return w.WEECHAT_RC_OK_EAT
+ reaction = re.match("^\s*(\d*)(\+|-):(.*):\s*$", data)
+ if reaction:
+ if reaction.group(2) == "+":
+ channel.send_add_reaction(int(reaction.group(1) or 1), reaction.group(3))
+ elif reaction.group(2) == "-":
+ channel.send_remove_reaction(int(reaction.group(1) or 1), reaction.group(3))
+ elif data.startswith('s/'):
+ try:
+ old, new, flags = re.split(r'(?<!\\)/', data)[1:]
+ except ValueError:
+ pass
+ else:
+ # Replacement string in re.sub() is a string, not a regex, so get
+ # rid of escapes.
+ new = new.replace(r'\/', '/')
+ old = old.replace(r'\/', '/')
+ channel.change_previous_message(old.decode("utf-8"), new.decode("utf-8"), flags)
+ else:
+ channel.send_message(data)
+ # channel.buffer_prnt(channel.server.nick, data)
+ channel.mark_read(True)
+ return w.WEECHAT_RC_ERROR
+
+
+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=""):
+ self.name = prepend_name + name
+ self.current_short_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.typing = {}
+ self.last_received = None
+ self.messages = []
+ self.scrolling = False
+ self.last_active_user = None
+ self.muted = 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.server_buffer_name, self.name))
+ if channel_buffer:
+ self.channel_buffer = channel_buffer
+ else:
+ self.channel_buffer = w.buffer_new("{}.{}".format(self.server.server_buffer_name, self.name), "buffer_input_cb", self.name, "", "")
+ if self.type == "im":
+ w.buffer_set(self.channel_buffer, "localvar_set_type", 'private')
+ else:
+ w.buffer_set(self.channel_buffer, "localvar_set_type", 'channel')
+ if self.server.alias:
+ w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.alias)
+ else:
+ w.buffer_set(self.channel_buffer, "localvar_set_server", self.server.team)
+ w.buffer_set(self.channel_buffer, "localvar_set_channel", self.name)
+ w.buffer_set(self.channel_buffer, "short_name", self.name)
+ buffer_list_update_next()
+
+ def attach_buffer(self):
+ channel_buffer = w.buffer_search("", "{}.{}".format(self.server.server_buffer_name, self.name))
+ if channel_buffer != main_weechat_buffer:
+ self.channel_buffer = channel_buffer
+ w.buffer_set(self.channel_buffer, "localvar_set_nick", self.server.nick)
+ w.buffer_set(self.channel_buffer, "highlight_words", self.server.nick)
+ else:
+ self.channel_buffer = None
+ channels.update_hashtable()
+ self.server.channels.update_hashtable()
+
+ def detach_buffer(self):
+ if self.channel_buffer is not None:
+ w.buffer_close(self.channel_buffer)
+ self.channel_buffer = None
+ channels.update_hashtable()
+ self.server.channels.update_hashtable()
+
+ def update_nicklist(self, user=None):
+ if not self.channel_buffer:
+ return
+
+ w.buffer_set(self.channel_buffer, "nicklist", "1")
+
+ # create nicklists for the current channel if they don't exist
+ # if they do, use the existing pointer
+ here = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_HERE)
+ if not here:
+ here = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_HERE, "weechat.color.nicklist_group", 1)
+ afk = w.nicklist_search_group(self.channel_buffer, '', NICK_GROUP_AWAY)
+ if not afk:
+ afk = w.nicklist_add_group(self.channel_buffer, '', NICK_GROUP_AWAY, "weechat.color.nicklist_group", 1)
+
+ if user:
+ user = self.members_table[user]
+ nick = w.nicklist_search_nick(self.channel_buffer, "", user.name)
+ # since this is a change just remove it regardless of where it is
+ w.nicklist_remove_nick(self.channel_buffer, nick)
+ # now add it back in to whichever..
+ if user.presence == 'away':
+ w.nicklist_add_nick(self.channel_buffer, afk, user.name, user.color_name, "", "", 1)
+ else:
+ w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
+
+ # if we didn't get a user, build a complete list. this is expensive.
+ else:
+ try:
+ for user in self.members:
+ user = self.members_table[user]
+ if user.deleted:
+ continue
+ if user.presence == 'away':
+ w.nicklist_add_nick(self.channel_buffer, afk, user.name, user.color_name, "", "", 1)
+ else:
+ w.nicklist_add_nick(self.channel_buffer, here, user.name, user.color_name, "", "", 1)
+ except Exception as e:
+ dbg("DEBUG: {} {} {}".format(self.identifier, self.name, e))
+
+ def fullname(self):
+ return "{}.{}".format(self.server.server_buffer_name, self.name)
+
+ def has_user(self, name):
+ return name in self.members
+
+ 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):
+ self.active = True
+
+ def set_inactive(self):
+ self.active = False
+
+ def set_typing(self, user):
+ if self.channel_buffer:
+ if w.buffer_get_integer(self.channel_buffer, "hidden") == 0:
+ self.typing[user] = time.time()
+ buffer_list_update_next()
+
+ def unset_typing(self, user):
+ if self.channel_buffer:
+ if w.buffer_get_integer(self.channel_buffer, "hidden") == 0:
+ try:
+ del self.typing[user]
+ buffer_list_update_next()
+ except:
+ pass
+
+ def send_message(self, message):
+ message = self.linkify_text(message)
+ dbg(message)
+ request = {"type": "message", "channel": self.identifier, "text": message, "_server": self.server.domain}
+ self.server.send_to_websocket(request)
+
+ def linkify_text(self, message):
+ message = message.split(' ')
+ for item in enumerate(message):
+ targets = re.match('.*([@#])([\w.]+\w)(\W*)', item[1])
+ if targets and targets.groups()[0] == '@':
+ named = targets.groups()
+ if named[1] in ["group", "channel", "here"]:
+ message[item[0]] = "<!{}>".format(named[1])
+ if self.server.users.find(named[1]):
+ message[item[0]] = "<@{}>{}".format(self.server.users.find(named[1]).identifier, named[2])
+ if targets and targets.groups()[0] == '#':
+ named = targets.groups()
+ if self.server.channels.find(named[1]):
+ message[item[0]] = "<#{}|{}>{}".format(self.server.channels.find(named[1]).identifier, named[1], named[2])
+ dbg(message)
+ return " ".join(message)
+
+ def set_topic(self, topic):
+ self.topic = topic.encode('utf-8')
+ w.buffer_set(self.channel_buffer, "title", self.topic)
+
+ def open(self, update_remote=True):
+ self.create_buffer()
+ self.active = True
+ self.get_history()
+ if "info" in SLACK_API_TRANSLATOR[self.type]:
+ async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["info"], {"name": self.name.lstrip("#")})
+ if update_remote:
+ if "join" in SLACK_API_TRANSLATOR[self.type]:
+ async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["join"], {"name": self.name.lstrip("#")})
+
+ def close(self, update_remote=True):
+ # remove from cache so messages don't reappear when reconnecting
+ if self.active:
+ self.active = False
+ self.current_short_name = ""
+ self.detach_buffer()
+ if update_remote:
+ async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["leave"], {"channel": self.identifier})
+
+ def closed(self):
+ self.channel_buffer = None
+ self.last_received = None
+ self.close()
+
+ def is_someone_typing(self):
+ for user in self.typing.keys():
+ if self.typing[user] + 4 > time.time():
+ return True
+ if len(self.typing) > 0:
+ self.typing = {}
+ buffer_list_update_next()
+ return False
+
+ def get_typing_list(self):
+ typing = []
+ for user in self.typing.keys():
+ if self.typing[user] + 4 > time.time():
+ typing.append(user)
+ return typing
+
+ def mark_read(self, update_remote=True):
+ if self.channel_buffer:
+ w.buffer_set(self.channel_buffer, "unread", "")
+ if update_remote:
+ self.last_read = time.time()
+ self.update_read_marker(self.last_read)
+
+ def update_read_marker(self, time):
+ async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["mark"], {"channel": self.identifier, "ts": time})
+
+ def rename(self):
+ if self.is_someone_typing():
+ new_name = ">{}".format(self.name[1:])
+ else:
+ new_name = self.name
+ if self.channel_buffer:
+ if self.current_short_name != new_name:
+ self.current_short_name = new_name
+ w.buffer_set(self.channel_buffer, "short_name", new_name)
+
+ 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)
+ tags = "nick_" + user
+ user_obj = self.server.users.find(user)
+ # XXX: we should not set log1 for robots.
+ if time_float != 0 and self.last_read >= time_float:
+ tags += ",no_highlight,notify_none,logger_backlog_end"
+ set_read_marker = True
+ elif message.find(self.server.nick.encode('utf-8')) > -1:
+ tags += ",notify_highlight,log1"
+ elif user != self.server.nick and self.name in self.server.users:
+ tags += ",notify_private,notify_message,log1,irc_privmsg"
+ elif self.muted:
+ tags += ",no_highlight,notify_none,logger_backlog_end"
+ elif user in [x.strip() for x in w.prefix("join"), w.prefix("quit")]:
+ tags += ",irc_smart_filter"
+ else:
+ tags += ",notify_message,log1,irc_privmsg"
+ # don't write these to local log files
+ # tags += ",no_log"
+ time_int = int(time_float)
+ if self.channel_buffer:
+ prefix_same_nick = w.config_string(w.config_get('weechat.look.prefix_same_nick'))
+ if user == self.last_active_user and prefix_same_nick != "":
+ if colorize_nicks and user_obj:
+ name = user_obj.color + prefix_same_nick
+ else:
+ name = prefix_same_nick
+ else:
+ nick_prefix = w.config_string(w.config_get('weechat.look.nick_prefix'))
+ nick_prefix_color_name = w.config_string(w.config_get('weechat.color.chat_nick_prefix'))
+ nick_prefix_color = w.color(nick_prefix_color_name)
+
+ nick_suffix = w.config_string(w.config_get('weechat.look.nick_suffix'))
+ nick_suffix_color_name = w.config_string(w.config_get('weechat.color.chat_nick_prefix'))
+ nick_suffix_color = w.color(nick_suffix_color_name)
+
+ if user_obj:
+ name = user_obj.formatted_name()
+ self.last_active_user = user
+ # XXX: handle bots properly here.
+ else:
+ name = user
+ self.last_active_user = None
+ name = nick_prefix_color + nick_prefix + w.color("reset") + name + nick_suffix_color + nick_suffix + w.color("reset")
+ name = name.decode('utf-8')
+ # colorize nicks in each line
+ chat_color = w.config_string(w.config_get('weechat.color.chat'))
+ if type(message) is not unicode:
+ message = message.decode('UTF-8', 'replace')
+ curr_color = w.color(chat_color)
+ if colorize_nicks and colorize_messages and user_obj:
+ curr_color = user_obj.color
+ message = curr_color + message
+ for user in self.server.users:
+ if user.name in message:
+ message = user.name_regex.sub(
+ r'\1\2{}\3'.format(user.formatted_name() + curr_color),
+ message)
+
+ message = HTMLParser.HTMLParser().unescape(message)
+ data = u"{}\t{}".format(name, message).encode('utf-8')
+ w.prnt_date_tags(self.channel_buffer, time_int, tags, data)
+
+ if set_read_marker:
+ self.mark_read(False)
+ else:
+ self.open(False)
+ 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
+
+ def has_message(self, ts):
+ return self.messages.count(ts) > 0
+
+ def change_message(self, ts, text=None, suffix=''):
+ if self.has_message(ts):
+ message_index = self.messages.index(ts)
+
+ if text is not None:
+ self.messages[message_index].change_text(text)
+ text = render_message(self.messages[message_index].message_json, True)
+
+ # if there is only one message with this timestamp, modify it directly.
+ # we do this because time resolution in weechat is less than slack
+ int_time = int(float(ts))
+ if self.messages.count(str(int_time)) == 1:
+ modify_buffer_line(self.channel_buffer, text + suffix, int_time)
+ # otherwise redraw the whole buffer, which is expensive
+ else:
+ self.buffer_redraw()
+ return True
+
+ def add_reaction(self, ts, reaction, user):
+ if self.has_message(ts):
+ message_index = self.messages.index(ts)
+ self.messages[message_index].add_reaction(reaction, user)
+ self.change_message(ts)
+ return True
+
+ def remove_reaction(self, ts, reaction, user):
+ if self.has_message(ts):
+ message_index = self.messages.index(ts)
+ self.messages[message_index].remove_reaction(reaction, user)
+ self.change_message(ts)
+ return True
+
+ def send_add_reaction(self, msg_number, reaction):
+ self.send_change_reaction("reactions.add", msg_number, reaction)
+
+ def send_remove_reaction(self, msg_number, reaction):
+ self.send_change_reaction("reactions.remove", msg_number, reaction)
+
+ def send_change_reaction(self, method, msg_number, reaction):
+ if 0 < msg_number < len(self.messages):
+ timestamp = self.messages[-msg_number].message_json["ts"]
+ data = {"channel": self.identifier, "timestamp": timestamp, "name": reaction}
+ async_slack_api_request(self.server.domain, self.server.token, method, data)
+
+ def change_previous_message(self, old, new, flags):
+ message = self.my_last_message()
+ if new == "" and old == "":
+ async_slack_api_request(self.server.domain, self.server.token, 'chat.delete', {"channel": self.identifier, "ts": message['ts']})
+ else:
+ num_replace = 1
+ if 'g' in flags:
+ num_replace = 0
+ new_message = re.sub(old, new, message["text"], num_replace)
+ if new_message != message["text"]:
+ async_slack_api_request(self.server.domain, self.server.token, 'chat.update', {"channel": self.identifier, "ts": message['ts'], "text": new_message.encode("utf-8")})
+
+ def my_last_message(self):
+ 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:
+ for message in message_cache[self.identifier]:
+ process_message(json.loads(message), True)
+ if self.last_received is not 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:
+ async_slack_api_request(self.server.domain, self.server.token, SLACK_API_TRANSLATOR[self.type]["history"], {"channel": self.identifier, "count": BACKLOG_SIZE})
+
+
+class GroupChannel(Channel):
+
+ def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic=""):
+ super(GroupChannel, self).__init__(server, name, identifier, active, last_read, prepend_name, members, topic)
+ self.type = "group"
+
+
+class MpdmChannel(Channel):
+
+ def __init__(self, server, name, identifier, active, last_read=0, prepend_name="", members=[], topic=""):
+ name = "|".join("-".join(name.split("-")[1:-1]).split("--"))
+ super(MpdmChannel, self).__init__(server, name, identifier, active, last_read, prepend_name, members, topic)
+ self.type = "group"
+
+
+class DmChannel(Channel):
+
+ def __init__(self, server, name, identifier, active, last_read=0, prepend_name=""):
+ super(DmChannel, self).__init__(server, name, identifier, active, last_read, prepend_name)
+ self.type = "im"
+
+ def rename(self):
+ global colorize_private_chats
+
+ if self.server.users.find(self.name).presence == "active":
+ new_name = self.server.users.find(self.name).formatted_name('+', colorize_private_chats)
+ else:
+ new_name = self.server.users.find(self.name).formatted_name(' ', colorize_private_chats)
+
+ if self.channel_buffer:
+ if self.current_short_name != new_name:
+ self.current_short_name = new_name
+ w.buffer_set(self.channel_buffer, "short_name", new_name)
+
+ def update_nicklist(self, user=None):
+ pass
+
+
+class User(object):
+
+ def __init__(self, server, name, identifier, presence="away", deleted=False, is_bot=False):
+ self.server = server
+ self.name = name
+ self.identifier = identifier
+ self.deleted = deleted
+ self.presence = presence
+
+ 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)")
+ self.is_bot = is_bot
+
+ if deleted:
+ return
+ self.nicklist_pointer = w.nicklist_add_nick(server.buffer, "", self.name, self.color_name, "", "", 1)
+ if self.presence == 'away':
+ w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0")
+ else:
+ w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "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):
+ try:
+ if compare_str == self.name or compare_str == self.identifier:
+ return True
+ elif compare_str[0] == '@' and compare_str[1:] == self.name:
+ return True
+ else:
+ return False
+ except:
+ return False
+
+ def get_aliases(self):
+ return [self.name, "@" + self.name, self.identifier]
+
+ def set_active(self):
+ if self.deleted:
+ return
+
+ self.presence = "active"
+ for channel in self.server.channels:
+ if channel.has_user(self.identifier):
+ channel.update_nicklist(self.identifier)
+ w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "1")
+ dm_channel = self.server.channels.find(self.name)
+ if dm_channel and dm_channel.active:
+ buffer_list_update_next()
+
+ def set_inactive(self):
+ if self.deleted:
+ return
+
+ self.presence = "away"
+ for channel in self.server.channels:
+ if channel.has_user(self.identifier):
+ channel.update_nicklist(self.identifier)
+ w.nicklist_nick_set(self.server.buffer, self.nicklist_pointer, "visible", "0")
+ dm_channel = self.server.channels.find(self.name)
+ if dm_channel and dm_channel.active:
+ buffer_list_update_next()
+
+ def update_color(self):
+ if colorize_nicks:
+ if self.name == self.server.nick:
+ self.color_name = w.config_string(w.config_get('weechat.color.chat_nick_self'))
+ else:
+ self.color_name = w.info_get('irc_nick_color_name', self.name)
+ self.color = w.color(self.color_name)
+ else:
+ self.color = ""
+ self.color_name = ""
+
+ def formatted_name(self, prepend="", enable_color=True):
+ if colorize_nicks and enable_color:
+ print_color = self.color
+ else:
+ print_color = ""
+ return print_color + prepend + self.name
+
+ def create_dm_channel(self):
+ async_slack_api_request(self.server.domain, self.server.token, "im.open", {"user": self.identifier})
+
+
+class Bot(object):
+
+ def __init__(self, server, name, identifier, deleted=False):
+ self.server = server
+ self.name = name
+ self.identifier = identifier
+ self.deleted = deleted
+ self.update_color()
+
+ def __eq__(self, compare_str):
+ if compare_str == self.identifier or compare_str == self.name:
+ return True
+ else:
+ return False
+
+ def __str__(self):
+ return "{}".format(self.identifier)
+
+ def __repr__(self):
+ return "{}".format(self.identifier)
+
+ def update_color(self):
+ if colorize_nicks:
+ self.color_name = w.info_get('irc_nick_color_name', self.name.encode('utf-8'))
+ self.color = w.color(self.color_name)
+ else:
+ self.color_name = ""
+ self.color = ""
+
+ def formatted_name(self, prepend="", enable_color=True):
+ if colorize_nicks and enable_color:
+ print_color = self.color
+ else:
+ print_color = ""
+ return print_color + prepend + self.name
+
+
+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):
+ if not isinstance(new_text, unicode):
+ new_text = unicode(new_text, 'utf-8')
+ self.message_json["text"] = new_text
+
+ def add_reaction(self, reaction, user):
+ if "reactions" in self.message_json:
+ found = False
+ for r in self.message_json["reactions"]:
+ if r["name"] == reaction and user not in r["users"]:
+ r["users"].append(user)
+ found = True
+
+ if not found:
+ self.message_json["reactions"].append({u"name": reaction, u"users": [user]})
+ else:
+ self.message_json["reactions"] = [{u"name": reaction, u"users": [user]}]
+
+ def remove_reaction(self, reaction, user):
+ if "reactions" in self.message_json:
+ for r in self.message_json["reactions"]:
+ if r["name"] == reaction and user in r["users"]:
+ r["users"].remove(user)
+ 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_buffer_or_ignore(f):
+ """
+ Only run this function if we're in a slack buffer, else ignore
+ """
+ @wraps(f)
+ def wrapper(current_buffer, *args, **kwargs):
+ server = servers.find(current_domain_name())
+ if not server:
+ return w.WEECHAT_RC_OK
+ return f(current_buffer, *args, **kwargs)
+ return wrapper
+
+
+def slack_command_cb(data, current_buffer, args):
+ a = args.split(' ', 1)
+ if len(a) > 1:
+ function_name, args = a[0], " ".join(a[1:])
+ else:
+ function_name, args = a[0], None
+
+ try:
+ cmds[function_name](current_buffer, args)
+ except KeyError:
+ w.prnt("", "Command not found: " + function_name)
+ return w.WEECHAT_RC_OK
+
+
+@slack_buffer_or_ignore
+def me_command_cb(data, current_buffer, args):
+ if channels.find(current_buffer):
+ # channel = channels.find(current_buffer)
+ # nick = channel.server.nick
+ message = "_{}_".format(args)
+ buffer_input_cb("", current_buffer, message)
+ return w.WEECHAT_RC_OK
+
+
+@slack_buffer_or_ignore
+def join_command_cb(data, current_buffer, args):
+ args = args.split()
+ if len(args) < 2:
+ w.prnt(current_buffer, "Missing channel argument")
+ return w.WEECHAT_RC_OK_EAT
+ elif command_talk(current_buffer, args[1]):
+ return w.WEECHAT_RC_OK_EAT
+ else:
+ return w.WEECHAT_RC_OK
+
+
+@slack_buffer_or_ignore
+def part_command_cb(data, current_buffer, args):
+ if channels.find(current_buffer) or servers.find(current_buffer):
+ args = args.split()
+ if len(args) > 1:
+ channel = args[1:]
+ servers.find(current_domain_name()).channels.find(channel).close(True)
+ else:
+ channels.find(current_buffer).close(True)
+ return w.WEECHAT_RC_OK_EAT
+ else:
+ return w.WEECHAT_RC_OK
+
+
+# Wrap command_ functions that require they be performed in a slack buffer
+def slack_buffer_required(f):
+ @wraps(f)
+ def wrapper(current_buffer, *args, **kwargs):
+ server = servers.find(current_domain_name())
+ if not server:
+ w.prnt(current_buffer, "This command must be used in a slack buffer")
+ return w.WEECHAT_RC_ERROR
+ return f(current_buffer, *args, **kwargs)
+ return wrapper
+
+
+def command_register(current_buffer, args):
+ CLIENT_ID = "2468770254.51917335286"
+ CLIENT_SECRET = "dcb7fe380a000cba0cca3169a5fe8d70" # this is not really a secret
+ if not args:
+ message = """
+# ### Retrieving a Slack token via OAUTH ####
+
+1) Paste this into a browser: https://slack.com/oauth/authorize?client_id=2468770254.51917335286&scope=client
+2) Select the team you wish to access from wee-slack in your browser.
+3) Click "Authorize" in the browser **IMPORTANT: the redirect will fail, this is expected**
+4) Copy the "code" portion of the URL to your clipboard
+5) Return to weechat and run `/slack register [code]`
+6) Add the returned token per the normal wee-slack setup instructions
+
+
+"""
+ w.prnt(current_buffer, message)
+ else:
+ aargs = args.split(None, 2)
+ if len(aargs) != 1:
+ w.prnt(current_buffer, "ERROR: invalid args to register")
+ else:
+ # w.prnt(current_buffer, "https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0]))
+ ret = urllib.urlopen("https://slack.com/api/oauth.access?client_id={}&client_secret={}&code={}".format(CLIENT_ID, CLIENT_SECRET, aargs[0])).read()
+ d = json.loads(ret)
+ if d["ok"] == True:
+ w.prnt(current_buffer, "Success! Access token is: " + d['access_token'])
+ else:
+ w.prnt(current_buffer, "Failed! Error is: " + d['error'])
+
+
+@slack_buffer_or_ignore
+def msg_command_cb(data, current_buffer, args):
+ dbg("msg_command_cb")
+ aargs = args.split(None, 2)
+ who = aargs[1]
+
+ command_talk(current_buffer, who)
+
+ if len(aargs) > 2:
+ message = aargs[2]
+ server = servers.find(current_domain_name())
+ if server:
+ channel = server.channels.find(who)
+ channel.send_message(message)
+ return w.WEECHAT_RC_OK_EAT
+
+
+@slack_buffer_required
+def command_upload(current_buffer, args):
+ """
+ Uploads a file to the current buffer
+ /slack upload [file_path]
+ """
+ post_data = {}
+ channel = current_buffer_name(short=True)
+ domain = current_domain_name()
+ token = servers.find(domain).token
+
+ if servers.find(domain).channels.find(channel):
+ channel_identifier = servers.find(domain).channels.find(channel).identifier
+
+ if channel_identifier:
+ post_data["token"] = token
+ post_data["channels"] = channel_identifier
+ post_data["file"] = args
+ async_slack_api_upload_request(token, "files.upload", post_data)
+
+
+def command_talk(current_buffer, args):
+ """
+ Open a chat with the specified user
+ /slack talk [user]
+ """
+
+ server = servers.find(current_domain_name())
+ if server:
+ channel = server.channels.find(args)
+ if channel is None:
+ user = server.users.find(args)
+ if user:
+ user.create_dm_channel()
+ else:
+ server.buffer_prnt("User or channel {} not found.".format(args))
+ else:
+ channel.open()
+ if w.config_get_plugin('switch_buffer_on_join') != '0':
+ w.buffer_set(channel.channel_buffer, "display", "1")
+ return True
+ else:
+ return False
+
+
+def command_join(current_buffer, args):
+ """
+ Join the specified channel
+ /slack join [channel]
+ """
+ domain = current_domain_name()
+ if domain == "":
+ if len(servers) == 1:
+ domain = servers[0]
+ else:
+ w.prnt(current_buffer, "You are connected to multiple Slack instances, please execute /join from a server buffer. i.e. (domain).slack.com")
+ return
+ channel = servers.find(domain).channels.find(args)
+ if channel is not None:
+ servers.find(domain).channels.find(args).open()
+ else:
+ w.prnt(current_buffer, "Channel not found.")
+
+
+@slack_buffer_required
+def command_channels(current_buffer, args):
+ """
+ List all the channels for the slack instance (name, id, active)
+ /slack channels
+ """
+ server = servers.find(current_domain_name())
+ for channel in server.channels:
+ line = "{:<25} {} {}".format(channel.name, channel.identifier, channel.active)
+ server.buffer_prnt(line)
+
+
+def command_nodistractions(current_buffer, args):
+ global hide_distractions
+ hide_distractions = not hide_distractions
+ if distracting_channels != ['']:
+ for channel in distracting_channels:
+ try:
+ 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 {} .. removing..".format(channel), main_buffer=True)
+ distracting_channels.pop(distracting_channels.index(channel))
+ save_distracting_channels()
+
+
+def command_distracting(current_buffer, args):
+ global distracting_channels
+ distracting_channels = [x.strip() for x in w.config_get_plugin("distracting_channels").split(',')]
+ if channels.find(current_buffer) is None:
+ w.prnt(current_buffer, "This command must be used in a channel buffer")
+ return
+ fullname = channels.find(current_buffer).fullname()
+ if distracting_channels.count(fullname) == 0:
+ distracting_channels.append(fullname)
+ else:
+ distracting_channels.pop(distracting_channels.index(fullname))
+ save_distracting_channels()
+
+
+def save_distracting_channels():
+ new = ','.join(distracting_channels)
+ w.config_set_plugin('distracting_channels', new)
+
+
+@slack_buffer_required
+def command_users(current_buffer, args):
+ """
+ List all the users for the slack instance (name, id, away)
+ /slack users
+ """
+ server = servers.find(current_domain_name())
+ for user in server.users:
+ line = "{:<40} {} {}".format(user.formatted_name(), user.identifier, user.presence)
+ server.buffer_prnt(line)
+
+
+def command_setallreadmarkers(current_buffer, args):
+ """
+ Sets the read marker for all channels
+ /slack setallreadmarkers
+ """
+ for channel in channels:
+ channel.mark_read()
+
+
+def command_changetoken(current_buffer, args):
+ w.config_set_plugin('slack_api_token', args)
+
+
+def command_test(current_buffer, args):
+ w.prnt(current_buffer, "worked!")
+
+
+@slack_buffer_required
+def command_away(current_buffer, args):
+ """
+ Sets your status as 'away'
+ /slack away
+ """
+ server = servers.find(current_domain_name())
+ async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "away"})
+
+
+@slack_buffer_required
+def command_back(current_buffer, args):
+ """
+ Sets your status as 'back'
+ /slack back
+ """
+ server = servers.find(current_domain_name())
+ async_slack_api_request(server.domain, server.token, 'presence.set', {"presence": "active"})
+
+
+@slack_buffer_required
+def command_markread(current_buffer, args):
+ """
+ Marks current channel as read
+ /slack markread
+ """
+ # refactor this - one liner i think
+ channel = current_buffer_name(short=True)
+ domain = current_domain_name()
+ if servers.find(domain).channels.find(channel):
+ servers.find(domain).channels.find(channel).mark_read()
+
+
+@slack_buffer_required
+def command_slash(current_buffer, args):
+ """
+ Support for custom slack commands
+ /slack slash /customcommand arg1 arg2 arg3
+ """
+
+ server = servers.find(current_domain_name())
+ channel = current_buffer_name(short=True)
+ domain = current_domain_name()
+
+ if args is None:
+ server.buffer_prnt("Usage: /slack slash /someslashcommand [arguments...].")
+ return
+
+ split_args = args.split(None, 1)
+
+ command = split_args[0]
+ text = split_args[1] if len(split_args) > 1 else ""
+
+ if servers.find(domain).channels.find(channel):
+ channel_identifier = servers.find(domain).channels.find(channel).identifier
+
+ if channel_identifier:
+ async_slack_api_request(server.domain, server.token, 'chat.command', {'command': command, 'text': text, 'channel': channel_identifier})
+ else:
+ server.buffer_prnt("User or channel not found.")
+
+
+def command_flushcache(current_buffer, args):
+ global message_cache
+ message_cache = collections.defaultdict(list)
+ cache_write_cb("", "")
+
+
+def command_cachenow(current_buffer, args):
+ cache_write_cb("", "")
+
+
+def command_neveraway(current_buffer, args):
+ global never_away
+ if never_away:
+ never_away = False
+ dbg("unset never_away", main_buffer=True)
+ else:
+ never_away = True
+ dbg("set never_away", main_buffer=True)
+
+
+def command_printvar(current_buffer, args):
+ w.prnt("", "{}".format(eval(args)))
+
+
+def command_p(current_buffer, args):
+ w.prnt("", "{}".format(eval(args)))
+
+
+def command_debug(current_buffer, args):
+ create_slack_debug_buffer()
+
+
+def command_debugstring(current_buffer, args):
+ global debug_string
+ if args == '':
+ debug_string = None
+ else:
+ debug_string = args
+
+
+def command_search(current_buffer, args):
+ pass
+# if not slack_buffer:
+# create_slack_buffer()
+# w.buffer_set(slack_buffer, "display", "1")
+# query = args
+# w.prnt(slack_buffer,"\nSearched for: %s\n\n" % (query))
+# reply = slack_api_request('search.messages', {"query":query}).read()
+# data = json.loads(reply)
+# for message in data['messages']['matches']:
+# message["text"] = message["text"].encode('ascii', 'ignore')
+# formatted_message = "%s / %s:\t%s" % (message["channel"]["name"], message['username'], message['text'])
+# w.prnt(slack_buffer,str(formatted_message))
+
+
+def command_nick(current_buffer, args):
+ pass
+# urllib.urlopen("https://%s/account/settings" % (domain))
+# browser.select_form(nr=0)
+# browser.form['username'] = args
+# reply = browser.submit()
+
+
+def command_help(current_buffer, args):
+ help_cmds = {k[8:]: v.__doc__ for k, v in globals().items() if k.startswith("command_")}
+
+ if args:
+ try:
+ help_cmds = {args: help_cmds[args]}
+ except KeyError:
+ w.prnt("", "Command not found: " + args)
+ return
+
+ for cmd, helptext in help_cmds.items():
+ w.prnt('', w.color("bold") + cmd)
+ w.prnt('', (helptext or 'No help text').strip())
+ w.prnt('', '')
+
+# Websocket handling methods
+
+
+def command_openweb(current_buffer, args):
+ trigger = w.config_get_plugin('trigger_value')
+ if trigger != "0":
+ if args is None:
+ channel = channels.find(current_buffer)
+ url = "{}/messages/{}".format(channel.server.server_buffer_name, channel.name)
+ topic = w.buffer_get_string(channel.channel_buffer, "title")
+ w.buffer_set(channel.channel_buffer, "title", "{}:{}".format(trigger, url))
+ w.hook_timer(1000, 0, 1, "command_openweb", json.dumps({"topic": topic, "buffer": current_buffer}))
+ else:
+ # TODO: fix this dirty hack because i don't know the right way to send multiple args.
+ args = current_buffer
+ data = json.loads(args)
+ channel_buffer = channels.find(data["buffer"]).channel_buffer
+ w.buffer_set(channel_buffer, "title", data["topic"])
+ return w.WEECHAT_RC_OK
+
+
+@slack_buffer_or_ignore
+def topic_command_cb(data, current_buffer, args):
+ n = len(args.split())
+ if n < 2:
+ channel = channels.find(current_buffer)
+ if channel:
+ w.prnt(current_buffer, 'Topic for {} is "{}"'.format(channel.name, channel.topic))
+ return w.WEECHAT_RC_OK_EAT
+ elif command_topic(current_buffer, args.split(None, 1)[1]):
+ return w.WEECHAT_RC_OK_EAT
+ else:
+ return w.WEECHAT_RC_ERROR
+
+
+def command_topic(current_buffer, args):
+ """
+ Change the topic of a channel
+ /slack topic [<channel>] [<topic>|-delete]
+ """
+ server = servers.find(current_domain_name())
+ if server:
+ arrrrgs = args.split(None, 1)
+ if arrrrgs[0].startswith('#'):
+ channel = server.channels.find(arrrrgs[0])
+ topic = arrrrgs[1]
+ else:
+ channel = server.channels.find(current_buffer)
+ topic = args
+
+ if channel:
+ if topic == "-delete":
+ async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": ""})
+ else:
+ async_slack_api_request(server.domain, server.token, 'channels.setTopic', {"channel": channel.identifier, "topic": topic})
+ return True
+ else:
+ return False
+ else:
+ return False
+
+
+def slack_websocket_cb(server, fd):
+ try:
+ data = servers.find(server).ws.recv()
+ message_json = json.loads(data)
+ # this magic attaches json that helps find the right dest
+ message_json['_server'] = server
+ except WebSocketConnectionClosedException:
+ servers.find(server).ws.close()
+ return w.WEECHAT_RC_OK
+ except Exception:
+ dbg("socket issue: {}\n".format(traceback.format_exc()))
+ return w.WEECHAT_RC_OK
+ # dispatch here
+ if "reply_to" in message_json:
+ function_name = "reply"
+ elif "type" in message_json:
+ function_name = message_json["type"]
+ else:
+ function_name = "unknown"
+ try:
+ proc[function_name](message_json)
+ except KeyError:
+ if function_name:
+ dbg("Function not implemented: {}\n{}".format(function_name, message_json))
+ else:
+ dbg("Function not implemented\n{}".format(message_json))
+ w.bar_item_update("slack_typing_notice")
+ return w.WEECHAT_RC_OK
+
+
+def process_reply(message_json):
+ global unfurl_ignore_alt_text
+
+ server = servers.find(message_json["_server"])
+ identifier = message_json["reply_to"]
+ item = server.message_buffer.pop(identifier)
+ if 'text' in item and type(item['text']) is not unicode:
+ item['text'] = item['text'].decode('UTF-8', 'replace')
+ if "type" in item:
+ if item["type"] == "message" and "channel" in item.keys():
+ item["ts"] = message_json["ts"]
+ channels.find(item["channel"]).cache_message(item, from_me=True)
+ text = unfurl_refs(item["text"], ignore_alt_text=unfurl_ignore_alt_text)
+
+ channels.find(item["channel"]).buffer_prnt(item["user"], text, item["ts"])
+ dbg("REPLY {}".format(item))
+
+
+def process_pong(message_json):
+ pass
+
+
+def process_pref_change(message_json):
+ server = servers.find(message_json["_server"])
+ if message_json['name'] == u'muted_channels':
+ muted = message_json['value'].split(',')
+ for c in server.channels:
+ if c.identifier in muted:
+ c.muted = True
+ else:
+ c.muted = False
+ else:
+ dbg("Preference change not implemented: {}\n".format(message_json['name']))
+
+
+def process_team_join(message_json):
+ server = servers.find(message_json["_server"])
+ item = message_json["user"]
+ server.add_user(User(server, item["name"], item["id"], item["presence"]))
+ server.buffer_prnt("New user joined: {}".format(item["name"]))
+
+
+def process_manual_presence_change(message_json):
+ process_presence_change(message_json)
+
+
+def process_presence_change(message_json):
+ server = servers.find(message_json["_server"])
+ identifier = message_json.get("user", server.nick)
+ if message_json["presence"] == 'active':
+ server.users.find(identifier).set_active()
+ else:
+ server.users.find(identifier).set_inactive()
+
+
+def process_channel_marked(message_json):
+ channel = channels.find(message_json["channel"])
+ channel.mark_read(False)
+ w.buffer_set(channel.channel_buffer, "hotlist", "-1")
+
+
+def process_group_marked(message_json):
+ channel = channels.find(message_json["channel"])
+ channel.mark_read(False)
+ w.buffer_set(channel.channel_buffer, "hotlist", "-1")
+
+
+def process_channel_created(message_json):
+ server = servers.find(message_json["_server"])
+ item = message_json["channel"]
+ if server.channels.find(message_json["channel"]["name"]):
+ server.channels.find(message_json["channel"]["name"]).open(False)
+ else:
+ item = message_json["channel"]
+ server.add_channel(Channel(server, item["name"], item["id"], False, prepend_name="#"))
+ server.buffer_prnt("New channel created: {}".format(item["name"]))
+
+
+def process_channel_left(message_json):
+ server = servers.find(message_json["_server"])
+ server.channels.find(message_json["channel"]).close(False)
+
+
+def process_channel_join(message_json):
+ server = servers.find(message_json["_server"])
+ channel = server.channels.find(message_json["channel"])
+ text = unfurl_refs(message_json["text"], ignore_alt_text=False)
+ channel.buffer_prnt(w.prefix("join").rstrip(), text, message_json["ts"])
+ channel.user_join(message_json["user"])
+
+
+def process_channel_topic(message_json):
+ server = servers.find(message_json["_server"])
+ channel = server.channels.find(message_json["channel"])
+ text = unfurl_refs(message_json["text"], ignore_alt_text=False)
+ channel.buffer_prnt(w.prefix("network").rstrip(), text, message_json["ts"])
+ channel.set_topic(message_json["topic"])
+
+
+def process_channel_joined(message_json):
+ server = servers.find(message_json["_server"])
+ if server.channels.find(message_json["channel"]["name"]):
+ server.channels.find(message_json["channel"]["name"]).open(False)
+ else:
+ item = message_json["channel"]
+ 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):
+ server = servers.find(message_json["_server"])
+ channel = server.channels.find(message_json["channel"])
+ text = unfurl_refs(message_json["text"], ignore_alt_text=False)
+ channel.buffer_prnt(w.prefix("quit").rstrip(), text, message_json["ts"])
+ channel.user_leave(message_json["user"])
+
+
+def process_channel_archive(message_json):
+ server = servers.find(message_json["_server"])
+ channel = server.channels.find(message_json["channel"])
+ channel.detach_buffer()
+
+
+def process_group_join(message_json):
+ process_channel_join(message_json)
+
+
+def process_group_leave(message_json):
+ process_channel_leave(message_json)
+
+
+def process_group_topic(message_json):
+ process_channel_topic(message_json)
+
+
+def process_group_left(message_json):
+ server = servers.find(message_json["_server"])
+ server.channels.find(message_json["channel"]).close(False)
+
+
+def process_group_joined(message_json):
+ server = servers.find(message_json["_server"])
+ if server.channels.find(message_json["channel"]["name"]):
+ server.channels.find(message_json["channel"]["name"]).open(False)
+ else:
+ item = message_json["channel"]
+ if item["name"].startswith("mpdm-"):
+ server.add_channel(MpdmChannel(server, item["name"], item["id"], item["is_open"], item["last_read"], "#", item["members"], item["topic"]["value"]))
+ else:
+ 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):
+ channel = server.channels.find(message_json["channel"])
+ channel.detach_buffer()
+
+
+def process_mpim_close(message_json):
+ server = servers.find(message_json["_server"])
+ server.channels.find(message_json["channel"]).close(False)
+
+
+def process_mpim_open(message_json):
+ server = servers.find(message_json["_server"])
+ server.channels.find(message_json["channel"]).open(False)
+
+
+def process_im_close(message_json):
+ server = servers.find(message_json["_server"])
+ server.channels.find(message_json["channel"]).close(False)
+
+
+def process_im_open(message_json):
+ server = servers.find(message_json["_server"])
+ server.channels.find(message_json["channel"]).open()
+
+
+def process_im_marked(message_json):
+ channel = channels.find(message_json["channel"])
+ channel.mark_read(False)
+ if channel.channel_buffer is not None:
+ w.buffer_set(channel.channel_buffer, "hotlist", "-1")
+
+
+def process_im_created(message_json):
+ server = servers.find(message_json["_server"])
+ item = message_json["channel"]
+ channel_name = server.users.find(item["user"]).name
+ if server.channels.find(channel_name):
+ server.channels.find(channel_name).open(False)
+ else:
+ item = message_json["channel"]
+ server.add_channel(DmChannel(server, channel_name, item["id"], item["is_open"], item["last_read"]))
+ server.buffer_prnt("New direct message channel created: {}".format(item["name"]))
+
+
+def process_user_typing(message_json):
+ server = servers.find(message_json["_server"])
+ channel = server.channels.find(message_json["channel"])
+ if channel:
+ channel.set_typing(server.users.find(message_json["user"]).name)
+
+
+def process_bot_enable(message_json):
+ process_bot_integration(message_json)
+
+
+def process_bot_disable(message_json):
+ process_bot_integration(message_json)
+
+
+def process_bot_integration(message_json):
+ server = servers.find(message_json["_server"])
+ channel = server.channels.find(message_json["channel"])
+
+ time = message_json['ts']
+ text = "{} {}".format(server.users.find(message_json['user']).formatted_name(),
+ render_message(message_json))
+ bot_name = get_user(message_json, server)
+ bot_name = bot_name.encode('utf-8')
+ channel.buffer_prnt(bot_name, text, time)
+
+# todo: does this work?
+
+
+def process_error(message_json):
+ pass
+
+
+def process_reaction_added(message_json):
+ if message_json["item"].get("type") == "message":
+ channel = channels.find(message_json["item"]["channel"])
+ channel.add_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"])
+ else:
+ dbg("Reaction to item type not supported: " + str(message_json))
+
+
+def process_reaction_removed(message_json):
+ if message_json["item"].get("type") == "message":
+ channel = channels.find(message_json["item"]["channel"])
+ channel.remove_reaction(message_json["item"]["ts"], message_json["reaction"], message_json["user"])
+ else:
+ dbg("Reaction to item type not supported: " + str(message_json))
+
+
+def create_reaction_string(reactions):
+ count = 0
+ if not isinstance(reactions, list):
+ reaction_string = " [{}]".format(reactions)
+ else:
+ reaction_string = ' ['
+ for r in reactions:
+ if len(r["users"]) > 0:
+ count += 1
+ if show_reaction_nicks:
+ nicks = [resolve_ref("@{}".format(user)) for user in r["users"]]
+ users = "({})".format(",".join(nicks))
+ else:
+ users = len(r["users"])
+ reaction_string += ":{}:{} ".format(r["name"], users)
+ reaction_string = reaction_string[:-1] + ']'
+ if count == 0:
+ reaction_string = ''
+ return reaction_string
+
+
+def modify_buffer_line(buffer, new_line, time):
+ time = int(float(time))
+ # get a pointer to this buffer's lines
+ own_lines = w.hdata_pointer(w.hdata_get('buffer'), buffer, 'own_lines')
+ if own_lines:
+ # get a pointer to the last line
+ line_pointer = w.hdata_pointer(w.hdata_get('lines'), own_lines, 'last_line')
+ # hold the structure of a line and of line data
+ struct_hdata_line = w.hdata_get('line')
+ struct_hdata_line_data = w.hdata_get('line_data')
+
+ while line_pointer:
+ # get a pointer to the data in line_pointer via layout of struct_hdata_line
+ data = w.hdata_pointer(struct_hdata_line, line_pointer, 'data')
+ if data:
+ date = w.hdata_time(struct_hdata_line_data, data, 'date')
+ # prefix = w.hdata_string(struct_hdata_line_data, data, 'prefix')
+
+ if int(date) == int(time):
+ # w.prnt("", "found matching time date is {}, time is {} ".format(date, time))
+ w.hdata_update(struct_hdata_line_data, data, {"message": new_line})
+ break
+ else:
+ pass
+ # move backwards one line and try again - exit the while if you hit the end
+ line_pointer = w.hdata_move(struct_hdata_line, line_pointer, -1)
+ return w.WEECHAT_RC_OK
+
+
+def render_message(message_json, force=False):
+ global unfurl_ignore_alt_text
+ # If we already have a rendered version in the object, just return that.
+ if not force and message_json.get("_rendered_text", ""):
+ return message_json["_rendered_text"]
+ else:
+ # server = servers.find(message_json["_server"])
+
+ if "fallback" in message_json:
+ text = message_json["fallback"]
+ elif "text" in message_json:
+ if message_json['text'] is not None:
+ text = message_json["text"]
+ else:
+ text = u""
+ else:
+ text = u""
+
+ text = unfurl_refs(text, ignore_alt_text=unfurl_ignore_alt_text)
+
+ text_before = (len(text) > 0)
+ text += unfurl_refs(unwrap_attachments(message_json, text_before), ignore_alt_text=unfurl_ignore_alt_text)
+
+ text = text.lstrip()
+ text = text.replace("\t", " ")
+ text = text.replace("&lt;", "<")
+ text = text.replace("&gt;", ">")
+ text = text.replace("&amp;", "&")
+ text = text.encode('utf-8')
+
+ if "reactions" in message_json:
+ text += create_reaction_string(message_json["reactions"])
+ message_json["_rendered_text"] = text
+ return text
+
+
+def process_message(message_json, cache=True):
+ try:
+ # send these subtype messages elsewhere
+ known_subtypes = ["message_changed", 'message_deleted', 'channel_join', 'channel_leave', 'channel_topic', 'group_join', 'group_leave', 'group_topic', 'bot_enable', 'bot_disable']
+ if "subtype" in message_json and message_json["subtype"] in known_subtypes:
+ proc[message_json["subtype"]](message_json)
+
+ else:
+ server = servers.find(message_json["_server"])
+ channel = channels.find(message_json["channel"])
+
+ # do not process messages in unexpected channels
+ if not channel.active:
+ channel.open(False)
+ dbg("message came for closed channel {}".format(channel.name))
+ return
+
+ time = message_json['ts']
+ text = render_message(message_json)
+ name = get_user(message_json, server)
+ name = name.encode('utf-8')
+
+ # special case with actions.
+ if text.startswith("_") and text.endswith("_"):
+ text = text[1:-1]
+ if name != channel.server.nick:
+ text = name + " " + text
+ channel.buffer_prnt(w.prefix("action").rstrip(), text, time)
+
+ else:
+ suffix = ''
+ if 'edited' in message_json:
+ suffix = ' (edited)'
+ channel.buffer_prnt(name, text + suffix, time)
+
+ if cache:
+ channel.cache_message(message_json)
+
+ except Exception:
+ channel = channels.find(message_json["channel"])
+ dbg("cannot process message {}\n{}".format(message_json, traceback.format_exc()))
+ if channel and ("text" in message_json) and message_json['text'] is not None:
+ channel.buffer_prnt('unknown', message_json['text'])
+
+
+def process_message_changed(message_json):
+ m = message_json["message"]
+ if "message" in message_json:
+ if "attachments" in m:
+ message_json["attachments"] = m["attachments"]
+ if "text" in m:
+ if "text" in message_json:
+ message_json["text"] += m["text"]
+ dbg("added text!")
+ else:
+ message_json["text"] = m["text"]
+ if "fallback" in m:
+ if "fallback" in message_json:
+ message_json["fallback"] += m["fallback"]
+ else:
+ message_json["fallback"] = m["fallback"]
+
+ text_before = (len(m['text']) > 0)
+ m["text"] += unwrap_attachments(message_json, text_before)
+ channel = channels.find(message_json["channel"])
+ if "edited" in m:
+ channel.change_message(m["ts"], m["text"], ' (edited)')
+ else:
+ channel.change_message(m["ts"], m["text"])
+
+
+def process_message_deleted(message_json):
+ channel = channels.find(message_json["channel"])
+ channel.change_message(message_json["deleted_ts"], "(deleted)")
+
+
+def unwrap_attachments(message_json, text_before):
+ attachment_text = ''
+ if "attachments" in message_json:
+ if text_before:
+ attachment_text = u'\n'
+ for attachment in message_json["attachments"]:
+ # Attachments should be rendered roughly like:
+ #
+ # $pretext
+ # $author: (if rest of line is non-empty) $title ($title_link) OR $from_url
+ # $author: (if no $author on previous line) $text
+ # $fields
+ t = []
+ prepend_title_text = ''
+ if 'author_name' in attachment:
+ prepend_title_text = attachment['author_name'] + ": "
+ if 'pretext' in attachment:
+ t.append(attachment['pretext'])
+ if "title" in attachment:
+ if 'title_link' in attachment:
+ t.append('%s%s (%s)' % (prepend_title_text, attachment["title"], attachment["title_link"],))
+ else:
+ t.append(prepend_title_text + attachment["title"])
+ prepend_title_text = ''
+ elif "from_url" in attachment:
+ t.append(attachment["from_url"])
+ if "text" in attachment:
+ tx = re.sub(r' *\n[\n ]+', '\n', attachment["text"])
+ t.append(prepend_title_text + tx)
+ prepend_title_text = ''
+ if 'fields' in attachment:
+ for f in attachment['fields']:
+ if f['title'] != '':
+ t.append('%s %s' % (f['title'], f['value'],))
+ else:
+ t.append(f['value'])
+ if t == [] and "fallback" in attachment:
+ t.append(attachment["fallback"])
+ attachment_text += "\n".join([x.strip() for x in t if x])
+ return attachment_text
+
+
+def resolve_ref(ref):
+ if ref.startswith('@U') or ref.startswith('@W'):
+ if users.find(ref[1:]):
+ try:
+ return "@{}".format(users.find(ref[1:]).name)
+ except:
+ dbg("NAME: {}".format(ref))
+ elif ref.startswith('#C'):
+ if channels.find(ref[1:]):
+ try:
+ return "{}".format(channels.find(ref[1:]).name)
+ except:
+ dbg("CHANNEL: {}".format(ref))
+
+ # Something else, just return as-is
+ return ref
+
+
+def unfurl_ref(ref, ignore_alt_text=False):
+ id = ref.split('|')[0]
+ display_text = ref
+ if ref.find('|') > -1:
+ if ignore_alt_text:
+ display_text = resolve_ref(id)
+ else:
+ if id.startswith("#C") or id.startswith("@U"):
+ display_text = ref.split('|')[1]
+ else:
+ url, desc = ref.split('|', 1)
+ display_text = u"{} ({})".format(url, desc)
+ else:
+ display_text = resolve_ref(ref)
+ return display_text
+
+
+def unfurl_refs(text, ignore_alt_text=False):
+ """
+ input : <@U096Q7CQM|someuser> has joined the channel
+ ouput : someuser has joined the channel
+ """
+ # Find all strings enclosed by <>
+ # - <https://example.com|example with spaces>
+ # - <#C2147483705|#otherchannel>
+ # - <@U2147483697|@othernick>
+ # Test patterns lives in ./_pytest/test_unfurl.py
+ matches = re.findall(r"(<[@#]?(?:[^<]*)>)", text)
+ for m in matches:
+ # Replace them with human readable strings
+ text = text.replace(m, unfurl_ref(m[1:-1], ignore_alt_text))
+ return text
+
+
+def get_user(message_json, server):
+ if 'bot_id' in message_json and message_json['bot_id'] is not None:
+ name = u"{} :]".format(server.bots.find(message_json["bot_id"]).formatted_name())
+ elif 'user' in message_json:
+ u = server.users.find(message_json['user'])
+ if u.is_bot:
+ name = u"{} :]".format(u.formatted_name())
+ else:
+ name = u.name
+ elif 'username' in message_json:
+ name = u"-{}-".format(message_json["username"])
+ elif 'service_name' in message_json:
+ name = u"-{}-".format(message_json["service_name"])
+ else:
+ name = u""
+ return name
+
+# END Websocket handling methods
+
+
+def typing_bar_item_cb(data, buffer, args):
+ typers = [x for x in channels if x.is_someone_typing()]
+ if len(typers) > 0:
+ direct_typers = []
+ channel_typers = []
+ for dm in channels.find_by_class(DmChannel):
+ direct_typers.extend(dm.get_typing_list())
+ direct_typers = ["D/" + x for x in direct_typers]
+ current_channel = w.current_buffer()
+ channel = channels.find(current_channel)
+ try:
+ if channel and channel.__class__ != DmChannel:
+ channel_typers = channels.find(current_channel).get_typing_list()
+ except:
+ w.prnt("", "Bug on {}".format(channel))
+ typing_here = ", ".join(channel_typers + direct_typers)
+ if len(typing_here) > 0:
+ color = w.color('yellow')
+ return color + "typing: " + typing_here
+ return ""
+
+
+def typing_update_cb(data, remaining_calls):
+ w.bar_item_update("slack_typing_notice")
+ return w.WEECHAT_RC_OK
+
+
+def buffer_list_update_cb(data, remaining_calls):
+ global buffer_list_update
+
+ now = time.time()
+ if buffer_list_update and previous_buffer_list_update + 1 < now:
+ # gray_check = False
+ # if len(servers) > 1:
+ # gray_check = True
+ for channel in channels:
+ channel.rename()
+ buffer_list_update = False
+ return w.WEECHAT_RC_OK
+
+
+def buffer_list_update_next():
+ global buffer_list_update
+ buffer_list_update = True
+
+
+def hotlist_cache_update_cb(data, remaining_calls):
+ # this keeps the hotlist dupe up to date for the buffer switch, but is prob technically a race condition. (meh)
+ global hotlist
+ prev_hotlist = hotlist
+ hotlist = w.infolist_get("hotlist", "", "")
+ w.infolist_free(prev_hotlist)
+ return w.WEECHAT_RC_OK
+
+
+def buffer_closing_cb(signal, sig_type, data):
+ if channels.find(data):
+ channels.find(data).closed()
+ return w.WEECHAT_RC_OK
+
+
+def buffer_opened_cb(signal, sig_type, data):
+ channels.update_hashtable()
+ return w.WEECHAT_RC_OK
+
+
+def buffer_switch_cb(signal, sig_type, data):
+ global previous_buffer, hotlist
+ # this is to see if we need to gray out things in the buffer list
+ if channels.find(previous_buffer):
+ channels.find(previous_buffer).mark_read()
+
+ # channel_name = current_buffer_name()
+ previous_buffer = data
+ return w.WEECHAT_RC_OK
+
+
+def typing_notification_cb(signal, sig_type, data):
+ if len(w.buffer_get_string(data, "input")) > 8:
+ global typing_timer
+ now = time.time()
+ if typing_timer + 4 < now:
+ channel = channels.find(current_buffer_name())
+ if channel:
+ identifier = channel.identifier
+ request = {"type": "typing", "channel": identifier}
+ channel.server.send_to_websocket(request, expect_reply=False)
+ typing_timer = now
+ return w.WEECHAT_RC_OK
+
+
+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..")
+ if server.ws_hook is not None:
+ w.unhook(server.ws_hook)
+ server.connect_to_slack()
+ return w.WEECHAT_RC_OK
+
+
+def slack_never_away_cb(data, remaining):
+ global never_away
+ if never_away:
+ for server in servers:
+ identifier = server.channels.find("slackbot").identifier
+ request = {"type": "typing", "channel": identifier}
+ # request = {"type":"typing","channel":"slackbot"}
+ server.send_to_websocket(request, expect_reply=False)
+ return w.WEECHAT_RC_OK
+
+
+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
+ if current_pos < 0:
+ current_pos = 0
+ 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
+def async_slack_api_request(domain, token, request, post_data, priority=False):
+ if not STOP_TALKING_TO_SLACK:
+ post_data["token"] = token
+ url = 'url:https://{}/api/{}?{}'.format(domain, request, urllib.urlencode(post_data))
+ context = pickle.dumps({"request": request, "token": token, "post_data": post_data})
+ params = {'useragent': 'wee_slack {}'.format(SCRIPT_VERSION)}
+ dbg("URL: {} context: {} params: {}".format(url, context, params))
+ w.hook_process_hashtable(url, params, slack_timeout, "url_processor_cb", context)
+
+
+def async_slack_api_upload_request(token, request, post_data, priority=False):
+ if not STOP_TALKING_TO_SLACK:
+ url = 'https://slack.com/api/{}'.format(request)
+ file_path = os.path.expanduser(post_data["file"])
+ command = 'curl -F file=@{} -F channels={} -F token={} {}'.format(file_path, post_data["channels"], token, url)
+ context = pickle.dumps({"request": request, "token": token, "post_data": post_data})
+ w.hook_process(command, slack_timeout, "url_processor_cb", context)
+
+
+# funny, right?
+big_data = {}
+
+
+def url_processor_cb(data, command, return_code, out, err):
+ global big_data
+ data = pickle.loads(data)
+ identifier = sha.sha("{}{}".format(data, command)).hexdigest()
+ if identifier not in big_data:
+ big_data[identifier] = ''
+ big_data[identifier] += out
+ if return_code == 0:
+ try:
+ my_json = json.loads(big_data[identifier])
+ except:
+ dbg("request failed, doing again...")
+ dbg("response length: {} identifier {}\n{}".format(len(big_data[identifier]), identifier, data))
+ my_json = False
+
+ big_data.pop(identifier, None)
+
+ 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"]:
+ channel = data["post_data"]["channel"]
+ token = data["token"]
+ if "messages" in my_json:
+ my_json["messages"].reverse()
+ for message in my_json["messages"]:
+ message["_server"] = servers.find(token).domain
+ message["channel"] = servers.find(token).channels.find(channel).identifier
+ process_message(message)
+ if "channel" in my_json:
+ if "members" in my_json["channel"]:
+ channels.find(my_json["channel"]["id"]).members = set(my_json["channel"]["members"])
+ else:
+ if return_code != -1:
+ big_data.pop(identifier, None)
+ dbg("return code: {}, data: {}, output: {}, error: {}".format(return_code, data, out, err))
+
+ return w.WEECHAT_RC_OK
+
+
+def cache_write_cb(data, remaining):
+ cache_file = open("{}/{}".format(WEECHAT_HOME, CACHE_NAME), 'w')
+ cache_file.write(CACHE_VERSION + "\n")
+ 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)
+ cache_file = open(file_name, 'r')
+ if cache_file.readline() == CACHE_VERSION + "\n":
+ dbg("Loading messages from cache.", main_buffer=True)
+ for line in cache_file:
+ j = json.loads(line)
+ message_cache[j["channel"]].append(line)
+ dbg("Completed loading messages from cache.", main_buffer=True)
+ except ValueError:
+ w.prnt("", "Failed to load cache file, probably illegal JSON.. Ignoring")
+ pass
+ except IOError:
+ w.prnt("", "cache file not found")
+ pass
+
+# END Slack specific requests
+
+# Utility Methods
+
+
+def current_domain_name():
+ buffer = w.current_buffer()
+ if servers.find(buffer):
+ return servers.find(buffer).domain
+ else:
+ # number = w.buffer_get_integer(buffer, "number")
+ name = w.buffer_get_string(buffer, "name")
+ name = ".".join(name.split(".")[:-1])
+ return name
+
+
+def current_buffer_name(short=False):
+ buffer = w.current_buffer()
+ # number = w.buffer_get_integer(buffer, "number")
+ name = w.buffer_get_string(buffer, "name")
+ if short:
+ try:
+ name = name.split('.')[-1]
+ except:
+ pass
+ return name
+
+
+def closed_slack_buffer_cb(data, buffer):
+ global slack_buffer
+ slack_buffer = None
+ return w.WEECHAT_RC_OK
+
+
+def create_slack_buffer():
+ global slack_buffer
+ slack_buffer = w.buffer_new("slack", "", "", "closed_slack_buffer_cb", "")
+ w.buffer_set(slack_buffer, "notify", "0")
+ # w.buffer_set(slack_buffer, "display", "1")
+ return w.WEECHAT_RC_OK
+
+
+def closed_slack_debug_buffer_cb(data, buffer):
+ global slack_debug
+ slack_debug = None
+ return w.WEECHAT_RC_OK
+
+
+def create_slack_debug_buffer():
+ global slack_debug, debug_string
+ if slack_debug is not None:
+ w.buffer_set(slack_debug, "display", "1")
+ else:
+ debug_string = None
+ slack_debug = w.buffer_new("slack-debug", "", "", "closed_slack_debug_buffer_cb", "")
+ w.buffer_set(slack_debug, "notify", "0")
+
+
+def config_changed_cb(data, option, value):
+ global slack_api_token, distracting_channels, colorize_nicks, colorize_private_chats, slack_debug, debug_mode, \
+ unfurl_ignore_alt_text, colorize_messages, show_reaction_nicks, slack_timeout
+
+ slack_api_token = w.config_get_plugin("slack_api_token")
+
+ if slack_api_token.startswith('${sec.data'):
+ slack_api_token = w.string_eval_expression(slack_api_token, {}, {}, {})
+
+ distracting_channels = [x.strip() for x in w.config_get_plugin("distracting_channels").split(',')]
+ colorize_nicks = w.config_get_plugin('colorize_nicks') == "1"
+ colorize_messages = w.config_get_plugin("colorize_messages") == "1"
+ debug_mode = w.config_get_plugin("debug_mode").lower()
+ if debug_mode != '' and debug_mode != 'false':
+ create_slack_debug_buffer()
+ colorize_private_chats = w.config_string_to_boolean(w.config_get_plugin("colorize_private_chats"))
+ show_reaction_nicks = w.config_string_to_boolean(w.config_get_plugin("show_reaction_nicks"))
+
+ unfurl_ignore_alt_text = False
+ if w.config_get_plugin('unfurl_ignore_alt_text') != "0":
+ unfurl_ignore_alt_text = True
+
+ slack_timeout = int(w.config_get_plugin('slack_timeout'))
+
+ return w.WEECHAT_RC_OK
+
+
+def quit_notification_cb(signal, sig_type, data):
+ 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
+if __name__ == "__main__":
+
+ if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
+ SCRIPT_DESC, "script_unloaded", ""):
+
+ version = w.info_get("version_number", "") or 0
+ if int(version) < 0x1030000:
+ w.prnt("", "\nERROR: Weechat version 1.3+ is required to use {}.\n\n".format(SCRIPT_NAME))
+ else:
+
+ WEECHAT_HOME = w.info_get("weechat_dir", "")
+ CACHE_NAME = "slack.cache"
+ STOP_TALKING_TO_SLACK = False
+
+ if not w.config_get_plugin('slack_api_token'):
+ w.config_set_plugin('slack_api_token', "INSERT VALID KEY HERE!")
+ if not w.config_get_plugin('distracting_channels'):
+ w.config_set_plugin('distracting_channels', "")
+ if not w.config_get_plugin('debug_mode'):
+ w.config_set_plugin('debug_mode', "")
+ if not w.config_get_plugin('colorize_nicks'):
+ w.config_set_plugin('colorize_nicks', "1")
+ if not w.config_get_plugin('colorize_messages'):
+ w.config_set_plugin('colorize_messages', "0")
+ if not w.config_get_plugin('colorize_private_chats'):
+ w.config_set_plugin('colorize_private_chats', "0")
+ 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")
+ if not w.config_get_plugin('switch_buffer_on_join'):
+ w.config_set_plugin('switch_buffer_on_join', "1")
+ if not w.config_get_plugin('show_reaction_nicks'):
+ w.config_set_plugin('show_reaction_nicks', "0")
+ if not w.config_get_plugin('slack_timeout'):
+ w.config_set_plugin('slack_timeout', "20000")
+ if w.config_get_plugin('channels_not_on_current_server_color'):
+ w.config_option_unset('channels_not_on_current_server_color')
+
+ # Global var section
+ slack_debug = None
+ config_changed_cb("", "", "")
+
+ cmds = {k[8:]: v for k, v in globals().items() if k.startswith("command_")}
+ proc = {k[8:]: v for k, v in globals().items() if k.startswith("process_")}
+
+ typing_timer = time.time()
+ domain = None
+ previous_buffer = None
+ slack_buffer = None
+
+ buffer_list_update = False
+ previous_buffer_list_update = 0
+
+ never_away = False
+ hide_distractions = False
+ hotlist = w.infolist_get("hotlist", "", "")
+ main_weechat_buffer = w.info_get("irc_buffer", "{}.{}".format(domain, "DOESNOTEXIST!@#$"))
+
+ message_cache = collections.defaultdict(list)
+ cache_load()
+
+ servers = SearchList()
+ for token in slack_api_token.split(','):
+ 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", "")
+
+ # attach to the weechat hooks we need
+ w.hook_timer(1000, 0, 0, "typing_update_cb", "")
+ w.hook_timer(1000, 0, 0, "buffer_list_update_cb", "")
+ w.hook_timer(1000, 0, 0, "hotlist_cache_update_cb", "")
+ w.hook_timer(1000 * 60 * 29, 0, 0, "slack_never_away_cb", "")
+ w.hook_timer(1000 * 60 * 5, 0, 0, "cache_write_cb", "")
+ w.hook_signal('buffer_closing', "buffer_closing_cb", "")
+ w.hook_signal('buffer_opened', "buffer_opened_cb", "")
+ w.hook_signal('buffer_switch', "buffer_switch_cb", "")
+ 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',
+ # Usage
+ '[command] [command options]',
+ # Description of arguments
+ 'Commands:\n' +
+ '\n'.join(cmds.keys()) +
+ '\nUse /slack help [command] to find out more\n',
+ # Completions
+ '|'.join(cmds.keys()),
+ # Function name
+ 'slack_command_cb', '')
+ # w.hook_command('me', 'me_command_cb', '')
+ w.hook_command('me', '', 'stuff', 'stuff2', '', 'me_command_cb', '')
+ w.hook_command_run('/query', 'join_command_cb', '')
+ 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('/topic', 'topic_command_cb', '')
+ w.hook_command_run('/msg', 'msg_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
diff --git a/weechat/python/autosort.py b/weechat/python/autosort.py
new file mode 100755
index 0000000..7b53b77
--- /dev/null
+++ b/weechat/python/autosort.py
@@ -0,0 +1,865 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013-2014 Maarten de Vries <maarten@de-vri.es>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+#
+# Autosort automatically keeps your buffers sorted and grouped by server.
+# You can define your own sorting rules. See /help autosort for more details.
+#
+# http://github.com/de-vri.es/weechat-autosort
+#
+
+#
+# Changelog:
+# 2.6:
+# * Ignore case in rules when doing case insensitive sorting.
+# 2.5:
+# * Fix handling unicode buffer names.
+# * Add hint to set irc.look.server_buffer to independent and buffers.look.indenting to on.
+# 2.4:
+# * Make script python3 compatible.
+# 2.3:
+# * Fix sorting items without score last (regressed in 2.2).
+# 2.2:
+# * Add configuration option for signals that trigger a sort.
+# * Add command to manually trigger a sort (/autosort sort).
+# * Add replacement patterns to apply before sorting.
+# 2.1:
+# * Fix some minor style issues.
+# 2.0:
+# * Allow for custom sort rules.
+#
+
+
+import weechat
+import re
+import json
+
+SCRIPT_NAME = 'autosort'
+SCRIPT_AUTHOR = 'Maarten de Vries <maarten@de-vri.es>'
+SCRIPT_VERSION = '2.6'
+SCRIPT_LICENSE = 'GPL3'
+SCRIPT_DESC = 'Automatically or manually keep your buffers sorted and grouped by server.'
+
+
+config = None
+hooks = []
+
+class HumanReadableError(Exception):
+ pass
+
+
+def parse_int(arg, arg_name = 'argument'):
+ ''' Parse an integer and provide a more human readable error. '''
+ arg = arg.strip()
+ try:
+ return int(arg)
+ except ValueError:
+ raise HumanReadableError('Invalid {0}: expected integer, got "{1}".'.format(arg_name, arg))
+
+
+class Pattern:
+ ''' A simple glob-like pattern for matching buffer names. '''
+
+ def __init__(self, pattern, case_sensitive):
+ ''' Construct a pattern from a string. '''
+ escaped = False
+ char_class = 0
+ chars = ''
+ regex = ''
+ for c in pattern:
+ if escaped and char_class:
+ escaped = False
+ chars += re.escape(c)
+ elif escaped:
+ escaped = False
+ regex += re.escape(c)
+ elif c == '\\':
+ escaped = True
+ elif c == '*' and not char_class:
+ regex += '[^.]*'
+ elif c == '?' and not char_class:
+ regex += '[^.]'
+ elif c == '[' and not char_class:
+ char_class = 1
+ chars = ''
+ elif c == '^' and char_class and not chars:
+ chars += '^'
+ elif c == ']' and char_class and chars not in ('', '^'):
+ char_class = False
+ regex += '[' + chars + ']'
+ elif c == '-' and char_class:
+ chars += '-'
+ elif char_class:
+ chars += re.escape(c)
+ else:
+ regex += re.escape(c)
+
+ if char_class:
+ raise ValueError("unmatched opening '['")
+ if escaped:
+ raise ValueError("unexpected trailing '\\'")
+
+ if case_sensitive:
+ self.regex = re.compile('^' + regex + '$')
+ else:
+ self.regex = re.compile('^' + regex + '$', flags = re.IGNORECASE)
+ self.pattern = pattern
+
+ def match(self, input):
+ ''' Match the pattern against a string. '''
+ return self.regex.match(input)
+
+
+class FriendlyList(object):
+ ''' A list with human readable errors. '''
+
+ def __init__(self):
+ self.__data = []
+
+ def raw(self):
+ return self.__data
+
+ def append(self, value):
+ ''' Add a rule to the list. '''
+ self.__data.append(value)
+
+ def insert(self, index, value):
+ ''' Add a rule to the list. '''
+ if not 0 <= index <= len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}], got {1}.'.format(len(self), index))
+ self.__data.insert(index, value)
+
+ def pop(self, index):
+ ''' Remove a rule from the list and return it. '''
+ if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
+ return self.__data.pop(index)
+
+ def move(self, index_a, index_b):
+ ''' Move a rule to a new position in the list. '''
+ self.insert(index_b, self.pop(index_a))
+
+ def swap(self, index_a, index_b):
+ ''' Swap two elements in the list. '''
+ self[index_a], self[index_b] = self[index_b], self[index_a]
+
+ def __len__(self):
+ return len(self.__data)
+
+ def __getitem__(self, index):
+ if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
+ return self.__data[index]
+
+ def __setitem__(self, index, value):
+ if not 0 <= index < len(self): raise HumanReadableError('Index out of range: expected an integer in the range [0, {0}), got {1}.'.format(len(self), index))
+ self.__data[index] = value
+
+ def __iter__(self):
+ return iter(self.__data)
+
+
+class RuleList(FriendlyList):
+ ''' A list of rules to test buffer names against. '''
+ rule_regex = re.compile(r'^(.*)=\s*([+-]?[^=]*)$')
+
+ def __init__(self, rules):
+ ''' Construct a RuleList from a list of rules. '''
+ super(RuleList, self).__init__()
+ for rule in rules: self.append(rule)
+
+ def get_score(self, name):
+ ''' Get the sort score of a partial name according to a rule list. '''
+ for rule in self:
+ if rule[0].match(name): return rule[1]
+ return 999999999
+
+ def encode(self):
+ ''' Encode the rules for storage. '''
+ return json.dumps(list(map(lambda x: (x[0].pattern, x[1]), self)))
+
+ @staticmethod
+ def decode(blob, case_sensitive):
+ ''' Parse rules from a string blob. '''
+ result = []
+
+ try:
+ decoded = json.loads(blob)
+ except ValueError:
+ log('Invalid rules: expected JSON encoded list of pairs, got "{0}".'.format(blob))
+ return [], 0
+
+ for rule in decoded:
+ # Rules must be a pattern,score pair.
+ if len(rule) != 2:
+ log('Invalid rule: expected (pattern, score), got "{0}". Rule ignored.'.format(rule))
+ continue
+
+ # Rules must have a valid pattern.
+ try:
+ pattern = Pattern(rule[0], case_sensitive)
+ except ValueError as e:
+ log('Invalid pattern: {0} in "{1}". Rule ignored.'.format(e, rule[0]))
+ continue
+
+ # Rules must have a valid score.
+ try:
+ score = int(rule[1])
+ except ValueError as e:
+ log('Invalid score: expected an integer, got "{0}". Rule ignored.'.format(score))
+ continue
+
+ result.append((pattern, score))
+
+ return RuleList(result)
+
+ @staticmethod
+ def parse_rule(arg, case_sensitive):
+ ''' Parse a rule argument. '''
+ arg = arg.strip()
+ match = RuleList.rule_regex.match(arg)
+ if not match:
+ raise HumanReadableError('Invalid rule: expected "<pattern> = <score>", got "{0}".'.format(arg))
+
+ pattern = match.group(1).strip()
+ try:
+ pattern = Pattern(pattern, case_sensitive)
+ except ValueError as e:
+ raise HumanReadableError('Invalid pattern: {0} in "{1}".'.format(e, pattern))
+
+ score = parse_int(match.group(2), 'score')
+ return (pattern, score)
+
+
+def decode_replacements(blob):
+ ''' Decode a replacement list encoded as JSON. '''
+ result = FriendlyList()
+ try:
+ decoded = json.loads(blob)
+ except ValueError:
+ log('Invalid replacement list: expected JSON encoded list of pairs, got "{0}".'.format(blob))
+ return [], 0
+
+ for replacement in decoded:
+ # Replacements must be a (string, string) pair.
+ if len(replacement) != 2:
+ log('Invalid replacement pattern: expected (pattern, replacement), got "{0}". Replacement ignored.'.format(rule))
+ continue
+ result.append(replacement)
+
+ return result
+
+
+def encode_replacements(replacements):
+ ''' Encode a list of replacement patterns as JSON. '''
+ return json.dumps(replacements.raw())
+
+
+class Config:
+ ''' The autosort configuration. '''
+
+ default_rules = json.dumps([
+ ('core', 0),
+ ('irc', 2),
+ ('*', 1),
+
+ ('irc.irc_raw', 0),
+ ('irc.server', 1),
+ ])
+
+ default_replacements = '[]'
+ default_signals = 'buffer_opened buffer_merged buffer_unmerged buffer_renamed'
+
+ def __init__(self, filename):
+ ''' Initialize the configuration. '''
+
+ self.filename = filename
+ self.config_file = weechat.config_new(self.filename, '', '')
+ self.sorting_section = None
+
+ self.case_sensitive = False
+ self.group_irc = True
+ self.rules = []
+ self.replacements = []
+ self.signals = []
+ self.sort_on_config = True
+
+ self.__case_sensitive = None
+ self.__group_irc = None
+ self.__rules = None
+ self.__replacements = None
+ self.__signals = None
+ self.__sort_on_config = None
+
+ if not self.config_file:
+ log('Failed to initialize configuration file "{0}".'.format(self.filename))
+ return
+
+ self.sorting_section = weechat.config_new_section(self.config_file, 'sorting', False, False, '', '', '', '', '', '', '', '', '', '')
+
+ if not self.sorting_section:
+ log('Failed to initialize section "sorting" of configuration file.')
+ weechat.config_free(self.config_file)
+ return
+
+ self.__case_sensitive = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'case_sensitive', 'boolean',
+ 'If this option is on, sorting is case sensitive.',
+ '', 0, 0, 'off', 'off', 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__group_irc = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'group_irc', 'boolean',
+ 'If this option is on, the script pretends that IRC channel/private buffers are renamed to "irc.server.{network}.{channel}" rather than "irc.{network}.{channel}".' +
+ 'This ensures that these buffers are grouped with their respective server buffer.',
+ '', 0, 0, 'on', 'on', 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__rules = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'rules', 'string',
+ 'An ordered list of sorting rules encoded as JSON. See /help autosort for commands to manipulate these rules.',
+ '', 0, 0, Config.default_rules, Config.default_rules, 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__replacements = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'replacements', 'string',
+ 'An ordered list of replacement patterns to use on buffer name components, encoded as JSON. See /help autosort for commands to manipulate these replacements.',
+ '', 0, 0, Config.default_replacements, Config.default_replacements, 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__signals = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'signals', 'string',
+ 'The signals that will cause autosort to resort your buffer list. Seperate signals with spaces.',
+ '', 0, 0, Config.default_signals, Config.default_signals, 0,
+ '', '', '', '', '', ''
+ )
+
+ self.__sort_on_config = weechat.config_new_option(
+ self.config_file, self.sorting_section,
+ 'sort_on_config_change', 'boolean',
+ 'Decides if the buffer list should be sorted when autosort configuration changes.',
+ '', 0, 0, 'on', 'on', 0,
+ '', '', '', '', '', ''
+ )
+
+ if weechat.config_read(self.config_file) != weechat.WEECHAT_RC_OK:
+ log('Failed to load configuration file.')
+
+ if weechat.config_write(self.config_file) != weechat.WEECHAT_RC_OK:
+ log('Failed to write configuration file.')
+
+ self.reload()
+
+ def reload(self):
+ ''' Load configuration variables. '''
+
+ self.case_sensitive = weechat.config_boolean(self.__case_sensitive)
+ self.group_irc = weechat.config_boolean(self.__group_irc)
+
+ rules_blob = weechat.config_string(self.__rules)
+ replacements_blob = weechat.config_string(self.__replacements)
+ signals_blob = weechat.config_string(self.__signals)
+
+ self.rules = RuleList.decode(rules_blob, self.case_sensitive)
+ self.replacements = decode_replacements(replacements_blob)
+ self.signals = signals_blob.split()
+ self.sort_on_config = weechat.config_boolean(self.__sort_on_config)
+
+ def save_rules(self, run_callback = True):
+ ''' Save the current rules to the configuration. '''
+ weechat.config_option_set(self.__rules, RuleList.encode(self.rules), run_callback)
+
+ def save_replacements(self, run_callback = True):
+ ''' Save the current replacement patterns to the configuration. '''
+ weechat.config_option_set(self.__replacements, encode_replacements(self.replacements), run_callback)
+
+
+def pad(sequence, length, padding = None):
+ ''' Pad a list until is has a certain length. '''
+ return sequence + [padding] * max(0, (length - len(sequence)))
+
+
+def log(message, buffer = 'NULL'):
+ weechat.prnt(buffer, 'autosort: {0}'.format(message))
+
+
+def get_buffers():
+ ''' Get a list of all the buffers in weechat. '''
+ buffers = []
+
+ buffer_list = weechat.infolist_get('buffer', '', '')
+
+ while weechat.infolist_next(buffer_list):
+ name = weechat.infolist_string (buffer_list, 'full_name')
+ number = weechat.infolist_integer(buffer_list, 'number')
+
+ # Buffer is merged with one we already have in the list, skip it.
+ if number <= len(buffers):
+ continue
+ buffers.append(name)
+
+ weechat.infolist_free(buffer_list)
+ return buffers
+
+
+def preprocess(buffer, config):
+ '''
+ Preprocess a buffers names.
+ '''
+ if not config.case_sensitive:
+ buffer = buffer.lower()
+
+ for replacement in config.replacements:
+ buffer = buffer.replace(replacement[0], replacement[1])
+
+ buffer = buffer.split('.')
+ if config.group_irc and len(buffer) >= 2 and buffer[0] == 'irc' and buffer[1] not in ('server', 'irc_raw'):
+ buffer.insert(1, 'server')
+
+ return buffer
+
+
+def buffer_sort_key(rules):
+ ''' Create a sort key function for a buffer list from a rule list. '''
+ def key(buffer):
+ result = []
+ name = ''
+ for word in preprocess(buffer.decode('utf-8'), config):
+ name += ('.' if name else '') + word
+ result.append((rules.get_score(name), word))
+ return result
+
+ return key
+
+
+def apply_buffer_order(buffers):
+ ''' Sort the buffers in weechat according to the order in the input list. '''
+ for i, buffer in enumerate(buffers):
+ weechat.command('', '/buffer swap {0} {1}'.format(buffer, i + 1))
+
+
+def split_args(args, expected, optional = 0):
+ ''' Split an argument string in the desired number of arguments. '''
+ split = args.split(' ', expected - 1)
+ if (len(split) < expected):
+ raise HumanReadableError('Expected at least {0} arguments, got {1}.'.format(expected, len(split)))
+ return split[:-1] + pad(split[-1].split(' ', optional), optional + 1, '')
+
+
+def command_sort(buffer, command, args):
+ ''' Sort the buffers and print a confirmation. '''
+ on_buffers_changed()
+ log("Finished sorting buffers.", buffer)
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_list(buffer, command, args):
+ ''' Show the list of sorting rules. '''
+ output = 'Sorting rules:\n'
+ for i, rule in enumerate(config.rules):
+ output += ' {0}: {1} = {2}\n'.format(i, rule[0].pattern, rule[1])
+ if not len(config.rules):
+ output += ' No sorting rules configured.\n'
+ log(output, buffer)
+
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_add(buffer, command, args):
+ ''' Add a rule to the rule list. '''
+ rule = RuleList.parse_rule(args, config.case_sensitive)
+
+ config.rules.append(rule)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_insert(buffer, command, args):
+ ''' Insert a rule at the desired position in the rule list. '''
+ index, rule = split_args(args, 2)
+ index = parse_int(index, 'index')
+ rule = RuleList.parse_rule(rule, config.case_sensitive)
+
+ config.rules.insert(index, rule)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_update(buffer, command, args):
+ ''' Update a rule in the rule list. '''
+ index, rule = split_args(args, 2)
+ index = parse_int(index, 'index')
+ rule = RuleList.parse_rule(rule, config.case_sensitive)
+
+ config.rules[index] = rule
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_delete(buffer, command, args):
+ ''' Delete a rule from the rule list. '''
+ index = args.strip()
+ index = parse_int(index, 'index')
+
+ config.rules.pop(index)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_move(buffer, command, args):
+ ''' Move a rule to a new position. '''
+ index_a, index_b = split_args(args, 2)
+ index_a = parse_int(index_a, 'index')
+ index_b = parse_int(index_b, 'index')
+
+ config.rules.move(index_a, index_b)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_rule_swap(buffer, command, args):
+ ''' Swap two rules. '''
+ index_a, index_b = split_args(args, 2)
+ index_a = parse_int(index_a, 'index')
+ index_b = parse_int(index_b, 'index')
+
+ config.rules.swap(index_a, index_b)
+ config.save_rules()
+ command_rule_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_list(buffer, command, args):
+ ''' Show the list of sorting rules. '''
+ output = 'Replacement patterns:\n'
+ for i, pattern in enumerate(config.replacements):
+ output += ' {0}: {1} -> {2}\n'.format(i, pattern[0], pattern[1])
+ if not len(config.replacements):
+ output += ' No replacement patterns configured.'
+ log(output, buffer)
+
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_add(buffer, command, args):
+ ''' Add a rule to the rule list. '''
+ pattern, replacement = split_args(args, 1, 1)
+
+ config.replacements.append((pattern, replacement))
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_insert(buffer, command, args):
+ ''' Insert a rule at the desired position in the rule list. '''
+ index, pattern, replacement = split_args(args, 2, 1)
+ index = parse_int(index, 'index')
+
+ config.replacements.insert(index, (pattern, replacement))
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_update(buffer, command, args):
+ ''' Update a rule in the rule list. '''
+ index, pattern, replacement = split_args(args, 2, 1)
+ index = parse_int(index, 'index')
+
+ config.replacements[index] = (pattern, replacement)
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_delete(buffer, command, args):
+ ''' Delete a rule from the rule list. '''
+ index = args.strip()
+ index = parse_int(index, 'index')
+
+ config.replacements.pop(index)
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_move(buffer, command, args):
+ ''' Move a rule to a new position. '''
+ index_a, index_b = split_args(args, 2)
+ index_a = parse_int(index_a, 'index')
+ index_b = parse_int(index_b, 'index')
+
+ config.replacements.move(index_a, index_b)
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+def command_replacement_swap(buffer, command, args):
+ ''' Swap two rules. '''
+ index_a, index_b = split_args(args, 2)
+ index_a = parse_int(index_a, 'index')
+ index_b = parse_int(index_b, 'index')
+
+ config.replacements.swap(index_a, index_b)
+ config.save_replacements()
+ command_replacement_list(buffer, command, '')
+ return weechat.WEECHAT_RC_OK
+
+
+
+
+def call_command(buffer, command, args, subcommands):
+ ''' Call a subccommand from a dictionary. '''
+ subcommand, tail = pad(args.split(' ', 1), 2, '')
+ subcommand = subcommand.strip()
+ if (subcommand == ''):
+ child = subcommands.get(' ')
+ else:
+ command = command + [subcommand]
+ child = subcommands.get(subcommand)
+
+ if isinstance(child, dict):
+ return call_command(buffer, command, tail, child)
+ elif callable(child):
+ return child(buffer, command, tail)
+
+ log('{0}: command not found'.format(' '.join(command)))
+ return weechat.WEECHAT_RC_ERROR
+
+
+def on_buffers_changed(*args, **kwargs):
+ ''' Called whenever the buffer list changes. '''
+ buffers = get_buffers()
+ buffers.sort(key=buffer_sort_key(config.rules))
+ apply_buffer_order(buffers)
+ return weechat.WEECHAT_RC_OK
+
+
+def on_config_changed(*args, **kwargs):
+ ''' Called whenever the configuration changes. '''
+ config.reload()
+
+ # Unhook all signals and hook the new ones.
+ for hook in hooks:
+ weechat.unhook(hook)
+ for signal in config.signals:
+ hooks.append(weechat.hook_signal(signal, 'on_buffers_changed', ''))
+
+ if config.sort_on_config:
+ on_buffers_changed()
+
+ return weechat.WEECHAT_RC_OK
+
+
+def on_autosort_command(data, buffer, args):
+ ''' Called when the autosort command is invoked. '''
+ try:
+ return call_command(buffer, ['/autosort'], args, {
+ ' ': command_sort,
+ 'sort': command_sort,
+
+ 'rules': {
+ ' ': command_rule_list,
+ 'list': command_rule_list,
+ 'add': command_rule_add,
+ 'insert': command_rule_insert,
+ 'update': command_rule_update,
+ 'delete': command_rule_delete,
+ 'move': command_rule_move,
+ 'swap': command_rule_swap,
+ },
+ 'replacements': {
+ ' ': command_replacement_list,
+ 'list': command_replacement_list,
+ 'add': command_replacement_add,
+ 'insert': command_replacement_insert,
+ 'update': command_replacement_update,
+ 'delete': command_replacement_delete,
+ 'move': command_replacement_move,
+ 'swap': command_replacement_swap,
+ },
+ 'sort': on_buffers_changed,
+ })
+ except HumanReadableError as e:
+ log(e, buffer)
+ return weechat.WEECHAT_RC_ERROR
+
+
+command_description = r'''
+NOTE: For the best effect, you may want to consider setting the option irc.look.server_buffer to independent and buffers.look.indenting to on.
+
+# Commands
+
+## Miscellaneous
+/autosort sort
+Manually trigger the buffer sorting.
+
+
+## Sorting rules
+
+/autosort rules list
+Print the list of sort rules.
+
+/autosort rules add <pattern> = <score>
+Add a new rule at the end of the list.
+
+/autosort rules insert <index> <pattern> = <score>
+Insert a new rule at the given index in the list.
+
+/autosort rules update <index> <pattern> = <score>
+Update a rule in the list with a new pattern and score.
+
+/autosort rules delete <index>
+Delete a rule from the list.
+
+/autosort rules move <index_from> <index_to>
+Move a rule from one position in the list to another.
+
+/autosort rules swap <index_a> <index_b>
+Swap two rules in the list
+
+
+## Replacement patterns
+
+/autosort replacements list
+Print the list of replacement patterns.
+
+/autosort replacements add <pattern> <replacement>
+Add a new replacement pattern at the end of the list.
+
+/autosort replacements insert <index> <pattern> <replacement>
+Insert a new replacement pattern at the given index in the list.
+
+/autosort replacements update <index> <pattern> <replacement>
+Update a replacement pattern in the list.
+
+/autosort replacements delete <index>
+Delete a replacement pattern from the list.
+
+/autosort replacements move <index_from> <index_to>
+Move a replacement pattern from one position in the list to another.
+
+/autosort replacements swap <index_a> <index_b>
+Swap two replacement pattern in the list
+
+
+# Introduction
+Autosort is a weechat script to automatically keep your buffers sorted.
+The sort order can be customized by defining your own sort rules,
+but the default should be sane enough for most people.
+It can also group IRC channel/private buffers under their server buffer if you like.
+
+Autosort first turns buffer names into a list of their components by splitting on them on the period character.
+For example, the buffer name "irc.server.freenode" is turned into ['irc', 'server', 'freenode'].
+The list of buffers is then lexicographically sorted.
+
+To facilitate custom sort orders, it is possible to assign a score to each component individually before the sorting is done.
+Any name component that did not get a score assigned will be sorted after those that did receive a score.
+Components are always sorted on their score first and on their name second.
+Lower scores are sorted first.
+
+## Automatic or manual sorting
+By default, autosort will automatically sort your buffer list whenever a buffer is opened, merged, unmerged or renamed.
+This should keep your buffers sorted in almost all situations.
+However, you may wish to change the list of signals that cause your buffer list to be sorted.
+Simply edit the "autosort.sorting.signals" option to add or remove any signal you like.
+If you remove all signals you can still sort your buffers manually with the "/autosort sort" command.
+To prevent all automatic sorting, "autosort.sorting.sort_on_config_change" should also be set to off.
+
+## Grouping IRC buffers
+In weechat, IRC channel/private buffers are named "irc.<network>.<#channel>",
+and IRC server buffers are named "irc.server.<network>".
+This does not work very well with lexicographical sorting if you want all buffers for one network grouped together.
+That is why autosort comes with the "autosort.sorting.group_irc" option,
+which secretly pretends IRC channel/private buffers are called "irc.server.<network>.<#channel>".
+The buffers are not actually renamed, autosort simply pretends they are for sorting purposes.
+
+## Replacement patterns
+Sometimes you may want to ignore some characters for sorting purposes.
+On Freenode for example, you may wish to ignore the difference between channels starting with a double or a single hash sign.
+To do so, simply add a replacement pattern that replaces ## with # with the following command:
+/autosort replacements add ## #
+
+Replacement patterns do not support wildcards or special characters at the moment.
+
+## Sort rules
+You can assign scores to name components by defining sort rules.
+The first rule that matches a component decides the score.
+Further rules are not examined.
+Sort rules use the following syntax:
+<glob-pattern> = <score>
+
+You can use the "/autosort rules" command to show and manipulate the list of sort rules.
+
+
+Allowed special characters in the glob patterns are:
+
+Pattern | Meaning
+--------|--------
+* | Matches a sequence of any characters except for periods.
+? | Matches a single character, but not a period.
+[a-z] | Matches a single character in the given regex-like character class.
+[^ab] | A negated regex-like character class.
+\* | A backslash escapes the next characters and removes its special meaning.
+\\ | A literal backslash.
+
+
+## Example
+As an example, consider the following rule list:
+0: core = 0
+1: irc = 2
+2: * = 1
+
+3: irc.server.*.#* = 1
+4: irc.server.*.* = 0
+
+Rule 0 ensures the core buffer is always sorted first.
+Rule 1 sorts IRC buffers last and rule 2 puts all remaining buffers in between the two.
+
+Rule 3 and 4 would make no sense with the group_irc option off.
+With the option on though, these rules will sort private buffers before regular channel buffers.
+Rule 3 matches channel buffers and assigns them a higher score,
+while rule 4 matches the buffers that remain and assigns them a lower score.
+The same effect could also be achieved with a single rule:
+irc.server.*.[^#]* = 0
+'''
+
+command_completion = 'sort||rules list|add|insert|update|delete|move|swap||replacements list|add|insert|update|delete|move|swap'
+
+
+if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
+ config = Config('autosort')
+
+ weechat.hook_config('autosort.*', 'on_config_changed', '')
+ weechat.hook_command('autosort', command_description, '', '', command_completion, 'on_autosort_command', 'NULL')
+ on_config_changed()
diff --git a/weechat/python/bufsave.py b/weechat/python/bufsave.py
new file mode 100755
index 0000000..0591127
--- /dev/null
+++ b/weechat/python/bufsave.py
@@ -0,0 +1,113 @@
+''' Buffer saver '''
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2009 by xt <tor@bash.no>
+# Copyright (c) 2012 by Sebastien Helleu <flashcode@flashtux.org>
+# Based on bufsave.pl for 0.2.x by FlashCode
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+#
+# Save content of current buffer in a file.
+# (this script requires WeeChat 0.3.0 or newer)
+#
+# History:
+# 2012-08-28, Sebastien Helleu <flashcode@flashtux.org>:
+# version 0.3: compatibility with WeeChat >= 0.3.9 (hdata_time is now long instead of string)
+# 2012-08-23, Sebastien Helleu <flashcode@flashtux.org>:
+# version 0.2: use hdata for WeeChat >= 0.3.6 (improve performance)
+# 2009-06-10, xt <tor@bash.no>
+# version 0.1: initial release
+#
+import weechat as w
+from os.path import exists
+import time
+
+SCRIPT_NAME = "bufsave"
+SCRIPT_AUTHOR = "xt <xt@bash.no>"
+SCRIPT_VERSION = "0.3"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Save buffer to a file"
+SCRIPT_COMMAND = SCRIPT_NAME
+
+
+if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
+ w.hook_command(SCRIPT_COMMAND,
+ "save current buffer to a file",
+ "[filename]",
+ " filename: target file (must not exist)\n",
+ "%f",
+ "bufsave_cmd",
+ '')
+
+def cstrip(text):
+ ''' Use weechat color strip on text'''
+
+ return w.string_remove_color(text, '')
+
+def bufsave_cmd(data, buffer, args):
+ ''' Callback for /bufsave command '''
+
+ filename = args
+
+ if not filename:
+ w.command('', '/help %s' %SCRIPT_COMMAND)
+ return w.WEECHAT_RC_OK
+
+ if exists(filename):
+ w.prnt('', 'Error: target file already exists!')
+ return w.WEECHAT_RC_OK
+
+ try:
+ fp = open(filename, 'w')
+ except:
+ w.prnt('', 'Error writing to target file!')
+ return w.WEECHAT_RC_OK
+
+ version = w.info_get('version_number', '') or 0
+ if int(version) >= 0x00030600:
+ # use hdata with WeeChat >= 0.3.6 (direct access to data, very fast)
+ 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, 'first_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')
+ # since WeeChat 0.3.9, hdata_time returns long instead of string
+ if not isinstance(date, str):
+ date = time.strftime('%F %T', time.localtime(int(date)))
+ fp.write('%s %s %s\n' %(\
+ date,
+ cstrip(w.hdata_string(hdata_line_data, data, 'prefix')),
+ cstrip(w.hdata_string(hdata_line_data, data, 'message')),
+ ))
+ line = w.hdata_move(hdata_line, line, 1)
+ else:
+ # use infolist with WeeChat <= 0.3.5 (full duplication of lines, slow and uses memory)
+ infolist = w.infolist_get('buffer_lines', buffer, '')
+ while w.infolist_next(infolist):
+ fp.write('%s %s %s\n' %(\
+ w.infolist_time(infolist, 'date'),
+ cstrip(w.infolist_string(infolist, 'prefix')),
+ cstrip(w.infolist_string(infolist, 'message')),
+ ))
+ w.infolist_free(infolist)
+
+ fp.close()
+
+ return w.WEECHAT_RC_OK
diff --git a/weechat/python/clone_scanner.py b/weechat/python/clone_scanner.py
new file mode 100644
index 0000000..e8205c2
--- /dev/null
+++ b/weechat/python/clone_scanner.py
@@ -0,0 +1,514 @@
+# -*- coding: utf-8 -*-
+#
+# Clone Scanner, version 1.3 for WeeChat version 0.3
+# Latest development version: https://github.com/FiXato/weechat_scripts
+#
+# A Clone Scanner that can manually scan channels and
+# automatically scans joins for users on the channel
+# with multiple nicknames from the same host.
+#
+# Upon join by a user, the user's host is compared to the infolist of
+# already connected users to see if they are already online from
+# another nickname. If the user is a clone, it will report it.
+# With the '/clone_scanner scan' command you can manually scan a chan.
+#
+# See /set plugins.var.python.clone_scanner.* for all possible options
+# Use the brilliant iset.pl plugin (/weeget install iset) to see what they do
+# Or check the sourcecode below.
+#
+# Example output for an on-join scan result:
+# 21:32:46 ▬▬▶ FiXato_Odie (FiXato@FiXato.net) has joined #lounge
+# 21:32:46 FiXato_Odie is already on the channel as FiXato!FiXato@FiXato.Net and FiX!FiXaphone@FiXato.net
+#
+# Example output for a manual scan:
+# 21:34:44 fixato.net is online from 3 nicks:
+# 21:34:44 - FiXato!FiXato@FiXato.Net
+# 21:34:44 - FiX!FiXaphone@FiXato.net
+# 21:34:44 - FiXato_Odie!FiXato@FiXato.net
+#
+## History:
+### 2011-09-11: FiXato:
+#
+# * version 0.1: initial release.
+# * Added an on-join clone scan. Any user that joins a channel will be
+# matched against users already on the channel.
+#
+# * version 0.2: manual clone scan
+# * Added a manual clone scan via /clone_scanner scan
+# you can specify a target channel with:
+# /clone_scanner scan #myChannelOnCurrentServer
+# or:
+# /clone_scanner scan Freenode.#myChanOnSpecifiedNetwork
+# * Added completion
+#
+### 2011-09-12: FiXato:
+#
+# * version 0.3: Refactor galore
+# * Refactored some code. Codebase should be DRYer and clearer now.
+# * Manual scan report lists by host instead of nick now.
+# * Case-insensitive host-matching
+# * Bugfixed the infolist memleak.
+# * on-join scanner works again
+# * Output examples added to the comments
+#
+### 2011-09-19
+# * version 0.4: Option galore
+# * Case-insensitive buffer lookup fix.
+# * Made most messages optional through settings.
+# * Made on-join alert and clone report key a bit more configurable.
+# * Added formatting options for on-join alerts.
+# * Added format_message helper method that accepts multiple whitespace-separated weechat.color() options.
+# * Added formatting options for join messages
+# * Added formatting options for clone reports
+# * Added format_from_config helper method that reads the given formatting key from the config
+#
+# * version 0.5: cs_buffer refactoring
+# * dropping the manual cs_create_buffer call in favor for a cs_get_buffer() method
+#
+### 2012-02-10: FiXato:
+#
+# * version 0.6: Stop shoving that buffer in my face!
+# * The clone_scanner buffer should no longer pop up by itself when you load the script.
+# It should only pop up now when you actually a line needs to show up in the buffer.
+#
+# * version 0.7: .. but please pop it up in my current window when I ask for it
+# * Added setting plugins.var.python.clone_scanner.autofocus
+# This will autofocus the clone_scanner buffer in the current window if another window isn't
+# already showing it, and of course only when the clone_scanner buffer is triggered
+#
+### 2012-02-10: FiXato:
+#
+# * version 0.8: .. and only when it is first created..
+# * Prevents the buffer from being focused every time there is activity in it and not being shown in a window.
+#
+### 2012-04-01: FiXato:
+#
+# * version 0.9: Hurrah for bouncers...
+# * Added the option plugins.var.python.clone_scanner.compare_idents
+# Set it to 'on' if you don't want people with different idents to be marked as clones.
+# Useful on channels with bouncers.
+#
+### 2012-04-02: FiXato:
+#
+# * version 1.0: Bugfix
+# * Fixed the on-join scanner bug introduced by the 0.9 release.
+# I was not properly comparing the new ident@host.name key in all places yet.
+# Should really have tested this better ><
+#
+### 2012-04-03: FiXato:
+#
+# * version 1.1: Stop being so sensitive!
+# * Continuing to fix the on-join scanner bugs introduced by the 0.9 release.
+# The ident@host.name dict key wasn't being lowercased for comparison in the on-join scan.
+#
+# * version 1.2: So shameless!
+# * Added shameless advertising for my script through /clone_scanner advertise
+#
+### 2013-04-09: FiXato:
+# * version 1.3: Such a killer rabbit
+# * Thanks to Curtis Sorensen aka killerrabbit clone_scanner.py now supports:
+# * local channels (&-prefixed)
+# * nameless channels (just # or &)
+#
+### 2014-12-07: FiXato:
+# * version 1.4: Inefficiency Warning Patch
+# WARNING! I recently noticed the clone_scanner script is currently rather inefficient, and requires a rewrite.
+# It may cause nasty lag when there are a lot of concurrent joins on the channel, as it evaluates the nicklist on every join.
+# For servers like twitch.tv and bitlbee, you might want to exclude the server with the new setting:
+# /set plugins.var.python.clone_scanner.hooks.excluded_servers twitchtv,bitlbee
+# This script update is an emergency update to add the above option, and warn the users of this script.
+# You can disable this warning with: /set plugins.var.python.clone_scanner.lag_warning off
+#
+# * Other patches in this version include:
+# * Re-did how settings are handled, using nils_2's skeleton code
+# * Settings are now automatically memoized when changed
+# * Added options hooks.excluded_servers, hooks.explicit_servers and lag_warning
+# * Updated advertise option to include /script install clone_scanner.py
+# * Updated min version to 0.3.6, though this will prob change in the next version
+#
+## Acknowledgements:
+# * Sebastien "Flashcode" Helleu, for developing the kick-ass chat/IRC
+# client WeeChat
+# * ArZa, whose kickban.pl script helped me get started with using the
+# infolist results.
+# * LayBot, for requesting the ident comparison
+# * Curtis "killerrabbit" Sorensen, for sending in two pull-requests,
+# adding support for local and nameless channels.
+# * nils_2 aka weechatter, for providing the excellent skeleton.py script
+#
+## TODO:
+# - REWRITE TO IMPROVE EFFICIENCY:
+# - Probably only do the infolist loop on self-join,
+# and from then on manually keep track of join/parts/quit/nick-changes
+# - Add option to enable/disable public clone reporting aka msg channels
+# - Add option to enable/disable scanning on certain channels
+# - Add cross-channel clone scan
+# - Add cross-server clone scan
+#
+## Copyright (c) 2011-2014 Filip H.F. "FiXato" Slagter,
+# <FiXato [at] Gmail [dot] com>
+# http://profile.fixato.org
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NON-INFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+SCRIPT_NAME = "clone_scanner"
+SCRIPT_AUTHOR = "Filip H.F. 'FiXato' Slagter <fixato [at] gmail [dot] com>"
+SCRIPT_VERSION = "1.4"
+SCRIPT_LICENSE = "MIT"
+SCRIPT_DESC = "A Clone Scanner that can manually scan channels and automatically scans joins for users on the channel with multiple nicknames from the same host."
+SCRIPT_COMMAND = "clone_scanner"
+SCRIPT_CLOSE_CB = "cs_close_cb"
+
+import_ok = True
+
+try:
+ import weechat
+except ImportError:
+ print "This script must be run under WeeChat."
+ import_ok = False
+
+import re
+cs_buffer = None
+OPTIONS = {
+ "autofocus": ("on", "Focus the clone_scanner buffer in the current window if it isn't already displayed by a window."),
+ "compare_idents": ("off", "Match against ident@host.name instead of just the hostname. Useful if you don't want different people from bouncers marked as clones"),
+ "display_join_messages": ("off", "Display all joins in the clone_scanner buffer"),
+ "display_onjoin_alert_clone_buffer": ("on", "Display an on-join clone alert in the clone_scanner buffer"),
+ "display_onjoin_alert_target_buffer": ("on", "Display an on-join clone alert in the buffer where the clone was detected"),
+ "display_onjoin_alert_current_buffer": ("off", "Display an on-join clone alert in the current buffer"),
+ "display_scan_report_clone_buffer": ("on", "Display manual scan reports in the clone buffer"),
+ "display_scan_report_target_buffer": ("off", "Display manual scan reports in the buffer of the scanned channel"),
+ "display_scan_report_current_buffer": ("on", "Display manual scan reports in the current buffer"),
+
+ "clone_report_key": ("mask", "Which 'key' to display in the clone report: 'mask' for full hostmasks, or 'nick' for nicks"),
+ "clone_onjoin_alert_key": ("mask", "Which 'key' to display in the on-join alerts: 'mask' for full hostmasks, or 'nick' for nicks"),
+
+ "colors.onjoin_alert.message": ("red", "The on-join clone alert's message colour. Formats are space separated."),
+ "colors.onjoin_alert.nick": ("bold red", "The on-join clone alert's nick colour. Formats are space separated. Note: if you have colorize_nicks, this option might not work as expected."),
+ "colors.onjoin_alert.channel": ("red", "The on-join clone alert's channel colour. Formats are space separated."),
+ "colors.onjoin_alert.matches": ("bold red", "The on-join clone alert's matches (masks or nicks) colour. Formats are space separated. Note: if you have colorize_nicks, this option might not work as expected."),
+
+ "colors.join_messages.message": ("chat", "The base colour for the join messages."),
+ "colors.join_messages.nick": ("bold", "The colour for the 'nick'-part of the join messages. Note: if you have colorize_nicks, this option might not always work as expected."),
+ "colors.join_messages.identhost": ("chat", "The colour for the 'ident@host'-part of the join messages."),
+ "colors.join_messages.channel": ("bold", "The colour for the 'channel'-part of the join messages."),
+
+ "colors.clone_report.header.message": ("chat", "The colour of the clone report header."),
+ "colors.clone_report.header.number_of_hosts": ("bold", "The colour of the number of hosts in the clone report header."),
+ "colors.clone_report.header.channel": ("bold", "The colour of the channel name in the clone report header."),
+
+ "colors.clone_report.subheader.message": ("chat", "The colour of the clone report subheader."),
+ "colors.clone_report.subheader.host": ("bold", "The colour of the host in the clone report subheader."),
+ "colors.clone_report.subheader.number_of_clones": ("bold", "The colour of the number of clones in the clone report subheader."),
+
+ "colors.clone_report.clone.message": ("chat", "The colour of the clone hit in the clone report message."),
+ "colors.clone_report.clone.match": ("chat", "The colour of the match details (masks or nicks) in the clone report."),
+
+ "colors.mask.nick": ("bold", "The formatting of the nick in the match mask."),
+ "colors.mask.identhost": ("", "The formatting of the identhost in the match mask."),
+ "hooks.explicit_servers": ("*", "Comma-separated, wildcard-supporting list of servers for which we should add hook to for monitoring clones. E.g. 'freenode,chat4all,esper*' or '*' (default)"),
+ "hooks.excluded_servers": ("bitlbee,twitchtv", "Which servers should the hook *not* be valid for? There's no support for wildcards unfortunately. E.g.: 'bitlbee,twitchtv' to exclude servers named bitlbee and twitchtv (default)."),
+ "lag_warning": ('on', 'Show temporary warning upon script load regarding the inefficiency of the script. Set to "off" to disable.')
+}
+hooks = set([])
+
+def get_validated_key_from_config(setting):
+ key = OPTIONS[setting]
+ if key != 'mask' and key != 'nick':
+ weechat.prnt("", "Key %s not found. Valid settings are 'nick' and 'mask'. Reverted the setting to 'mask'" % key)
+ weechat.config_set_plugin("clone_report_key", "mask")
+ key = "mask"
+ return key
+
+def format_message(msg, formats, reset_color='chat'):
+ if type(formats) == str:
+ formats = formats.split()
+ formatted_message = msg
+ needs_color_reset = False
+ for format in formats:
+ if format in ['bold', 'reverse', 'italic', 'underline']:
+ end_format = '-%s' % format
+ else:
+ needs_color_reset = True
+ end_format = ""
+ formatted_message = "%s%s%s" % (weechat.color(format), formatted_message, weechat.color(end_format))
+ if needs_color_reset:
+ formatted_message += weechat.color(reset_color)
+ return formatted_message
+
+def format_from_config(msg, config_option):
+ return format_message(msg, OPTIONS[config_option])
+
+def on_join_scan_cb(data, signal, signal_data):
+ network = signal.split(',')[0]
+ if network in OPTIONS['hooks.excluded_servers'].split(','):
+ return weechat.WEECHAT_RC_OK
+
+ joined_nick = weechat.info_get("irc_nick_from_host", signal_data)
+ join_match_data = re.match(':[^!]+!([^@]+@(\S+)) JOIN :?([#&]\S*)', signal_data)
+ parsed_ident_host = join_match_data.group(1).lower()
+ parsed_host = join_match_data.group(2).lower()
+ if OPTIONS["compare_idents"] == "on":
+ hostkey = parsed_ident_host
+ else:
+ hostkey = parsed_host
+
+ chan_name = join_match_data.group(3)
+ network_chan_name = "%s.%s" % (network, chan_name)
+ chan_buffer = weechat.info_get("irc_buffer", "%s,%s" % (network, chan_name))
+ if not chan_buffer:
+ print "No IRC channel buffer found for %s" % network_chan_name
+ return weechat.WEECHAT_RC_OK
+
+ if OPTIONS["display_join_messages"] == "on":
+ message = "%s%s%s%s%s" % (
+ format_from_config(joined_nick, "colors.join_messages.nick"),
+ format_from_config("!", "colors.join_messages.message"),
+ format_from_config(parsed_ident_host, "colors.join_messages.identhost"),
+ format_from_config(" JOINed ", "colors.join_messages.message"),
+ format_from_config(network_chan_name, "colors.join_messages.channel"),
+ )
+ #Make sure message format is also applied if no formatting is given for nick
+ message = format_from_config(message, "colors.join_messages.message")
+ weechat.prnt(cs_get_buffer(), message)
+
+ clones = get_clones_for_buffer("%s,%s" % (network, chan_name), hostkey)
+ if clones:
+ key = get_validated_key_from_config("clone_onjoin_alert_key")
+
+ filtered_clones = filter(lambda clone: clone['nick'] != joined_nick, clones[hostkey])
+ match_strings = map(lambda m: format_from_config(m[key], "colors.onjoin_alert.matches"), filtered_clones)
+
+ join_string = format_from_config(' and ',"colors.onjoin_alert.message")
+ masks = join_string.join(match_strings)
+ message = "%s %s %s %s %s" % (
+ format_from_config(joined_nick, "colors.onjoin_alert.nick"),
+ format_from_config("is already on", "colors.onjoin_alert.message"),
+ format_from_config(network_chan_name, "colors.onjoin_alert.channel"),
+ format_from_config("as", "colors.onjoin_alert.message"),
+ masks
+ )
+ message = format_from_config(message, 'colors.onjoin_alert.message')
+
+ if OPTIONS["display_onjoin_alert_clone_buffer"] == "on":
+ weechat.prnt(cs_get_buffer(),message)
+ if OPTIONS["display_onjoin_alert_target_buffer"] == "on":
+ weechat.prnt(chan_buffer, message)
+ if OPTIONS["display_onjoin_alert_current_buffer"] == "on":
+ weechat.prnt(weechat.current_buffer(),message)
+ return weechat.WEECHAT_RC_OK
+
+def cs_get_buffer():
+ global cs_buffer
+
+ if not cs_buffer:
+ # Sets notify to 0 as this buffer does not need to be in hotlist.
+ cs_buffer = weechat.buffer_new("clone_scanner", "", \
+ "", SCRIPT_CLOSE_CB, "")
+ weechat.buffer_set(cs_buffer, "title", "Clone Scanner")
+ weechat.buffer_set(cs_buffer, "notify", "0")
+ weechat.buffer_set(cs_buffer, "nicklist", "0")
+ if OPTIONS["autofocus"] == "on":
+ if not weechat.window_search_with_buffer(cs_buffer):
+ weechat.command("", "/buffer " + weechat.buffer_get_string(cs_buffer,"name"))
+
+ return cs_buffer
+
+def cs_close_cb(*kwargs):
+ """ A callback for buffer closing. """
+ global cs_buffer
+
+ #TODO: Ensure the clone_scanner buffer gets closed if its option is set and the script unloads
+
+ cs_buffer = None
+ return weechat.WEECHAT_RC_OK
+
+
+def get_channel_from_buffer_args(buffer, args):
+ server_name = weechat.buffer_get_string(buffer, "localvar_server")
+ channel_name = args
+ if not channel_name:
+ channel_name = weechat.buffer_get_string(buffer, "localvar_channel")
+
+ match_data = re.match('\A(irc.)?([^.]+)\.([#&]\S*)\Z', channel_name)
+ if match_data:
+ channel_name = match_data.group(3)
+ server_name = match_data.group(2)
+
+ return server_name, channel_name
+
+#TODO: track the hosts + nicks ourselves instead of looking up the entire list every join...
+def get_clones_for_buffer(infolist_buffer_name, hostname_to_match=None):
+ matches = {}
+ infolist = weechat.infolist_get("irc_nick", "", infolist_buffer_name)
+ while(weechat.infolist_next(infolist)):
+ ident_hostname = weechat.infolist_string(infolist, "host")
+ host_matchdata = re.match('([^@]+)@(\S+)', ident_hostname)
+ if not host_matchdata:
+ continue
+
+ hostname = host_matchdata.group(2).lower()
+ ident = host_matchdata.group(1).lower()
+ if OPTIONS["compare_idents"] == "on":
+ hostkey = ident_hostname.lower()
+ else:
+ hostkey = hostname
+
+ if hostname_to_match and hostname_to_match.lower() != hostkey:
+ continue
+
+ nick = weechat.infolist_string(infolist, "name")
+
+ matches.setdefault(hostkey,[]).append({
+ 'nick': nick,
+ 'mask': "%s!%s" % (
+ format_from_config(nick, "colors.mask.nick"),
+ format_from_config(ident_hostname, "colors.mask.identhost")),
+ 'ident': ident,
+ 'ident_hostname': ident_hostname,
+ 'hostname': hostname,
+ })
+ weechat.infolist_free(infolist)
+
+ #Select only the results that have more than 1 match for a host
+ return dict((k, v) for (k, v) in matches.iteritems() if len(v) > 1)
+
+def report_clones(clones, scanned_buffer_name, target_buffer=None):
+ # Default to clone_scanner buffer
+ if not target_buffer:
+ target_buffer = cs_get_buffer()
+
+ if clones:
+ clone_report_header = "%s %s %s%s" % (
+ format_from_config(len(clones), "colors.clone_report.header.number_of_hosts"),
+ format_from_config("hosts with clones were found on", "colors.clone_report.header.message"),
+ format_from_config(scanned_buffer_name, "colors.clone_report.header.channel"),
+ format_from_config(":", "colors.clone_report.header.message"),
+ )
+ clone_report_header = format_from_config(clone_report_header, "colors.clone_report.header.message")
+ weechat.prnt(target_buffer, clone_report_header)
+
+ for (host, clones) in clones.iteritems():
+ host_message = "%s %s %s %s" % (
+ format_from_config(host, "colors.clone_report.subheader.host"),
+ format_from_config("is online from", "colors.clone_report.subheader.message"),
+ format_from_config(len(clones), "colors.clone_report.subheader.number_of_clones"),
+ format_from_config("nicks:", "colors.clone_report.subheader.message"),
+ )
+ host_message = format_from_config(host_message, "colors.clone_report.subheader.message")
+ weechat.prnt(target_buffer, host_message)
+
+ for user in clones:
+ key = get_validated_key_from_config("clone_report_key")
+ clone_message = "%s%s" % (" - ", format_from_config(user[key], "colors.clone_report.clone.match"))
+ clone_message = format_from_config(clone_message,"colors.clone_report.clone.message")
+ weechat.prnt(target_buffer, clone_message)
+ else:
+ weechat.prnt(target_buffer, "No clones found on %s" % scanned_buffer_name)
+
+def cs_command_main(data, buffer, args):
+ if args[0:4] == 'scan':
+ server_name, channel_name = get_channel_from_buffer_args(buffer, args[5:])
+ clones = get_clones_for_buffer('%s,%s' % (server_name, channel_name))
+ if OPTIONS["display_scan_report_target_buffer"] == "on":
+ target_buffer = weechat.info_get("irc_buffer", "%s,%s" % (server_name, channel_name))
+ report_clones(clones, '%s.%s' % (server_name, channel_name), target_buffer)
+ if OPTIONS["display_scan_report_clone_buffer"] == "on":
+ report_clones(clones, '%s.%s' % (server_name, channel_name))
+ if OPTIONS["display_scan_report_current_buffer"] == "on":
+ report_clones(clones, '%s.%s' % (server_name, channel_name), weechat.current_buffer())
+ elif args[0:9] == 'advertise':
+ weechat.command("", "/input insert /me is using FiXato's CloneScanner v%s for WeeChat. Get the latest version from: https://github.com/FiXato/weechat_scripts/blob/master/clone_scanner.py or /script install clone_scanner.py" % SCRIPT_VERSION)
+ return weechat.WEECHAT_RC_OK
+
+def cs_set_default_settings():
+ global OPTIONS
+
+ # Set default settings
+ for option,value in OPTIONS.items():
+ if not weechat.config_is_set_plugin(option):
+ weechat.config_set_plugin(option, value[0])
+ OPTIONS[option] = value[0]
+ else:
+ OPTIONS[option] = weechat.config_get_plugin(option)
+ weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0]))
+
+def toggle_refresh(pointer, name, value):
+ global OPTIONS
+
+ config_option = name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname
+ OPTIONS[config_option] = value # save new value
+ if config_option in ('hooks.excluded_servers', 'hooks.explicit_servers'):
+ remove_hooks()
+ add_hooks()
+ weechat.config_set_plugin(config_option, value)
+ return weechat.WEECHAT_RC_OK
+
+def add_hooks():
+ global hooks
+ hooked_servers = OPTIONS['hooks.explicit_servers'].split(',')
+ for server_name in hooked_servers:
+ signal = "%s,irc_in2_join" % server_name
+ # weechat.prnt('', "Adding hook on %s" % signal)
+ hook = weechat.hook_signal(signal, "on_join_scan_cb", "")
+ hooks.add(hook)
+
+def remove_hooks():
+ global hooks
+ for hook in hooks:
+ weechat.unhook(hook)
+ hooks = set([])
+
+if __name__ == "__main__" and import_ok:
+ if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, SCRIPT_CLOSE_CB, ""):
+ version = weechat.info_get("version_number", "") or 0
+ if int(version) >= 0x00030600:
+ if (not weechat.config_is_set_plugin('lag_warning') or weechat.config_get_plugin('lag_warning') == 'on'):
+ weechat.prnt('', '%s%sWARNING! This %s script is currently rather inefficient, and requires a rewrite.' % (weechat.prefix('error'), weechat.color('red'), SCRIPT_NAME))
+ weechat.prnt('', '%s It may cause nasty lag when there are a lot of concurrent joins on the channel, as it evaluates the nicklist on every join.' % weechat.prefix('notice'))
+ weechat.prnt('', '%s For servers like twitch.tv and bitlbee, you might want to exclude the server with the new setting:' % weechat.prefix('notice'))
+ weechat.prnt('', '%s %s /set plugins.var.python.clone_scanner.hooks.excluded_servers twitchtv,bitlbee' % (weechat.prefix("notice"), weechat.color('*white')))
+ weechat.prnt('', '%s This script update is an emergency update to add the above option, and warn the users of this script.' % weechat.prefix("notice"))
+ weechat.prnt('', '%s You can disable this warning with:%s /set plugins.var.python.clone_scanner.lag_warning off' % (weechat.prefix("notice"), weechat.color('*white')))
+
+ cs_set_default_settings()
+ cs_buffer = weechat.buffer_search("python", "clone_scanner")
+
+ weechat.hook_config( 'plugins.var.python.%s.*' % SCRIPT_NAME, 'toggle_refresh', '' )
+ add_hooks()
+
+ weechat.hook_command(SCRIPT_COMMAND,
+ SCRIPT_DESC,
+ "[scan] [[plugin.][network.]channel] | [advertise] | [help]",
+ "the target_buffer can be: \n"
+ "- left out, so the current channel buffer will be scanned.\n"
+ "- a plain channel name, such as #weechat, in which case it will prefixed with the current network name\n"
+ "- a channel name prefixed with network name, such as Freenode.#weechat\n"
+ "- a channel name prefixed with plugin and network name, such as irc.freenode.#weechat\n"
+ "See /set plugins.var.python.clone_scanner.* for all possible configuration options",
+
+ " || scan %(buffers_names)"
+ " || advertise"
+ " || help",
+
+ "cs_command_main", "")
+ else:
+ weechat.prnt("","%s%s %s" % (weechat.prefix("error"),SCRIPT_NAME,": needs version 0.3.6 or higher"))
+ weechat.command("","/wait 1ms /python unload %s" % SCRIPT_NAME)
diff --git a/weechat/python/confversion.py b/weechat/python/confversion.py
new file mode 100644
index 0000000..d41a357
--- /dev/null
+++ b/weechat/python/confversion.py
@@ -0,0 +1,124 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2010-2010 by drubin <drubin at smartcube.co.za>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+# Allows you to visually see if there are updates to your weechat system
+
+#Versions
+# 0.1 drubin - First release.
+# - Basic functionality to save version history of your config files (only git, bzr)
+# 0.2 ShockkPony - Fixed massive weechat startup time caused by initial config loading
+
+SCRIPT_NAME = "confversion"
+SCRIPT_AUTHOR = "drubin <drubin at smartcube.co.za>"
+SCRIPT_VERSION = "0.2"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Stores version controlled history of your configuration files"
+
+import_ok = True
+import subprocess
+try:
+ import weechat
+except ImportError:
+ print "This script must be run under WeeChat."
+ print "Get WeeChat now at: http://www.weechat.org/"
+ import_ok = False
+
+
+# script options
+settings = {
+ #Currently supports git and bzr and possibly other that support simple "init" "add *.conf" "commit -m "message" "
+ "versioning_method" : "git",
+ "commit_each_change" : "true",
+ "commit_message" : "Commiting changes",
+ #Allows you to not auto commit stuff that relates to these configs
+ #, (comma) seperated list of config options
+ #The toggle_nicklist script can make this property annoying.
+ "auto_commit_ignore" : "weechat.bar.nicklist.hidden",
+}
+
+def shell_in_home(cmd):
+ try:
+ output = file("/dev/null","w")
+ subprocess.Popen(ver_method()+" "+cmd, cwd = weechat_home(),
+ stdout= output, stderr=output, shell=True)
+ except Exception as e:
+ print e
+
+def weechat_home():
+ return weechat.info_get ("weechat_dir", "")
+
+def ver_method():
+ return weechat.config_get_plugin("versioning_method")
+
+def init_repo():
+ #Set up version control (doesn't matter if previously setup for bzr, git)
+ shell_in_home("init")
+ #Save first import OR on start up if needed.
+ commit_cb()
+
+confversion_commit_finish_hook = 0
+
+def commit_cb(data=None, remaning=None):
+ global confversion_commit_finish_hook
+
+ # only hook timer if not already hooked
+ if confversion_commit_finish_hook == 0:
+ confversion_commit_finish_hook = weechat.hook_timer(500, 0, 1, "commit_cb_finish", "")
+
+ return weechat.WEECHAT_RC_OK
+
+def commit_cb_finish(data=None, remaining=None):
+ global confversion_commit_finish_hook
+
+ # save before doing commit
+ weechat.command("","/save")
+
+ # add all config changes to git
+ shell_in_home("add ./*.conf")
+
+ # do the commit
+ shell_in_home("commit -m \"%s\"" % weechat.config_get_plugin("commit_message"))
+
+ # set hook back to 0
+ confversion_commit_finish_hook = 0
+
+ return weechat.WEECHAT_RC_OK
+
+def conf_update_cb(data, option, value):
+ #Commit data if not part of ignore list.
+ if weechat.config_get_plugin("commit_each_change") == "true" and not option in weechat.config_get_plugin("auto_commit_ignore").split(","):
+ #Call use pause else /save will be called before the config is actually saved to disc
+ #This is kinda hack but better input would be appricated.
+ weechat.hook_timer(500, 0, 1, "commit_cb", "")
+ return weechat.WEECHAT_RC_OK
+
+def confversion_cmd(data, buffer, args):
+ commit_cb()
+ return weechat.WEECHAT_RC_OK
+
+if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
+ SCRIPT_DESC, "", ""):
+ for option, default_value in settings.iteritems():
+ if weechat.config_get_plugin(option) == "":
+ weechat.config_set_plugin(option, default_value)
+
+ weechat.hook_command("confversion", "Saves configurations to version control", "",
+ "",
+ "", "confversion_cmd", "")
+ init_repo()
+ hook = weechat.hook_config("*", "conf_update_cb", "")
diff --git a/weechat/python/country.py b/weechat/python/country.py
new file mode 100644
index 0000000..530aa79
--- /dev/null
+++ b/weechat/python/country.py
@@ -0,0 +1,577 @@
+# -*- coding: utf-8 -*-
+###
+# Copyright (c) 2009-2011 by Elián Hanisch <lambdae2@gmail.com>
+# Copyright (c) 2013 by Filip H.F. "FiXato" Slagter <fixato+weechat@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+###
+# Prints user's country and local time information in
+# whois/whowas replies (for WeeChat 0.3.*)
+#
+# This script uses MaxMind's GeoLite database from
+# http://www.maxmind.com/app/geolitecountry
+#
+# This script depends in pytz third party module for retrieving
+# timezone information for a given country. Without it the local time
+# for a user won't be displayed.
+# Get it from http://pytz.sourceforge.net or from your distro packages,
+# python-tz in Ubuntu/Debian
+#
+# Commands:
+# * /country
+# Prints country for a given ip, uri or nick. See /help country
+#
+# Settings:
+# * plugins.var.python.country.show_in_whois:
+# If 'off' /whois or /whowas replies won't contain country information.
+# Valid values: on, off
+# * plugins.var.python.country.show_localtime:
+# If 'off' timezone and local time infomation won't be looked for.
+# Valid values: on, off
+#
+#
+# TODO
+# * Add support for IPv6 addresses
+#
+#
+# History:
+# 2013-04-28
+# version 0.6:
+# * Improved support for target msgbuffer. Takes the following settings into account:
+# - irc.msgbuffer.whois
+# - irc.msgbuffer.$servername.whois
+# - irc.look.msgbuffer_fallback
+#
+# 2011-08-14
+# version 0.5:
+# * make time format configurable.
+# * print to private buffer based on msgbuffer setting.
+#
+# 2011-01-09
+# version 0.4.1: bug fixes
+#
+# 2010-11-15
+# version 0.4:
+# * support for users using webchat (at least in freenode)
+# * enable Archlinux workaround.
+#
+# 2010-01-11
+# version 0.3.1: bug fix
+# * irc_nick infolist wasn't freed in get_host_by_nick()
+#
+# 2009-12-12
+# version 0.3: update WeeChat site.
+#
+# 2009-09-17
+# version 0.2: added timezone and local time information.
+#
+# 2009-08-24
+# version 0.1.1: fixed python 2.5 compatibility.
+#
+# 2009-08-21
+# version 0.1: initial release.
+#
+###
+
+SCRIPT_NAME = "country"
+SCRIPT_AUTHOR = "Elián Hanisch <lambdae2@gmail.com>"
+SCRIPT_VERSION = "0.6"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Prints user's country and local time in whois replies"
+SCRIPT_COMMAND = "country"
+
+try:
+ import weechat
+ from weechat import WEECHAT_RC_OK, prnt
+ import_ok = True
+except ImportError:
+ print "This script must be run under WeeChat."
+ print "Get WeeChat now at: http://www.weechat.org/"
+ import_ok = False
+
+try:
+ import pytz, datetime
+ pytz_module = True
+except:
+ pytz_module = False
+
+import os, re, socket
+
+### ip database
+database_url = 'http://geolite.maxmind.com/download/geoip/database/GeoIPCountryCSV.zip'
+database_file = 'GeoIPCountryWhois.csv'
+
+### config
+settings = {
+ 'time_format': '%x %X %Z',
+ 'show_in_whois': 'on',
+ 'show_localtime': 'on'
+ }
+
+boolDict = {'on':True, 'off':False}
+def get_config_boolean(config):
+ value = weechat.config_get_plugin(config)
+ try:
+ return boolDict[value]
+ except KeyError:
+ default = settings[config]
+ error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
+ error("'%s' is invalid, allowed: 'on', 'off'" %value)
+ return boolDict[default]
+
+### messages
+
+script_nick = SCRIPT_NAME
+def error(s, buffer=''):
+ """Error msg"""
+ prnt(buffer, '%s%s %s' % (weechat.prefix('error'), script_nick, s))
+ if weechat.config_get_plugin('debug'):
+ import traceback
+ if traceback.sys.exc_type:
+ trace = traceback.format_exc()
+ prnt('', trace)
+
+def say(s, buffer=''):
+ """normal msg"""
+ prnt(buffer, '%s\t%s' % (script_nick, s))
+
+def whois(nick, string, buffer=''):
+ """Message formatted like a whois reply."""
+ prefix_network = weechat.prefix('network')
+ color_delimiter = weechat.color('chat_delimiters')
+ color_nick = weechat.color('chat_nick')
+ prnt(buffer, '%s%s[%s%s%s] %s' % (prefix_network,
+ color_delimiter,
+ color_nick,
+ nick,
+ color_delimiter,
+ string))
+
+def string_country(country, code):
+ """Format for country info string."""
+ color_delimiter = weechat.color('chat_delimiters')
+ color_chat = weechat.color('chat')
+ return '%s%s %s(%s%s%s)' % (color_chat,
+ country,
+ color_delimiter,
+ color_chat,
+ code,
+ color_delimiter)
+
+def string_time(dt):
+ """Format for local time info string."""
+ if not dt: return '--'
+ color_delimiter = weechat.color('chat_delimiters')
+ color_chat = weechat.color('chat')
+ date = dt.strftime(weechat.config_get_plugin("time_format"))
+ tz = dt.strftime('UTC%z')
+ return '%s%s %s(%s%s%s)' % (color_chat,
+ date,
+ color_delimiter,
+ color_chat,
+ tz,
+ color_delimiter)
+
+### functions
+def get_script_dir():
+ """Returns script's dir, creates it if needed."""
+ script_dir = weechat.info_get('weechat_dir', '')
+ script_dir = os.path.join(script_dir, 'country')
+ if not os.path.isdir(script_dir):
+ os.makedirs(script_dir)
+ return script_dir
+
+ip_database = ''
+def check_database():
+ """Check if there's a database already installed."""
+ global ip_database
+ if not ip_database:
+ ip_database = os.path.join(get_script_dir(), database_file)
+ return os.path.isfile(ip_database)
+
+timeout = 1000*60*10
+hook_download = ''
+def update_database():
+ """Downloads and uncompress the database."""
+ global hook_download, ip_database
+ if not ip_database:
+ check_database()
+ if hook_download:
+ weechat.unhook(hook_download)
+ hook_download = ''
+ script_dir = get_script_dir()
+ say("Downloading IP database...")
+ python_bin = weechat.info_get('python2_bin', '') or 'python'
+ hook_download = weechat.hook_process(
+ python_bin + " -c \"\n"
+ "import urllib2, zipfile, os, sys\n"
+ "try:\n"
+ " temp = os.path.join('%(script_dir)s', 'temp.zip')\n"
+ " try:\n"
+ " zip = urllib2.urlopen('%(url)s', timeout=10)\n"
+ " except TypeError: # python2.5\n"
+ " import socket\n"
+ " socket.setdefaulttimeout(10)\n"
+ " zip = urllib2.urlopen('%(url)s')\n"
+ " fd = open(temp, 'w')\n"
+ " fd.write(zip.read())\n"
+ " fd.close()\n"
+ " print 'Download complete, uncompressing...'\n"
+ " zip = zipfile.ZipFile(temp)\n"
+ " try:\n"
+ " zip.extractall(path='%(script_dir)s')\n"
+ " except AttributeError: # python2.5\n"
+ " fd = open('%(ip_database)s', 'w')\n"
+ " fd.write(zip.read('%(database_file)s'))\n"
+ " fd.close()\n"
+ " os.remove(temp)\n"
+ "except Exception, e:\n"
+ " print >>sys.stderr, e\n\"" % {'url':database_url,
+ 'script_dir':script_dir,
+ 'ip_database':ip_database,
+ 'database_file':database_file
+ },
+ timeout, 'update_database_cb', '')
+
+process_stderr = ''
+def update_database_cb(data, command, rc, stdout, stderr):
+ """callback for our database download."""
+ global hook_download, process_stderr
+ #debug("%s @ stderr: '%s', stdout: '%s'" %(rc, stderr.strip('\n'), stdout.strip('\n')))
+ if stdout:
+ say(stdout)
+ if stderr:
+ process_stderr += stderr
+ if int(rc) >= 0:
+ if process_stderr:
+ error(process_stderr)
+ process_stderr = ''
+ else:
+ say('Success.')
+ hook_download = ''
+ return WEECHAT_RC_OK
+
+hook_get_ip = ''
+def get_ip_process(host):
+ """Resolves host to ip."""
+ # because getting the ip might take a while, we must hook a process so weechat doesn't hang.
+ global hook_get_ip
+ if hook_get_ip:
+ weechat.unhook(hook_get_ip)
+ hook_get_ip = ''
+ python_bin = weechat.info_get('python2_bin', '') or 'python'
+ hook_get_ip = weechat.hook_process(
+ python_bin + " -c \"\n"
+ "import socket, sys\n"
+ "try:\n"
+ " ip = socket.gethostbyname('%(host)s')\n"
+ " print ip\n"
+ "except Exception, e:\n"
+ " print >>sys.stderr, e\n\"" %{'host':host},
+ timeout, 'get_ip_process_cb', '')
+
+def get_ip_process_cb(data, command, rc, stdout, stderr):
+ """Called when uri resolve finished."""
+ global hook_get_ip, reply_wrapper
+ #debug("%s @ stderr: '%s', stdout: '%s'" %(rc, stderr.strip('\n'), stdout.strip('\n')))
+ if stdout and reply_wrapper:
+ code, country = search_in_database(stdout[:-1])
+ reply_wrapper(code, country)
+ reply_wrapper = None
+ if stderr and reply_wrapper:
+ reply_wrapper(*unknown)
+ reply_wrapper = None
+ if int(rc) >= 0:
+ hook_get_ip = ''
+ return WEECHAT_RC_OK
+
+def is_ip(s):
+ """Returns whether or not a given string is an IPV4 address."""
+ try:
+ return bool(socket.inet_aton(s))
+ except socket.error:
+ return False
+
+_valid_label = re.compile(r'^([\da-z]|[\da-z][-\da-z]*[\da-z])$', re.I)
+def is_domain(s):
+ """
+ Checks if 's' is a valid domain."""
+ if not s or len(s) > 255:
+ return False
+ labels = s.split('.')
+ if len(labels) < 2:
+ return False
+ for label in labels:
+ if not label or len(label) > 63 \
+ or not _valid_label.match(label):
+ return False
+ return True
+
+def hex_to_ip(s):
+ """
+ '7f000001' => '127.0.0.1'"""
+ try:
+ ip = map(lambda n: s[n:n+2], range(0, len(s), 2))
+ ip = map(lambda n: int(n, 16), ip)
+ return '.'.join(map(str, ip))
+ except:
+ return ''
+
+def get_userhost_from_nick(buffer, nick):
+ """Return host of a given nick in buffer."""
+ channel = weechat.buffer_get_string(buffer, 'localvar_channel')
+ server = weechat.buffer_get_string(buffer, 'localvar_server')
+ if channel and server:
+ infolist = weechat.infolist_get('irc_nick', '', '%s,%s' %(server, channel))
+ if infolist:
+ try:
+ while weechat.infolist_next(infolist):
+ name = weechat.infolist_string(infolist, 'name')
+ if nick == name:
+ return weechat.infolist_string(infolist, 'host')
+ finally:
+ weechat.infolist_free(infolist)
+ return ''
+
+def get_ip_from_userhost(user, host):
+ ip = get_ip_from_host(host)
+ if ip:
+ return ip
+ ip = get_ip_from_user(user)
+ if ip:
+ return ip
+ return host
+
+def get_ip_from_host(host):
+ if is_domain(host):
+ return host
+ else:
+ if host.startswith('gateway/web/freenode/ip.'):
+ ip = host.split('.', 1)[1]
+ return ip
+
+def get_ip_from_user(user):
+ user = user[-8:] # only interested in the last 8 chars
+ if len(user) == 8:
+ ip = hex_to_ip(user)
+ if ip and is_ip(ip):
+ return ip
+
+def sum_ip(ip):
+ """Converts the ip number from dot-decimal notation to decimal."""
+ L = map(int, ip.split('.'))
+ return L[0]*16777216 + L[1]*65536 + L[2]*256 + L[3]
+
+unknown = ('--', 'unknown')
+def search_in_database(ip):
+ """
+ search_in_database(ip_number) => (code, country)
+ returns ('--', 'unknown') if nothing found
+ """
+ import csv
+ global ip_database
+ if not ip or not ip_database:
+ return unknown
+ try:
+ # do a binary search.
+ n = sum_ip(ip)
+ fd = open(ip_database)
+ reader = csv.reader(fd)
+ max = os.path.getsize(ip_database)
+ last_high = last_low = min = 0
+ while True:
+ mid = (max + min)/2
+ fd.seek(mid)
+ fd.readline() # move cursor to next line
+ _, _, low, high, code, country = reader.next()
+ if low == last_low and high == last_high:
+ break
+ if n < long(low):
+ max = mid
+ elif n > long(high):
+ min = mid
+ elif n > long(low) and n < long(high):
+ return (code, country)
+ else:
+ break
+ last_low, last_high = low, high
+ except StopIteration:
+ pass
+ return unknown
+
+def print_country(host, buffer, quiet=False, broken=False, nick=''):
+ """
+ Prints country and local time for a given host, if quiet is True prints only if there's a match,
+ if broken is True reply will be split in two messages.
+ """
+ #debug('host: ' + host)
+ def reply_country(code, country):
+ if quiet and code == '--':
+ return
+ if pytz_module and get_config_boolean('show_localtime') and code != '--':
+ dt = get_country_datetime(code)
+ if broken:
+ whois(nick or host, string_country(country, code), buffer)
+ whois(nick or host, string_time(dt), buffer)
+ else:
+ s = '%s - %s' %(string_country(country, code), string_time(dt))
+ whois(nick or host, s, buffer)
+ else:
+ whois(nick or host, string_country(country, code), buffer)
+
+ if is_ip(host):
+ # good, got an ip
+ code, country = search_in_database(host)
+ elif is_domain(host):
+ # try to resolve uri
+ global reply_wrapper
+ reply_wrapper = reply_country
+ get_ip_process(host)
+ return
+ else:
+ # probably a cloak or ipv6
+ code, country = unknown
+ reply_country(code, country)
+
+### timezone
+def get_country_datetime(code):
+ """Get datetime object with country's timezone."""
+ try:
+ tzname = pytz.country_timezones(code)[0]
+ tz = pytz.timezone(tzname)
+ return datetime.datetime.now(tz)
+ except:
+ return None
+
+### commands
+def cmd_country(data, buffer, args):
+ """Shows country and local time for a given ip, uri or nick."""
+ if not args:
+ weechat.command('', '/HELP %s' %SCRIPT_COMMAND)
+ return WEECHAT_RC_OK
+ if ' ' in args:
+ # picks the first argument only
+ args = args[:args.find(' ')]
+ if args == 'update':
+ update_database()
+ else:
+ if not check_database():
+ error("IP database not found. You must download a database with '/country update' before "
+ "using this script.", buffer)
+ return WEECHAT_RC_OK
+ #check if is a nick
+ userhost = get_userhost_from_nick(buffer, args)
+ if userhost:
+ host = get_ip_from_userhost(*userhost.split('@'))
+ else:
+ host = get_ip_from_userhost(args, args)
+ print_country(host, buffer)
+ return WEECHAT_RC_OK
+
+def find_buffer(server, nick, message_type='whois'):
+ # See if there is a target msgbuffer set for this server
+ msgbuffer = weechat.config_string(weechat.config_get('irc.msgbuffer.%s.%s' % (server, message_type)))
+ # No whois msgbuffer for this server; use the global setting
+ if msgbuffer == '':
+ msgbuffer = weechat.config_string(weechat.config_get('irc.msgbuffer.%s' % message_type))
+
+ # Use the fallback msgbuffer setting if private buffer doesn't exist
+ if msgbuffer == 'private':
+ buffer = weechat.buffer_search('irc', '%s.%s' %(server, nick))
+ if buffer != '':
+ return buffer
+ else:
+ msgbuffer = weechat.config_string(weechat.config_get('irc.look.msgbuffer_fallback'))
+
+ # Find the appropriate buffer
+ if msgbuffer == "current":
+ return weechat.current_buffer()
+ elif msgbuffer == "weechat":
+ return weechat.buffer_search_main()
+ else:
+ return weechat.buffer_search('irc', 'server.%s' % server)
+
+### signal callbacks
+def whois_cb(data, signal, signal_data):
+ """function for /WHOIS"""
+ if not get_config_boolean('show_in_whois') or not check_database():
+ return WEECHAT_RC_OK
+ nick, user, host = signal_data.split()[3:6]
+ server = signal[:signal.find(',')]
+ #debug('%s | %s | %s' %(data, signal, signal_data))
+ host = get_ip_from_userhost(user, host)
+ print_country(host, find_buffer(server, nick), quiet=True, broken=True, nick=nick)
+ return WEECHAT_RC_OK
+
+### main
+if import_ok and weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
+ SCRIPT_DESC, '', ''):
+
+ # colors
+ color_delimiter = weechat.color('chat_delimiters')
+ color_chat_nick = weechat.color('chat_nick')
+ color_reset = weechat.color('reset')
+
+ # pretty [SCRIPT_NAME]
+ script_nick = '%s[%s%s%s]%s' % (color_delimiter,
+ color_chat_nick,
+ SCRIPT_NAME,
+ color_delimiter,
+ color_reset)
+
+ weechat.hook_signal('*,irc_in2_311', 'whois_cb', '') # /whois
+ weechat.hook_signal('*,irc_in2_314', 'whois_cb', '') # /whowas
+ weechat.hook_command('country', cmd_country.__doc__, 'update | (nick|ip|uri)',
+ " update: Downloads/updates ip database with country codes.\n"
+ "nick, ip, uri: Gets country and local time for a given ip, domain or nick.",
+ 'update||%(nick)', 'cmd_country', '')
+
+ # settings
+ for opt, val in settings.iteritems():
+ if not weechat.config_is_set_plugin(opt):
+ weechat.config_set_plugin(opt, val)
+
+ if not check_database():
+ say("IP database not found. You must download a database with '/country update' before "
+ "using this script.")
+
+ if not pytz_module and get_config_boolean('show_localtime'):
+ error(
+ "pytz module isn't installed, local time information is DISABLED. "
+ "Get it from http://pytz.sourceforge.net or from your distro packages "
+ "(python-tz in Ubuntu/Debian).")
+ weechat.config_set_plugin('show_localtime', 'off')
+
+ # -------------------------------------------------------------------------
+ # Debug
+
+ if weechat.config_get_plugin('debug'):
+ try:
+ # custom debug module I use, allows me to inspect script's objects.
+ import pybuffer
+ debug = pybuffer.debugBuffer(globals(), '%s_debug' % SCRIPT_NAME)
+ except:
+ def debug(s, *args):
+ if not isinstance(s, basestring):
+ s = str(s)
+ if args:
+ s = s %args
+ prnt('', '%s\t%s' % (script_nick, s))
+ else:
+ def debug(*args):
+ pass
+
+# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100:
diff --git a/weechat/python/emoji_aliases.py b/weechat/python/emoji_aliases.py
new file mode 100644
index 0000000..51959ba
--- /dev/null
+++ b/weechat/python/emoji_aliases.py
@@ -0,0 +1,1481 @@
+# -*- coding: utf-8 -*-
+# @author: Mike Reinhardt
+# @email: mreinhardt@gmail.com
+# @license: BSD
+"""Convert emoji aliases to unicode emoji.
+
+Primarily intended to support Slack chat over IRC or XMPP. Contains many extra
+aliases found in other programs as well.
+
+See http://www.emoji-cheat-sheet.com/ for supported sites and emoji chart.
+"""
+
+import re
+
+import weechat
+
+
+weechat.register(
+ "emoji_aliases", # name
+ "Mike Reinhardt", # author
+ "1.0.2", # version
+ "BSD", # license
+ "Convert emoji aliases to unicode emoji.", # description
+ "", # shutdown function
+ "utf-8") # charset
+
+
+EMOJI_ALIASES = {
+ u':+1:': u'\U0001F44D',
+ u':-1:': u'\U0001F44E',
+ u':100:': u'\U0001F4AF',
+ u':1234:': u'\U0001F522',
+ u':8ball:': u'\U0001F3B1',
+ u':a:': u'\U0001F170',
+ u':ab:': u'\U0001F18E',
+ u':abc:': u'\U0001F524',
+ u':abcd:': u'\U0001F521',
+ u':accept:': u'\U0001F251',
+ u':admission_tickets:': u'\U0001F39F',
+ u':aerial_tramway:': u'\U0001F6A1',
+ u':airplane:': u'\U00002708',
+ u':airplane_arriving:': u'\U0001F6EC',
+ u':airplane_departure:': u'\U0001F6EB',
+ u':alarm_clock:': u'\U000023F0',
+ u':alembic:': u'\U00002697',
+ u':alien:': u'\U0001F47D',
+ u':alien_monster:': u'\U0001F47E',
+ u':ambulance:': u'\U0001F691',
+ u':american_football:': u'\U0001F3C8',
+ u':amphora:': u'\U0001F3FA',
+ u':anchor:': u'\U00002693',
+ u':angel:': u'\U0001F47C',
+ u':anger:': u'\U0001F4A2',
+ u':anger_symbol:': u'\U0001F4A2',
+ u':angry:': u'\U0001F620',
+ u':angry_face:': u'\U0001F620',
+ u':anguished:': u'\U0001F627',
+ u':anguished_face:': u'\U0001F627',
+ u':ant:': u'\U0001F41C',
+ u':antenna_with_bars:': u'\U0001F4F6',
+ u':apple:': u'\U0001F34E',
+ u':aquarius:': u'\U00002652',
+ u':aries:': u'\U00002648',
+ u':arrow_backward:': u'\U000025C0',
+ u':arrow_double_down:': u'\U000023EC',
+ u':arrow_double_up:': u'\U000023EB',
+ u':arrow_down:': u'\U00002B07',
+ u':arrow_down_small:': u'\U0001F53D',
+ u':arrow_forward:': u'\U000025B6',
+ u':arrow_heading_down:': u'\U00002935',
+ u':arrow_heading_up:': u'\U00002934',
+ u':arrow_left:': u'\U00002B05',
+ u':arrow_lower_left:': u'\U00002199',
+ u':arrow_lower_right:': u'\U00002198',
+ u':arrow_right:': u'\U000027A1',
+ u':arrow_right_hook:': u'\U000021AA',
+ u':arrow_up:': u'\U00002B06',
+ u':arrow_up_down:': u'\U00002195',
+ u':arrow_up_small:': u'\U0001F53C',
+ u':arrow_upper_left:': u'\U00002196',
+ u':arrow_upper_right:': u'\U00002197',
+ u':arrows_clockwise:': u'\U0001F503',
+ u':arrows_counterclockwise:': u'\U0001F504',
+ u':art:': u'\U0001F3A8',
+ u':articulated_lorry:': u'\U0001F69B',
+ u':artist_palette:': u'\U0001F3A8',
+ u':astonished:': u'\U0001F632',
+ u':astonished_face:': u'\U0001F632',
+ u':athletic_shoe:': u'\U0001F45F',
+ u':atm:': u'\U0001F3E7',
+ u':atom_symbol:': u'\U0000269B',
+ u':aubergine:': u'\U0001F346',
+ u':automated_teller_machine:': u'\U0001F3E7',
+ u':automobile:': u'\U0001F697',
+ u':b:': u'\U0001F171',
+ u':baby:': u'\U0001F476',
+ u':baby_angel:': u'\U0001F47C',
+ u':baby_bottle:': u'\U0001F37C',
+ u':baby_chick:': u'\U0001F424',
+ u':baby_symbol:': u'\U0001F6BC',
+ u':back:': u'\U0001F519',
+ u':back_with_leftwards_arrow_above:': u'\U0001F519',
+ u':bactrian_camel:': u'\U0001F42B',
+ u':badminton_racquet_and_shuttlecock:': u'\U0001F3F8',
+ u':baggage_claim:': u'\U0001F6C4',
+ u':balloon:': u'\U0001F388',
+ u':ballot_box_with_ballot:': u'\U0001F5F3',
+ u':ballot_box_with_check:': u'\U00002611',
+ u':bamboo:': u'\U0001F38D',
+ u':banana:': u'\U0001F34C',
+ u':bangbang:': u'\U0000203C',
+ u':bank:': u'\U0001F3E6',
+ u':banknote_with_dollar_sign:': u'\U0001F4B5',
+ u':banknote_with_euro_sign:': u'\U0001F4B6',
+ u':banknote_with_pound_sign:': u'\U0001F4B7',
+ u':banknote_with_yen_sign:': u'\U0001F4B4',
+ u':bar_chart:': u'\U0001F4CA',
+ u':barber:': u'\U0001F488',
+ u':barber_pole:': u'\U0001F488',
+ u':barely_sunny:': u'\U0001F325',
+ u':baseball:': u'\U000026BE',
+ u':basketball:': u'\U0001F3C0',
+ u':basketball_and_hoop:': u'\U0001F3C0',
+ u':bath:': u'\U0001F6C0',
+ u':bathtub:': u'\U0001F6C1',
+ u':battery:': u'\U0001F50B',
+ u':beach_with_umbrella:': u'\U0001F3D6',
+ u':bear:': u'\U0001F43B',
+ u':bear_face:': u'\U0001F43B',
+ u':beating_heart:': u'\U0001F493',
+ u':bed:': u'\U0001F6CF',
+ u':bee:': u'\U0001F41D',
+ u':beer:': u'\U0001F37A',
+ u':beer_mug:': u'\U0001F37A',
+ u':beers:': u'\U0001F37B',
+ u':beetle:': u'\U0001F41E',
+ u':beginner:': u'\U0001F530',
+ u':bell:': u'\U0001F514',
+ u':bell_with_cancellation_stroke:': u'\U0001F515',
+ u':bellhop_bell:': u'\U0001F6CE',
+ u':bento:': u'\U0001F371',
+ u':bento_box:': u'\U0001F371',
+ u':bicycle:': u'\U0001F6B2',
+ u':bicyclist:': u'\U0001F6B4',
+ u':bike:': u'\U0001F6B2',
+ u':bikini:': u'\U0001F459',
+ u':billiards:': u'\U0001F3B1',
+ u':biohazard_sign:': u'\U00002623',
+ u':bird:': u'\U0001F426',
+ u':birthday:': u'\U0001F382',
+ u':birthday_cake:': u'\U0001F382',
+ u':black_circle:': u'\U000026AB',
+ u':black_circle_for_record:': u'\U000023FA',
+ u':black_club_suit:': u'\U00002663',
+ u':black_diamond_suit:': u'\U00002666',
+ u':black_down-pointing_double_triangle:': u'\U000023EC',
+ u':black_heart_suit:': u'\U00002665',
+ u':black_joker:': u'\U0001F0CF',
+ u':black_large_square:': u'\U00002B1B',
+ u':black_left-pointing_double_triangle:': u'\U000023EA',
+ u':black_left-pointing_triangle:': u'\U000025C0',
+ u':black_medium_small_square:': u'\U000025FE',
+ u':black_medium_square:': u'\U000025FC',
+ u':black_nib:': u'\U00002712',
+ u':black_question_mark_ornament:': u'\U00002753',
+ u':black_right-pointing_double_triangle:': u'\U000023E9',
+ u':black_right-pointing_triangle:': u'\U000025B6',
+ u':black_rightwards_arrow:': u'\U000027A1',
+ u':black_scissors:': u'\U00002702',
+ u':black_small_square:': u'\U000025AA',
+ u':black_spade_suit:': u'\U00002660',
+ u':black_square_button:': u'\U0001F532',
+ u':black_square_for_stop:': u'\U000023F9',
+ u':black_sun_with_rays:': u'\U00002600',
+ u':black_telephone:': u'\U0000260E',
+ u':black_universal_recycling_symbol:': u'\U0000267B',
+ u':black_up-pointing_double_triangle:': u'\U000023EB',
+ u':blossom:': u'\U0001F33C',
+ u':blowfish:': u'\U0001F421',
+ u':blue_book:': u'\U0001F4D8',
+ u':blue_car:': u'\U0001F699',
+ u':blue_heart:': u'\U0001F499',
+ u':blush:': u'\U0001F60A',
+ u':boar:': u'\U0001F417',
+ u':boat:': u'\U000026F5',
+ u':bomb:': u'\U0001F4A3',
+ u':book:': u'\U0001F4D6',
+ u':bookmark:': u'\U0001F516',
+ u':bookmark_tabs:': u'\U0001F4D1',
+ u':books:': u'\U0001F4DA',
+ u':boom:': u'\U0001F4A5',
+ u':boot:': u'\U0001F462',
+ u':bottle_with_popping_cork:': u'\U0001F37E',
+ u':bouquet:': u'\U0001F490',
+ u':bow:': u'\U0001F647',
+ u':bow_and_arrow:': u'\U0001F3F9',
+ u':bowling:': u'\U0001F3B3',
+ u':boy:': u'\U0001F466',
+ u':bread:': u'\U0001F35E',
+ u':bride_with_veil:': u'\U0001F470',
+ u':bridge_at_night:': u'\U0001F309',
+ u':briefcase:': u'\U0001F4BC',
+ u':broken_heart:': u'\U0001F494',
+ u':bug:': u'\U0001F41B',
+ u':building_construction:': u'\U0001F3D7',
+ u':bulb:': u'\U0001F4A1',
+ u':bullettrain_front:': u'\U0001F685',
+ u':bullettrain_side:': u'\U0001F684',
+ u':burrito:': u'\U0001F32F',
+ u':bus:': u'\U0001F68C',
+ u':bus_stop:': u'\U0001F68F',
+ u':busstop:': u'\U0001F68F',
+ u':bust_in_silhouette:': u'\U0001F464',
+ u':busts_in_silhouette:': u'\U0001F465',
+ u':cactus:': u'\U0001F335',
+ u':cake:': u'\U0001F370',
+ u':calendar:': u'\U0001F4C5',
+ u':calendar:': u'\U0001F4C6',
+ u':calling:': u'\U0001F4F2',
+ u':camel:': u'\U0001F42B',
+ u':camera:': u'\U0001F4F7',
+ u':camera_with_flash:': u'\U0001F4F8',
+ u':camping:': u'\U0001F3D5',
+ u':cancer:': u'\U0000264B',
+ u':candle:': u'\U0001F56F',
+ u':candy:': u'\U0001F36C',
+ u':capital_abcd:': u'\U0001F520',
+ u':capricorn:': u'\U00002651',
+ u':car:': u'\U0001F697',
+ u':card_file_box:': u'\U0001F5C3',
+ u':card_index:': u'\U0001F4C7',
+ u':card_index_dividers:': u'\U0001F5C2',
+ u':carousel_horse:': u'\U0001F3A0',
+ u':carp_streamer:': u'\U0001F38F',
+ u':cat2:': u'\U0001F408',
+ u':cat:': u'\U0001F408',
+ u':cat:': u'\U0001F431',
+ u':cat_face:': u'\U0001F431',
+ u':cat_face_with_tears_of_joy:': u'\U0001F639',
+ u':cat_face_with_wry_smile:': u'\U0001F63C',
+ u':cd:': u'\U0001F4BF',
+ u':chains:': u'\U000026D3',
+ u':champagne:': u'\U0001F37E',
+ u':chart:': u'\U0001F4B9',
+ u':chart_with_downwards_trend:': u'\U0001F4C9',
+ u':chart_with_upwards_trend:': u'\U0001F4C8',
+ u':chart_with_upwards_trend_and_yen_sign:': u'\U0001F4B9',
+ u':checkered_flag:': u'\U0001F3C1',
+ u':cheering_megaphone:': u'\U0001F4E3',
+ u':cheese_wedge:': u'\U0001F9C0',
+ u':chequered_flag:': u'\U0001F3C1',
+ u':cherries:': u'\U0001F352',
+ u':cherry_blossom:': u'\U0001F338',
+ u':chestnut:': u'\U0001F330',
+ u':chicken:': u'\U0001F414',
+ u':children_crossing:': u'\U0001F6B8',
+ u':chipmunk:': u'\U0001F43F',
+ u':chocolate_bar:': u'\U0001F36B',
+ u':christmas_tree:': u'\U0001F384',
+ u':church:': u'\U000026EA',
+ u':cinema:': u'\U0001F3A6',
+ u':circled_ideograph_accept:': u'\U0001F251',
+ u':circled_ideograph_advantage:': u'\U0001F250',
+ u':circled_ideograph_congratulation:': u'\U00003297',
+ u':circled_ideograph_secret:': u'\U00003299',
+ u':circled_latin_capital_letter_m:': u'\U000024C2',
+ u':circus_tent:': u'\U0001F3AA',
+ u':city_sunrise:': u'\U0001F307',
+ u':city_sunset:': u'\U0001F306',
+ u':cityscape:': u'\U0001F3D9',
+ u':cityscape_at_dusk:': u'\U0001F306',
+ u':cl:': u'\U0001F191',
+ u':clap:': u'\U0001F44F',
+ u':clapper:': u'\U0001F3AC',
+ u':clapper_board:': u'\U0001F3AC',
+ u':clapping_hands_sign:': u'\U0001F44F',
+ u':classical_building:': u'\U0001F3DB',
+ u':clinking_beer_mugs:': u'\U0001F37B',
+ u':clipboard:': u'\U0001F4CB',
+ u':clock1030:': u'\U0001F565',
+ u':clock10:': u'\U0001F559',
+ u':clock1130:': u'\U0001F566',
+ u':clock11:': u'\U0001F55A',
+ u':clock1230:': u'\U0001F567',
+ u':clock12:': u'\U0001F55B',
+ u':clock130:': u'\U0001F55C',
+ u':clock1:': u'\U0001F550',
+ u':clock230:': u'\U0001F55D',
+ u':clock2:': u'\U0001F551',
+ u':clock330:': u'\U0001F55E',
+ u':clock3:': u'\U0001F552',
+ u':clock430:': u'\U0001F55F',
+ u':clock4:': u'\U0001F553',
+ u':clock530:': u'\U0001F560',
+ u':clock5:': u'\U0001F554',
+ u':clock630:': u'\U0001F561',
+ u':clock6:': u'\U0001F555',
+ u':clock730:': u'\U0001F562',
+ u':clock7:': u'\U0001F556',
+ u':clock830:': u'\U0001F563',
+ u':clock8:': u'\U0001F557',
+ u':clock930:': u'\U0001F564',
+ u':clock9:': u'\U0001F558',
+ u':clock_face_eight-thirty:': u'\U0001F563',
+ u':clock_face_eight_oclock:': u'\U0001F557',
+ u':clock_face_eleven-thirty:': u'\U0001F566',
+ u':clock_face_eleven_oclock:': u'\U0001F55A',
+ u':clock_face_five-thirty:': u'\U0001F560',
+ u':clock_face_five_oclock:': u'\U0001F554',
+ u':clock_face_four-thirty:': u'\U0001F55F',
+ u':clock_face_four_oclock:': u'\U0001F553',
+ u':clock_face_nine-thirty:': u'\U0001F564',
+ u':clock_face_nine_oclock:': u'\U0001F558',
+ u':clock_face_one-thirty:': u'\U0001F55C',
+ u':clock_face_one_oclock:': u'\U0001F550',
+ u':clock_face_seven-thirty:': u'\U0001F562',
+ u':clock_face_seven_oclock:': u'\U0001F556',
+ u':clock_face_six-thirty:': u'\U0001F561',
+ u':clock_face_six_oclock:': u'\U0001F555',
+ u':clock_face_ten-thirty:': u'\U0001F565',
+ u':clock_face_ten_oclock:': u'\U0001F559',
+ u':clock_face_three-thirty:': u'\U0001F55E',
+ u':clock_face_three_oclock:': u'\U0001F552',
+ u':clock_face_twelve-thirty:': u'\U0001F567',
+ u':clock_face_twelve_oclock:': u'\U0001F55B',
+ u':clock_face_two-thirty:': u'\U0001F55D',
+ u':clock_face_two_oclock:': u'\U0001F551',
+ u':closed_book:': u'\U0001F4D5',
+ u':closed_lock_with_key:': u'\U0001F510',
+ u':closed_mailbox_with_lowered_flag:': u'\U0001F4EA',
+ u':closed_mailbox_with_raised_flag:': u'\U0001F4EB',
+ u':closed_umbrella:': u'\U0001F302',
+ u':cloud:': u'\U00002601',
+ u':cloud_with_lightning:': u'\U0001F329',
+ u':cloud_with_rain:': u'\U0001F327',
+ u':cloud_with_snow:': u'\U0001F328',
+ u':cloud_with_tornado:': u'\U0001F32A',
+ u':clubs:': u'\U00002663',
+ u':cocktail:': u'\U0001F378',
+ u':cocktail_glass:': u'\U0001F378',
+ u':coffee:': u'\U00002615',
+ u':coffin:': u'\U000026B0',
+ u':cold_sweat:': u'\U0001F630',
+ u':collision:': u'\U0001F4A5',
+ u':collision_symbol:': u'\U0001F4A5',
+ u':comet:': u'\U00002604',
+ u':compression:': u'\U0001F5DC',
+ u':computer:': u'\U0001F4BB',
+ u':confetti_ball:': u'\U0001F38A',
+ u':confounded:': u'\U0001F616',
+ u':confounded_face:': u'\U0001F616',
+ u':confused:': u'\U0001F615',
+ u':confused_face:': u'\U0001F615',
+ u':congratulations:': u'\U00003297',
+ u':construction:': u'\U0001F6A7',
+ u':construction_sign:': u'\U0001F6A7',
+ u':construction_worker:': u'\U0001F477',
+ u':control_knobs:': u'\U0001F39B',
+ u':convenience_store:': u'\U0001F3EA',
+ u':cooked_rice:': u'\U0001F35A',
+ u':cookie:': u'\U0001F36A',
+ u':cooking:': u'\U0001F373',
+ u':cool:': u'\U0001F192',
+ u':cop:': u'\U0001F46E',
+ u':copyright:': u'\U000000A9',
+ u':copyright_sign:': u'\U000000A9',
+ u':corn:': u'\U0001F33D',
+ u':couch_and_lamp:': u'\U0001F6CB',
+ u':couple:': u'\U0001F46B',
+ u':couple_with_heart:': u'\U0001F491',
+ u':couplekiss:': u'\U0001F48F',
+ u':cow2:': u'\U0001F404',
+ u':cow:': u'\U0001F404',
+ u':cow:': u'\U0001F42E',
+ u':cow_face:': u'\U0001F42E',
+ u':crab:': u'\U0001F980',
+ u':credit_card:': u'\U0001F4B3',
+ u':crescent_moon:': u'\U0001F319',
+ u':cricket_bat_and_ball:': u'\U0001F3CF',
+ u':crocodile:': u'\U0001F40A',
+ u':cross_mark:': u'\U0000274C',
+ u':crossed_flags:': u'\U0001F38C',
+ u':crossed_swords:': u'\U00002694',
+ u':crown:': u'\U0001F451',
+ u':cry:': u'\U0001F622',
+ u':crying_cat_face:': u'\U0001F63F',
+ u':crying_face:': u'\U0001F622',
+ u':crystal_ball:': u'\U0001F52E',
+ u':cupid:': u'\U0001F498',
+ u':curly_loop:': u'\U000027B0',
+ u':currency_exchange:': u'\U0001F4B1',
+ u':curry:': u'\U0001F35B',
+ u':curry_and_rice:': u'\U0001F35B',
+ u':custard:': u'\U0001F36E',
+ u':customs:': u'\U0001F6C3',
+ u':cyclone:': u'\U0001F300',
+ u':dagger_knife:': u'\U0001F5E1',
+ u':dancer:': u'\U0001F483',
+ u':dancers:': u'\U0001F46F',
+ u':dango:': u'\U0001F361',
+ u':dark_sunglasses:': u'\U0001F576',
+ u':dart:': u'\U0001F3AF',
+ u':dash:': u'\U0001F4A8',
+ u':dash_symbol:': u'\U0001F4A8',
+ u':date:': u'\U0001F4C5',
+ u':deciduous_tree:': u'\U0001F333',
+ u':delivery_truck:': u'\U0001F69A',
+ u':department_store:': u'\U0001F3EC',
+ u':derelict_house_building:': u'\U0001F3DA',
+ u':desert:': u'\U0001F3DC',
+ u':desert_island:': u'\U0001F3DD',
+ u':desktop_computer:': u'\U0001F5A5',
+ u':diamond_shape_with_a_dot_inside:': u'\U0001F4A0',
+ u':diamonds:': u'\U00002666',
+ u':direct_hit:': u'\U0001F3AF',
+ u':disappointed:': u'\U0001F61E',
+ u':disappointed_but_relieved_face:': u'\U0001F625',
+ u':disappointed_face:': u'\U0001F61E',
+ u':disappointed_relieved:': u'\U0001F625',
+ u':dizzy:': u'\U0001F4AB',
+ u':dizzy_face:': u'\U0001F635',
+ u':dizzy_symbol:': u'\U0001F4AB',
+ u':do_not_litter:': u'\U0001F6AF',
+ u':do_not_litter_symbol:': u'\U0001F6AF',
+ u':dog2:': u'\U0001F415',
+ u':dog:': u'\U0001F415',
+ u':dog:': u'\U0001F436',
+ u':dog_face:': u'\U0001F436',
+ u':dollar:': u'\U0001F4B5',
+ u':dolls:': u'\U0001F38E',
+ u':dolphin:': u'\U0001F42C',
+ u':door:': u'\U0001F6AA',
+ u':double_curly_loop:': u'\U000027BF',
+ u':double_exclamation_mark:': u'\U0000203C',
+ u':double_vertical_bar:': u'\U000023F8',
+ u':doughnut:': u'\U0001F369',
+ u':dove_of_peace:': u'\U0001F54A',
+ u':down-pointing_red_triangle:': u'\U0001F53B',
+ u':down-pointing_small_red_triangle:': u'\U0001F53D',
+ u':downwards_black_arrow:': u'\U00002B07',
+ u':dragon:': u'\U0001F409',
+ u':dragon_face:': u'\U0001F432',
+ u':dress:': u'\U0001F457',
+ u':dromedary_camel:': u'\U0001F42A',
+ u':droplet:': u'\U0001F4A7',
+ u':dvd:': u'\U0001F4C0',
+ u':e-mail:': u'\U0001F4E7',
+ u':e-mail_symbol:': u'\U0001F4E7',
+ u':ear:': u'\U0001F442',
+ u':ear_of_maize:': u'\U0001F33D',
+ u':ear_of_rice:': u'\U0001F33E',
+ u':earth_africa:': u'\U0001F30D',
+ u':earth_americas:': u'\U0001F30E',
+ u':earth_asia:': u'\U0001F30F',
+ u':earth_globe_americas:': u'\U0001F30E',
+ u':earth_globe_asia-australia:': u'\U0001F30F',
+ u':earth_globe_europe-africa:': u'\U0001F30D',
+ u':egg:': u'\U0001F373',
+ u':eggplant:': u'\U0001F346',
+ u':eight_pointed_black_star:': u'\U00002734',
+ u':eight_spoked_asterisk:': u'\U00002733',
+ u':eject_symbol:': u'\U000023CF',
+ u':electric_light_bulb:': u'\U0001F4A1',
+ u':electric_plug:': u'\U0001F50C',
+ u':electric_torch:': u'\U0001F526',
+ u':elephant:': u'\U0001F418',
+ u':email:': u'\U00002709',
+ u':emoji_modifier_fitzpatrick_type-1-2:': u'\U0001F3FB',
+ u':emoji_modifier_fitzpatrick_type-3:': u'\U0001F3FC',
+ u':emoji_modifier_fitzpatrick_type-4:': u'\U0001F3FD',
+ u':emoji_modifier_fitzpatrick_type-5:': u'\U0001F3FE',
+ u':emoji_modifier_fitzpatrick_type-6:': u'\U0001F3FF',
+ u':end:': u'\U0001F51A',
+ u':end_with_leftwards_arrow_above:': u'\U0001F51A',
+ u':envelope:': u'\U00002709',
+ u':envelope_with_arrow:': u'\U0001F4E9',
+ u':envelope_with_downwards_arrow_above:': u'\U0001F4E9',
+ u':euro:': u'\U0001F4B6',
+ u':european_castle:': u'\U0001F3F0',
+ u':european_post_office:': u'\U0001F3E4',
+ u':evergreen_tree:': u'\U0001F332',
+ u':exclamation:': u'\U00002757',
+ u':exclamation_question_mark:': u'\U00002049',
+ u':expressionless:': u'\U0001F611',
+ u':expressionless_face:': u'\U0001F611',
+ u':extraterrestrial_alien:': u'\U0001F47D',
+ u':eye:': u'\U0001F441',
+ u':eyeglasses:': u'\U0001F453',
+ u':eyes:': u'\U0001F440',
+ u':face_massage:': u'\U0001F486',
+ u':face_savouring_delicious_food:': u'\U0001F60B',
+ u':face_screaming_in_fear:': u'\U0001F631',
+ u':face_throwing_a_kiss:': u'\U0001F618',
+ u':face_with_cold_sweat:': u'\U0001F613',
+ u':face_with_head-bandage:': u'\U0001F915',
+ u':face_with_head_bandage:': u'\U0001F915',
+ u':face_with_look_of_triumph:': u'\U0001F624',
+ u':face_with_medical_mask:': u'\U0001F637',
+ u':face_with_no_good_gesture:': u'\U0001F645',
+ u':face_with_ok_gesture:': u'\U0001F646',
+ u':face_with_open_mouth:': u'\U0001F62E',
+ u':face_with_open_mouth_and_cold_sweat:': u'\U0001F630',
+ u':face_with_rolling_eyes:': u'\U0001F644',
+ u':face_with_stuck-out_tongue:': u'\U0001F61B',
+ u':face_with_tears_of_joy:': u'\U0001F602',
+ u':face_with_thermometer:': u'\U0001F912',
+ u':face_without_mouth:': u'\U0001F636',
+ u':facepunch:': u'\U0001F44A',
+ u':factory:': u'\U0001F3ED',
+ u':fallen_leaf:': u'\U0001F342',
+ u':family:': u'\U0001F46A',
+ u':fast_forward:': u'\U000023E9',
+ u':father_christmas:': u'\U0001F385',
+ u':fax:': u'\U0001F4E0',
+ u':fax_machine:': u'\U0001F4E0',
+ u':fearful:': u'\U0001F628',
+ u':fearful_face:': u'\U0001F628',
+ u':feet:': u'\U0001F43E',
+ u':ferris_wheel:': u'\U0001F3A1',
+ u':ferry:': u'\U000026F4',
+ u':field_hockey_stick_and_ball:': u'\U0001F3D1',
+ u':file_cabinet:': u'\U0001F5C4',
+ u':file_folder:': u'\U0001F4C1',
+ u':film_frames:': u'\U0001F39E',
+ u':film_projector:': u'\U0001F4FD',
+ u':fire:': u'\U0001F525',
+ u':fire_engine:': u'\U0001F692',
+ u':firework_sparkler:': u'\U0001F387',
+ u':fireworks:': u'\U0001F386',
+ u':first_quarter_moon:': u'\U0001F313',
+ u':first_quarter_moon_symbol:': u'\U0001F313',
+ u':first_quarter_moon_with_face:': u'\U0001F31B',
+ u':fish:': u'\U0001F41F',
+ u':fish_cake:': u'\U0001F365',
+ u':fish_cake_with_swirl_design:': u'\U0001F365',
+ u':fishing_pole_and_fish:': u'\U0001F3A3',
+ u':fist:': u'\U0000270A',
+ u':fisted_hand_sign:': u'\U0001F44A',
+ u':flag_in_hole:': u'\U000026F3',
+ u':flags:': u'\U0001F38F',
+ u':flashlight:': u'\U0001F526',
+ u':fleur-de-lis:': u'\U0000269C',
+ u':fleur_de_lis:': u'\U0000269C',
+ u':flexed_biceps:': u'\U0001F4AA',
+ u':flipper:': u'\U0001F42C',
+ u':floppy_disk:': u'\U0001F4BE',
+ u':flower_playing_cards:': u'\U0001F3B4',
+ u':flushed:': u'\U0001F633',
+ u':flushed_face:': u'\U0001F633',
+ u':fog:': u'\U0001F32B',
+ u':foggy:': u'\U0001F301',
+ u':football:': u'\U0001F3C8',
+ u':footprints:': u'\U0001F463',
+ u':fork_and_knife:': u'\U0001F374',
+ u':fork_and_knife_with_plate:': u'\U0001F37D',
+ u':fountain:': u'\U000026F2',
+ u':four_leaf_clover:': u'\U0001F340',
+ u':frame_with_picture:': u'\U0001F5BC',
+ u':free:': u'\U0001F193',
+ u':french_fries:': u'\U0001F35F',
+ u':fried_shrimp:': u'\U0001F364',
+ u':fries:': u'\U0001F35F',
+ u':frog:': u'\U0001F438',
+ u':frog_face:': u'\U0001F438',
+ u':front-facing_baby_chick:': u'\U0001F425',
+ u':frowning:': u'\U0001F626',
+ u':frowning_face_with_open_mouth:': u'\U0001F626',
+ u':fuel_pump:': u'\U000026FD',
+ u':fuelpump:': u'\U000026FD',
+ u':full_moon:': u'\U0001F315',
+ u':full_moon_symbol:': u'\U0001F315',
+ u':full_moon_with_face:': u'\U0001F31D',
+ u':funeral_urn:': u'\U000026B1',
+ u':game_die:': u'\U0001F3B2',
+ u':gear:': u'\U00002699',
+ u':gem:': u'\U0001F48E',
+ u':gem_stone:': u'\U0001F48E',
+ u':gemini:': u'\U0000264A',
+ u':ghost:': u'\U0001F47B',
+ u':gift:': u'\U0001F381',
+ u':gift_heart:': u'\U0001F49D',
+ u':girl:': u'\U0001F467',
+ u':globe_with_meridians:': u'\U0001F310',
+ u':glowing_star:': u'\U0001F31F',
+ u':goat:': u'\U0001F410',
+ u':golf:': u'\U000026F3',
+ u':golfer:': u'\U0001F3CC',
+ u':graduation_cap:': u'\U0001F393',
+ u':grapes:': u'\U0001F347',
+ u':green_apple:': u'\U0001F34F',
+ u':green_book:': u'\U0001F4D7',
+ u':green_heart:': u'\U0001F49A',
+ u':grey_exclamation:': u'\U00002755',
+ u':grey_question:': u'\U00002754',
+ u':grimacing:': u'\U0001F62C',
+ u':grimacing_face:': u'\U0001F62C',
+ u':grin:': u'\U0001F601',
+ u':grinning:': u'\U0001F600',
+ u':grinning_cat_face_with_smiling_eyes:': u'\U0001F638',
+ u':grinning_face:': u'\U0001F600',
+ u':grinning_face_with_smiling_eyes:': u'\U0001F601',
+ u':growing_heart:': u'\U0001F497',
+ u':guardsman:': u'\U0001F482',
+ u':guitar:': u'\U0001F3B8',
+ u':gun:': u'\U0001F52B',
+ u':haircut:': u'\U0001F487',
+ u':hamburger:': u'\U0001F354',
+ u':hammer:': u'\U0001F528',
+ u':hammer_and_pick:': u'\U00002692',
+ u':hammer_and_wrench:': u'\U0001F6E0',
+ u':hamster:': u'\U0001F439',
+ u':hamster_face:': u'\U0001F439',
+ u':hand:': u'\U0000270B',
+ u':handbag:': u'\U0001F45C',
+ u':hankey:': u'\U0001F4A9',
+ u':happy_person_raising_one_hand:': u'\U0001F64B',
+ u':hatched_chick:': u'\U0001F425',
+ u':hatching_chick:': u'\U0001F423',
+ u':headphone:': u'\U0001F3A7',
+ u':headphones:': u'\U0001F3A7',
+ u':hear-no-evil_monkey:': u'\U0001F649',
+ u':hear_no_evil:': u'\U0001F649',
+ u':heart:': u'\U00002764',
+ u':heart_decoration:': u'\U0001F49F',
+ u':heart_eyes:': u'\U0001F60D',
+ u':heart_eyes_cat:': u'\U0001F63B',
+ u':heart_with_arrow:': u'\U0001F498',
+ u':heart_with_ribbon:': u'\U0001F49D',
+ u':heartbeat:': u'\U0001F493',
+ u':heartpulse:': u'\U0001F497',
+ u':hearts:': u'\U00002665',
+ u':heavy_black_heart:': u'\U00002764',
+ u':heavy_check_mark:': u'\U00002714',
+ u':heavy_division_sign:': u'\U00002797',
+ u':heavy_dollar_sign:': u'\U0001F4B2',
+ u':heavy_exclamation_mark:': u'\U00002757',
+ u':heavy_exclamation_mark_symbol:': u'\U00002757',
+ u':heavy_heart_exclamation_mark_ornament:': u'\U00002763',
+ u':heavy_large_circle:': u'\U00002B55',
+ u':heavy_minus_sign:': u'\U00002796',
+ u':heavy_multiplication_x:': u'\U00002716',
+ u':heavy_plus_sign:': u'\U00002795',
+ u':helicopter:': u'\U0001F681',
+ u':helm_symbol:': u'\U00002388',
+ u':helmet_with_white_cross:': u'\U000026D1',
+ u':herb:': u'\U0001F33F',
+ u':hibiscus:': u'\U0001F33A',
+ u':high-heeled_shoe:': u'\U0001F460',
+ u':high-speed_train:': u'\U0001F684',
+ u':high-speed_train_with_bullet_nose:': u'\U0001F685',
+ u':high_brightness:': u'\U0001F506',
+ u':high_brightness_symbol:': u'\U0001F506',
+ u':high_heel:': u'\U0001F460',
+ u':high_voltage_sign:': u'\U000026A1',
+ u':hocho:': u'\U0001F52A',
+ u':hole:': u'\U0001F573',
+ u':honey_pot:': u'\U0001F36F',
+ u':honeybee:': u'\U0001F41D',
+ u':horizontal_traffic_light:': u'\U0001F6A5',
+ u':horse:': u'\U0001F40E',
+ u':horse:': u'\U0001F434',
+ u':horse_face:': u'\U0001F434',
+ u':horse_racing:': u'\U0001F3C7',
+ u':hospital:': u'\U0001F3E5',
+ u':hot_beverage:': u'\U00002615',
+ u':hot_dog:': u'\U0001F32D',
+ u':hot_pepper:': u'\U0001F336',
+ u':hot_springs:': u'\U00002668',
+ u':hotdog:': u'\U0001F32D',
+ u':hotel:': u'\U0001F3E8',
+ u':hotsprings:': u'\U00002668',
+ u':hourglass:': u'\U0000231B',
+ u':hourglass_flowing_sand:': u'\U000023F3',
+ u':hourglass_with_flowing_sand:': u'\U000023F3',
+ u':house:': u'\U0001F3E0',
+ u':house_building:': u'\U0001F3E0',
+ u':house_buildings:': u'\U0001F3D8',
+ u':house_with_garden:': u'\U0001F3E1',
+ u':hugging_face:': u'\U0001F917',
+ u':hundred_points_symbol:': u'\U0001F4AF',
+ u':hushed:': u'\U0001F62F',
+ u':hushed_face:': u'\U0001F62F',
+ u':ice_cream:': u'\U0001F368',
+ u':ice_hockey_stick_and_puck:': u'\U0001F3D2',
+ u':ice_skate:': u'\U000026F8',
+ u':icecream:': u'\U0001F366',
+ u':id:': u'\U0001F194',
+ u':ideograph_advantage:': u'\U0001F250',
+ u':imp:': u'\U0001F47F',
+ u':inbox_tray:': u'\U0001F4E5',
+ u':incoming_envelope:': u'\U0001F4E8',
+ u':information_desk_person:': u'\U0001F481',
+ u':information_source:': u'\U00002139',
+ u':innocent:': u'\U0001F607',
+ u':input_symbol_for_latin_capital_letters:': u'\U0001F520',
+ u':input_symbol_for_latin_letters:': u'\U0001F524',
+ u':input_symbol_for_latin_small_letters:': u'\U0001F521',
+ u':input_symbol_for_numbers:': u'\U0001F522',
+ u':input_symbol_for_symbols:': u'\U0001F523',
+ u':interrobang:': u'\U00002049',
+ u':iphone:': u'\U0001F4F1',
+ u':izakaya_lantern:': u'\U0001F3EE',
+ u':jack-o-lantern:': u'\U0001F383',
+ u':jack_o_lantern:': u'\U0001F383',
+ u':japan:': u'\U0001F5FE',
+ u':japanese_castle:': u'\U0001F3EF',
+ u':japanese_dolls:': u'\U0001F38E',
+ u':japanese_goblin:': u'\U0001F47A',
+ u':japanese_ogre:': u'\U0001F479',
+ u':japanese_post_office:': u'\U0001F3E3',
+ u':japanese_symbol_for_beginner:': u'\U0001F530',
+ u':jeans:': u'\U0001F456',
+ u':joy:': u'\U0001F602',
+ u':joy_cat:': u'\U0001F639',
+ u':joystick:': u'\U0001F579',
+ u':kaaba:': u'\U0001F54B',
+ u':key:': u'\U0001F511',
+ u':keyboard:': u'\U00002328',
+ u':keycap_ten:': u'\U0001F51F',
+ u':kimono:': u'\U0001F458',
+ u':kiss:': u'\U0001F48B',
+ u':kiss:': u'\U0001F48F',
+ u':kiss_mark:': u'\U0001F48B',
+ u':kissing:': u'\U0001F617',
+ u':kissing_cat:': u'\U0001F63D',
+ u':kissing_cat_face_with_closed_eyes:': u'\U0001F63D',
+ u':kissing_closed_eyes:': u'\U0001F61A',
+ u':kissing_face:': u'\U0001F617',
+ u':kissing_face_with_closed_eyes:': u'\U0001F61A',
+ u':kissing_face_with_smiling_eyes:': u'\U0001F619',
+ u':kissing_heart:': u'\U0001F618',
+ u':kissing_smiling_eyes:': u'\U0001F619',
+ u':knife:': u'\U0001F52A',
+ u':knife_fork_plate:': u'\U0001F37D',
+ u':koala:': u'\U0001F428',
+ u':koko:': u'\U0001F201',
+ u':label:': u'\U0001F3F7',
+ u':lady_beetle:': u'\U0001F41E',
+ u':lantern:': u'\U0001F3EE',
+ u':large_blue_circle:': u'\U0001F535',
+ u':large_blue_diamond:': u'\U0001F537',
+ u':large_orange_diamond:': u'\U0001F536',
+ u':large_red_circle:': u'\U0001F534',
+ u':last_quarter_moon:': u'\U0001F317',
+ u':last_quarter_moon_symbol:': u'\U0001F317',
+ u':last_quarter_moon_with_face:': u'\U0001F31C',
+ u':latin_cross:': u'\U0000271D',
+ u':laughing:': u'\U0001F606',
+ u':leaf_fluttering_in_wind:': u'\U0001F343',
+ u':leaves:': u'\U0001F343',
+ u':ledger:': u'\U0001F4D2',
+ u':left-pointing_magnifying_glass:': u'\U0001F50D',
+ u':left_luggage:': u'\U0001F6C5',
+ u':left_right_arrow:': u'\U00002194',
+ u':left_speech_bubble:': u'\U0001F4AC',
+ u':leftwards_arrow_with_hook:': u'\U000021A9',
+ u':leftwards_black_arrow:': u'\U00002B05',
+ u':lemon:': u'\U0001F34B',
+ u':leo:': u'\U0000264C',
+ u':leopard:': u'\U0001F406',
+ u':level_slider:': u'\U0001F39A',
+ u':libra:': u'\U0000264E',
+ u':light_rail:': u'\U0001F688',
+ u':lightning:': u'\U0001F329',
+ u':link:': u'\U0001F517',
+ u':link_symbol:': u'\U0001F517',
+ u':linked_paperclips:': u'\U0001F587',
+ u':lion_face:': u'\U0001F981',
+ u':lips:': u'\U0001F444',
+ u':lipstick:': u'\U0001F484',
+ u':lock:': u'\U0001F512',
+ u':lock_with_ink_pen:': u'\U0001F50F',
+ u':lollipop:': u'\U0001F36D',
+ u':loop:': u'\U000027BF',
+ u':loud_sound:': u'\U0001F50A',
+ u':loudly_crying_face:': u'\U0001F62D',
+ u':loudspeaker:': u'\U0001F4E2',
+ u':love_hotel:': u'\U0001F3E9',
+ u':love_letter:': u'\U0001F48C',
+ u':low_brightness:': u'\U0001F505',
+ u':low_brightness_symbol:': u'\U0001F505',
+ u':lower_left_ballpoint_pen:': u'\U0001F58A',
+ u':lower_left_crayon:': u'\U0001F58D',
+ u':lower_left_fountain_pen:': u'\U0001F58B',
+ u':lower_left_paintbrush:': u'\U0001F58C',
+ u':m:': u'\U000024C2',
+ u':mag:': u'\U0001F50D',
+ u':mag_right:': u'\U0001F50E',
+ u':mahjong:': u'\U0001F004',
+ u':mahjong_tile_red_dragon:': u'\U0001F004',
+ u':mailbox:': u'\U0001F4EB',
+ u':mailbox_closed:': u'\U0001F4EA',
+ u':mailbox_with_mail:': u'\U0001F4EC',
+ u':mailbox_with_no_mail:': u'\U0001F4ED',
+ u':man:': u'\U0001F468',
+ u':man_and_woman_holding_hands:': u'\U0001F46B',
+ u':man_in_business_suit_levitating:': u'\U0001F574',
+ u':man_with_gua_pi_mao:': u'\U0001F472',
+ u':man_with_turban:': u'\U0001F473',
+ u':mans_shoe:': u'\U0001F45E',
+ u':mantelpiece_clock:': u'\U0001F570',
+ u':maple_leaf:': u'\U0001F341',
+ u':mask:': u'\U0001F637',
+ u':massage:': u'\U0001F486',
+ u':meat_on_bone:': u'\U0001F356',
+ u':medal:': u'\U0001F3C5',
+ u':medium_black_circle:': u'\U000026AB',
+ u':medium_white_circle:': u'\U000026AA',
+ u':mega:': u'\U0001F4E3',
+ u':melon:': u'\U0001F348',
+ u':memo:': u'\U0001F4DD',
+ u':menorah_with_nine_branches:': u'\U0001F54E',
+ u':mens:': u'\U0001F6B9',
+ u':mens_symbol:': u'\U0001F6B9',
+ u':metro:': u'\U0001F687',
+ u':microphone:': u'\U0001F3A4',
+ u':microscope:': u'\U0001F52C',
+ u':middle_finger:': u'\U0001F595',
+ u':military_medal:': u'\U0001F396',
+ u':milky_way:': u'\U0001F30C',
+ u':minibus:': u'\U0001F690',
+ u':minidisc:': u'\U0001F4BD',
+ u':mobile_phone:': u'\U0001F4F1',
+ u':mobile_phone_off:': u'\U0001F4F4',
+ u':money-mouth_face:': u'\U0001F911',
+ u':money_bag:': u'\U0001F4B0',
+ u':money_mouth_face:': u'\U0001F911',
+ u':money_with_wings:': u'\U0001F4B8',
+ u':moneybag:': u'\U0001F4B0',
+ u':monkey:': u'\U0001F412',
+ u':monkey_face:': u'\U0001F435',
+ u':monorail:': u'\U0001F69D',
+ u':moon:': u'\U0001F314',
+ u':moon_viewing_ceremony:': u'\U0001F391',
+ u':mortar_board:': u'\U0001F393',
+ u':mosque:': u'\U0001F54C',
+ u':mostly_sunny:': u'\U0001F324',
+ u':motor_boat:': u'\U0001F6E5',
+ u':motorway:': u'\U0001F6E3',
+ u':mount_fuji:': u'\U0001F5FB',
+ u':mountain:': u'\U000026F0',
+ u':mountain_bicyclist:': u'\U0001F6B5',
+ u':mountain_cableway:': u'\U0001F6A0',
+ u':mountain_railway:': u'\U0001F69E',
+ u':mouse2:': u'\U0001F401',
+ u':mouse:': u'\U0001F401',
+ u':mouse:': u'\U0001F42D',
+ u':mouse_face:': u'\U0001F42D',
+ u':mouth:': u'\U0001F444',
+ u':movie_camera:': u'\U0001F3A5',
+ u':moyai:': u'\U0001F5FF',
+ u':multiple_musical_notes:': u'\U0001F3B6',
+ u':muscle:': u'\U0001F4AA',
+ u':mushroom:': u'\U0001F344',
+ u':musical_keyboard:': u'\U0001F3B9',
+ u':musical_note:': u'\U0001F3B5',
+ u':musical_score:': u'\U0001F3BC',
+ u':mute:': u'\U0001F507',
+ u':nail_care:': u'\U0001F485',
+ u':nail_polish:': u'\U0001F485',
+ u':name_badge:': u'\U0001F4DB',
+ u':national_park:': u'\U0001F3DE',
+ u':necktie:': u'\U0001F454',
+ u':negative_squared_ab:': u'\U0001F18E',
+ u':negative_squared_cross_mark:': u'\U0000274E',
+ u':nerd_face:': u'\U0001F913',
+ u':neutral_face:': u'\U0001F610',
+ u':new:': u'\U0001F195',
+ u':new_moon:': u'\U0001F311',
+ u':new_moon_symbol:': u'\U0001F311',
+ u':new_moon_with_face:': u'\U0001F31A',
+ u':newspaper:': u'\U0001F4F0',
+ u':ng:': u'\U0001F196',
+ u':night_with_stars:': u'\U0001F303',
+ u':no_bell:': u'\U0001F515',
+ u':no_bicycles:': u'\U0001F6B3',
+ u':no_entry:': u'\U000026D4',
+ u':no_entry_sign:': u'\U0001F6AB',
+ u':no_good:': u'\U0001F645',
+ u':no_mobile_phones:': u'\U0001F4F5',
+ u':no_mouth:': u'\U0001F636',
+ u':no_one_under_eighteen_symbol:': u'\U0001F51E',
+ u':no_pedestrians:': u'\U0001F6B7',
+ u':no_smoking:': u'\U0001F6AD',
+ u':no_smoking_symbol:': u'\U0001F6AD',
+ u':non-potable_water:': u'\U0001F6B1',
+ u':non-potable_water_symbol:': u'\U0001F6B1',
+ u':north_east_arrow:': u'\U00002197',
+ u':north_west_arrow:': u'\U00002196',
+ u':nose:': u'\U0001F443',
+ u':notebook:': u'\U0001F4D3',
+ u':notebook_with_decorative_cover:': u'\U0001F4D4',
+ u':notes:': u'\U0001F3B6',
+ u':nut_and_bolt:': u'\U0001F529',
+ u':o2:': u'\U0001F17E',
+ u':o:': u'\U00002B55',
+ u':ocean:': u'\U0001F30A',
+ u':octopus:': u'\U0001F419',
+ u':oden:': u'\U0001F362',
+ u':office:': u'\U0001F3E2',
+ u':office_building:': u'\U0001F3E2',
+ u':oil_drum:': u'\U0001F6E2',
+ u':ok:': u'\U0001F197',
+ u':ok_hand:': u'\U0001F44C',
+ u':ok_hand_sign:': u'\U0001F44C',
+ u':ok_woman:': u'\U0001F646',
+ u':old_key:': u'\U0001F5DD',
+ u':older_man:': u'\U0001F474',
+ u':older_woman:': u'\U0001F475',
+ u':om_symbol:': u'\U0001F549',
+ u':on:': u'\U0001F51B',
+ u':oncoming_automobile:': u'\U0001F698',
+ u':oncoming_bus:': u'\U0001F68D',
+ u':oncoming_police_car:': u'\U0001F694',
+ u':oncoming_taxi:': u'\U0001F696',
+ u':open_book:': u'\U0001F4D6',
+ u':open_file_folder:': u'\U0001F4C2',
+ u':open_hands:': u'\U0001F450',
+ u':open_hands_sign:': u'\U0001F450',
+ u':open_lock:': u'\U0001F513',
+ u':open_mailbox_with_lowered_flag:': u'\U0001F4ED',
+ u':open_mailbox_with_raised_flag:': u'\U0001F4EC',
+ u':open_mouth:': u'\U0001F62E',
+ u':ophiuchus:': u'\U000026CE',
+ u':optical_disc:': u'\U0001F4BF',
+ u':orange_book:': u'\U0001F4D9',
+ u':orthodox_cross:': u'\U00002626',
+ u':outbox_tray:': u'\U0001F4E4',
+ u':ox:': u'\U0001F402',
+ u':package:': u'\U0001F4E6',
+ u':page_facing_up:': u'\U0001F4C4',
+ u':page_with_curl:': u'\U0001F4C3',
+ u':pager:': u'\U0001F4DF',
+ u':palm_tree:': u'\U0001F334',
+ u':panda_face:': u'\U0001F43C',
+ u':paperclip:': u'\U0001F4CE',
+ u':parking:': u'\U0001F17F',
+ u':part_alternation_mark:': u'\U0000303D',
+ u':partly_sunny:': u'\U000026C5',
+ u':partly_sunny_rain:': u'\U0001F326',
+ u':party_popper:': u'\U0001F389',
+ u':passenger_ship:': u'\U0001F6F3',
+ u':passport_control:': u'\U0001F6C2',
+ u':paw_prints:': u'\U0001F43E',
+ u':peace_symbol:': u'\U0000262E',
+ u':peach:': u'\U0001F351',
+ u':pear:': u'\U0001F350',
+ u':pedestrian:': u'\U0001F6B6',
+ u':pencil2:': u'\U0000270F',
+ u':pencil:': u'\U0000270F',
+ u':pencil:': u'\U0001F4DD',
+ u':penguin:': u'\U0001F427',
+ u':pensive:': u'\U0001F614',
+ u':pensive_face:': u'\U0001F614',
+ u':performing_arts:': u'\U0001F3AD',
+ u':persevere:': u'\U0001F623',
+ u':persevering_face:': u'\U0001F623',
+ u':person_bowing_deeply:': u'\U0001F647',
+ u':person_frowning:': u'\U0001F64D',
+ u':person_with_ball:': u'\U000026F9',
+ u':person_with_blond_hair:': u'\U0001F471',
+ u':person_with_folded_hands:': u'\U0001F64F',
+ u':person_with_pouting_face:': u'\U0001F64E',
+ u':personal_computer:': u'\U0001F4BB',
+ u':phone:': u'\U0000260E',
+ u':pick:': u'\U000026CF',
+ u':pig2:': u'\U0001F416',
+ u':pig:': u'\U0001F416',
+ u':pig:': u'\U0001F437',
+ u':pig_face:': u'\U0001F437',
+ u':pig_nose:': u'\U0001F43D',
+ u':pile_of_poo:': u'\U0001F4A9',
+ u':pill:': u'\U0001F48A',
+ u':pine_decoration:': u'\U0001F38D',
+ u':pineapple:': u'\U0001F34D',
+ u':pisces:': u'\U00002653',
+ u':pistol:': u'\U0001F52B',
+ u':pizza:': u'\U0001F355',
+ u':place_of_worship:': u'\U0001F6D0',
+ u':playing_card_black_joker:': u'\U0001F0CF',
+ u':point_down:': u'\U0001F447',
+ u':point_left:': u'\U0001F448',
+ u':point_right:': u'\U0001F449',
+ u':point_up:': u'\U0000261D',
+ u':point_up_2:': u'\U0001F446',
+ u':police_car:': u'\U0001F693',
+ u':police_cars_revolving_light:': u'\U0001F6A8',
+ u':police_officer:': u'\U0001F46E',
+ u':poodle:': u'\U0001F429',
+ u':poop:': u'\U0001F4A9',
+ u':popcorn:': u'\U0001F37F',
+ u':post_office:': u'\U0001F3E3',
+ u':postal_horn:': u'\U0001F4EF',
+ u':postbox:': u'\U0001F4EE',
+ u':pot_of_food:': u'\U0001F372',
+ u':potable_water:': u'\U0001F6B0',
+ u':potable_water_symbol:': u'\U0001F6B0',
+ u':pouch:': u'\U0001F45D',
+ u':poultry_leg:': u'\U0001F357',
+ u':pound:': u'\U0001F4B7',
+ u':pouting_cat:': u'\U0001F63E',
+ u':pouting_cat_face:': u'\U0001F63E',
+ u':pouting_face:': u'\U0001F621',
+ u':pray:': u'\U0001F64F',
+ u':prayer_beads:': u'\U0001F4FF',
+ u':princess:': u'\U0001F478',
+ u':printer:': u'\U0001F5A8',
+ u':public_address_loudspeaker:': u'\U0001F4E2',
+ u':punch:': u'\U0001F44A',
+ u':purple_heart:': u'\U0001F49C',
+ u':purse:': u'\U0001F45B',
+ u':pushpin:': u'\U0001F4CC',
+ u':put_litter_in_its_place:': u'\U0001F6AE',
+ u':put_litter_in_its_place_symbol:': u'\U0001F6AE',
+ u':question:': u'\U00002753',
+ u':rabbit2:': u'\U0001F407',
+ u':rabbit:': u'\U0001F407',
+ u':rabbit:': u'\U0001F430',
+ u':rabbit_face:': u'\U0001F430',
+ u':racehorse:': u'\U0001F40E',
+ u':racing_car:': u'\U0001F3CE',
+ u':racing_motorcycle:': u'\U0001F3CD',
+ u':radio:': u'\U0001F4FB',
+ u':radio_button:': u'\U0001F518',
+ u':radioactive_sign:': u'\U00002622',
+ u':rage:': u'\U0001F621',
+ u':railway_car:': u'\U0001F683',
+ u':railway_track:': u'\U0001F6E4',
+ u':rain_cloud:': u'\U0001F327',
+ u':rainbow:': u'\U0001F308',
+ u':raised_fist:': u'\U0000270A',
+ u':raised_hand:': u'\U0000270B',
+ u':raised_hand_with_fingers_splayed:': u'\U0001F590',
+ u':raised_hands:': u'\U0001F64C',
+ u':raising_hand:': u'\U0001F64B',
+ u':ram:': u'\U0001F40F',
+ u':ramen:': u'\U0001F35C',
+ u':rat:': u'\U0001F400',
+ u':recreational_vehicle:': u'\U0001F699',
+ u':recycle:': u'\U0000267B',
+ u':red_apple:': u'\U0001F34E',
+ u':red_car:': u'\U0001F697',
+ u':red_circle:': u'\U0001F534',
+ u':registered:': u'\U000000AE',
+ u':registered_sign:': u'\U000000AE',
+ u':relaxed:': u'\U0000263A',
+ u':relieved:': u'\U0001F60C',
+ u':relieved_face:': u'\U0001F60C',
+ u':reminder_ribbon:': u'\U0001F397',
+ u':repeat:': u'\U0001F501',
+ u':repeat_one:': u'\U0001F502',
+ u':restroom:': u'\U0001F6BB',
+ u':revolving_hearts:': u'\U0001F49E',
+ u':rewind:': u'\U000023EA',
+ u':ribbon:': u'\U0001F380',
+ u':rice:': u'\U0001F35A',
+ u':rice_ball:': u'\U0001F359',
+ u':rice_cracker:': u'\U0001F358',
+ u':rice_scene:': u'\U0001F391',
+ u':right-pointing_magnifying_glass:': u'\U0001F50E',
+ u':right_anger_bubble:': u'\U0001F5EF',
+ u':rightwards_arrow_with_hook:': u'\U000021AA',
+ u':ring:': u'\U0001F48D',
+ u':roasted_sweet_potato:': u'\U0001F360',
+ u':robot_face:': u'\U0001F916',
+ u':rocket:': u'\U0001F680',
+ u':rolled-up_newspaper:': u'\U0001F5DE',
+ u':rolled_up_newspaper:': u'\U0001F5DE',
+ u':roller_coaster:': u'\U0001F3A2',
+ u':rooster:': u'\U0001F413',
+ u':rose:': u'\U0001F339',
+ u':rosette:': u'\U0001F3F5',
+ u':rotating_light:': u'\U0001F6A8',
+ u':round_pushpin:': u'\U0001F4CD',
+ u':rowboat:': u'\U0001F6A3',
+ u':rugby_football:': u'\U0001F3C9',
+ u':runner:': u'\U0001F3C3',
+ u':running:': u'\U0001F3C3',
+ u':running_shirt_with_sash:': u'\U0001F3BD',
+ u':sa:': u'\U0001F202',
+ u':sagittarius:': u'\U00002650',
+ u':sailboat:': u'\U000026F5',
+ u':sake:': u'\U0001F376',
+ u':sake_bottle_and_cup:': u'\U0001F376',
+ u':sandal:': u'\U0001F461',
+ u':santa:': u'\U0001F385',
+ u':satellite:': u'\U0001F4E1',
+ u':satellite:': u'\U0001F6F0',
+ u':satellite_antenna:': u'\U0001F4E1',
+ u':satisfied:': u'\U0001F606',
+ u':saxophone:': u'\U0001F3B7',
+ u':scales:': u'\U00002696',
+ u':school:': u'\U0001F3EB',
+ u':school_satchel:': u'\U0001F392',
+ u':scissors:': u'\U00002702',
+ u':scorpion:': u'\U0001F982',
+ u':scorpius:': u'\U0000264F',
+ u':scream:': u'\U0001F631',
+ u':scream_cat:': u'\U0001F640',
+ u':scroll:': u'\U0001F4DC',
+ u':seat:': u'\U0001F4BA',
+ u':secret:': u'\U00003299',
+ u':see-no-evil_monkey:': u'\U0001F648',
+ u':see_no_evil:': u'\U0001F648',
+ u':seedling:': u'\U0001F331',
+ u':shamrock:': u'\U00002618',
+ u':shaved_ice:': u'\U0001F367',
+ u':sheep:': u'\U0001F411',
+ u':shell:': u'\U0001F41A',
+ u':shield:': u'\U0001F6E1',
+ u':shinto_shrine:': u'\U000026E9',
+ u':ship:': u'\U0001F6A2',
+ u':shirt:': u'\U0001F455',
+ u':shit:': u'\U0001F4A9',
+ u':shoe:': u'\U0001F45E',
+ u':shooting_star:': u'\U0001F320',
+ u':shopping_bags:': u'\U0001F6CD',
+ u':shortcake:': u'\U0001F370',
+ u':shower:': u'\U0001F6BF',
+ u':sign_of_the_horns:': u'\U0001F918',
+ u':signal_strength:': u'\U0001F4F6',
+ u':silhouette_of_japan:': u'\U0001F5FE',
+ u':simple_smile:': u'\U0001F642',
+ u':six_pointed_star:': u'\U0001F52F',
+ u':ski:': u'\U0001F3BF',
+ u':ski_and_ski_boot:': u'\U0001F3BF',
+ u':skier:': u'\U000026F7',
+ u':skull:': u'\U0001F480',
+ u':skull_and_crossbones:': u'\U00002620',
+ u':sleeping:': u'\U0001F634',
+ u':sleeping_accommodation:': u'\U0001F6CC',
+ u':sleeping_face:': u'\U0001F634',
+ u':sleeping_symbol:': u'\U0001F4A4',
+ u':sleepy:': u'\U0001F62A',
+ u':sleepy_face:': u'\U0001F62A',
+ u':sleuth_or_spy:': u'\U0001F575',
+ u':slice_of_pizza:': u'\U0001F355',
+ u':slightly_frowning_face:': u'\U0001F641',
+ u':slightly_smiling_face:': u'\U0001F642',
+ u':slot_machine:': u'\U0001F3B0',
+ u':small_airplane:': u'\U0001F6E9',
+ u':small_blue_diamond:': u'\U0001F539',
+ u':small_orange_diamond:': u'\U0001F538',
+ u':small_red_triangle:': u'\U0001F53A',
+ u':small_red_triangle_down:': u'\U0001F53B',
+ u':smile:': u'\U0001F604',
+ u':smile_cat:': u'\U0001F638',
+ u':smiley:': u'\U0001F603',
+ u':smiley_cat:': u'\U0001F63A',
+ u':smiling_cat_face_with_heart-shaped_eyes:': u'\U0001F63B',
+ u':smiling_cat_face_with_open_mouth:': u'\U0001F63A',
+ u':smiling_face_with_halo:': u'\U0001F607',
+ u':smiling_face_with_heart-shaped_eyes:': u'\U0001F60D',
+ u':smiling_face_with_horns:': u'\U0001F608',
+ u':smiling_face_with_open_mouth:': u'\U0001F603',
+ u':smiling_face_with_open_mouth_and_cold_sweat:': u'\U0001F605',
+ u':smiling_face_with_open_mouth_and_smiling_eyes:': u'\U0001F604',
+ u':smiling_face_with_open_mouth_and_tightly-closed_eyes:': u'\U0001F606',
+ u':smiling_face_with_smiling_eyes:': u'\U0001F60A',
+ u':smiling_face_with_sunglasses:': u'\U0001F60E',
+ u':smiling_imp:': u'\U0001F608',
+ u':smirk:': u'\U0001F60F',
+ u':smirk_cat:': u'\U0001F63C',
+ u':smirking_face:': u'\U0001F60F',
+ u':smoking:': u'\U0001F6AC',
+ u':smoking_symbol:': u'\U0001F6AC',
+ u':snail:': u'\U0001F40C',
+ u':snake:': u'\U0001F40D',
+ u':snow_capped_mountain:': u'\U0001F3D4',
+ u':snow_cloud:': u'\U0001F328',
+ u':snowboarder:': u'\U0001F3C2',
+ u':snowflake:': u'\U00002744',
+ u':snowman:': u'\U00002603',
+ u':snowman_without_snow:': u'\U000026C4',
+ u':sob:': u'\U0001F62D',
+ u':soccer:': u'\U000026BD',
+ u':soccer_ball:': u'\U000026BD',
+ u':soft_ice_cream:': u'\U0001F366',
+ u':soon:': u'\U0001F51C',
+ u':soon_with_rightwards_arrow_above:': u'\U0001F51C',
+ u':sos:': u'\U0001F198',
+ u':sound:': u'\U0001F509',
+ u':south_east_arrow:': u'\U00002198',
+ u':south_west_arrow:': u'\U00002199',
+ u':space_invader:': u'\U0001F47E',
+ u':spades:': u'\U00002660',
+ u':spaghetti:': u'\U0001F35D',
+ u':sparkle:': u'\U00002747',
+ u':sparkler:': u'\U0001F387',
+ u':sparkles:': u'\U00002728',
+ u':sparkling_heart:': u'\U0001F496',
+ u':speak-no-evil_monkey:': u'\U0001F64A',
+ u':speak_no_evil:': u'\U0001F64A',
+ u':speaker:': u'\U0001F508',
+ u':speaker_with_cancellation_stroke:': u'\U0001F507',
+ u':speaker_with_one_sound_wave:': u'\U0001F509',
+ u':speaker_with_three_sound_waves:': u'\U0001F50A',
+ u':speaking_head_in_silhouette:': u'\U0001F5E3',
+ u':speech_balloon:': u'\U0001F4AC',
+ u':speedboat:': u'\U0001F6A4',
+ u':spider:': u'\U0001F577',
+ u':spider_web:': u'\U0001F578',
+ u':spiral_calendar_pad:': u'\U0001F5D3',
+ u':spiral_note_pad:': u'\U0001F5D2',
+ u':spiral_shell:': u'\U0001F41A',
+ u':splashing_sweat_symbol:': u'\U0001F4A6',
+ u':spock-hand:': u'\U0001F596',
+ u':spock_hand:': u'\U0001F596',
+ u':sports_medal:': u'\U0001F3C5',
+ u':spouting_whale:': u'\U0001F433',
+ u':squared_cl:': u'\U0001F191',
+ u':squared_cool:': u'\U0001F192',
+ u':squared_free:': u'\U0001F193',
+ u':squared_id:': u'\U0001F194',
+ u':squared_katakana_koko:': u'\U0001F201',
+ u':squared_katakana_sa:': u'\U0001F202',
+ u':squared_new:': u'\U0001F195',
+ u':squared_ng:': u'\U0001F196',
+ u':squared_ok:': u'\U0001F197',
+ u':squared_sos:': u'\U0001F198',
+ u':squared_up_with_exclamation_mark:': u'\U0001F199',
+ u':squared_vs:': u'\U0001F19A',
+ u':stadium:': u'\U0001F3DF',
+ u':star2:': u'\U0001F31F',
+ u':star:': u'\U00002B50',
+ u':star_and_crescent:': u'\U0000262A',
+ u':star_of_david:': u'\U00002721',
+ u':stars:': u'\U0001F320',
+ u':station:': u'\U0001F689',
+ u':statue_of_liberty:': u'\U0001F5FD',
+ u':steam_locomotive:': u'\U0001F682',
+ u':steaming_bowl:': u'\U0001F35C',
+ u':stew:': u'\U0001F372',
+ u':stopwatch:': u'\U000023F1',
+ u':straight_ruler:': u'\U0001F4CF',
+ u':strawberry:': u'\U0001F353',
+ u':stuck_out_tongue:': u'\U0001F61B',
+ u':stuck_out_tongue_closed_eyes:': u'\U0001F61D',
+ u':stuck_out_tongue_winking_eye:': u'\U0001F61C',
+ u':studio_microphone:': u'\U0001F399',
+ u':sun_behind_cloud:': u'\U000026C5',
+ u':sun_with_face:': u'\U0001F31E',
+ u':sunflower:': u'\U0001F33B',
+ u':sunglasses:': u'\U0001F60E',
+ u':sunny:': u'\U00002600',
+ u':sunrise:': u'\U0001F305',
+ u':sunrise_over_mountains:': u'\U0001F304',
+ u':sunset_over_buildings:': u'\U0001F307',
+ u':surfer:': u'\U0001F3C4',
+ u':sushi:': u'\U0001F363',
+ u':suspension_railway:': u'\U0001F69F',
+ u':sweat:': u'\U0001F613',
+ u':sweat_drops:': u'\U0001F4A6',
+ u':sweat_smile:': u'\U0001F605',
+ u':sweet_potato:': u'\U0001F360',
+ u':swimmer:': u'\U0001F3CA',
+ u':symbols:': u'\U0001F523',
+ u':synagogue:': u'\U0001F54D',
+ u':syringe:': u'\U0001F489',
+ u':t-shirt:': u'\U0001F455',
+ u':table_tennis_paddle_and_ball:': u'\U0001F3D3',
+ u':taco:': u'\U0001F32E',
+ u':tada:': u'\U0001F389',
+ u':tanabata_tree:': u'\U0001F38B',
+ u':tangerine:': u'\U0001F34A',
+ u':taurus:': u'\U00002649',
+ u':taxi:': u'\U0001F695',
+ u':tea:': u'\U0001F375',
+ u':teacup_without_handle:': u'\U0001F375',
+ u':tear-off_calendar:': u'\U0001F4C6',
+ u':telephone:': u'\U0000260E',
+ u':telephone_receiver:': u'\U0001F4DE',
+ u':telescope:': u'\U0001F52D',
+ u':television:': u'\U0001F4FA',
+ u':ten:': u'\U0001F51F',
+ u':tennis:': u'\U0001F3BE',
+ u':tennis_racquet_and_ball:': u'\U0001F3BE',
+ u':tent:': u'\U000026FA',
+ u':the_horns:': u'\U0001F918',
+ u':thermometer:': u'\U0001F321',
+ u':thinking_face:': u'\U0001F914',
+ u':thought_balloon:': u'\U0001F4AD',
+ u':three_button_mouse:': u'\U0001F5B1',
+ u':thumbs_down_sign:': u'\U0001F44E',
+ u':thumbs_up_sign:': u'\U0001F44D',
+ u':thumbsdown:': u'\U0001F44E',
+ u':thumbsup:': u'\U0001F44D',
+ u':thunder_cloud_and_rain:': u'\U000026C8',
+ u':ticket:': u'\U0001F3AB',
+ u':tiger2:': u'\U0001F405',
+ u':tiger:': u'\U0001F405',
+ u':tiger:': u'\U0001F42F',
+ u':tiger_face:': u'\U0001F42F',
+ u':timer_clock:': u'\U000023F2',
+ u':tired_face:': u'\U0001F62B',
+ u':tm:': u'\U00002122',
+ u':toilet:': u'\U0001F6BD',
+ u':tokyo_tower:': u'\U0001F5FC',
+ u':tomato:': u'\U0001F345',
+ u':tongue:': u'\U0001F445',
+ u':top:': u'\U0001F51D',
+ u':top_hat:': u'\U0001F3A9',
+ u':top_with_upwards_arrow_above:': u'\U0001F51D',
+ u':tophat:': u'\U0001F3A9',
+ u':tornado:': u'\U0001F32A',
+ u':trackball:': u'\U0001F5B2',
+ u':tractor:': u'\U0001F69C',
+ u':trade_mark_sign:': u'\U00002122',
+ u':traffic_light:': u'\U0001F6A5',
+ u':train2:': u'\U0001F686',
+ u':train:': u'\U0001F686',
+ u':train:': u'\U0001F68B',
+ u':tram:': u'\U0001F68A',
+ u':tram_car:': u'\U0001F68B',
+ u':triangular_flag_on_post:': u'\U0001F6A9',
+ u':triangular_ruler:': u'\U0001F4D0',
+ u':trident:': u'\U0001F531',
+ u':trident_emblem:': u'\U0001F531',
+ u':triumph:': u'\U0001F624',
+ u':trolleybus:': u'\U0001F68E',
+ u':trophy:': u'\U0001F3C6',
+ u':tropical_drink:': u'\U0001F379',
+ u':tropical_fish:': u'\U0001F420',
+ u':truck:': u'\U0001F69A',
+ u':trumpet:': u'\U0001F3BA',
+ u':tshirt:': u'\U0001F455',
+ u':tulip:': u'\U0001F337',
+ u':turkey:': u'\U0001F983',
+ u':turtle:': u'\U0001F422',
+ u':tv:': u'\U0001F4FA',
+ u':twisted_rightwards_arrows:': u'\U0001F500',
+ u':two_hearts:': u'\U0001F495',
+ u':two_men_holding_hands:': u'\U0001F46C',
+ u':two_women_holding_hands:': u'\U0001F46D',
+ u':umbrella:': u'\U00002602',
+ u':umbrella_on_ground:': u'\U000026F1',
+ u':umbrella_with_rain_drops:': u'\U00002614',
+ u':unamused:': u'\U0001F612',
+ u':unamused_face:': u'\U0001F612',
+ u':underage:': u'\U0001F51E',
+ u':unicorn_face:': u'\U0001F984',
+ u':unlock:': u'\U0001F513',
+ u':up-pointing_red_triangle:': u'\U0001F53A',
+ u':up-pointing_small_red_triangle:': u'\U0001F53C',
+ u':up:': u'\U0001F199',
+ u':up_down_arrow:': u'\U00002195',
+ u':upside-down_face:': u'\U0001F643',
+ u':upside_down_face:': u'\U0001F643',
+ u':upwards_black_arrow:': u'\U00002B06',
+ u':v:': u'\U0000270C',
+ u':vertical_traffic_light:': u'\U0001F6A6',
+ u':vhs:': u'\U0001F4FC',
+ u':vibration_mode:': u'\U0001F4F3',
+ u':victory_hand:': u'\U0000270C',
+ u':video_camera:': u'\U0001F4F9',
+ u':video_game:': u'\U0001F3AE',
+ u':videocassette:': u'\U0001F4FC',
+ u':violin:': u'\U0001F3BB',
+ u':virgo:': u'\U0000264D',
+ u':volcano:': u'\U0001F30B',
+ u':volleyball:': u'\U0001F3D0',
+ u':vs:': u'\U0001F19A',
+ u':walking:': u'\U0001F6B6',
+ u':waning_crescent_moon:': u'\U0001F318',
+ u':waning_crescent_moon_symbol:': u'\U0001F318',
+ u':waning_gibbous_moon:': u'\U0001F316',
+ u':waning_gibbous_moon_symbol:': u'\U0001F316',
+ u':warning:': u'\U000026A0',
+ u':warning_sign:': u'\U000026A0',
+ u':wastebasket:': u'\U0001F5D1',
+ u':watch:': u'\U0000231A',
+ u':water_buffalo:': u'\U0001F403',
+ u':water_closet:': u'\U0001F6BE',
+ u':water_wave:': u'\U0001F30A',
+ u':watermelon:': u'\U0001F349',
+ u':wave:': u'\U0001F44B',
+ u':waving_black_flag:': u'\U0001F3F4',
+ u':waving_hand_sign:': u'\U0001F44B',
+ u':waving_white_flag:': u'\U0001F3F3',
+ u':wavy_dash:': u'\U00003030',
+ u':waxing_crescent_moon:': u'\U0001F312',
+ u':waxing_crescent_moon_symbol:': u'\U0001F312',
+ u':waxing_gibbous_moon:': u'\U0001F314',
+ u':waxing_gibbous_moon_symbol:': u'\U0001F314',
+ u':wc:': u'\U0001F6BE',
+ u':weary:': u'\U0001F629',
+ u':weary_cat_face:': u'\U0001F640',
+ u':weary_face:': u'\U0001F629',
+ u':wedding:': u'\U0001F492',
+ u':weight_lifter:': u'\U0001F3CB',
+ u':whale2:': u'\U0001F40B',
+ u':whale:': u'\U0001F40B',
+ u':whale:': u'\U0001F433',
+ u':wheel_of_dharma:': u'\U00002638',
+ u':wheelchair:': u'\U0000267F',
+ u':wheelchair_symbol:': u'\U0000267F',
+ u':white_check_mark:': u'\U00002705',
+ u':white_circle:': u'\U000026AA',
+ u':white_down_pointing_backhand_index:': u'\U0001F447',
+ u':white_exclamation_mark_ornament:': u'\U00002755',
+ u':white_flower:': u'\U0001F4AE',
+ u':white_frowning_face:': u'\U00002639',
+ u':white_heavy_check_mark:': u'\U00002705',
+ u':white_large_square:': u'\U00002B1C',
+ u':white_left_pointing_backhand_index:': u'\U0001F448',
+ u':white_medium_small_square:': u'\U000025FD',
+ u':white_medium_square:': u'\U000025FB',
+ u':white_medium_star:': u'\U00002B50',
+ u':white_question_mark_ornament:': u'\U00002754',
+ u':white_right_pointing_backhand_index:': u'\U0001F449',
+ u':white_small_square:': u'\U000025AB',
+ u':white_smiling_face:': u'\U0000263A',
+ u':white_square_button:': u'\U0001F533',
+ u':white_sun_behind_cloud:': u'\U0001F325',
+ u':white_sun_behind_cloud_with_rain:': u'\U0001F326',
+ u':white_sun_with_small_cloud:': u'\U0001F324',
+ u':white_up_pointing_backhand_index:': u'\U0001F446',
+ u':white_up_pointing_index:': u'\U0000261D',
+ u':wind_blowing_face:': u'\U0001F32C',
+ u':wind_chime:': u'\U0001F390',
+ u':wine_glass:': u'\U0001F377',
+ u':wink:': u'\U0001F609',
+ u':winking_face:': u'\U0001F609',
+ u':wolf:': u'\U0001F43A',
+ u':wolf_face:': u'\U0001F43A',
+ u':woman:': u'\U0001F469',
+ u':woman_with_bunny_ears:': u'\U0001F46F',
+ u':womans_boots:': u'\U0001F462',
+ u':womans_clothes:': u'\U0001F45A',
+ u':womans_hat:': u'\U0001F452',
+ u':womans_sandal:': u'\U0001F461',
+ u':womens:': u'\U0001F6BA',
+ u':womens_symbol:': u'\U0001F6BA',
+ u':world_map:': u'\U0001F5FA',
+ u':worried:': u'\U0001F61F',
+ u':worried_face:': u'\U0001F61F',
+ u':wrapped_present:': u'\U0001F381',
+ u':wrench:': u'\U0001F527',
+ u':writing_hand:': u'\U0000270D',
+ u':x:': u'\U0000274C',
+ u':yellow_heart:': u'\U0001F49B',
+ u':yen:': u'\U0001F4B4',
+ u':yin_yang:': u'\U0000262F',
+ u':yum:': u'\U0001F60B',
+ u':zap:': u'\U000026A1',
+ u':zipper-mouth_face:': u'\U0001F910',
+ u':zipper_mouth_face:': u'\U0001F910',
+ u':zzz:': u'\U0001F4A4',
+}
+ALIAS_RE = re.compile(r':[+-]?[\w-]+:', flags=re.DOTALL)
+NEEDSPLIT = ('irc_in_PRIVMSG', 'irc_in_NOTICE', 'irc_in_PART', 'irc_in_QUIT', 'irc_in_KNOCK', 'irc_in_AWAY')
+
+HOOKS = (
+ "away",
+ "cnotice",
+ "cprivmsg",
+ "kick",
+ "knock",
+ "notice",
+ "part",
+ "privmsg",
+ "quit",
+ "wallops",
+)
+
+
+def convert_aliases_to_emoji(data, modifier, modifier_data, string):
+ if modifier in NEEDSPLIT:
+ aliases_found = ALIAS_RE.findall(string.split(':', 1)[1])
+ else:
+ aliases_found = ALIAS_RE.findall(string)
+ for alias in aliases_found:
+ if alias in EMOJI_ALIASES:
+ string = string.replace(alias, '{} '.format(EMOJI_ALIASES[alias].encode('utf-8')))
+ return string
+
+
+for hook in HOOKS:
+ weechat.hook_modifier(
+ "irc_in_{0}".format(hook), "convert_aliases_to_emoji", "")
+weechat.hook_modifier("input_text_for_buffer", "convert_aliases_to_emoji", "")
diff --git a/weechat/python/grep.py b/weechat/python/grep.py
new file mode 100644
index 0000000..30bf2a6
--- /dev/null
+++ b/weechat/python/grep.py
@@ -0,0 +1,1738 @@
+# -*- coding: utf-8 -*-
+###
+# Copyright (c) 2009-2011 by Elián Hanisch <lambdae2@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+###
+
+###
+# Search in Weechat buffers and logs (for Weechat 0.3.*)
+#
+# Inspired by xt's grep.py
+# Originally I just wanted to add some fixes in grep.py, but then
+# I got carried away and rewrote everything, so new script.
+#
+# Commands:
+# * /grep
+# Search in logs or buffers, see /help grep
+# * /logs:
+# Lists logs in ~/.weechat/logs, see /help logs
+#
+# Settings:
+# * plugins.var.python.grep.clear_buffer:
+# Clear the results buffer before each search. Valid values: on, off
+#
+# * plugins.var.python.grep.go_to_buffer:
+# Automatically go to grep buffer when search is over. Valid values: on, off
+#
+# * plugins.var.python.grep.log_filter:
+# Coma separated list of patterns that grep will use for exclude logs, e.g.
+# if you use '*server/*' any log in the 'server' folder will be excluded
+# when using the command '/grep log'
+#
+# * plugins.var.python.grep.show_summary:
+# Shows summary for each log. Valid values: on, off
+#
+# * plugins.var.python.grep.max_lines:
+# Grep will only print the last matched lines that don't surpass the value defined here.
+#
+# * plugins.var.python.grep.size_limit:
+# Size limit in KiB, is used for decide whenever grepping should run in background or not. If
+# the logs to grep have a total size bigger than this value then grep run as a new process.
+# It can be used for force or disable background process, using '0' forces to always grep in
+# background, while using '' (empty string) will disable it.
+#
+# * plugins.var.python.grep.default_tail_head:
+# Config option for define default number of lines returned when using --head or --tail options.
+# Can be overriden in the command with --number option.
+#
+#
+# TODO:
+# * try to figure out why hook_process chokes in long outputs (using a tempfile as a
+# workaround now)
+# * possibly add option for defining time intervals
+#
+#
+# History:
+#
+# 2016-06-23, mickael9
+# version 0.7.7: fix get_home function
+#
+# 2015-11-26
+# version 0.7.6: fix a typo
+#
+# 2015-01-31, Nicd-
+# version 0.7.5:
+# '~' is now expaned to the home directory in the log file path so
+# paths like '~/logs/' should work.
+#
+# 2015-01-14, nils_2
+# version 0.7.4: make q work to quit grep buffer (requested by: gb)
+#
+# 2014-03-29, Felix Eckhofer <felix@tribut.de>
+# version 0.7.3: fix typo
+#
+# 2011-01-09
+# version 0.7.2: bug fixes
+#
+# 2010-11-15
+# version 0.7.1:
+# * use TempFile so temporal files are guaranteed to be deleted.
+# * enable Archlinux workaround.
+#
+# 2010-10-26
+# version 0.7:
+# * added templates.
+# * using --only-match shows only unique strings.
+# * fixed bug that inverted -B -A switches when used with -t
+#
+# 2010-10-14
+# version 0.6.8: by xt <xt@bash.no>
+# * supress highlights when printing in grep buffer
+#
+# 2010-10-06
+# version 0.6.7: by xt <xt@bash.no>
+# * better temporary file:
+# use tempfile.mkstemp. to create a temp file in log dir,
+# makes it safer with regards to write permission and multi user
+#
+# 2010-04-08
+# version 0.6.6: bug fixes
+# * use WEECHAT_LIST_POS_END in log file completion, makes completion faster
+# * disable bytecode if using python 2.6
+# * use single quotes in command string
+# * fix bug that could change buffer's title when using /grep stop
+#
+# 2010-01-24
+# version 0.6.5: disable bytecode is a 2.6 feature, instead, resort to delete the bytecode manually
+#
+# 2010-01-19
+# version 0.6.4: bug fix
+# version 0.6.3: added options --invert --only-match (replaces --exact, which is still available
+# but removed from help)
+# * use new 'irc_nick_color' info
+# * don't generate bytecode when spawning a new process
+# * show active options in buffer title
+#
+# 2010-01-17
+# version 0.6.2: removed 2.6-ish code
+# version 0.6.1: fixed bug when grepping in grep's buffer
+#
+# 2010-01-14
+# version 0.6.0: implemented grep in background
+# * improved context lines presentation.
+# * grepping for big (or many) log files runs in a weechat_process.
+# * added /grep stop.
+# * added 'size_limit' option
+# * fixed a infolist leak when grepping buffers
+# * added 'default_tail_head' option
+# * results are sort by line count
+# * don't die if log is corrupted (has NULL chars in it)
+# * changed presentation of /logs
+# * log path completion doesn't suck anymore
+# * removed all tabs, because I learned how to configure Vim so that spaces aren't annoying
+# anymore. This was the script's original policy.
+#
+# 2010-01-05
+# version 0.5.5: rename script to 'grep.py' (FlashCode <flashcode@flashtux.org>).
+#
+# 2010-01-04
+# version 0.5.4.1: fix index error when using --after/before-context options.
+#
+# 2010-01-03
+# version 0.5.4: new features
+# * added --after-context and --before-context options.
+# * added --context as a shortcut for using both -A -B options.
+#
+# 2009-11-06
+# version 0.5.3: improvements for long grep output
+# * grep buffer input accepts the same flags as /grep for repeat a search with different
+# options.
+# * tweaks in grep's output.
+# * max_lines option added for limit grep's output.
+# * code in update_buffer() optimized.
+# * time stats in buffer title.
+# * added go_to_buffer config option.
+# * added --buffer for search only in buffers.
+# * refactoring.
+#
+# 2009-10-12, omero
+# version 0.5.2: made it python-2.4.x compliant
+#
+# 2009-08-17
+# version 0.5.1: some refactoring, show_summary option added.
+#
+# 2009-08-13
+# version 0.5: rewritten from xt's grep.py
+# * fixed searching in non weechat logs, for cases like, if you're
+# switching from irssi and rename and copy your irssi logs to %h/logs
+# * fixed "timestamp rainbow" when you /grep in grep's buffer
+# * allow to search in other buffers other than current or in logs
+# of currently closed buffers with cmd 'buffer'
+# * allow to search in any log file in %h/logs with cmd 'log'
+# * added --count for return the number of matched lines
+# * added --matchcase for case sensible search
+# * added --hilight for color matches
+# * added --head and --tail options, and --number
+# * added command /logs for list files in %h/logs
+# * added config option for clear the buffer before a search
+# * added config option for filter logs we don't want to grep
+# * added the posibility to repeat last search with another regexp by writing
+# it in grep's buffer
+# * changed spaces for tabs in the code, which is my preference
+#
+###
+
+from os import path
+import sys, getopt, time, os, re, tempfile
+
+try:
+ import weechat
+ from weechat import WEECHAT_RC_OK, prnt, prnt_date_tags
+ import_ok = True
+except ImportError:
+ import_ok = False
+
+SCRIPT_NAME = "grep"
+SCRIPT_AUTHOR = "Elián Hanisch <lambdae2@gmail.com>"
+SCRIPT_VERSION = "0.7.7"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Search in buffers and logs"
+SCRIPT_COMMAND = "grep"
+
+### Default Settings ###
+settings = {
+'clear_buffer' : 'off',
+'log_filter' : '',
+'go_to_buffer' : 'on',
+'max_lines' : '4000',
+'show_summary' : 'on',
+'size_limit' : '2048',
+'default_tail_head' : '10',
+}
+
+### Class definitions ###
+class linesDict(dict):
+ """
+ Class for handling matched lines in more than one buffer.
+ linesDict[buffer_name] = matched_lines_list
+ """
+ def __setitem__(self, key, value):
+ assert isinstance(value, list)
+ if key not in self:
+ dict.__setitem__(self, key, value)
+ else:
+ dict.__getitem__(self, key).extend(value)
+
+ def get_matches_count(self):
+ """Return the sum of total matches stored."""
+ if dict.__len__(self):
+ return sum(map(lambda L: L.matches_count, self.itervalues()))
+ else:
+ return 0
+
+ def __len__(self):
+ """Return the sum of total lines stored."""
+ if dict.__len__(self):
+ return sum(map(len, self.itervalues()))
+ else:
+ return 0
+
+ def __str__(self):
+ """Returns buffer count or buffer name if there's just one stored."""
+ n = len(self.keys())
+ if n == 1:
+ return self.keys()[0]
+ elif n > 1:
+ return '%s logs' %n
+ else:
+ return ''
+
+ def items(self):
+ """Returns a list of items sorted by line count."""
+ items = dict.items(self)
+ items.sort(key=lambda i: len(i[1]))
+ return items
+
+ def items_count(self):
+ """Returns a list of items sorted by match count."""
+ items = dict.items(self)
+ items.sort(key=lambda i: i[1].matches_count)
+ return items
+
+ def strip_separator(self):
+ for L in self.itervalues():
+ L.strip_separator()
+
+ def get_last_lines(self, n):
+ total_lines = len(self)
+ #debug('total: %s n: %s' %(total_lines, n))
+ if n >= total_lines:
+ # nothing to do
+ return
+ for k, v in reversed(self.items()):
+ l = len(v)
+ if n > 0:
+ if l > n:
+ del v[:l-n]
+ v.stripped_lines = l-n
+ n -= l
+ else:
+ del v[:]
+ v.stripped_lines = l
+
+class linesList(list):
+ """Class for list of matches, since sometimes I need to add lines that aren't matches, I need an
+ independent counter."""
+ _sep = '...'
+ def __init__(self, *args):
+ list.__init__(self, *args)
+ self.matches_count = 0
+ self.stripped_lines = 0
+
+ def append(self, item):
+ """Append lines, can be a string or a list with strings."""
+ if isinstance(item, str):
+ list.append(self, item)
+ else:
+ self.extend(item)
+
+ def append_separator(self):
+ """adds a separator into the list, makes sure it doen't add two together."""
+ s = self._sep
+ if (self and self[-1] != s) or not self:
+ self.append(s)
+
+ def onlyUniq(self):
+ s = set(self)
+ del self[:]
+ self.extend(s)
+
+ def count_match(self, item=None):
+ if item is None or isinstance(item, str):
+ self.matches_count += 1
+ else:
+ self.matches_count += len(item)
+
+ def strip_separator(self):
+ """removes separators if there are first or/and last in the list."""
+ if self:
+ s = self._sep
+ if self[0] == s:
+ del self[0]
+ if self[-1] == s:
+ del self[-1]
+
+### Misc functions ###
+now = time.time
+def get_size(f):
+ try:
+ return os.stat(f).st_size
+ except OSError:
+ return 0
+
+sizeDict = {0:'b', 1:'KiB', 2:'MiB', 3:'GiB', 4:'TiB'}
+def human_readable_size(size):
+ power = 0
+ while size > 1024:
+ power += 1
+ size /= 1024.0
+ return '%.2f %s' %(size, sizeDict.get(power, ''))
+
+def color_nick(nick):
+ """Returns coloured nick, with coloured mode if any."""
+ if not nick: return ''
+ wcolor = weechat.color
+ config_string = lambda s : weechat.config_string(weechat.config_get(s))
+ config_int = lambda s : weechat.config_integer(weechat.config_get(s))
+ # prefix and suffix
+ prefix = config_string('irc.look.nick_prefix')
+ suffix = config_string('irc.look.nick_suffix')
+ prefix_c = suffix_c = wcolor(config_string('weechat.color.chat_delimiters'))
+ if nick[0] == prefix:
+ nick = nick[1:]
+ else:
+ prefix = prefix_c = ''
+ if nick[-1] == suffix:
+ nick = nick[:-1]
+ suffix = wcolor(color_delimiter) + suffix
+ else:
+ suffix = suffix_c = ''
+ # nick mode
+ modes = '@!+%'
+ if nick[0] in modes:
+ mode, nick = nick[0], nick[1:]
+ mode_color = wcolor(config_string('weechat.color.nicklist_prefix%d' \
+ %(modes.find(mode) + 1)))
+ else:
+ mode = mode_color = ''
+ # nick color
+ nick_color = weechat.info_get('irc_nick_color', nick)
+ if not nick_color:
+ # probably we're in WeeChat 0.3.0
+ #debug('no irc_nick_color')
+ color_nicks_number = config_int('weechat.look.color_nicks_number')
+ idx = (sum(map(ord, nick))%color_nicks_number) + 1
+ nick_color = wcolor(config_string('weechat.color.chat_nick_color%02d' %idx))
+ return ''.join((prefix_c, prefix, mode_color, mode, nick_color, nick, suffix_c, suffix))
+
+### Config and value validation ###
+boolDict = {'on':True, 'off':False}
+def get_config_boolean(config):
+ value = weechat.config_get_plugin(config)
+ try:
+ return boolDict[value]
+ except KeyError:
+ default = settings[config]
+ error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
+ error("'%s' is invalid, allowed: 'on', 'off'" %value)
+ return boolDict[default]
+
+def get_config_int(config, allow_empty_string=False):
+ value = weechat.config_get_plugin(config)
+ try:
+ return int(value)
+ except ValueError:
+ if value == '' and allow_empty_string:
+ return value
+ default = settings[config]
+ error("Error while fetching config '%s'. Using default value '%s'." %(config, default))
+ error("'%s' is not a number." %value)
+ return int(default)
+
+def get_config_log_filter():
+ filter = weechat.config_get_plugin('log_filter')
+ if filter:
+ return filter.split(',')
+ else:
+ return []
+
+def get_home():
+ home = weechat.config_string(weechat.config_get('logger.file.path'))
+ home = home.replace('%h', weechat.info_get('weechat_dir', ''))
+ home = path.abspath(path.expanduser(home))
+ return home
+
+def strip_home(s, dir=''):
+ """Strips home dir from the begging of the log path, this makes them sorter."""
+ if not dir:
+ global home_dir
+ dir = home_dir
+ l = len(dir)
+ if s[:l] == dir:
+ return s[l:]
+ return s
+
+### Messages ###
+script_nick = SCRIPT_NAME
+def error(s, buffer=''):
+ """Error msg"""
+ prnt(buffer, '%s%s %s' %(weechat.prefix('error'), script_nick, s))
+ if weechat.config_get_plugin('debug'):
+ import traceback
+ if traceback.sys.exc_type:
+ trace = traceback.format_exc()
+ prnt('', trace)
+
+def say(s, buffer=''):
+ """normal msg"""
+ prnt_date_tags(buffer, 0, 'no_highlight', '%s\t%s' %(script_nick, s))
+
+
+
+### Log files and buffers ###
+cache_dir = {} # note: don't remove, needed for completion if the script was loaded recently
+def dir_list(dir, filter_list=(), filter_excludes=True, include_dir=False):
+ """Returns a list of files in 'dir' and its subdirs."""
+ global cache_dir
+ from os import walk
+ from fnmatch import fnmatch
+ #debug('dir_list: listing in %s' %dir)
+ key = (dir, include_dir)
+ try:
+ return cache_dir[key]
+ except KeyError:
+ pass
+
+ filter_list = filter_list or get_config_log_filter()
+ dir_len = len(dir)
+ if filter_list:
+ def filter(file):
+ file = file[dir_len:] # pattern shouldn't match home dir
+ for pattern in filter_list:
+ if fnmatch(file, pattern):
+ return filter_excludes
+ return not filter_excludes
+ else:
+ filter = lambda f : not filter_excludes
+
+ file_list = []
+ extend = file_list.extend
+ join = path.join
+ def walk_path():
+ for basedir, subdirs, files in walk(dir):
+ #if include_dir:
+ # subdirs = map(lambda s : join(s, ''), subdirs)
+ # files.extend(subdirs)
+ files_path = map(lambda f : join(basedir, f), files)
+ files_path = [ file for file in files_path if not filter(file) ]
+ extend(files_path)
+
+ walk_path()
+ cache_dir[key] = file_list
+ #debug('dir_list: got %s' %str(file_list))
+ return file_list
+
+def get_file_by_pattern(pattern, all=False):
+ """Returns the first log whose path matches 'pattern',
+ if all is True returns all logs that matches."""
+ if not pattern: return []
+ #debug('get_file_by_filename: searching for %s.' %pattern)
+ # do envvar expandsion and check file
+ file = path.expanduser(pattern)
+ file = path.expandvars(file)
+ if path.isfile(file):
+ return [file]
+ # lets see if there's a matching log
+ global home_dir
+ file = path.join(home_dir, pattern)
+ if path.isfile(file):
+ return [file]
+ else:
+ from fnmatch import fnmatch
+ file = []
+ file_list = dir_list(home_dir)
+ n = len(home_dir)
+ for log in file_list:
+ basename = log[n:]
+ if fnmatch(basename, pattern):
+ file.append(log)
+ #debug('get_file_by_filename: got %s.' %file)
+ if not all and file:
+ file.sort()
+ return [ file[-1] ]
+ return file
+
+def get_file_by_buffer(buffer):
+ """Given buffer pointer, finds log's path or returns None."""
+ #debug('get_file_by_buffer: searching for %s' %buffer)
+ infolist = weechat.infolist_get('logger_buffer', '', '')
+ if not infolist: return
+ try:
+ while weechat.infolist_next(infolist):
+ pointer = weechat.infolist_pointer(infolist, 'buffer')
+ if pointer == buffer:
+ file = weechat.infolist_string(infolist, 'log_filename')
+ if weechat.infolist_integer(infolist, 'log_enabled'):
+ #debug('get_file_by_buffer: got %s' %file)
+ return file
+ #else:
+ # debug('get_file_by_buffer: got %s but log not enabled' %file)
+ finally:
+ #debug('infolist gets freed')
+ weechat.infolist_free(infolist)
+
+def get_file_by_name(buffer_name):
+ """Given a buffer name, returns its log path or None. buffer_name should be in 'server.#channel'
+ or '#channel' format."""
+ #debug('get_file_by_name: searching for %s' %buffer_name)
+ # common mask options
+ config_masks = ('logger.mask.irc', 'logger.file.mask')
+ # since there's no buffer pointer, we try to replace some local vars in mask, like $channel and
+ # $server, then replace the local vars left with '*', and use it as a mask for get the path with
+ # get_file_by_pattern
+ for config in config_masks:
+ mask = weechat.config_string(weechat.config_get(config))
+ #debug('get_file_by_name: mask: %s' %mask)
+ if '$name' in mask:
+ mask = mask.replace('$name', buffer_name)
+ elif '$channel' in mask or '$server' in mask:
+ if '.' in buffer_name and \
+ '#' not in buffer_name[:buffer_name.find('.')]: # the dot isn't part of the channel name
+ # ^ I'm asuming channel starts with #, i'm lazy.
+ server, channel = buffer_name.split('.', 1)
+ else:
+ server, channel = '*', buffer_name
+ if '$channel' in mask:
+ mask = mask.replace('$channel', channel)
+ if '$server' in mask:
+ mask = mask.replace('$server', server)
+ # change the unreplaced vars by '*'
+ from string import letters
+ if '%' in mask:
+ # vars for time formatting
+ mask = mask.replace('%', '$')
+ if '$' in mask:
+ masks = mask.split('$')
+ masks = map(lambda s: s.lstrip(letters), masks)
+ mask = '*'.join(masks)
+ if mask[0] != '*':
+ mask = '*' + mask
+ #debug('get_file_by_name: using mask %s' %mask)
+ file = get_file_by_pattern(mask)
+ #debug('get_file_by_name: got file %s' %file)
+ if file:
+ return file
+ return None
+
+def get_buffer_by_name(buffer_name):
+ """Given a buffer name returns its buffer pointer or None."""
+ #debug('get_buffer_by_name: searching for %s' %buffer_name)
+ pointer = weechat.buffer_search('', buffer_name)
+ if not pointer:
+ try:
+ infolist = weechat.infolist_get('buffer', '', '')
+ while weechat.infolist_next(infolist):
+ short_name = weechat.infolist_string(infolist, 'short_name')
+ name = weechat.infolist_string(infolist, 'name')
+ if buffer_name in (short_name, name):
+ #debug('get_buffer_by_name: found %s' %name)
+ pointer = weechat.buffer_search('', name)
+ return pointer
+ finally:
+ weechat.infolist_free(infolist)
+ #debug('get_buffer_by_name: got %s' %pointer)
+ return pointer
+
+def get_all_buffers():
+ """Returns list with pointers of all open buffers."""
+ buffers = []
+ infolist = weechat.infolist_get('buffer', '', '')
+ while weechat.infolist_next(infolist):
+ buffers.append(weechat.infolist_pointer(infolist, 'pointer'))
+ weechat.infolist_free(infolist)
+ grep_buffer = weechat.buffer_search('python', SCRIPT_NAME)
+ if grep_buffer and grep_buffer in buffers:
+ # remove it from list
+ del buffers[buffers.index(grep_buffer)]
+ return buffers
+
+### Grep ###
+def make_regexp(pattern, matchcase=False):
+ """Returns a compiled regexp."""
+ if pattern in ('.', '.*', '.?', '.+'):
+ # because I don't need to use a regexp if we're going to match all lines
+ return None
+ # matching takes a lot more time if pattern starts or ends with .* and it isn't needed.
+ if pattern[:2] == '.*':
+ pattern = pattern[2:]
+ if pattern[-2:] == '.*':
+ pattern = pattern[:-2]
+ try:
+ if not matchcase:
+ regexp = re.compile(pattern, re.IGNORECASE)
+ else:
+ regexp = re.compile(pattern)
+ except Exception, e:
+ raise Exception, 'Bad pattern, %s' %e
+ return regexp
+
+def check_string(s, regexp, hilight='', exact=False):
+ """Checks 's' with a regexp and returns it if is a match."""
+ if not regexp:
+ return s
+
+ elif exact:
+ matchlist = regexp.findall(s)
+ if matchlist:
+ if isinstance(matchlist[0], tuple):
+ # join tuples (when there's more than one match group in regexp)
+ return [ ' '.join(t) for t in matchlist ]
+ return matchlist
+
+ elif hilight:
+ matchlist = regexp.findall(s)
+ if matchlist:
+ if isinstance(matchlist[0], tuple):
+ # flatten matchlist
+ matchlist = [ item for L in matchlist for item in L if item ]
+ matchlist = list(set(matchlist)) # remove duplicates if any
+ # apply hilight
+ color_hilight, color_reset = hilight.split(',', 1)
+ for m in matchlist:
+ s = s.replace(m, '%s%s%s' % (color_hilight, m, color_reset))
+ return s
+
+ # no need for findall() here
+ elif regexp.search(s):
+ return s
+
+def grep_file(file, head, tail, after_context, before_context, count, regexp, hilight, exact, invert):
+ """Return a list of lines that match 'regexp' in 'file', if no regexp returns all lines."""
+ if count:
+ tail = head = after_context = before_context = False
+ hilight = ''
+ elif exact:
+ before_context = after_context = False
+ hilight = ''
+ elif invert:
+ hilight = ''
+ #debug(' '.join(map(str, (file, head, tail, after_context, before_context))))
+
+ lines = linesList()
+ # define these locally as it makes the loop run slightly faster
+ append = lines.append
+ count_match = lines.count_match
+ separator = lines.append_separator
+ if invert:
+ def check(s):
+ if check_string(s, regexp, hilight, exact):
+ return None
+ else:
+ return s
+ else:
+ check = lambda s: check_string(s, regexp, hilight, exact)
+
+ try:
+ file_object = open(file, 'r')
+ except IOError:
+ # file doesn't exist
+ return lines
+ if tail or before_context:
+ # for these options, I need to seek in the file, but is slower and uses a good deal of
+ # memory if the log is too big, so we do this *only* for these options.
+ file_lines = file_object.readlines()
+
+ if tail:
+ # instead of searching in the whole file and later pick the last few lines, we
+ # reverse the log, search until count reached and reverse it again, that way is a lot
+ # faster
+ file_lines.reverse()
+ # don't invert context switches
+ before_context, after_context = after_context, before_context
+
+ if before_context:
+ before_context_range = range(1, before_context + 1)
+ before_context_range.reverse()
+
+ limit = tail or head
+
+ line_idx = 0
+ while line_idx < len(file_lines):
+ line = file_lines[line_idx]
+ line = check(line)
+ if line:
+ if before_context:
+ separator()
+ trimmed = False
+ for id in before_context_range:
+ try:
+ context_line = file_lines[line_idx - id]
+ if check(context_line):
+ # match in before context, that means we appended these same lines in a
+ # previous match, so we delete them merging both paragraphs
+ if not trimmed:
+ del lines[id - before_context - 1:]
+ trimmed = True
+ else:
+ append(context_line)
+ except IndexError:
+ pass
+ append(line)
+ count_match(line)
+ if after_context:
+ id, offset = 0, 0
+ while id < after_context + offset:
+ id += 1
+ try:
+ context_line = file_lines[line_idx + id]
+ _context_line = check(context_line)
+ if _context_line:
+ offset = id
+ context_line = _context_line # so match is hilighted with --hilight
+ count_match()
+ append(context_line)
+ except IndexError:
+ pass
+ separator()
+ line_idx += id
+ if limit and lines.matches_count >= limit:
+ break
+ line_idx += 1
+
+ if tail:
+ lines.reverse()
+ else:
+ # do a normal grep
+ limit = head
+
+ for line in file_object:
+ line = check(line)
+ if line:
+ count or append(line)
+ count_match(line)
+ if after_context:
+ id, offset = 0, 0
+ while id < after_context + offset:
+ id += 1
+ try:
+ context_line = file_object.next()
+ _context_line = check(context_line)
+ if _context_line:
+ offset = id
+ context_line = _context_line
+ count_match()
+ count or append(context_line)
+ except StopIteration:
+ pass
+ separator()
+ if limit and lines.matches_count >= limit:
+ break
+
+ file_object.close()
+ return lines
+
+def grep_buffer(buffer, head, tail, after_context, before_context, count, regexp, hilight, exact,
+ invert):
+ """Return a list of lines that match 'regexp' in 'buffer', if no regexp returns all lines."""
+ lines = linesList()
+ if count:
+ tail = head = after_context = before_context = False
+ hilight = ''
+ elif exact:
+ before_context = after_context = False
+ #debug(' '.join(map(str, (tail, head, after_context, before_context, count, exact, hilight))))
+
+ # Using /grep in grep's buffer can lead to some funny effects
+ # We should take measures if that's the case
+ def make_get_line_funcion():
+ """Returns a function for get lines from the infolist, depending if the buffer is grep's or
+ not."""
+ string_remove_color = weechat.string_remove_color
+ infolist_string = weechat.infolist_string
+ grep_buffer = weechat.buffer_search('python', SCRIPT_NAME)
+ if grep_buffer and buffer == grep_buffer:
+ def function(infolist):
+ prefix = infolist_string(infolist, 'prefix')
+ message = infolist_string(infolist, 'message')
+ if prefix: # only our messages have prefix, ignore it
+ return None
+ return message
+ else:
+ infolist_time = weechat.infolist_time
+ def function(infolist):
+ prefix = string_remove_color(infolist_string(infolist, 'prefix'), '')
+ message = string_remove_color(infolist_string(infolist, 'message'), '')
+ date = infolist_time(infolist, 'date')
+ return '%s\t%s\t%s' %(date, prefix, message)
+ return function
+ get_line = make_get_line_funcion()
+
+ infolist = weechat.infolist_get('buffer_lines', buffer, '')
+ if tail:
+ # like with grep_file() if we need the last few matching lines, we move the cursor to
+ # the end and search backwards
+ infolist_next = weechat.infolist_prev
+ infolist_prev = weechat.infolist_next
+ else:
+ infolist_next = weechat.infolist_next
+ infolist_prev = weechat.infolist_prev
+ limit = head or tail
+
+ # define these locally as it makes the loop run slightly faster
+ append = lines.append
+ count_match = lines.count_match
+ separator = lines.append_separator
+ if invert:
+ def check(s):
+ if check_string(s, regexp, hilight, exact):
+ return None
+ else:
+ return s
+ else:
+ check = lambda s: check_string(s, regexp, hilight, exact)
+
+ if before_context:
+ before_context_range = range(1, before_context + 1)
+ before_context_range.reverse()
+
+ while infolist_next(infolist):
+ line = get_line(infolist)
+ if line is None: continue
+ line = check(line)
+ if line:
+ if before_context:
+ separator()
+ trimmed = False
+ for id in before_context_range:
+ if not infolist_prev(infolist):
+ trimmed = True
+ for id in before_context_range:
+ context_line = get_line(infolist)
+ if check(context_line):
+ if not trimmed:
+ del lines[id - before_context - 1:]
+ trimmed = True
+ else:
+ append(context_line)
+ infolist_next(infolist)
+ count or append(line)
+ count_match(line)
+ if after_context:
+ id, offset = 0, 0
+ while id < after_context + offset:
+ id += 1
+ if infolist_next(infolist):
+ context_line = get_line(infolist)
+ _context_line = check(context_line)
+ if _context_line:
+ context_line = _context_line
+ offset = id
+ count_match()
+ append(context_line)
+ else:
+ # in the main loop infolist_next will start again an cause an infinite loop
+ # this will avoid it
+ infolist_next = lambda x: 0
+ separator()
+ if limit and lines.matches_count >= limit:
+ break
+ weechat.infolist_free(infolist)
+
+ if tail:
+ lines.reverse()
+ return lines
+
+### this is our main grep function
+hook_file_grep = None
+def show_matching_lines():
+ """
+ Greps buffers in search_in_buffers or files in search_in_files and updates grep buffer with the
+ result.
+ """
+ global pattern, matchcase, number, count, exact, hilight, invert
+ global tail, head, after_context, before_context
+ global search_in_files, search_in_buffers, matched_lines, home_dir
+ global time_start
+ matched_lines = linesDict()
+ #debug('buffers:%s \nlogs:%s' %(search_in_buffers, search_in_files))
+ time_start = now()
+
+ # buffers
+ if search_in_buffers:
+ regexp = make_regexp(pattern, matchcase)
+ for buffer in search_in_buffers:
+ buffer_name = weechat.buffer_get_string(buffer, 'name')
+ matched_lines[buffer_name] = grep_buffer(buffer, head, tail, after_context,
+ before_context, count, regexp, hilight, exact, invert)
+
+ # logs
+ if search_in_files:
+ size_limit = get_config_int('size_limit', allow_empty_string=True)
+ background = False
+ if size_limit or size_limit == 0:
+ size = sum(map(get_size, search_in_files))
+ if size > size_limit * 1024:
+ background = True
+ elif size_limit == '':
+ background = False
+
+ if not background:
+ # run grep normally
+ regexp = make_regexp(pattern, matchcase)
+ for log in search_in_files:
+ log_name = strip_home(log)
+ matched_lines[log_name] = grep_file(log, head, tail, after_context, before_context,
+ count, regexp, hilight, exact, invert)
+ buffer_update()
+ else:
+ # we hook a process so grepping runs in background.
+ #debug('on background')
+ global hook_file_grep, script_path, bytecode
+ timeout = 1000*60*5 # 5 min
+
+ quotify = lambda s: '"%s"' %s
+ files_string = ', '.join(map(quotify, search_in_files))
+
+ global tmpFile
+ # we keep the file descriptor as a global var so it isn't deleted until next grep
+ tmpFile = tempfile.NamedTemporaryFile(prefix=SCRIPT_NAME,
+ dir=weechat.info_get('weechat_dir', ''))
+ cmd = grep_process_cmd %dict(logs=files_string, head=head, pattern=pattern, tail=tail,
+ hilight=hilight, after_context=after_context, before_context=before_context,
+ exact=exact, matchcase=matchcase, home_dir=home_dir, script_path=script_path,
+ count=count, invert=invert, bytecode=bytecode, filename=tmpFile.name,
+ python=weechat.info_get('python2_bin', '') or 'python')
+
+ #debug(cmd)
+ hook_file_grep = weechat.hook_process(cmd, timeout, 'grep_file_callback', tmpFile.name)
+ global pattern_tmpl
+ if hook_file_grep:
+ buffer_create("Searching for '%s' in %s worth of data..." %(pattern_tmpl,
+ human_readable_size(size)))
+ else:
+ buffer_update()
+
+# defined here for commodity
+grep_process_cmd = """%(python)s -%(bytecode)sc '
+import sys, cPickle, os
+sys.path.append("%(script_path)s") # add WeeChat script dir so we can import grep
+from grep import make_regexp, grep_file, strip_home
+logs = (%(logs)s, )
+try:
+ regexp = make_regexp("%(pattern)s", %(matchcase)s)
+ d = {}
+ for log in logs:
+ log_name = strip_home(log, "%(home_dir)s")
+ lines = grep_file(log, %(head)s, %(tail)s, %(after_context)s, %(before_context)s,
+ %(count)s, regexp, "%(hilight)s", %(exact)s, %(invert)s)
+ d[log_name] = lines
+ fd = open("%(filename)s", "wb")
+ cPickle.dump(d, fd, -1)
+ fd.close()
+except Exception, e:
+ print >> sys.stderr, e'
+"""
+
+grep_stdout = grep_stderr = ''
+def grep_file_callback(filename, command, rc, stdout, stderr):
+ global hook_file_grep, grep_stderr, grep_stdout
+ global matched_lines
+ #debug("rc: %s\nstderr: %s\nstdout: %s" %(rc, repr(stderr), repr(stdout)))
+ if stdout:
+ grep_stdout += stdout
+ if stderr:
+ grep_stderr += stderr
+ if int(rc) >= 0:
+
+ def set_buffer_error():
+ grep_buffer = buffer_create()
+ title = weechat.buffer_get_string(grep_buffer, 'title')
+ title = title + ' %serror' %color_title
+ weechat.buffer_set(grep_buffer, 'title', title)
+
+ try:
+ if grep_stderr:
+ error(grep_stderr)
+ set_buffer_error()
+ #elif grep_stdout:
+ #debug(grep_stdout)
+ elif path.exists(filename):
+ import cPickle
+ try:
+ #debug(file)
+ fd = open(filename, 'rb')
+ d = cPickle.load(fd)
+ matched_lines.update(d)
+ fd.close()
+ except Exception, e:
+ error(e)
+ set_buffer_error()
+ else:
+ buffer_update()
+ global tmpFile
+ tmpFile = None
+ finally:
+ grep_stdout = grep_stderr = ''
+ hook_file_grep = None
+ return WEECHAT_RC_OK
+
+def get_grep_file_status():
+ global search_in_files, matched_lines, time_start
+ elapsed = now() - time_start
+ if len(search_in_files) == 1:
+ log = '%s (%s)' %(strip_home(search_in_files[0]),
+ human_readable_size(get_size(search_in_files[0])))
+ else:
+ size = sum(map(get_size, search_in_files))
+ log = '%s log files (%s)' %(len(search_in_files), human_readable_size(size))
+ return 'Searching in %s, running for %.4f seconds. Interrupt it with "/grep stop" or "stop"' \
+ ' in grep buffer.' %(log, elapsed)
+
+### Grep buffer ###
+def buffer_update():
+ """Updates our buffer with new lines."""
+ global pattern_tmpl, matched_lines, pattern, count, hilight, invert, exact
+ time_grep = now()
+
+ buffer = buffer_create()
+ if get_config_boolean('clear_buffer'):
+ weechat.buffer_clear(buffer)
+ matched_lines.strip_separator() # remove first and last separators of each list
+ len_total_lines = len(matched_lines)
+ max_lines = get_config_int('max_lines')
+ if not count and len_total_lines > max_lines:
+ weechat.buffer_clear(buffer)
+
+ def _make_summary(log, lines, note):
+ return '%s matches "%s%s%s"%s in %s%s%s%s' \
+ %(lines.matches_count, color_summary, pattern_tmpl, color_info,
+ invert and ' (inverted)' or '',
+ color_summary, log, color_reset, note)
+
+ if count:
+ make_summary = lambda log, lines : _make_summary(log, lines, ' (not shown)')
+ else:
+ def make_summary(log, lines):
+ if lines.stripped_lines:
+ if lines:
+ note = ' (last %s lines shown)' %len(lines)
+ else:
+ note = ' (not shown)'
+ else:
+ note = ''
+ return _make_summary(log, lines, note)
+
+ global weechat_format
+ if hilight:
+ # we don't want colors if there's match highlighting
+ format_line = lambda s : '%s %s %s' %split_line(s)
+ else:
+ def format_line(s):
+ global nick_dict, weechat_format
+ date, nick, msg = split_line(s)
+ if weechat_format:
+ try:
+ nick = nick_dict[nick]
+ except KeyError:
+ # cache nick
+ nick_c = color_nick(nick)
+ nick_dict[nick] = nick_c
+ nick = nick_c
+ return '%s%s %s%s %s' %(color_date, date, nick, color_reset, msg)
+ else:
+ #no formatting
+ return msg
+
+ prnt(buffer, '\n')
+ print_line('Search for "%s%s%s"%s in %s%s%s.' %(color_summary, pattern_tmpl, color_info,
+ invert and ' (inverted)' or '', color_summary, matched_lines, color_reset),
+ buffer)
+ # print last <max_lines> lines
+ if matched_lines.get_matches_count():
+ if count:
+ # with count we sort by matches lines instead of just lines.
+ matched_lines_items = matched_lines.items_count()
+ else:
+ matched_lines_items = matched_lines.items()
+
+ matched_lines.get_last_lines(max_lines)
+ for log, lines in matched_lines_items:
+ if lines.matches_count:
+ # matched lines
+ if not count:
+ # print lines
+ weechat_format = True
+ if exact:
+ lines.onlyUniq()
+ for line in lines:
+ #debug(repr(line))
+ if line == linesList._sep:
+ # separator
+ prnt(buffer, context_sep)
+ else:
+ if '\x00' in line:
+ # log was corrupted
+ error("Found garbage in log '%s', maybe it's corrupted" %log)
+ line = line.replace('\x00', '')
+ prnt_date_tags(buffer, 0, 'no_highlight', format_line(line))
+
+ # summary
+ if count or get_config_boolean('show_summary'):
+ summary = make_summary(log, lines)
+ print_line(summary, buffer)
+
+ # separator
+ if not count and lines:
+ prnt(buffer, '\n')
+ else:
+ print_line('No matches found.', buffer)
+
+ # set title
+ global time_start
+ time_end = now()
+ # total time
+ time_total = time_end - time_start
+ # percent of the total time used for grepping
+ time_grep_pct = (time_grep - time_start)/time_total*100
+ #debug('time: %.4f seconds (%.2f%%)' %(time_total, time_grep_pct))
+ if not count and len_total_lines > max_lines:
+ note = ' (last %s lines shown)' %len(matched_lines)
+ else:
+ note = ''
+ title = "'q': close buffer | Search in %s%s%s %s matches%s | pattern \"%s%s%s\"%s %s | %.4f seconds (%.2f%%)" \
+ %(color_title, matched_lines, color_reset, matched_lines.get_matches_count(), note,
+ color_title, pattern_tmpl, color_reset, invert and ' (inverted)' or '', format_options(),
+ time_total, time_grep_pct)
+ weechat.buffer_set(buffer, 'title', title)
+
+ if get_config_boolean('go_to_buffer'):
+ weechat.buffer_set(buffer, 'display', '1')
+
+ # free matched_lines so it can be removed from memory
+ del matched_lines
+
+def split_line(s):
+ """Splits log's line 's' in 3 parts, date, nick and msg."""
+ global weechat_format
+ if weechat_format and s.count('\t') >= 2:
+ date, nick, msg = s.split('\t', 2) # date, nick, message
+ else:
+ # looks like log isn't in weechat's format
+ weechat_format = False # incoming lines won't be formatted
+ date, nick, msg = '', '', s
+ # remove tabs
+ if '\t' in msg:
+ msg = msg.replace('\t', ' ')
+ return date, nick, msg
+
+def print_line(s, buffer=None, display=False):
+ """Prints 's' in script's buffer as 'script_nick'. For displaying search summaries."""
+ if buffer is None:
+ buffer = buffer_create()
+ say('%s%s' %(color_info, s), buffer)
+ if display and get_config_boolean('go_to_buffer'):
+ weechat.buffer_set(buffer, 'display', '1')
+
+def format_options():
+ global matchcase, number, count, exact, hilight, invert
+ global tail, head, after_context, before_context
+ options = []
+ append = options.append
+ insert = options.insert
+ chars = 'cHmov'
+ for i, flag in enumerate((count, hilight, matchcase, exact, invert)):
+ if flag:
+ append(chars[i])
+
+ if head or tail:
+ n = get_config_int('default_tail_head')
+ if head:
+ append('h')
+ if head != n:
+ insert(-1, ' -')
+ append('n')
+ append(head)
+ elif tail:
+ append('t')
+ if tail != n:
+ insert(-1, ' -')
+ append('n')
+ append(tail)
+
+ if before_context and after_context and (before_context == after_context):
+ append(' -C')
+ append(before_context)
+ else:
+ if before_context:
+ append(' -B')
+ append(before_context)
+ if after_context:
+ append(' -A')
+ append(after_context)
+
+ s = ''.join(map(str, options)).strip()
+ if s and s[0] != '-':
+ s = '-' + s
+ return s
+
+def buffer_create(title=None):
+ """Returns our buffer pointer, creates and cleans the buffer if needed."""
+ buffer = weechat.buffer_search('python', SCRIPT_NAME)
+ if not buffer:
+ buffer = weechat.buffer_new(SCRIPT_NAME, 'buffer_input', '', '', '')
+ weechat.buffer_set(buffer, 'time_for_each_line', '0')
+ weechat.buffer_set(buffer, 'nicklist', '0')
+ weechat.buffer_set(buffer, 'title', title or 'grep output buffer')
+ weechat.buffer_set(buffer, 'localvar_set_no_log', '1')
+ elif title:
+ weechat.buffer_set(buffer, 'title', title)
+ return buffer
+
+def buffer_input(data, buffer, input_data):
+ """Repeats last search with 'input_data' as regexp."""
+ try:
+ cmd_grep_stop(buffer, input_data)
+ except:
+ return WEECHAT_RC_OK
+ if input_data in ('q', 'Q'):
+ weechat.buffer_close(buffer)
+ return weechat.WEECHAT_RC_OK
+
+ global search_in_buffers, search_in_files
+ global pattern
+ try:
+ if pattern and (search_in_files or search_in_buffers):
+ # check if the buffer pointers are still valid
+ for pointer in search_in_buffers:
+ infolist = weechat.infolist_get('buffer', pointer, '')
+ if not infolist:
+ del search_in_buffers[search_in_buffers.index(pointer)]
+ weechat.infolist_free(infolist)
+ try:
+ cmd_grep_parsing(input_data)
+ except Exception, e:
+ error('Argument error, %s' %e, buffer=buffer)
+ return WEECHAT_RC_OK
+ try:
+ show_matching_lines()
+ except Exception, e:
+ error(e)
+ except NameError:
+ error("There isn't any previous search to repeat.", buffer=buffer)
+ return WEECHAT_RC_OK
+
+### Commands ###
+def cmd_init():
+ """Resets global vars."""
+ global home_dir, cache_dir, nick_dict
+ global pattern_tmpl, pattern, matchcase, number, count, exact, hilight, invert
+ global tail, head, after_context, before_context
+ hilight = ''
+ head = tail = after_context = before_context = invert = False
+ matchcase = count = exact = False
+ pattern_tmpl = pattern = number = None
+ home_dir = get_home()
+ cache_dir = {} # for avoid walking the dir tree more than once per command
+ nick_dict = {} # nick cache for don't calculate nick color every time
+
+def cmd_grep_parsing(args):
+ """Parses args for /grep and grep input buffer."""
+ global pattern_tmpl, pattern, matchcase, number, count, exact, hilight, invert
+ global tail, head, after_context, before_context
+ global log_name, buffer_name, only_buffers, all
+ opts, args = getopt.gnu_getopt(args.split(), 'cmHeahtivn:bA:B:C:o', ['count', 'matchcase', 'hilight',
+ 'exact', 'all', 'head', 'tail', 'number=', 'buffer', 'after-context=', 'before-context=',
+ 'context=', 'invert', 'only-match'])
+ #debug(opts, 'opts: '); debug(args, 'args: ')
+ if len(args) >= 2:
+ if args[0] == 'log':
+ del args[0]
+ log_name = args.pop(0)
+ elif args[0] == 'buffer':
+ del args[0]
+ buffer_name = args.pop(0)
+
+ def tmplReplacer(match):
+ """This function will replace templates with regexps"""
+ s = match.groups()[0]
+ tmpl_args = s.split()
+ tmpl_key, _, tmpl_args = s.partition(' ')
+ try:
+ template = templates[tmpl_key]
+ if callable(template):
+ r = template(tmpl_args)
+ if not r:
+ error("Template %s returned empty string "\
+ "(WeeChat doesn't have enough data)." %t)
+ return r
+ else:
+ return template
+ except:
+ return t
+
+ args = ' '.join(args) # join pattern for keep spaces
+ if args:
+ pattern_tmpl = args
+ pattern = _tmplRe.sub(tmplReplacer, args)
+ debug('Using regexp: %s', pattern)
+ if not pattern:
+ raise Exception, 'No pattern for grep the logs.'
+
+ def positive_number(opt, val):
+ try:
+ number = int(val)
+ if number < 0:
+ raise ValueError
+ return number
+ except ValueError:
+ if len(opt) == 1:
+ opt = '-' + opt
+ else:
+ opt = '--' + opt
+ raise Exception, "argument for %s must be a positive integer." %opt
+
+ for opt, val in opts:
+ opt = opt.strip('-')
+ if opt in ('c', 'count'):
+ count = not count
+ elif opt in ('m', 'matchcase'):
+ matchcase = not matchcase
+ elif opt in ('H', 'hilight'):
+ # hilight must be always a string!
+ if hilight:
+ hilight = ''
+ else:
+ hilight = '%s,%s' %(color_hilight, color_reset)
+ # we pass the colors in the variable itself because check_string() must not use
+ # weechat's module when applying the colors (this is for grep in a hooked process)
+ elif opt in ('e', 'exact', 'o', 'only-match'):
+ exact = not exact
+ invert = False
+ elif opt in ('a', 'all'):
+ all = not all
+ elif opt in ('h', 'head'):
+ head = not head
+ tail = False
+ elif opt in ('t', 'tail'):
+ tail = not tail
+ head = False
+ elif opt in ('b', 'buffer'):
+ only_buffers = True
+ elif opt in ('n', 'number'):
+ number = positive_number(opt, val)
+ elif opt in ('C', 'context'):
+ n = positive_number(opt, val)
+ after_context = n
+ before_context = n
+ elif opt in ('A', 'after-context'):
+ after_context = positive_number(opt, val)
+ elif opt in ('B', 'before-context'):
+ before_context = positive_number(opt, val)
+ elif opt in ('i', 'v', 'invert'):
+ invert = not invert
+ exact = False
+ # number check
+ if number is not None:
+ if number == 0:
+ head = tail = False
+ number = None
+ elif head:
+ head = number
+ elif tail:
+ tail = number
+ else:
+ n = get_config_int('default_tail_head')
+ if head:
+ head = n
+ elif tail:
+ tail = n
+
+def cmd_grep_stop(buffer, args):
+ global hook_file_grep, pattern, matched_lines, tmpFile
+ if hook_file_grep:
+ if args == 'stop':
+ weechat.unhook(hook_file_grep)
+ hook_file_grep = None
+ s = 'Search for \'%s\' stopped.' %pattern
+ say(s, buffer)
+ grep_buffer = weechat.buffer_search('python', SCRIPT_NAME)
+ if grep_buffer:
+ weechat.buffer_set(grep_buffer, 'title', s)
+ del matched_lines
+ tmpFile = None
+ else:
+ say(get_grep_file_status(), buffer)
+ raise Exception
+
+def cmd_grep(data, buffer, args):
+ """Search in buffers and logs."""
+ global pattern, matchcase, head, tail, number, count, exact, hilight
+ try:
+ cmd_grep_stop(buffer, args)
+ except:
+ return WEECHAT_RC_OK
+
+ if not args:
+ weechat.command('', '/help %s' %SCRIPT_COMMAND)
+ return WEECHAT_RC_OK
+
+ cmd_init()
+ global log_name, buffer_name, only_buffers, all
+ log_name = buffer_name = ''
+ only_buffers = all = False
+
+ # parse
+ try:
+ cmd_grep_parsing(args)
+ except Exception, e:
+ error('Argument error, %s' %e)
+ return WEECHAT_RC_OK
+
+ # find logs
+ log_file = search_buffer = None
+ if log_name:
+ log_file = get_file_by_pattern(log_name, all)
+ if not log_file:
+ error("Couldn't find any log for %s. Try /logs" %log_name)
+ return WEECHAT_RC_OK
+ elif all:
+ search_buffer = get_all_buffers()
+ elif buffer_name:
+ search_buffer = get_buffer_by_name(buffer_name)
+ if not search_buffer:
+ # there's no buffer, try in the logs
+ log_file = get_file_by_name(buffer_name)
+ if not log_file:
+ error("Logs or buffer for '%s' not found." %buffer_name)
+ return WEECHAT_RC_OK
+ else:
+ search_buffer = [search_buffer]
+ else:
+ search_buffer = [buffer]
+
+ # make the log list
+ global search_in_files, search_in_buffers
+ search_in_files = []
+ search_in_buffers = []
+ if log_file:
+ search_in_files = log_file
+ elif not only_buffers:
+ #debug(search_buffer)
+ for pointer in search_buffer:
+ log = get_file_by_buffer(pointer)
+ #debug('buffer %s log %s' %(pointer, log))
+ if log:
+ search_in_files.append(log)
+ else:
+ search_in_buffers.append(pointer)
+ else:
+ search_in_buffers = search_buffer
+
+ # grepping
+ try:
+ show_matching_lines()
+ except Exception, e:
+ error(e)
+ return WEECHAT_RC_OK
+
+def cmd_logs(data, buffer, args):
+ """List files in Weechat's log dir."""
+ cmd_init()
+ global home_dir
+ sort_by_size = False
+ filter = []
+
+ try:
+ opts, args = getopt.gnu_getopt(args.split(), 's', ['size'])
+ if args:
+ filter = args
+ for opt, var in opts:
+ opt = opt.strip('-')
+ if opt in ('size', 's'):
+ sort_by_size = True
+ except Exception, e:
+ error('Argument error, %s' %e)
+ return WEECHAT_RC_OK
+
+ # is there's a filter, filter_excludes should be False
+ file_list = dir_list(home_dir, filter, filter_excludes=not filter)
+ if sort_by_size:
+ file_list.sort(key=get_size)
+ else:
+ file_list.sort()
+
+ file_sizes = map(lambda x: human_readable_size(get_size(x)), file_list)
+ # calculate column lenght
+ if file_list:
+ L = file_list[:]
+ L.sort(key=len)
+ bigest = L[-1]
+ column_len = len(bigest) + 3
+ else:
+ column_len = ''
+
+ buffer = buffer_create()
+ if get_config_boolean('clear_buffer'):
+ weechat.buffer_clear(buffer)
+ file_list = zip(file_list, file_sizes)
+ msg = 'Found %s logs.' %len(file_list)
+
+ print_line(msg, buffer, display=True)
+ for file, size in file_list:
+ separator = column_len and '.'*(column_len - len(file))
+ prnt(buffer, '%s %s %s' %(strip_home(file), separator, size))
+ if file_list:
+ print_line(msg, buffer)
+ return WEECHAT_RC_OK
+
+
+### Completion ###
+def completion_log_files(data, completion_item, buffer, completion):
+ #debug('completion: %s' %', '.join((data, completion_item, buffer, completion)))
+ global home_dir
+ l = len(home_dir)
+ completion_list_add = weechat.hook_completion_list_add
+ WEECHAT_LIST_POS_END = weechat.WEECHAT_LIST_POS_END
+ for log in dir_list(home_dir):
+ completion_list_add(completion, log[l:], 0, WEECHAT_LIST_POS_END)
+ return WEECHAT_RC_OK
+
+def completion_grep_args(data, completion_item, buffer, completion):
+ for arg in ('count', 'all', 'matchcase', 'hilight', 'exact', 'head', 'tail', 'number', 'buffer',
+ 'after-context', 'before-context', 'context', 'invert', 'only-match'):
+ weechat.hook_completion_list_add(completion, '--' + arg, 0, weechat.WEECHAT_LIST_POS_SORT)
+ for tmpl in templates:
+ weechat.hook_completion_list_add(completion, '%{' + tmpl, 0, weechat.WEECHAT_LIST_POS_SORT)
+ return WEECHAT_RC_OK
+
+
+### Templates ###
+# template placeholder
+_tmplRe = re.compile(r'%\{(\w+.*?)(?:\}|$)')
+# will match 999.999.999.999 but I don't care
+ipAddress = r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
+domain = r'[\w-]{2,}(?:\.[\w-]{2,})*\.[a-z]{2,}'
+url = r'\w+://(?:%s|%s)(?::\d+)?(?:/[^\])>\s]*)?' % (domain, ipAddress)
+
+def make_url_regexp(args):
+ #debug('make url: %s', args)
+ if args:
+ words = r'(?:%s)' %'|'.join(map(re.escape, args.split()))
+ return r'(?:\w+://|www\.)[^\s]*%s[^\s]*(?:/[^\])>\s]*)?' %words
+ else:
+ return url
+
+def make_simple_regexp(pattern):
+ s = ''
+ for c in pattern:
+ if c == '*':
+ s += '.*'
+ elif c == '?':
+ s += '.'
+ else:
+ s += re.escape(c)
+ return s
+
+templates = {
+ 'ip': ipAddress,
+ 'url': make_url_regexp,
+ 'escape': lambda s: re.escape(s),
+ 'simple': make_simple_regexp,
+ 'domain': domain,
+ }
+
+### Main ###
+def delete_bytecode():
+ global script_path
+ bytecode = path.join(script_path, SCRIPT_NAME + '.pyc')
+ if path.isfile(bytecode):
+ os.remove(bytecode)
+ return WEECHAT_RC_OK
+
+if __name__ == '__main__' and import_ok and \
+ weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, \
+ SCRIPT_DESC, 'delete_bytecode', ''):
+ home_dir = get_home()
+
+ # for import ourselves
+ global script_path
+ script_path = path.dirname(__file__)
+ sys.path.append(script_path)
+ delete_bytecode()
+
+ # check python version
+ import sys
+ global bytecode
+ if sys.version_info > (2, 6):
+ bytecode = 'B'
+ else:
+ bytecode = ''
+
+
+ weechat.hook_command(SCRIPT_COMMAND, cmd_grep.__doc__,
+ "[log <file> | buffer <name> | stop] [-a|--all] [-b|--buffer] [-c|--count] [-m|--matchcase] "
+ "[-H|--hilight] [-o|--only-match] [-i|-v|--invert] [(-h|--head)|(-t|--tail) [-n|--number <n>]] "
+ "[-A|--after-context <n>] [-B|--before-context <n>] [-C|--context <n> ] <expression>",
+# help
+"""
+ log <file>: Search in one log that matches <file> in the logger path.
+ Use '*' and '?' as wildcards.
+ buffer <name>: Search in buffer <name>, if there's no buffer with <name> it will
+ try to search for a log file.
+ stop: Stops a currently running search.
+ -a --all: Search in all open buffers.
+ If used with 'log <file>' search in all logs that matches <file>.
+ -b --buffer: Search only in buffers, not in file logs.
+ -c --count: Just count the number of matched lines instead of showing them.
+ -m --matchcase: Don't do case insensitive search.
+ -H --hilight: Colour exact matches in output buffer.
+-o --only-match: Print only the matching part of the line (unique matches).
+ -v -i --invert: Print lines that don't match the regular expression.
+ -t --tail: Print the last 10 matching lines.
+ -h --head: Print the first 10 matching lines.
+-n --number <n>: Overrides default number of lines for --tail or --head.
+-A --after-context <n>: Shows <n> lines of trailing context after matching lines.
+-B --before-context <n>: Shows <n> lines of leading context before matching lines.
+-C --context <n>: Same as using both --after-context and --before-context simultaneously.
+ <expression>: Expression to search.
+
+Grep buffer:
+ Input line accepts most arguments of /grep, it'll repeat last search using the new
+ arguments provided. You can't search in different logs from the buffer's input.
+ Boolean arguments like --count, --tail, --head, --hilight, ... are toggleable
+
+Python regular expression syntax:
+ See http://docs.python.org/lib/re-syntax.html
+
+Grep Templates:
+ %{url [text]}: Matches anything like an url, or an url with text.
+ %{ip}: Matches anything that looks like an ip.
+ %{domain}: Matches anything like a domain.
+ %{escape text}: Escapes text in pattern.
+ %{simple pattern}: Converts a pattern with '*' and '?' wildcards into a regexp.
+
+Examples:
+ Search for urls with the word 'weechat' said by 'nick'
+ /grep nick\\t.*%{url weechat}
+ Search for '*.*' string
+ /grep %{escape *.*}
+""",
+ # completion template
+ "buffer %(buffers_names) %(grep_arguments)|%*"
+ "||log %(grep_log_files) %(grep_arguments)|%*"
+ "||stop"
+ "||%(grep_arguments)|%*",
+ 'cmd_grep' ,'')
+ weechat.hook_command('logs', cmd_logs.__doc__, "[-s|--size] [<filter>]",
+ "-s --size: Sort logs by size.\n"
+ " <filter>: Only show logs that match <filter>. Use '*' and '?' as wildcards.", '--size', 'cmd_logs', '')
+
+ weechat.hook_completion('grep_log_files', "list of log files",
+ 'completion_log_files', '')
+ weechat.hook_completion('grep_arguments', "list of arguments",
+ 'completion_grep_args', '')
+
+ # settings
+ for opt, val in settings.iteritems():
+ if not weechat.config_is_set_plugin(opt):
+ weechat.config_set_plugin(opt, val)
+
+ # colors
+ color_date = weechat.color('brown')
+ color_info = weechat.color('cyan')
+ color_hilight = weechat.color('lightred')
+ color_reset = weechat.color('reset')
+ color_title = weechat.color('yellow')
+ color_summary = weechat.color('lightcyan')
+ color_delimiter = weechat.color('chat_delimiters')
+ color_script_nick = weechat.color('chat_nick')
+
+ # pretty [grep]
+ script_nick = '%s[%s%s%s]%s' %(color_delimiter, color_script_nick, SCRIPT_NAME, color_delimiter,
+ color_reset)
+ script_nick_nocolor = '[%s]' %SCRIPT_NAME
+ # paragraph separator when using context options
+ context_sep = '%s\t%s--' %(script_nick, color_info)
+
+ # -------------------------------------------------------------------------
+ # Debug
+
+ if weechat.config_get_plugin('debug'):
+ try:
+ # custom debug module I use, allows me to inspect script's objects.
+ import pybuffer
+ debug = pybuffer.debugBuffer(globals(), '%s_debug' % SCRIPT_NAME)
+ except:
+ def debug(s, *args):
+ if not isinstance(s, basestring):
+ s = str(s)
+ if args:
+ s = s %args
+ prnt('', '%s\t%s' %(script_nick, s))
+ else:
+ def debug(*args):
+ pass
+
+# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100:
diff --git a/weechat/python/histman.py b/weechat/python/histman.py
new file mode 100644
index 0000000..68486c9
--- /dev/null
+++ b/weechat/python/histman.py
@@ -0,0 +1,429 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2012-2015 by nils_2 <weechatter@arcor.de>
+#
+# save and restore global and/or buffer command history
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# 2015-04-05: nils_2 (freenode.#weechat)
+# 0.5 : change priority of hook_signal('buffer_opened') to 100
+#
+# 2013-01-25: nils_2 (freenode.#weechat)
+# 0.4 : make script compatible with Python 3.x
+#
+# 2013-01-20: nils_2, (freenode.#weechat)
+# 0.3: fix wrong command argument in help-text
+#
+# 2012-12-21: nils_2, (freenode.#weechat)
+# 0.2 : fix UnicodeEncodeError
+#
+# 2012-12-09: nils_2, (freenode.#weechat)
+# 0.1 : initial release
+#
+# thanks to nestib for help with regex and for the rmodifier idea
+#
+# requires: WeeChat version 0.4.0
+#
+# Development is currently hosted at
+# https://github.com/weechatter/weechat-scripts
+
+try:
+ import weechat,re,os
+
+except Exception:
+ print("This script must be run under WeeChat.")
+ print("Get WeeChat now at: http://www.weechat.org/")
+ quit()
+
+SCRIPT_NAME = 'histman'
+SCRIPT_AUTHOR = 'nils_2 <weechatter@arcor.de>'
+SCRIPT_VERSION = '0.5'
+SCRIPT_LICENSE = 'GPL'
+SCRIPT_DESC = 'save and restore global and/or buffer command history'
+
+OPTIONS = { 'number' : ('0','number of history commands/text to save. A positive number will save from oldest to latest, a negative number will save from latest to oldest. 0 = save whole history (e.g. -10 will save the last 10 history entries'),
+ 'pattern' : ('(.*password|.*nickserv|/quit)','a simple regex to ignore commands/text. Empty value disable pattern matching'),
+ 'skip_double' : ('on','skip lines that already exists (case sensitive)'),
+ 'save' : ('all','define what should be save from history. Possible values are \"command\", \"text\", \"all\". This is a fallback option (see /help ' + SCRIPT_NAME +')'),
+ 'history_dir' : ('%h/history','locale cache directory for history files (\"%h\" will be replaced by WeeChat home, \"~/.weechat\" by default)'),
+ 'save_global' : ('off','save global history, possible values are \"command\", \"text\", \"all\" or \"off\"(default: off)'),
+ 'min_length' : ('2','minimum length of command/text (default: 2)'),
+ 'rmodifier' : ('off','use rmodifier options to ignore commands/text (default:off)'),
+ 'buffer_close' : ('off','save command history, when buffer will be closed (default: off)'),
+ }
+
+filename_global_history = 'global_history'
+possible_save_options = ['command', 'text', 'all']
+
+history_list = []
+
+# =================================[ save/restore buffer history ]================================
+def save_history():
+ global history_list
+
+ # get buffers
+ ptr_infolist_buffer = weechat.infolist_get('buffer','','')
+
+ while weechat.infolist_next(ptr_infolist_buffer):
+ ptr_buffer = weechat.infolist_pointer(ptr_infolist_buffer,'pointer')
+
+ # check for localvar_save_history
+ if not weechat.buffer_get_string(ptr_buffer, 'localvar_save_history'):
+ continue
+
+ plugin = weechat.buffer_get_string(ptr_buffer, 'localvar_plugin')
+ name = weechat.buffer_get_string(ptr_buffer, 'localvar_name')
+ filename = get_filename_with_path('%s.%s' % (plugin,name))
+
+ get_buffer_history(ptr_buffer)
+ if len(history_list):
+ write_history(filename)
+
+ weechat.infolist_free(ptr_infolist_buffer)
+
+ # global history
+ if OPTIONS['save_global'].lower() != 'off':
+ get_buffer_history('')
+ if len(history_list):
+ write_history(filename_global_history)
+
+def get_buffer_history(ptr_buffer):
+ global history_list
+
+ history_list = []
+ ptr_buffer_history = weechat.infolist_get('history',ptr_buffer,'')
+
+ if not ptr_buffer_history:
+ return
+
+ while weechat.infolist_next(ptr_buffer_history):
+ line = weechat.infolist_string(ptr_buffer_history, 'text')
+
+ if add_buffer_line(line,ptr_buffer):
+ history_list.insert(0,line)
+
+ weechat.infolist_free(ptr_buffer_history)
+
+# return 1; line will be saved
+# return 0; line won't be saved
+def add_buffer_line(line, ptr_buffer):
+ global history_list
+
+ # min_length reached?
+ if len(line) < int(OPTIONS['min_length']):
+ return 0
+
+ add_line = 0
+
+ if ptr_buffer: # buffer history
+ save_history = weechat.buffer_get_string(ptr_buffer, 'localvar_save_history')
+ if not save_history.lower() in possible_save_options:
+ save_history = OPTIONS['save']
+ else: # global history
+ if not OPTIONS['save_global'].lower() in possible_save_options:
+ save_history = OPTIONS['save']
+
+ # no save option given? save nothing
+ if save_history == '':
+ return 0
+
+ if save_history.lower() == 'command':
+ command_chars = weechat.config_string(weechat.config_get('weechat.look.command_chars')) + '/'
+ # a valid command must have at least two chars and first and second char are not equal!
+ if len(line) > 1 and line[0] in command_chars and line[0] != line[1]:
+ add_line = 1
+ else:
+ return 0
+ elif save_history.lower() == 'text':
+ command_chars = weechat.config_string(weechat.config_get('weechat.look.command_chars')) + '/'
+ # test for "//" = text
+ if line[0] == line[1] and line[0] in command_chars:
+ add_line = 1
+ # test for "/n" = command
+ elif line[0] != line[1] and line[0] in command_chars:
+ return 0
+ else:
+ add_line = 1
+ elif save_history.lower() == 'all':
+ add_line = 1
+ else: # not one of given values. save nothing!
+ return 0
+
+ # lines already exist?
+ if OPTIONS['skip_double'].lower() == 'on':
+ history_list2 = []
+ history_list2 = [element.lower() for element in history_list]
+ if line.lower() in history_list2:
+ return 0
+ else:
+ add_line = 1
+ else:
+ add_line = 1
+
+ if add_line == 0:
+ return 0
+
+ pattern_matching = 0
+ # pattern matching for user option and rmodifier options
+ if OPTIONS['pattern'] != '':
+ filter_re=re.compile(OPTIONS['pattern'], re.I)
+ # pattern matched
+ if filter_re.match(line):
+ pattern_matching = 1
+
+ if OPTIONS['rmodifier'].lower() == 'on' and pattern_matching == 0:
+ ptr_infolist_options = weechat.infolist_get('option','','rmodifier.modifier.*')
+ if ptr_infolist_options:
+ while weechat.infolist_next(ptr_infolist_options):
+ value = weechat.infolist_string(ptr_infolist_options,'value')
+ pattern = re.findall(r";(.*);", value)
+
+ filter_re=re.compile(pattern[0], re.I)
+ # pattern matched
+ if filter_re.match(line):
+ pattern_matching = 1
+ break
+ weechat.infolist_free(ptr_infolist_options)
+
+ if add_line == 1 and pattern_matching == 0:
+ return 1
+ return 0
+
+# =================================[ read/write history to file ]=================================
+def read_history(filename,ptr_buffer):
+ global_history = 0
+
+ # global history does not use buffer pointers!
+ if filename == filename_global_history:
+ global_history = 1
+
+ filename = get_filename_with_path(filename)
+
+ # filename exists?
+ if not os.path.isfile(filename):
+ return
+
+ # check for global history
+ if global_history == 0:
+ # localvar_save_history exists for buffer?
+ if not ptr_buffer or not weechat.buffer_get_string(ptr_buffer, 'localvar_save_history'):
+ return
+
+ hdata = weechat.hdata_get('history')
+ if not hdata:
+ return
+
+ try:
+ f = open(filename, 'r')
+# for line in f.xreadlines(): # old python 2.x
+ for line in f: # should also work with python 2.x
+# line = line.decode('utf-8')
+ line = str(line.strip())
+ if ptr_buffer:
+ # add to buffer history
+ weechat.hdata_update(hdata, '', { 'buffer': ptr_buffer, 'text': line })
+ else:
+ # add to global history
+ weechat.hdata_update(hdata, '', { 'text': line })
+ f.close()
+ except:
+ if global_history == 1:
+ weechat.prnt('','%s%s: Error loading global history from "%s"' % (weechat.prefix('error'), SCRIPT_NAME, filename))
+ else:
+ name = weechat.buffer_get_string(ptr_buffer, 'localvar_name')
+ weechat.prnt('','%s%s: Error loading buffer history for buffer "%s" from "%s"' % (weechat.prefix('error'), SCRIPT_NAME, name, filename))
+ raise
+
+def write_history(filename):
+ global history_list
+
+ filename = get_filename_with_path(filename)
+
+ if OPTIONS['number'] != '' and OPTIONS['number'].isdigit():
+ if int(OPTIONS['number']) < 0:
+ save_from_position = len(history_list) - abs(int(OPTIONS['number']))
+ if save_from_position == len(history_list) or save_from_position < 0:
+ save_from_position = 0
+ elif int(OPTIONS['number']) > 0:
+ save_to_position = int(OPTIONS['number'])
+ if save_to_position > len(history_list):
+ save_to_position = len(history_list)
+ else:
+ save_from_position = 0
+ try:
+ f = open(filename, 'w')
+
+ if int(OPTIONS['number']) <= 0:
+ i = save_from_position
+ # for i in range(len(a)):
+ while i < len(history_list):
+ f.write('%s\n' % history_list[i])
+ i = i + 1
+ if int(OPTIONS['number']) > 0:
+ i = 0
+ while i < save_to_position:
+ f.write('%s\n' % history_list[i])
+ i = i + 1
+ f.close()
+
+ except:
+ weechat.prnt('','%s%s: Error writing history to "%s"' % (weechat.prefix('error'),SCRIPT_NAME,filename))
+ raise
+
+def get_filename_with_path(filename):
+ path = OPTIONS['history_dir'].replace("%h",weechat.info_get("weechat_dir", ""))
+ return os.path.join(path,filename)
+
+def config_create_dir():
+ dir = OPTIONS['history_dir'].replace("%h",weechat.info_get("weechat_dir", ""))
+ if not os.path.isdir(dir):
+ os.makedirs(dir, mode=0o700)
+
+# ===========================================[ Hooks() ]==========================================
+def create_hooks():
+ # create hooks
+ weechat.hook_signal('quit', 'quit_signal_cb', '')
+ weechat.hook_signal('upgrade_ended', 'upgrade_ended_cb', '')
+ # low priority for hook_signal('buffer_opened') to ensure that buffer_autoset hook_signal() runs first
+ weechat.hook_signal('100|buffer_opened', 'buffer_opened_cb', '')
+ weechat.hook_config('plugins.var.python.' + SCRIPT_NAME + '.*', 'toggle_refresh', '' )
+ weechat.hook_signal('buffer_closing', 'buffer_closing_cb', '')
+
+def quit_signal_cb(data, signal, signal_data):
+ # create dir, if not exist
+ config_create_dir()
+ save_history()
+ return weechat.WEECHAT_RC_OK
+
+def buffer_opened_cb(data, signal, signal_data):
+ plugin = weechat.buffer_get_string(signal_data, 'localvar_plugin')
+ name = weechat.buffer_get_string(signal_data, 'localvar_name')
+ filename = get_filename_with_path('%s.%s' % (plugin,name))
+
+ read_history(filename,signal_data)
+ return weechat.WEECHAT_RC_OK
+
+def buffer_closing_cb(data, signal, signal_data):
+ if OPTIONS['buffer_close'].lower() == 'on' and signal_data:
+ # check for localvar_save_history
+ if not weechat.buffer_get_string(signal_data, 'localvar_save_history'):
+ return weechat.WEECHAT_RC_OK
+
+ plugin = weechat.buffer_get_string(signal_data, 'localvar_plugin')
+ name = weechat.buffer_get_string(signal_data, 'localvar_name')
+ filename = get_filename_with_path('%s.%s' % (plugin,name))
+ get_buffer_history(signal_data)
+
+ if len(history_list):
+ write_history(filename)
+ return weechat.WEECHAT_RC_OK
+
+def upgrade_ended_cb(data, signal, signal_data):
+ weechat.buffer_set(weechat.buffer_search_main(), 'localvar_set_histman', 'on')
+ return weechat.WEECHAT_RC_OK
+
+def histman_cmd_cb(data, buffer, args):
+ if args == '':
+ weechat.command('', '/help %s' % SCRIPT_NAME)
+ return weechat.WEECHAT_RC_OK
+
+ argv = args.strip().split(' ', 1)
+ if len(argv) == 0:
+ return weechat.WEECHAT_RC_OK
+
+ if argv[0].lower() == 'save':
+ quit_signal_cb('', '', '')
+ elif argv[0].lower() == 'list':
+ weechat.command('','/set *.localvar_set_save_history')
+ else:
+ weechat.command('', '/help %s' % SCRIPT_NAME)
+
+ return weechat.WEECHAT_RC_OK
+
+# ================================[ weechat options & description ]===============================
+def init_options():
+ for option,value in list(OPTIONS.items()):
+ if not weechat.config_is_set_plugin(option):
+ weechat.config_set_plugin(option, value[0])
+ weechat.config_set_desc_plugin(option, '%s (default: "%s")' % (value[1], value[0]))
+ OPTIONS[option] = value[0]
+ else:
+ OPTIONS[option] = weechat.config_get_plugin(option)
+
+def toggle_refresh(pointer, name, value):
+ global OPTIONS
+ option = name[len('plugins.var.python.' + SCRIPT_NAME + '.'):] # get optionname
+ OPTIONS[option] = value # save new value
+ weechat.bar_item_update(SCRIPT_NAME)
+ return weechat.WEECHAT_RC_OK
+
+# ================================[ main ]===============================
+if __name__ == '__main__':
+ if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''):
+ version = weechat.info_get('version_number', '') or 0
+
+ weechat.hook_command(SCRIPT_NAME, SCRIPT_DESC, '[save] || [list]',
+ ' save: force to save command history:\n'
+ ' list: list local buffer variable(s)\n'
+ '\n'
+ 'If you \"/quit\" WeeChat, the script will automatically save the command history to file.\n'
+ 'You can also force the script to save command history, when a buffer will be closed.\n'
+ 'If you restart WeeChat again the command history will be restored, when buffer opens again.\n'
+ 'To save and restore \"global\" command history, use option \"save_global\".\n'
+ '\n'
+ 'The command history of a buffer will be saved \"only\", if the the local variable \"save_history\" is set.\n'
+ 'You will need script \"buffer_autoset.py\" to make local variabe persistent (see examples, below)!!\n'
+ '\n'
+ 'You can use following values for local variable:\n'
+ ' command: save commands only\n'
+ ' text: save text only (text sent to a channel buffer)\n'
+ ' all: save commands and text\n'
+ '\n'
+ 'Examples:\n'
+ ' save the command history manually (for example with /cron script):\n'
+ ' /' + SCRIPT_NAME + ' save\n'
+ ' save and restore command history for buffer #weechat on freenode (text only):\n'
+ ' /autosetbuffer add irc.freenode.#weechat localvar_set_save_history text\n'
+ ' save and restore command history for weechat core buffer (commands only):\n'
+ ' /autosetbuffer add core.weechat localvar_set_save_history command\n',
+ 'save %-'
+ '|| list %-',
+ 'histman_cmd_cb', '')
+
+ if int(version) >= 0x00040000:
+ if weechat.buffer_get_string(weechat.buffer_search_main(),'localvar_histman') == 'on':
+ init_options()
+ create_hooks()
+ weechat.prnt('','%s%s: do not start this script two times. command history was already restored, during this session!' % (weechat.prefix('error'),SCRIPT_NAME))
+ else:
+ init_options()
+ # create dir, if not exist
+ config_create_dir()
+
+ # look for global_history
+ if OPTIONS['save_global'].lower() != 'off':
+ read_history(filename_global_history,'')
+
+ # core buffer is already open on script startup. Check manually!
+ filename = get_filename_with_path('core.weechat')
+ read_history('core.weechat',weechat.buffer_search_main())
+
+ create_hooks()
+
+ # set localvar, to start script only once!
+ weechat.buffer_set(weechat.buffer_search_main(), 'localvar_set_histman', 'on')
+ else:
+ weechat.prnt('','%s%s: needs version 0.4.0 or higher' % (weechat.prefix('error'),SCRIPT_NAME))
+ weechat.command('','/wait 1ms /python unload %s' % SCRIPT_NAME)
diff --git a/weechat/python/irssi_awaylog.py b/weechat/python/irssi_awaylog.py
new file mode 100644
index 0000000..0743963
--- /dev/null
+++ b/weechat/python/irssi_awaylog.py
@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+#
+# irssi_awaylog.py: emulates irssi awaylog (replay of hilights and privmsg)
+# - 2013, henrik <henrik at affekt dot org>
+#
+# TODO: store awaylog in a file instead of memory
+#
+###########################################################################
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+###########################################################################
+
+import_ok = True
+try:
+ import weechat as wc
+except Exception:
+ print "This script must be run under WeeChat."
+ print "Get WeeChat now at: http://www.weechat.org/"
+ import_ok = False
+
+import time
+
+SCRIPT_NAME = "irssi_awaylog"
+SCRIPT_AUTHOR = "henrik"
+SCRIPT_VERSION = "0.3"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Emulates irssis awaylog behaviour"
+
+awaylog = []
+
+def replaylog():
+ global awaylog
+
+ if awaylog:
+ wc.prnt("", "-->\t")
+ for a in awaylog:
+ wc.prnt_date_tags("", a[0], "", a[1])
+ wc.prnt("", "<--\t")
+
+ awaylog = []
+
+
+def away_cb(data, bufferp, command):
+ isaway = wc.buffer_get_string(bufferp, "localvar_away") != ""
+
+ if not isaway:
+ replaylog()
+ return wc.WEECHAT_RC_OK
+
+def msg_cb(data, bufferp, date, tagsn, isdisplayed, ishilight, prefix, message):
+ global awaylog
+
+ isaway = wc.buffer_get_string(bufferp, "localvar_away") != ""
+ isprivate = wc.buffer_get_string(bufferp, "localvar_type") == "private"
+
+ # catch private messages or highlights when away
+ if isaway and (isprivate or int(ishilight)):
+ logentry = "awaylog\t"
+
+ if int(ishilight) and not isprivate:
+ buffer = (wc.buffer_get_string(bufferp, "short_name") or
+ wc.buffer_get_string(bufferp, "name"))
+ else:
+ buffer = "priv"
+
+ buffer = wc.color("green") + buffer + wc.color("reset")
+
+ logentry += "[" + buffer + "]"
+ logentry += wc.color("default") + " <" + wc.color("blue") + prefix + wc.color("default") + "> " + wc.color("reset") + message
+
+ awaylog.append((int(time.time()), logentry))
+ return wc.WEECHAT_RC_OK
+
+if __name__ == "__main__":
+ if import_ok and wc.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
+ wc.hook_print("", "notify_message", "", 1, "msg_cb", "")
+ wc.hook_print("", "notify_private", "", 1, "msg_cb", "")
+ wc.hook_command_run("/away", "away_cb", "")
diff --git a/weechat/python/otr.py b/weechat/python/otr.py
new file mode 100644
index 0000000..404e96a
--- /dev/null
+++ b/weechat/python/otr.py
@@ -0,0 +1,2062 @@
+# -*- coding: utf-8 -*-
+# otr - WeeChat script for Off-the-Record IRC messaging
+#
+# DISCLAIMER: To the best of my knowledge this script securely provides OTR
+# messaging in WeeChat, but I offer no guarantee. Please report any security
+# holes you find.
+#
+# Copyright (c) 2012-2015 Matthew M. Boedicker <matthewm@boedicker.org>
+# Nils Görs <weechatter@arcor.de>
+# Daniel "koolfy" Faucon <koolfy@koolfy.be>
+# Felix Eckhofer <felix@tribut.de>
+#
+# Report issues at https://github.com/mmb/weechat-otr
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import unicode_literals
+
+import collections
+import glob
+import io
+import os
+import platform
+import re
+import traceback
+import shlex
+import shutil
+import sys
+
+class PythonVersion2(object):
+ """Python 2 version of code that must differ between Python 2 and 3."""
+
+ def __init__(self):
+ import cgi
+ self.cgi = cgi
+
+ import HTMLParser
+ self.html_parser = HTMLParser
+ self.html_parser_init_kwargs = {}
+
+ import htmlentitydefs
+ self.html_entities = htmlentitydefs
+
+ def html_escape(self, strng):
+ """Escape HTML characters in a string."""
+ return self.cgi.escape(strng)
+
+ def unicode(self, *args, **kwargs):
+ """Return the Unicode version of a string."""
+ return unicode(*args, **kwargs)
+
+ def unichr(self, *args, **kwargs):
+ """Return the one character string of a Unicode character number."""
+ return unichr(*args, **kwargs)
+
+ def to_unicode(self, strng):
+ """Convert a utf-8 encoded string to a Unicode."""
+ if isinstance(strng, unicode):
+ return strng
+ else:
+ return strng.decode('utf-8', 'replace')
+
+ def to_str(self, strng):
+ """Convert a Unicode to a utf-8 encoded string."""
+ return strng.encode('utf-8', 'replace')
+
+class PythonVersion3(object):
+ """Python 3 version of code that must differ between Python 2 and 3."""
+
+ def __init__(self, minor):
+ self.minor = minor
+
+ import html
+ self.html = html
+
+ import html.parser
+ self.html_parser = html.parser
+ if self.minor >= 4:
+ self.html_parser_init_kwargs = { 'convert_charrefs' : True }
+ else:
+ self.html_parser_init_kwargs = {}
+
+ import html.entities
+ self.html_entities = html.entities
+
+ def html_escape(self, strng):
+ """Escape HTML characters in a string."""
+ return self.html.escape(strng, quote=False)
+
+ def unicode(self, *args, **kwargs):
+ """Return the Unicode version of a string."""
+ return str(*args, **kwargs)
+
+ def unichr(self, *args, **kwargs):
+ """Return the one character string of a Unicode character number."""
+ return chr(*args, **kwargs)
+
+ def to_unicode(self, strng):
+ """Convert a utf-8 encoded string to unicode."""
+ if isinstance(strng, bytes):
+ return strng.decode('utf-8', 'replace')
+ else:
+ return strng
+
+ def to_str(self, strng):
+ """Convert a Unicode to a utf-8 encoded string."""
+ return strng
+
+if sys.version_info.major >= 3:
+ PYVER = PythonVersion3(sys.version_info.minor)
+else:
+ PYVER = PythonVersion2()
+
+import weechat
+
+import potr
+
+SCRIPT_NAME = 'otr'
+SCRIPT_DESC = 'Off-the-Record messaging for IRC'
+SCRIPT_HELP = """{description}
+
+Quick start:
+
+Add an OTR item to the status bar by adding '[otr]' to the config setting
+weechat.bar.status.items. This will show you whether your current conversation
+is encrypted, authenticated and logged. /set otr.* for OTR status bar
+customization options.
+
+Start a private conversation with a friend who has OTR: /query yourpeer hi
+
+In the private chat buffer: /otr start
+
+If you have not authenticated your peer yet, follow the instructions for
+authentication.
+
+You can, at any time, see the current OTR session status and fingerprints with:
+/otr status
+
+View OTR policies for your peer: /otr policy
+
+View default OTR policies: /otr policy default
+
+Start/Stop log recording for the current OTR session: /otr log [start|stop]
+This will be reverted back to the previous log setting at the end of the session.
+
+To refresh the OTR session: /otr refresh
+
+To end your private conversation: /otr finish
+
+This script supports only OTR protocol version 2.
+""".format(description=SCRIPT_DESC)
+
+SCRIPT_AUTHOR = 'Matthew M. Boedicker'
+SCRIPT_LICENCE = 'GPL3'
+SCRIPT_VERSION = '1.8.0'
+
+OTR_DIR_NAME = 'otr'
+
+OTR_QUERY_RE = re.compile(r'\?OTR(\?|\??v[a-z\d]*\?)')
+
+POLICIES = {
+ 'allow_v2' : 'allow OTR protocol version 2, effectively enable OTR '
+ 'since v2 is the only supported version',
+ 'require_encryption' : 'refuse to send unencrypted messages when OTR is '
+ 'enabled',
+ 'log' : 'enable logging of OTR conversations',
+ 'send_tag' : 'advertise your OTR capability using the whitespace tag',
+ 'html_escape' : 'escape HTML special characters in outbound messages',
+ 'html_filter' : 'filter HTML in incoming messages',
+ }
+
+READ_ONLY_POLICIES = {
+ 'allow_v1' : False,
+ }
+
+ACTION_PREFIX = '/me '
+IRC_ACTION_RE = re.compile('^\x01ACTION (?P<text>.*)\x01$')
+PLAIN_ACTION_RE = re.compile('^'+ACTION_PREFIX+'(?P<text>.*)$')
+
+IRC_SANITIZE_TABLE = dict((ord(char), None) for char in '\n\r\x00')
+
+global otr_debug_buffer
+otr_debug_buffer = None
+
+# Patch potr.proto.TaggedPlaintext to not end plaintext tags in a space.
+#
+# When POTR adds OTR tags to plaintext it puts them at the end of the message.
+# The tags end in a space which gets stripped off by WeeChat because it
+# strips trailing spaces from commands. This causes OTR initiation to fail so
+# the following code adds an extra tab at the end of the plaintext tags if
+# they end in a space.
+#
+# The patched version also skips OTR tagging for CTCP messages because it
+# breaks the CTCP format.
+def patched__bytes__(self):
+ # Do not tag CTCP messages.
+ if self.msg.startswith(b'\x01') and \
+ self.msg.endswith(b'\x01'):
+ return self.msg
+
+ data = self.msg + potr.proto.MESSAGE_TAG_BASE
+ for v in self.versions:
+ data += potr.proto.MESSAGE_TAGS[v]
+ if data.endswith(b' '):
+ data += b'\t'
+ return data
+
+potr.proto.TaggedPlaintext.__bytes__ = patched__bytes__
+
+def command(buf, command_str):
+ """Wrap weechat.command() with utf-8 encode."""
+ debug(command_str)
+ weechat.command(buf, PYVER.to_str(command_str))
+
+def privmsg(server, nick, message):
+ """Send a private message to a nick."""
+ for line in message.splitlines():
+ command('', '/quote -server {server} PRIVMSG {nick} :{line}'.format(
+ server=irc_sanitize(server),
+ nick=irc_sanitize(nick),
+ line=irc_sanitize(line)))
+
+def build_privmsg_in(fromm, target, msg):
+ """Build inbound IRC PRIVMSG command."""
+ return ':{user} PRIVMSG {target} :{msg}'.format(
+ user=irc_sanitize(fromm),
+ target=irc_sanitize(target),
+ msg=irc_sanitize(msg))
+
+def build_privmsgs_in(fromm, target, msg, prefix=''):
+ """Build an inbound IRC PRIVMSG command for each line in msg.
+ If prefix is supplied, prefix each line of msg with it."""
+ cmd = []
+ for line in msg.splitlines():
+ cmd.append(build_privmsg_in(fromm, target, prefix+line))
+ return '\r\n'.join(cmd)
+
+def build_privmsg_out(target, msg):
+ """Build outbound IRC PRIVMSG command(s)."""
+ cmd = []
+ for line in msg.splitlines():
+ cmd.append('PRIVMSG {target} :{line}'.format(
+ target=irc_sanitize(target),
+ line=irc_sanitize(line)))
+ return '\r\n'.join(cmd)
+
+def irc_sanitize(msg):
+ """Remove NUL, CR and LF characters from msg.
+ The (utf-8 encoded version of a) string returned from this function
+ should be safe to use as an argument in an irc command."""
+ return PYVER.unicode(msg).translate(IRC_SANITIZE_TABLE)
+
+def prnt(buf, message):
+ """Wrap weechat.prnt() with utf-8 encode."""
+ weechat.prnt(buf, PYVER.to_str(message))
+
+def print_buffer(buf, message, level='info'):
+ """Print message to buf with prefix,
+ using color according to level."""
+ prnt(buf, '{prefix}\t{msg}'.format(
+ prefix=get_prefix(),
+ msg=colorize(message, 'buffer.{}'.format(level))))
+
+def get_prefix():
+ """Returns configured message prefix."""
+ return weechat.string_eval_expression(
+ config_string('look.prefix'),
+ {}, {}, {})
+
+def debug(msg):
+ """Send a debug message to the OTR debug buffer."""
+ debug_option = weechat.config_get(config_prefix('general.debug'))
+ global otr_debug_buffer
+
+ if weechat.config_boolean(debug_option):
+ if not otr_debug_buffer:
+ otr_debug_buffer = weechat.buffer_new("OTR Debug", "", "",
+ "debug_buffer_close_cb", "")
+ weechat.buffer_set(otr_debug_buffer, 'title', 'OTR Debug')
+ weechat.buffer_set(otr_debug_buffer, 'localvar_set_no_log', '1')
+ prnt(otr_debug_buffer, ('{script} debug\t{text}'.format(
+ script=SCRIPT_NAME,
+ text=PYVER.unicode(msg)
+ )))
+
+def debug_buffer_close_cb(data, buf):
+ """Set the OTR debug buffer to None."""
+ global otr_debug_buffer
+ otr_debug_buffer = None
+ return weechat.WEECHAT_RC_OK
+
+def current_user(server_name):
+ """Get the nick and server of the current user on a server."""
+ return irc_user(info_get('irc_nick', server_name), server_name)
+
+def irc_user(nick, server):
+ """Build an IRC user string from a nick and server."""
+ return '{nick}@{server}'.format(
+ nick=nick.lower(),
+ server=server)
+
+def isupport_value(server, feature):
+ """Get the value of an IRC server feature."""
+ args = '{server},{feature}'.format(server=server, feature=feature)
+ return info_get('irc_server_isupport_value', args)
+
+def is_a_channel(channel, server):
+ """Return true if a string has an IRC channel prefix."""
+ prefixes = \
+ tuple(isupport_value(server, 'CHANTYPES')) + \
+ tuple(isupport_value(server, 'STATUSMSG'))
+
+ # If the server returns nothing for CHANTYPES and STATUSMSG use
+ # default prefixes.
+ if not prefixes:
+ prefixes = ('#', '&', '+', '!', '@')
+
+ return channel.startswith(prefixes)
+
+# Exception class for PRIVMSG parsing exceptions.
+class PrivmsgParseException(Exception):
+ pass
+
+def parse_irc_privmsg(message, server):
+ """Parse an IRC PRIVMSG command and return a dictionary.
+
+ Either the to_channel key or the to_nick key will be set depending on
+ whether the message is to a nick or a channel. The other will be None.
+
+ Example input:
+
+ :nick!user@host PRIVMSG #weechat :message here
+
+ Output:
+
+ {'from': 'nick!user@host',
+ 'from_nick': 'nick',
+ 'to': '#weechat',
+ 'to_channel': '#weechat',
+ 'to_nick': None,
+ 'text': 'message here'}
+ """
+
+ weechat_result = weechat.info_get_hashtable(
+ 'irc_message_parse', dict(message=message))
+
+ if weechat_result['command'].upper() == 'PRIVMSG':
+ target, text = PYVER.to_unicode(
+ weechat_result['arguments']).split(' ', 1)
+ if text.startswith(':'):
+ text = text[1:]
+
+ result = {
+ 'from': PYVER.to_unicode(weechat_result['host']),
+ 'to' : target,
+ 'text': text,
+ }
+
+ if weechat_result['host']:
+ result['from_nick'] = PYVER.to_unicode(weechat_result['nick'])
+ else:
+ result['from_nick'] = ''
+
+ if is_a_channel(target, server):
+ result['to_channel'] = target
+ result['to_nick'] = None
+ else:
+ result['to_channel'] = None
+ result['to_nick'] = target
+
+ return result
+ else:
+ raise PrivmsgParseException(message)
+
+def has_otr_end(msg):
+ """Return True if the message is the end of an OTR message."""
+ return msg.endswith('.') or msg.endswith(',')
+
+def first_instance(objs, klass):
+ """Return the first object in the list that is an instance of a class."""
+ for obj in objs:
+ if isinstance(obj, klass):
+ return obj
+
+def config_prefix(option):
+ """Add the config prefix to an option and return the full option name."""
+ return '{script}.{option}'.format(
+ script=SCRIPT_NAME,
+ option=option)
+
+def config_color(option):
+ """Get the color of a color config option."""
+ return weechat.color(weechat.config_color(weechat.config_get(
+ config_prefix('color.{}'.format(option)))))
+
+def config_string(option):
+ """Get the string value of a config option with utf-8 decode."""
+ return PYVER.to_unicode(weechat.config_string(
+ weechat.config_get(config_prefix(option))))
+
+def buffer_get_string(buf, prop):
+ """Wrap weechat.buffer_get_string() with utf-8 encode/decode."""
+ if buf is not None:
+ encoded_buf = PYVER.to_str(buf)
+ else:
+ encoded_buf = None
+
+ return PYVER.to_unicode(weechat.buffer_get_string(
+ encoded_buf, PYVER.to_str(prop)))
+
+def buffer_is_private(buf):
+ """Return True if a buffer is private."""
+ return buffer_get_string(buf, 'localvar_type') == 'private'
+
+def info_get(info_name, arguments):
+ """Wrap weechat.info_get() with utf-8 encode/decode."""
+ return PYVER.to_unicode(weechat.info_get(
+ PYVER.to_str(info_name), PYVER.to_str(arguments)))
+
+def msg_irc_from_plain(msg):
+ """Transform a plain-text message to irc format.
+ This will replace lines that start with /me with the respective
+ irc command."""
+ return PLAIN_ACTION_RE.sub('\x01ACTION \g<text>\x01', msg)
+
+def msg_plain_from_irc(msg):
+ """Transform an irc message to plain-text.
+ Any ACTION found will be rewritten as /me <text>."""
+ return IRC_ACTION_RE.sub(ACTION_PREFIX + r'\g<text>', msg)
+
+def default_peer_args(args, buf):
+ """Get the nick and server of a remote peer from command arguments or
+ a buffer.
+
+ args is the [nick, server] slice of arguments from a command.
+ If these are present, return them. If args is empty and the buffer buf
+ is private, return the remote nick and server of buf."""
+ result = None, None
+
+ if len(args) == 2:
+ result = tuple(args)
+ else:
+ if buffer_is_private(buf):
+ result = (
+ buffer_get_string(buf, 'localvar_channel'),
+ buffer_get_string(buf, 'localvar_server'))
+
+ return result
+
+def format_default_policies():
+ """Return current default policies formatted as a string for the user."""
+ buf = io.StringIO()
+
+ buf.write('Current default OTR policies:\n')
+
+ for policy, desc in sorted(POLICIES.items()):
+ buf.write(' {policy} ({desc}) : {value}\n'.format(
+ policy=policy,
+ desc=desc,
+ value=config_string('policy.default.{}'.format(policy))))
+
+ buf.write('Change default policies with: /otr policy default NAME on|off')
+
+ return buf.getvalue()
+
+def to_bytes(strng):
+ """Convert a python str or unicode to bytes."""
+ return strng.encode('utf-8', 'replace')
+
+def colorize(msg, color):
+ """Colorize each line of msg using color."""
+ result = []
+ colorstr = config_color(color)
+
+ for line in msg.splitlines():
+ result.append('{color}{msg}'.format(
+ color=colorstr,
+ msg=line))
+
+ return '\r\n'.join(result)
+
+def accounts():
+ """Return a list of all IrcOtrAccounts sorted by name."""
+ result = []
+ for key_path in glob.iglob(os.path.join(OTR_DIR, '*.key3')):
+ key_name, _ = os.path.splitext(os.path.basename(key_path))
+ result.append(ACCOUNTS[key_name])
+
+ return sorted(result, key=lambda account: account.name)
+
+def show_account_fingerprints():
+ """Print all account names and their fingerprints to the core buffer."""
+ table_formatter = TableFormatter()
+ for account in accounts():
+ table_formatter.add_row([
+ account.name,
+ str(account.getPrivkey())])
+ print_buffer('', table_formatter.format())
+
+def show_peer_fingerprints(grep=None):
+ """Print peer names and their fingerprints to the core buffer.
+
+ If grep is passed in, show all peer names containing that substring."""
+ trust_descs = {
+ '' : 'unverified',
+ 'smp' : 'SMP verified',
+ 'verified' : 'verified',
+ }
+
+ table_formatter = TableFormatter()
+ for account in accounts():
+ for peer, peer_data in sorted(account.trusts.items()):
+ for fingerprint, trust in sorted(peer_data.items()):
+ if grep is None or grep in peer:
+ table_formatter.add_row([
+ peer,
+ account.name,
+ potr.human_hash(fingerprint),
+ trust_descs[trust],
+ ])
+ print_buffer('', table_formatter.format())
+
+def private_key_file_path(account_name):
+ """Return the private key file path for an account."""
+ return os.path.join(OTR_DIR, '{}.key3'.format(account_name))
+
+def read_private_key(key_file_path):
+ """Return the private key in a private key file."""
+ debug(('read private key', key_file_path))
+
+ with open(key_file_path, 'rb') as key_file:
+ return potr.crypt.PK.parsePrivateKey(key_file.read())[0]
+
+class AccountDict(collections.defaultdict):
+ """Dictionary that adds missing keys as IrcOtrAccount instances."""
+
+ def __missing__(self, key):
+ debug(('add account', key))
+ self[key] = IrcOtrAccount(key)
+
+ return self[key]
+
+class Assembler(object):
+ """Reassemble fragmented OTR messages.
+
+ This does not deal with OTR fragmentation, which is handled by potr, but
+ fragmentation of received OTR messages that are too large for IRC.
+ """
+ def __init__(self):
+ self.clear()
+
+ def add(self, data):
+ """Add data to the buffer."""
+ self.value += data
+
+ def clear(self):
+ """Empty the buffer."""
+ self.value = ''
+
+ def is_done(self):
+ """Return True if the buffer is a complete message."""
+ return self.is_query() or \
+ not to_bytes(self.value).startswith(potr.proto.OTRTAG) or \
+ has_otr_end(self.value)
+
+ def get(self):
+ """Return the current value of the buffer and empty it."""
+ result = self.value
+ self.clear()
+
+ return result
+
+ def is_query(self):
+ """Return true if the buffer is an OTR query."""
+ return OTR_QUERY_RE.search(self.value)
+
+class IrcContext(potr.context.Context):
+ """Context class for OTR over IRC."""
+
+ def __init__(self, account, peername):
+ super(IrcContext, self).__init__(account, peername)
+
+ self.peer_nick, self.peer_server = peername.split('@', 1)
+ self.in_assembler = Assembler()
+ self.in_otr_message = False
+ self.in_smp = False
+ self.smp_question = False
+
+ def policy_config_option(self, policy):
+ """Get the option name of a policy option for this context."""
+ return config_prefix('.'.join([
+ 'policy', self.peer_server, self.user.nick, self.peer_nick,
+ policy.lower()]))
+
+ def getPolicy(self, key):
+ """Get the value of a policy option for this context."""
+ key_lower = key.lower()
+
+ if key_lower in READ_ONLY_POLICIES:
+ result = READ_ONLY_POLICIES[key_lower]
+ elif key_lower == 'send_tag' and self.no_send_tag():
+ result = False
+ else:
+ option = weechat.config_get(
+ PYVER.to_str(self.policy_config_option(key)))
+
+ if option == '':
+ option = weechat.config_get(
+ PYVER.to_str(self.user.policy_config_option(key)))
+
+ if option == '':
+ option = weechat.config_get(config_prefix('.'.join(
+ ['policy', self.peer_server, key_lower])))
+
+ if option == '':
+ option = weechat.config_get(
+ config_prefix('policy.default.{}'.format(key_lower)))
+
+ result = bool(weechat.config_boolean(option))
+
+ debug(('getPolicy', key, result))
+
+ return result
+
+ def inject(self, msg, appdata=None):
+ """Send a message to the remote peer."""
+ if isinstance(msg, potr.proto.OTRMessage):
+ msg = PYVER.unicode(msg)
+ else:
+ msg = PYVER.to_unicode(msg)
+
+ debug(('inject', msg, 'len {}'.format(len(msg)), appdata))
+
+ privmsg(self.peer_server, self.peer_nick, msg)
+
+ def setState(self, newstate):
+ """Handle state transition."""
+ debug(('state', self.state, newstate))
+
+ if self.is_encrypted():
+ if newstate == potr.context.STATE_ENCRYPTED:
+ self.print_buffer(
+ 'Private conversation has been refreshed.', 'success')
+ elif newstate == potr.context.STATE_FINISHED:
+ self.print_buffer(
+ '{peer} has ended the private conversation. You should do '
+ 'the same:\n/otr finish'.format(peer=self.peer_nick))
+ elif newstate == potr.context.STATE_ENCRYPTED:
+ # unencrypted => encrypted
+ trust = self.getCurrentTrust()
+
+ # Disable logging before any proof of OTR activity is generated.
+ # This is necessary when the session is started automatically, and
+ # not by /otr start.
+ if not self.getPolicy('log'):
+ self.previous_log_level = self.disable_logging()
+ else:
+ self.previous_log_level = self.get_log_level()
+ if self.is_logged():
+ self.hint(
+ 'You have enabled the recording to disk of OTR '
+ 'conversations. By doing this you are potentially '
+ 'putting yourself and your correspondent in danger. '
+ 'Please consider disabling this policy with '
+ '"/otr policy default log off". To disable logging '
+ 'for this OTR session, use "/otr log stop"')
+
+ if trust is None:
+ fpr = str(self.getCurrentKey())
+ self.print_buffer('New fingerprint: {}'.format(fpr), 'warning')
+ self.setCurrentTrust('')
+
+ if bool(trust):
+ self.print_buffer(
+ 'Authenticated secured OTR conversation started.',
+ 'success')
+ else:
+ self.print_buffer(
+ 'Unauthenticated secured OTR conversation started.',
+ 'warning')
+ self.hint(self.verify_instructions())
+
+ if self.state != potr.context.STATE_PLAINTEXT and \
+ newstate == potr.context.STATE_PLAINTEXT:
+ self.print_buffer('Private conversation ended.')
+
+ # If we altered the logging value, restore it.
+ if self.previous_log_level is not None:
+ self.restore_logging(self.previous_log_level)
+
+ super(IrcContext, self).setState(newstate)
+
+ def maxMessageSize(self, appdata=None):
+ """Return the max message size for this context."""
+ # remove 'PRIVMSG <nick> :' from max message size
+ result = self.user.maxMessageSize - 10 - len(self.peer_nick)
+ debug('max message size {}'.format(result))
+
+ return result
+
+ def buffer(self):
+ """Get the buffer for this context."""
+ return info_get(
+ 'irc_buffer', '{server},{nick}'.format(
+ server=self.peer_server,
+ nick=self.peer_nick
+ ))
+
+ def print_buffer(self, msg, level='info'):
+ """Print a message to the buffer for this context.
+ level is used to colorize the message."""
+ buf = self.buffer()
+
+ # add [nick] prefix if we have only a server buffer for the query
+ if self.peer_nick and not buffer_is_private(buf):
+ msg = '[{nick}] {msg}'.format(
+ nick=self.peer_nick,
+ msg=msg)
+
+ print_buffer(buf, msg, level)
+
+ def hint(self, msg):
+ """Print a message to the buffer but only when hints are enabled."""
+ hints_option = weechat.config_get(config_prefix('general.hints'))
+
+ if weechat.config_boolean(hints_option):
+ self.print_buffer(msg, 'hint')
+
+ def smp_finish(self, message=False, level='info'):
+ """Reset SMP state and send a message to the user."""
+ self.in_smp = False
+ self.smp_question = False
+
+ self.user.saveTrusts()
+ if message:
+ self.print_buffer(message, level)
+
+ def handle_tlvs(self, tlvs):
+ """Handle SMP states."""
+ if tlvs:
+ smp1q = first_instance(tlvs, potr.proto.SMP1QTLV)
+ smp3 = first_instance(tlvs, potr.proto.SMP3TLV)
+ smp4 = first_instance(tlvs, potr.proto.SMP4TLV)
+
+ if first_instance(tlvs, potr.proto.SMPABORTTLV):
+ debug('SMP aborted by peer')
+ self.smp_finish('SMP aborted by peer.', 'warning')
+ elif self.in_smp and not self.smpIsValid():
+ debug('SMP aborted')
+ self.smp_finish('SMP aborted.', 'error')
+ elif first_instance(tlvs, potr.proto.SMP1TLV):
+ debug('SMP1')
+ self.in_smp = True
+
+ self.print_buffer(
+ """Peer has requested SMP verification.
+Respond with: /otr smp respond <secret>""")
+ elif smp1q:
+ debug(('SMP1Q', smp1q.msg))
+ self.in_smp = True
+ self.smp_question = True
+
+ self.print_buffer(
+ """Peer has requested SMP verification: {msg}
+Respond with: /otr smp respond <answer>""".format(
+ msg=PYVER.to_unicode(smp1q.msg)))
+ elif first_instance(tlvs, potr.proto.SMP2TLV):
+ if not self.in_smp:
+ debug('Received unexpected SMP2')
+ self.smp_finish()
+ else:
+ debug('SMP2')
+ self.print_buffer('SMP progressing.')
+ elif smp3 or smp4:
+ if smp3:
+ debug('SMP3')
+ elif smp4:
+ debug('SMP4')
+
+ if self.smpIsSuccess():
+
+ if self.smp_question:
+ self.smp_finish('SMP verification succeeded.',
+ 'success')
+ if not self.is_verified:
+ self.print_buffer(
+ """You may want to authenticate your peer by asking your own question:
+/otr smp ask <'question'> 'secret'""")
+
+ else:
+ self.smp_finish('SMP verification succeeded.',
+ 'success')
+
+ else:
+ self.smp_finish('SMP verification failed.', 'error')
+
+ def verify_instructions(self):
+ """Generate verification instructions for user."""
+ return """You can verify that this contact is who they claim to be in one of the following ways:
+
+1) Verify each other's fingerprints using a secure channel:
+ Your fingerprint : {your_fp}
+ {peer}'s fingerprint : {peer_fp}
+ then use the command: /otr trust {peer_nick} {peer_server}
+
+2) SMP pre-shared secret that you both know:
+ /otr smp ask {peer_nick} {peer_server} 'secret'
+
+3) SMP pre-shared secret that you both know with a question:
+ /otr smp ask {peer_nick} {peer_server} <'question'> 'secret'
+
+Note: You can safely omit specifying the peer and server when
+ executing these commands from the appropriate conversation
+ buffer
+""".format(
+ your_fp=self.user.getPrivkey(),
+ peer=self.peer,
+ peer_nick=self.peer_nick,
+ peer_server=self.peer_server,
+ peer_fp=potr.human_hash(
+ self.crypto.theirPubkey.cfingerprint()),
+ )
+
+ def is_encrypted(self):
+ """Return True if the conversation with this context's peer is
+ currently encrypted."""
+ return self.state == potr.context.STATE_ENCRYPTED
+
+ def is_verified(self):
+ """Return True if this context's peer is verified."""
+ return bool(self.getCurrentTrust())
+
+ def format_policies(self):
+ """Return current policies for this context formatted as a string for
+ the user."""
+ buf = io.StringIO()
+
+ buf.write('Current OTR policies for {peer}:\n'.format(
+ peer=self.peer))
+
+ for policy, desc in sorted(POLICIES.items()):
+ buf.write(' {policy} ({desc}) : {value}\n'.format(
+ policy=policy,
+ desc=desc,
+ value='on' if self.getPolicy(policy) else 'off'))
+
+ buf.write('Change policies with: /otr policy NAME on|off')
+
+ return buf.getvalue()
+
+ def is_logged(self):
+ """Return True if conversations with this context's peer are currently
+ being logged to disk."""
+ infolist = weechat.infolist_get('logger_buffer', '', '')
+
+ buf = self.buffer()
+
+ result = False
+
+ while weechat.infolist_next(infolist):
+ if weechat.infolist_pointer(infolist, 'buffer') == buf:
+ result = bool(weechat.infolist_integer(infolist, 'log_enabled'))
+ break
+
+ weechat.infolist_free(infolist)
+
+ return result
+
+ def get_log_level(self):
+ """Return the current logging level for this context's peer
+ or -1 if the buffer uses the default log level of weechat."""
+ infolist = weechat.infolist_get('logger_buffer', '', '')
+
+ buf = self.buffer()
+
+ if not weechat.config_get(self.get_logger_option_name(buf)):
+ result = -1
+ else:
+ result = 0
+
+ while weechat.infolist_next(infolist):
+ if weechat.infolist_pointer(infolist, 'buffer') == buf:
+ result = weechat.infolist_integer(infolist, 'log_level')
+ break
+
+ weechat.infolist_free(infolist)
+
+ return result
+
+ def get_logger_option_name(self, buf):
+ """Returns the logger config option for the specified buffer."""
+ name = buffer_get_string(buf, 'name')
+ plugin = buffer_get_string(buf, 'plugin')
+
+ return 'logger.level.{plugin}.{name}'.format(
+ plugin=plugin, name=name)
+
+ def disable_logging(self):
+ """Return the previous logger level and set the buffer logger level
+ to 0. If it was already 0, return None."""
+ # If previous_log_level has not been previously set, return the level
+ # we detect now.
+ if not hasattr(self, 'previous_log_level'):
+ previous_log_level = self.get_log_level()
+
+ if self.is_logged():
+ weechat.command(self.buffer(), '/mute logger disable')
+ self.print_buffer(
+ 'Logs have been temporarily disabled for the session. They will be restored upon finishing the OTR session.')
+
+ return previous_log_level
+
+ # If previous_log_level was already set, it means we already altered it
+ # and that we just detected an already modified logging level.
+ # Return the pre-existing value so it doesn't get lost, and we can
+ # restore it later.
+ else:
+ return self.previous_log_level
+
+ def restore_logging(self, previous_log_level):
+ """Restore the log level of the buffer."""
+ buf = self.buffer()
+
+ if (previous_log_level >= 0) and (previous_log_level < 10):
+ self.print_buffer(
+ 'Restoring buffer logging value to: {}'.format(
+ previous_log_level), 'warning')
+ weechat.command(buf, '/mute logger set {}'.format(
+ previous_log_level))
+
+ if previous_log_level == -1:
+ logger_option_name = self.get_logger_option_name(buf)
+ self.print_buffer(
+ 'Restoring buffer logging value to default', 'warning')
+ weechat.command(buf, '/mute unset {}'.format(
+ logger_option_name))
+
+ del self.previous_log_level
+
+ def msg_convert_in(self, msg):
+ """Transform incoming OTR message to IRC format.
+ This includes stripping html, converting plain-text ACTIONs
+ and character encoding conversion.
+ Only character encoding is changed if context is unencrypted."""
+ msg = PYVER.to_unicode(msg)
+
+ if not self.is_encrypted():
+ return msg
+
+ if self.getPolicy('html_filter'):
+ try:
+ msg = IrcHTMLParser.parse(msg)
+ except PYVER.html_parser.HTMLParseError:
+ pass
+
+ return msg_irc_from_plain(msg)
+
+ def msg_convert_out(self, msg):
+ """Convert an outgoing IRC message to be sent over OTR.
+ This includes escaping html, converting ACTIONs to plain-text
+ and character encoding conversion
+ Only character encoding is changed if context is unencrypted."""
+ if self.is_encrypted():
+ msg = msg_plain_from_irc(msg)
+
+ if self.getPolicy('html_escape'):
+ msg = PYVER.html_escape(msg)
+
+ # potr expects bytes to be returned
+ return to_bytes(msg)
+
+ def no_send_tag(self):
+ """Skip OTR whitespace tagging to bots and services.
+
+ Any nicks matching the otr.general.no_send_tag_regex config setting
+ will not be tagged.
+ """
+ no_send_tag_regex = config_string('general.no_send_tag_regex')
+ debug(('no_send_tag', no_send_tag_regex, self.peer_nick))
+ if no_send_tag_regex:
+ return re.match(no_send_tag_regex, self.peer_nick, re.IGNORECASE)
+
+ def __repr__(self):
+ return PYVER.to_str(('<{} {:x} peer_nick={c.peer_nick} '
+ 'peer_server={c.peer_server}>').format(
+ self.__class__.__name__, id(self), c=self))
+
+class IrcOtrAccount(potr.context.Account):
+ """Account class for OTR over IRC."""
+
+ contextclass = IrcContext
+
+ PROTOCOL = 'irc'
+ MAX_MSG_SIZE = 415
+
+ def __init__(self, name):
+ super(IrcOtrAccount, self).__init__(
+ name, IrcOtrAccount.PROTOCOL, IrcOtrAccount.MAX_MSG_SIZE)
+
+ self.nick, self.server = self.name.split('@', 1)
+
+ # IRC messages cannot have newlines, OTR query and "no plugin" text
+ # need to be one message
+ self.defaultQuery = self.defaultQuery.replace("\n", ' ')
+
+ self.key_file_path = private_key_file_path(name)
+ self.fpr_file_path = os.path.join(OTR_DIR, '{}.fpr'.format(name))
+
+ self.load_trusts()
+
+ def load_trusts(self):
+ """Load trust data from the fingerprint file."""
+ if os.path.exists(self.fpr_file_path):
+ with open(self.fpr_file_path) as fpr_file:
+ for line in fpr_file:
+ debug(('load trust check', line))
+
+ context, account, protocol, fpr, trust = \
+ PYVER.to_unicode(line[:-1]).split('\t')
+
+ if account == self.name and \
+ protocol == IrcOtrAccount.PROTOCOL:
+ debug(('set trust', context, fpr, trust))
+ self.setTrust(context, fpr, trust)
+
+ def loadPrivkey(self):
+ """Load key file.
+
+ If no key file exists, load the default key. If there is no default
+ key, a new key will be generated automatically by potr."""
+ debug(('load private key', self.key_file_path))
+
+ if os.path.exists(self.key_file_path):
+ return read_private_key(self.key_file_path)
+ else:
+ default_key = config_string('general.defaultkey')
+ if default_key:
+ default_key_path = private_key_file_path(default_key)
+
+ if os.path.exists(default_key_path):
+ shutil.copyfile(default_key_path, self.key_file_path)
+ return read_private_key(self.key_file_path)
+
+ def savePrivkey(self):
+ """Save key file."""
+ debug(('save private key', self.key_file_path))
+
+ with open(self.key_file_path, 'wb') as key_file:
+ key_file.write(self.getPrivkey().serializePrivateKey())
+
+ def saveTrusts(self):
+ """Save trusts."""
+ with open(self.fpr_file_path, 'w') as fpr_file:
+ for uid, trusts in self.trusts.items():
+ for fpr, trust in trusts.items():
+ debug(('trust write', uid, self.name,
+ IrcOtrAccount.PROTOCOL, fpr, trust))
+ fpr_file.write(PYVER.to_str('\t'.join(
+ (uid, self.name, IrcOtrAccount.PROTOCOL, fpr,
+ trust))))
+ fpr_file.write('\n')
+
+ def end_all_private(self):
+ """End all currently encrypted conversations."""
+ for context in self.ctxs.values():
+ if context.is_encrypted():
+ context.disconnect()
+
+ def policy_config_option(self, policy):
+ """Get the option name of a policy option for this account."""
+ return config_prefix('.'.join([
+ 'policy', self.server, self.nick, policy.lower()]))
+
+class IrcHTMLParser(PYVER.html_parser.HTMLParser):
+ """A simple HTML parser that throws away anything but newlines and links"""
+
+ @staticmethod
+ def parse(data):
+ """Create a temporary IrcHTMLParser and parse a single string"""
+ parser = IrcHTMLParser(**PYVER.html_parser_init_kwargs)
+ parser.feed(data)
+ parser.close()
+ return parser.result
+
+ def reset(self):
+ """Forget all state, called from __init__"""
+ PYVER.html_parser.HTMLParser.reset(self)
+ self.result = ''
+ self.linktarget = ''
+ self.linkstart = 0
+
+ def handle_starttag(self, tag, attrs):
+ """Called when a start tag is encountered"""
+ if tag == 'br':
+ self.result += '\n'
+ elif tag == 'a':
+ attrs = dict(attrs)
+ if 'href' in attrs:
+ self.result += '['
+ self.linktarget = attrs['href']
+ self.linkstart = len(self.result)
+
+ def handle_endtag(self, tag):
+ """Called when an end tag is encountered"""
+ if tag == 'a':
+ if self.linktarget:
+ if self.result[self.linkstart:] == self.linktarget:
+ self.result += ']'
+ else:
+ self.result += ']({})'.format(self.linktarget)
+ self.linktarget = ''
+
+ def handle_data(self, data):
+ """Called for character data (i.e. text)"""
+ self.result += data
+
+ def handle_entityref(self, name):
+ """Called for entity references, such as &amp;"""
+ try:
+ self.result += PYVER.unichr(
+ PYVER.html_entities.name2codepoint[name])
+ except KeyError:
+ self.result += '&{};'.format(name)
+
+ def handle_charref(self, name):
+ """Called for character references, such as &#39;"""
+ try:
+ if name.startswith('x'):
+ self.result += PYVER.unichr(int(name[1:], 16))
+ else:
+ self.result += PYVER.unichr(int(name))
+ except ValueError:
+ self.result += '&#{};'.format(name)
+
+class TableFormatter(object):
+ """Format lists of string into aligned tables."""
+
+ def __init__(self):
+ self.rows = []
+ self.max_widths = None
+
+ def add_row(self, row):
+ """Add a row to the table."""
+ self.rows.append(row)
+ row_widths = [ len(s) for s in row ]
+ if self.max_widths is None:
+ self.max_widths = row_widths
+ else:
+ self.max_widths = list(map(max, self.max_widths, row_widths))
+
+ def format(self):
+ """Return the formatted table as a string."""
+ return '\n'.join([ self.format_row(row) for row in self.rows ])
+
+ def format_row(self, row):
+ """Format a single row as a string."""
+ return ' |'.join(
+ [ s.ljust(self.max_widths[i]) for i, s in enumerate(row) ])
+
+def message_in_cb(data, modifier, modifier_data, string):
+ """Incoming message callback"""
+ debug(('message_in_cb', data, modifier, modifier_data, string))
+
+ parsed = parse_irc_privmsg(
+ PYVER.to_unicode(string), PYVER.to_unicode(modifier_data))
+ debug(('parsed message', parsed))
+
+ # skip processing messages to public channels
+ if parsed['to_channel']:
+ return string
+
+ server = PYVER.to_unicode(modifier_data)
+
+ from_user = irc_user(parsed['from_nick'], server)
+ local_user = current_user(server)
+
+ context = ACCOUNTS[local_user].getContext(from_user)
+
+ context.in_assembler.add(parsed['text'])
+
+ result = ''
+
+ if context.in_assembler.is_done():
+ try:
+ msg, tlvs = context.receiveMessage(
+ # potr expects bytes
+ to_bytes(context.in_assembler.get()))
+
+ debug(('receive', msg, tlvs))
+
+ if msg:
+ result = PYVER.to_str(build_privmsgs_in(
+ parsed['from'], parsed['to'],
+ context.msg_convert_in(msg)))
+
+ context.handle_tlvs(tlvs)
+ except potr.context.ErrorReceived as err:
+ context.print_buffer('Received OTR error: {}'.format(
+ PYVER.to_unicode(err.args[0].error)), 'error')
+ except potr.context.NotEncryptedError:
+ context.print_buffer(
+ 'Received encrypted data but no private session established.',
+ 'warning')
+ except potr.context.NotOTRMessage:
+ result = string
+ except potr.context.UnencryptedMessage as err:
+ result = PYVER.to_str(build_privmsgs_in(
+ parsed['from'], parsed['to'], PYVER.to_unicode(
+ msg_plain_from_irc(err.args[0])),
+ 'Unencrypted message received: '))
+
+ weechat.bar_item_update(SCRIPT_NAME)
+
+ return result
+
+def message_out_cb(data, modifier, modifier_data, string):
+ """Outgoing message callback."""
+ result = ''
+
+ # If any exception is raised in this function, WeeChat will not send the
+ # outgoing message, which could be something that the user intended to be
+ # encrypted. This paranoid exception handling ensures that the system
+ # fails closed and not open.
+ try:
+ debug(('message_out_cb', data, modifier, modifier_data, string))
+
+ parsed = parse_irc_privmsg(
+ PYVER.to_unicode(string), PYVER.to_unicode(modifier_data))
+ debug(('parsed message', parsed))
+
+ # skip processing messages to public channels
+ if parsed['to_channel']:
+ return string
+
+ server = PYVER.to_unicode(modifier_data)
+
+ to_user = irc_user(parsed['to_nick'], server)
+ local_user = current_user(server)
+
+ context = ACCOUNTS[local_user].getContext(to_user)
+ is_query = OTR_QUERY_RE.search(parsed['text'])
+
+ parsed_text_bytes = to_bytes(parsed['text'])
+
+ is_otr_message = \
+ parsed_text_bytes[:len(potr.proto.OTRTAG)] == potr.proto.OTRTAG
+
+ if is_otr_message and not is_query:
+ if not has_otr_end(parsed['text']):
+ debug('in OTR message')
+ context.in_otr_message = True
+ else:
+ debug('complete OTR message')
+ result = string
+ elif context.in_otr_message:
+ if has_otr_end(parsed['text']):
+ context.in_otr_message = False
+ debug('in OTR message end')
+ result = string
+ else:
+ debug(('context send message', parsed['text'], parsed['to_nick'],
+ server))
+
+ if context.policyOtrEnabled() and \
+ not context.is_encrypted() and \
+ not is_query and \
+ context.getPolicy('require_encryption'):
+ context.print_buffer(
+ 'Your message will not be sent, because policy requires an '
+ 'encrypted connection.', 'error')
+ context.hint(
+ 'Wait for the OTR connection or change the policy to allow '
+ 'clear-text messages:\n'
+ '/policy set require_encryption off')
+
+ try:
+ ret = context.sendMessage(
+ potr.context.FRAGMENT_SEND_ALL,
+ context.msg_convert_out(parsed['text']))
+
+ if ret:
+ debug(('sendMessage returned', ret))
+ result = PYVER.to_str(
+ build_privmsg_out(
+ parsed['to_nick'], PYVER.to_unicode(ret)
+ ))
+
+ except potr.context.NotEncryptedError as err:
+ if err.args[0] == potr.context.EXC_FINISHED:
+ context.print_buffer(
+ """Your message was not sent. End your private conversation:\n/otr finish""",
+ 'error')
+ else:
+ raise
+
+ weechat.bar_item_update(SCRIPT_NAME)
+ except:
+ try:
+ print_buffer('', traceback.format_exc(), 'error')
+ print_buffer('', 'Versions: {versions}'.format(
+ versions=dependency_versions()), 'error')
+ context.print_buffer(
+ 'Failed to send message. See core buffer for traceback.',
+ 'error')
+ except:
+ pass
+
+ return result
+
+def shutdown():
+ """Script unload callback."""
+ debug('shutdown')
+
+ weechat.config_write(CONFIG_FILE)
+
+ for account in ACCOUNTS.values():
+ account.end_all_private()
+
+ free_all_config()
+
+ weechat.bar_item_remove(OTR_STATUSBAR)
+
+ return weechat.WEECHAT_RC_OK
+
+def command_cb(data, buf, args):
+ """Parse and dispatch WeeChat OTR commands."""
+ result = weechat.WEECHAT_RC_ERROR
+
+ try:
+ arg_parts = [PYVER.to_unicode(arg) for arg in shlex.split(args)]
+ except:
+ debug("Command parsing error.")
+ return result
+
+ if len(arg_parts) in (1, 3) and arg_parts[0] in ('start', 'refresh'):
+ nick, server = default_peer_args(arg_parts[1:3], buf)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+ # We need to wall disable_logging() here so that no OTR-related
+ # buffer messages get logged at any point. disable_logging() will
+ # be called again when effectively switching to encrypted, but
+ # the previous_log_level we set here will be preserved for later
+ # restoring.
+ if not context.getPolicy('log'):
+ context.previous_log_level = context.disable_logging()
+ else:
+ context.previous_log_level = context.get_log_level()
+
+ context.hint('Sending OTR query... Please await confirmation of the OTR session being started before sending a message.')
+ if not context.getPolicy('send_tag'):
+ context.hint(
+ 'To try OTR on all conversations with {peer}: /otr policy send_tag on'.format(
+ peer=context.peer))
+
+ privmsg(server, nick, '?OTR?')
+
+ result = weechat.WEECHAT_RC_OK
+ elif len(arg_parts) in (1, 3) and arg_parts[0] in ('finish', 'end'):
+ nick, server = default_peer_args(arg_parts[1:3], buf)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+ context.disconnect()
+
+ result = weechat.WEECHAT_RC_OK
+
+ elif len(arg_parts) in (1, 3) and arg_parts[0] == 'status':
+ nick, server = default_peer_args(arg_parts[1:3], buf)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+ if context.is_encrypted():
+ context.print_buffer(
+ 'This conversation is encrypted.', 'success')
+ context.print_buffer("Your fingerprint is: {}".format(
+ context.user.getPrivkey()))
+ context.print_buffer("Your peer's fingerprint is: {}".format(
+ potr.human_hash(context.crypto.theirPubkey.cfingerprint())))
+ if context.is_verified():
+ context.print_buffer(
+ "The peer's identity has been verified.",
+ 'success')
+ else:
+ context.print_buffer(
+ "You have not verified the peer's identity yet.",
+ 'warning')
+ else:
+ context.print_buffer(
+ "This current conversation is not encrypted.",
+ 'warning')
+
+ result = weechat.WEECHAT_RC_OK
+
+ elif len(arg_parts) in range(2, 7) and arg_parts[0] == 'smp':
+ action = arg_parts[1]
+
+ if action == 'respond':
+ # Check if nickname and server are specified
+ if len(arg_parts) == 3:
+ nick, server = default_peer_args([], buf)
+ secret = arg_parts[2]
+ elif len(arg_parts) == 5:
+ nick, server = default_peer_args(arg_parts[2:4], buf)
+ secret = arg_parts[4]
+ else:
+ return weechat.WEECHAT_RC_ERROR
+
+ if secret:
+ secret = PYVER.to_str(secret)
+
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+ context.smpGotSecret(secret)
+
+ result = weechat.WEECHAT_RC_OK
+
+ elif action == 'ask':
+ question = None
+ secret = None
+
+ # Nickname and server are not specified
+ # Check whether it's a simple challenge or a question/answer request
+ if len(arg_parts) == 3:
+ nick, server = default_peer_args([], buf)
+ secret = arg_parts[2]
+ elif len(arg_parts) == 4:
+ nick, server = default_peer_args([], buf)
+ secret = arg_parts[3]
+ question = arg_parts[2]
+
+ # Nickname and server are specified
+ # Check whether it's a simple challenge or a question/answer request
+ elif len(arg_parts) == 5:
+ nick, server = default_peer_args(arg_parts[2:4], buf)
+ secret = arg_parts[4]
+ elif len(arg_parts) == 6:
+ nick, server = default_peer_args(arg_parts[2:4], buf)
+ secret = arg_parts[5]
+ question = arg_parts[4]
+ else:
+ return weechat.WEECHAT_RC_ERROR
+
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ if secret:
+ secret = PYVER.to_str(secret)
+ if question:
+ question = PYVER.to_str(question)
+
+ try:
+ context.smpInit(secret, question)
+ except potr.context.NotEncryptedError:
+ context.print_buffer(
+ 'There is currently no encrypted session with {}.'.format(
+ context.peer), 'error')
+ else:
+ if question:
+ context.print_buffer('SMP challenge sent...')
+ else:
+ context.print_buffer('SMP question sent...')
+ context.in_smp = True
+ result = weechat.WEECHAT_RC_OK
+
+ elif action == 'abort':
+ # Nickname and server are not specified
+ if len(arg_parts) == 2:
+ nick, server = default_peer_args([], buf)
+ # Nickname and server are specified
+ elif len(arg_parts) == 4:
+ nick, server = default_peer_args(arg_parts[2:4], buf)
+ else:
+ return weechat.WEECHAT_RC_ERROR
+
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ if context.in_smp:
+ try:
+ context.smpAbort()
+ except potr.context.NotEncryptedError:
+ context.print_buffer(
+ 'There is currently no encrypted session with {}.'.format(
+ context.peer), 'error')
+ else:
+ debug('SMP aborted')
+ context.smp_finish('SMP aborted.')
+ result = weechat.WEECHAT_RC_OK
+
+ elif len(arg_parts) in (1, 3) and arg_parts[0] == 'trust':
+ nick, server = default_peer_args(arg_parts[1:3], buf)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ if context.crypto.theirPubkey is not None:
+ context.setCurrentTrust('verified')
+ context.print_buffer('{peer} is now authenticated.'.format(
+ peer=context.peer))
+
+ weechat.bar_item_update(SCRIPT_NAME)
+ else:
+ context.print_buffer(
+ 'No fingerprint for {peer}. Start an OTR conversation first: /otr start'.format(
+ peer=context.peer), 'error')
+
+ result = weechat.WEECHAT_RC_OK
+ elif len(arg_parts) in (1, 3) and arg_parts[0] == 'distrust':
+ nick, server = default_peer_args(arg_parts[1:3], buf)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ if context.crypto.theirPubkey is not None:
+ context.setCurrentTrust('')
+ context.print_buffer(
+ '{peer} is now de-authenticated.'.format(
+ peer=context.peer))
+
+ weechat.bar_item_update(SCRIPT_NAME)
+ else:
+ context.print_buffer(
+ 'No fingerprint for {peer}. Start an OTR conversation first: /otr start'.format(
+ peer=context.peer), 'error')
+
+ result = weechat.WEECHAT_RC_OK
+
+ elif len(arg_parts) in (1, 2) and arg_parts[0] == 'log':
+ nick, server = default_peer_args([], buf)
+ if len(arg_parts) == 1:
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ if context.is_encrypted():
+ if context.is_logged():
+ context.print_buffer(
+ 'This conversation is currently being logged.',
+ 'warning')
+ result = weechat.WEECHAT_RC_OK
+
+ else:
+ context.print_buffer(
+ 'This conversation is currently NOT being logged.')
+ result = weechat.WEECHAT_RC_OK
+ else:
+ context.print_buffer(
+ 'OTR LOG: Not in an OTR session', 'error')
+ result = weechat.WEECHAT_RC_OK
+
+ else:
+ print_buffer('', 'OTR LOG: Not in an OTR session', 'error')
+ result = weechat.WEECHAT_RC_OK
+
+ if len(arg_parts) == 2:
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ if arg_parts[1] == 'start' and \
+ not context.is_logged() and \
+ context.is_encrypted():
+ if context.previous_log_level is None:
+ context.previous_log_level = context.get_log_level()
+ context.print_buffer('From this point on, this conversation will be logged. Please keep in mind that by doing so you are potentially putting yourself and your interlocutor at risk. You can disable this by doing /otr log stop', 'warning')
+ weechat.command(buf, '/mute logger set 9')
+ result = weechat.WEECHAT_RC_OK
+
+ elif arg_parts[1] == 'stop' and \
+ context.is_logged() and \
+ context.is_encrypted():
+ if context.previous_log_level is None:
+ context.previous_log_level = context.get_log_level()
+ weechat.command(buf, '/mute logger set 0')
+ context.print_buffer('From this point on, this conversation will NOT be logged ANYMORE.')
+ result = weechat.WEECHAT_RC_OK
+
+ elif not context.is_encrypted():
+ context.print_buffer(
+ 'OTR LOG: Not in an OTR session', 'error')
+ result = weechat.WEECHAT_RC_OK
+
+ else:
+ # Don't need to do anything.
+ result = weechat.WEECHAT_RC_OK
+
+ else:
+ print_buffer('', 'OTR LOG: Not in an OTR session', 'error')
+
+ elif len(arg_parts) in (1, 2, 3, 4) and arg_parts[0] == 'policy':
+ if len(arg_parts) == 1:
+ nick, server = default_peer_args([], buf)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ context.print_buffer(context.format_policies())
+ else:
+ prnt('', format_default_policies())
+
+ result = weechat.WEECHAT_RC_OK
+
+ elif len(arg_parts) == 2 and arg_parts[1].lower() == 'default':
+ nick, server = default_peer_args([], buf)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ context.print_buffer(format_default_policies())
+ else:
+ prnt('', format_default_policies())
+
+ result = weechat.WEECHAT_RC_OK
+
+ elif len(arg_parts) == 3 and arg_parts[1].lower() in POLICIES:
+ nick, server = default_peer_args([], buf)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ policy_var = context.policy_config_option(arg_parts[1].lower())
+
+ command('', '/set {policy} {value}'.format(
+ policy=policy_var,
+ value=arg_parts[2]))
+
+ context.print_buffer(context.format_policies())
+
+ result = weechat.WEECHAT_RC_OK
+
+ elif len(arg_parts) == 4 and \
+ arg_parts[1].lower() == 'default' and \
+ arg_parts[2].lower() in POLICIES:
+ nick, server = default_peer_args([], buf)
+
+ policy_var = "otr.policy.default." + arg_parts[2].lower()
+
+ command('', '/set {policy} {value}'.format(
+ policy=policy_var,
+ value=arg_parts[3]))
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+
+ context.print_buffer(format_default_policies())
+ else:
+ prnt('', format_default_policies())
+
+ result = weechat.WEECHAT_RC_OK
+ elif len(arg_parts) in (1, 2) and arg_parts[0] == 'fingerprint':
+ if len(arg_parts) == 1:
+ show_account_fingerprints()
+ result = weechat.WEECHAT_RC_OK
+ elif len(arg_parts) == 2:
+ if arg_parts[1] == 'all':
+ show_peer_fingerprints()
+ else:
+ show_peer_fingerprints(grep=arg_parts[1])
+ result = weechat.WEECHAT_RC_OK
+
+ return result
+
+def otr_statusbar_cb(data, item, window):
+ """Update the statusbar."""
+ if window:
+ buf = weechat.window_get_pointer(window, 'buffer')
+ else:
+ # If the bar item is in a root bar that is not in a window, window
+ # will be empty.
+ buf = weechat.current_buffer()
+
+ result = ''
+
+ if buffer_is_private(buf):
+ local_user = irc_user(
+ buffer_get_string(buf, 'localvar_nick'),
+ buffer_get_string(buf, 'localvar_server'))
+
+ remote_user = irc_user(
+ buffer_get_string(buf, 'localvar_channel'),
+ buffer_get_string(buf, 'localvar_server'))
+
+ context = ACCOUNTS[local_user].getContext(remote_user)
+
+ encrypted_str = config_string('look.bar.state.encrypted')
+ unencrypted_str = config_string('look.bar.state.unencrypted')
+ authenticated_str = config_string('look.bar.state.authenticated')
+ unauthenticated_str = config_string('look.bar.state.unauthenticated')
+ logged_str = config_string('look.bar.state.logged')
+ notlogged_str = config_string('look.bar.state.notlogged')
+
+ bar_parts = []
+
+ if context.is_encrypted():
+ if encrypted_str:
+ bar_parts.append(''.join([
+ config_color('status.encrypted'),
+ encrypted_str,
+ config_color('status.default')]))
+
+ if context.is_verified():
+ if authenticated_str:
+ bar_parts.append(''.join([
+ config_color('status.authenticated'),
+ authenticated_str,
+ config_color('status.default')]))
+ elif unauthenticated_str:
+ bar_parts.append(''.join([
+ config_color('status.unauthenticated'),
+ unauthenticated_str,
+ config_color('status.default')]))
+
+ if context.is_logged():
+ if logged_str:
+ bar_parts.append(''.join([
+ config_color('status.logged'),
+ logged_str,
+ config_color('status.default')]))
+ elif notlogged_str:
+ bar_parts.append(''.join([
+ config_color('status.notlogged'),
+ notlogged_str,
+ config_color('status.default')]))
+
+ elif unencrypted_str:
+ bar_parts.append(''.join([
+ config_color('status.unencrypted'),
+ unencrypted_str,
+ config_color('status.default')]))
+
+ result = config_string('look.bar.state.separator').join(bar_parts)
+
+ if result:
+ result = '{color}{prefix}{result}'.format(
+ color=config_color('status.default'),
+ prefix=config_string('look.bar.prefix'),
+ result=result)
+
+ return result
+
+def bar_config_update_cb(data, option):
+ """Callback for updating the status bar when its config changes."""
+ weechat.bar_item_update(SCRIPT_NAME)
+
+ return weechat.WEECHAT_RC_OK
+
+def policy_completion_cb(data, completion_item, buf, completion):
+ """Callback for policy tab completion."""
+ for policy in POLICIES:
+ weechat.hook_completion_list_add(
+ completion, policy, 0, weechat.WEECHAT_LIST_POS_SORT)
+
+ return weechat.WEECHAT_RC_OK
+
+def policy_create_option_cb(data, config_file, section, name, value):
+ """Callback for creating a new policy option when the user sets one
+ that doesn't exist."""
+ weechat.config_new_option(
+ config_file, section, name, 'boolean', '', '', 0, 0, value, value, 0,
+ '', '', '', '', '', '')
+
+ return weechat.WEECHAT_CONFIG_OPTION_SET_OK_CHANGED
+
+def logger_level_update_cb(data, option, value):
+ """Callback called when any logger level changes."""
+ weechat.bar_item_update(SCRIPT_NAME)
+
+ return weechat.WEECHAT_RC_OK
+
+def buffer_switch_cb(data, signal, signal_data):
+ """Callback for buffer switched.
+
+ Used for updating the status bar item when it is in a root bar.
+ """
+ weechat.bar_item_update(SCRIPT_NAME)
+
+ return weechat.WEECHAT_RC_OK
+
+def buffer_closing_cb(data, signal, signal_data):
+ """Callback for buffer closed.
+
+ It closes the OTR session when the buffer is about to be closed.
+ """
+ result = weechat.WEECHAT_RC_ERROR
+ nick, server = default_peer_args([], signal_data)
+
+ if nick is not None and server is not None:
+ context = ACCOUNTS[current_user(server)].getContext(
+ irc_user(nick, server))
+ context.disconnect()
+
+ result = weechat.WEECHAT_RC_OK
+ return result
+
+def init_config():
+ """Set up configuration options and load config file."""
+ global CONFIG_FILE
+ CONFIG_FILE = weechat.config_new(SCRIPT_NAME, 'config_reload_cb', '')
+
+ global CONFIG_SECTIONS
+ CONFIG_SECTIONS = {}
+
+ CONFIG_SECTIONS['general'] = weechat.config_new_section(
+ CONFIG_FILE, 'general', 0, 0, '', '', '', '', '', '', '', '', '', '')
+
+ for option, typ, desc, default in [
+ ('debug', 'boolean', 'OTR script debugging', 'off'),
+ ('hints', 'boolean', 'Give helpful hints how to use this script and how to stay secure while using OTR (recommended)', 'on'),
+ ('defaultkey', 'string',
+ 'default private key to use for new accounts (nick@server)', ''),
+ ('no_send_tag_regex', 'string',
+ 'do not OTR whitespace tag messages to nicks matching this regex '
+ '(case insensitive)',
+ '^(alis|chanfix|global|.+serv|\*.+)$'),
+ ]:
+ weechat.config_new_option(
+ CONFIG_FILE, CONFIG_SECTIONS['general'], option, typ, desc, '', 0,
+ 0, default, default, 0, '', '', '', '', '', '')
+
+ CONFIG_SECTIONS['color'] = weechat.config_new_section(
+ CONFIG_FILE, 'color', 0, 0, '', '', '', '', '', '', '', '', '', '')
+
+ for option, desc, default, update_cb in [
+ ('status.default', 'status bar default color', 'default',
+ 'bar_config_update_cb'),
+ ('status.encrypted', 'status bar encrypted indicator color', 'green',
+ 'bar_config_update_cb'),
+ ('status.unencrypted', 'status bar unencrypted indicator color',
+ 'lightred', 'bar_config_update_cb'),
+ ('status.authenticated', 'status bar authenticated indicator color',
+ 'green', 'bar_config_update_cb'),
+ ('status.unauthenticated', 'status bar unauthenticated indicator color',
+ 'lightred', 'bar_config_update_cb'),
+ ('status.logged', 'status bar logged indicator color', 'lightred',
+ 'bar_config_update_cb'),
+ ('status.notlogged', 'status bar not logged indicator color',
+ 'green', 'bar_config_update_cb'),
+ ('buffer.hint', 'text color for hints', 'lightblue', ''),
+ ('buffer.info', 'text color for informational messages', 'default', ''),
+ ('buffer.success', 'text color for success messages', 'lightgreen', ''),
+ ('buffer.warning', 'text color for warnings', 'yellow', ''),
+ ('buffer.error', 'text color for errors', 'lightred', ''),
+ ]:
+ weechat.config_new_option(
+ CONFIG_FILE, CONFIG_SECTIONS['color'], option, 'color', desc, '', 0,
+ 0, default, default, 0, '', '', update_cb, '', '', '')
+
+ CONFIG_SECTIONS['look'] = weechat.config_new_section(
+ CONFIG_FILE, 'look', 0, 0, '', '', '', '', '', '', '', '', '', '')
+
+ for option, desc, default, update_cb in [
+ ('bar.prefix', 'prefix for OTR status bar item', 'OTR:',
+ 'bar_config_update_cb'),
+ ('bar.state.encrypted',
+ 'shown in status bar when conversation is encrypted', 'SEC',
+ 'bar_config_update_cb'),
+ ('bar.state.unencrypted',
+ 'shown in status bar when conversation is not encrypted', '!SEC',
+ 'bar_config_update_cb'),
+ ('bar.state.authenticated',
+ 'shown in status bar when peer is authenticated', 'AUTH',
+ 'bar_config_update_cb'),
+ ('bar.state.unauthenticated',
+ 'shown in status bar when peer is not authenticated', '!AUTH',
+ 'bar_config_update_cb'),
+ ('bar.state.logged',
+ 'shown in status bar when peer conversation is being logged to disk',
+ 'LOG',
+ 'bar_config_update_cb'),
+ ('bar.state.notlogged',
+ 'shown in status bar when peer conversation is not being logged to disk',
+ '!LOG',
+ 'bar_config_update_cb'),
+ ('bar.state.separator', 'separator for states in the status bar', ',',
+ 'bar_config_update_cb'),
+ ('prefix', 'prefix used for messages from otr (note: content is evaluated, see /help eval)',
+ '${color:default}:! ${color:brown}otr${color:default} !:', ''),
+ ]:
+ weechat.config_new_option(
+ CONFIG_FILE, CONFIG_SECTIONS['look'], option, 'string', desc, '',
+ 0, 0, default, default, 0, '', '', update_cb, '', '', '')
+
+ CONFIG_SECTIONS['policy'] = weechat.config_new_section(
+ CONFIG_FILE, 'policy', 1, 1, '', '', '', '', '', '',
+ 'policy_create_option_cb', '', '', '')
+
+ for option, desc, default in [
+ ('default.allow_v2', 'default allow OTR v2 policy', 'on'),
+ ('default.require_encryption', 'default require encryption policy',
+ 'off'),
+ ('default.log', 'default enable logging to disk', 'off'),
+ ('default.send_tag', 'default send tag policy', 'off'),
+ ('default.html_escape', 'default HTML escape policy', 'off'),
+ ('default.html_filter', 'default HTML filter policy', 'on'),
+ ]:
+ weechat.config_new_option(
+ CONFIG_FILE, CONFIG_SECTIONS['policy'], option, 'boolean', desc, '',
+ 0, 0, default, default, 0, '', '', '', '', '', '')
+
+ weechat.config_read(CONFIG_FILE)
+
+def config_reload_cb(data, config_file):
+ """/reload callback to reload config from file."""
+ free_all_config()
+ init_config()
+
+ return weechat.WEECHAT_CONFIG_READ_OK
+
+def free_all_config():
+ """Free all config options, sections and config file."""
+ for section in CONFIG_SECTIONS.values():
+ weechat.config_section_free_options(section)
+ weechat.config_section_free(section)
+
+ weechat.config_free(CONFIG_FILE)
+
+def create_dir():
+ """Create the OTR subdirectory in the WeeChat config directory if it does
+ not exist."""
+ if not os.path.exists(OTR_DIR):
+ weechat.mkdir_home(OTR_DIR_NAME, 0o700)
+
+def git_info():
+ """If this script is part of a git repository return the repo state."""
+ result = None
+ script_dir = os.path.dirname(os.path.realpath(__file__))
+ git_dir = os.path.join(script_dir, '.git')
+ if os.path.isdir(git_dir):
+ import subprocess
+ try:
+ result = PYVER.to_unicode(subprocess.check_output([
+ 'git',
+ '--git-dir', git_dir,
+ '--work-tree', script_dir,
+ 'describe', '--dirty', '--always',
+ ])).lstrip('v').rstrip()
+ except (OSError, subprocess.CalledProcessError):
+ pass
+
+ return result
+
+def weechat_version_ok():
+ """Check if the WeeChat version is compatible with this script.
+
+ If WeeChat version < 0.4.2 log an error to the core buffer and return
+ False. Otherwise return True.
+ """
+ weechat_version = weechat.info_get('version_number', '') or 0
+ if int(weechat_version) < 0x00040200:
+ error_message = (
+ '{script_name} requires WeeChat version >= 0.4.2. The current '
+ 'version is {current_version}.').format(
+ script_name=SCRIPT_NAME,
+ current_version=weechat.info_get('version', ''))
+ prnt('', error_message)
+ return False
+ else:
+ return True
+
+SCRIPT_VERSION = git_info() or SCRIPT_VERSION
+
+def dependency_versions():
+ """Return a string containing the versions of all dependencies."""
+ return ('weechat-otr {script_version}, '
+ 'potr {potr_major}.{potr_minor}.{potr_patch}-{potr_sub}, '
+ 'Python {python_version}, '
+ 'WeeChat {weechat_version}'
+ ).format(
+ script_version=SCRIPT_VERSION,
+ potr_major=potr.VERSION[0],
+ potr_minor=potr.VERSION[1],
+ potr_patch=potr.VERSION[2],
+ potr_sub=potr.VERSION[3],
+ python_version=platform.python_version(),
+ weechat_version=weechat.info_get('version', ''))
+
+def excepthook(typ, value, traceback):
+ sys.stderr.write('Versions: ')
+ sys.stderr.write(dependency_versions())
+ sys.stderr.write('\n')
+
+ sys.__excepthook__(typ, value, traceback)
+
+sys.excepthook = excepthook
+
+if weechat.register(
+ SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENCE, SCRIPT_DESC,
+ 'shutdown', ''):
+ if weechat_version_ok():
+ init_config()
+
+ OTR_DIR = os.path.join(info_get('weechat_dir', ''), OTR_DIR_NAME)
+ create_dir()
+
+ ACCOUNTS = AccountDict()
+
+ weechat.hook_modifier('irc_in_privmsg', 'message_in_cb', '')
+ weechat.hook_modifier('irc_out_privmsg', 'message_out_cb', '')
+
+ weechat.hook_command(
+ SCRIPT_NAME, SCRIPT_HELP,
+ 'start [NICK SERVER] || '
+ 'refresh [NICK SERVER] || '
+ 'finish [NICK SERVER] || '
+ 'end [NICK SERVER] || '
+ 'status [NICK SERVER] || '
+ 'smp ask [NICK SERVER] [QUESTION] SECRET || '
+ 'smp respond [NICK SERVER] SECRET || '
+ 'smp abort [NICK SERVER] || '
+ 'trust [NICK SERVER] || '
+ 'distrust [NICK SERVER] || '
+ 'log [on|off] || '
+ 'policy [POLICY on|off] || '
+ 'fingerprint [SEARCH|all]',
+ '',
+ 'start %(nick) %(irc_servers) %-||'
+ 'refresh %(nick) %(irc_servers) %-||'
+ 'finish %(nick) %(irc_servers) %-||'
+ 'end %(nick) %(irc_servers) %-||'
+ 'status %(nick) %(irc_servers) %-||'
+ 'smp ask|respond %(nick) %(irc_servers) %-||'
+ 'smp abort %(nick) %(irc_servers) %-||'
+ 'trust %(nick) %(irc_servers) %-||'
+ 'distrust %(nick) %(irc_servers) %-||'
+ 'log on|off %-||'
+ 'policy %(otr_policy) on|off %-||'
+ 'fingerprint all %-||',
+ 'command_cb',
+ '')
+
+ weechat.hook_completion(
+ 'otr_policy', 'OTR policies', 'policy_completion_cb', '')
+
+ weechat.hook_config('logger.level.irc.*', 'logger_level_update_cb', '')
+
+ weechat.hook_signal('buffer_switch', 'buffer_switch_cb', '')
+ weechat.hook_signal('buffer_closing', 'buffer_closing_cb', '')
+
+ OTR_STATUSBAR = weechat.bar_item_new(
+ SCRIPT_NAME, 'otr_statusbar_cb', '')
+ weechat.bar_item_update(SCRIPT_NAME)
diff --git a/weechat/python/pyrnotify.py b/weechat/python/pyrnotify.py
new file mode 100644
index 0000000..5d24f92
--- /dev/null
+++ b/weechat/python/pyrnotify.py
@@ -0,0 +1,158 @@
+# -*- coding: utf-8 -*-
+# ex:sw=4 ts=4:ai:
+#
+# Copyright (c) 2012 by Krister Svanlund <krister.svanlund@gmail.com>
+# based on tcl version:
+# Remote Notification Script v1.1
+# by Gotisch <gotisch@gmail.com>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+# Example usage when Weechat is running on a remote PC and you want
+# want to use port 4321 for the connection.
+#
+# On the "client" (where the notifications will end up), host is
+# the remote host where weechat is running:
+# python2 location/of/pyrnotify.py 4321 & ssh -R 4321:localhost:4321 username@host
+# You can have a second argument to specified the time to display the notification
+# python2 location/of/pyrnotify.py 4321 2000 & ssh -R 4321:localhost:4321 username@host
+# Important to remember is that you should probably setup the
+# connection with public key encryption and use something like
+# autossh to do this in the background.
+#
+# In weechat:
+# /python load pyrnotify.py
+# and set the port
+# /set plugins.var.python.pyrnotify.port 4321
+#
+# It is also possible to set which host pyrnotify shall connect to,
+# this is not recommended. Using a ssh port-forward is much safer
+# and doesn't require any ports but ssh to be open.
+
+# ChangeLog:
+#
+# 2014-05-10: Change hook_print callback argument type of displayed/highlight
+# (WeeChat >= 1.0)
+# 2012-06-19: Added simple escaping to the title and body strings for
+# the script to handle trailing backslashes.
+
+try:
+ import weechat as w
+ in_weechat = True
+except ImportError as e:
+ in_weechat = False
+
+import os, sys, re
+import socket
+import subprocess
+import shlex
+
+SCRIPT_NAME = "pyrnotify"
+SCRIPT_AUTHOR = "Krister Svanlund <krister.svanlund@gmail.com>"
+SCRIPT_VERSION = "1.0"
+SCRIPT_LICENSE = "GPL3"
+SCRIPT_DESC = "Send remote notifications over SSH"
+
+def escape(s):
+ return re.sub(r'([\\"\'])', r'\\\1', s)
+
+def run_notify(icon, nick,chan,message):
+ host = w.config_get_plugin('host')
+ try:
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.connect((host, int(w.config_get_plugin('port'))))
+ s.send("normal %s \"%s to %s\" \"%s\"" % (icon, nick, escape(chan), escape(message)))
+ s.close()
+ except Exception as e:
+ w.prnt("", "Could not send notification: %s" % str(e))
+
+def on_msg(*a):
+ if len(a) == 8:
+ data, buffer, timestamp, tags, displayed, highlight, sender, message = a
+ if data == "private" or int(highlight):
+ if data == "private" and w.config_get_plugin('pm-icon'):
+ icon = w.config_get_plugin('pm-icon')
+ else:
+ icon = w.config_get_plugin('icon')
+ buffer = "me" if data == "private" else w.buffer_get_string(buffer, "short_name")
+ run_notify(icon, sender, buffer, message)
+ #w.prnt("", str(a))
+ return w.WEECHAT_RC_OK
+
+def weechat_script():
+ settings = {'host' : "localhost",
+ 'port' : "4321",
+ 'icon' : "utilities-terminal",
+ 'pm-icon' : "emblem-favorite"}
+ if w.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, "", ""):
+ for (kw, v) in settings.items():
+ if not w.config_get_plugin(kw):
+ w.config_set_plugin(kw, v)
+ w.hook_print("", "notify_message", "", 1, "on_msg", "")
+ w.hook_print("", "notify_private", "", 1, "on_msg", "private")
+ w.hook_print("", "notify_highlight", "", 1, "on_msg", "") # Not sure if this is needed
+
+
+
+
+
+
+######################################
+## This is where the client starts, except for the global if-check nothing below this line is
+## supposed to be executed in weechat, instead it runs when the script is executed from
+## commandline.
+
+def accept_connections(s, timeout=None):
+ conn, addr = s.accept()
+ try:
+ data = ""
+ d = conn.recv(1024)
+ while d:
+ data += d
+ d = conn.recv(1024)
+ finally:
+ conn.close()
+ if data:
+ try:
+ urgency, icon, title, body = shlex.split(data)
+ if timeout:
+ subprocess.call(["notify-send", "-t", timeout, "-u", urgency, "-c", "IRC", "-i", icon, escape(title), escape(body)])
+ else:
+ subprocess.call(["notify-send", "-u", urgency, "-c", "IRC", "-i", icon, escape(title), escape(body)])
+
+ except ValueError as e:
+ print e
+ except OSError as e:
+ print e
+ accept_connections(s, timeout)
+
+def weechat_client(argv):
+ s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind(("localhost", int(argv[1] if len(sys.argv) > 1 else 4321)))
+ s.listen(5)
+ try:
+ accept_connections(s, argv[2] if len(sys.argv) > 2 else None)
+ except KeyboardInterrupt as e:
+ print "Keyboard interrupt"
+ print e
+ finally:
+ s.close()
+
+if __name__ == '__main__':
+ if in_weechat:
+ weechat_script()
+ else:
+ weechat_client(sys.argv)
diff --git a/weechat/python/queryman.py b/weechat/python/queryman.py
new file mode 100644
index 0000000..c979f93
--- /dev/null
+++ b/weechat/python/queryman.py
@@ -0,0 +1,130 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (c) 2013-2016 by nils_2 <weechatter@arcor.de>
+#
+# save and restore query buffers after /quit
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+# idea by lasers@freenode.#weechat
+#
+# 2016-02-27: nils_2, (freenode.#weechat)
+# 0.3 : make script consistent with "buffer_switch_autojoin" option (idea haasn)
+#
+# 2013-11-07: nils_2, (freenode.#weechat)
+# 0.2 : fix file not found error (reported by calcifea)
+# : make script compatible with Python 3.x
+#
+# 2013-07-26: nils_2, (freenode.#weechat)
+# 0.1 : initial release
+#
+# script will create a config file (~./weechat/queryman.txt)
+# format: "servername nickname" (without "")
+#
+# Development is currently hosted at
+# https://github.com/weechatter/weechat-scripts
+
+try:
+ import weechat,re,os
+
+except Exception:
+ print("This script must be run under WeeChat.")
+ print("Get WeeChat now at: http://www.weechat.org/")
+ quit()
+
+SCRIPT_NAME = 'queryman'
+SCRIPT_AUTHOR = 'nils_2 <weechatter@arcor.de>'
+SCRIPT_VERSION = '0.3'
+SCRIPT_LICENSE = 'GPL'
+SCRIPT_DESC = 'save and restore query buffers after /quit'
+
+query_buffer_list = []
+queryman_filename = 'queryman.txt'
+
+# ================================[ callback ]===============================
+def quit_signal_cb(data, signal, signal_data):
+ save_query_buffer_to_file()
+ return weechat.WEECHAT_RC_OK
+
+# signal_data contains servername
+def irc_server_connected_signal_cb(data, signal, signal_data):
+ load_query_buffer_irc_server_opened(signal_data)
+ return weechat.WEECHAT_RC_OK
+
+# ================================[ file ]===============================
+def get_filename_with_path():
+ global queryman_filename
+ path = weechat.info_get("weechat_dir", "")
+ return os.path.join(path,queryman_filename)
+
+def load_query_buffer_irc_server_opened(server_connected):
+ global query_buffer_list
+
+ filename = get_filename_with_path()
+
+ if os.path.isfile(filename):
+ f = open(filename, 'rb')
+ for line in f:
+ servername,nick = line.split(' ')
+ if servername == server_connected:
+ noswitch = ""
+ switch_autojoin = weechat.config_get("irc.look.buffer_switch_autojoin")
+ if not weechat.config_boolean(switch_autojoin):
+ noswitch = "-noswitch"
+ weechat.command('','/query %s -server %s %s' % ( noswitch, servername, nick ))
+ f.close()
+ else:
+ weechat.prnt('','%s%s: Error loading query buffer from "%s"' % (weechat.prefix('error'), SCRIPT_NAME, filename))
+
+def save_query_buffer_to_file():
+ global query_buffer_list
+
+ ptr_infolist_buffer = weechat.infolist_get('buffer', '', '')
+
+ while weechat.infolist_next(ptr_infolist_buffer):
+ ptr_buffer = weechat.infolist_pointer(ptr_infolist_buffer,'pointer')
+
+ type = weechat.buffer_get_string(ptr_buffer, 'localvar_type')
+ if type == 'private':
+ server = weechat.buffer_get_string(ptr_buffer, 'localvar_server')
+ channel = weechat.buffer_get_string(ptr_buffer, 'localvar_channel')
+ query_buffer_list.insert(0,"%s %s" % (server,channel))
+
+ weechat.infolist_free(ptr_infolist_buffer)
+
+ filename = get_filename_with_path()
+
+ if len(query_buffer_list):
+ try:
+ f = open(filename, 'w')
+ i = 0
+ while i < len(query_buffer_list):
+ f.write('%s\n' % query_buffer_list[i])
+ i = i + 1
+ f.close()
+ except:
+ weechat.prnt('','%s%s: Error writing query buffer to "%s"' % (weechat.prefix('error'), SCRIPT_NAME, filename))
+ raise
+ else: # no query buffer(s). remove file
+ if os.path.isfile(filename):
+ os.remove(filename)
+ return
+
+# ================================[ main ]===============================
+if __name__ == '__main__':
+ if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE, SCRIPT_DESC, '', ''):
+ version = weechat.info_get('version_number', '') or 0
+ if int(version) >= 0x00030700:
+ weechat.hook_signal('quit', 'quit_signal_cb', '')
+ weechat.hook_signal('irc_server_connected', 'irc_server_connected_signal_cb', '')
diff --git a/weechat/python/sshnotify.py b/weechat/python/sshnotify.py
new file mode 100755
index 0000000..64bdcdf
--- /dev/null
+++ b/weechat/python/sshnotify.py
@@ -0,0 +1,308 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 2011 delwin <delwin@skyehaven.net>
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+#based on the 'lnotify' script for weechat, which was based on 'notify'
+#
+#the main reason to use this script would be if you use tmux or screen
+#with weechat and would like to receive desktop notifications on multiple
+#computers (regardless of whether or not you are connected to weechat on
+#any of those systems.) however, there is a reason to use this if you
+#only use one system for weechat. if you restart X and then reconnect
+#to weechat, notifications will no longer work because of dbus errors.
+#this solution solves that problem at the expense of some extra setup.
+#
+#IMPORTANT:
+#use of this script assumes that you have:
+#1) /usr/bin/notify-send
+#2) ssh configured to use key identification (including localhost!)
+#
+#ALSO IMPORTANT:
+#see comments below in the default settings section for description of options
+#and tips. pay particular attention to the addresses section if you need to
+#specify non-default ports or non-default display settings
+#
+#NOT so important:
+#if you define icons to show up in the notifications, this script will expect
+#to find them in the same place on all systems. missing icons will not stop the
+#notification from displaying, however. (this also applies for any extra
+#commands you add to be executed after notify-send. sound files and utilities
+#have to exist on the machine receiving the command in order to work)
+#
+#
+#changelog:
+#v0.2.3 - Sébastien Helleu <flashcode@flashtux.org>
+# change hook_print callback argument type of displayed/highlight
+# (WeeChat >= 1.0)
+#v0.2.2 - <ldvx@freenode> fixed bug in (1) which didn't allow user to get
+# notifications on private messages if irc.look.nick_prefix or
+# irc.look.nick_suffix were used. (2012-04-27)
+#v0.2.1 - added help messages and hint for empty addresses option (2011-10-15)
+#v0.2.0 - added several options, including: proper weechat options
+# for multiple addresses, ignore strings, urgencies, images,
+# and extra commands to be sent along with notifications.
+# shifted handling of DISPLAY to the address portion of the command.
+# submitted to weechat scripts. (2011-10-14)
+#v0.1.0 - converted lnotify to work via ssh. accepts multiple addresses
+# hardcoded into the script. (2011-09-18)
+#
+#TODO... if i get bored enough/anyone actually requests these things:
+# add notification whitelist, only send notifications matching whitelist
+# set up proper /sshnotify <args> command to manually send notifications
+# set up new definitions for messages so that extra_commands can use them?
+# toggle to suppress notifications when away
+# toggle to show server and channel names in notifications
+# toggle to show notifications for dcc?
+#
+import weechat, string, subprocess, re
+
+weechat.register("sshnotify", "delwin", "0.2.3", "GPL3", "the overkill desktop notification solution", "", "")
+
+#options which can be defined with /set plugins.var.python.sshnotify.foo
+settings = {
+ "show_highlight" : "on", #trigger on highlighted nick/word
+ "show_priv_msg" : "on", #trigger on private message
+ "ignore" : "", #comma seperated list of strings to ignore,
+ #e.g. if using bitlbee, you might want to add
+ #'@root: jabber,@root: otr' to avoid spam on startup
+ "pm-urgency" : "critical", #notification urgency for private messages. valid values are: low,normal,critical
+ "pm-image" : "", #location of notification icon for private messages, optional
+ "mention-urgency" : "normal", #notification urgency for highlighted strings, valid values are: low,normal,critical
+ "mention-image" : "", #location of notification icon for highlighted strings, optional
+ "extra_commands" : "", #extra commands to pass via ssh, appeneded after notify command.
+ #note: you need to include && to execute the commands sequentially
+ #for example, in order to have espeak announce that you have a new message and then play an audio file:
+ #/set plugins.var.python.sshnotify.extra_commands && espeak 'hey, you have a new message' && aplay whatever.wav'
+ "addresses" : "localhost", #comma delimited lists of addresses (and options) to use via ssh
+ #default is localhost. if no address is specified, this script won't do anything for you
+ #if you set addresses to "", it will reset to localhost the next time it loads.
+ #if you need to specify a different display, add DISPLAY=:1 (or the appropriate number)
+ #by default, sshnotify will use DISPLAY=:0, so there is no need to specify that in your address string
+ #other ssh options can be added to each address:
+ #/set plugins.var.python.sshnotify.addresses localhost,foo@bar,-p 1234 bar@foo DISPLAY=:1
+ #will send a notification to 3 places, you@localhost, foot@bar, and bar@foo on port 1234 on display 1
+
+}
+
+# Init everything
+for option, default_value in settings.items():
+ if weechat.config_get_plugin(option) == "":
+ weechat.config_set_plugin(option, default_value)
+
+# Hook privmsg/hilights
+weechat.hook_print("", "irc_privmsg", "", 1, "get_notified", "")
+
+# Functions
+
+def get_addresses():
+ addies = weechat.config_get_plugin('addresses')
+ if addies == '':
+ weechat.prnt("","You need to specify a destination if you want notifications sent. ")
+ weechat.prnt("","hint: /set plugins.var.python.sshnotify.addresses localhost")
+ return []
+ else:
+ return addies.split(',')
+
+#if there are any strings
+def get_ignore():
+ ignores = weechat.config_get_plugin('ignore')
+ if ignores == '':
+ return []
+ else:
+ return ignores.split(',')
+
+#notification routine
+def get_notified(data, bufferp, uber_empty, tagsn, isdisplayed,
+ ishilight, prefix, message):
+
+ #if message contains and ignored string, don't send the notification
+ ilist = get_ignore()
+ for i in ilist:
+ if re.search(i,prefix + ": " + message):
+ return weechat.WEECHAT_RC_OK
+
+ extracommands = weechat.config_get_plugin('extra_commands')
+ #set a default value for DISPLAY. this should be fine for almost everyone
+ dispnum = "DISPLAY=:0"
+
+ #if the message came in via private message...
+ if (weechat.buffer_get_string(bufferp, "localvar_type") == "private" and
+ weechat.config_get_plugin('show_priv_msg') == "on"):
+ buffer = (weechat.buffer_get_string(bufferp, "short_name") or
+ weechat.buffer_get_string(bufferp, "name"))
+
+ #set notification image
+ if weechat.config_get_plugin('pm-image') != "":
+ imagestring = "--icon=" + str(weechat.config_get_plugin('pm-image')) + " "
+ else:
+ imagestring = ""
+
+ #set notification urgency
+ if weechat.config_get_plugin('pm-urgency') == "low":
+ urgencystring = "--urgency=low "
+ elif weechat.config_get_plugin('pm-urgency') == "critical":
+ urgencystring = "--urgency=critical "
+ else:
+ urgencystring = "--urgency=normal "
+
+ uistring = urgencystring + imagestring
+
+ # (1) if buffer == prefix was used here, pressumibly to avoid notifications when on own
+ # messages, checking for the tag notify_private has the same effect, and the user can
+ # set irc.look.nick_suffix or irc.look.nick_prefix this way.
+ if "notify_private" in tagsn.split(","):
+ #the ' character currently needs changed to something else or the message formatting fails
+ #substituting " for '
+ #notification title
+ prefix = re.sub("'",'"',prefix)
+ #escaping all special characters so that the message formatting doesn't fail
+ prefix = re.escape(prefix)
+ #notification message
+ message = re.sub("'",'"',message)
+ message = re.escape(message)
+ #setting the command which will be passed by ssh to push the notification
+ #note the DISPLAY variable is now accounted for in the ssh part of the command rather than this location
+ disp = '"/usr/bin/notify-send ' + uistring + '\'In PM\' \'' + prefix + ': ' + message + '\' ' + extracommands + '\"'
+
+ #fire when ready
+ alist = get_addresses()
+ for a in alist:
+ if a != '':
+ #first check to see if DISPLAY is set in an address listing
+ if re.search('DISPLAY\=',a):
+ #if yes, do not set the default value defined above
+ dispnum = ""
+ #generate the ssh portion of the command to send
+ com = "ssh -X " + a + " " + dispnum + " "
+ #add all the bits together and send the notification. time out if it can't connect in 5 seconds
+ weechat.hook_process(com + disp, 5000, "", "")
+ #just a debug message
+ #print(com + disp)
+
+
+ #if the message comes from a highlight rather than private message
+ elif (int(ishilight) and
+ weechat.config_get_plugin('show_highlight') == "on"):
+ buffer = (weechat.buffer_get_string(bufferp, "short_name") or
+ weechat.buffer_get_string(bufferp, "name"))
+ #convert ' to " and escape special characters so the ssh command doesn't puke
+ buffer = re.sub("'",'"',buffer)
+ buffer = re.escape(buffer)
+ #notification title
+ prefix = re.sub("'",'"',prefix)
+ prefix = re.escape(prefix)
+ #notification message
+ message = re.sub("'",'"',message)
+ message = re.escape(message)
+
+ #set notification image
+ if weechat.config_get_plugin('mention-image') != "":
+ imagestring = "--icon=" + weechat.config_get_plugin('mention-image') + " "
+ else:
+ imagestring = ""
+
+ #set notification urgency
+ if weechat.config_get_plugin('mention-urgency') == "low":
+ urgencystring = "--urgency=low "
+ elif weechat.config_get_plugin('mention-urgency') == "critical":
+ urgencystring = "--urgency=critical "
+ else:
+ urgencystring = "--urgency=normal "
+
+ uistring = urgencystring + imagestring
+ #adding the notify-send command bits together
+ disp = '"/usr/bin/notify-send ' + uistring + '\'In ' + buffer + '\' \'' + prefix + ': ' + message + '\'' + extracommands + '\"'
+
+ #decide where to send the notification
+ alist = get_addresses()
+ for a in alist:
+ if a != '':
+ #if display is set in the address, do not apply default value defined above
+ if re.search('DISPLAY\=',a):
+ dispnum = ""
+ #adding the ssh command bits together
+ com = "ssh -X " + a + " " + dispnum + " "
+ #add all the bits together and send the notification. timeout in 5 seconds
+ weechat.hook_process(com + disp, 5000, "", "")
+ #just a debug message
+ #print(com + disp)
+
+
+ return weechat.WEECHAT_RC_OK
+
+#right now this is just to enable /help sshnotify
+#might be a better way to do this but i might extend it
+#to allow /sshnotify to send notications directly with
+#arguments as the messages
+def notifying(data,buffer,args):
+ weechat.prnt("","the command /sshnotify won't do much for you...yet. see /help sshnotify for info on how to use this plugin")
+ return weechat.WEECHAT_RC_OK
+
+#the help message from /help sshnotify
+hook = weechat.hook_command(
+ "sshnotify","overkill desktop notification","",
+"""
+ This script allows you to send desktop
+ notifications of private messages and
+ highlighted strings to one or more
+ computers via ssh. This script expects
+ ssh key identification to be set up and
+ /usr/bin/notify-send on the systems to
+ which you are sending notifications.
+
+ It is quite configurable, allowing you
+ to specify any ssh options you might find
+ necessary (e.g. custom ports and
+ non-default DISPLAY settings.) It allows
+ you to specify strings to be ignored to
+ help cut down on notification spam. Both
+ of these settings accept comma delimited lists.
+
+ Examples:
+ for multiple ssh connections:
+ /set plugins.var.python.sshnotify.addresses
+ localhost,foo@bar,-p 1234 bar@foo DISPLAY=:1
+ (sends notifications to you@localhost, foo@bar,
+ and bar@foo on port 1234 at DISPLAY 1)
+
+ for multiple ignore values:
+ /set plugins.var.python.sshnotify.ignore
+ @root: jabber,@root: otr,ignore this string
+ (I use these values to eliminate notification
+ spam from bitlbee when I connect)
+
+ It also allows you to chain extra commands
+ to be executed after the notify-send command,
+ like so:
+ /set plugins.var.python.sshnotify.extra_commands
+ && espeak 'you have a new message && aplay somerandom.wav
+ (note: this is not comma delimited and expects '&&'
+ in front of any command you wish to execute,
+ including the first one.)
+
+ You can also set the urgency level and icons
+ used for private messages or highlighted
+ strings (the 'mention' configure options.)
+ Valid urgency levels are 'low','normal', and
+ 'critical'. Icons need to be on the system
+ receiving the notification in order to display,
+ but if they are missing it does not interfere
+ with the functionality of this script.
+
+ I should also note that apostrophes in messages
+ will be converted to double quotes in your
+ notifications due to formatting issues with
+ the ssh/notify-send command.
+ """
+ ,"",'notifying','')
diff --git a/weechat/python/theme.py b/weechat/python/theme.py
new file mode 100755
index 0000000..fcfe76d
--- /dev/null
+++ b/weechat/python/theme.py
@@ -0,0 +1,1282 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2011-2015 Sébastien Helleu <flashcode@flashtux.org>
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+#
+
+#
+# WeeChat theme manager.
+# (this script requires WeeChat 0.3.7 or newer)
+#
+# History:
+#
+# 2015-07-13, Sébastien Helleu <flashcode@flashtux.org>:
+# version 0.1: dev snapshot
+# 2011-02-22, Sébastien Helleu <flashcode@flashtux.org>:
+# start dev
+#
+
+SCRIPT_NAME = 'theme'
+SCRIPT_AUTHOR = 'Sébastien Helleu <flashcode@flashtux.org>'
+SCRIPT_VERSION = '0.1-dev'
+SCRIPT_LICENSE = 'GPL3'
+SCRIPT_DESC = 'WeeChat theme manager'
+
+SCRIPT_COMMAND = 'theme'
+
+import_weechat_ok = True
+import_other_ok = True
+
+try:
+ import weechat
+except ImportError:
+ import_weechat_ok = False
+
+try:
+ import sys
+ import os
+ import re
+ import datetime
+ import time
+ import cgi
+ import tarfile
+ import traceback
+ import xml.dom.minidom
+except ImportError as e:
+ print('Missing package(s) for {0}: {1}'.format(SCRIPT_NAME, e))
+ import_other_ok = False
+
+THEME_CONFIG_FILENAME = 'theme'
+
+THEME_URL = 'https://weechat.org/files/themes.tar.bz2'
+
+# color attributes (in weechat color options)
+COLOR_ATTRIBUTES = ('*', '_', '/', '!')
+
+# timeout for download of themes.tar.bz2
+TIMEOUT_UPDATE = 120 * 1000
+
+# hook process and stdout
+theme_hook_process = ''
+theme_stdout = ''
+
+# themes read from themes.xml
+theme_themes = {}
+
+# config file and options
+theme_cfg_file = ''
+theme_cfg = {}
+
+theme_bars = 'input|nicklist|status|title'
+theme_plugins = 'weechat|alias|aspell|charset|fifo|irc|logger|relay|'\
+ 'rmodifier|xfer'
+
+theme_options_include_re = (
+ r'^weechat\.bar\.({0})\.color.*'.format(theme_bars),
+ r'^weechat\.look\.buffer_time_format$',
+ r'^({0})\.color\..*'.format(theme_plugins),
+ r'^({0})\.look\..*color.*'.format(theme_plugins),
+)
+
+theme_options_exclude_re = (
+ r'^weechat.look.color_pairs_auto_reset$',
+ r'^weechat.look.color_real_white$',
+ r'^weechat.look.color_basic_force_bold$',
+ r'^irc\.look\.',
+)
+
+
+# =================================[ config ]=================================
+
+
+def theme_config_init():
+ """Initialization of configuration file. Sections: color, themes."""
+ global theme_cfg_file, theme_cfg
+ theme_cfg_file = weechat.config_new(THEME_CONFIG_FILENAME,
+ 'theme_config_reload_cb', '')
+
+ # section "color"
+ section = weechat.config_new_section(
+ theme_cfg_file, 'color', 0, 0,
+ '', '', '', '', '', '', '', '', '', '')
+ theme_cfg['color_script'] = weechat.config_new_option(
+ theme_cfg_file, section, 'script', 'color',
+ 'Color for script names', '', 0, 0,
+ 'cyan', 'cyan', 0, '', '', '', '', '', '')
+ theme_cfg['color_installed'] = weechat.config_new_option(
+ theme_cfg_file, section, 'installed', 'color',
+ 'Color for "installed" indicator', '', 0, 0,
+ 'yellow', 'yellow', 0, '', '', '', '', '', '')
+ theme_cfg['color_running'] = weechat.config_new_option(
+ theme_cfg_file, section, 'running', 'color',
+ 'Color for "running" indicator', '', 0, 0,
+ 'lightgreen', 'lightgreen', 0, '', '', '', '', '', '')
+ theme_cfg['color_obsolete'] = weechat.config_new_option(
+ theme_cfg_file, section, 'obsolete', 'color',
+ 'Color for "obsolete" indicator', '', 0, 0,
+ 'lightmagenta', 'lightmagenta', 0, '', '', '', '', '', '')
+ theme_cfg['color_unknown'] = weechat.config_new_option(
+ theme_cfg_file, section, 'unknown', 'color',
+ 'Color for "unknown status" indicator', '', 0, 0,
+ 'lightred', 'lightred', 0, '', '', '', '', '', '')
+ theme_cfg['color_language'] = weechat.config_new_option(
+ theme_cfg_file, section, 'language', 'color',
+ 'Color for language names', '', 0, 0,
+ 'lightblue', 'lightblue', 0, '', '', '', '', '', '')
+
+ # section "themes"
+ section = weechat.config_new_section(
+ theme_cfg_file, 'themes', 0, 0,
+ '', '', '', '', '', '', '', '', '', '')
+ theme_cfg['themes_url'] = weechat.config_new_option(
+ theme_cfg_file, section,
+ 'url', 'string', 'URL for file with themes (.tar.bz2 file)', '', 0, 0,
+ THEME_URL, THEME_URL, 0, '', '', '', '', '', '')
+ theme_cfg['themes_cache_expire'] = weechat.config_new_option(
+ theme_cfg_file, section,
+ 'cache_expire', 'integer', 'Local cache expiration time, in minutes '
+ '(-1 = never expires, 0 = always expires)', '',
+ -1, 60 * 24 * 365, '60', '60', 0, '', '', '', '', '', '')
+ theme_cfg['themes_dir'] = weechat.config_new_option(
+ theme_cfg_file, section,
+ 'dir', 'string', 'Local directory for themes', '', 0, 0,
+ '%h/themes', '%h/themes', 0, '', '', '', '', '', '')
+
+
+def theme_config_reload_cb(data, config_file):
+ """Reload configuration file."""
+ return weechat.config_read(config_file)
+
+
+def theme_config_read():
+ """Read configuration file."""
+ return weechat.config_read(theme_cfg_file)
+
+
+def theme_config_write():
+ """Write configuration file."""
+ return weechat.config_write(theme_cfg_file)
+
+
+def theme_config_color(color):
+ """Get a color from configuration."""
+ option = theme_cfg.get('color_' + color, '')
+ if not option:
+ return ''
+ return weechat.color(weechat.config_string(option))
+
+
+def theme_config_get_dir():
+ """Return themes directory, with expanded WeeChat home dir."""
+ return weechat.config_string(
+ theme_cfg['themes_dir']).replace('%h',
+ weechat.info_get('weechat_dir', ''))
+
+
+def theme_config_get_backup():
+ """
+ Return name of backup theme
+ (by default "~/.weechat/themes/_backup.theme").
+ """
+ return theme_config_get_dir() + '/_backup.theme'
+
+
+def theme_config_get_undo():
+ """
+ Return name of undo file
+ (by default "~/.weechat/themes/_undo.theme").
+ """
+ return theme_config_get_dir() + '/_undo.theme'
+
+
+def theme_config_create_dir():
+ """Create "themes" directory."""
+ directory = theme_config_get_dir()
+ if not os.path.isdir(directory):
+ os.makedirs(directory, mode=0o700)
+
+
+def theme_config_get_tarball_filename():
+ """Get local tarball filename, based on URL."""
+ return theme_config_get_dir() + '/' + \
+ os.path.basename(weechat.config_string(theme_cfg['themes_url']))
+
+
+def theme_config_get_xml_filename():
+ """Get XML filename."""
+ return theme_config_get_dir() + '/themes.xml'
+
+
+# =================================[ themes ]=================================
+
+
+class Theme:
+
+ def __init__(self, filename=None):
+ self.filename = filename
+ self.props = {}
+ self.listprops = []
+ self.options = {}
+ self.theme_ok = True
+ if self.filename:
+ self.theme_ok = self.load(self.filename)
+ else:
+ self.init_weechat()
+ self.nick_prefixes = self._get_nick_prefixes()
+
+ def isok(self):
+ return self.theme_ok
+
+ def _option_is_used(self, option):
+ global theme_options_include_re, theme_options_exclude_re
+ for regex in theme_options_exclude_re:
+ if re.search(regex, option):
+ return False
+ for regex in theme_options_include_re:
+ if re.search(regex, option):
+ return True
+ return False
+
+ def _get_nick_prefixes(self):
+ """Get dict with nick prefixes."""
+ prefixes = {}
+ nick_prefixes = self.options.get('irc.color.nick_prefixes', '')
+ for prefix in nick_prefixes.split(';'):
+ values = prefix.split(':', 1)
+ if len(values) == 2:
+ prefixes[values[0]] = values[1]
+ return prefixes
+
+ def _get_attr_color(self, color):
+ """Return tuple with attributes and color."""
+ m = re.match('([*_!]*)(.*)', color)
+ if m:
+ return m.group(1), m.group(2)
+ return '', color
+
+ def _get_color_without_alias(self, color):
+ """
+ Return color without alias (color can be "fg", "fg,bg" or "fg:bg").
+ """
+ pos = color.find(',')
+ if pos < 0:
+ pos = color.find(':')
+ if pos > 0:
+ fg = color[0:pos]
+ bg = color[pos + 1:]
+ else:
+ fg = color
+ bg = ''
+ attr, col = self._get_attr_color(fg)
+ fg = attr + self.palette.get(col, col)
+ attr, col = self._get_attr_color(bg)
+ bg = attr + self.palette.get(col, col)
+ if bg:
+ return fg + color[pos:pos + 1] + bg
+ return fg
+
+ def _replace_color_alias(self, match):
+ value = match.group()[8:-1]
+ if value in self.palette:
+ value = self.palette[value]
+ return '${color:' + value + '}'
+
+ def init_weechat(self):
+ """
+ Initialize theme using current WeeChat options (aliases are
+ replaced with their values from palette).
+ """
+ # get palette options
+ self.palette = {}
+ infolist = weechat.infolist_get('option', '', 'weechat.palette.*')
+ while weechat.infolist_next(infolist):
+ option_name = weechat.infolist_string(infolist, 'option_name')
+ value = weechat.infolist_string(infolist, 'value')
+ self.palette[value] = option_name
+ weechat.infolist_free(infolist)
+ # get color options (replace aliases by values from palette)
+ self.options = {}
+ infolist = weechat.infolist_get('option', '', '')
+ while weechat.infolist_next(infolist):
+ full_name = weechat.infolist_string(infolist, 'full_name')
+ if self._option_is_used(full_name):
+ value = weechat.infolist_string(infolist, 'value')
+ self.options[full_name] = self._get_color_without_alias(value)
+ weechat.infolist_free(infolist)
+ # replace aliases in chat_nick_colors
+ option = 'weechat.color.chat_nick_colors'
+ colors = []
+ for color in self.options.get(option, '').split(','):
+ colors.append(self._get_color_without_alias(color))
+ if colors:
+ self.options[option] = ','.join(colors)
+ # replace aliases in buffer_time_format
+ option = 'weechat.look.buffer_time_format'
+ if option in self.options:
+ value = re.compile(r'\$\{color:[^\}]+\}').sub(
+ self._replace_color_alias, self.options[option])
+ if value:
+ self.options[option] = value
+ # build dict with nick prefixes (and replace alisases)
+ prefixes = []
+ option = 'irc.color.nick_prefixes'
+ for prefix in self.options.get(option, '').split(';'):
+ values = prefix.split(':', 1)
+ if len(values) == 2:
+ prefixes.append(values[0] + ':' +
+ self._get_color_without_alias(values[1]))
+ if prefixes:
+ self.options[option] = ';'.join(prefixes)
+ # delete palette
+ del self.palette
+
+ def prnt(self, message):
+ try:
+ weechat.prnt('', message)
+ except:
+ print(message)
+
+ def prnt_error(self, message):
+ try:
+ weechat.prnt('', weechat.prefix('error') + message)
+ except:
+ print(message)
+
+ def load(self, filename):
+ self.options = {}
+ try:
+ lines = open(filename, 'rb').readlines()
+ for line in lines:
+ line = str(line.strip().decode('utf-8'))
+ if line.startswith('#'):
+ m = re.match('^# \\$([A-Za-z]+): (.*)', line)
+ if m:
+ self.props[m.group(1)] = m.group(2)
+ self.listprops.append(m.group(1))
+ else:
+ items = line.split('=', 1)
+ if len(items) == 2:
+ value = items[1].strip()
+ if value.startswith('"') and value.endswith('"'):
+ value = value[1:-1]
+ self.options[items[0].strip()] = value
+ return True
+ except:
+ self.prnt('Error loading theme "{0}"'.format(filename))
+ return False
+
+ def save(self, filename):
+ names = sorted(self.options)
+ try:
+ f = open(filename, 'w')
+ version = weechat.info_get('version', '')
+ pos = version.find('-')
+ if pos > 0:
+ version = version[0:pos]
+ header = ('#',
+ '# -- WeeChat theme --',
+ '# $name: {0}'.format(os.path.basename(filename)),
+ '# $date: {0}'.format(datetime.date.today()),
+ '# $weechat: {0}'.format(version),
+ '# $script: {0}.py {1}'.format(SCRIPT_NAME,
+ SCRIPT_VERSION),
+ '#\n')
+ f.write('\n'.join(header))
+ for option in names:
+ f.write('{0} = "{1}"\n'.format(option, self.options[option]))
+ f.close()
+ self.prnt('Theme saved to "{0}"'.format(filename))
+ except:
+ self.prnt_error('Error writing theme to "{0}"'.format(filename))
+ raise
+
+ def show(self, header):
+ """Display content of theme."""
+ names = sorted(self.options)
+ self.prnt('')
+ self.prnt(header)
+ for name in names:
+ self.prnt(' {0} {1}= {2}{3}'
+ ''.format(name,
+ weechat.color('chat_delimiters'),
+ weechat.color('chat_value'),
+ self.options[name]))
+
+ def info(self, header):
+ """Display info about theme."""
+ self.prnt('')
+ self.prnt(header)
+ for prop in self.listprops:
+ self.prnt(' {0}: {1}{2}'
+ ''.format(prop,
+ weechat.color('chat_value'),
+ self.props[prop]))
+ numerrors = 0
+ for name in self.options:
+ if not weechat.config_get(name):
+ numerrors += 1
+ if numerrors == 0:
+ text = 'all OK'
+ else:
+ text = ('WARNING: {0} option(s) not found in your WeeChat'
+ ''.format(numerrors))
+ self.prnt(' options: {0}{1}{2} ({3})'
+ ''.format(weechat.color('chat_value'),
+ len(self.options),
+ weechat.color('reset'),
+ text))
+
+ def install(self):
+ try:
+ numset = 0
+ numerrors = 0
+ for name in self.options:
+ option = weechat.config_get(name)
+ if option:
+ rc = weechat.config_option_set(option,
+ self.options[name], 1)
+ if rc == weechat.WEECHAT_CONFIG_OPTION_SET_ERROR:
+ self.prnt_error('Error setting option "{0}" to value '
+ '"{1}" (running an old WeeChat?)'
+ ''.format(name, self.options[name]))
+ numerrors += 1
+ else:
+ numset += 1
+ else:
+ self.prnt('Warning: option not found: "{0}" '
+ '(running an old WeeChat?)'.format(name))
+ numerrors += 1
+ errors = ''
+ if numerrors > 0:
+ errors = ', {0} error(s)'.format(numerrors)
+ if self.filename:
+ self.prnt('Theme "{0}" installed ({1} options set{2})'
+ ''.format(self.filename, numset, errors))
+ else:
+ self.prnt('Theme installed ({0} options set{1})'
+ ''.format(numset, errors))
+ except:
+ if self.filename:
+ self.prnt_error('Failed to install theme "{0}"'
+ ''.format(self.filename))
+ else:
+ self.prnt_error('Failed to install theme')
+
+ def nick_prefix_color(self, prefix):
+ """Get color for a nick prefix."""
+ modes = 'qaohv'
+ prefixes = '~&@%+'
+ pos = prefixes.find(prefix)
+ if pos < 0:
+ return ''
+ while pos < len(modes):
+ if modes[pos] in self.nick_prefixes:
+ return self.nick_prefixes[modes[pos]]
+ pos += 1
+ return self.nick_prefixes.get('*', '')
+
+
+# =============================[ themes / html ]==============================
+
+
+class HtmlTheme(Theme):
+
+ def __init__(self, filename=None, chat_width=85, chat_height=25,
+ prefix_width=10, nicklist_width=10):
+ Theme.__init__(self, filename)
+ self.chat_width = chat_width
+ self.chat_height = chat_height
+ self.prefix_width = prefix_width
+ self.nicklist_width = nicklist_width
+
+ def html_color(self, index):
+ """Return HTML color with index in table of 256 colors."""
+ terminal_colors = (
+ '000000cd000000cd00cdcd000000cdcd00cd00cdcde5e5e54d4d4dff0000'
+ '00ff00ffff000000ffff00ff00ffffffffff00000000002a000055000080'
+ '0000aa0000d4002a00002a2a002a55002a80002aaa002ad400550000552a'
+ '0055550055800055aa0055d400800000802a0080550080800080aa0080d4'
+ '00aa0000aa2a00aa5500aa8000aaaa00aad400d40000d42a00d45500d480'
+ '00d4aa00d4d42a00002a002a2a00552a00802a00aa2a00d42a2a002a2a2a'
+ '2a2a552a2a802a2aaa2a2ad42a55002a552a2a55552a55802a55aa2a55d4'
+ '2a80002a802a2a80552a80802a80aa2a80d42aaa002aaa2a2aaa552aaa80'
+ '2aaaaa2aaad42ad4002ad42a2ad4552ad4802ad4aa2ad4d455000055002a'
+ '5500555500805500aa5500d4552a00552a2a552a55552a80552aaa552ad4'
+ '55550055552a5555555555805555aa5555d455800055802a558055558080'
+ '5580aa5580d455aa0055aa2a55aa5555aa8055aaaa55aad455d40055d42a'
+ '55d45555d48055d4aa55d4d480000080002a8000558000808000aa8000d4'
+ '802a00802a2a802a55802a80802aaa802ad480550080552a805555805580'
+ '8055aa8055d480800080802a8080558080808080aa8080d480aa0080aa2a'
+ '80aa5580aa8080aaaa80aad480d40080d42a80d45580d48080d4aa80d4d4'
+ 'aa0000aa002aaa0055aa0080aa00aaaa00d4aa2a00aa2a2aaa2a55aa2a80'
+ 'aa2aaaaa2ad4aa5500aa552aaa5555aa5580aa55aaaa55d4aa8000aa802a'
+ 'aa8055aa8080aa80aaaa80d4aaaa00aaaa2aaaaa55aaaa80aaaaaaaaaad4'
+ 'aad400aad42aaad455aad480aad4aaaad4d4d40000d4002ad40055d40080'
+ 'd400aad400d4d42a00d42a2ad42a55d42a80d42aaad42ad4d45500d4552a'
+ 'd45555d45580d455aad455d4d48000d4802ad48055d48080d480aad480d4'
+ 'd4aa00d4aa2ad4aa55d4aa80d4aaaad4aad4d4d400d4d42ad4d455d4d480'
+ 'd4d4aad4d4d40808081212121c1c1c2626263030303a3a3a4444444e4e4e'
+ '5858586262626c6c6c7676768080808a8a8a9494949e9e9ea8a8a8b2b2b2'
+ 'bcbcbcc6c6c6d0d0d0dadadae4e4e4eeeeee')
+ color = terminal_colors[index * 6:(index * 6) + 6]
+ #if color in ('000000', 'e5e5e5'): # keep black or 'default' (gray)
+ # return color
+ r = int(color[0:2], 16)
+ g = int(color[2:4], 16)
+ b = int(color[4:6], 16)
+ r = int(min(r * (1.5 - (r / 510.0)), 255))
+ g = int(min(g * (1.5 - (r / 510.0)), 255))
+ b = int(min(b * (1.5 - (r / 510.0)), 255))
+ return '{0:02x}{1:02x}{2:02x}'.format(r, g, b)
+
+ def html_style(self, fg, bg):
+ """Return HTML style with WeeChat fg and bg colors."""
+ weechat_basic_colors = {
+ 'default': 7, 'black': 0, 'darkgray': 8, 'red': 1, 'lightred': 9,
+ 'green': 2, 'lightgreen': 10, 'brown': 3, 'yellow': 11, 'blue': 4,
+ 'lightblue': 12, 'magenta': 5, 'lightmagenta': 13, 'cyan': 6,
+ 'lightcyan': 14, 'gray': 7, 'white': 15}
+ delim = max(fg.find(','), fg.find(':'))
+ if delim > 0:
+ bg = fg[delim + 1:]
+ fg = fg[0:delim]
+ bold = ''
+ underline = ''
+ reverse = False
+ while fg[0] in COLOR_ATTRIBUTES:
+ if fg[0] == '*':
+ bold = '; font-weight: bold'
+ elif fg[0] == '_':
+ underline = '; text-decoration: underline'
+ elif fg[0] == '!':
+ reverse = True
+ fg = fg[1:]
+ while bg[0] in COLOR_ATTRIBUTES:
+ bg = bg[1:]
+ if fg == 'default':
+ fg = self.options['fg']
+ if bg == 'default':
+ bg = self.options['bg']
+ if bold and fg in ('black', '0'):
+ fg = 'darkgray'
+ reverse = ''
+ if reverse:
+ fg2 = bg
+ bg = fg
+ fg = fg2
+ if fg == 'white' and self.whitebg:
+ fg = 'black'
+ num_fg = 0
+ num_bg = 0
+ if fg in weechat_basic_colors:
+ num_fg = weechat_basic_colors[fg]
+ else:
+ try:
+ num_fg = int(fg)
+ except:
+ self.prnt('Warning: unknown fg color "{0}", '
+ 'using "default" instead'.format(fg))
+ num_fg = weechat_basic_colors['default']
+ if bg in weechat_basic_colors:
+ num_bg = weechat_basic_colors[bg]
+ else:
+ try:
+ num_bg = int(bg)
+ except:
+ self.prnt('Warning: unknown bg color "{0}", '
+ 'using "default" instead'.format(bg))
+ num_bg = weechat_basic_colors['default']
+ style = ('color: #{0}; background-color: #{1}{2}{3}'
+ ''.format(self.html_color(num_fg),
+ self.html_color(num_bg),
+ bold,
+ underline))
+ return style
+
+ def html_string(self, string, maxlen, optfg='fg', optbg='bg', escape=True):
+ """Write html string using fg/bg colors."""
+ fg = optfg
+ bg = optbg
+ if fg in self.options:
+ fg = self.options[optfg]
+ if bg in self.options:
+ bg = self.options[optbg]
+ if maxlen >= 0:
+ string = string.ljust(maxlen)
+ else:
+ string = string.rjust(maxlen * -1)
+ if escape:
+ string = cgi.escape(string)
+ return '<span style="{0}">{1}</span>'.format(self.html_style(fg, bg),
+ string)
+
+ def html_nick(self, nicks, index, prefix, usecolor, highlight, maxlen,
+ optfg='fg', optbg='bg'):
+ """Print a nick."""
+ nick = nicks[index]
+ nickfg = optfg
+ if usecolor and optfg != 'weechat.color.nicklist_away':
+ nick_colors = \
+ self.options['weechat.color.chat_nick_colors'].split(',')
+ nickfg = nick_colors[index % len(nick_colors)]
+ if usecolor and nick == self.html_nick_self:
+ nickfg = 'weechat.color.chat_nick_self'
+ if nick[0] in ('@', '%', '+'):
+ color = self.nick_prefix_color(nick[0]) or optfg
+ str_prefix = self.html_string(nick[0], 1, color, optbg)
+ nick = nick[1:]
+ else:
+ str_prefix = self.html_string(' ', 1, optfg, optbg)
+ length = 1 + len(nick)
+ if not prefix:
+ str_prefix = ''
+ maxlen += 1
+ length -= 1
+ padding = ''
+ if length < abs(maxlen):
+ padding = self.html_string('', abs(maxlen) - length, optfg, optbg)
+ if highlight:
+ nickfg = 'weechat.color.chat_highlight'
+ optbg = 'weechat.color.chat_highlight_bg'
+ string = str_prefix + self.html_string(nick, 0, nickfg, optbg)
+ if maxlen < 0:
+ return padding + string
+ return string + padding
+
+ def html_concat(self, messages, width, optfg, optbg):
+ """Concatenate some messages with colors."""
+ string = ''
+ remaining = width
+ for msg in messages:
+ if msg[0] != '':
+ string += self.html_string(msg[1], 0, msg[0], optbg)
+ remaining -= len(msg[1])
+ else:
+ string += self.html_nick((msg[1],), 0, False, True, False, 0,
+ optfg, optbg)
+ remaining -= len(msg[1])
+ if msg[1][0] in ('@', '%', '+'):
+ remaining += 1
+ string += self.html_string('', remaining, optfg, optbg)
+ return string
+
+ def _html_apply_colors(self, match):
+ string = match.group()
+ end = string.find('}')
+ if end < 0:
+ return string
+ color = string[8:end]
+ text = string[end + 1:]
+ return self.html_string(text, 0, color)
+
+ def _html_apply_color_chat_time_delimiters(self, match):
+ return self.html_string(match.group(), 0,
+ 'weechat.color.chat_time_delimiters')
+
+ def html_chat_time(self, msgtime):
+ """Return formatted time with colors."""
+ option = 'weechat.look.buffer_time_format'
+ if self.options[option].find('${') >= 0:
+ str_without_colors = re.sub(r'\$\{color:[^\}]+\}', '',
+ self.options[option])
+ length = len(time.strftime(str_without_colors, msgtime))
+ value = re.compile(r'\$\{color:[^\}]+\}[^\$]*').sub(
+ self._html_apply_colors, self.options[option])
+ else:
+ value = time.strftime(self.options[option], msgtime)
+ length = len(value)
+ value = re.compile(r'[^0-9]+').sub(
+ self._html_apply_color_chat_time_delimiters, value)
+ value = self.html_string(value, 0, 'weechat.color.chat_time',
+ escape=False)
+ return (time.strftime(value, msgtime), length)
+
+ def html_chat(self, hhmmss, prefix, messages):
+ """Print a message in chat area."""
+ delimiter = self.html_string(':', 0,
+ 'weechat.color.chat_time_delimiters',
+ 'weechat.color.chat_bg')
+ str_datetime = ('2010-12-25 {0:02d}:{1:02d}:{2:02d}'
+ ''.format(hhmmss[0], hhmmss[1], hhmmss[2]))
+ t = time.strptime(str_datetime, '%Y-%m-%d %H:%M:%S')
+ (str_time, length_time) = self.html_chat_time(t)
+ return (str_time + prefix +
+ self.html_string(' &#9474; ', 0,
+ 'weechat.color.chat_prefix_suffix',
+ 'weechat.color.chat_bg', escape=False) +
+ self.html_concat(messages,
+ self.chat_width - length_time -
+ self.prefix_width - 3,
+ 'weechat.color.chat',
+ 'weechat.color.chat_bg'))
+
+ def to_html(self):
+ """Print HTML version of theme."""
+ self.html_nick_self = 'mario'
+ channel = '#weechat'
+ oldtopic = 'Welcome'
+ newtopic = 'Welcome to ' + channel + ' - help channel for WeeChat'
+ nicks = ('@carl', '@jessika', '@louise', '%Diego', '%Melody', '+Max',
+ 'celia', 'Eva', 'freddy', 'Harold^', 'henry4', 'jimmy17',
+ 'jodie', 'lee', 'madeleine', self.html_nick_self, 'mark',
+ 'peter', 'Rachel', 'richard', 'sheryl', 'Vince', 'warren',
+ 'zack')
+ nicks_hosts = ('test@foo.com', 'something@host.com')
+ chat_msgs = ('Hello!',
+ 'hi mario, I just tested your patch',
+ 'I would like to ask something',
+ 'just ask!',
+ 'WeeChat is great?',
+ 'yes',
+ 'indeed',
+ 'sure',
+ 'of course!',
+ 'affirmative',
+ 'all right',
+ 'obviously...',
+ 'certainly!')
+ html = []
+ #html.append('<pre style="line-height: 1.2em">')
+ html.append('<pre>')
+ width = self.chat_width + 1 + self.nicklist_width
+
+ # title bar
+ html.append(self.html_string(newtopic, width,
+ 'weechat.bar.title.color_fg',
+ 'weechat.bar.title.color_bg'))
+
+ # chat
+ chat = []
+ str_prefix_join = self.html_string(
+ '-->', self.prefix_width * -1,
+ 'weechat.color.chat_prefix_join', 'weechat.color.chat_bg')
+ str_prefix_quit = self.html_string(
+ '<--', self.prefix_width * -1,
+ 'weechat.color.chat_prefix_quit', 'weechat.color.chat_bg')
+ str_prefix_network = self.html_string(
+ '--', self.prefix_width * -1,
+ 'weechat.color.chat_prefix_network', 'weechat.color.chat_bg')
+ str_prefix_empty = self.html_string(
+ '', self.prefix_width * -1,
+ 'weechat.color.chat', 'weechat.color.chat_bg')
+ chat.append(
+ self.html_chat(
+ (9, 10, 00),
+ str_prefix_join,
+ (('', self.html_nick_self),
+ ('weechat.color.chat_delimiters', ' ('),
+ ('weechat.color.chat_host', nicks_hosts[0]),
+ ('weechat.color.chat_delimiters', ')'),
+ ('irc.color.message_join', ' has joined '),
+ ('weechat.color.chat_channel', channel))))
+ chat.append(
+ self.html_chat(
+ (9, 10, 25),
+ self.html_nick(nicks, 8, True, True, False,
+ self.prefix_width * -1),
+ (('weechat.color.chat', chat_msgs[0]),)))
+ chat.append(
+ self.html_chat(
+ (9, 11, 2),
+ str_prefix_network,
+ (('', nicks[0]),
+ ('weechat.color.chat', ' has changed topic for '),
+ ('weechat.color.chat_channel', channel),
+ ('weechat.color.chat', ' from "'),
+ ('irc.color.topic_old', oldtopic),
+ ('weechat.color.chat', '"'))))
+ chat.append(
+ self.html_chat(
+ (9, 11, 2),
+ str_prefix_empty,
+ (('weechat.color.chat', 'to "'),
+ ('irc.color.topic_new', newtopic),
+ ('weechat.color.chat', '"'))))
+ chat.append(
+ self.html_chat(
+ (9, 11, 36),
+ self.html_nick(nicks, 16, True, True, True,
+ self.prefix_width * -1),
+ (('weechat.color.chat', chat_msgs[1]),)))
+ chat.append(
+ self.html_chat(
+ (9, 12, 4),
+ str_prefix_quit,
+ (('', 'joe'),
+ ('weechat.color.chat_delimiters', ' ('),
+ ('weechat.color.chat_host', nicks_hosts[1]),
+ ('weechat.color.chat_delimiters', ')'),
+ ('irc.color.message_quit', ' has left '),
+ ('weechat.color.chat_channel', channel),
+ ('weechat.color.chat_delimiters', ' ('),
+ ('irc.color.reason_quit', 'bye!'),
+ ('weechat.color.chat_delimiters', ')'))))
+ chat.append(
+ self.html_chat(
+ (9, 15, 58),
+ self.html_nick(nicks, 12, True, True, False,
+ self.prefix_width * -1),
+ (('weechat.color.chat', chat_msgs[2]),)))
+ chat.append(
+ self.html_chat(
+ (9, 16, 12),
+ self.html_nick(nicks, 0, True, True, False,
+ self.prefix_width * -1),
+ (('weechat.color.chat', chat_msgs[3]),)))
+ chat.append(
+ self.html_chat(
+ (9, 16, 27),
+ self.html_nick(nicks, 12, True, True, False,
+ self.prefix_width * -1),
+ (('weechat.color.chat', chat_msgs[4]),)))
+ for i in range(5, len(chat_msgs)):
+ chat.append(
+ self.html_chat(
+ (9, 17, (i - 5) * 4),
+ self.html_nick(nicks, i - 2, True, True,
+ False, self.prefix_width * -1),
+ (('weechat.color.chat', chat_msgs[i]),)))
+ chat_empty = self.html_string(' ', self.chat_width,
+ 'weechat.color.chat',
+ 'weechat.color.chat_bg')
+
+ # separator (between chat and nicklist)
+ str_separator = self.html_string(
+ '&#9474;', 0, 'weechat.color.separator', 'weechat.color.chat_bg',
+ escape=False)
+
+ # nicklist
+ nicklist = []
+ for index in range(0, len(nicks)):
+ fg = 'weechat.bar.nicklist.color_fg'
+ if nicks[index].endswith('a'):
+ fg = 'weechat.color.nicklist_away'
+ nicklist.append(self.html_nick(nicks, index, True, True, False,
+ self.nicklist_width, fg,
+ 'weechat.bar.nicklist.color_bg'))
+ nicklist_empty = self.html_string('', self.nicklist_width,
+ 'weechat.bar.nicklist.color_fg',
+ 'weechat.bar.nicklist.color_bg')
+
+ # print chat + nicklist
+ for i in range(0, self.chat_height):
+ if i < len(chat):
+ str1 = chat[i]
+ else:
+ str1 = chat_empty
+ if i < len(nicklist):
+ str2 = nicklist[i]
+ else:
+ str2 = nicklist_empty
+ html.append(str1 + str_separator + str2)
+
+ # status
+ html.append(
+ self.html_concat(
+ (('weechat.bar.status.color_delim', '['),
+ ('weechat.color.status_time', '12:34'),
+ ('weechat.bar.status.color_delim', '] ['),
+ ('weechat.bar.status.color_fg', '18'),
+ ('weechat.bar.status.color_delim', '] ['),
+ ('weechat.bar.status.color_fg', 'irc'),
+ ('weechat.bar.status.color_delim', '/'),
+ ('weechat.bar.status.color_fg', 'freenode'),
+ ('weechat.bar.status.color_delim', '] '),
+ ('weechat.color.status_number', '2'),
+ ('weechat.bar.status.color_delim', ':'),
+ ('weechat.color.status_name', '#weechat'),
+ ('weechat.bar.status.color_delim', '('),
+ ('irc.color.item_channel_modes', '+nt'),
+ ('weechat.bar.status.color_delim', '){'),
+ ('weechat.bar.status.color_fg', str(len(nicks))),
+ ('weechat.bar.status.color_delim', '} ['),
+ ('weechat.bar.status.color_fg', 'Act: '),
+ ('weechat.color.status_data_highlight', '3'),
+ ('weechat.bar.status.color_delim', ':'),
+ ('weechat.bar.status.color_fg', '#linux'),
+ ('weechat.bar.status.color_delim', ','),
+ ('weechat.color.status_data_private', '18'),
+ ('weechat.bar.status.color_delim', ','),
+ ('weechat.color.status_data_msg', '4'),
+ ('weechat.bar.status.color_delim', ','),
+ ('weechat.color.status_data_other', '5'),
+ ('weechat.bar.status.color_delim', ','),
+ ('weechat.color.status_data_other', '6'),
+ ('weechat.bar.status.color_delim', ']')),
+ width, 'weechat.bar.status.color_fg',
+ 'weechat.bar.status.color_bg'))
+
+ # input
+ html.append(
+ self.html_concat(
+ (('weechat.bar.input.color_delim', '['),
+ (self.nick_prefix_color('+'), '+'),
+ ('irc.color.input_nick', self.html_nick_self),
+ ('weechat.bar.input.color_delim', '('),
+ ('weechat.bar.input.color_fg', 'i'),
+ ('weechat.bar.input.color_delim', ')] '),
+ ('weechat.bar.input.color_fg', 'this is misspelled '),
+ ('aspell.color.misspelled', 'woord'),
+ ('weechat.bar.input.color_fg', ' '),
+ ('cursor', ' ')),
+ width, 'weechat.bar.input.color_fg',
+ 'weechat.bar.input.color_bg'))
+
+ # end
+ html.append('</pre>')
+ del self.html_nick_self
+ return '\n'.join(html)
+
+ def get_html(self, whitebg=False):
+ if whitebg:
+ self.options['fg'] = 'black'
+ self.options['bg'] = 'white'
+ self.options['cursor'] = '!black'
+ else:
+ self.options['fg'] = '250'
+ self.options['bg'] = 'black'
+ self.options['cursor'] = '!yellow'
+ self.whitebg = whitebg
+ html = self.to_html()
+ del self.whitebg
+ del self.options['fg']
+ del self.options['bg']
+ del self.options['cursor']
+ return html
+
+ def save_html(self, filename, whitebg=False):
+ html = self.get_html(whitebg)
+ try:
+ f = open(filename, 'w')
+ f.write(html)
+ f.close()
+ self.prnt('Theme exported as HTML to "{0}"'.format(filename))
+ except:
+ self.prnt_error('Error writing HTML to "{0}"'.format(filename))
+ raise
+
+
+# =============================[ themes package ]=============================
+
+
+def theme_parse_xml():
+ """
+ Parse XML themes list and return dictionary with list, with key 'id'.
+ Example of item return in dictionary :
+ '15': { 'name': 'flashcode.theme',
+ 'version': '0.4.0',
+ 'url': 'http://www.weechat.org/files/themes/flashcode.theme',
+ 'md5sum': '172d3b9c99e3a8720e8a40abd768092a',
+ 'desc': 'My theme.',
+ 'author': 'FlashCode',
+ 'mail': 'flashcode [at] flashtux [dot] org',
+ 'added': '2011-09-27 18:12:57',
+ 'updated': '2012-12-11 14:28:17' }
+ """
+ global theme_themes
+ theme_themes = {}
+ try:
+ f = open(theme_config_get_xml_filename(), 'rb')
+ string = f.read()
+ f.close()
+ except:
+ weechat.prnt('',
+ '{0}{1}: unable to read xml file'
+ ''.format(weechat.prefix('error'), SCRIPT_NAME))
+ else:
+ try:
+ dom = xml.dom.minidom.parseString(string)
+ except:
+ weechat.prnt('',
+ '{0}{1}: unable to parse xml list of themes: {2}'
+ ''.format(weechat.prefix('error'),
+ SCRIPT_NAME,
+ traceback.format_exc()))
+ else:
+ for scriptNode in dom.getElementsByTagName('theme'):
+ id = scriptNode.getAttribute('id')
+ themedata = {}
+ for node in scriptNode.childNodes:
+ if node.nodeType == node.ELEMENT_NODE:
+ if node.firstChild is not None:
+ nodename = node.nodeName.encode('utf-8')
+ value = node.firstChild.data.encode('utf-8')
+ themedata[nodename] = value
+ theme_themes[id] = themedata
+
+
+def theme_unpack():
+ """Unpack theme file (themes.tar.bz2)."""
+ filename = theme_config_get_tarball_filename()
+ if not os.path.isfile(filename):
+ weechat.prnt('',
+ '{0}{1}: file not found: {1}'
+ ''.format(weechat.prefix('error'),
+ SCRIPT_NAME,
+ filename))
+ return False
+ try:
+ tar = tarfile.open(filename, 'r:bz2')
+ tar.extractall(path=theme_config_get_dir())
+ tar.close()
+ except (tarfile.ReadError, tarfile.CompressionError, IOError):
+ weechat.prnt('',
+ '{0}{1}: invalid file (format .tar.bz2 expected): {2}'
+ ''.format(weechat.prefix('error'),
+ SCRIPT_NAME,
+ filename))
+ weechat.prnt('',
+ '{0}{1}: try /unset theme.themes.url'
+ ''.format(weechat.prefix('error'), SCRIPT_NAME))
+ return False
+ return True
+
+
+def theme_process_update_cb(data, command, rc, stdout, stderr):
+ """Callback when reading themes.tar.bz2 from website."""
+ global theme_hook_process, theme_stdout, theme_themes
+ if stdout:
+ theme_stdout += stdout
+ if stderr:
+ theme_stdout += stderr
+ if int(rc) >= 0:
+ if theme_stdout.startswith('error:'):
+ weechat.prnt('',
+ '{0}{1}: error downloading themes ({2})'
+ ''.format(weechat.prefix('error'),
+ SCRIPT_NAME,
+ theme_stdout[6:].strip()))
+ else:
+ if theme_unpack():
+ theme_parse_xml()
+ weechat.prnt('',
+ '{0}: {1} themes loaded'
+ ''.format(SCRIPT_NAME, len(theme_themes)))
+ theme_hook_process = ''
+ return weechat.WEECHAT_RC_OK
+
+
+def theme_update():
+ """Download themes (themes.tar.bz2)."""
+ global theme_hook_process, theme_stdout
+ # get data from website, via hook_process
+ if theme_hook_process:
+ weechat.unhook(theme_hook_process)
+ theme_hook_process = ''
+ weechat.prnt('', SCRIPT_NAME + ': downloading themes...')
+ theme_config_create_dir()
+ theme_stdout = ''
+ theme_hook_process = (
+ weechat.hook_process_hashtable(
+ 'url:' + weechat.config_string(theme_cfg['themes_url']),
+ {'file_out': theme_config_get_tarball_filename()},
+ TIMEOUT_UPDATE,
+ 'theme_process_update_cb', ''))
+
+
+def theme_list(search):
+ """List themes."""
+ global theme_themes
+ if (len(theme_themes) > 0):
+ weechat.prnt('', '')
+ weechat.prnt('', '{0} themes:'.format(len(theme_themes)))
+ for idtheme, theme in theme_themes.items():
+ weechat.prnt('', ' ' + theme['name'])
+ else:
+ weechat.prnt('', 'No theme loaded')
+
+
+# ================================[ command ]=================================
+
+
+def theme_cmd(data, buffer, args):
+ """Callback for /theme command."""
+ if args == '':
+ weechat.command('', '/help ' + SCRIPT_COMMAND)
+ return weechat.WEECHAT_RC_OK
+ argv = args.strip().split(' ', 1)
+ if len(argv) == 0:
+ return weechat.WEECHAT_RC_OK
+
+ if argv[0] in ('install',):
+ weechat.prnt('',
+ '{0}: action "{1}" not developed'
+ ''.format(SCRIPT_NAME, argv[0]))
+ return weechat.WEECHAT_RC_OK
+
+ # check arguments
+ if len(argv) < 2:
+ if argv[0] in ('install', 'installfile', 'save', 'export'):
+ weechat.prnt('',
+ '{0}: too few arguments for action "{1}"'
+ ''.format(SCRIPT_NAME, argv[0]))
+ return weechat.WEECHAT_RC_OK
+
+ # execute asked action
+ if argv[0] == 'list':
+ theme_list(argv[1] if len(argv) >= 2 else '')
+ elif argv[0] == 'info':
+ filename = None
+ if len(argv) >= 2:
+ filename = argv[1]
+ theme = Theme(filename)
+ if filename:
+ theme.info('Info about theme "{0}":'.format(filename))
+ else:
+ theme.info('Info about current theme:')
+ elif argv[0] == 'show':
+ filename = None
+ if len(argv) >= 2:
+ filename = argv[1]
+ theme = Theme(filename)
+ if filename:
+ theme.show('Content of theme "{0}":'.format(filename))
+ else:
+ theme.show('Content of current theme:')
+ elif argv[0] == 'installfile':
+ theme = Theme()
+ theme.save(theme_config_get_undo())
+ theme = Theme(argv[1])
+ if theme.isok():
+ theme.install()
+ elif argv[0] == 'update':
+ theme_update()
+ elif argv[0] == 'undo':
+ theme = Theme(theme_config_get_undo())
+ if theme.isok():
+ theme.install()
+ elif argv[0] == 'save':
+ theme = Theme()
+ theme.save(argv[1])
+ elif argv[0] == 'backup':
+ theme = Theme()
+ theme.save(theme_config_get_backup())
+ elif argv[0] == 'restore':
+ theme = Theme(theme_config_get_backup())
+ if theme.isok():
+ theme.install()
+ elif argv[0] == 'export':
+ htheme = HtmlTheme()
+ whitebg = False
+ htmlfile = argv[1]
+ argv2 = args.strip().split(' ', 2)
+ if len(argv2) >= 3 and argv2[1] == 'white':
+ whitebg = True
+ htmlfile = argv2[2]
+ htheme.save_html(htmlfile, whitebg)
+
+ return weechat.WEECHAT_RC_OK
+
+
+# ==================================[ main ]==================================
+
+
+def theme_init():
+ """Called when script is loaded."""
+ theme_config_create_dir()
+ filename = theme_config_get_backup()
+ if not os.path.isfile(filename):
+ theme = Theme()
+ theme.save(filename)
+
+
+def main_weechat():
+ """Main function, called only in WeeChat."""
+ if not weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION,
+ SCRIPT_LICENSE, SCRIPT_DESC, '', ''):
+ return
+ theme_config_init()
+ theme_config_read()
+ theme_init()
+ weechat.hook_command(
+ SCRIPT_COMMAND,
+ 'WeeChat theme manager',
+ 'list [<text>] || info|show [<theme>] || install <theme>'
+ ' || installfile <file> || update || undo || backup || save <file>'
+ ' || restore || export [-white] <file>',
+ ' list: list themes (search text if given)\n'
+ ' info: show info about theme (without argument: for current '
+ 'theme)\n'
+ ' show: show all options in theme (without argument: for '
+ 'current theme)\n'
+ ' install: install a theme from repository\n'
+ 'installfile: load theme from a file\n'
+ ' update: download and unpack themes in themes directory\n'
+ ' undo: undo last theme install\n'
+ ' backup: backup current theme (by default in '
+ '~/.weechat/themes/_backup.theme); this is done the first time script '
+ 'is loaded\n'
+ ' save: save current theme in a file\n'
+ ' restore: restore theme backuped by script\n'
+ ' export: save current theme as HTML in a file (with "-white": '
+ 'use white background in HTML)\n\n'
+ 'Examples:\n'
+ ' /' + SCRIPT_COMMAND + ' save /tmp/flashcode.theme => save current '
+ 'theme',
+ 'list'
+ ' || info %(filename)'
+ ' || show %(filename)'
+ ' || install %(themes)'
+ ' || installfile %(filename)'
+ ' || update'
+ ' || undo'
+ ' || save %(filename)'
+ ' || backup'
+ ' || restore'
+ ' || export -white|%(filename) %(filename)',
+ 'theme_cmd', '')
+
+
+def theme_usage():
+ """Display usage."""
+ padding = ' ' * len(sys.argv[0])
+ print('')
+ print('Usage: {0} --export <themefile> <htmlfile> [white]'
+ ''.format(sys.argv[0]))
+ print(' {0} --info <filename>'.format(padding))
+ print(' {0} --help'.format(padding))
+ print('')
+ print(' -e, --export export a theme file to HTML')
+ print(' -i, --info display info about a theme')
+ print(' -h, --help display this help')
+ print('')
+ sys.exit(0)
+
+
+def main_cmdline():
+ """Main function, called only outside WeeChat."""
+ if len(sys.argv) < 2 or sys.argv[1] in ('-h', '--help'):
+ theme_usage()
+ elif len(sys.argv) > 1:
+ if sys.argv[1] in ('-e', '--export'):
+ if len(sys.argv) < 4:
+ theme_usage()
+ whitebg = 'white' in sys.argv[4:]
+ htheme = HtmlTheme(sys.argv[2])
+ htheme.save_html(sys.argv[3], whitebg)
+ elif sys.argv[1] in ('-i', '--info'):
+ if len(sys.argv) < 3:
+ theme_usage()
+ theme = Theme(sys.argv[2])
+ theme.info('Info about theme "{0}":'.format(sys.argv[2]))
+ else:
+ theme_usage()
+
+
+if __name__ == '__main__' and import_other_ok:
+ if import_weechat_ok:
+ main_weechat()
+ else:
+ main_cmdline()