mvoau-klant/wekober.py
2021-04-10 14:57:37 +02:00

325 lines
9.6 KiB
Python

from aiohttp import web
from aiohttp_sse import sse_response
from enum import Enum
from jwt import JWT, jwk_from_pem
from multidict import MultiDict
from telegram import ChatAction
from . import bot as my_bot
import asyncio
import aio_msgpack_rpc
import base64
import json
import logging
import os
import random
import ssl
import string
import time
import wakeonlan
class PcStatus(Enum):
ONBEKEND = 0
OPSTARTEN = 1
AAN = 2
NAAR_SLAAPSTAND = 3
SLAAPSTAND = 4
WAKKER_WORDEN = 5
UIT = 6
def can_wake(self) -> bool:
return self.value == 0 or self.value == 4 or self.value == 6
def friendly_name(self) -> str:
if self.value == 0:
return "onbekend (waarschijnlijk uit)"
elif self.value == 1:
return "opstarten"
elif self.value == 2:
return "aan"
elif self.value == 3:
return "proberen in slaap te komen"
elif self.value == 4:
return "slapen"
elif self.value == 5:
return "proberen wakker te worden"
elif self.value == 6:
return "uit"
else:
LOGGER.warning(f"Unknown status: {self.value}")
return "onbekend"
PC_STATUS = PcStatus.ONBEKEND
LOGGER = logging.getLogger(__name__)
MAC = "70:85:c2:af:3c:48"
WAKE_MSGS = [("Ik zal een poging wagen om de ober wakker te maken.",
"Volgens mij is het gelukt!",
"Ik heb het geprobeerd, maar het lukte volgens mij niet",
"De ober was al wakker."),
("Een ogenblikje…", "Gelukt!", "Oh nee, het lukte niet.", "De ober staat\
al aan"),
("Nou, dat is wel moeilijk. Maar speciaal voor jou wil ik wel een\
uitzondering maken", "En speciaal voor jou staat de ober nu aan!",
"Ik heb mijn best gedaan, maar het wilde niet lukken.", "De ober was al aan\
, speciaal voor jou!"),
("Oké, ik stuur mijn boodschapper wel op een trojka naar de ober\
toe.", "Zover ik heb vernomen is mijn boodschapper aangekomen.",
"De boodschapper is helaas opgegeten door een aantal wolven.",
"De boodschapper kwam terug met de boodschap dat de ober al aan stond"),
("Hocus pocus, pilatus pas, ik wou dat de ober aan was!",
"Nou, daar is 'ie dan!", "Zit eens niet zo dicht op mijn nues. Nu is mijn\ groote goocheltruc mislukt!", "Nou, daar is 'ie dan. Snel hë?"),
("Zeg makker…", "De ober is geen specerij, maar hij staat wel aan!",
"Mijn poging om de ober in te schakelen is mislukt door de Hispanjolen."),
("Binnenkort leven jullie in een samenleving…", "…waar de ober aan staat.",
"…waar de ober niet gestart kon worden.", "…waar de ober al aan staat."),
("Oké, maar eerst moet ik nog een aantal kokosnoten aan de vulkaan opofferen.\
Zou je nog heel even geduld kunnen hebben?", "De kokosnoten zijn vernietigd\
\nde ober staat aan.\nIk ben niet creatief\nen eindig deze rijm met banaan",
"De vulkaangoden hebben mij verboden de ober aan te zetten. Helaas.",
"De ober staat al aan"),
("Ik zal wel even bij de ober langsgaan", "Hallo meneer de Ober. Ja, ik moest\
kloppen want de bel deed het niet! Maar wat fijn dat u er bent!", "De ober\
gooide de deur recht in mijn gezicht weer dicht.", "De deur was al open,\
ik kon zo naar binnen!")]
PC_IP = "roku"
PING_TIMEOUT = "2"
WWW_REALM = "Netsoj"
WAKE_REQUESTED = []
WAKE_REQUESTED_MSG = []
ALLOWED_CHATS = [-1001304423616]
ALLOWED_USERS = [9811869]
SSE_STREAMS = []
PUB_KEY = jwk_from_pem(b"""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkFMN9Bl7SQgizcdC44aH
nM/8bgkwhIyC4WtoRpN1lD9glVe1+f2f6UkWP+49rsyGJy2fOwUcH6mOJu+R12Tr
IGChLITDogQPRfsoINttprXXSl8Koa5iA4F2EWv3IJdFlXXhIvZoEPXNrAmgaEt/
CMYUf7UXjBONzqvDFPgbW06v0wtKS+K8WC7dQZ8pSmTNOqpZe4z0hO7MfqWZ1lF9
/aH5ARlGBI2aYtVw++lxbXg5c/BpLOaD7TvmntIhumxtiEHBDJGt+QQEDVeqPKvO
zSBqZLctHAa0oLMjp1qU1cs1yHJg7VKwp2TKrfzHPfHeEVOp3/GTL50tVf+zDVNW
UQIDAQAB
-----END PUBLIC KEY-----""");
MJWT = JWT()
def is_ober_aan() -> bool:
return os.system(f"ping -c 1 {PC_IP}") == 0
async def async_is_ober_aan() -> bool:
proc = await asyncio.create_subprocess_exec("ping", "-c", "1", "-W", PING_TIMEOUT, PC_IP)
return await proc.wait() == 0
def handle_wekober(update, context):
global WAKE_REQUESTED
global WAKE_REQUESTED_MSG
messages = random.choice(WAKE_MSGS)
context.bot.send_chat_action(update.effective_chat.id, ChatAction.TYPING)
if is_ober_aan():
context.bot.send_sticker(update.effective_chat.id, my_bot.STICKER_AL_AAN)
else:
context.bot.send_message(update.effective_chat.id, messages[0])
if update.effective_chat.id not in ALLOWED_CHATS and\
update.effective_user.id not in ALLOWED_USERS:
time.sleep(1)
context.bot.send_message(update.effective_chat.id, messages[2])
LOGGER.info(f"Unauthorised user: {update.effective_user.id}")
return
#LOGGER.info(f"Chat id: {update.effective_chat.id}")
wakeonlan.send_magic_packet(MAC)
if update.effective_chat.id not in WAKE_REQUESTED:
WAKE_REQUESTED += [update.effective_chat.id]
WAKE_REQUESTED_MSG += [messages]
async def on_pc_status_changed(bot, status):
global SSE_STREAMS
global WAKE_REQUESTED
global WAKE_REQUESTED_MSG
global PC_STATUS
PC_STATUS = status
LOGGER.info(f"New status: {status.friendly_name()}")
if status == PcStatus.AAN:
for i, chat in enumerate(WAKE_REQUESTED):
LOGGER.debug("i: {i}, chat: {chat}, msg: {WAKE_REQUESTED_MSG[i]}")
bot.send_message(chat, WAKE_REQUESTED_MSG[i][1])
WAKE_REQUESTED = []
WAKE_REQUESTED_MSG = []
# Notify the SSE-streams
t = []
for stream in SSE_STREAMS:
t.append(stream.send(json.dumps({"status": status.friendly_name(), "can_wake": status.can_wake()})))
LOGGER.info(f"Notifying clients ({len(SSE_STREAMS)})")
await asyncio.gather(*t)
LOGGER.info("Clients notified")
class PcBridgeBoot():
def __init__(self, queue):
self.queue = queue
def GetBootReason(self) -> str:
LOGGER.info("PC booted up!")
try:
self.queue.put_nowait({"type": "pc_status_changed", "status": PcStatus.AAN})
except:
pass
return str("Minecraft")
def NotifySleep(self) -> None:
LOGGER.info("PC going to sleep")
try:
self.queue.put_nowait({"type": "pc_status_changed", "status": PcStatus.SLAAPSTAND})
except:
pass
def NotifyWakeup(self) -> None:
LOGGER.info("PC woke up")
try:
self.queue.put_nowait({"type": "pc_status_changed", "status": PcStatus.AAN})
except:
pass
def NotifyShutdown(self) -> None:
LOGGER.info("PC shutting down")
try:
self.queue.put_nowait({"type": "pc_status_changed", "status": PcStatus.UIT})
except:
pass
@web.middleware
async def middleware_auth(request, handler):
""" Check if the user is authorized. """
if "Authorization" in request.headers:
parts = request.headers["Authorization"].split(" ")
if len(parts) != 2 and parts[0].casefold() != "bearer":
LOGGER.info("Invalid authorization header")
return web.Response(status=401,
headers=MultiDict({"WWW-Authenticate": f"BEARER Realm=\"{WWW_REALM}\""}))
userpass = parts[1]
elif "token" in request.query:
userpass = request.query["token"];
else:
# Authentication not in request headers.
LOGGER.info("Authorization not in request headers")
return web.Response(status=401,
headers=MultiDict({"WWW-Authenticate": f"BEARER Realm=\"{WWW_REALM}\""}))
# Continue as usual
try:
token = MJWT.decode(userpass, PUB_KEY, do_time_check=False)
except Exception as e:
LOGGER.info(f"Incorrect token: {e}")
return web.Response(status=401,
headers=MultiDict({"WWW-Authenticate": f"BEARER Realm=\"{WWW_REALM}\""}))
try:
can_turn_on = "minecraft-player" in token["realm_access"]["roles"]
except KeyError:
can_turn_on = False
if not can_turn_on:
return web.Response(status=403)
# Proceed normally:
response = await handler(request)
return response
async def post_boot(request):
global PC_STATUS
print(request)
print(request.headers)
# Notify the SSE streams
# t = []
if PC_STATUS == PcStatus.SLAAPSTAND:
status = PcStatus.WAKKER_WORDEN
else:
status = PcStatus.OPSTARTEN
await QUEUE.put({"type": "pc_status_changed", "status": status})
# for stream in SSE_STREAMS:
# t.append(stream.send(json.dumps({"status": "opstarten"})))
# await asyncio.gather(*t)
# Send a magic packet
wakeonlan.send_magic_packet(MAC)
return web.Response(status=205)
async def get_boot(request):
global PC_STATUS
if (await async_is_ober_aan()):
PC_STATUS = PcStatus.AAN
elif PC_STATUS == PcStatus.AAN:
PC_STATUS = PcStatus.ONBEKEND
antwoord = {"status": PC_STATUS.friendly_name(), "can_wake": PC_STATUS.can_wake()}
LOGGER.info(PC_STATUS.friendly_name())
return web.json_response(antwoord, status=200)
async def get_boot_stand_van_zaken(request):
global SSE_STREAMS
LOGGER.info("EventStream connected")
resp = await sse_response(request)
SSE_STREAMS.append(resp)
try:
await resp.wait()
finally:
LOGGER.info("EventStream disconnected")
SSE_STREAMS.remove(resp)
return resp
async def start_http(queue):
ssl_c = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ssl_c.check_hostname = False
ssl_c.load_cert_chain("certs/client.crt", "certs/client.key.u")
app = web.Application(middlewares=[middleware_auth])
app.add_routes([
#web.post("/ober/aan/", post_boot),
web.post("/ober/aan", post_boot),
web.get("/ober/stand-van-zaken", get_boot_stand_van_zaken),
web.get("/ober/", get_boot)
])
runner = web.AppRunner(app)
await runner.setup()
try:
site = web.TCPSite(runner, "0.0.0.0", 8086, ssl_context=ssl_c)
LOGGER.info("Started http server")
await site.start()
while True:
await asyncio.sleep(1)
finally:
await runner.cleanup()
LOGGER.info("Stopped http server")
async def start_dbus(queue):
global QUEUE
QUEUE = queue
try:
server = await asyncio.start_server(aio_msgpack_rpc.Server(PcBridgeBoot(queue)), port=18002)
logging.info("RPC server started")
while True:
await asyncio.sleep(1)
except asyncio.CancelledError:
pass
except Exception as e:
logging.warn(f"Could not start RPC: {e}")
finally:
logging.info("Closing RPC")
if server:
server.close()