# -*- coding: utf-8 -*-
# Copyright (c) 2009-2010 by Nicolas Reynolds <fauno@kiwwwi.com.ar>
#
# 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/>.
## ABOUT
# This plugin gives format to identi.ca's bot messages, converting
# the @sender into the bot's nick and colorizing usernames, groups
# and hashtags.
# For example:
# 11:37:45 update | fauno: hi there
# Will turn into
# 11:37:45 fauno | hi there
# It's written for bitlbee, but should work with anything that permits
# the XMPP bot open a query buffer with you.
# Since version 0.2 it includes suscription handling and whois
# habilities.
# HISTORY
# 2009-07-27, fauno:
# initial release
#
# 2009-09-17, fauno:
# added basic suscription handling (sub/unsub/block/unblock)
# username whois
# remind user color
#
# 2009-09-27, fauno:
# help definition
#
# 2009-10-11, fauno:
# hability to check up to 20 updates from users (/sn updates <username> <quantity>)
#
# 2010-01-20, fauno:
# fixed int to str error caused by api changes.
# default regexp's for @names, etc. includes trailing space
#
# 2010-03-15, fauno:
# new commands:
# - groups see in which groups a user is subscribed
# - join join a group
# - leave leave a group
# - group group profile
#
# nick completion adding %(sn_nicks) to weechat.completion.default_template
# unicode hashtags, usernames and groups
# (changes in plugins.var.python.identica.nick_re
# plugins.var.python.identica.hashtag_re
# plugins.var.python.identica.group_re)
# prepopulate nicklist with plugins.var.python.identica.prepopulate
# (will take a while to download all subscriptions)
#
# 2011-01-16, fauno:
# - Removed chat_nick_colors in favor of configurable array (useful for
# weechat's 256 colors on 0.3.4)
# see plugins.var.python.identica.colors
# - Fixed nick completion
#
# 2011-01-18, fauno:
# - Fixed error on load when no username nor password were given
#
# TODO - cache json requests
import weechat
import re
import urllib2
import simplejson as json
from base64 import encodestring
from urllib import urlencode
from random import randint
SCRIPT_NAME = 'identica'
SCRIPT_AUTHOR = 'fauno <fauno@kiwwwi.com.ar>'
SCRIPT_VERSION = '0.4.2'
SCRIPT_LICENSE = 'GPL3'
SCRIPT_DESC = 'Formats identi.ca\'s bot messages'
settings = {
'username' : '',
'password' : '',
'service' : 'identi.ca',
'scheme' : 'https',
'channel' : 'localhost.update',
're' : '^(?P<update>\w+)(?P<separator>\W+?)(?P<username>\w+): (?P<dent>.+)$',
'me' : '^(?P<update>\w+)(?P<separator>\W+?)(?P<username>\w+): \/me (?P<dent>.+)$',
'nick_color' : 'green',
'hashtag_color' : 'blue',
'group_color' : 'red',
'nick_color_identifier' : 'blue',
'hashtag_color_identifier': 'green',
'group_color_identifier' : 'green',
'nick_re' : '(@)(\w+)',
'hashtag_re' : '(#)(\w+)',
'group_re' : '(!)(\w+)',
'prepopulate' : 'on',
'completion_blacklist' : '',
'shorten' : 'on',
'shorten_service' : 'http://ur1.ca',
'colors' : 'red,lightred,green,lightgreen,brown,yellow,blue,lightblue,magenta,lightmagenta,cyan,lightcyan,white'
}
users = {}
groups = {}
class StatusNet():
def __init__(self, username, password, scheme, service):
self.username = username
self.password = password
self.realm = 'StatusNet API'
self.service = service
self.scheme = scheme
self.opener = self.__get_auth_opener()
def __get_auth_opener(self):
'''Authentication'''
basic_auth = encodestring(':'.join([self.username, self.password]))
basic_auth = ' '.join(['Basic', basic_auth])
handler = urllib2.HTTPBasicAuthHandler()
handler.add_password(realm=self.realm,
uri=self.service,
user=self.username,
passwd=self.password)
self.headers = {'Authorization':basic_auth}
return urllib2.build_opener(handler)
def build_request(self, api_method, api_action, user_or_id, data={}):
'''Builds an API request'''
url = '%s://%s/api/%s/%s/%s.json' % (self.scheme,
self.service,
api_method,
api_action,
user_or_id)
request = urllib2.Request(url, urlencode(data), self.headers)
return request
def handle_request(self, request):
'''Sends an API request and handles errors'''
try:
response = self.opener.open(request)
except urllib2.HTTPError, error:
if error.code == 403:
return False
else:
weechat.prnt(weechat.current_buffer(),
'%s[%s] Server responded with a %d error code' % (weechat.prefix('error'),
self.service,
error.code))
return None
else:
return response
# End of StatusNet
class ur1():
'''Shortens URL using ur1.ca free service'''
def __init__ (self, service='http://ur1.ca'):
self.service = service
def __build_request (self, url):
data = { 'longurl' : url }
return urllib2.Request(self.service, urlencode(data))
def __handle_request (self, request):
try:
response = urllib2.urlopen(request)
return re.findall(r'Your ur1 is: <a[^>]*>([^<]*)', response.read(), re.UNICODE)[0]
except urllib2.HTTPError, error:
weechat.prnt('', '%s[%s] Got a HTTP %d error code, sending long url.' % (weechat.prefix('error'), self.service, error.code))
return False
def shorten (self, url):
return self.__handle_request(self.__build_request(url))
## User functions
def subscribe (username):
'''Subscribes to a user'''
if len(username) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('friendships', 'create', username))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sYou\'re already suscribed to %s' % (weechat.prefix('error'), username)))
else:
weechat.prnt(weechat.current_buffer(), ('%sSuscribed to %s updates' % (weechat.prefix('join'), username)))
return weechat.WEECHAT_RC_OK
def unsubscribe (username):
'''Drops a subscription'''
if len(username) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('friendships', 'destroy', username))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sYou aren\'t suscribed to %s' % (weechat.prefix('error'), username)))
else:
weechat.prnt(weechat.current_buffer(), ('%sUnsuscribed from %s\'s updates' % (weechat.prefix('quit'), username)))
return weechat.WEECHAT_RC_OK
def whois (username):
'''Shows profile information about a given user'''
if len(username) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('users', 'show', username))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sCan\'t retrieve information about %s' % (weechat.prefix('error'), username)))
else:
whois = json.load(response)
whois['summary'] = ' '.join([u'\u00B5', str(whois['statuses_count']),
u'\u2764', str(whois['favourites_count']),
'subscribers', str(whois['followers_count']),
'subscriptions', str(whois['friends_count'])])
for property in ['name', 'description', 'url', 'location', 'profile_image_url', 'summary']:
if property in whois and whois[property] != None:
weechat.prnt(weechat.current_buffer(), ('%s[%s] %s' % (weechat.prefix('network'),
nick_color(username),
whois[property].encode('utf-8'))))
return weechat.WEECHAT_RC_OK
def block (username):
'''Blocks users'''
if len(username) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('blocks', 'create', username))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sCan\'t block %s' % (weechat.prefix('error'), username)))
else:
weechat.prnt(weechat.current_buffer(), ('%sBlocked %s' % (weechat.prefix('network'), username)))
return weechat.WEECHAT_RC_OK
def unblock (username):
'''Unblocks users'''
if len(username) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('blocks', 'destroy', username))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sCan\'t unblock %s' % (weechat.prefix('error'), username)))
else:
weechat.prnt(weechat.current_buffer(), ('%sUnblocked %s' % (weechat.prefix('network'), username)))
return weechat.WEECHAT_RC_OK
def updates (username, quantity):
'''Shows user updates'''
if len(username) == 0 or quantity > 20:
return weechat.WEECHAT_RC_ERROR
if quantity < 1:
quantity = 1
response = statusnet_handler.handle_request(statusnet_handler.build_request('statuses', 'user_timeline', username))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sCan\'t retrieve %s\'s updates' % (weechat.prefix('error'), username)))
else:
statuses = json.load(response)[:quantity]
while quantity > 0:
quantity -= 1
weechat.prnt_date_tags(weechat.buffer_search('', weechat.config_get_plugin('channel')), 0, 'irc_privmsg', 'update\t%s: %s' % (username, statuses[quantity]['text'].encode('utf-8')))
return weechat.WEECHAT_RC_OK
## Group functions
def group (group):
'''Shows information about a group'''
if len(group) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('statusnet/groups', 'show', group))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sCan\'t show %s' % (weechat.prefix('error'), group)))
else:
group_info = json.load(response)
for property in ['fullname', 'description', 'homepage_url', 'location', 'original_logo']:
if property in group_info and group_info[property] != None:
weechat.prnt(weechat.current_buffer(), ('%s[%s] %s' % (weechat.prefix('network'),
group,
group_info[property].encode('utf-8'))))
return weechat.WEECHAT_RC_OK
def groups (username):
'''Shows groups a user is in'''
if len(username) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('statusnet/groups', 'list', username))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), '%sCan\'t show %s\'s groups' % (weechat.prefix('error'), username))
else:
groups = json.load(response)
group_list = ' '.join([group['nickname'].encode('utf-8') for group in groups])
weechat.prnt(weechat.buffer_search('', weechat.config_get_plugin('channel')),
'%sGroups %s is in: %s' % (weechat.prefix('network'),
nick_color(username),
group_list))
return weechat.WEECHAT_RC_OK
def join (group):
'''Joins a group'''
if len(group) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('statusnet/groups', 'join', group))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sCan\'t join group %s' % (weechat.prefix('error'), group)))
else:
group_info = json.load(response)
weechat.prnt(weechat.current_buffer(), '%sYou joined group %s (%s)' % (weechat.prefix('network'), group_info['fullname'].encode('utf-8'), group))
return weechat.WEECHAT_RC_OK
def leave (group):
'''Leaves a group'''
if len(group) == 0:
return weechat.WEECHAT_RC_ERROR
response = statusnet_handler.handle_request(statusnet_handler.build_request('statusnet/groups', 'leave', group))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sCan\'t leave %s' % (weechat.prefix('error'), group)))
else:
group_info = json.load(response)
weechat.prnt(weechat.current_buffer(), '%sYou left group %s (%s)' % (weechat.prefix('network'), group_info['fullname'].encode('utf-8'), group))
return weechat.WEECHAT_RC_OK
def populate_subscriptions ():
'''Populates users dict with subscriptions'''
response = statusnet_handler.handle_request(statusnet_handler.build_request('statuses', 'friends', weechat.config_get_plugin('username')))
if response == None:
pass
elif response == False:
weechat.prnt(weechat.current_buffer(), ('%sCan\'t obtain subscription list ' % weechat.prefix('error')))
else:
subscriptions = json.load(response)
for profile in subscriptions:
populate = nick_color(profile['screen_name'].encode('utf-8'))
weechat.prnt(weechat.buffer_search('', weechat.config_get_plugin('channel')), ' '.join([ weechat.prefix('network'), 'Subscriptions', '(%d)' % len(users)] + [username for username in users]))
return weechat.WEECHAT_RC_OK
## Parsing and formatting functions
def colorize (message):
'''Colorizes replies, hashtags and groups'''
for identifier in ['nick','hashtag','group']:
identifier_name = ''.join([identifier, '_re'])
identifier_color = ''.join([identifier, '_color'])
identifier_color_identifier = ''.join([identifier, '_color_identifier'])
identifier_re = re.compile(weechat.config_get_plugin(identifier_name), re.UNICODE)
replace = r''.join([
weechat.color(weechat.config_get_plugin(identifier_color_identifier)),
'\\1',
weechat.color(weechat.config_get_plugin(identifier_color)),
'\\2',
weechat.color('reset')
])
message = identifier_re.sub(replace, message)
return message
def nick_color (nick):
'''Randomizes color for nicks'''
# Get the colors
colors = weechat.config_get_plugin('colors').split(',')
if nick in users and 'color' in users[nick]:
pass
else:
users[nick] = {}
users[nick]['color'] = ''.join(colors[randint(0,len(colors)-1)])
nick = ''.join([weechat.color(users[nick]['color']), nick, weechat.color('reset')])
return nick
def clean (message):
'''Cleans URLs added by bot'''
return re.sub(r''.join([' \(http://', service, '/[a-zA-Z0-9/\-_#]+\)']), '', message)
def parse_in (server, modifier, data, the_string):
'''Parses incoming messages'''
plugin, channel, flags = data.split(';')
flag = flags.split(',')
if channel == weechat.config_get_plugin('channel') and 'irc_privmsg' in flag:
the_string = weechat.string_remove_color(the_string, '')
matcher = re.compile(weechat.config_get_plugin('re'), re.UNICODE)
m = matcher.search(the_string)
if not m \
or m.group('update') == weechat.config_get_plugin('username'):
return colorize(the_string)
dent = colorize(clean(m.group('dent')))
username = nick_color(m.group('username'))
the_string = ''.join([ username, m.group('separator'), dent ])
return the_string
def parse_out (server, modifier, data, the_string):
'''Parses outgoing messages, provides @nick completion and url shortening'''
# data => localhost
# the_string => PRIVMSG update :help
# server =>
# modifier => irc_out_PRIVMSG
command, buffer, message = the_string.split(' ', 2)
channel = '.'.join([data, buffer])
if channel == weechat.config_get_plugin('channel'):
completion_blacklist = weechat.config_get_plugin('completion_blacklist').split(',')
# the regexp will match any word that is not preceded by [@#!]
# oddly, for "@fauno", it will match "auno", when the opposite
# "(?<=[@#!])\w+" matches the full word with prefix ("@fauno")
# nevertheless, it breaks the word, so it'll never match an
# already prefixed nick, hashtag nor group name.
for word in re.findall(r'[\S]+[^\W]', message, re.UNICODE):
if word in users and not word in completion_blacklist:
message = re.sub(r''.join(['(?<![#@!])',word]), ''.join(['@', word]), message)
if weechat.config_get_plugin('shorten') == 'on':
u = ur1(weechat.config_get_plugin('shorten_service'))
for url in re.findall(r'http://[^ ]*', message, re.UNICODE):
if len(url) > 20:
s = u.shorten(url)
if s != False:
message = message.replace(url, s)
the_string = ' '.join([command, buffer, message])
return the_string
## /SN functions
def nicklist(data, completion_item, buffer, completion):
'''Completion for /sn'''
if weechat.buffer_get_string(buffer, 'name') == weechat.config_get_plugin('channel'):
for username in users:
weechat.hook_completion_list_add(completion, username, 1, weechat.WEECHAT_LIST_POS_SORT)
return weechat.WEECHAT_RC_OK
def sn (data, buffer, args):
'''/sn command'''
if args == '':
weechat.command('', '/help sn')
return weechat.WEECHAT_RC_OK
argv = args.strip().split(' ')
if argv[0] == 'subscribe':
subscribe(argv[1])
elif argv[0] == 'unsubscribe':
unsubscribe(argv[1])
elif argv[0] == 'whois':
whois(argv[1])
elif argv[0] == 'block':
block(argv[1])
elif argv[0] == 'unblock':
unblock(argv[1])
elif argv[0] == 'updates':
try:
updates(argv[1], int(argv[2]))
except:
updates(argv[1], 20)
elif argv[0] == 'join':
join(argv[1])
elif argv[0] == 'leave':
leave(argv[1])
elif argv[0] == 'group':
group(argv[1])
elif argv[0] == 'groups':
try:
groups(argv[1])
except:
groups(weechat.config_get_plugin('username'))
return weechat.WEECHAT_RC_OK
## init
if weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
SCRIPT_DESC, '', ''):
for option, default_value in settings.iteritems():
if not weechat.config_is_set_plugin(option):
weechat.config_set_plugin(option, default_value)
username = weechat.config_get_plugin('username')
password = weechat.config_get_plugin('password')
service = weechat.config_get_plugin('service')
scheme = weechat.config_get_plugin('scheme')
if len(username) == 0 or len(password) == 0:
weechat.prnt(weechat.current_buffer(),
'%s[%s] Please set your username and password and reload the plugin to get the /sn commands working' %
(weechat.prefix('error'), service))
else:
statusnet_handler = StatusNet(username, password, scheme, service)
if weechat.config_get_plugin('prepopulate') == 'on':
populate_subscriptions()
# hook incoming messages for parsing
weechat.hook_modifier('weechat_print', 'parse_in', '')
# hook outgoing messages for nick completion
weechat.hook_modifier('irc_out_privmsg', 'parse_out', '')
# /sn
weechat.hook_command('sn',
'StatusNet manager',
'whois | subscribe | unsubscribe | block | unblock | updates | groups <username> || group | join | leave <group>',
' whois: retrieves profile information from <username>'
"\n"
' subscribe: subscribes to <username>'
"\n"
' unsubscribe: unsubscribes from <username>'
"\n"
' block: blocks <username>'
"\n"
' unblock: unblocks <username>'
"\n"
' updates: recent updates from <username> <quantity (<20)>'
"\n"
' join: joins group <group>'
"\n"
' leave: leaves group <group>'
"\n"
' groups: groups (<username>) you or a specified username is subscribed'
"\n"
' group: shows info about <group>',
'whois %(sn_nicks) || subscribe %(sn_nicks) || unsubscribe %(sn_nicks) || block %(sn_nicks) || unblock %(sn_nicks) || updates %(sn_nicks) || join || leave || group || groups',
'sn',
'')
# Completion for /sn commands
weechat.hook_completion('sn_nicks', 'list of SN users', 'nicklist', '')