# -*- coding: utf-8 -*-
import os
import sys
import gtk
import gobject
import zipimport, imp
from thread import start_new_thread
from traceback import extract_stack, extract_tb, format_list
import traceback
from pynicotine import slskmessages
from slskmessages import ToBeEncoded
from utils import _
from logfacility import log
WIN32 = sys.platform.startswith("win")
returncode = {'break':0, # don't give other plugins the event, do let n+ process it
'zap':1, # don't give other plugins the event, don't let n+ process it
'pass':2} # do give other plugins the event, do let n+ process it
# returning nothing is the same as 'pass'
tupletype = type(('',''))
def cast_to_unicode_if_needed(text, logfunc):
if type(text) == type(u''):
return text
try:
better = str.decode(text, 'utf8')
logfunc("Plugin problem: casting '%s' to unicode!" % repr(text))
return better
except UnicodeError:
better = str.decode(text, 'utf8', 'replace')
logfunc("Plugin problem: casting '%s' to unicode, losing characters in the process." % repr(text))
return better
except:
logfunc("Plugin problem: failed to completely cast '%s', you're on your own from here on." % repr(text))
return text
class PluginHandler(object):
frame = None # static variable... but should it be?
guiqueue = [] # fifo isn't supported by older python
def __init__(self, frame, plugindir=None):
self.frame = frame
log.add("Loading plugin handler")
self.myUsername = self.frame.np.config.sections["server"]["login"]
self.plugindirs = []
self.enabled_plugins = {}
self.loaded_plugins = {}
self.type2cast = {'integer':int,'int':int, 'float':float, 'string':str,'str':str, }
if not plugindir:
if WIN32:
try:
mydir = os.path.join(os.environ['APPDATA'], 'nicotine')
except KeyError:
# windows 9x?
mydir,x = os.path.split(sys.argv[0])
self.plugindir = os.path.join(mydir, "plugins")
else:
self.plugindir = os.path.join(os.path.expanduser("~"),'.nicotine','plugins')
else:
self.plugindir = plugindir
try:
os.makedirs(self.plugindir)
except:
pass
self.plugindirs.append(self.plugindir)
if os.path.isdir(self.plugindir):
#self.load_directory(self.plugindir)
self.load_enabled()
else:
log.add("It appears '%s' is not a directory, not loading plugins." % self.plugindir)
def __findplugin(self, pluginname):
for directory in self.plugindirs:
fullpath = os.path.join(directory, pluginname)
if os.path.exists(fullpath):
return fullpath
return None
def load_plugin(self, pluginname, reload=False):
if not reload and pluginname in self.loaded_plugins:
return self.loaded_plugins[pluginname]
path = self.__findplugin(pluginname)
if path is None:
log.add("Failed to load plugin '%s', could not find it." % (pluginname, ))
return False
sys.path.insert(0, path)
plugin = imp.load_source(pluginname, os.path.join(path,'__init__.py'))
instance = plugin.Plugin(self)
self.plugin_settings(instance)
instance.LoadNotification()
#log.add("Loaded plugin %s (version %s) from %s" % (instance.__name__, instance.__version__, modulename))
#self.plugins.append((module, instance))
sys.path = sys.path[1:]
self.loaded_plugins[pluginname] = plugin
return plugin
def install_plugin(self, path):
try:
tar = tarfile.open(path, "r:*") #transparently supports gz, bz2
except (tarfile.ReadError, OSError):
raise InvalidPluginError(_('Plugin archive is not in the correct format'))
#ensure the paths in the archive are sane
mems = tar.getmembers()
base = os.path.basename(path)[:-4]
if os.path.isdir(os.path.join(self.plugindirs[0], base)):
raise InvalidPluginError(_('A plugin with the name "%s" is '
'already installed') % base)
for m in mems:
if not m.name.startswith(base):
raise InvalidPluginError(_("Plugin archive contains an unsafe path"))
tar.extractall(self.plugindirs[0])
def uninstall_plugin(self, pluginname):
self.disable_plugin(pluginname)
for dir in self.plugindirs:
try:
shutil.rmtree(self.__findplugin(pluginname))
return True
except:
pass
return False
def enable_plugin(self, pluginname):
if pluginname in self.enabled_plugins:
return
try:
plugin = self.load_plugin(pluginname)
if not plugin: raise Exception("Error loading plugin '%s'" % pluginname)
plugin.enable(self)
self.enabled_plugins[pluginname] = plugin
log.add(_("Loaded plugin %s") % plugin.PLUGIN.__name__)
except:
traceback.print_exc()
log.addwarning(_("Unable to enable plugin %s")%pluginname)
#common.log_exception(logger)
return False
return True
def list_installed_plugins(self):
pluginlist = []
for dir in self.plugindirs:
if os.path.exists(dir):
for file in os.listdir(dir):
if file not in pluginlist and os.path.isdir(os.path.join(dir, file)):
pluginlist.append(file)
return pluginlist
def disable_plugin(self, pluginname):
try:
plugin = self.enabled_plugins[pluginname]
del self.enabled_plugins[pluginname]
plugin.disable(self)
except:
traceback.print_exc()
log.addwarning(_("Unable to fully disable plugin %s")%pluginname)
#common.log_exception(logger)
return False
return True
def get_plugin_settings(self, pluginname):
if pluginname in self.enabled_plugins:
plugin = self.enabled_plugins[pluginname]
if hasattr(plugin.PLUGIN, "metasettings"):
return plugin.PLUGIN.metasettings
def get_plugin_info(self, pluginname):
path = os.path.join(self.__findplugin(pluginname), 'PLUGININFO')
f = open(path)
infodict = {}
for line in f:
try:
key, val = line.split("=",1)
infodict[key] = eval(val)
except ValueError:
pass # this happens on blank lines
return infodict
def save_enabled(self):
self.frame.np.config.sections["plugins"]["enabled"] = self.enabled_plugins.keys()
def check_enabled(self):
if self.frame.np.config.sections["plugins"]["enable"]:
self.load_enabled()
else:
to_enable = self.frame.np.config.sections["plugins"]["enabled"]
for plugin in self.enabled_plugins:
self.enabled_plugins[plugin].disable(self)
print "Disabled plugin: %s" %plugin
def load_enabled(self):
enable = self.frame.np.config.sections["plugins"]["enable"]
if not enable:
return
to_enable = self.frame.np.config.sections["plugins"]["enabled"]
for plugin in to_enable:
self.enable_plugin(plugin)
def plugin_settings(self, plugin):
try:
#customsettings = self.frame.np.config.sections["plugins"][plugin.__id__]
if not hasattr(plugin, "settings"):
return
if plugin.__id__ not in self.frame.np.config.sections["plugins"]:
self.frame.np.config.sections["plugins"][plugin.__id__] = plugin.settings
for i in plugin.settings:
if i not in self.frame.np.config.sections["plugins"][plugin.__id__]:
self.frame.np.config.sections["plugins"][plugin.__id__][i] = plugin.settings[i]
customsettings = self.frame.np.config.sections["plugins"][plugin.__id__]
#if customsettings = self.frame.np.config.sections["plugins"][plugin.__id__]
#for settingname, info in plugin.metasettings.items():
#if settingname not in ('
',):
#settingdescr = info["description"]
#settingtype = info["type"]
#try:
#value = customsettings[settingname]
#try:
#if settingtype.startswith('list '):
#value = list(value)
#(junk, junk, listtype) = settingtype.partition(' ')
#index = 0
#for index in xrange(0, len(value)):
#value[index] = self.type2cast[listtype](value[index])
#else:
#value = self.type2cast[settingtype](value)
#plugin.settings[settingname] = value
#except ValueError:
#log.add(_("Failed to cast the value '%(value)s', stored under '%(name)s', to %(type)s. Using default value." %
#{'value':value, 'name':settingname, 'type':settingtype}))
#except KeyError:
#log.add(_("Unknown setting type '%(type)s'." % {'type':settingtype}))
#except KeyError:
#pass
for key in customsettings:
if key in plugin.settings:
plugin.settings[key] = customsettings[key]
else:
log.add(_("Stored setting '%(name)s' is no longer present in the plugin") % {'name':key})
except KeyError:
log.add("No custom settings found for %s" % (plugin.__name__,))
pass
def TriggerPublicCommandEvent(self, room, command, args):
return self._TriggerCommand("plugin.PLUGIN.PublicCommandEvent", command, room, args)
def TriggerPrivateCommandEvent(self, user, command, args):
return self._TriggerCommand("plugin.PLUGIN.PrivateCommandEvent", command, user, args)
def _TriggerCommand(self, strfunc, command, source, args):
for module, plugin in self.enabled_plugins.items():
try:
if plugin.PLUGIN is None:
continue
func = eval(strfunc)
ret = func(command, source, args)
if ret is not None:
if ret == returncode['zap']:
return True
elif ret == returncode['pass']:
pass
else:
log.add(_("Plugin %(module)s returned something weird, '%(value)s', ignoring") % {'module':module, 'value':str(ret)})
except:
log.add(_("Plugin %(module)s failed with error %(errortype)s: %(error)s.\nTrace: %(trace)s\nProblem area:%(area)s") %
{'module':module,
'errortype':sys.exc_info()[0],
'error':sys.exc_info()[1],
'trace':''.join(format_list(extract_stack())),
'area':''.join(format_list(extract_tb(sys.exc_info()[2])))})
return False
def TriggerEvent(self, function, args):
"""Triggers an event for the plugins. Since events and notifications
are precisely the same except for how n+ responds to them, both can be
triggered by this function."""
hotpotato = args
for module, plugin in self.enabled_plugins.items():
try:
func = eval("plugin.PLUGIN." + function)
ret = func(*hotpotato)
if ret != None and type(ret) != tupletype:
if ret == returncode['zap']:
return None
elif ret == returncode['break']:
return hotpotato
elif ret == returncode['pass']:
pass
else:
log.add(_("Plugin %(module) returned something weird, '%(value)', ignoring") % {'module':module, 'value':ret})
if ret != None:
hotpotato = ret
except:
log.add(_("Plugin %(module)s failed with error %(errortype)s: %(error)s.\nTrace: %(trace)s\nProblem area:%(area)s") %
{'module':module,
'errortype':sys.exc_info()[0],
'error':sys.exc_info()[1],
'trace':''.join(format_list(extract_stack())),
'area':''.join(format_list(extract_tb(sys.exc_info()[2])))})
return hotpotato
def IncomingPrivateChatEvent(self, user, line):
if user != self.myUsername:
# dont trigger the scripts on our own talking - we've got "Outgoing" for that
return self.TriggerEvent("IncomingPrivateChatEvent", (user, line))
else:
return (user, line)
def IncomingPrivateChatNotification(self, user, line):
start_new_thread(self.TriggerEvent, ("IncomingPrivateChatNotification", (user, line)))
def IncomingPublicChatEvent(self, room, user, line):
return self.TriggerEvent("IncomingPublicChatEvent", (room, user, line))
def IncomingPublicChatNotification(self, room, user, line):
start_new_thread(self.TriggerEvent, ("IncomingPublicChatNotification", (room, user, line)))
def OutgoingPrivateChatEvent(self, user, line):
if line != None:
# if line is None nobody actually said anything
return self.TriggerEvent("OutgoingPrivateChatEvent", (user, line))
else:
return (user, line)
def OutgoingPrivateChatNotification(self, user, line):
start_new_thread(self.TriggerEvent, ("OutgoingPrivateChatNotification", (user, line)))
def OutgoingPublicChatEvent(self, room, line):
return self.TriggerEvent("OutgoingPublicChatEvent", (room, line))
def OutgoingPublicChatNotification(self, room, line):
start_new_thread(self.TriggerEvent, ("OutgoingPublicChatNotification", (room, line)))
def OutgoingGlobalSearchEvent(self, text):
return self.TriggerEvent("OutgoingGlobalSearchEvent", (text,))
def OutgoingRoomSearchEvent(self, rooms, text):
return self.TriggerEvent("OutgoingRoomSearchEvent", (rooms, text))
def OutgoingBuddySearchEvent(self, text):
return self.TriggerEvent("OutgoingBuddySearchEvent", (text,))
def OutgoingUserSearchEvent(self, users):
return self.TriggerEvent("OutgoingUserSearchEvent", (users,))
def UserResolveNotification(self, user, ip, port, country=None):
"""Notification for user IP:Port resolving.
Note that country is only set when the user requested the resolving"""
start_new_thread(self.TriggerEvent, ("UserResolveNotification", (user, ip, port, country)))
def ServerConnectNotification(self):
start_new_thread(self.TriggerEvent, ("ServerConnectNotification", (),))
def ServerDisconnectNotification(self, userchoice):
start_new_thread(self.TriggerEvent, ("ServerDisconnectNotification", (userchoice, )))
def JoinChatroomNotification(self, room):
start_new_thread(self.TriggerEvent, ("JoinChatroomNotification", (room,)))
def LeaveChatroomNotification(self, room):
start_new_thread(self.TriggerEvent, ("LeaveChatroomNotification", (room,)))
def UploadQueuedNotification(self, user, virtualfile, realfile):
start_new_thread(self.TriggerEvent, ("UploadQueuedNotification", (user, virtualfile, realfile)))
def UserStatsNotification(self, user, stats):
start_new_thread(self.TriggerEvent, ("UserStatsNotification", (user, stats)))
# other functions
def appendqueue(self, item):
# We cannot do a test after adding the item since it's possible
# this function will be called twice simultaneously - and then
# len(self.guiqueue) might be 2 for both calls.
# Calling the processQueue twice is not a problem though.
addidle = False
self.guiqueue.append(item)
if len(self.guiqueue) >= 0:
addidle = True
if addidle:
#print "Adding idle_add"
gobject.idle_add(self.processQueue)
def log(self, text):
self.appendqueue({'type':'logtext', 'text':text})
def saychatroom(self, room, text):
text = cast_to_unicode_if_needed(text, log.addwarning)
self.frame.np.queue.put(slskmessages.SayChatroom(room, ToBeEncoded(text, 'UTF-8')))
def sayprivate(self, user, text):
'''Send user message in private (showing up in GUI)'''
self.appendqueue({'type':'sayprivate', 'user':user, 'text':text})
def sendprivate(self, user, text):
'''Send user message in private (not showing up in GUI)'''
self.appendqueue({'type':'sendprivate', 'user':user, 'text':text})
def processQueue(self):
while len(self.guiqueue) > 0:
i = self.guiqueue.pop(0)
if i['type'] == 'logtext':
log.add(i['text'])
elif i['type'] == 'sayprivate':
# If we use the np the chat lines only show up on the receiving end, we won't see anything ourselves.
self.frame.privatechats.users[i['user']].SendMessage(i['text'])
elif i['type'] == 'sendprivate':
self.frame.privatechats.SendMessage(i['user'], i['text'])
else:
log.add(_('Unknown queue item %s: %s' % (i['type'], repr(i))))
return False
class BasePlugin(object):
__name__ = "BasePlugin"
__desc__ = "No description provided"
#__id__ = "baseplugin_original" # you normally don't have to set this manually
__version__ = "2008-11-26"
__publiccommands__ = []
__privatecommands__ = []
def __init__(self, parent):
# Never override this function, override init() instead
self.parent = parent
self.frame = parent.frame
try:
self.__id__
except AttributeError:
# See http://docs.python.org/library/configparser.html
# %(name)s will lead to replacements so we need to filter out those symbols.
self.__id__ = self.__name__.lower().replace(' ', '_').replace('%', '_').replace('=', '_')
self.init()
for (trigger, func) in self.__publiccommands__:
self.frame.chatrooms.roomsctrl.CMDS.add('/'+trigger+' ')
for (trigger, func) in self.__privatecommands__:
self.frame.privatechats.CMDS.add('/'+trigger+' ')
def init(self):
pass
def LoadSettings(self, settings):
self.settings = settings
def LoadNotification(self):
pass
def IncomingPrivateChatEvent(self, user, line):
pass
def IncomingPrivateChatNotification(self, user, line):
pass
def IncomingPublicChatEvent(self, room, user, line):
pass
def IncomingPublicChatNotification(self, room, user, line):
pass
def OutgoingPrivateChatEvent(self, user, line):
pass
def OutgoingPrivateChatNotification(self, user, line):
pass
def OutgoingPublicChatEvent(self, room, line):
pass
def OutgoingPublicChatNotification(self, room, line):
pass
def OutgoingGlobalSearchEvent(self, text):
pass
def OutgoingRoomSearchEvent(self, rooms, text):
pass
def OutgoingBuddySearchEvent(self, text):
pass
def OutgoingUserSearchEvent(self, users):
pass
def UserResolveNotification(self, user, ip, port, country):
pass
def ServerConnectNotification(self):
pass
def ServerDisconnectNotification(self, userchoice):
pass
def JoinChatroomNotification(self, room):
pass
def LeaveChatroomNotification(self, room):
pass
def UploadQueuedNotification(self, user, virtualfile, realfile):
pass
def UserStatsNotification(self, user, stats):
pass
# The following are functions to make your life easier,
# you shouldn't override them.
def log(self, text):
self.parent.log(self.__name__ + ": " + text)
def saypublic(self, room, text):
self.parent.saychatroom(room, text)
def sayprivate(self, user, text):
'''Send user message in private (shows up in GUI)'''
self.parent.sayprivate(user, text)
def sendprivate(self, user, text):
'''Send user message in private (doesn't show up in GUI)'''
self.parent.sendprivate(user, text)
def fakepublic(self, room, user, text):
try:
room = self.frame.chatrooms.roomsctrl.joinedrooms[room]
except KeyError:
return False
text = cast_to_unicode_if_needed(text, self.log)
msg = slskmessages.SayChatroom(room, ToBeEncoded(text, 'UTF-8'))
msg.user = user
room.SayChatRoom(msg, text)
return True
# The following are functions used by the plugin system,
# you are not allowed to override these.
def PublicCommandEvent(self, command, room, args):
for (trigger, func) in self.__publiccommands__:
if trigger == command:
return func(self, room, args)
def PrivateCommandEvent(self, command, user, args):
for (trigger, func) in self.__privatecommands__:
if trigger == command:
return func(self, user, args)