From 8c6eb02a9b070da7c999ff3a7101c742bab0ea24 Mon Sep 17 00:00:00 2001 From: Chris Josten Date: Sat, 10 Apr 2021 14:57:37 +0200 Subject: [PATCH] Eerste pleging! --- .gitignore | 2 + __init__.py | 0 __main__.py | 4 + bot.py | 100 +++++++++++++++ config.py | 2 + taalgebruik.py | 138 +++++++++++++++++++++ wekober.py | 324 +++++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 570 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 __main__.py create mode 100755 bot.py create mode 100644 config.py create mode 100644 taalgebruik.py create mode 100644 wekober.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a295864 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.pyc +__pycache__ diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/__main__.py b/__main__.py new file mode 100644 index 0000000..958dbc5 --- /dev/null +++ b/__main__.py @@ -0,0 +1,4 @@ +import asyncio +from . import bot + +asyncio.run(bot.main()) diff --git a/bot.py b/bot.py new file mode 100755 index 0000000..7aa8942 --- /dev/null +++ b/bot.py @@ -0,0 +1,100 @@ +#!/usr/bin/python3 +import asyncio +import os +import time +import logging + +from . import config, taalgebruik, wekober + +# Telegram +from telegram.ext import Updater, CommandHandler, MessageHandler, Filters,\ +CallbackQueryHandler, CallbackContext +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ChatAction, ParseMode + +STICKER_AL_AAN = None +API_KEY = os.environ["TG_APIKEY"]; + + +############################################################################### +# TELEGRAM HANDLERS # +############################################################################### +def start(update, context): + context.bot.send_message(chat_id=update.message.chat_id, text="Laden versie v2.1…") + time.sleep(5) + context.bot.send_message(chat_id=update.message.chat_id, text="Versie v2.1 geladen!") + context.bot.send_message(chat_id=update.message.chat_id, text="Ik ben terug van weggeweest en ik ben klaar om te rollen!\n\ + \nJullie hebben misschien een lange tijd vrijspel gehad, maar nu sla ik terug!") + +def sokpop(update, context): + if update.message.from_user.id not in config.SOKPOP_GEBRUIKERS: + update.message.reply_text("U heeft onvoldoende rechten.") + elif len(context.args) < 2: + update.message.reply_text("Gebruik als volgt: /sokpop ") + else: + try: + chat_id = int(context.args[0]) + text = " ".join(context.args[1:]) + context.bot.send_message(chat_id=chat_id, text=text) + except ValueError: + update.message.reply_text("Het eerste argument moet een\ +geheel getal zijn!") + +async def bot_main(queue: asyncio.Queue): + global STICKER_AL_AAN + logging.info("Connecting to Telegram") + updater = Updater(token=API_KEY, use_context=True) + dispatcher = updater.dispatcher + + start_handler = CommandHandler("start", start) + # Handlers from taalgebruik + dump_dataset_handler = CommandHandler("dump", taalgebruik.dump_dataset) + rapporteer_handler = CommandHandler("rapporteer", taalgebruik.rapporteer) + message_handler = MessageHandler(Filters.text, taalgebruik.handle_message) + callback_handler = CallbackQueryHandler(taalgebruik.handle_correction) + + # Handlers for wekober + wekober_handler = CommandHandler("wekober", wekober.handle_wekober) + + dispatcher.add_handler(start_handler) + dispatcher.add_handler(dump_dataset_handler) + dispatcher.add_handler(rapporteer_handler) + dispatcher.add_handler(callback_handler) + dispatcher.add_handler(wekober_handler) + dispatcher.add_handler(CommandHandler("sokpop", sokpop)) + dispatcher.add_handler(message_handler) + + STICKER_AL_AAN = updater.bot.get_sticker_set("mijnvervaardigingsoberrobot")\ + .stickers[0] + + + updater.start_polling() + logging.info("Started bot") + #updater.idle() + try: + while True: + event = await queue.get() + if event["type"] == "pc_status_changed": + await wekober.on_pc_status_changed(updater.bot, event["status"]) + queue.task_done() + except asyncio.CancelledError: + pass + finally: + logging.info("Stopping bot") + updater.stop() + +# Main +async def main(): + # Set up logging + queue = asyncio.Queue() + logging.basicConfig(format='[%(asctime)s %(levelname)s] %(name)s: %(message)s', level=logging.INFO) + + # Initalize taalgebruik + taalgebruik.init() + t_dbus_main = asyncio.create_task(wekober.start_dbus(queue)) + t_http_main = asyncio.create_task(wekober.start_http(queue)) + t_bot_main = asyncio.create_task(bot_main(queue)) + + await asyncio.gather(t_dbus_main, t_bot_main, t_http_main) + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/config.py b/config.py new file mode 100644 index 0000000..ecb0c1f --- /dev/null +++ b/config.py @@ -0,0 +1,2 @@ +BEHEERDERS = [9811869] +SOKPOP_GEBRUIKERS = [9811869] diff --git a/taalgebruik.py b/taalgebruik.py new file mode 100644 index 0000000..ecd43c1 --- /dev/null +++ b/taalgebruik.py @@ -0,0 +1,138 @@ +import logging +import random +# Telegram +from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackQueryHandler +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ChatAction, ParseMode +# Machine learning +from pandas import DataFrame +import numpy +from sklearn import preprocessing +from sklearn.feature_extraction.text import CountVectorizer +from sklearn.naive_bayes import MultinomialNB +from . import config + +LOGGER = logging.getLogger(__name__) + +count_vectorizer = CountVectorizer() +classifier = MultinomialNB() + +WAARHEID = 'w' +LEUGEN = 'l' +SOURCES = [ + ("slecht.tkst", LEUGEN), + ("goed.tkst", WAARHEID), + ("slecht-gebruiker.tkst", LEUGEN), + ("goed-gebruiker.tkst", WAARHEID) +] +LEUGEN_ANTWOORDEN = ["Zeg makker...", + "FOUTMELDING_BERICHT_BEVAT_LEUGEN", + "Ik hoopte dat je inmiddels wijzer was.", + "Ik ben niet boos, ik ben slechts teleurgesteld", + "Zeg makkeroni...", + "Voor jou heb ik nog een leuke anagram: je bent een flikkerende flakker.", + "Zge mkaker", + "Tijd voor een handhaving", + "Jullie fantasieën kunnen ook nooit uitgedoofd worden. Jullie frikkende frikken!"] +LEUGEN_ANTWOORDEN_GEWICHT = [ + 25, + 1, + 1, + 1, + 1, + 1, + 1, + 1, + 1] + +data_frame = DataFrame({"tekst": [], "klasse": []}) + +def load_file(file_name): + f = open(file_name) + for line in f: + yield line + f.close() + +def build_frame(file_name, classification): + rows = [] + index = [] + i = 0; + for message in load_file(file_name): + rows.append({"tekst": message, "klasse": classification}) + index.append(file_name + ":" + str(i)) + i += 1 + data_frame = DataFrame(rows, index=index) + return data_frame + +def retrain_data(): + global data_frame + data_frame = DataFrame({"tekst": [], "klasse": []}) + LOGGER.info("Loading dataset") + for file_name, classification in SOURCES: + data_frame = data_frame.append(build_frame(file_name, classification)) + + LOGGER.info("Transforming dataset") + counts = count_vectorizer.fit_transform(data_frame['tekst'].values) + + LOGGER.info("Training model") + targets = data_frame['klasse'].values + classifier.fit(counts, targets) + +def gen_leugen_message(message, isEdit): + return random.choices(LEUGEN_ANTWOORDEN, weights=LEUGEN_ANTWOORDEN_GEWICHT)[0] + +def dump_dataset(update, context): + context.bot.send_message(chat_id=update.message.chat_id, text="```" + str(data_frame) + "```", parse_mode=ParseMode.MARKDOWN) + +def handle_message(update, context): + mesg = update.effective_message + is_edit = mesg.edit_date is not None + + message_counts = count_vectorizer.transform([mesg.text]) + result = classifier.predict(message_counts)[0] + if result == WAARHEID: + LOGGER.info("'{}' bevat de waarheid".format(mesg.text)) + elif result == LEUGEN: + context.bot.send_chat_action(chat_id=mesg.chat_id, action=ChatAction.TYPING) + keyboard = [[InlineKeyboardButton("Goed", callback_data=LEUGEN), InlineKeyboardButton("Fout", callback_data=WAARHEID)]] + reply_markup = InlineKeyboardMarkup(keyboard) + mesg.reply_text(gen_leugen_message(mesg.text, is_edit), quote=True, reply_markup=reply_markup) + +def handle_correction(update, context): + callback = update.callback_query + LOGGER.info("{} drukte op {}".format(callback.from_user.id, callback.data)) + if callback.from_user.id in config.BEHEERDERS: + if callback.data == WAARHEID: + with open("goed-gebruiker.tkst", "a") as my_file: + my_file.write(callback.message.reply_to_message.text + "\n") + callback.answer(text="Bedankt voor de verbetering, beheerder!") + #callback.edit_message_text("Bewerking: Sorry, ik zat fout. Oeps.".format(gen_leugen_message()), parse_mode=ParseMode.MARKDOWN) + callback.message.delete() + elif callback.data == LEUGEN: + callback.answer(text="Bedankt voor de bevestiging, beheerder!") + context.bot.edit_message_reply_markup(chat_id=callback.message.chat_id, message_id=callback.message.message_id, reply_markup=InlineKeyboardMarkup([[]])) + #update.callback_query.edit_message_reply_markup(InlineKeyboardMarkup([[]])) + retrain_data() + + else: + if callback.data == WAARHEID: + update.callback_query.answer(text="Bedankt voor de verbetering!") + else: + update.callback_query.answer(text="Bedankt voor de bevestiging!") + +def rapporteer(update, context): + if update.message.from_user.id not in config.BEHEERDERS: + update.message.reply_text("Sorry, jij bent geen beheerder. Deze actie wordt niet uitgevoerd.") + else: + if update.message.reply_to_message is not None: + text = update.message.reply_to_message.text + else: + text = " ".join(args) + with open("slecht-gebruiker.tkst", "a") as my_file: + my_file.write(text + "\n") + update.message.reply_text("'{}' is toegevoegd aan de lijst met verboden uitdrukkingen!".format(text)) + retrain_data() + +def init(): + """Initializes the machine learning data""" + retrain_data() + diff --git a/wekober.py b/wekober.py new file mode 100644 index 0000000..71c44a9 --- /dev/null +++ b/wekober.py @@ -0,0 +1,324 @@ +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()