2015-02-21 12:51:40 +00:00
# Nemubot is a smart and modulable IM bot.
2016-01-31 19:45:44 +00:00
# Copyright (C) 2012-2016 Mercier Pierre-Olivier
2015-02-21 12:51:40 +00:00
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
2015-10-30 20:10:06 +00:00
from datetime import datetime , timezone
2015-02-21 12:51:40 +00:00
import logging
import threading
2015-10-08 22:24:44 +00:00
import sys
2015-02-21 12:51:40 +00:00
from nemubot import __version__
from nemubot . consumer import Consumer , EventConsumer , MessageConsumer
from nemubot import datastore
2015-04-17 06:43:03 +00:00
import nemubot . hooks
2015-02-21 12:51:40 +00:00
logger = logging . getLogger ( " nemubot " )
class Bot ( threading . Thread ) :
""" Class containing the bot context and ensuring key goals """
def __init__ ( self , ip = " 127.0.0.1 " , modules_paths = list ( ) ,
data_store = datastore . Abstract ( ) , verbosity = 0 ) :
""" Initialize the bot context
Keyword arguments :
ip - - The external IP of the bot ( default : 127.0 .0 .1 )
modules_paths - - Paths to all directories where looking for module
data_store - - An instance of the nemubot datastore for bot ' s modules
"""
threading . Thread . __init__ ( self )
logger . info ( " Initiate nemubot v %s " , __version__ )
self . verbosity = verbosity
2015-09-11 06:10:20 +00:00
self . stop = None
2015-02-21 12:51:40 +00:00
# External IP for accessing this bot
import ipaddress
self . ip = ipaddress . ip_address ( ip )
# Context paths
self . modules_paths = modules_paths
self . datastore = data_store
self . datastore . open ( )
# Keep global context: servers and modules
self . servers = dict ( )
self . modules = dict ( )
self . modules_configuration = dict ( )
# Events
self . events = list ( )
self . event_timer = None
# Own hooks
2015-09-04 20:47:02 +00:00
from nemubot . treatment import MessageTreater
self . treater = MessageTreater ( )
2015-02-21 12:51:40 +00:00
import re
def in_ping ( msg ) :
2015-11-14 15:17:25 +00:00
return msg . respond ( " pong " )
self . treater . hm . add_hook ( nemubot . hooks . Message ( in_ping ,
match = lambda msg : re . match ( " ^ *(m[ ' ]?entends?[ -]+tu|h?ear me|do you copy|ping) " ,
msg . message , re . I ) ) ,
" in " , " DirectAsk " )
2015-02-21 12:51:40 +00:00
2015-09-28 22:54:05 +00:00
def in_echo ( msg ) :
from nemubot . message import Text
return Text ( msg . nick + " : " + " " . join ( msg . args ) , to = msg . to_response )
2015-11-02 19:19:12 +00:00
self . treater . hm . add_hook ( nemubot . hooks . Command ( in_echo , " echo " ) , " in " , " Command " )
2015-09-28 22:54:05 +00:00
2015-02-21 12:51:40 +00:00
def _help_msg ( msg ) :
""" Parse and response to help messages """
from more import Response
2015-09-19 12:52:18 +00:00
res = Response ( channel = msg . to_response )
2015-07-04 10:39:52 +00:00
if len ( msg . args ) > = 1 :
2015-02-21 12:51:40 +00:00
if msg . args [ 0 ] in self . modules :
2015-09-23 05:50:16 +00:00
if hasattr ( self . modules [ msg . args [ 0 ] ] , " help_full " ) :
2015-07-04 10:52:39 +00:00
hlp = self . modules [ msg . args [ 0 ] ] . help_full ( )
if isinstance ( hlp , Response ) :
return hlp
else :
res . append_message ( hlp )
2015-02-21 12:51:40 +00:00
else :
2015-09-23 05:50:16 +00:00
res . append_message ( [ str ( h ) for s , h in self . modules [ msg . args [ 0 ] ] . __nemubot_context__ . hooks ] , title = " Available commands for module " + msg . args [ 0 ] )
elif msg . args [ 0 ] [ 0 ] == " ! " :
2015-11-18 20:35:53 +00:00
from nemubot . message . command import Command
for h in self . treater . _in_hooks ( Command ( msg . args [ 0 ] [ 1 : ] ) ) :
if h . help_usage :
lp = [ " \x03 \x02 %s %s \x03 \x02 : %s " % ( msg . args [ 0 ] , ( " " + k if k is not None else " " ) , h . help_usage [ k ] ) for k in h . help_usage ]
jp = h . keywords . help ( )
return res . append_message ( lp + ( [ " . Moreover, you can provides some optional parameters: " ] + jp if len ( jp ) else [ ] ) , title = " Usage for command %s " % msg . args [ 0 ] )
elif h . help :
return res . append_message ( " Command %s : %s " % ( msg . args [ 0 ] , h . help ) )
else :
return res . append_message ( " Sorry, there is currently no help for the command %s . Feel free to make a pull request at https://github.com/nemunaire/nemubot/compare " % msg . args [ 0 ] )
res . append_message ( " Sorry, there is no command %s " % msg . args [ 0 ] )
2015-02-21 12:51:40 +00:00
else :
2015-09-23 05:50:16 +00:00
res . append_message ( " Sorry, there is no module named %s " % msg . args [ 0 ] )
2015-02-21 12:51:40 +00:00
else :
res . append_message ( " Pour me demander quelque chose, commencez "
" votre message par mon nom ; je réagis "
" également à certaine commandes commençant par "
" !. Pour plus d ' informations, envoyez le "
" message \" !more \" . " )
res . append_message ( " Mon code source est libre, publié sous "
" licence AGPL (http://www.gnu.org/licenses/). "
" Vous pouvez le consulter, le dupliquer, "
" envoyer des rapports de bogues ou bien "
" contribuer au projet sur GitHub : "
" http://github.com/nemunaire/nemubot/ " )
res . append_message ( title = " Pour plus de détails sur un module, "
" envoyez \" !help nomdumodule \" . Voici la liste "
" de tous les modules disponibles localement " ,
message = [ " \x03 \x02 %s \x03 \x02 ( %s ) " % ( im , self . modules [ im ] . __doc__ ) for im in self . modules if self . modules [ im ] . __doc__ ] )
return res
2015-11-02 19:19:12 +00:00
self . treater . hm . add_hook ( nemubot . hooks . Command ( _help_msg , " help " ) , " in " , " Command " )
2015-02-21 12:51:40 +00:00
from queue import Queue
2015-05-07 04:39:34 +00:00
# Messages to be treated
2015-02-21 12:51:40 +00:00
self . cnsr_queue = Queue ( )
self . cnsr_thrd = list ( )
self . cnsr_thrd_size = - 1
2015-05-07 04:39:34 +00:00
# Synchrone actions to be treated by main thread
self . sync_queue = Queue ( )
2015-02-21 12:51:40 +00:00
def run ( self ) :
from select import select
2015-05-11 05:41:04 +00:00
from nemubot . server import _lock , _rlist , _wlist , _xlist
2015-02-21 12:51:40 +00:00
self . stop = False
while not self . stop :
2015-09-05 08:14:10 +00:00
with _lock :
2015-02-21 12:51:40 +00:00
try :
2015-09-05 08:14:10 +00:00
rl , wl , xl = select ( _rlist , _wlist , _xlist , 0.1 )
2015-02-21 12:51:40 +00:00
except :
2015-09-05 08:14:10 +00:00
logger . error ( " Something went wrong in select " )
fnd_smth = False
# Looking for invalid server
for r in _rlist :
if not hasattr ( r , " fileno " ) or not isinstance ( r . fileno ( ) , int ) or r . fileno ( ) < 0 :
_rlist . remove ( r )
logger . error ( " Found invalid object in _rlist: " + str ( r ) )
fnd_smth = True
for w in _wlist :
if not hasattr ( w , " fileno " ) or not isinstance ( w . fileno ( ) , int ) or w . fileno ( ) < 0 :
_wlist . remove ( w )
logger . error ( " Found invalid object in _wlist: " + str ( w ) )
fnd_smth = True
for x in _xlist :
if not hasattr ( x , " fileno " ) or not isinstance ( x . fileno ( ) , int ) or x . fileno ( ) < 0 :
_xlist . remove ( x )
logger . error ( " Found invalid object in _xlist: " + str ( x ) )
fnd_smth = True
if not fnd_smth :
logger . exception ( " Can ' t continue, sorry " )
self . quit ( )
continue
for x in xl :
try :
x . exception ( )
except :
logger . exception ( " Uncatched exception on server exception " )
for w in wl :
2015-02-21 12:51:40 +00:00
try :
2015-09-05 08:14:10 +00:00
w . write_select ( )
2015-02-21 12:51:40 +00:00
except :
2015-09-05 08:14:10 +00:00
logger . exception ( " Uncatched exception on server write " )
for r in rl :
for i in r . read ( ) :
try :
self . receive_message ( r , i )
except :
logger . exception ( " Uncatched exception on server read " )
2015-02-21 12:51:40 +00:00
2015-05-11 05:41:04 +00:00
2015-05-06 11:29:26 +00:00
# Launch new consumer threads if necessary
while self . cnsr_queue . qsize ( ) > self . cnsr_thrd_size :
# Next launch if two more items in queue
self . cnsr_thrd_size + = 2
c = Consumer ( self )
self . cnsr_thrd . append ( c )
c . start ( )
2015-05-07 04:39:34 +00:00
while self . sync_queue . qsize ( ) > 0 :
action = self . sync_queue . get_nowait ( )
if action [ 0 ] == " exit " :
self . quit ( )
elif action [ 0 ] == " loadconf " :
for path in action [ 1 : ] :
2015-11-10 06:05:42 +00:00
logger . debug ( " Load configuration from %s " , path )
2015-10-27 17:03:28 +00:00
self . load_file ( path )
2015-11-10 06:05:42 +00:00
logger . info ( " Configurations successfully loaded " )
2015-05-07 04:39:34 +00:00
self . sync_queue . task_done ( )
2015-05-06 11:29:26 +00:00
2015-02-21 12:51:40 +00:00
2015-10-27 17:03:28 +00:00
# Config methods
def load_file ( self , filename ) :
""" Load a configuration file
Arguments :
filename - - the path to the file to load
"""
import os
# Unexisting file, assume a name was passed, import the module!
if not os . path . isfile ( filename ) :
return self . import_module ( filename )
2015-11-03 07:08:39 +00:00
from nemubot . channel import Channel
from nemubot import config
2015-10-27 17:03:28 +00:00
from nemubot . tools . xmlparser import XMLParser
try :
2015-11-03 07:08:39 +00:00
p = XMLParser ( {
" nemubotconfig " : config . Nemubot ,
" server " : config . Server ,
" channel " : Channel ,
" module " : config . Module ,
" include " : config . Include ,
} )
2015-10-27 17:03:28 +00:00
config = p . parse_file ( filename )
except :
logger . exception ( " Can ' t load ` %s ' ; this is not a valid nemubot "
" configuration file. " % filename )
return False
# Preset each server in this file
for server in config . servers :
srv = server . server ( config )
# Add the server in the context
if self . add_server ( srv , server . autoconnect ) :
logger . info ( " Server ' %s ' successfully added. " % srv . id )
else :
logger . error ( " Can ' t add server ' %s ' . " % srv . id )
# Load module and their configuration
for mod in config . modules :
self . modules_configuration [ mod . name ] = mod
if mod . autoload :
try :
__import__ ( mod . name )
except :
logger . exception ( " Exception occurs when loading module "
" ' %s ' " , mod . name )
# Load files asked by the configuration file
for load in config . includes :
self . load_file ( load . path )
2015-02-21 12:51:40 +00:00
# Events methods
def add_event ( self , evt , eid = None , module_src = None ) :
""" Register an event and return its identifiant for futur update
Return :
None if the event is not in the queue ( eg . if it has been executed during the call ) or
returns the event ID .
Argument :
evt - - The event object to add
Keyword arguments :
eid - - The desired event ID ( object or string UUID )
module_src - - The module to which the event is attached to
"""
2015-06-24 17:50:23 +00:00
if hasattr ( self , " stop " ) and self . stop :
logger . warn ( " The bot is stopped, can ' t register new events " )
return
2015-02-21 12:51:40 +00:00
import uuid
# Generate the event id if no given
if eid is None :
eid = uuid . uuid1 ( )
# Fill the id field of the event
if type ( eid ) is uuid . UUID :
evt . id = str ( eid )
else :
# Ok, this is quite useless...
try :
evt . id = str ( uuid . UUID ( eid ) )
except ValueError :
evt . id = eid
# TODO: mutex here plz
# Add the event in its place
t = evt . current
i = 0 # sentinel
for i in range ( 0 , len ( self . events ) ) :
if self . events [ i ] . current > t :
break
self . events . insert ( i , evt )
if i == 0 :
# First event changed, reset timer
self . _update_event_timer ( )
if len ( self . events ) < = 0 or self . events [ i ] != evt :
# Our event has been executed and removed from queue
return None
# Register the event in the source module
if module_src is not None :
module_src . __nemubot_context__ . events . append ( evt . id )
evt . module_src = module_src
logger . info ( " New event registered: %s -> %s " , evt . id , evt )
return evt . id
def del_event ( self , evt , module_src = None ) :
""" Find and remove an event from list
Return :
True if the event has been found and removed , False else
Argument :
evt - - The ModuleEvent object to remove or just the event identifier
Keyword arguments :
module_src - - The module to which the event is attached to ( ignored if evt is a ModuleEvent )
"""
logger . info ( " Removing event: %s from %s " , evt , module_src )
from nemubot . event import ModuleEvent
if type ( evt ) is ModuleEvent :
id = evt . id
module_src = evt . module_src
else :
id = evt
if len ( self . events ) > 0 and id == self . events [ 0 ] . id :
self . events . remove ( self . events [ 0 ] )
self . _update_event_timer ( )
if module_src is not None :
module_src . __nemubot_context__ . events . remove ( id )
return True
for evt in self . events :
if evt . id == id :
self . events . remove ( evt )
if module_src is not None :
module_src . __nemubot_context__ . events . remove ( evt . id )
return True
return False
def _update_event_timer ( self ) :
""" (Re)launch the timer to end with the closest event """
# Reset the timer if this is the first item
if self . event_timer is not None :
self . event_timer . cancel ( )
2015-10-02 03:23:58 +00:00
if len ( self . events ) :
2016-01-22 18:52:21 +00:00
remaining = self . events [ 0 ] . time_left . seconds + self . events [ 0 ] . time_left . microseconds / 1000000
logger . debug ( " Update timer: next event in %d seconds " , remaining )
self . event_timer = threading . Timer ( remaining if remaining > 0 else 0 , self . _end_event_timer )
2015-10-02 03:23:58 +00:00
self . event_timer . start ( )
2015-02-21 12:51:40 +00:00
else :
logger . debug ( " Update timer: no timer left " )
def _end_event_timer ( self ) :
""" Function called at the end of the event timer """
while len ( self . events ) > 0 and datetime . now ( timezone . utc ) > = self . events [ 0 ] . current :
evt = self . events . pop ( 0 )
self . cnsr_queue . put_nowait ( EventConsumer ( evt ) )
self . _update_event_timer ( )
# Consumers methods
def add_server ( self , srv , autoconnect = True ) :
""" Add a new server to the context
Arguments :
srv - - a concrete AbstractServer instance
autoconnect - - connect after add ?
"""
if srv . id not in self . servers :
self . servers [ srv . id ] = srv
if autoconnect and not hasattr ( self , " noautoconnect " ) :
srv . open ( )
return True
else :
return False
# Modules methods
def import_module ( self , name ) :
""" Load a module
Argument :
name - - name of the module to load
"""
if name in self . modules :
self . unload_module ( name )
2015-10-08 22:24:44 +00:00
__import__ ( name )
2015-02-21 12:51:40 +00:00
def add_module ( self , module ) :
""" Add a module to the context, if already exists, unload the
old one before """
2015-05-22 22:33:15 +00:00
module_name = module . __spec__ . name if hasattr ( module , " __spec__ " ) else module . __name__
2015-06-24 17:50:23 +00:00
if hasattr ( self , " stop " ) and self . stop :
logger . warn ( " The bot is stopped, can ' t register new modules " )
return
2015-02-21 12:51:40 +00:00
# Check if the module already exists
2015-05-22 22:33:15 +00:00
if module_name in self . modules :
self . unload_module ( module_name )
2015-02-21 12:51:40 +00:00
# Overwrite print built-in
def prnt ( * args ) :
if hasattr ( module , " logger " ) :
2015-06-04 04:46:39 +00:00
module . logger . info ( " " . join ( [ str ( s ) for s in args ] ) )
2015-07-16 18:38:04 +00:00
else :
logger . info ( " [ %s ] %s " , module_name , " " . join ( [ str ( s ) for s in args ] ) )
2015-02-21 12:51:40 +00:00
module . print = prnt
# Create module context
2015-09-04 20:47:02 +00:00
from nemubot . modulecontext import ModuleContext
2015-02-21 12:51:40 +00:00
module . __nemubot_context__ = ModuleContext ( self , module )
2015-03-26 06:45:33 +00:00
if not hasattr ( module , " logger " ) :
2015-05-22 22:33:15 +00:00
module . logger = logging . getLogger ( " nemubot.module. " + module_name )
2015-03-26 06:45:33 +00:00
2015-02-21 12:51:40 +00:00
# Replace imported context by real one
for attr in module . __dict__ :
if attr != " __nemubot_context__ " and type ( module . __dict__ [ attr ] ) == ModuleContext :
module . __dict__ [ attr ] = module . __nemubot_context__
# Register decorated functions
import nemubot . hooks
2015-11-02 19:19:12 +00:00
for s , h in nemubot . hooks . hook . last_registered :
2015-11-16 06:19:09 +00:00
module . __nemubot_context__ . add_hook ( h , * s if isinstance ( s , list ) else s )
2015-11-02 19:19:12 +00:00
nemubot . hooks . hook . last_registered = [ ]
2015-02-21 12:51:40 +00:00
# Launch the module
if hasattr ( module , " load " ) :
2015-06-02 20:24:45 +00:00
try :
module . load ( module . __nemubot_context__ )
except :
module . __nemubot_context__ . unload ( )
raise
2015-02-21 12:51:40 +00:00
2015-06-02 20:24:45 +00:00
# Save a reference to the module
self . modules [ module_name ] = module
2015-02-21 12:51:40 +00:00
def unload_module ( self , name ) :
""" Unload a module """
if name in self . modules :
self . modules [ name ] . print ( " Unloading module %s " % name )
2015-10-08 22:24:44 +00:00
# Call the user defined unload method
2015-02-21 12:51:40 +00:00
if hasattr ( self . modules [ name ] , " unload " ) :
self . modules [ name ] . unload ( self )
self . modules [ name ] . __nemubot_context__ . unload ( )
2015-10-08 22:24:44 +00:00
# Remove from the nemubot dict
2015-02-21 12:51:40 +00:00
del self . modules [ name ]
2015-10-08 22:24:44 +00:00
# Remove from the Python dict
del sys . modules [ name ]
for mod in [ i for i in sys . modules ] :
if mod [ : len ( name ) + 1 ] == name + " . " :
logger . debug ( " Module ' %s ' also removed from system modules list. " , mod )
del sys . modules [ mod ]
2015-02-21 12:51:40 +00:00
logger . info ( " Module ` %s ' successfully unloaded. " , name )
2015-10-08 22:24:44 +00:00
2015-02-21 12:51:40 +00:00
return True
return False
2015-07-13 22:48:56 +00:00
def receive_message ( self , srv , msg ) :
""" Queued the message for treatment
Arguments :
srv - - The server where the message comes from
msg - - The message not parsed , as simple as possible
"""
2015-02-21 12:51:40 +00:00
self . cnsr_queue . put_nowait ( MessageConsumer ( srv , msg ) )
def quit ( self ) :
""" Save and unload modules and disconnect servers """
self . datastore . close ( )
if self . event_timer is not None :
logger . info ( " Stop the event timer... " )
self . event_timer . cancel ( )
2015-06-09 16:48:32 +00:00
logger . info ( " Stop consumers " )
k = self . cnsr_thrd
for cnsr in k :
cnsr . stop = True
2015-02-21 12:51:40 +00:00
logger . info ( " Save and unload all modules... " )
k = list ( self . modules . keys ( ) )
for mod in k :
self . unload_module ( mod )
logger . info ( " Close all servers connection... " )
k = list ( self . servers . keys ( ) )
for srv in k :
self . servers [ srv ] . close ( )
self . stop = True
# Treatment
def check_rest_times ( self , store , hook ) :
""" Remove from store the hook if it has been executed given time """
if hook . times == 0 :
if isinstance ( store , dict ) :
store [ hook . name ] . remove ( hook )
if len ( store ) == 0 :
del store [ hook . name ]
elif isinstance ( store , list ) :
store . remove ( hook )
def hotswap ( bak ) :
bak . stop = True
if bak . event_timer is not None :
bak . event_timer . cancel ( )
bak . datastore . close ( )
new = Bot ( str ( bak . ip ) , bak . modules_paths , bak . datastore )
new . servers = bak . servers
new . modules = bak . modules
new . modules_configuration = bak . modules_configuration
new . events = bak . events
new . hooks = bak . hooks
new . _update_event_timer ( )
return new