2019-12-09 20:38:25 +02:00
#!/usr/bin/env python3
2019-12-09 19:54:57 +02:00
import asyncio
2020-02-09 18:13:38 +02:00
import functools
2019-12-09 20:24:51 +02:00
import glob
import importlib
2020-02-19 23:06:50 +02:00
import json
2020-02-09 12:38:40 +02:00
import yaml
2020-01-02 14:27:29 +02:00
import os
import re
2020-02-09 18:13:38 +02:00
import signal
2020-01-02 14:27:29 +02:00
import sys
import traceback
2019-12-10 18:05:40 +02:00
import urllib . parse
2020-02-08 23:16:19 +02:00
import logging
2020-02-09 12:38:40 +02:00
import logging . config
2020-02-27 22:19:56 +02:00
import datetime
2020-02-02 23:08:15 +02:00
from importlib import reload
2021-04-21 11:06:58 +03:00
from io import BytesIO
from PIL import Image
2020-01-02 14:27:29 +02:00
import requests
2020-12-20 21:42:18 +02:00
from nio import AsyncClient , InviteEvent , JoinError , RoomMessageText , MatrixRoom , LoginError , RoomMemberEvent , RoomVisibility , RoomPreset , RoomCreateError , RoomResolveAliasResponse , UploadError , UploadResponse
2019-12-09 19:54:57 +02:00
2019-12-25 20:49:20 +02:00
# Couple of custom exceptions
2020-01-02 14:27:29 +02:00
2019-12-25 20:49:20 +02:00
class CommandRequiresAdmin ( Exception ) :
pass
2020-01-02 14:27:29 +02:00
2019-12-25 20:49:20 +02:00
class CommandRequiresOwner ( Exception ) :
pass
2019-12-09 19:54:57 +02:00
2020-01-02 14:27:29 +02:00
2019-12-09 19:54:57 +02:00
class Bot :
2020-02-08 23:16:19 +02:00
def __init__ ( self ) :
self . appid = ' org.vranki.hemppa '
2020-09-12 16:48:47 +03:00
self . version = ' 1.5 '
2020-02-08 23:16:19 +02:00
self . client = None
self . join_on_invite = False
self . modules = dict ( )
2021-03-30 06:05:10 +03:00
self . module_aliases = dict ( )
2021-01-23 21:41:24 +02:00
self . leave_empty_rooms = True
2021-05-11 20:08:53 +03:00
self . uri_cache = dict ( )
2020-02-08 23:16:19 +02:00
self . pollcount = 0
self . poll_task = None
self . owners = [ ]
2020-02-15 19:51:02 +02:00
self . debug = os . getenv ( " DEBUG " , " false " ) . lower ( ) == " true "
self . logger = None
2020-02-08 23:16:19 +02:00
2020-02-27 22:19:56 +02:00
self . jointime = None # HACKHACKHACK to avoid running old commands after join
self . join_hack_time = 5 # Seconds
2020-02-15 19:51:02 +02:00
self . initialize_logger ( )
2020-02-08 23:16:19 +02:00
2020-02-15 19:51:02 +02:00
def initialize_logger ( self ) :
2020-02-09 12:38:40 +02:00
if os . path . exists ( ' config/logging.yml ' ) :
with open ( ' config/logging.yml ' ) as f :
config = yaml . load ( f , Loader = yaml . Loader )
logging . config . dictConfig ( config )
else :
log_format = ' %(levelname)s - %(name)s - %(message)s '
logging . basicConfig ( format = log_format )
2020-02-15 19:51:02 +02:00
self . logger = logging . getLogger ( " hemppa " )
2020-02-08 23:16:19 +02:00
if self . debug :
logging . root . setLevel ( logging . DEBUG )
2020-02-15 19:51:02 +02:00
self . logger . info ( " enabled debugging " )
self . logger . debug ( " Logger initialized " )
2020-02-08 23:16:19 +02:00
2021-04-21 11:06:58 +03:00
async def upload_and_send_image ( self , room , url , text = None , blob = False , blob_content_type = " image/png " ) :
2021-04-22 18:57:55 +03:00
"""
: param room : A MatrixRoom the image should be send to after uploading
: param url : Url of binary content of the image to upload
: param text : A textual representation of the image
: param blob : Flag to indicate if the second param is an url or a binary content
: param blob_content_type : Content type of the image in case of binary content
: return :
"""
2021-05-11 20:08:53 +03:00
try :
matrix_uri , mimetype , w , h , size = self . uri_cache [ url ]
except KeyError :
res = await self . upload_image ( url , blob , blob_content_type )
matrix_uri , mimetype , w , h , size = res
if matrix_uri :
self . uri_cache [ url ] = list ( res )
self . save_settings ( )
else :
return await self . send_text ( room , " sorry. something went wrong uploading the image to matrix server :( " )
2021-04-21 11:06:58 +03:00
if not text and not blob :
text = f " { url } "
2021-05-11 20:08:53 +03:00
return await self . send_image ( room , matrix_uri , text , mimetype , w , h , size )
2021-04-21 11:06:58 +03:00
# Helper function to upload a image from URL to homeserver. Use send_image() to actually send it to room.
async def upload_image ( self , url , blob = False , blob_content_type = " image/png " ) :
2021-04-22 18:57:55 +03:00
"""
: param url : Url of binary content of the image to upload
: param blob : Flag to indicate if the first param is an url or a binary content
: param blob_content_type : Content type of the image in case of binary content
: return : A MXC - Uri https : / / matrix . org / docs / spec / client_server / r0 .6 .0 #mxc-uri, Content type, Width, Height, Image size in bytes
"""
2021-04-21 11:06:58 +03:00
self . client : AsyncClient
response : UploadResponse
if blob :
( response , alist ) = await self . client . upload ( lambda a , b : url , blob_content_type )
i = Image . open ( BytesIO ( url ) )
image_length = len ( url )
content_type = blob_content_type
else :
self . logger . debug ( f " start downloading image from url { url } " )
headers = { ' User-Agent ' : ' Mozilla/5.0 ' }
url_response = requests . get ( url , headers = headers )
self . logger . debug ( f " response [status_code= { url_response . status_code } , headers= { url_response . headers } " )
if url_response . status_code == 200 :
content_type = url_response . headers . get ( " content-type " )
self . logger . info ( f " uploading content to matrix server [size= { len ( url_response . content ) } , content-type: { content_type } ] " )
( response , alist ) = await self . client . upload ( lambda a , b : url_response . content , content_type )
self . logger . debug ( " response: %s " , response )
i = Image . open ( BytesIO ( url_response . content ) )
image_length = len ( url_response . content )
else :
self . logger . error ( " unable to request url: %s " , url_response )
return None , None , None , None
if isinstance ( response , UploadResponse ) :
self . logger . info ( " uploaded file to %s " , response . content_uri )
return response . content_uri , content_type , i . size [ 0 ] , i . size [ 1 ] , image_length
else :
response : UploadError
self . logger . error ( " unable to upload file. msg: %s " , response . message )
return None , None , None , None
2020-03-29 20:34:57 +03:00
async def send_text ( self , room , body , msgtype = " m.notice " , bot_ignore = False ) :
2021-04-22 18:57:55 +03:00
"""
: param room : A MatrixRoom the text should be send to
: param body : Textual content of the message
: param msgtype : The message type for the room https : / / matrix . org / docs / spec / client_server / latest #m-room-message-msgtypes
: param bot_ignore : Flag to mark the message to be ignored by the bot
: return :
"""
2019-12-09 19:54:57 +02:00
msg = {
" body " : body ,
2020-03-29 20:34:57 +03:00
" msgtype " : msgtype ,
2019-12-09 19:54:57 +02:00
}
2020-03-29 20:34:57 +03:00
if bot_ignore :
msg [ " org.vranki.hemppa.ignore " ] = " true "
2019-12-10 23:43:08 +02:00
await self . client . room_send ( room . room_id , ' m.room.message ' , msg )
2019-12-09 19:54:57 +02:00
2020-03-29 20:34:57 +03:00
async def send_html ( self , room , html , plaintext , msgtype = " m.notice " , bot_ignore = False ) :
2021-04-22 18:57:55 +03:00
"""
: param room : A MatrixRoom the html should be send to
: param html : Html content of the message
: param plaintext : Plaintext content of the message
: param msgtype : The message type for the room https : / / matrix . org / docs / spec / client_server / latest #m-room-message-msgtypes
: param bot_ignore : Flag to mark the message to be ignored by the bot
: return :
"""
2019-12-09 22:30:06 +02:00
msg = {
2020-03-17 19:40:41 +02:00
" msgtype " : msgtype ,
2019-12-09 22:30:06 +02:00
" format " : " org.matrix.custom.html " ,
2019-12-10 18:05:40 +02:00
" formatted_body " : html ,
" body " : plaintext
2019-12-09 22:30:06 +02:00
}
2020-03-29 20:34:57 +03:00
if bot_ignore :
msg [ " org.vranki.hemppa.ignore " ] = " true "
2019-12-10 23:43:08 +02:00
await self . client . room_send ( room . room_id , ' m.room.message ' , msg )
2020-03-01 21:40:31 +02:00
2021-04-25 03:00:52 +03:00
async def send_location ( self , room , body , latitude , longitude , bot_ignore = False ) :
"""
: param room : A MatrixRoom the html should be send to
: param html : Html content of the message
: param body : Plaintext content of the message
: param latitude : Latitude in WGS84 coordinates ( float )
: param longitude : Longitude in WGS84 coordinates ( float )
: param bot_ignore : Flag to mark the message to be ignored by the bot
: return :
"""
locationmsg = {
" body " : str ( body ) ,
" geo_uri " : ' geo: ' + str ( latitude ) + ' , ' + str ( longitude ) ,
" msgtype " : " m.location " ,
}
await self . client . room_send ( room . room_id , ' m.room.message ' , locationmsg )
2021-04-21 11:06:58 +03:00
async def send_image ( self , room , url , body , mimetype = None , width = None , height = None , size = None ) :
2020-03-01 21:40:31 +02:00
"""
: param room : A MatrixRoom the image should be send to
: param url : A MXC - Uri https : / / matrix . org / docs / spec / client_server / r0 .6 .0 #mxc-uri
: param body : A textual representation of the image
2021-04-22 18:57:55 +03:00
: param mimetype : The mimetype of the image
: param width : Width in pixel of the image
: param height : Height in pixel of the image
: param size : Size in bytes of the image
2020-03-01 21:40:31 +02:00
: return :
"""
msg = {
" url " : url ,
" body " : body ,
2021-04-21 11:06:58 +03:00
" msgtype " : " m.image " ,
" info " : {
" thumbnail_info " : None ,
" thumbnail_url " : None ,
} ,
2020-03-01 21:40:31 +02:00
}
2021-04-21 11:06:58 +03:00
if mimetype :
msg [ " info " ] [ " mimetype " ] = mimetype
if width :
msg [ " info " ] [ " w " ] = width
if height :
msg [ " info " ] [ " h " ] = height
if size :
msg [ " info " ] [ " size " ] = size
2021-05-11 20:08:53 +03:00
return await self . client . room_send ( room . room_id , ' m.room.message ' , msg )
2019-12-09 19:54:57 +02:00
2020-03-29 23:50:33 +03:00
async def send_msg ( self , mxid , roomname , message ) :
2021-04-22 18:57:55 +03:00
"""
: param mxid : A Matrix user id to send the message to
: param roomname : A Matrix room id to send the message to
: param message : Text to be sent as message
: return bool : Success upon sending the message
"""
2020-03-29 23:50:33 +03:00
# Sends private message to user. Returns true on success.
2021-05-07 00:08:49 +03:00
msg_room = await self . find_or_create_private_msg ( mxid , roomname )
if not msg_room or ( type ( msg_room ) is RoomCreateError ) :
self . logger . error ( f ' Unable to create room when trying to message { mxid } ' )
return False
# Send message to the room
await self . send_text ( msg_room , message )
return True
2020-03-29 23:50:33 +03:00
2021-05-07 00:08:49 +03:00
async def find_or_create_private_msg ( self , mxid , roomname ) :
2020-03-29 23:50:33 +03:00
# Find if we already have a common room with user:
msg_room = None
for croomid in self . client . rooms :
roomobj = self . client . rooms [ croomid ]
if len ( roomobj . users ) == 2 :
for user in roomobj . users :
if user == mxid :
msg_room = roomobj
# Nope, let's create one
if not msg_room :
msg_room = await self . client . room_create ( visibility = RoomVisibility . private ,
name = roomname ,
is_direct = True ,
preset = RoomPreset . private_chat ,
invite = { mxid } ,
2021-05-07 00:08:49 +03:00
)
return msg_room
2020-03-29 23:50:33 +03:00
2020-02-06 21:56:53 +02:00
def remove_callback ( self , callback ) :
2020-02-09 18:13:38 +02:00
for cb_object in self . client . event_callbacks :
2020-02-06 21:56:53 +02:00
if cb_object . func == callback :
2020-02-09 12:38:40 +02:00
self . logger . info ( " remove callback " )
2020-02-09 18:13:38 +02:00
self . client . event_callbacks . remove ( cb_object )
2020-02-06 21:56:53 +02:00
2019-12-10 18:05:40 +02:00
def get_room_by_id ( self , room_id ) :
2020-11-04 21:53:23 +02:00
try :
return self . client . rooms [ room_id ]
except KeyError :
return None
2019-12-10 18:05:40 +02:00
2020-09-12 16:48:47 +03:00
async def get_room_by_alias ( self , alias ) :
rar = await self . client . room_resolve_alias ( alias )
if type ( rar ) is RoomResolveAliasResponse :
return rar . room_id
return None
2019-12-25 20:49:20 +02:00
# Throws exception if event sender is not a room admin
2021-03-29 23:34:41 +03:00
def must_be_admin ( self , room , event , power_level = 50 ) :
if not self . is_admin ( room , event , power_level = power_level ) :
2019-12-25 20:49:20 +02:00
raise CommandRequiresAdmin
# Throws exception if event sender is not a bot owner
def must_be_owner ( self , event ) :
if not self . is_owner ( event ) :
raise CommandRequiresOwner
2020-09-12 18:13:22 +03:00
# Returns true if event's sender has PL50 or more in the room event was sent in,
2020-01-02 14:27:29 +02:00
# or is bot owner
2021-03-29 23:34:41 +03:00
def is_admin ( self , room , event , power_level = 50 ) :
2019-12-30 22:40:55 +02:00
if self . is_owner ( event ) :
return True
2020-01-02 14:27:29 +02:00
if event . sender not in room . power_levels . users :
2019-12-25 20:49:20 +02:00
return False
2021-03-29 23:34:41 +03:00
return room . power_levels . users [ event . sender ] > = power_level
2019-12-25 20:49:20 +02:00
# Returns true if event's sender is owner of the bot
def is_owner ( self , event ) :
return event . sender in self . owners
2020-03-29 20:34:57 +03:00
# Checks if this event should be ignored by bot, including custom property
def should_ignore_event ( self , event ) :
return " org.vranki.hemppa.ignore " in event . source [ ' content ' ]
2019-12-10 18:05:40 +02:00
def save_settings ( self ) :
module_settings = dict ( )
for modulename , moduleobject in self . modules . items ( ) :
2020-02-06 21:56:53 +02:00
try :
module_settings [ modulename ] = moduleobject . get_settings ( )
except Exception :
2021-05-05 07:56:06 +03:00
self . logger . exception ( f ' unhandled exception { modulename } .get_settings ' )
2021-05-11 20:08:53 +03:00
data = { self . appid : self . version , ' module_settings ' : module_settings , ' uri_cache ' : self . uri_cache }
2019-12-10 18:05:40 +02:00
self . set_account_data ( data )
def load_settings ( self , data ) :
2019-12-10 22:09:21 +02:00
if not data :
return
2019-12-10 18:05:40 +02:00
if not data . get ( ' module_settings ' ) :
return
2021-05-11 20:08:53 +03:00
if data . get ( ' uri_cache ' ) :
self . uri_cache = data [ ' uri_cache ' ]
2019-12-10 18:05:40 +02:00
for modulename , moduleobject in self . modules . items ( ) :
if data [ ' module_settings ' ] . get ( modulename ) :
2020-02-06 21:56:53 +02:00
try :
moduleobject . set_settings (
data [ ' module_settings ' ] [ modulename ] )
except Exception :
2021-05-05 07:56:06 +03:00
self . logger . exception ( f ' unhandled exception { modulename } .set_settings ' )
2019-12-10 18:05:40 +02:00
2019-12-09 19:54:57 +02:00
async def message_cb ( self , room , event ) :
2020-03-29 20:34:57 +03:00
# Ignore if asked to ignore
if self . should_ignore_event ( event ) :
2020-09-12 16:48:47 +03:00
if self . debug :
2020-11-04 21:53:23 +02:00
self . logger . debug ( ' Ignoring event! ' )
2020-03-29 20:34:57 +03:00
return
2019-12-09 20:24:51 +02:00
body = event . body
2020-03-29 20:34:57 +03:00
# Figure out the command
2020-02-15 12:26:19 +02:00
if not self . starts_with_command ( body ) :
2020-01-10 21:59:59 +02:00
return
2020-03-17 19:35:33 +02:00
if self . owners_only and not self . is_owner ( event ) :
self . logger . info ( f " Ignoring { event . sender } , because they ' re not an owner " )
await self . send_text ( room , " Sorry, only bot owner can run commands. " )
return
2020-02-27 22:19:56 +02:00
# HACK to ignore messages for some time after joining.
if self . jointime :
if ( datetime . datetime . now ( ) - self . jointime ) . seconds < self . join_hack_time :
self . logger . info ( f " Waiting for join delay, ignoring message: { body } " )
return
self . jointime = None
2019-12-09 20:24:51 +02:00
command = body . split ( ) . pop ( 0 )
# Strip away non-alphanumeric characters, including leading ! for security
command = re . sub ( r ' \ W+ ' , ' ' , command )
2021-03-30 06:05:10 +03:00
# Fallback to any declared aliases
moduleobject = self . modules . get ( command ) or self . modules . get ( self . module_aliases . get ( command ) )
2019-12-09 20:24:51 +02:00
2020-02-07 18:54:36 +02:00
if moduleobject is not None :
if moduleobject . enabled :
try :
2020-02-09 18:13:38 +02:00
await moduleobject . matrix_message ( self , room , event )
2020-02-07 18:54:36 +02:00
except CommandRequiresAdmin :
await self . send_text ( room , f ' Sorry, you need admin power level in this room to run that command. ' )
except CommandRequiresOwner :
await self . send_text ( room , f ' Sorry, only bot owner can run that command. ' )
except Exception :
2020-02-08 23:16:19 +02:00
await self . send_text ( room , f ' Module { command } experienced difficulty: { sys . exc_info ( ) [ 0 ] } - see log for details ' )
2021-05-05 07:56:06 +03:00
self . logger . exception ( f ' unhandled exception in ! { command } ' )
2020-02-07 18:54:36 +02:00
else :
2020-02-08 23:16:19 +02:00
self . logger . error ( f " Unknown command: { command } " )
2020-02-16 14:49:00 +02:00
# TODO Make this configurable
# await self.send_text(room,
# f"Sorry. I don't know what to do. Execute !help to get a list of available commands.")
2019-12-09 19:54:57 +02:00
2020-02-13 23:50:23 +02:00
@staticmethod
def starts_with_command ( body ) :
2020-02-15 12:26:19 +02:00
""" Checks if body starts with ! and has one or more letters after it """
return re . match ( r " ^! \ w.* " , body ) is not None
2020-02-13 23:50:23 +02:00
2019-12-09 19:54:57 +02:00
async def invite_cb ( self , room , event ) :
2020-02-04 20:22:11 +02:00
room : MatrixRoom
event : InviteEvent
2019-12-29 17:26:47 +02:00
if self . join_on_invite or self . is_owner ( event ) :
for attempt in range ( 3 ) :
2020-02-27 22:19:56 +02:00
self . jointime = datetime . datetime . now ( )
2019-12-29 17:26:47 +02:00
result = await self . client . join ( room . room_id )
if type ( result ) == JoinError :
2020-02-08 23:16:19 +02:00
self . logger . error ( f " Error joining room %s (attempt %d): %s " , room . room_id , attempt , result . message )
2019-12-29 17:26:47 +02:00
else :
2020-02-08 23:16:19 +02:00
self . logger . info ( f " joining room ' { room . display_name } ' ( { room . room_id } ) invited by ' { event . sender } ' " )
2020-02-27 22:19:56 +02:00
return
2019-12-29 17:26:47 +02:00
else :
2020-02-08 23:16:19 +02:00
self . logger . warning ( f ' Received invite event, but not joining as sender is not owner or bot not configured to join on invite. { event } ' )
2019-12-09 19:54:57 +02:00
2020-03-29 23:50:33 +03:00
async def memberevent_cb ( self , room , event ) :
# Automatically leaves rooms where bot is alone.
if room . member_count == 1 and event . membership == ' leave ' :
self . logger . info ( f " membership event in { room . display_name } ( { room . room_id } ) with { room . member_count } members by ' { event . sender } ' - leaving room as i don ' t want to be left alone! " )
await self . client . room_leave ( room . room_id )
2019-12-09 20:24:51 +02:00
def load_module ( self , modulename ) :
try :
2020-02-08 23:16:19 +02:00
self . logger . info ( f ' load module: { modulename } ' )
2019-12-09 20:24:51 +02:00
module = importlib . import_module ( ' modules. ' + modulename )
2020-01-06 00:33:42 +02:00
module = reload ( module )
2019-12-09 20:24:51 +02:00
cls = getattr ( module , ' MatrixModule ' )
2020-02-06 21:56:53 +02:00
return cls ( modulename )
2021-04-25 03:00:52 +03:00
except Exception :
2021-05-05 07:56:06 +03:00
self . logger . exception ( f ' Module { modulename } failed to load ' )
2019-12-09 20:24:51 +02:00
return None
2020-01-10 21:59:59 +02:00
def reload_modules ( self ) :
2020-02-19 23:06:50 +02:00
for modulename in self . modules :
2020-02-08 23:16:19 +02:00
self . logger . info ( f ' Reloading { modulename } .. ' )
2020-01-10 21:59:59 +02:00
self . modules [ modulename ] = self . load_module ( modulename )
self . load_settings ( self . get_account_data ( ) )
2020-01-06 00:33:42 +02:00
2019-12-09 20:24:51 +02:00
def get_modules ( self ) :
modulefiles = glob . glob ( ' ./modules/*.py ' )
for modulefile in modulefiles :
modulename = os . path . splitext ( os . path . basename ( modulefile ) ) [ 0 ]
moduleobject = self . load_module ( modulename )
if moduleobject :
self . modules [ modulename ] = moduleobject
2020-02-02 23:08:15 +02:00
2020-01-06 00:33:42 +02:00
def clear_modules ( self ) :
self . modules = dict ( )
2019-12-09 20:24:51 +02:00
2019-12-09 22:30:06 +02:00
async def poll_timer ( self ) :
while True :
self . pollcount = self . pollcount + 1
for modulename , moduleobject in self . modules . items ( ) :
2020-02-06 01:19:45 +02:00
if moduleobject . enabled :
2020-02-06 21:56:53 +02:00
try :
2020-02-09 18:13:38 +02:00
await moduleobject . matrix_poll ( self , self . pollcount )
2020-02-06 21:56:53 +02:00
except Exception :
2021-05-05 07:56:06 +03:00
self . logger . exception ( f ' unhandled exception from { modulename } .matrix_poll ' )
2019-12-09 22:30:06 +02:00
await asyncio . sleep ( 10 )
2019-12-10 18:05:40 +02:00
def set_account_data ( self , data ) :
2020-02-05 01:48:19 +02:00
userid = urllib . parse . quote ( self . matrix_user )
2019-12-10 18:05:40 +02:00
ad_url = f " { self . client . homeserver } /_matrix/client/r0/user/ { userid } /account_data/ { self . appid } ?access_token= { self . client . access_token } "
response = requests . put ( ad_url , json . dumps ( data ) )
2020-02-05 01:48:19 +02:00
self . __handle_error_response ( response )
2019-12-10 18:05:40 +02:00
if response . status_code != 200 :
2020-02-08 23:16:19 +02:00
self . logger . error ( ' Setting account data failed. response: %s json: %s ' , response , response . json ( ) )
2019-12-10 18:05:40 +02:00
def get_account_data ( self ) :
2020-02-05 01:48:19 +02:00
userid = urllib . parse . quote ( self . matrix_user )
2019-12-10 18:05:40 +02:00
ad_url = f " { self . client . homeserver } /_matrix/client/r0/user/ { userid } /account_data/ { self . appid } ?access_token= { self . client . access_token } "
response = requests . get ( ad_url )
2020-02-05 01:48:19 +02:00
self . __handle_error_response ( response )
2019-12-10 18:05:40 +02:00
if response . status_code == 200 :
return response . json ( )
2020-02-08 23:16:19 +02:00
self . logger . error ( f ' Getting account data failed: { response } { response . json ( ) } - this is normal if you have not saved any settings yet. ' )
2019-12-10 19:03:19 +02:00
return None
2019-12-10 18:05:40 +02:00
2020-02-05 01:48:19 +02:00
def __handle_error_response ( self , response ) :
if response . status_code == 401 :
2020-02-08 23:16:19 +02:00
self . logger . error ( " access token is invalid or missing " )
2020-03-10 20:11:19 +02:00
self . logger . info ( " NOTE: check MATRIX_ACCESS_TOKEN " )
2020-02-05 01:48:19 +02:00
sys . exit ( 2 )
2019-12-09 19:54:57 +02:00
def init ( self ) :
2020-02-05 01:48:19 +02:00
2020-02-06 21:56:53 +02:00
self . matrix_user = os . getenv ( ' MATRIX_USER ' )
matrix_server = os . getenv ( ' MATRIX_SERVER ' )
bot_owners = os . getenv ( ' BOT_OWNERS ' )
access_token = os . getenv ( ' MATRIX_ACCESS_TOKEN ' )
join_on_invite = os . getenv ( ' JOIN_ON_INVITE ' )
2020-03-17 19:35:33 +02:00
owners_only = os . getenv ( ' OWNERS_ONLY ' ) is not None
2021-01-23 21:41:24 +02:00
leave_empty_rooms = os . getenv ( ' LEAVE_EMPTY_ROOMS ' )
2020-02-05 01:48:19 +02:00
2020-03-10 20:11:19 +02:00
if matrix_server and self . matrix_user and bot_owners and access_token :
2020-11-15 21:03:57 +02:00
self . client = AsyncClient ( matrix_server , self . matrix_user , ssl = matrix_server . startswith ( " https:// " ) )
2020-02-08 12:02:58 +02:00
self . client . access_token = access_token
2021-01-23 21:23:48 +02:00
self . join_on_invite = ( join_on_invite or ' ' ) . lower ( ) == ' true '
2021-01-23 21:41:24 +02:00
self . leave_empty_rooms = ( leave_empty_rooms or ' true ' ) . lower ( ) == ' true '
2020-02-05 01:48:19 +02:00
self . owners = bot_owners . split ( ' , ' )
2020-03-17 19:35:33 +02:00
self . owners_only = owners_only
2020-02-05 01:48:19 +02:00
self . get_modules ( )
2020-02-06 01:19:45 +02:00
2020-02-05 01:48:19 +02:00
else :
2020-03-10 20:11:19 +02:00
self . logger . error ( " The environment variables MATRIX_SERVER, MATRIX_USER, MATRIX_ACCESS_TOKEN and BOT_OWNERS are mandatory " )
2020-02-05 01:48:19 +02:00
sys . exit ( 1 )
2020-01-06 00:33:42 +02:00
def start ( self ) :
2020-02-06 01:19:45 +02:00
self . load_settings ( self . get_account_data ( ) )
enabled_modules = [ module for module_name , module in self . modules . items ( ) if module . enabled ]
2020-02-08 23:16:19 +02:00
self . logger . info ( f ' Starting { len ( enabled_modules ) } modules.. ' )
2020-01-06 00:33:42 +02:00
for modulename , moduleobject in self . modules . items ( ) :
2020-02-06 01:19:45 +02:00
if moduleobject . enabled :
2020-02-06 21:56:53 +02:00
try :
2020-02-09 18:13:38 +02:00
moduleobject . matrix_start ( self )
2020-02-06 21:56:53 +02:00
except Exception :
2021-05-05 07:56:06 +03:00
self . logger . exception ( f ' unhandled exception from { modulename } .matrix_start ' )
2020-11-23 00:19:23 +02:00
self . logger . info ( f ' All modules started. ' )
2020-01-06 00:33:42 +02:00
2019-12-09 20:24:51 +02:00
def stop ( self ) :
2020-02-08 23:16:19 +02:00
self . logger . info ( f ' Stopping { len ( self . modules ) } modules.. ' )
2019-12-09 20:24:51 +02:00
for modulename , moduleobject in self . modules . items ( ) :
2020-02-06 21:56:53 +02:00
try :
2020-02-09 18:13:38 +02:00
moduleobject . matrix_stop ( self )
2020-02-06 21:56:53 +02:00
except Exception :
2021-05-05 07:56:06 +03:00
self . logger . exception ( f ' unhandled exception from { modulename } .matrix_stop ' )
2020-11-23 00:19:23 +02:00
self . logger . info ( f ' All modules stopped. ' )
2019-12-09 20:24:51 +02:00
2019-12-09 19:54:57 +02:00
async def run ( self ) :
await self . client . sync ( )
2020-02-04 20:22:11 +02:00
for roomid , room in self . client . rooms . items ( ) :
2020-02-08 23:16:19 +02:00
self . logger . info ( f " Bot is on ' { room . display_name } ' ( { roomid } ) with { len ( room . users ) } users " )
2021-01-23 21:41:24 +02:00
if len ( room . users ) == 1 and self . leave_empty_rooms :
2020-02-08 23:16:19 +02:00
self . logger . info ( f ' Room { roomid } has no other users - leaving it. ' )
self . logger . info ( await self . client . room_leave ( roomid ) )
2019-12-11 00:04:24 +02:00
2020-01-06 00:33:42 +02:00
self . start ( )
2019-12-11 00:04:24 +02:00
2019-12-09 22:30:06 +02:00
self . poll_task = asyncio . get_event_loop ( ) . create_task ( self . poll_timer ( ) )
2019-12-09 19:54:57 +02:00
if self . client . logged_in :
2019-12-10 18:05:40 +02:00
self . load_settings ( self . get_account_data ( ) )
2019-12-09 19:54:57 +02:00
self . client . add_event_callback ( self . message_cb , RoomMessageText )
2019-12-26 00:21:32 +02:00
self . client . add_event_callback ( self . invite_cb , ( InviteEvent , ) )
2020-03-29 23:50:33 +03:00
self . client . add_event_callback ( self . memberevent_cb , ( RoomMemberEvent , ) )
2019-12-26 00:21:32 +02:00
2019-12-09 19:54:57 +02:00
if self . join_on_invite :
2020-02-08 23:16:19 +02:00
self . logger . info ( ' Note: Bot will join rooms if invited ' )
self . logger . info ( ' Bot running as %s , owners %s ' , self . client . user , self . owners )
2020-01-05 00:28:57 +02:00
self . bot_task = asyncio . create_task ( self . client . sync_forever ( timeout = 30000 ) )
await self . bot_task
2019-12-09 19:54:57 +02:00
else :
2020-02-08 23:16:19 +02:00
self . logger . error ( ' Client was not able to log in, check env variables! ' )
2019-12-09 19:54:57 +02:00
2020-02-04 20:22:11 +02:00
async def shutdown ( self ) :
2020-02-23 22:13:02 +02:00
await self . close ( )
2020-02-04 20:22:11 +02:00
2020-02-23 22:13:02 +02:00
async def close ( self ) :
try :
await self . client . close ( )
self . logger . info ( " Connection closed " )
except Exception as ex :
self . logger . error ( " error while closing client: %s " , ex )
2020-02-04 20:22:11 +02:00
2020-02-09 18:13:38 +02:00
def handle_exit ( self , signame , loop ) :
self . logger . info ( f " Received signal { signame } " )
if self . poll_task :
self . poll_task . cancel ( )
self . bot_task . cancel ( )
self . stop ( )
async def main ( ) :
bot = Bot ( )
bot . init ( )
loop = asyncio . get_running_loop ( )
for signame in { ' SIGINT ' , ' SIGTERM ' } :
loop . add_signal_handler (
getattr ( signal , signame ) ,
functools . partial ( bot . handle_exit , signame , loop ) )
await bot . run ( )
await bot . shutdown ( )
2020-02-08 12:02:58 +02:00
2019-12-09 20:24:51 +02:00
try :
2020-02-09 18:13:38 +02:00
asyncio . run ( main ( ) )
except Exception as e :
2020-11-04 21:53:23 +02:00
traceback . print_exc ( file = sys . stderr )