teeworlds-bridge/teeworlds-bridge.py

249 lines
9.2 KiB
Python

"""
Copyright (C) 2024 Skylar Widulski <cobra@vern.cc>
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 <https://www.gnu.org/licenses/>.
"""
import re
import functools
import nio
import asyncio
import aiofiles
from promise import Promise
import json
import os
import sys
from config import (mx_room_id, mx_user_id, mx_homeserver, mx_pass,
econ_host, econ_port, econ_pass)
source_url = 'http://git.vern.cc/cobra/teeworlds-bridge'
CHAT_PATTERN = re.compile(
r'^\[\S+\]\[chat\]: \d+:\d+:(.+?): (.*)$'
)
JOIN_PATTERN = re.compile(
r'^\[\S+\]\[game\]: team_join player=\'(.+?)\' team=(-?[0-9]->)?(-?[0-9])'
)
LEAVE_PATTERN = re.compile(
r'^\[\S+\]\[game\]: leave player=\'(.+?)\''
)
KILL_PATTERN = re.compile(
r'^\[\S+\]\[game\]: kill killer=\'[0-9]+?:(.+?)\' victim=\'[0-9]+?:(.+?)\' weapon=(-?[0-9]) special=[0-9]'
)
START_PATTERN = re.compile(
r'^\[\S+\]\[game\]: start match type=\'(.+?)\' teamplay=\'([01])\''
)
MAP_PATTERN = re.compile(
r'^\[\S+\]\[server\]: maps/(.+?)\.map crc is .+'
)
def acquire(*modes):
def outer(f):
@functools.wraps(f)
async def inner(self, *args, **kwargs):
for mode in modes:
await getattr(self, f'{mode}_lock').acquire()
try:
return await f(self, *args, **kwargs)
finally:
for mode in modes:
getattr(self, f'{mode}_lock').release()
return inner
return outer
class TeeworldsBridge:
def __init__(self, *args, **kwargs):
self.client = nio.AsyncClient(mx_homeserver, mx_user_id)
self.reader, self.writer = (None, None)
async def readline(self):
r = (await self.reader.readline()).decode('utf-8')
if r == '':
self.writer.close()
raise Exception(
f'Connection to {self.host}:{self.port} has been closed'
)
else:
return r.replace('\0', '').rstrip()
async def say(self, message):
message = message[:120]
message.replace('\n', ' ')
self.writer.write(f'say {message}\n'.encode())
await self.writer.drain()
async def connect(self):
self.reader, self.writer = await asyncio.open_connection(econ_host, econ_port)
try:
print(f'Connected to {econ_host}:{econ_port}')
self.writer.write(f'{econ_pass}\n'.encode())
await self.writer.drain()
await self.reader.readline()
line = await self.reader.readline()
if 'Authentication successful' \
not in line.decode('utf-8'):
raise Exception(f'Failed to authenticate to {econ_host}:{econ_port}')
except Exception as e:
print(f'Failed to connect to {econ_host}:{econ_port}')
raise e
async def watch_econ(self):
self.teamplay = 0
self.matchtype = "DM"
self.mapname = "dm1"
while (True):
if self.writer == None or self.writer.is_closing():
try:
self.connect()
except:
await asyncio.sleep(5)
continue
try:
line = await self.readline()
except:
continue
match = re.match(CHAT_PATTERN, line)
if match:
name = match.group(1)
message = match.group(2)
await self.client.room_send(
mx_room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": f'[chat] {name}: {message}'},
)
continue
match = re.match(JOIN_PATTERN, line)
if match:
name = match.group(1).split(':')
team = int(match.group(3))
team_str = f'{team} team'
if team == -1:
team_str = 'spectators'
if team == 0:
if self.teamplay:
team_str = 'red team'
else:
team_str = 'game'
if team == 1:
team_str = 'blue team'
await self.client.room_send(
mx_room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": f'[game] {name[1]} joined the {team_str}'},
)
match = re.match(LEAVE_PATTERN, line)
if match:
name = match.group(1).split(':')
await self.client.room_send(
mx_room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": f'[game] {name[1]} left the game'},
)
match = re.match(KILL_PATTERN, line)
if match:
killer = match.group(1).split(':')
victim = match.group(2).split(':')
weapon = int(match.group(3))
weapon_str = "weapon " + str(weapon)
if weapon == -3:
continue
if weapon == -1:
weapon_str = None
if weapon == 0:
weapon_str = "a Mallet"
if weapon == 1:
weapon_str = "a Gun"
if weapon == 2:
weapon_str = "a Shotgun"
if weapon == 3:
weapon_str = "a Grenade Launcher"
if weapon == 4:
weapon_str = "a Laser Rifle"
if weapon == 5:
weapon_str = "a Katana"
await self.client.room_send(
mx_room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": f'[game] {killer[1]} killed {victim[1] if (victim[0] != killer[0] or victim[1] != killer[1]) else "themself"}{f" with {weapon_str}" if weapon_str else ""}'},
)
match = re.match(START_PATTERN, line)
if match:
self.matchtype = match.group(1)
self.teamplay = int(match.group(2))
await self.client.room_send(
mx_room_id,
message_type="m.room.message",
content={"msgtype": "m.text", "body": f'[game] Starting {self.matchtype} match on map {self.mapname}'},
)
match = re.match(MAP_PATTERN, line)
if match:
self.mapname = match.group(1)
await asyncio.sleep(0)
async def watch_matrix(self):
if not os.path.exists("teeworlds.json"):
response = await self.client.login(mx_pass)
print(response)
if isinstance(response, nio.LoginResponse):
write_creds(response, "https://mtrx.vern.cc")
print("Logged in.")
else:
async with aiofiles.open("teeworlds.json", "r") as f:
contents = await f.read()
config = json.loads(contents)
self.client = nio.AsyncClient(config["homeserver"])
self.client.access_token = config["access_token"]
self.client.user_id = config["user_id"]
self.client.device_id = config["device_id"]
with open("next_batch","w+") as next_batch_token:
self.client.next_batch = next_batch_token.read()
await self.client.room_send(mx_room_id, message_type="m.room.message", content={"msgtype": "m.text", "body": f"Starting Teeworlds bridge. Source code: {source_url}"})
while (True):
sync_response = await self.client.sync(30000)
with open("next_batch","w") as next_batch_token:
next_batch_token.write(sync_response.next_batch)
if len(sync_response.rooms.join) > 0:
joins = sync_response.rooms.join
for room_id in joins:
for event in joins[room_id].timeline.events:
if isinstance(event, nio.RoomMessageText) and event.sender != self.client.user_id:
await self.say(f'<{event.sender}> {event.body}')
def write_creds(resp: nio.LoginResponse, homeserver) -> None:
with open("teeworlds.json", "w") as f:
json.dump(
{
"homeserver": homeserver,
"user_id": resp.user_id,
"device_id": resp.device_id,
"access_token": resp.access_token,
},
f,
)
async def main():
print("Starting...")
bridge = TeeworldsBridge()
await bridge.connect()
await Promise.all([bridge.watch_econ(), bridge.watch_matrix()])
asyncio.run(main())