User Modules¶
UModule is a new framework, created to unify the multiple copies of frameworks for every module. It is currently in development, on the umodule
branch, and not yet in trunk.
Naming¶
Module filenames must be in the format of psm_yourmodulenamehere.py
preferrably all lowercase.
Module classes must be in the format of PSModule_yourmodulenamehere
,
with both yourmodulenamehere’s being the same case.
For example, I may name my example module psm_example.py
, with the
class name PSModule_example
.
Class¶
All modules must extend UModule
Class Variables¶
You should implement class variables:
NAME
= (string), no whitespace. Will be automagically set to the string after'PSModule_'
in the class name if not otherwise set, so you shouldn’t need to change it. (eg:PSModule_twitter
would result in a NAME of'twitter'
). Must be the same as your module’s folder name.DISPLAY_NAME
= (string), name of this module when shown to the user. Used, for instance, in info lists and such. Defaults toNAME.title()
, but examples of a few ‘special’ ones may includeeRepublik
ore-Sim
.VERSION
= (number), up to you.DEVELOPERS
= (string),'Dev Elepor <dev@elep.or>, Some Onelse <som@wan.wan>'
.
You may choose to implement these class variables:
parser
= (optparse.OptionParser). Used for custom option settings when commands are parsed.parser_option
= (optparse.Option). See above. Look through/esim/esimparser.py
for examples.LISTBOTS
= (list). If specified,/psm_listbots.py
will display this name/description in its list of network bots.
Additionally, the default __init__
sets several useful class
variables for you to use in your module:
parent
= object (received from__init__
, your parent server object, e.g., TS6Protocol()).config
= object (received from__init__
, the same configuration object used in the parent server object).logchan
= string (is set in__init__
, channel from config where your bot users reside if you use one).log
= object (is set in__init__
, file/stdout logger (usage: log.error(msg)|log.info(msg)...etc, see python logging lib for details).dbp
= object (is set in__init__
, is the parent’s database pointer) – NB you should always make your own database if your module needs one.
config.ini¶
UModule assumes that users want to have a virtual user connect to the server.
The following info must be present in config.ini for this to work, where NAME is the same as the class variable NAME, above.
[NAME]
nick: twitter
user: twitter
host: 140.or.bust
gecos: Network Services Bot
modes: +SUoipqNx
nspass: twit
channel: #a
Libraries¶
A few different convenience libraries are loaded by default. To find out
how to use these, simply look at how other umodule-based modules
implement them, and at the code in libs/sys_<library>.py
itself.
These are implemented as class variables, as follows:
ones you are likely to call yourself¶
auth
: Handles situations where you need to verify the user is the founder of a channel.channels
: Handles channels your fake user is ‘allowed’ in, and has been requested in.log
: Provides simple, consistent logging, such as self.log.debug(‘string’).options
: Handles database-stored module options, with a simple interface.
background libs¶
antiflood
: Makes sure users don’t flood you to death with messages.users
: Keeps track of banned users.
Commands¶
There are two sorts of commands in UModules, Admin Commands (acmd), and User Commands (ucmd). This section talks about how to properly use the two types in a UModule.
acmd¶
Admin Commands, as they’re called, are actual pypsd commands. To create
an acmd_something.py library, simply add the functions you wish, and
return the (function, usagestring) as a tuple from get_commands()
,
like this:
def admin_stats(self, source, target, pieces):
self.msg(target, 'Registered users: @b%d@b.' % len(self.users.list_all()))
self.msg(target, 'Registered channels: @b%d@b.' % len(self.channels.list_all()))
return True
def get_commands():
return {
'stats': (admin_stats, 'counts registered users and channels')
}
This file, acmd_something, is put in your umodule folder. Then, in your
module’s init block, you use the load_acmd()
function to load
your library:
from umodule import UModule
from libs import acmd_shared
class PSModule_twitter(UModule):
def __init__(self, parent, config):
UModule.__init__(self, parent, config)
self.load_acmd('something', acmd_something)
After that, UModule will take care of the rest, including setting each
command to only be runnable by pypsd admins only. You can additionally
set custom pypsd permissions and such per command by adding a dict the
the end of a command’s get_commands()
tuple. Like this:
def get_commands():
return {
'stats': (admin_stats, 'counts registered users and channels', {'permission': 'e'})
}
If you do require special acmd loading, you may load your own custom
commands using UModule’s register_command()
manually. Look into the
register_command()
docstring yourself for specific information on
how to use it
ucmd¶
User Commands are commands that are entirely handled by UModule itself. These are what all your users will be calling, and how your users will interact with your module. There are a number of commands that are shared between various modules, such as help, info, request, and remove. Let’s take a look at how those work:
from ucmd_manager import *
def shared_info(self, manager, zone, opts, arg, channel, sender, userinfo):
message = '@sep @bRizon %s Bot@b @sep @bVersion@b %s @sep @bDevelopers@b %s @sep' % (self.NAME, self.VERSION, self.DEVELOPERS)
self.notice(sender, message)
def shared_help(self, manager, zone, opts, arg, channel, sender, userinfo):
command = arg.lower()
for line in self.get_help(zone, command):
self.notice(sender, line)
class SharedCommandManager(CommandManager):
command_list = {
# info's key here, is a string. This is because the only name for this command is 'info'.
'info': (shared_info, CMD_ALL, ARG_NO|ARG_OFFLINE, 'Displays version and author information', []),
# help's key, however, is a tuple containing 'help' and 'hello'. The primary name of this command is 'help', but this shows that 'hello' will also trigger this command. Which name comes first will be the primary one displayed in help output.
('help', 'hello'): (shared_help, CMD_ALL, ARG_OPT|ARG_OFFLINE, 'Displays available commands and their usage', []),
}
Each function that receives UCMDs has a number of arguments, namely: *
self
: The UModule itself * manager
: The Command Manager the
command is a part of * zone
: The ucmd zone the command came from
(CMD_PUBLIC
, or CMD_PRIVATE
) * opts
: Option dictionary *
arg
: Arguments * channel
: Command’s target. Who the user was
sending the message to in the first place * sender
: Nick of the
user who sent the message * userinfo
: Sending user’s userinfo
In a similar sort of fashion, a command_list
value is a list
containing these values: * handler
: Handler function. Receives all
those arguments above * zone
: Zone the command can work in *
args
: Argument settings * description
: Description, used for
help messages * UNKNOWN, look into this a bit later * usage
: Not
shown here, this can either be a string or a list, containing usage
strings. eg: ['#channel', 'nick']
or just '#channel/nick'
. The
main difference here is that list items will be shown on totally new
lines, and a single string will be shown on just a single line
Argument Types¶
Zones¶
You may have noticed the zone argument in a few of the ucmd methods. This is how we differentiate whether a command can be used publicly (in a channel), privately (privmsg right to module user), or both.
CMD_PUBLIC
: Shows the command can be used publiclyCMD_PRIVATE
: Shows the command can be used privatelyCMD_ALL
: Both of the above zones OR’d together, meaning both public and private
Args¶
TODO: Description here
Subsystems¶
Subsystems are blocks of code that manage stuff within your UModule.
self.auth
, self.elog
, and self.channels
are examples of a
few default subsystems. Subsystems have database access, as well as a
few other specific options and functions, setup for them automagically.
If you want to disable a default subsystem, to use your own in place of
it, or to just do something different, add the module’s name to your
class variable disabled_default_modules
, as such:
class MyAwesomeModule(UModule):
u_settings = {
'disabled_default_modules': ['antiflood', 'users']
}
Be aware, though, that the default subsystems are quite tightly
integrated with each other. Unloading, for instance, the options
subsystem is a very bad idea unless you’re going to add your own
options
subsystem in place of it. (note that you would need to add
the subsystem before calling UModule.__init__
, and load it as a
core
subsystem, otherwise all the other modules would fail. This is
what I mean when I say it’s really bad to unload certain subsystems)
To bind your own manager to be loaded, simply call bind_subsystem
.
It will be automatically loaded, and accessable at the name you give.
For instance, this:
self.bind_subsystem('cool', sys_coolguys.CoolManager)
would allow you to access your cool manager at self.cool
once it was
loaded by self.startup
. This is why you check whether your module is
initialized and online before doing stuff, because the managers you try
to access may not even exist before then.
Subsystem Priorities¶
By default, there are three ‘priorities’ of subsystems: * core
:
Primarily, subsystems that are loaded first, and are required by other
lower-level subsystems such as elog
and options
. * normal
:
By default, these contain the useful subsystems that modules use, such
as channels
, users
, auth
, and antiflood
. This is the
default priority for newly added subsystems. * minor
: No subsystems
are in this group by default. This is intended for things like module
APIs, that may make use of the default core
and/or normal
subsystems.
An example of both loading subsystems with different priorities, and
providing kwargs to subsystems on start, is below. Keep in mind that for
regular positional args, you would simply add something like
args=[2, 4, 5]
.
self.bind_subsystem('options', sys_options.OptionManager, priority='core')
self.bind_subsystem('weather', sys_weather.Weather, priority='minor', kwargs={
'key': self.config.get('internets', 'key_wunderground')
})
Event Hooks¶
General hooks¶
Modules can hook into the parent code at any point. Currently hooks
exist for uid
(when a user connects), privmsg
(on receiving a
message), notice
(on receiving a notice), and a few other events.
You can add extra hook points by modifying pseudoserver.py (see irc_UID() for proper usage). Your hooks must be in the following format:
def yourmodulenamehere_hooknamehere(self, prefix, params)
Where hooknamehere is whatever you want to call your hook. prefix
and params
are what get passed from the source hook method. It is of
utmost importance to prefix your hooks with your module’s name,
otherwise the unloading code will break.
After writing your hook method, you add it in a tuple of tuples to your getHooks() definition as in the example below:
def getHooks(self):
return (('uid', self.bopm_SCANUSER),)
UModule-specific hooks¶
UModule has a few default hook functions that must be run when certain
hooks happen. These, currently, are umodule_TMODE
,
umodule_PRIVMSG
, and umodule_NOTICE
. These can either be passed
directly as the hook function, or simply called from the module-specific
hook function, as this:
def example_NOTICE(self, prefix, params):
self.umodule_NOTICE(prefix, params)
pass # do whatever else here
If not using just the default hooks, the getHooks function must be
manually specified, including at least the hooks tmode
, privmsg
,
and notice
. An example of such a basic getHooks call is this:
def getHooks(self):
return (('tmode', self.umodule_TMODE), # default
('notice', self.example_NOTICE), # custom hook
('privmsg', self.umodule_PRIVMSG)) # default
If you require a more complete example of how this works, look into
psm_example.py
.