|
|
|
|
@@ -10,8 +10,12 @@ from pprint import pprint
|
|
|
|
|
from nio import AsyncClient, MatrixRoom, RoomMessageText
|
|
|
|
|
from nio.exceptions import OlmUnverifiedDeviceError
|
|
|
|
|
import asyncio
|
|
|
|
|
import urllib.request
|
|
|
|
|
import urllib.parse
|
|
|
|
|
import urllib.error
|
|
|
|
|
import base64
|
|
|
|
|
|
|
|
|
|
__version__ = "3.0.0"
|
|
|
|
|
__version__ = "2.5.0"
|
|
|
|
|
versionstring='Taurix TellMe server v' + __version__
|
|
|
|
|
|
|
|
|
|
log_dir = '/var/log/tellme'
|
|
|
|
|
@@ -24,25 +28,41 @@ logging.basicConfig(
|
|
|
|
|
)
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
hooks = {}
|
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
|
|
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
|
# context = zmq.Context()
|
|
|
|
|
# socket = context.socket(zmq.REQ)
|
|
|
|
|
# socket.connect("tcp://localhost:5555")
|
|
|
|
|
|
|
|
|
|
hooks = {}
|
|
|
|
|
config = {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def read_configs():
|
|
|
|
|
global hooks, config
|
|
|
|
|
|
|
|
|
|
print("DEBUG: read_configs() called") # Temporary debug
|
|
|
|
|
log.info("read_configs() called")
|
|
|
|
|
|
|
|
|
|
if not os.path.isfile('/etc/tellme/hooks.yml'):
|
|
|
|
|
log.error("hooks.yml not found at /etc/tellme/hooks.yml")
|
|
|
|
|
else:
|
|
|
|
|
try:
|
|
|
|
|
with open(r'/etc/tellme/hooks.yml') as hooksfile:
|
|
|
|
|
hooks = yaml.load(hooksfile, Loader=yaml.FullLoader)
|
|
|
|
|
loaded = yaml.load(hooksfile, Loader=yaml.FullLoader)
|
|
|
|
|
hooks = {str(k): v for k, v in loaded.items()} if loaded else {}
|
|
|
|
|
log.info("Loaded hooks: %s" % (list(hooks.keys()) if hooks else 'empty'))
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.error("Failed to load hooks.yml: %s" % (e))
|
|
|
|
|
|
|
|
|
|
if os.path.isfile('/etc/tellme/config.yml'):
|
|
|
|
|
try:
|
|
|
|
|
with open(r'/etc/tellme/config.yml') as configfile:
|
|
|
|
|
config = yaml.load(configfile, Loader=yaml.FullLoader)
|
|
|
|
|
log.info("Loaded config")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.error("Failed to load config.yml: %s" % (e))
|
|
|
|
|
else:
|
|
|
|
|
log.error("config.yml not found at /etc/tellme/config.yml")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_smp_message(target, message):
|
|
|
|
|
@@ -58,19 +78,40 @@ def send_smp_message(target, message):
|
|
|
|
|
json_command = json.dumps(command)
|
|
|
|
|
|
|
|
|
|
uri = "ws://localhost:5080"
|
|
|
|
|
try:
|
|
|
|
|
ws = websocket.create_connection(uri)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.error("Failed to connect to SimpleX WebSocket at %s: %s" % (uri, e))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
log.info("Sending to SimpleX WebSocket: %s" % (json_command))
|
|
|
|
|
ws.send(json_command) # Send message to WebSocket
|
|
|
|
|
responsejson = ws.recv() # Receive response
|
|
|
|
|
log.info("SimpleX raw response: %s" % (responsejson))
|
|
|
|
|
response = json.loads(responsejson)
|
|
|
|
|
ws.close()
|
|
|
|
|
|
|
|
|
|
if response is not None:
|
|
|
|
|
log.info("Sent message to SimpleX with %s" % (response))
|
|
|
|
|
if response and isinstance(response, dict):
|
|
|
|
|
resp = response.get('resp', {})
|
|
|
|
|
if isinstance(resp, dict) and resp.get('type') == 'subscriptionStatus':
|
|
|
|
|
log.warning("SimpleX response indicates subscription status, not message delivery: %s" % (response))
|
|
|
|
|
elif resp and resp.get('type') == 'sent':
|
|
|
|
|
log.info("SimpleX message sent successfully")
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
log.error("Failed to send message to SimpleX with %s" % (response))
|
|
|
|
|
log.info("SimpleX response: %s" % (response))
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
log.error("Unexpected SimpleX response format: %s" % (response))
|
|
|
|
|
return False
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.error("Error sending SimpleX message: %s" % (e))
|
|
|
|
|
return False
|
|
|
|
|
finally:
|
|
|
|
|
try:
|
|
|
|
|
ws.close()
|
|
|
|
|
except:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def matrix_login_and_send(homeserver, access_token, user_id, target, message):
|
|
|
|
|
@@ -79,31 +120,52 @@ async def matrix_login_and_send(homeserver, access_token, user_id, target, messa
|
|
|
|
|
|
|
|
|
|
invited_rooms = []
|
|
|
|
|
|
|
|
|
|
async def auto_join_callback(room: MatrixRoom, event: RoomMessageText, client: AsyncClient):
|
|
|
|
|
async def auto_join_callback(room: MatrixRoom, event: RoomMessageText):
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
async def invited_callback(room: MatrixRoom, event: RoomMessageText, client: AsyncClient):
|
|
|
|
|
invited_rooms.append(room.room_id)
|
|
|
|
|
|
|
|
|
|
client.add_event_callback(auto_join_callback, RoomMessageText)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
response = await client.sync(full_state=True)
|
|
|
|
|
await client.sync(full_state=True)
|
|
|
|
|
for room_id, invite_state in client.invited_rooms.items():
|
|
|
|
|
await client.join(room_id)
|
|
|
|
|
invited_rooms.append(room_id)
|
|
|
|
|
|
|
|
|
|
if target not in client.rooms and target not in invited_rooms:
|
|
|
|
|
await client.join(target)
|
|
|
|
|
room = None
|
|
|
|
|
room_id = None
|
|
|
|
|
|
|
|
|
|
room = client.rooms.get(target)
|
|
|
|
|
if room:
|
|
|
|
|
if target.startswith('#'):
|
|
|
|
|
for room_obj in client.rooms.values():
|
|
|
|
|
if hasattr(room_obj, 'canonical_alias') and room_obj.canonical_alias == target:
|
|
|
|
|
room = room_obj
|
|
|
|
|
room_id = room_obj.room_id
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
if not room or not room_id:
|
|
|
|
|
try:
|
|
|
|
|
join_response = await client.join(target)
|
|
|
|
|
log.info("Join response: %s" % (join_response))
|
|
|
|
|
if hasattr(join_response, 'room_id'):
|
|
|
|
|
room_id = join_response.room_id
|
|
|
|
|
await client.sync(full_state=True)
|
|
|
|
|
room = client.rooms.get(room_id)
|
|
|
|
|
elif hasattr(join_response, 'room'):
|
|
|
|
|
room_id = join_response.room.room_id
|
|
|
|
|
room = join_response.room
|
|
|
|
|
except Exception as join_error:
|
|
|
|
|
log.error("Join failed for %s: %s" % (target, join_error))
|
|
|
|
|
|
|
|
|
|
if not room_id:
|
|
|
|
|
log.error("Could not get room_id for %s" % (target))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if room_id:
|
|
|
|
|
await client.room_send(
|
|
|
|
|
room_id=target,
|
|
|
|
|
room_id=room_id,
|
|
|
|
|
message_type="m.room.message",
|
|
|
|
|
content={"msgtype": "m.text", "body": message}
|
|
|
|
|
)
|
|
|
|
|
log.info("Sent message to Matrix room %s" % (target))
|
|
|
|
|
log.info("Sent message to Matrix room %s (room_id: %s)" % (target, room_id))
|
|
|
|
|
return True
|
|
|
|
|
else:
|
|
|
|
|
log.error("Could not join Matrix room %s" % (target))
|
|
|
|
|
@@ -125,6 +187,13 @@ def send_matrix_message(target, message):
|
|
|
|
|
log.error("Matrix credentials not configured")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
return asyncio.run(
|
|
|
|
|
matrix_login_and_send(homeserver, access_token, user_id, target, message)
|
|
|
|
|
)
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.error("Failed to send message to Matrix: %s" % (e))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
return asyncio.get_event_loop().run_until_complete(
|
|
|
|
|
matrix_login_and_send(homeserver, access_token, user_id, target, message)
|
|
|
|
|
)
|
|
|
|
|
@@ -133,8 +202,43 @@ def send_matrix_message(target, message):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def send_nextcloud_message(target, message):
|
|
|
|
|
try:
|
|
|
|
|
server = config.get('nextcloud_server')
|
|
|
|
|
username = config.get('nextcloud_username')
|
|
|
|
|
password = config.get('nextcloud_password')
|
|
|
|
|
|
|
|
|
|
if not server or not username or not password:
|
|
|
|
|
log.error("Nextcloud Talk credentials not configured")
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
url = f"{server.rstrip('/')}/ocs/v2.php/apps/spreed/api/v1/chat/{target}"
|
|
|
|
|
|
|
|
|
|
credentials = base64.b64encode(f"{username}:{password}".encode()).decode()
|
|
|
|
|
|
|
|
|
|
data = urllib.parse.urlencode({'message': message}).encode()
|
|
|
|
|
|
|
|
|
|
req = urllib.request.Request(url, data=data)
|
|
|
|
|
req.add_header('OCS-APIRequest', 'true')
|
|
|
|
|
req.add_header('Content-Type', 'application/x-www-form-urlencoded')
|
|
|
|
|
req.add_header('Authorization', f'Basic {credentials}')
|
|
|
|
|
|
|
|
|
|
response = urllib.request.urlopen(req)
|
|
|
|
|
log.info("Nextcloud Talk message sent to conversation %s, response: %d" % (target, response.status))
|
|
|
|
|
return True
|
|
|
|
|
except urllib.error.HTTPError as e:
|
|
|
|
|
log.error("Nextcloud Talk HTTP error: %d %s" % (e.code, e.reason))
|
|
|
|
|
return False
|
|
|
|
|
except Exception as e:
|
|
|
|
|
log.error("Failed to send Nextcloud Talk message: %s" % (e))
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_hook(hook_id):
|
|
|
|
|
return hooks.get(str(hook_id))
|
|
|
|
|
global hooks
|
|
|
|
|
hook = hooks.get(str(hook_id))
|
|
|
|
|
log.info("Looking up hook_id=%s, found=%s, available_keys=%s" % (hook_id, hook, list(hooks.keys())))
|
|
|
|
|
return hook
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.route("/webhook/<id>", methods=['POST'])
|
|
|
|
|
@@ -166,10 +270,17 @@ def webhook_receiver(id):
|
|
|
|
|
if type == 'AlertStatus':
|
|
|
|
|
message = ("Alert %s: %s" % (data.get('AlertID'), data.get('LogEntry')))
|
|
|
|
|
|
|
|
|
|
if type == 'ScheduleOnCallUsers':
|
|
|
|
|
message = ("On call rotation for schedule %s changed to user(s):" % (data.get('ScheduleName')))
|
|
|
|
|
users = data.get('Users')
|
|
|
|
|
for user in users:
|
|
|
|
|
message = ("%s %s" % (message, user.get('Name')))
|
|
|
|
|
|
|
|
|
|
hook = get_hook(id)
|
|
|
|
|
|
|
|
|
|
if hook is None:
|
|
|
|
|
return jsonify({'message': 'Hook not found'}), 404
|
|
|
|
|
log.error("Webhook %s found, dropping message" % (id))
|
|
|
|
|
return jsonify({'message': 'Hook not found'}), 400
|
|
|
|
|
|
|
|
|
|
transport = hook.get('transport')
|
|
|
|
|
target = hook.get('target')
|
|
|
|
|
@@ -179,9 +290,11 @@ def webhook_receiver(id):
|
|
|
|
|
if target is not None:
|
|
|
|
|
log.info(target)
|
|
|
|
|
if message is not None:
|
|
|
|
|
send_smp_message(target, message)
|
|
|
|
|
if not send_smp_message(target, message):
|
|
|
|
|
return jsonify({'message': 'Failed to send SimpleX message'}), 500
|
|
|
|
|
else:
|
|
|
|
|
log.error("No message, dropping")
|
|
|
|
|
return jsonify({'message': 'No message, dropping'}), 400
|
|
|
|
|
else:
|
|
|
|
|
log.error("No target found, dropping message")
|
|
|
|
|
return jsonify({'message': 'No target found, dropping message'}), 400
|
|
|
|
|
@@ -189,9 +302,23 @@ def webhook_receiver(id):
|
|
|
|
|
if target is not None:
|
|
|
|
|
log.info(target)
|
|
|
|
|
if message is not None:
|
|
|
|
|
send_matrix_message(target, message)
|
|
|
|
|
if not send_matrix_message(target, message):
|
|
|
|
|
return jsonify({'message': 'Failed to send Matrix message'}), 500
|
|
|
|
|
else:
|
|
|
|
|
log.error("No message, dropping")
|
|
|
|
|
return jsonify({'message': 'No message, dropping'}), 400
|
|
|
|
|
else:
|
|
|
|
|
log.error("No target found, dropping message")
|
|
|
|
|
return jsonify({'message': 'No target found, dropping message'}), 400
|
|
|
|
|
elif transport == 'nextcloud':
|
|
|
|
|
if target is not None:
|
|
|
|
|
log.info(target)
|
|
|
|
|
if message is not None:
|
|
|
|
|
if not send_nextcloud_message(target, message):
|
|
|
|
|
return jsonify({'message': 'Failed to send Nextcloud Talk message'}), 500
|
|
|
|
|
else:
|
|
|
|
|
log.error("No message, dropping")
|
|
|
|
|
return jsonify({'message': 'No message, dropping'}), 400
|
|
|
|
|
else:
|
|
|
|
|
log.error("No target found, dropping message")
|
|
|
|
|
return jsonify({'message': 'No target found, dropping message'}), 400
|
|
|
|
|
@@ -205,7 +332,9 @@ def webhook_receiver(id):
|
|
|
|
|
return jsonify({'message': 'Webhook received successfully'}), 200
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
read_configs()
|
|
|
|
|
log.info("Config loaded: %s" % (config))
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
log.info("Started %s" % (versionstring))
|
|
|
|
|
read_configs()
|
|
|
|
|
app.run()
|
|
|
|
|
|