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()