Compare commits
7 Commits
4033d214ff
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 80ba03fdf9 | |||
| 3c4acd0a47 | |||
| c8624e4799 | |||
| 4ab2bf4cac | |||
| f2fc79862d | |||
| 4c6ed4c514 | |||
| ec7b330b90 |
11
Changelog
11
Changelog
@@ -1,11 +0,0 @@
|
||||
TellMe Server (2.2.0)
|
||||
* Moved the notifier into tellmesrv
|
||||
|
||||
TellMe Server (2.1.0)
|
||||
* Added support for GoAlert messages
|
||||
|
||||
TellMe Server (2.0.0b2)
|
||||
* Add logging
|
||||
|
||||
TellMe Server (2.0.0b1)
|
||||
* First standalone bot with SimpleX support
|
||||
11
client/CHANGELOG.md
Normal file
11
client/CHANGELOG.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Changelog
|
||||
|
||||
## [2.3.0] - 2026-05-04
|
||||
### Added
|
||||
- Client version 2.3.0 release
|
||||
|
||||
### Features
|
||||
- Send messages: `-m "Your message"` - Send custom notifications
|
||||
- Monitor processes: `-p <pid>` - Wait for a process to exit, then notify
|
||||
- Watch commands: `-w "command"` - Run a command periodically and notify on output
|
||||
- Ping hosts: `-P <host>` - Monitor host availability until it's reachable
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TellMe",
|
||||
"version": "3.0.0",
|
||||
"version": "2.3.0",
|
||||
"description": "TellMe CLI",
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --inline --hot"
|
||||
|
||||
@@ -39,7 +39,7 @@ def sendmessage(message):
|
||||
ran = True
|
||||
|
||||
|
||||
__version__ = "3.0.0"
|
||||
__version__ = "2.3.0"
|
||||
versionstring='Taurix TellMe v' + __version__
|
||||
|
||||
signal.signal(signal.SIGINT, signal_handler)
|
||||
|
||||
37
server/CHANGELOG.md
Normal file
37
server/CHANGELOG.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# Changelog
|
||||
|
||||
## [2.5.0] - 2026-05-14
|
||||
### Added
|
||||
- Added Nextcloud Talk support via OCS Chat API
|
||||
|
||||
## [2.4.0] - 2026-05-13
|
||||
### Added
|
||||
- Added support for GoAlert Rotation shift changes
|
||||
|
||||
## [2.3.0] - 2026-05-04
|
||||
### Added
|
||||
- Matrix/Element support via matrix-nio
|
||||
- Auto-join Matrix rooms on invite
|
||||
- Matrix access token instructions in README
|
||||
|
||||
### Fixed
|
||||
- Fixed Code typo in GoAlert verification message
|
||||
|
||||
## [2.2.0] - 2025-02-04
|
||||
### Changed
|
||||
- Moved the notifier into tellmesrv
|
||||
|
||||
### Added
|
||||
- Support for GoAlert messages
|
||||
|
||||
## [2.1.0] - 2025-02-04
|
||||
### Added
|
||||
- Added support for GoAlert messages
|
||||
|
||||
## [2.0.0b2] - 2025-02-04
|
||||
### Added
|
||||
- Add logging
|
||||
|
||||
## [2.0.0b1] - 2025-02-04
|
||||
### Added
|
||||
- First standalone bot with SimpleX support
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "TellMe Server",
|
||||
"version": "3.0.0",
|
||||
"version": "2.4.0",
|
||||
"description": "TellMe Server",
|
||||
"scripts": {
|
||||
"dev": "webpack-dev-server --inline --hot"
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
matrix_homeserver: ""
|
||||
matrix_access_token: ""
|
||||
matrix_user_id: ""
|
||||
|
||||
nextcloud_server: ""
|
||||
nextcloud_username: ""
|
||||
nextcloud_password: ""
|
||||
@@ -2,3 +2,8 @@
|
||||
2345555XE:
|
||||
transport: "simplex"
|
||||
target: "#Bottest"
|
||||
|
||||
# Nextcloud Talk example:
|
||||
# myhook:
|
||||
# transport: "nextcloud"
|
||||
# target: "conversation_token_here"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
flask
|
||||
websocket-client
|
||||
pyyaml
|
||||
matrix-client
|
||||
matrix-nio
|
||||
|
||||
@@ -7,9 +7,15 @@ import random
|
||||
import logging
|
||||
import os
|
||||
from pprint import pprint
|
||||
from matrix_client.client import MatrixClient
|
||||
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'
|
||||
@@ -22,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):
|
||||
@@ -56,19 +78,103 @@ 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):
|
||||
client = AsyncClient(homeserver, user_id)
|
||||
client.access_token = access_token
|
||||
|
||||
invited_rooms = []
|
||||
|
||||
async def auto_join_callback(room: MatrixRoom, event: RoomMessageText):
|
||||
pass
|
||||
|
||||
client.add_event_callback(auto_join_callback, RoomMessageText)
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
room = None
|
||||
room_id = None
|
||||
|
||||
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=room_id,
|
||||
message_type="m.room.message",
|
||||
content={"msgtype": "m.text", "body": message}
|
||||
)
|
||||
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))
|
||||
return False
|
||||
except Exception as e:
|
||||
log.error("Failed to send Matrix message: %s" % (e))
|
||||
return False
|
||||
finally:
|
||||
await client.close()
|
||||
|
||||
|
||||
def send_matrix_message(target, message):
|
||||
@@ -81,22 +187,58 @@ def send_matrix_message(target, message):
|
||||
log.error("Matrix credentials not configured")
|
||||
return False
|
||||
|
||||
client = MatrixClient(homeserver)
|
||||
client.login(token=access_token, user_id=user_id)
|
||||
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
|
||||
|
||||
room = client.join_room(target)
|
||||
room.send_text(message)
|
||||
|
||||
client.logout()
|
||||
log.info("Sent message to Matrix room %s" % (target))
|
||||
return True
|
||||
return asyncio.get_event_loop().run_until_complete(
|
||||
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
|
||||
|
||||
|
||||
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'])
|
||||
@@ -128,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')
|
||||
@@ -141,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
|
||||
@@ -151,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
|
||||
@@ -167,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()
|
||||
|
||||
Reference in New Issue
Block a user