Dockerized and fixed errors

This commit is contained in:
Remy Moll
2022-01-15 22:14:12 +01:00
parent 3bbe3e6cc6
commit 54b52f78bf
48 changed files with 35 additions and 11 deletions

1
app/bot/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Placeholder

12
app/bot/api/__init__.py Normal file
View File

@@ -0,0 +1,12 @@
from . import reddit
from . import weather
from . import reddit
from . import search
from . import metmuseum
import os
if os.getenv("dockerized", "") == "true":
import sys
sys.path.append("/keys")
import api_keys as keys
else:
from . import keys

39
app/bot/api/metmuseum.py Normal file
View File

@@ -0,0 +1,39 @@
import requests
import random
from PIL import Image
import io
class ArtFetch:
def __init__(self):
self.base_url = "https://collectionapi.metmuseum.org/"
self.objects = self.fetch_objects() # chosen set of images to select randomly
def fetch_objects(self):
"""We restrict ourselves to a few domains."""
# fetch all departements
t = requests.get(self.base_url + "public/collection/v1/departments").json()
deps = t["departments"]
keep_id = []
for d in deps:
name = d["displayName"]
if name == "American Decorative Arts" or name == "Arts of Africa, Oceania, and the Americas" or name == "Asian Art" or name == "European Paintings":
keep_id.append(str(d["departmentId"]))
# fetch artworks listed under these departments
data = {"departmentIds" : "|".join(keep_id)}
t = requests.get(self.base_url + "public/collection/v1/objects",params=data).json()
# num = t["total"]
ids = t["objectIDs"]
return ids
def get_random_art(self):
"""Returns an image object of a randomly selected artwork"""
# fetch the artwork's url
r_id = self.objects[random.randint(0,len(self.objects))]
t = requests.get(self.base_url + "public/collection/v1/objects/" + str(r_id)).json()
im_url = t["primaryImageSmall"]
# download the image
resp = requests.get(im_url)
img = Image.open(io.BytesIO(resp.content))
return img

56
app/bot/api/reddit.py Normal file
View File

@@ -0,0 +1,56 @@
import praw
class RedditFetch():
def __init__(self, key):
self.stream = praw.Reddit(client_id = key["id"], client_secret = key["secret"], user_agent=key["user_agent"])
def get_top(self, subreddit, number, return_type="text"):
if return_type == "text":
posts = []
try:
for submission in self.stream.subreddit(subreddit).top(limit=number):
p = {}
if not submission.stickied:
p["title"] = submission.title
p["content"] = submission.selftext
posts.append(p)
return posts
except:
return []
else:
images = []
try:
for submission in self.stream.subreddit(subreddit).top(limit=number):
if not submission.stickied:
t = {"image": submission.url, "caption": submission.title}
images.append(t)
return images
except:
return []
def get_random_rising(self, subreddit, number, return_type="text"):
if return_type == "text":
posts = []
try:
for submission in self.stream.subreddit(subreddit).random_rising(limit=number):
p = {}
if not submission.stickied:
p["title"] = submission.title
p["content"] = submission.selftext
posts.append(p)
return posts
except:
return []
else:
images = []
try:
for submission in self.stream.subreddit(subreddit).random_rising(limit=number):
if not submission.stickied:
t = {"image": submission.url, "caption": submission.title}
images.append(t)
return images
except:
return []

21
app/bot/api/search.py Normal file
View File

@@ -0,0 +1,21 @@
import duckduckpy
class WebSearch():
def __init__(self):
self.search = duckduckpy.query
def get_result(self, query):
try:
res = []
response = self.search(query, container = "dict")["related_topics"]
for r in response:
if "text" in r:
res.append({
"text" : r["text"],
"url": r["first_url"]
})
except:
res = ["Connection error"]
return res
# TODO: this api has more potential. Extract images or quick facts!

50
app/bot/api/weather.py Normal file
View File

@@ -0,0 +1,50 @@
import requests
import datetime
import logging
logger = logging.getLogger(__name__)
class WeatherFetch():
def __init__(self, key):
self.last_fetch = datetime.datetime.fromtimestamp(0)
self.last_fetch_location = []
self.last_weather = []
self.calls = 0
self.url = "https://api.openweathermap.org/data/2.5/onecall?"
self.key = key
def show_weather(self, location):
delta = datetime.datetime.now() - self.last_fetch
# 1 hour passed, error, or location change
if delta.total_seconds() > 3600 \
or len(self.last_weather) == 0\
or self.last_fetch_location != location:
data = {"lat" : location[0], "lon" : location[1], "exclude" : "minutely,hourly", "appid" : self.key, "units" : "metric"}
self.calls += 1
logger.info("Just fetched weather. ({}th time)".format(self.calls))
try:
weather = requests.get(self.url,params=data).json()
now = weather["current"]
ret_weather = []
ret_weather.append({
"short" : now["weather"][0]["main"],
"temps" : [int(now["temp"])]
})
weather_days = weather["daily"]
for i, day in enumerate(weather_days):
ret_weather.append({
"short" : day["weather"][0]["main"],
"temps" : [int(day["temp"]["min"]),int(day["temp"]["max"])]
})
self.last_fetch_location = location
self.last_weather = ret_weather
self.last_fetch = datetime.datetime.now()
except:
ret_weather = []
else:
ret_weather = self.last_weather
return ret_weather

View File

@@ -0,0 +1,2 @@
# Placeholder
from . import clock, help, weather, status, zvv, lists, alias, plaintext, reddit, search

64
app/bot/commands/alias.py Normal file
View File

@@ -0,0 +1,64 @@
from .template import *
FIRST = range(1)
class Alias(BotFunc):
"""create a new command for command-paths you often use"""
def __init__(self, dispatcher, db):
super().__init__(db)
self.dispatcher = dispatcher
# do not interact with him yet!
def create_handler(self):
conv_handler = ConversationHandler(
entry_points=[CommandHandler('alias', self.entry_point)],
states={
FIRST: [
CallbackQueryHandler(self.print_all, pattern="^all$"),
CallbackQueryHandler(self.create_alias, pattern="^new$"),
CallbackQueryHandler(self.delete_alias, pattern='^delete$'),
]
},
fallbacks=[CommandHandler('alias', self.entry_point)],
)
return conv_handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
test = self.dispatcher
keyboard = [
[InlineKeyboardButton("All aliases", callback_data="all")],
[InlineKeyboardButton("Create new alias", callback_data="new")],
[InlineKeyboardButton("Delete alias", callback_data="delete")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
super().log_activity(receive=True, execute=False, send=True)
update.message.reply_text("What exactly do you want?", reply_markup=reply_markup)
return FIRST
def print_all(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
all_alias = ""
for k in self.persistence["bot"]["aliases"]:
all_alias += k + " - " + self.persistence["bot"]["aliases"] +"\n"
query.edit_message_text(text="List of all commands:\n" + all_alias)
return ConversationHandler.END
def create_alias(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
all_alias = ""
for k in self.persistence["bot"]["aliases"]:
all_alias += k + " - " + self.persistence["bot"]["aliases"] +"\n"
query.edit_message_text(text="List of all commands:\n" + all_alias)
return ConversationHandler.END
def delete_alias(self, update: Update, context: CallbackContext) -> None:
return ConversationHandler.END

206
app/bot/commands/clock.py Normal file
View File

@@ -0,0 +1,206 @@
from .template import *
import time
import numpy
from PIL import Image
import io
CHOOSE, ADDARG = range(2)
MESSAGE, WAKE, ALARM, IMAGE, ART = range(3,8)
class Clock(BotFunc):
"""pass on commands to clock-module"""
def __init__(self, db_utils, clock_module, art_api):
super().__init__(db_utils)
self.clock = clock_module
self.art_api = art_api
def create_handler(self):
handler = ConversationHandler(
entry_points=[CommandHandler("clock", self.entry_point)],
states={
CHOOSE : [
CallbackQueryHandler(self.wake_light, pattern="^wake$"),
CallbackQueryHandler(self.alarm_blink, pattern="^alarm$"),
CallbackQueryHandler(self.show_message, pattern="^message$"),
CallbackQueryHandler(self.show_image, pattern="^image$"),
CallbackQueryHandler(self.art_gallery, pattern="^gallery$"),
],
ADDARG : [MessageHandler(Filters.text, callback=self.get_arg1)],
MESSAGE: [MessageHandler(Filters.text, callback=self.exec_show_message)],
WAKE : [MessageHandler(Filters.text, callback=self.exec_wake_light)],
ALARM : [MessageHandler(Filters.text, callback=self.exec_alarm_blink)],
IMAGE : [MessageHandler(Filters.photo, callback=self.exec_show_image)],
ART : [MessageHandler(Filters.text, callback=self.exec_art_gallery)],
},
fallbacks=[CommandHandler('clock', self.entry_point)],
)
return handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
keyboard = [
[InlineKeyboardButton("Make a wake-light", callback_data="wake")],
[InlineKeyboardButton("Blink as alarm", callback_data="alarm")],
[InlineKeyboardButton("Show a message", callback_data="message")],
[InlineKeyboardButton("Show an image", callback_data="image")],
[InlineKeyboardButton("Art gallery!", callback_data="gallery")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
update.message.reply_text("What exactly do you want?", reply_markup=reply_markup)
return CHOOSE
def wake_light(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
query.edit_message_text("Ok. How long should the color cycle last? (In seconds)")
return WAKE
def alarm_blink(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
query.edit_message_text("Ok. How long should it blink? (In seconds)")
self.next_state = {ALARM : "What frequency (Hertz)"}
return ADDARG
def show_message(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
query.edit_message_text("Ok. What message will I show?")
return MESSAGE
def show_image(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
query.edit_message_text("How long (in minutes) should the image be displayed?")
self.next_state = {IMAGE : "Please send me the photo to display."}
return ADDARG
def art_gallery(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
query.edit_message_text("Ok. How long should we display art? (in hours")
self.next_state = {ART : "And how many artworks would you like to see during that time?"}
return ADDARG
def get_arg1(self, update: Update, context: CallbackContext) -> None:
a = update.message.text
self.additional_argument = a
update.message.reply_text("Furthermore: "+ list(self.next_state.values())[0])
return list(self.next_state.keys())[0]
###### actually running clock actions
def exec_wake_light(self, update: Update, context: CallbackContext) -> None:
duration = update.message.text
matrices = []
start_color = numpy.array([153, 0, 51])
end_color = numpy.array([255, 255, 0])
col_show = numpy.zeros((*self.clock.MOP.shape, 3))
col_show[:,:,...] = start_color
gradient = end_color - start_color
# steps are shown at a frequency of ?? frames / second =>
for i in range(duration * 2): # / 0.5
ct = i/20 * gradient
col_show[:,:,...] = [int(x) for x in ct+start_color]
matrices.append(col_show)
self.clock.out.queue.append({"matrices" : matrices})
return ConversationHandler.END
def exec_alarm_blink(self, update: Update, context: CallbackContext) -> None:
duration = self.additional_argument
matrices = []
duration = int(duration * 2)
empty = numpy.zeros((*self.clock.MOP.shape,3))
red = numpy.ones_like(empty) * 255
for _ in range(int(duration / 2)):
matrices.append(red)
matrices.append(empty)
self.clock.out.queue.append({"matrices": matrices})
return ConversationHandler.END
def exec_show_image(self, update: Update, context: CallbackContext) -> None:
duration = self.additional_argument
i = update.message.photo
img = update.message.photo[0]
bot = img.bot
id = img.file_id
file = bot.getFile(id).download_as_bytearray()
width = self.clock.shape[1]
height = self.clock.shape[0]
img = Image.open(io.BytesIO(file))
im_height = img.height
im_width = img.width
scalex = im_width // width
scaley = im_height // height
scale = min(scalex, scaley)
t = img.resize((width, height),box=(0,0,width*scale,height*scale))
a = numpy.asarray(t)
matrices = [a for _ in range(2*60*duration)]
self.clock.out.queue.append({"matrices": matrices})
return ConversationHandler.END
def exec_show_message(self, update: Update, context: CallbackContext) -> None:
message_str = update.message.text
update.message.reply_text("Now showing: " + message_str)
self.clock.run(self.clock.text_scroll,(message_str,))
return ConversationHandler.END
def exec_art_gallery(self, update: Update, context: CallbackContext) -> None:
duration = float(self.additional_argument)
number = int(update.message.text)
def output(number, duration):
for i in range(number):
img = self.art_api.get_random_art() # returns an PIL.Image object
im_height = img.height
im_width = img.width
width = self.clock.shape[1]
height = self.clock.shape[0]
scalex = im_width // width
scaley = im_height // height
scale = min(scalex, scaley)
t = img.resize((width, height),box=(0,0,width*scale,height*scale))
a = numpy.asarray(t)
self.clock.IO.put(a)
time.sleep(duration*3600 / number)
update.message.reply_text("Ok. Showing art for the next "+ str(duration) + " hours.")
self.clock.run(output,(number, duration))
return ConversationHandler.END
# TODO FIx this to work with the new backend

128
app/bot/commands/help.py Normal file
View File

@@ -0,0 +1,128 @@
from .template import *
FIRST, EXECUTE = range(2)
class Help(BotFunc):
"""Shows the functions and their usage"""
def __init__(self, db):
super().__init__(db)
self.available_commands = {}
def create_handler(self):
conv_handler = ConversationHandler(
entry_points=[CommandHandler('help', self.entry_point)],
states={
FIRST: [
CallbackQueryHandler(self.print_all, pattern="^all$"),
CallbackQueryHandler(self.choose_specific, pattern="^specific$"),
CallbackQueryHandler(self.print_one, pattern='func-'),
],
EXECUTE :[CallbackQueryHandler(self.execute_now)],
# ConversationHandler.TIMEOUT : [
# CallbackQueryHandler(self.timeout)
# ]
},
fallbacks=[CommandHandler('help', self.entry_point)],
conversation_timeout=15,
)
return conv_handler
def add_commands(self, commands):
# commands is a dict {"name": class}
for k in commands:
if k != "plaintext":
self.available_commands[k] = commands[k].__doc__
def entry_point(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context)
keyboard = [
[
InlineKeyboardButton("All commands", callback_data="all"),
InlineKeyboardButton("Just one", callback_data="specific"),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
super().log_activity(read=True, execute=True, send=True) # at this point every step has been fulfilled
if update.message:
update.message.reply_text("What exactly do you want?", reply_markup=reply_markup)
else:
update._effective_chat.send_message("What exactly do you want?", reply_markup=reply_markup)
return FIRST
def print_all(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
all_cmd = ""
for h in self.available_commands:
all_cmd += "{} - `{}`\n".format(h, self.available_commands[h])
query.edit_message_text(text="List of all commands:\n" + all_cmd, parse_mode = ParseMode.MARKDOWN)
return ConversationHandler.END
def choose_specific(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
keyboard = [[InlineKeyboardButton(k, callback_data="func-" + k)] for k in self.available_commands]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text(
text="What command should be printed?", reply_markup=reply_markup
)
return FIRST
def print_one(self, update: Update, context: CallbackContext) -> None:
"""Show new choice of buttons"""
query = update.callback_query
name = query.data.replace("func-", "")
query.answer()
message = name + ": `" + self.available_commands[name] + "`"
keyboard = [[InlineKeyboardButton("Call " + name + " now", callback_data=name),]]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text(
text= message,
reply_markup = reply_markup,
parse_mode = ParseMode.MARKDOWN_V2
)
return EXECUTE
def execute_now(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
name = query.data
query.answer()
funcs = context.dispatcher.handlers[0]
for func in funcs:
if name == func.entry_points[0].command[0]:
break
callback = func.entry_points[0].handle_update
callback(update, context.dispatcher, check_result=True, context=context)
return ConversationHandler.END
def timeout(self, update: Update, context: CallbackContext) -> None:
"""For dying conversation. Currently unused."""
query = update.callback_query
name = query.data.replace("func-", "")
query.answer()
message = name + ": `" + self.available_commands[name] + "`"
query.edit_message_text(
text= "Timed out...",
parse_mode = ParseMode.MARKDOWN_V2
)
return ConversationHandler.END

184
app/bot/commands/lists.py Normal file
View File

@@ -0,0 +1,184 @@
from .template import *
NAME, NEW, ACTION, ITEMADD, ITEMREMOVE = range(5)
class Lists(BotFunc):
"""Create and edit lists"""
def __init__(self, db_utils):
super().__init__(db_utils)
self.current_name = ""
# self.db_utils set through super()
def create_handler(self):
conv_handler = ConversationHandler(
entry_points=[CommandHandler('list', self.entry_point)],
states={
NAME: [
CallbackQueryHandler(self.choose_list, pattern="^list-"),
CallbackQueryHandler(self.new_list, pattern="^new$"),
],
NEW : [MessageHandler(Filters.text, callback=self.new_listname)],
ACTION: [
CallbackQueryHandler(self.list_add, pattern="^add$"),
CallbackQueryHandler(self.list_remove, pattern="^remove$"),
CallbackQueryHandler(self.list_clear, pattern="^clear$"),
CallbackQueryHandler(self.list_delete, pattern="^delete$"),
CallbackQueryHandler(self.list_print, pattern="^print$"),
CallbackQueryHandler(self.list_menu, pattern="^overview$"),
],
ITEMADD : [MessageHandler(Filters.text, callback=self.list_add_item)],
ITEMREMOVE : [CallbackQueryHandler(self.list_remove_index)]
},
fallbacks=[CommandHandler('list', self.entry_point)],
)
return conv_handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context)
lists = self.db_utils.list_get()
keyboard = [[InlineKeyboardButton(k, callback_data="list-"+k)] for k in lists] + [[InlineKeyboardButton("New list", callback_data="new")]]
reply_markup = InlineKeyboardMarkup(keyboard)
super().log_activity(read=True, execute=False, send=True)
update.message.reply_text(text="Here are the existing lists. You can also create a new one:", reply_markup=reply_markup)
return NAME
def choose_list(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
data = query.data
name = data.replace("list-","")
query.answer()
self.current_name = name
keyboard = [
[InlineKeyboardButton("Add item", callback_data="add")],
[InlineKeyboardButton("Remove item", callback_data="remove")],
[InlineKeyboardButton("Clear list", callback_data="clear")],
[InlineKeyboardButton("Print list", callback_data="print")],
[InlineKeyboardButton("Delete list", callback_data="delete")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text("Very well. For " + name + " the following actions are available:", reply_markup=reply_markup)
return ACTION
def list_menu(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
keyboard = [
[InlineKeyboardButton("Add item", callback_data="add")],
[InlineKeyboardButton("Remove item", callback_data="remove")],
[InlineKeyboardButton("Clear list", callback_data="clear")],
[InlineKeyboardButton("Print list", callback_data="print")],
[InlineKeyboardButton("Delete list", callback_data="delete")],
]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text("Very well. For " + self.current_name + " the following actions are available:", reply_markup=reply_markup)
return ACTION
def new_list(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
query.edit_message_text("What's the name of the new list?")
return NEW
def new_listname(self, update: Update, context: CallbackContext) -> None:
name = update.message.text
try:
self.db_utils.list_create(name)
keyboard = [[InlineKeyboardButton("Add an item", callback_data="add"), InlineKeyboardButton("To the menu!", callback_data="overview")]]
reply_markup = InlineKeyboardMarkup(keyboard)
self.current_name = name
update.message.reply_text("Thanks. List " + name + " was successfully created.", reply_markup=reply_markup)
super().log_activity(read=False, execute=True, send=True)
return ACTION
except Exception as e:
update.message.reply_text("Oh no! Encountered exception: {}".format(e))
return ConversationHandler.END
def list_add(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
query.edit_message_text("What would you like to add?")
return ITEMADD
def list_remove(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
sl = self.db_utils.list_get(self.current_name)
keyboard = [[InlineKeyboardButton(k, callback_data=i)] for i,k in enumerate(sl)]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text("Which item would you like to remove?", reply_markup = reply_markup)
return ITEMREMOVE
def list_clear(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
self.db_utils.list_update(self.current_name, replace=[])
keyboard = [[InlineKeyboardButton("Add an item", callback_data="add"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text("List " + self.current_name + " cleared", reply_markup=reply_markup)
return ACTION
def list_delete(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
self.db_utils.list_delete(self.current_name)
query.edit_message_text("List " + self.current_name + " deleted")
return ConversationHandler.END
def list_print(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
it = self.db_utils.list_get(self.current_name)
if it:
content = "\n".join(it)
else:
content = "List empty"
keyboard = [[InlineKeyboardButton("Add an item", callback_data="add"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text("Content of " + self.current_name + ":\n" + content, reply_markup=reply_markup)
return ACTION
def list_add_item(self, update: Update, context: CallbackContext) -> None:
item = update.message.text
self.db_utils.list_update(self.current_name, append=item)
keyboard = [[InlineKeyboardButton("Add some more", callback_data="add"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
reply_markup = InlineKeyboardMarkup(keyboard)
update.message.reply_text("Added " + item, reply_markup=reply_markup)
return ACTION
def list_remove_index(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
ind = int(query.data)
query.answer()
old = self.db_utils.list_get(self.current_name)
name = old.pop(ind)
self.db_utils.list_update(self.current_name, replace=old)
keyboard = [[InlineKeyboardButton("Remove another", callback_data="remove"), InlineKeyboardButton("Back to the menu", callback_data="overview")]]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text("Removed " + name, reply_markup=reply_markup)
return ACTION

View File

@@ -0,0 +1,19 @@
from .template import *
class Plain(BotFunc):
"""Not a command: just keeps logs and usage_data"""
def __init__(self, db_utils):
super().__init__(db_utils)
def create_handler(self):
h = MessageHandler(Filters.text, callback=self.add_to_log)
return h
def add_to_log(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context)
super().log_activity(
read = True,
send = False,
execute = False
)

179
app/bot/commands/reddit.py Normal file
View File

@@ -0,0 +1,179 @@
from re import U
from .template import *
CHOOSE_NUM = 1
class Joke(BotFunc):
"""Tells a joke from reddit."""
def __init__(self, api, db):
super().__init__(db)
self.available_commands = {}
self.api = api
def create_handler(self):
conv_handler = ConversationHandler(
entry_points=[CommandHandler('joke', self.entry_point)],
states={
CHOOSE_NUM: [CallbackQueryHandler(self.get_jokes),],
},
fallbacks=[CommandHandler('joke', self.entry_point)],
# conversation_timeout=5,
)
return conv_handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context)
keyboard = [[InlineKeyboardButton(str(i), callback_data=str(i)) for i in range(1,11)]]
reply_markup = InlineKeyboardMarkup(keyboard)
super().log_activity(read=True, execute=True, send=True) # at this point every step has been fulfilled
update.message.reply_text("How many jokes?", reply_markup=reply_markup)
return CHOOSE_NUM
def get_jokes(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
number = int(query.data)
query.answer()
jokes = self.api.get_random_rising("jokes", number, "text")
# formating
message = ""
for j in jokes:
message += "<b>" + j["title"] + "</b> \n" + j["content"] + "\n\n"
if message == "":
message += "Could not fetch jokes."
query.edit_message_text(text = message, parse_mode = ParseMode.HTML)
return ConversationHandler.END
CHOOSE_TOPIC = 0
class Meme(BotFunc):
"""Gets the latest memes from reddit"""
def __init__(self, api, db):
super().__init__(db)
self.available_commands = {}
self.api = api
def create_handler(self):
conv_handler = ConversationHandler(
entry_points=[CommandHandler('meme', self.entry_point)],
states={
CHOOSE_TOPIC: [CallbackQueryHandler(self.choose_topic)],
CHOOSE_NUM :[CallbackQueryHandler(self.get_memes)],
},
fallbacks=[CommandHandler('meme', self.entry_point)],
)
return conv_handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
keyboard = [
[InlineKeyboardButton("General", callback_data="memes"),],
[InlineKeyboardButton("Dank memes", callback_data="dankmemes"),],
[InlineKeyboardButton("Maths", callback_data="mathmemes"),],
[InlineKeyboardButton("Physics", callback_data="physicsmemes"),],
[InlineKeyboardButton("Biology", callback_data="biologymemes"),],
]
reply_markup = InlineKeyboardMarkup(keyboard)
super().log_activity(read=True, execute=True, send=True) # at this point every step has been fulfilled
update.message.reply_text("What kind of memes?", reply_markup=reply_markup)
return CHOOSE_TOPIC
def choose_topic(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
d = query.data
query.answer()
keyboard = [[InlineKeyboardButton(str(i), callback_data=d + "-" + str(i)) for i in range(1,11)]]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text("How many memes?", reply_markup=reply_markup)
return CHOOSE_NUM
def get_memes(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
data = query.data.split("-")
query.answer()
memes = self.api.get_random_rising(data[0], int(data[1]), "photo")
if len(memes) != 0:
for m in memes:
super().log_activity(read=False, execute=False, send=True) # we just sent an additional message
update.effective_chat.send_photo(photo = m["image"],caption = m["caption"])
else:
update.effective_chat.send_message("Sorry, the meme won't yeet.")
return ConversationHandler.END
# class News(BotFunc):
# """Gets the latest news from reddit"""
# def __init__(self, api, prst):
# super().__init__(prst)
# self.available_commands = {}
# self.api = api
# def create_handler(self):
# conv_handler = ConversationHandler(
# entry_points=[CommandHandler('news', self.entry_point)],
# states={
# CHOOSE_TOPIC: [CallbackQueryHandler(self.choose_topic)],
# CHOOSE_NUM :[CallbackQueryHandler(self.get_news)],
# },
# fallbacks=[CommandHandler('news', self.entry_point)],
# )
# return conv_handler
# def entry_point(self, update: Update, context: CallbackContext) -> None:
# super().entry_point()
# keyboard = [
# [InlineKeyboardButton("World", callback_data="worldnews"),],
# [InlineKeyboardButton("Germany", callback_data="germannews"),],
# [InlineKeyboardButton("France", callback_data="francenews"),],
# [InlineKeyboardButton("Europe", callback_data="eunews"),],
# [InlineKeyboardButton("USA", callback_data="usanews"),],
# ]
# reply_markup = InlineKeyboardMarkup(keyboard)
# update.message.reply_text("What kind of news?", reply_markup=reply_markup)
# return CHOOSE_TOPIC
# def choose_topic(self, update: Update, context: CallbackContext) -> None:
# super().entry_point()
# query = update.callback_query
# d = query.data
# query.answer()
# keyboard = [[InlineKeyboardButton(str(i), callback_data=d + "-" + str(i)) for i in range(1,11)]]
# reply_markup = InlineKeyboardMarkup(keyboard)
# query.edit_message_text("How many entries?", reply_markup=reply_markup)
# return CHOOSE_NUM
# def get_news(self, update: Update, context: CallbackContext) -> None:
# query = update.callback_query
# data = query.data.split("-")
# query.answer()
# #try:
# news = self.api.get_top(data[0], data[1], "text")
# # formating
# message = ""
# for j in news:
# message += "<b>" + j["title"] + "</b> \n" + j["content"] + "\n\n"
# if message == "":
# message += "Could not fetch news."
# query.edit_message_text(news, paresemode=ParseMode.HTML)
# return ConversationHandler.END

View File

@@ -0,0 +1,61 @@
from .template import *
SEARCH, MORE = range(2)
class Search(BotFunc):
"""Browse the web for a topic."""
def __init__(self, api, db):
super().__init__(db)
self.available_commands = {}
self.api = api
def create_handler(self):
conv_handler = ConversationHandler(
entry_points=[CommandHandler('search', self.entry_point)],
states={
SEARCH: [MessageHandler(Filters.text, self.get_results),],
MORE: [CallbackQueryHandler(self.show_more, pattern="^more$"),],
},
fallbacks=[CommandHandler('search', self.entry_point)],
conversation_timeout=20,
)
return conv_handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context)
update.message.reply_text("What are we searching?")
return SEARCH
def get_results(self, update: Update, context: CallbackContext) -> None:
search = update.message.text
results = self.api.get_result(search)
keyboard = [[InlineKeyboardButton("More!", callback_data="more")]]
reply_markup = InlineKeyboardMarkup(keyboard)
# formating
self.results = results
if results:
first = results[0]
message = first["text"] + "\n(" + first["url"] + ")\n\n"
else:
message = "No results for search query."
update.message.reply_text(text = message, reply_markup=reply_markup)
super().log_activity(read = True, execute = True, send = True)
return MORE
def show_more(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
query.answer()
message = ""
for r in self.results:
message += r["text"] + "\n(" + r["url"] + ")\n\n"
query.edit_message_text(message)
super().log_activity(read = False, execute = False, send = True)
return ConversationHandler.END

103
app/bot/commands/status.py Normal file
View File

@@ -0,0 +1,103 @@
from .template import *
import datetime
import requests
import socket
import json
FIRST = 1
class Status(BotFunc):
"""Shows a short status of the program."""
def __init__(self, name, version, db_utils):
super().__init__(db_utils)
self.start_time = datetime.datetime.now()
self.name = name
self.version = version
def create_handler(self):
conv_handler = ConversationHandler(
entry_points=[CommandHandler('status', self.entry_point)],
states={
FIRST: [
CallbackQueryHandler(self.send_log, pattern="^full$"),
]
},
fallbacks=[CommandHandler('status', self.entry_point)],
)
return conv_handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context)
keyboard = [
[
InlineKeyboardButton("And the log?", callback_data="full"),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
delta = str(datetime.datetime.now() - self.start_time)
message = "BeebBop, this is " + self.name + " (V." + self.version + ")\n"
try:
ip = requests.get('https://api.ipify.org').text
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:
s.connect(('8.8.8.8', 80))
(addr, port) = s.getsockname()
local_ips = addr
except:
ip = "not fetchable"
local_ips = "not fetchable"
message += "Status: Running 🟢\n"
message += "Uptime: `" + delta[:delta.rfind(".")] + "`\n"
# message += "Reboots: `" + str(self.persistence["global"]["reboots"]) + "`\n"
message += "IP (public): `" + ip + "`\n"
message += "IP (private): `" + str(local_ips) + "`\n"
u = str(self.get_ngrok_url())
message += "URL: [" + u + "](" + u + ")\n"
# TODO new DB
tot_r = self.db_utils.chat_count("read")
message += "Total messages read: `{}`\n".format(tot_r)
tot_s = self.db_utils.chat_count("send")
message += "Total messages sent: `{}`\n".format(tot_s)
tot_e = self.db_utils.chat_count("execute")
message += "Total commands executed: `{}`\n".format(tot_e)
if update.message:
update.message.reply_text(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
else:
update._effective_chat.send_message(message, reply_markup=reply_markup, parse_mode=ParseMode.MARKDOWN)
super().log_activity(read = True, execute = True, send = True)
return FIRST
def send_log(self, update: Update, context: CallbackContext) -> None:
query = update.callback_query
wanted = query.data.replace("status-","")
query.answer()
with open("persistence/server.log") as l:
query.message.reply_document(l)
super().log_activity(read = False, execute = False, send = True)
return ConversationHandler.END
def get_ngrok_url(self):
try:
url = "http://localhost:4040/api/tunnels/"
res = requests.get(url)
res_unicode = res.content.decode("utf-8")
res_json = json.loads(res_unicode)
for i in res_json["tunnels"]:
if i['name'] == 'command_line':
return i['public_url']
except:
return "Not available"

View File

@@ -0,0 +1,31 @@
import logging
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update, ParseMode
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext, MessageHandler, Filters
from telegram.ext import (
Updater,
CommandHandler,
CallbackQueryHandler,
ConversationHandler,
CallbackContext,
)
import datetime
class BotFunc():
"""Base class for a specific bot-functionality"""
def __init__(self, db_utils):
self.logger = logging.getLogger(__name__)
self.db_utils = db_utils
def log_activity(self, **kwargs):
# mark that a new command has been executed
self.db_utils.sensor_log(**kwargs)
def entry_point(self, update: Update, context: CallbackContext) -> None:
if update.message.text:
self.logger.info("Chat said: {}".format(update.message.text))
else:
self.logger.info("Chat said: {}".format(update.message))

117
app/bot/commands/weather.py Normal file
View File

@@ -0,0 +1,117 @@
from .template import *
import datetime
FIRST = 1
class Weather(BotFunc):
"""Shows a weatherforecast for a given location"""
def __init__(self, api, db):
"""initialize api and persistence"""
super().__init__(db)
self.api = api
self.city = ""
def create_handler(self):
"""returns the handlers with button-logic"""
conv_handler = ConversationHandler(
entry_points=[CommandHandler('weather', self.entry_point)],
states={
FIRST: [
CallbackQueryHandler(self.choose_city, pattern="^city-"),
CallbackQueryHandler(self.choose_time, pattern="^time-"),
]
},
fallbacks=[CommandHandler('weather', self.entry_point)],
)
return conv_handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context)
"""Reacts the call of the command. Prints the first buttons"""
keyboard = [
[
InlineKeyboardButton("Zürich", callback_data="city-zurich"),
InlineKeyboardButton("Freiburg", callback_data="city-freiburg"),
InlineKeyboardButton("Mulhouse", callback_data="city-mulhouse"),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
if update.message:
update.message.reply_text("Which city?", reply_markup=reply_markup)
else:
update.callback_query.edit_message_text("Which city", reply_markup=reply_markup)
return FIRST
def choose_city(self, update: Update, context: CallbackContext) -> None:
"""Prompt same text & keyboard as `start` does but not as new message"""
# Get CallbackQuery from Update
query = update.callback_query
data = query.data
self.city = data.replace("city-","")
query.answer()
keyboard = [
[
InlineKeyboardButton("Now", callback_data="time-now"),
InlineKeyboardButton("Tomorrow", callback_data="time-tomorrow"),
InlineKeyboardButton("7 days", callback_data="time-7"),
]
]
reply_markup = InlineKeyboardMarkup(keyboard)
query.edit_message_text(
text = "Which time?", reply_markup=reply_markup
)
return FIRST
def choose_time(self, update: Update, context: CallbackContext) -> None:
"""Show new choice of buttons"""
query = update.callback_query
query.answer()
forecast_time = query.data.replace("time-","")
weather = self.get_weather(self.city, forecast_time)
query.edit_message_text(
text = "Broadcast for {}: \n\n{}".format(self.city, weather),
parse_mode = ParseMode.HTML
)
super().log_activity(read = True, execute = True, send = True)
return ConversationHandler.END
def get_weather(self, city, forecast_time) -> None:
"""get the weather that matches the given params"""
locations = {"freiburg": [47.9990, 7.8421], "zurich": [47.3769, 8.5417], "mulhouse": [47.7508, 7.3359]}
city = locations[city]
categories = {"Clouds": "", "Rain": "🌧", "Thunderstorm": "🌩", "Drizzle": ":droplet:", "Snow": "", "Clear": "", "Mist": "🌫", "Smoke": "Smoke", "Haze": "Haze", "Dust": "Dust", "Fog": "Fog", "Sand": "Sand", "Dust": "Dust", "Ash": "Ash", "Squall": "Squall", "Tornado": "Tornado",}
days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
today = datetime.datetime.today().weekday()
weather = self.api.show_weather(city)
message = ""
if forecast_time == "now" or forecast_time == "7":
now = weather.pop(0)
message += "<b>Now:</b> " + categories[now["short"]] + "\n"
message += "🌡" + str(now["temps"][0]) + "°\n\n"
tod = weather.pop(0)
message += "<b>" + "Today" + ":</b> " + categories[tod["short"]] + "\n"
message += "🌡 ❄ " + str(tod["temps"][0]) + "° , 🌡 🔥 " + str(tod["temps"][1]) + "°\n\n"
if forecast_time == "tomorrow" or forecast_time == "7":
if forecast_time == "tomorrow": # previous statement was not executed: tomorrow is at weather[2]
tom = weather.pop(2)
else:
tom = weather.pop(0)
message += "<b>" + "Tomorrow" + ":</b> " + categories[tom["short"]] + "\n"
message += "🌡 ❄ " + str(tom["temps"][0]) + "° , 🌡 🔥 " + str(tom["temps"][1]) + "°\n\n"
if forecast_time == "7":
for i, day in enumerate(weather):
message += "<b>" + days[(today + i + 2) % 7] + ":</b> " + categories[day["short"]] + "\n"
message += "🌡 ❄ " + str(day["temps"][0]) + "° , 🌡 🔥 " + str(day["temps"][1]) + "°\n\n"
return message

87
app/bot/commands/zvv.py Normal file
View File

@@ -0,0 +1,87 @@
from .template import *
import datetime
import requests
START, DEST = range(2)
class Zvv(BotFunc):
"""Connects to the swiss travel-api to get public transport routes"""
def __init__(self, db):
super().__init__(db)
self.start = ""
self.dest = ""
pass
def create_handler(self):
conv_handler = ConversationHandler(
entry_points=[CommandHandler('zvv', self.entry_point)],
states={
START: [MessageHandler(Filters.text, callback=self.get_start)],
DEST: [MessageHandler(Filters.text, callback=self.get_dest)]
},
fallbacks=[CommandHandler('zvv', self.entry_point)],
)
return conv_handler
def entry_point(self, update: Update, context: CallbackContext) -> None:
super().entry_point(update, context)
update.message.reply_text("What is the start point?")
return START
def get_start(self, update: Update, context: CallbackContext) -> None:
loc = update.message.text
self.start = loc
update.message.reply_text("Ok. Going from " + loc + ", what is the destination?")
return DEST
def get_dest(self, update: Update, context: CallbackContext) -> None:
loc = update.message.text
self.dest = loc
route = self.get_result()
update.message.reply_text("Here are the routes I've got:\n" + route)
super().log_activity(read=True, execute=True, send=True)
return ConversationHandler.END
def get_result(self):
url = "http://transport.opendata.ch/v1/connections"
start = self.start
dest = self.dest
data = {"from" : start, "to" : dest, "limit" : 2}
try:
routes = requests.get(url, params=data).json()
result = routes["connections"]
text = result[0]["from"]["station"]["name"] + "" + result[0]["to"]["station"]["name"] + "\n\n"
for con in result:
text += "Start: " + datetime.datetime.fromtimestamp(int(con["from"]["departureTimestamp"])).strftime("%d/%m - %H:%M") + "\n"
text += "🏁 " + datetime.datetime.fromtimestamp(int(con["to"]["arrivalTimestamp"])).strftime("%d/%m - %H:%M") + "\n"
text += "" + con["duration"] + "\n"
text += "🗺️ Route:\n"
for step in con["sections"]:
if step["journey"] != None:
text += step["journey"]["passList"][0]["station"]["name"] + " (" + datetime.datetime.fromtimestamp(int(step["journey"]["passList"][0]["departureTimestamp"])).strftime("%H:%M") + ")\n"
text += "➡️ Linie " + self.number_to_emoji(step["journey"]["number"]) + "\n"
text += step["journey"]["passList"][-1]["station"]["name"] + " (" + datetime.datetime.fromtimestamp(int(step["journey"]["passList"][-1]["arrivalTimestamp"])).strftime("%H:%M") +")\n"
else:
text += "Walk."
text += "\n"
return text
except:
return "Invalid api call."
def number_to_emoji(self, number):
out = ""
numbers = ["0","1","2","3","4","5","6","7","8","9"]
for i in str(number):
out += numbers[int(i)]
return str(out)

69
app/bot/main.py Normal file
View File

@@ -0,0 +1,69 @@
from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, CallbackContext
from . import api, commands
import logging
logger = logging.getLogger(__name__)
class ChatBot():
"""better framwork - unites all functions"""
def __init__(self, name, version):
"""Inits the Bot with a few conf. vars
Args: -> name:str - Name of the bot
-> version:str - Version number
-> hw_commands - dict with commands executable by the clock module
-> prst:dict - persistence (overloaded dict that writes to json file)
-> logger - logging object to unify log messages
"""
# added by the launcher, we have self.modules (dict) and persistence and db_utils
self.name = name
self.version = version
# Import apis
self.api_weather = api.weather.WeatherFetch(api.keys.weather_api)
self.api_reddit = api.reddit.RedditFetch(api.keys.reddit_api)
self.api_search = api.search.WebSearch()
self.api_art = api.metmuseum.ArtFetch()
# and so on
self.telegram = Updater(api.keys.telegram_api, use_context=True)
self.dispatcher = self.telegram.dispatcher
def add_commands(self):
# Mark modules as available
db = self.db_utils
# self.help_module = commands.help.Help(db)
self.sub_modules = {
"weather": commands.weather.Weather(self.api_weather, db),
"help" : commands.help.Help(db),
"status" : commands.status.Status(self.name, self.version, db),
"zvv" : commands.zvv.Zvv(db),
"list" : commands.lists.Lists(db),
# "alias" : commands.alias.Alias(self.dispatcher, db),
"joke" : commands.reddit.Joke(self.api_reddit, db),
"meme" : commands.reddit.Meme(self.api_reddit, db),
"search" : commands.search.Search(self.api_search, db),
"clock" : commands.clock.Clock(self.api_art, self.modules["clock"], db),
# ...
"plaintext" : commands.plaintext.Plain(db)
# for handling non-command messages that should simply contribute to statistics
}
# must be a class that has a method create_handler
for k in self.sub_modules:
self.dispatcher.add_handler(self.sub_modules[k].create_handler())
# self.help_module.add_commands(self.sub_modules)
self.sub_modules["help"].add_commands(self.sub_modules)
def start(self):
self.add_commands()
self.telegram.start_polling(
poll_interval=0.2,
)
self.telegram.idle()

View File

@@ -0,0 +1 @@
# Placeholder

71
app/broadcast/b_in.py Normal file
View File

@@ -0,0 +1,71 @@
import requests
import logging
logger = logging.getLogger(__name__)
class FetchUpdates:
"""Fetches updates from the main server and relays them to the clock"""
def __init__(self, server_ip, port):
"""Both methods return a list as python-object. This should be then converted to a numpy array."""
# self.server_ip = server_ip
self.base_url = server_ip + ":" + port
# self.modules gets added through the caller
self.update_calls = 0
self.last_fetch = {}
def start(self):
# dummy for errorless launching
pass
def get_updates(self):
update_url = "http://" + self.base_url + "/getupdates"
result = self.call_api(update_url)
return result
def get_last(self):
update_url = "http://" + self.base_url + "/getlast"
result = self.call_api(update_url)
return result
def fetch_data(self):
try:
if self.update_calls == 0:
fetch = self.get_last()
else:
fetch = self.get_updates()
if not fetch["is_new"]:
fetch = self.last_fetch
else:
self.last_fetch = fetch
data = fetch["data"]
has_queue = fetch["has_queue"]
except:
data = {}
has_queue = False
self.update_calls += 1
return has_queue, data
def call_api(self, url):
ret = {}
try:
result = requests.get(url)
result = result.json()
if result.pop("status") == "ok":
ret = result
except:
logger.error("Bad api call for method {}.".format(url[url.rfind("/"):]))
return ret

86
app/broadcast/b_out.py Normal file
View File

@@ -0,0 +1,86 @@
import flask
from flask import request, jsonify
import numpy as np
from threading import Thread
import logging
log = logging.getLogger('werkzeug')
log.setLevel(logging.ERROR)
# hide the info-messages of each GET-request
class BroadcastUpdates:
"""Broadcasts (out) updates for the hw-handler to be fetched periodically"""
def __init__(self, port):
""""""
self.last_show = ""
self.queue = [] #[{"matrices" : [np.full((16,16,3), 10).tolist(), np.full((16,16,3), 100).tolist(), np.full((16,16,3), 200).tolist()]} for _ in range(4)]
self.port = port
def start(self):
t = Thread(target=self.run)
t.start()
def run(self):
app = flask.Flask(__name__)
@app.route('/getupdates', methods=['GET'])
def get_updates():
return self.get_updates()
@app.route('/getlast', methods=['GET'])
def get_last():
return self.get_last()
app.run('0.0.0.0', port=self.port)
def get_updates(self):
try:
data = self.queue.pop(0)
self.last_show = data
is_new = True
has_queue = len(self.queue) > 0
except:
data = ""
is_new = False
has_queue = False
return self.generate_response(
is_new = is_new,
data = data,
has_queue = has_queue
)
def get_last(self):
try:
try:
data = self.queue[-1]
self.queue = []
except: # list empty
data = self.last_show,
except:
data = ""
return self.generate_response(
data = data,
has_queue = False,
)
def generate_response(self, **kwargs):
ret = {
"status" : "ok",
**kwargs
}
return jsonify(ret)

42
app/client.py Normal file
View File

@@ -0,0 +1,42 @@
# functionality
from clock import c_in, c_out
from broadcast import b_in
import launcher
import logging
import platform
if platform.uname().node == "ArchSpectre":
# development
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
else:
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO,
filename='persistence/client.log',
)
class ReceiverLauncher(launcher.Launcher):
"""Launcher for all server-side modules. The hard-computations"""
def __init__(self):
self.clock_sensor_module = c_in.SensorReadout()
# active: periodically takes readouts
self.clock_hardware_module = c_out.ClockFace()
# active: periodically calls fetcher
self.receive_module = b_in.FetchUpdates(server_ip="localhost", port="1111")
# passive: fetches data on demand
super().__init__(
sensors = self.clock_sensor_module,
clock = self.clock_hardware_module,
receive = self.receive_module
)
ReceiverLauncher()

1
app/clock/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Placeholder

73
app/clock/c_back.py Normal file
View File

@@ -0,0 +1,73 @@
import numpy as np
import datetime
from . import helpers
class ClockBackend:
"""Heavy lifting of matrix operations"""
def __init__(self):
self.MOP = helpers.computations.MatrixOperations()
self.weather = {"weather":"", "high":"", "low":""}
self.weather_raw = {}
self.weather_face_swap = False
def start(self):
self.out = self.modules["broadcast"]
helpers.timer.RepeatedTimer(15, self.clock_loop)
def clock_loop(self):
t = int(datetime.datetime.now().strftime("%H%M"))
if t % 5 == 0:
# switch secondary face every 5 minutes
weather = self.modules["bot"].api_weather.show_weather([47.3769, 8.5417]) # zürich
if weather != self.weather_raw and len(weather) != 0:
td = weather[1]
low = td["temps"][0]
high = td["temps"][1]
self.weather["weather"] = td["short"]
self.weather["high"] = high
self.weather["low"] = low
elif len(weather) == 0:
self.weather["weather"] = "error"
self.weather["high"] = "error"
self.weather["low"] = "error"
self.weather_face_swap = not self.weather_face_swap
self.send_face()
def send_face(self):
"""Set the clock face (time + weather) by getting updated info - gets called every minute"""
matrices = self.MOP.clock_face(self.weather)
if self.weather_face_swap:
matrices = [matrices[0], matrices[2], matrices[1]]
matrices = [m.tolist() for m in matrices]
self.out.queue.append({"matrices" : matrices})
# def text_scroll(self, text, color=[[200,200,200]]):
# pixels = self.MOP.text_converter(text, 12, color)
# sleep_time = 1 / self.tspeed
# width = self.shape[1]
# frames = pixels.shape[1] - width
# if frames <= 0:
# frames = 1
# for i in range(frames):
# visible = pixels[:,i:width+i]
# self.IO.put(visible*self.brightness)
# time.sleep(sleep_time)
# time.sleep(10 * sleep_time)

38
app/clock/c_in.py Normal file
View File

@@ -0,0 +1,38 @@
import time
from . import hardware, helpers
class SensorReadout:
"""Overview class for (actual and potential) sensor sources"""
def __init__(self):
""""""
self.sensor_modules = { # we already call them, they are objects and not classes anymore
"temperature" : hardware.sensors.TemperatureModule(),
"humidity" : hardware.sensors.HumidityModule(),
"luminosity" : hardware.sensors.BrightnessModule(),
# more to come?
}
# self db_utils set externally
def start(self):
helpers.timer.RepeatedTimer(120, self.spread_measure)
def spread_measure(self):
measurements = dict((el,[]) for el in self.sensor_modules.keys())
# create an empty dict with a list for each readout-type
for _ in range(5): # number of measures to average out
for name in self.sensor_modules.keys():
measure = self.sensor_modules[name].readout()
measurements[name].append(measure)
time.sleep(3)
results = {}
for e in measurements.keys():
lst = measurements[e]
results[e] = sum(lst) / len(lst)
self.db_utils.sensor_log(**results)

65
app/clock/c_out.py Normal file
View File

@@ -0,0 +1,65 @@
import datetime
import time
from threading import Thread
import numpy as np
from . import hardware, helpers
class ClockFace:
"""Actual functions one might need for a clock"""
def __init__(self):
""""""
# added by the launcher, we have self.modules (dict)
self.IO = hardware.led.get_handler()
self.shape = self.IO.shape # (16,32) for now
# TODO handle differently!
self.MOP = helpers.computations.MatrixOperations()
self.kill_output = False
def start(self):
Thread(target = self.clock_loop).start()
def clock_loop(self):
while True: # TODO: allow this to be exited gracefully
t_start = datetime.datetime.now()
self.set_brightness()
has_queue, data = self.modules["receive"].fetch_data()
tnext = 1 if has_queue else 30
if data == {}:
matrices = self.MOP.get_fallback()
matrices[0][0,0] = [255, 0, 0] # red dot on the top left
else:
matrices = [np.asarray(d).astype(int) for d in data["matrices"]]
matrices[0][0,0] = [0, 255, 0] # green dot on the top left
if not self.kill_output:
self.IO.put(matrices)
else:
z = np.zeros((16,16,3))
self.IO.put([z,z,z])
t_end = datetime.datetime.now()
delta_planned = datetime.timedelta(seconds = tnext)
delta = delta_planned - (t_end - t_start)
time.sleep(max(delta.total_seconds(), 0))
def set_brightness(self):
"""Kill the brightness at night"""
is_WE = datetime.datetime.now().weekday() > 4
now = int(datetime.datetime.now().strftime("%H%M"))
self.kill_output = (now < 1000 or now > 2200) if is_WE else (now < 830 or now > 2130)

View File

@@ -0,0 +1,2 @@
# Placeholder
from . import led, sensors

17
app/clock/hardware/led.py Normal file
View File

@@ -0,0 +1,17 @@
from . import unicorn as led
# or neopixel soon:
# from . import neopixel as led
def get_handler():
OUT = led.ClockOut()
shape = OUT.shape
if led.SETUP_FAIL:
# we use the sim
del OUT
from . import sim
OUT = sim.ClockOut(shape)
return OUT

View File

@@ -0,0 +1,51 @@
import time
import numpy as np
import colorsys
import random
try:
import rpi_ws281x as ws
except ImportError:
from unittest.mock import Mock
ws = Mock()
SETUP_FAIL = True
class ClockOut:
def __init__(self):
self.shape = (45, 20) # H x W
num = self.shape[0] * self.shape[1]
pin = 18
freq = 800000 # maybe higher
dma = 5
invert = False
brightness = 100
channel = 0
led_type = None # ??
self.strip = ws.PixelStrip(num, pin, freq, dma, invert, brightness, channel, led_type)
self.strip.begin()
def put(self, matrix):
self.render(matrix)
def render(self, matrix):
p = 0
for i in range(matrix.shape[0]):
for j in range(matrix.shape[1]):
col = int(ws.Color(*matrix[i,j]))
self.strip.setPixelColor(p, col)
p += 1
self.strip.show()
# test = ClockOut()
# z = np.zeros((30,30, 3), dtype=int)
# for i in range(30):
# for j in range(30):
# z[i, j, ...] = [random.randint(0,255), random.randint(0,255), random.randint(0,255)]
# test.put(z)
# #time.sleep(0.1)

View File

@@ -0,0 +1,87 @@
import time
import logging
logger = logging.getLogger(__name__)
class TempSim:
"""Simulates a temperature for running on windows"""
temperature = 23.23 # return a celsius value
humidity = 30.4
class LightSim:
def input(self, *args):
return 1
class SensorModule:
def __init__(self):
logger.info("Using module " + self.__class__.__name__)
## Real sensors!
try:
import board
import adafruit_dht
dht11 = adafruit_dht.DHT11(board.D18)
import RPi.GPIO as GPIO
GPIO.setmode(GPIO.BCM)
GPIO.setup(4, GPIO.IN)
except ImportError:
logger.warn("Simulating sensor modules")
dht11 = TempSim()
GPIO = LightSim()
class TemperatureModule(SensorModule):
"""Takes readouts from the DHT 11
Returns: temperature"""
def __init__(self):
super().__init__()
self.device = dht11
def readout(self):
try:
temp = self.device.temperature
except:
time.sleep(1)
try:
temp = self.device.temperature
except:
temp = -1
return temp
class HumidityModule(SensorModule):
"""Takes readouts from the DHT 11
Returns: humidity"""
def __init__(self):
super().__init__()
self.device = dht11
def readout(self):
try:
hum = self.device.humidity
except:
time.sleep(1)
try:
hum = self.device.humidity
except:
hum = -1
return hum
class BrightnessModule(SensorModule):
"""Returns one for HIGH and zero for LOW"""
def __init__(self):
super().__init__()
def readout(self):
# The sensor is reversed: 0 when bright and 1 if dark
light = GPIO.input(4)
if light == 0:
return 1
else:
return 0

55
app/clock/hardware/sim.py Normal file
View File

@@ -0,0 +1,55 @@
import sys
import colorsys
import pygame.gfxdraw
import time
import pygame
import numpy as np
class ClockOut:
"""Creates a drawable window in case the real hardware is not accessible. For development"""
def __init__(self, shape):
self.pixel_size = 20
self.shape = shape
self.pixels = np.zeros((*shape,3), dtype=int)
self.WIDTH = shape[1]
self.HEIGHT = shape[0]
self.window_width = self.WIDTH * self.pixel_size
self.window_height = self.HEIGHT * self.pixel_size
pygame.init()
pygame.display.set_caption("Unicorn HAT simulator")
self.screen = pygame.display.set_mode([self.window_width, self.window_height])
def put(self, matrices):
self.screen.fill((0, 0, 0))
for event in pygame.event.get(): # User did something
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if self.shape == (16, 32):
matrix = np.concatenate((matrices[0], matrices[1]), axis=1)
self.pixels = matrix
self.draw_pixels()
pygame.display.flip()
pygame.event.pump()
def draw_pixels(self):
p = self.pixel_size
r = int(p / 4)
for i in range(self.HEIGHT):
for j in range(self.WIDTH):
w_x = int(j * p + p / 2)
#w_y = int((self.HEIGHT - 1 - y) * p + p / 2)
w_y = int(i * p + p / 2)
color = self.pixels[i,j,:]
color = color.astype("int")
pygame.gfxdraw.aacircle(self.screen, w_x, w_y, r, color)
pygame.gfxdraw.filled_circle(self.screen, w_x, w_y, r, color)

View File

@@ -0,0 +1,96 @@
import colorsys
import time
import numpy as np
try:
import RPi.GPIO as GPIO
SETUP_FAIL = False
except ImportError:
from unittest.mock import Mock
GPIO = Mock()
SETUP_FAIL = True
class ClockOut:
def __init__(self):
self.PIN_CLK = 11
##################################
# Hardcoded vaules:
# GPIO Pins for the actual signal. The other ones are for signal clocks and resets.
self.PINS_DAT = [10, 22]
self.PIN_CS = 8
# for data transmission
self.SOF = 0x72
self.DELAY = 1.0/120
# shape of 2 unicorn hats
self.shape = (16, 32)
##################################
GPIO.setmode(GPIO.BCM)
GPIO.setwarnings(False)
GPIO.setup(self.PIN_CS, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(self.PIN_CLK, GPIO.OUT, initial=GPIO.LOW)
GPIO.setup(self.PINS_DAT, GPIO.OUT, initial=GPIO.LOW)
self.HEIGHT = self.shape[0] #16
self.WIDTH = self.shape[1] #32
self.reset_clock()
def reset_clock(self):
GPIO.output(self.PIN_CS, GPIO.LOW)
time.sleep(0.00001)
GPIO.output(self.PIN_CS, GPIO.HIGH)
def spi_write(self, buf1, buf2):
GPIO.output(self.PIN_CS, GPIO.LOW)
self.spi_write_byte(self.SOF, self.SOF)
for x in range(len(buf1)):
b1, b2= buf1[x], buf2[x]
self.spi_write_byte(b1, b2)
GPIO.output(self.PIN_CS, GPIO.HIGH)
def spi_write_byte(self, b1, b2):
for x in range(8):
GPIO.output(self.PINS_DAT[0], b1 & 0b10000000)
GPIO.output(self.PINS_DAT[1], b2 & 0b10000000)
GPIO.output(self.PIN_CLK, GPIO.HIGH)
b1 <<= 1
b2 <<= 1
#time.sleep(0.00000001)
GPIO.output(self.PIN_CLK, GPIO.LOW)
def put(self, matrices):
"""Sets a height x width matrix directly"""
self.reset_clock()
matrix = np.concatenate((matrices[0], matrices[1]), axis=1) # or 1??
self.show(matrix)
def clear(self):
"""Clear the buffer."""
zero = np.zero((self.HEIGHT, self. WIDTH))
self.put(zero)
def show(self, matrix):
"""Output the contents of the buffer to Unicorn HAT HD."""
##########################################################
## Change to desire
buff2 = np.rot90(matrix[:self.HEIGHT,:16],3)
buff1 = np.rot90(matrix[:self.HEIGHT,16:32],1)
##########################################################
# separated these are: 16x16x3 arrays
buff1, buff2 = [(x.reshape(768)).astype(np.uint8).tolist() for x in (buff1, buff2)]
self.spi_write(buff1, buff2)
time.sleep(self.DELAY)

View File

@@ -0,0 +1 @@
from . import computations, timer

View File

@@ -0,0 +1,178 @@
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import datetime
import time
# bulky hard-coded values:
from . import shapes
digits = shapes.digits
weather_categories = shapes.weather_categories
digit_position = [[2,4], [2,10], [9,4], [9,10]]
days = np.append(np.zeros((15,16)), np.array([0,1,0,1,0,1,0,1,0,1,0,1,1,0,1,1])).reshape((16,16))
class MatrixOperations():
"""Helper functions to generate frequently-used images"""
def __init__(self, shape=[16,16]):
self.shape = shape
# shape is going to be (16,32) for the moment
self.primary = [200, 200, 200]
self.secondary = [10, 200, 10]
self.error = [200, 10, 10]
def time_converter(self, top="", bottom=""):
"""Converts 4-digit time to a 16x16 pixel-matrix
returns: np.array"""
# nshape = (self.shape[0], int(self.shape[1]/2))
nshape = (16, 16)
pixels = np.zeros(nshape, dtype=np.uint8)
if bottom == "" or top == "":
top = datetime.datetime.now().strftime("%H")
bottom = datetime.datetime.now().strftime("%M")
if len(top) < 2:
top = "0" * (2 - len(top)) + top
if len(bottom) < 2:
bottom = "0" * (2 - len(bottom)) + bottom
if ("-" in top and len(top) > 2) or ("-" in bottom and len(bottom) > 2):
time_split = 4*["-"]
elif "error" in top and "error" in bottom:
time_split = 4*["error"]
else:
time_split = [i for i in top] + [i for i in bottom]
if "-1" in top and len(top) != 2:
time_split = ["-1", top[-1]] + [i for i in bottom]
if "-1" in bottom and len(bottom) != 2:
time_split = [i for i in top] + ["-1", bottom[-1]]
for i in range(4):
x = digit_position[i][0]
y = digit_position[i][1]
number = digits[time_split[i]]
pixels[x: x + 5, y: y + 3] = np.array(number)
return pixels
def date_converter(self):
# nshape = (self.shape[0], int(self.shape[1]/2))
nshape = (16, 16)
today = datetime.datetime.today()
weekday = datetime.datetime.weekday(today)
# size of the reduced array according to weekday
size = [2,4,6,8,10,13,16]
pixels = days.copy() #base color background
lrow = np.append(pixels[15,:size[weekday]], [0 for i in range(16 - size[weekday])])
lrow = np.append(np.zeros((15,16)), lrow).reshape(nshape)
pixels += lrow
return pixels
def weather_converter(self, name):
"""Fills one half of the screen with weather info."""
# nshape = (self.shape[0], int(self.shape[1]/2))
nshape = (16, 16)
result = np.zeros(nshape)
cwd = __file__.replace("\\","/") # for windows
cwd = cwd.rsplit("/", 1)[0] # the current working directory (where this file is)
if len(cwd) == 0:
cwd = "."
icon_spritesheet = cwd + "/weather-icons.bmp"
icons = Image.open(icon_spritesheet)
icons_full = np.array(icons)
icon_loc = ["sun","moon","sun and clouds", "moon and clouds", "cloud","fog and clouds","2 clouds", "3 clouds", "rain and cloud", "rain and clouds", "rain and cloud and sun", "rain and cloud and moon", "thunder and cloud", "thunder and cloud and moon", "snow and cloud", "snow and cloud and moon", "fog","fog night"]
#ordered 1 2 \n 3 4 \ 5 5 ...
if name == "":
name = "error"
name = weather_categories[name]
try:
iy, ix = int(icon_loc.index(name)/2), icon_loc.index(name)%2
# x and y coords
except:
return np.zeros((*nshape,3))
icon_single = icons_full[16*iy:16*(iy + 1),16*ix:16*(ix + 1),...]
return icon_single
def matrix_add_depth(self, matrix, colors = []):
"""transforms a 2d-array with 0,1,2 to a 3d-array with the rgb values for primary and secondary color"""
c1 = self.primary
c2 = self.secondary
c3 = self.error
if len(colors) > 0:
c1 = colors[0]
if len(colors) > 1:
c2 = colors[1]
if len(colors) > 2:
c3 = colors[2]
if len(colors) > 3:
print("Too many colors")
r3 = np.zeros((matrix.shape[0],matrix.shape[1],3),dtype=int)
for i in range(matrix.shape[0]):
for j in range(matrix.shape[1]):
t = int(matrix[i, j])
if t == 0:
r3[i, j, :] = [0,0,0]
elif t == 1:
r3[i, j, :] = c1
elif t == 2:
r3[i, j, :] = c2
else:
r3[i, j, :] = c3
return r3
def clock_face(self, weather):
"""weather as a dict"""
hour = self.time_converter()
day = self.date_converter()
face1 = hour + day
# time + date:
face1_3d = self.matrix_add_depth(face1)
# weather icons
face2_3d = self.weather_converter(weather["weather"])
# weather temps
face3 = self.time_converter(top=str(weather["low"]), bottom=str(weather["high"]))
face3 = np.concatenate((face3[:8,...],2*face3[8:,...]))
face3_3d = self.matrix_add_depth(face3,[[0, 102, 255],[255, 102, 0]])
return [face1_3d, face2_3d, face3_3d]
def text_converter(self, text, height, color):
"""Converts a text to a pixel-matrix
returns: np.array((16, x, 3))"""
font = ImageFont.truetype("verdanab.ttf", height)
size = font.getsize(text)
img = Image.new("1",size,"black")
draw = ImageDraw.Draw(img)
draw.text((0, 0), text, "white", font=font)
pixels = np.array(img, dtype=np.uint8)
pixels3d = self.matrix_add_depth(pixels, color)
return pixels3d
def get_fallback(self):
hour = self.time_converter()
day = self.date_converter()
face1 = hour + day
face1_3d = self.matrix_add_depth(face1)
face2_3d = face3_3d = np.zeros((16,16,3))
return [face1_3d, face2_3d, face3_3d]

103
app/clock/helpers/shapes.py Normal file
View File

@@ -0,0 +1,103 @@
import numpy as np
digits = {
"1" : np.array([
[0,0,1],
[0,0,1],
[0,0,1],
[0,0,1],
[0,0,1]]),
"2" : np.array([
[1,1,1],
[0,0,1],
[1,1,1],
[1,0,0],
[1,1,1]]),
"3" : np.array([
[1,1,1],
[0,0,1],
[1,1,1],
[0,0,1],
[1,1,1]]),
"4" : np.array([
[1,0,1],
[1,0,1],
[1,1,1],
[0,0,1],
[0,0,1]]),
"5" : np.array([
[1,1,1],
[1,0,0],
[1,1,1],
[0,0,1],
[1,1,1]]),
"6" : np.array([
[1,1,1],
[1,0,0],
[1,1,1],
[1,0,1],
[1,1,1]]),
"7" : np.array([
[1,1,1],
[0,0,1],
[0,0,1],
[0,0,1],
[0,0,1]]),
"8" : np.array([
[1,1,1],
[1,0,1],
[1,1,1],
[1,0,1],
[1,1,1]]),
"9" : np.array([
[1,1,1],
[1,0,1],
[1,1,1],
[0,0,1],
[1,1,1]]),
"0" : np.array([
[1,1,1],
[1,0,1],
[1,0,1],
[1,0,1],
[1,1,1]]),
"-" : np.array([
[0,0,0],
[0,0,0],
[1,1,1],
[0,0,0],
[0,0,0]]),
"-1" : np.array([
[0,0,1],
[0,0,1],
[1,1,1],
[0,0,1],
[0,0,1]]),
"error" : np.array([
[1,0,1],
[1,0,1],
[0,1,0],
[1,0,1],
[1,0,1]]),
}
weather_categories = {
"Clouds": "cloud",
"Rain": "rain and cloud",
"Thunderstorm": "thunder and cloud",
"Drizzle": "rain and cloud",
"Snow": "snow and cloud",
"Clear": "sun",
"Mist": "fog and clouds",
"Smoke": "Smoke",
"Haze": "Haze",
"Dust": "Dust",
"Fog": "fog",
"Sand": "Sand",
"Dust": "Dust",
"Ash": "Ash",
"Squal": "Squal",
"Tornado": "Tornado",
"error" : "moon"
}

View File

@@ -0,0 +1,30 @@
from threading import Timer
import time
class RepeatedTimer(object):
def __init__(self, interval, function, *args, **kwargs):
self._timer = None
self.interval = interval
self.function = function
self.args = args
self.kwargs = kwargs
self.is_running = False
self.next_call = time.time()
self.start()
def _run(self):
self.is_running = False
self.start()
self.function(*self.args, **self.kwargs)
def start(self):
if not self.is_running:
self.next_call += self.interval
self._timer = Timer(self.next_call - time.time(), self._run)
self._timer.start()
self.is_running = True
def stop(self):
self._timer.cancel()
self.is_running = False

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

68
app/launcher.py Normal file
View File

@@ -0,0 +1,68 @@
import logging
from persistence import local_io, database
logger = logging.getLogger(__name__)
class Launcher:
"""Template for launching collections of modules"""
def __init__(self, **modules):
""""""
self.persistence = local_io.PersistentDict("persistence/prst.json")
self.db_utils = database.DatabaseUtils
self.modules = modules
logger.info(self.__class__.__name__ + " initialized")
if len(self.persistence) == 0:
self.init_persistence()
self.persistence["global"]["reboots"] += 1
self.launch_modules()
def launch_modules(self):
for module in self.modules.values():
logger.info("Starting module {}".format(module.__class__.__name__))
module.modules = self.modules
module.persistence = self.persistence
module.db_utils = self.db_utils()
module.start()
def init_persistence(self):
logger.warning("No persistence found, created a new one")
self.persistence["global"] ={
"lists" : {},
"reboots": 0
}
for m_name in self.modules.keys():
data = {}
if m_name == "bot":
data = {
"send_activity" : {"hour":[], "count":[]},
"receive_activity" : {"hour":[], "count":[]},
"execute_activity" : {"hour":[], "count":[]},
"log": [],
"chat_members": {},
"aliases" : {}
}
if m_name == "clock":
data = {
"sensors" : {
"time" : [],
"temperature":[],
"humidity":[],
"brightness" : [],
}
}
self.persistence[m_name] = data

12
app/persistence/README.md Normal file
View File

@@ -0,0 +1,12 @@
## What is happening here?
This "persistence"-module aims to standardize 2 things:
* the creation of a common set of variables that survives a potential (let's face it, likely) crash
* advanced logging and analytics
### Common variables
These are saved as a json file and are handled internally as a dict. Each change in the dict triggers a write to the file.
### Logging
A chunky sqlite-db which periodically gets new entries. From all modules. Ideally this db is then visualized through grafana. WIP

View File

@@ -0,0 +1 @@
#placeholder

View File

@@ -0,0 +1,98 @@
from . import models
from peewee import *
from playhouse.pool import PooledMySQLDatabase
import logging
logger = logging.getLogger(__name__)
import os
if os.getenv("dockerized", "") == "true":
import sys
sys.path.append("/keys")
import db_keys as keys
else:
from . import keys
dbk = keys.db_access
db_connection = PooledMySQLDatabase(
dbk["name"],
user=dbk["username"],
password=dbk["password"],
host=dbk["url"],
port=dbk["port"],
autorollback=True
)
def auto_connect_db(func):
def wrapper(*args, **kwargs):
#before:
db_connection.connect()
ret = func(*args, **kwargs)
#after:
db_connection.close()
# also, action is in scope now
return ret
return wrapper
class DatabaseUtils:
"""This object is the only database-related stuff getting exposed to the other modules. It must explicitly handle the connections!"""
def __init__(self) -> None:
# TODO specify arguments!
models.db.initialize(db_connection)
models.create_tables(db_connection)
@auto_connect_db
def chat_count(self, attribute):
if attribute == "read":
return models.ChatMetric.select().where(models.ChatMetric.read == True).count()
elif attribute == "send":
return models.ChatMetric.select().where(models.ChatMetric.send == True).count()
elif attribute == "execute":
return models.ChatMetric.select().where(models.ChatMetric.execute == True).count()
else: # does not exist
return -1
@auto_connect_db
def chat_log(self, **kwargs):
models.ChatMetric(**kwargs)
@auto_connect_db
def list_get(self, list_name=""):
if not list_name: # return all
cursor = models.List.select(models.List.name).execute()
return [k.name for k in cursor]
else:
return models.List.get(models.List.name == list_name).content_as_list
@auto_connect_db
def list_update(self, list_name, append="", replace=None):
if replace != None:
models.List.get(models.List.name == list_name).set_content(replace)
elif append:
l_obj = models.List.get(models.List.name == list_name)
l = l_obj.content_as_list
l.append(append)
l_obj.set_content(l)
else:
logger.warning("Empty update_list() query was made. Ignoring")
@auto_connect_db
def list_create(self, list_name):
models.List(name=list_name).save()
@auto_connect_db
def list_delete(self, list_name):
models.List.delete().where(models.List.name == list_name).execute()
@auto_connect_db
def sensor_log(self, **kwargs):
models.SensorMetric(**kwargs).save()

View File

@@ -0,0 +1,87 @@
import json
import os
class PersistentDict(dict):
"""Extended dict that writes its content to a file every time a value is changed"""
def __init__(self, file_name, *args, **kwargs):
"""initialization of the dict and of the required files"""
super().__init__(*args, **kwargs)
self.path = file_name
self.last_action = ""
try:
self.read_dict()
except:
with open(self.path, "a") as f:
f.write("{}")
## helper - functions
def write_dict(self):
with open(self.path, "w") as f:
json.dump(self, f)
self.last_action = "w"
def read_dict(self):
with open(self.path) as f:
tmp = dict(json.load(f))
for key in tmp:
super().__setitem__(key, tmp[key])
self.last_action = "r"
## extended dictionary - logic
def __setitem__(self, key, value):
if self.last_action != "r":
self.read_dict()
# not sure if the step to read is necessary, but I'll keep it for safety
super().__setitem__(key,value)
self.write_dict() # writes 'self' to a json file.
def __getitem__(self, key):
if self.last_action != "r":
self.read_dict()
ret_val = super().__getitem__(key)
if type(ret_val) != int and type(ret_val) != str:
ret_val = create_struct(type(ret_val), key, self, ret_val)
return ret_val
def clear(self):
super().clear()
self.write_dict()
return {}
def create_struct(struct_type, own_name, parent_name, *args, **kwargs):
class HookedStruct(struct_type):
def __init__(self, own_name, parent_name, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = own_name
self.parent = parent_name
def __setitem__(self, *args, **kwargs):
super().__setitem__(*args, **kwargs)
self.parent.__setitem__(self.name, self)
def __getitem__(self, *args, **kwargs):
ret_val = super().__getitem__(*args, **kwargs)
if type(ret_val) != int and type(ret_val) != str:
ret_val = create_struct(type(ret_val), args[0], self, ret_val)
return ret_val
def pop(self, *args):
retvalue = super().pop(*args)
self.parent.__setitem__(self.name, self)
return retvalue
def append(self, *args):
super().append(*args)
self.parent.__setitem__(self.name, self)
return HookedStruct(own_name, parent_name, *args, **kwargs)

56
app/persistence/models.py Normal file
View File

@@ -0,0 +1,56 @@
from datetime import datetime
from peewee import *
import logging
import json
logger = logging.getLogger(__name__)
def create_tables(db):
db.create_tables([SensorMetric, ChatMetric, ErrorMetric, List])
db = DatabaseProxy()
# set the nature of the db at runtime
class DBModel(Model):
# specific to the above DB
class Meta:
database = db
class Metric(DBModel):
time = DateTimeField(default = datetime.now())
### Actual metrics:
class SensorMetric(Metric):
# this is a continuous metric
temperature = FloatField()
humidity = FloatField()
luminosity = FloatField()
class ChatMetric(Metric):
read = BooleanField()
send = BooleanField()
execute = BooleanField()
class ErrorMetric(Metric):
# same as above
error = TextField()
class List(DBModel):
name = CharField(unique=True)
content = TextField(default="") # unlimited length, use to serialise list into
@property
def content_as_list(self):
return json.loads(self.content or '[]')
def set_content(self, list_content):
self.content = json.dumps(list_content)
self.save()

39
app/server.py Normal file
View File

@@ -0,0 +1,39 @@
# functionality
from bot import main
from clock import c_back
from broadcast import b_out
import launcher
import logging
import platform
import os
if os.getenv("dockerized", "") == "true" or platform.uname().node == "ArchSpectre":
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO
)
else:
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO,
filename='persistence/server.log',
)
class BroadcastLauncher(launcher.Launcher):
"""Launcher for all server-side modules. The hard-computations"""
def __init__(self):
self.bot_module = main.ChatBot(name="Norbit", version="4.1a")
self.clock_backend_module = c_back.ClockBackend() # threaded through threading.Timer
self.broadcast_module = b_out.BroadcastUpdates(port="1111") # Thread
super().__init__(
bot = self.bot_module,
clock = self.clock_backend_module,
broadcast = self.broadcast_module
)
BroadcastLauncher()