249 lines
9.2 KiB
Python
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())
|