2020-02-09 15:24:57 +02:00
import collections
2021-05-04 18:55:49 +03:00
import logging
2021-04-22 04:17:37 +03:00
import json
2021-04-25 03:10:19 +03:00
import requests
2021-05-07 00:09:17 +03:00
from html import escape
2021-04-28 21:10:51 +03:00
from datetime import timedelta
2021-04-25 03:10:19 +03:00
import time
2020-02-09 15:07:05 +02:00
2021-05-07 00:09:17 +03:00
from nio import RoomCreateError
2021-04-28 23:46:38 +03:00
from modules . common . module import BotModule , ModuleCannotBeDisabled
2020-02-02 23:08:15 +02:00
2021-05-04 18:55:49 +03:00
class LogDequeHandler ( logging . Handler ) :
def __init__ ( self , count ) :
super ( ) . __init__ ( level = logging . NOTSET )
2021-05-07 00:09:17 +03:00
self . logs = dict ( )
2021-05-04 18:55:49 +03:00
self . level = logging . INFO
def emit ( self , record ) :
2021-05-07 00:09:17 +03:00
try :
self . logs [ str ( record . module ) ] . append ( record )
except :
self . logs [ str ( record . module ) ] = collections . deque ( [ record ] , maxlen = 15 )
2021-05-04 18:55:49 +03:00
2020-02-02 23:08:15 +02:00
class MatrixModule ( BotModule ) :
2020-01-05 00:28:57 +02:00
2020-02-06 21:56:53 +02:00
def __init__ ( self , name ) :
super ( ) . __init__ ( name )
2020-02-09 15:07:05 +02:00
self . starttime = None
self . can_be_disabled = False
2020-02-06 01:19:45 +02:00
2020-01-06 00:33:42 +02:00
def matrix_start ( self , bot ) :
2020-02-06 21:56:53 +02:00
super ( ) . matrix_start ( bot )
2021-04-28 21:10:51 +03:00
self . starttime = time . time ( )
2021-05-04 18:55:49 +03:00
self . loghandler = LogDequeHandler ( 10 )
self . loghandler . setFormatter ( logging . Formatter ( ' %(levelname)s - %(name)s - %(message)s ' ) )
logging . root . addHandler ( self . loghandler )
2021-01-23 22:11:42 +02:00
2020-01-05 00:28:57 +02:00
async def matrix_message ( self , bot , room , event ) :
2021-04-22 04:17:37 +03:00
args = event . body . split ( None , 2 )
2020-02-09 15:07:05 +02:00
2020-11-24 23:02:47 +02:00
if len ( args ) == 2 :
2020-02-02 23:08:15 +02:00
if args [ 1 ] == ' quit ' :
2020-02-03 23:45:14 +02:00
await self . quit ( bot , room , event )
2020-02-02 23:08:15 +02:00
elif args [ 1 ] == ' version ' :
2020-02-03 23:45:14 +02:00
await self . version ( bot , room )
2020-02-02 23:08:15 +02:00
elif args [ 1 ] == ' reload ' :
2020-02-03 23:45:14 +02:00
await self . reload ( bot , room , event )
2020-02-02 23:08:15 +02:00
elif args [ 1 ] == ' status ' :
2020-02-03 23:45:14 +02:00
await self . status ( bot , room )
2020-02-02 23:08:15 +02:00
elif args [ 1 ] == ' stats ' :
2020-02-03 23:45:14 +02:00
await self . stats ( bot , room )
2020-02-02 23:08:15 +02:00
elif args [ 1 ] == ' leave ' :
2020-02-03 23:45:14 +02:00
await self . leave ( bot , room , event )
2020-02-06 01:19:45 +02:00
elif args [ 1 ] == ' modules ' :
await self . show_modules ( bot , room )
2021-04-22 04:17:37 +03:00
elif args [ 1 ] == ' export ' :
await self . export_settings ( bot , event )
2021-04-25 03:10:19 +03:00
elif args [ 1 ] == ' ping ' :
await self . get_ping ( bot , room , event )
2021-05-19 23:01:04 +03:00
elif args [ 1 ] == ' rooms ' :
await self . rooms ( bot , room , event )
2020-02-06 01:19:45 +02:00
elif len ( args ) == 3 :
if args [ 1 ] == ' enable ' :
await self . enable_module ( bot , room , event , args [ 2 ] )
elif args [ 1 ] == ' disable ' :
await self . disable_module ( bot , room , event , args [ 2 ] )
2021-04-22 04:17:37 +03:00
elif args [ 1 ] == ' export ' :
await self . export_settings ( bot , event , module_name = args [ 2 ] )
elif args [ 1 ] == ' import ' :
await self . import_settings ( bot , event )
2021-05-07 00:09:17 +03:00
elif args [ 1 ] == ' logs ' :
await self . last_logs ( bot , room , event , args [ 2 ] )
2021-05-11 20:52:50 +03:00
elif args [ 1 ] == ' uricache ' :
await self . manage_uri_cache ( bot , room , event , args [ 2 ] )
2020-01-05 00:28:57 +02:00
else :
2020-02-16 14:23:20 +02:00
pass
# TODO: Make this configurable. By default don't say anything.
# await bot.send_text(room, 'Unknown command, sorry.')
2020-01-05 00:28:57 +02:00
2021-04-25 03:10:19 +03:00
async def get_ping ( self , bot , room , event ) :
self . logger . info ( f ' { event . sender } pinged the bot in { room . room_id } ' )
# initial pong
serv_before = event . server_timestamp
local_before = time . time ( )
2021-06-06 18:09:06 +03:00
pong = await bot . send_text ( room , ' Pong! ' )
2021-04-25 03:10:19 +03:00
local_delta = int ( ( time . time ( ) - local_before ) * 1000 )
# ask the server what the timestamp was on our pong
serv_delta = None
event_url = f ' { bot . client . homeserver } /_matrix/client/r0/rooms/ { room . room_id } /event/ { pong . event_id } ?access_token= { bot . client . access_token } '
try :
serv_delta = requests . get ( event_url ) . json ( ) [ ' origin_server_ts ' ] - serv_before
delta = f ' server response in { local_delta } ms, event created in { serv_delta } ms '
except Exception as e :
self . logger . error ( f " Failed getting server timestamp: { e } " )
delta = f ' server response in { local_delta } ms '
# update event
content = {
' m.new_content ' : {
' msgtype ' : ' m.notice ' ,
' body ' : f ' Pong! ( { delta } ) '
} ,
' m.relates_to ' : {
' rel_type ' : ' m.replace ' ,
' event_id ' : pong . event_id
} ,
' msgtype ' : ' m.notice ' ,
' body ' : delta
}
await bot . client . room_send ( room . room_id , ' m.room.message ' , content )
2020-02-03 23:45:14 +02:00
async def leave ( self , bot , room , event ) :
bot . must_be_admin ( room , event )
2020-02-08 23:16:19 +02:00
self . logger . info ( f ' { event . sender } asked bot to leave room { room . room_id } ' )
2020-02-03 23:45:14 +02:00
await bot . send_text ( room , f ' By your command. ' )
await bot . client . room_leave ( room . room_id )
async def stats ( self , bot , room ) :
roomcount = len ( bot . client . rooms )
homeservers = dict ( )
for croomid in bot . client . rooms :
2021-04-28 21:13:21 +03:00
try :
users = bot . client . rooms [ croomid ] . users
except ( KeyError , ValueError ) as e :
self . logger . warning ( f " Couldn ' t get user list in room with id { croomid } , skipping: { repr ( e ) } " )
continue
for user in users :
user , hs = user . split ( ' : ' , 1 )
2020-02-03 23:45:14 +02:00
if homeservers . get ( hs ) :
2021-04-28 21:13:21 +03:00
homeservers [ hs ] . add ( user )
2020-02-03 23:45:14 +02:00
else :
2021-04-28 21:13:21 +03:00
homeservers [ hs ] = { user }
homeservers = { k : len ( v ) for k , v in homeservers . items ( ) }
usercount = sum ( homeservers . values ( ) )
hscount = len ( homeservers )
2020-02-03 23:45:14 +02:00
homeservers = sorted ( homeservers . items ( ) , key = lambda kv : ( kv [ 1 ] , kv [ 0 ] ) , reverse = True )
2021-04-30 19:35:32 +03:00
homeservers = ' , ' . join ( [ ' {} ( {} users, {:.1f} % ) ' . format ( hs [ 0 ] , hs [ 1 ] , 100.0 * hs [ 1 ] / usercount )
for hs in homeservers [ : 10 ] ] )
2021-04-28 21:13:21 +03:00
await bot . send_text ( room , f ' I \' m seeing { usercount } users in { roomcount } rooms. '
f ' Top ten homeservers (out of { hscount } ): { homeservers } ' )
2020-02-03 23:45:14 +02:00
async def status ( self , bot , room ) :
2021-04-28 21:10:51 +03:00
systime = time . time ( )
uptime = str ( timedelta ( seconds = ( systime - self . starttime ) ) ) . split ( ' . ' , 1 ) [ 0 ]
systime = time . ctime ( systime )
enabled = sum ( 1 for module in bot . modules . values ( ) if module . enabled )
return await bot . send_text ( room , f ' Uptime: { uptime } - System time: { systime } '
f ' - { enabled } modules enabled out of { len ( bot . modules ) } loaded. ' )
2020-02-03 23:45:14 +02:00
async def reload ( self , bot , room , event ) :
2020-11-24 23:02:47 +02:00
bot . must_be_owner ( event )
2021-05-18 00:21:13 +03:00
msg = await bot . send_text ( room , f ' Reloading modules... ' )
2020-02-03 23:45:14 +02:00
bot . stop ( )
bot . reload_modules ( )
bot . start ( )
2021-05-18 00:21:13 +03:00
# update event
content = {
' m.new_content ' : {
' msgtype ' : ' m.notice ' ,
' body ' : ' Modules reloaded! '
} ,
' m.relates_to ' : {
' rel_type ' : ' m.replace ' ,
' event_id ' : msg . event_id
} ,
' msgtype ' : ' m.notice ' ,
' body ' : ' Modules reloaded! '
}
await bot . client . room_send ( room . room_id , ' m.room.message ' , content )
2020-02-03 23:45:14 +02:00
async def version ( self , bot , room ) :
await bot . send_text ( room , f ' Hemppa version { bot . version } - https://github.com/vranki/hemppa ' )
async def quit ( self , bot , room , event ) :
2020-11-24 23:02:47 +02:00
bot . must_be_owner ( event )
2020-02-03 23:45:14 +02:00
await bot . send_text ( room , f ' Quitting, as requested ' )
2020-02-08 23:16:19 +02:00
self . logger . info ( f ' { event . sender } commanded bot to quit, so quitting.. ' )
2020-02-03 23:45:14 +02:00
bot . bot_task . cancel ( )
2020-02-06 01:19:45 +02:00
async def enable_module ( self , bot , room , event , module_name ) :
2020-11-24 23:02:47 +02:00
bot . must_be_owner ( event )
self . logger . info ( f " Asked to enable { module_name } " )
2020-02-06 01:19:45 +02:00
if bot . modules . get ( module_name ) :
module = bot . modules . get ( module_name )
module . enable ( )
module . matrix_start ( bot )
bot . save_settings ( )
2021-04-28 23:46:38 +03:00
return await bot . send_text ( room , f " Module { module_name } enabled " )
return await bot . send_text ( room , f " Module with name { module_name } not found. Execute !bot modules for a list of available modules " )
2020-02-06 01:19:45 +02:00
async def disable_module ( self , bot , room , event , module_name ) :
2020-11-24 23:02:47 +02:00
bot . must_be_owner ( event )
2020-02-08 23:16:19 +02:00
self . logger . info ( f " asked to disable { module_name } " )
2020-02-06 01:19:45 +02:00
if bot . modules . get ( module_name ) :
module = bot . modules . get ( module_name )
2021-04-28 23:46:38 +03:00
try :
2020-02-09 15:07:05 +02:00
module . disable ( )
2021-04-28 23:46:38 +03:00
except ModuleCannotBeDisabled :
return await bot . send_text ( room , f " Module { module_name } cannot be disabled. " )
except Exception as e :
return await bot . send_text ( room , f " Module { module_name } was not disabled: { repr ( e ) } " )
module . matrix_stop ( bot )
bot . save_settings ( )
return await bot . send_text ( room , f " Module { module_name } disabled " )
return await bot . send_text ( room , f " Module with name { module_name } not found. Execute !bot modules for a list of available modules " )
2020-02-06 01:19:45 +02:00
async def show_modules ( self , bot , room ) :
2021-01-23 22:11:42 +02:00
modules_message = " Modules: \n "
2020-02-09 15:24:57 +02:00
for modulename , module in collections . OrderedDict ( sorted ( bot . modules . items ( ) ) ) . items ( ) :
2020-02-10 01:04:34 +02:00
state = ' Enabled ' if module . enabled else ' Disabled '
2021-01-23 22:11:42 +02:00
modules_message + = f " { state } : { modulename } - { module . help ( ) } \n "
await bot . send_text ( room , modules_message )
2020-02-06 01:19:45 +02:00
2021-04-22 04:17:37 +03:00
async def export_settings ( self , bot , event , module_name = None ) :
bot . must_be_owner ( event )
data = bot . get_account_data ( ) [ ' module_settings ' ]
if module_name :
data = data [ module_name ]
self . logger . info ( f " { event . sender } is exporting settings for module { module_name } " )
else :
self . logger . info ( f " { event . sender } is exporting all settings " )
await bot . send_msg ( event . sender , f ' Private message from { bot . matrix_user } ' , json . dumps ( data ) )
async def import_settings ( self , bot , event ) :
bot . must_be_owner ( event )
self . logger . info ( f " { event . sender } is importing settings " )
try :
account_data = bot . get_account_data ( )
child = account_data [ ' module_settings ' ]
except KeyError : # no data yet
account_data [ ' module_settings ' ] = dict ( )
child = account_data [ ' module_settings ' ]
key = None
data = event . body . split ( None , 2 ) [ 2 ]
while not data . startswith ( ' { ' ) :
key , data = data . split ( None , 1 )
if child . get ( key ) :
child = child [ key ]
key = None
else :
break
data = json . loads ( data )
if not key :
child . update ( data )
else :
child [ key ] = data
bot . load_settings ( account_data )
bot . save_settings ( )
await bot . send_msg ( event . sender , f ' Private message from { bot . matrix_user } ' , ' Updated bot settings ' )
2021-05-09 03:17:02 +03:00
async def last_logs ( self , bot , room , event , target ) :
2021-05-04 18:55:49 +03:00
bot . must_be_owner ( event )
2021-05-09 03:17:02 +03:00
self . logger . info ( f ' { event . sender } asked for recent log messages. ' )
2021-05-07 00:09:17 +03:00
msg_room = await bot . find_or_create_private_msg ( event . sender , f ' Private message from { bot . matrix_user } ' )
if not msg_room or ( type ( msg_room ) is RoomCreateError ) :
# fallback to current room if we can't create one
msg_room = room
2021-05-09 03:17:02 +03:00
try :
target , count = target . split ( )
count = - abs ( int ( count ) )
except ValueError :
count = 0
keys = list ( self . loghandler . logs )
for key in [ target , f ' module { target } ' ] :
try :
logs = list ( self . loghandler . logs [ key ] )
2021-05-07 00:09:17 +03:00
break
2021-05-09 03:17:02 +03:00
except ( KeyError , TypeError ) :
pass
2021-05-07 00:09:17 +03:00
else :
2021-05-09 03:17:02 +03:00
return await bot . send_text ( msg_room , f ' Unknown module { target } , or no logs yet ' )
if count :
logs = logs [ count : ]
logs = ' \n ' . join ( [ self . loghandler . format ( record ) for record in logs ] )
2021-05-07 00:09:17 +03:00
2021-05-09 03:17:02 +03:00
return await bot . send_html ( msg_room , f ' <strong>Logs for { key } :</strong> \n <pre><code class= " language-txt " > { escape ( logs ) } </code></pre> ' , f ' Logs for { key } : \n ' + logs )
2021-05-04 18:55:49 +03:00
2021-05-11 20:52:50 +03:00
async def manage_uri_cache ( self , bot , room , event , action ) :
bot . must_be_owner ( event )
if action == ' view ' :
self . logger . info ( f " { event . sender } wants to see the uri cache " )
msg = [ f ' uri cache size: { len ( bot . uri_cache ) } ' ]
for key , val in bot . uri_cache . items ( ) :
msg . append ( ' - ' + key + ' : ' + val [ 0 ] )
return await bot . send_text ( room , ' \n ' . join ( msg ) )
if action in [ ' clean ' , ' clear ' ] :
self . logger . info ( f " { event . sender } wants to clear the uri cache " )
bot . uri_cache = dict ( )
bot . save_settings ( )
2021-05-19 23:01:04 +03:00
async def rooms ( self , bot , room , event ) :
bot . must_be_owner ( event )
rooms = [ ]
for croomid in bot . client . rooms :
roomobj = bot . get_room_by_id ( croomid )
rooms . append ( roomobj . machine_name )
await bot . send_text ( room , f ' I \' m in following { len ( rooms ) } rooms: { rooms } ' )
2021-04-28 23:46:38 +03:00
def disable ( self ) :
raise ModuleCannotBeDisabled
2020-01-05 00:28:57 +02:00
def help ( self ) :
2021-04-25 03:10:19 +03:00
return ' Bot management commands. (quit, version, reload, status, stats, leave, modules, enable, disable, import, export, ping) '
2021-04-12 22:39:23 +03:00
def long_help ( self , bot = None , event = None , * * kwargs ) :
text = self . help ( ) + (
' \n - " !bot version " : get bot version '
2021-04-25 03:10:19 +03:00
' \n - " !bot ping " : get the ping time to the server '
2021-04-12 22:39:23 +03:00
' \n - " !bot status " : get bot uptime and status '
' \n - " !bot stats " : get current users, rooms, and homeservers ' )
if bot and event and bot . is_owner ( event ) :
text + = ( ' \n - " !bot quit " : kill the bot :( '
' \n - " !bot reload " : reload the bot modules '
2021-05-12 07:12:40 +03:00
' \n - " !bot uricache (view|clean) " : view or clean the bot \' s URI cache '
' \n - " !bot logs [module] ([count]) " : get [count] most recent logs from [module] '
2021-04-12 22:39:23 +03:00
' \n - " !bot enable [module] " : enable a module '
2021-04-25 03:10:19 +03:00
' \n - " !bot disable [module] " : disable a module '
' \n - " !bot import ([module]) [json] " : import settings into the bot '
' \n - " !bot export ([module]) " : export settings from the bot '
)
2021-04-12 22:39:23 +03:00
return text