Eerste pleging!

This commit is contained in:
Chris Josten 2021-04-10 14:57:37 +02:00
commit 8c6eb02a9b
7 changed files with 570 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.pyc
__pycache__

0
__init__.py Normal file
View file

4
__main__.py Normal file
View file

@ -0,0 +1,4 @@
import asyncio
from . import bot
asyncio.run(bot.main())

100
bot.py Executable file
View file

@ -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 <gespreksid> <tekst>")
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 <gespreksid> 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())

2
config.py Normal file
View file

@ -0,0 +1,2 @@
BEHEERDERS = [9811869]
SOKPOP_GEBRUIKERS = [9811869]

138
taalgebruik.py Normal file
View file

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

324
wekober.py Normal file
View file

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