325 lines
9.6 KiB
Python
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()
|