diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..63bbefa
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,11 @@
+[flake8]
+exclude =
+ .git,
+ __pycache__,
+ docs/source/conf.py,
+ old,
+ build,
+ dist,
+ venv/*,
+
+max-complexity = 10
\ No newline at end of file
diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml
new file mode 100644
index 0000000..91f332c
--- /dev/null
+++ b/.github/workflows/python-app.yml
@@ -0,0 +1,33 @@
+# This workflow will install Python dependencies, run tests and lint with a single version of Python
+# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
+
+name: Python application
+
+on:
+ push:
+ branches: [ master ]
+ pull_request:
+ branches: [ master ]
+
+jobs:
+ build:
+
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - name: Set up Python 3.8
+ uses: actions/setup-python@v2
+ with:
+ python-version: 3.8
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install flake8 pytest
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
+ - name: Lint with flake8
+ run: |
+ # stop the build if there are Python syntax errors or undefined names
+ flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
+ # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
+ flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index fabc081..89ba654 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,12 @@
+# native
+token.pickle
+reminder.csv
+credentials.json
+
+# virtual env
+bin/
+pyvenv.cfg
+
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
@@ -115,4 +124,4 @@ venv.bak/
dmypy.json
# Pyre type checker
-.pyre/
\ No newline at end of file
+.pyre/
diff --git a/Procfile b/Procfile
index bb83ddf..9aab65d 100644
--- a/Procfile
+++ b/Procfile
@@ -1 +1 @@
-bot: python laundrybot.py
\ No newline at end of file
+bot: python main/laundrybot.py
diff --git a/README.md b/README.md
index 4867ba5..7ce7cc3 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# NUSCollegeLaundryBot
Telegram BOT interface for the Laundry Machine Bot Project;
made with python-telegram-bot wrapper;
- made for RC4 by RC4Space
+ made for RC4 by Orcatech(RC4Space)
# The Bot
## Interface
diff --git a/laundrybot.py b/laundrybot.py
deleted file mode 100644
index e0c8328..0000000
--- a/laundrybot.py
+++ /dev/null
@@ -1,192 +0,0 @@
-# Laundry Bot for RC4, current telegram handle: @RC4LaundryBot
-
-import os
-import re
-import logging
-import requests
-from datetime import datetime
-import time
-from emoji import emojize
-from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode
-from telegram.ext import Updater, CommandHandler, CallbackQueryHandler
-
-# Set up logging
-logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
- level=logging.INFO)
-logger = logging.getLogger(__name__)
-
-# Initialize global variables
-RC_URL = "https://us-central1-rc4laundrybot.cloudfunctions.net/readData/RC4-"
-LAUNDRY_LEVELS = [5, 8, 11, 14, 17]
-MACHINES_INFO = {
- 'washer1': 'Washer 1',
- 'washer2': 'Washer 2',
- 'dryer1': 'Dryer 1',
- 'dryer2': 'Dryer 2'
-}
-
-# Building menu for every occasion
-def build_menu(buttons, n_cols, header_buttons=None, footer_buttons=None):
- menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)]
- if header_buttons:
- menu.insert(0, header_buttons)
- if footer_buttons:
- menu.append(footer_buttons)
- return InlineKeyboardMarkup(menu)
-
-# Building emojis for every occasion
-ebluediamond = emojize(":small_blue_diamond: ", use_aliases=True)
-etick = emojize(":white_check_mark: ", use_aliases=True)
-ecross = emojize(":x: ", use_aliases=True)
-esoon = emojize(":soon: ", use_aliases=True)
-
-# start command initializes:
-def check_handler(bot, update, user_data):
- user = update.message.from_user
- if 'pinned_level' in user_data:
- level_status(bot, update, user_data,
- from_pinned_level=True, new_message=True)
- else:
- ask_level(bot, update)
-
-def ask_level(bot, update):
- level_text = "Heyyo! I am RC4's Laundry Bot. As I am currently in [BETA] mode, I can only show details for Ursa floor.\n\nWhich laundry level do you wish to check?"
- level_buttons = []
- for level in LAUNDRY_LEVELS:
- label = 'Level {}'.format(level)
- data = 'set_L{}'.format(level)
- buttons = InlineKeyboardButton(text=label, callback_data=data) # data callback to set_pinned_level
- level_buttons.append(buttons)
- update.message.reply_text(text=level_text,
- reply_markup=build_menu(level_buttons, 1),
- parse_mode=ParseMode.HTML)
-
-def set_pinned_level(bot, update, user_data):
- query = update.callback_query
- level = int(re.match('^set_L(5|8|11|14|17)$', query.data).group(1))
- user_data['pinned_level'] = level
-
- level_status(bot, update, user_data, from_pinned_level=True)
-
-# Carves the status text for each level
-def make_status_text(level_number):
- laundry_data = ''
- floor_url = RC_URL + str(level_number)
-
- # Get Request to the database backend
- machine_status = requests.get(floor_url).json()
-
- for machine_id in MACHINES_INFO:
- if machine_status[machine_id] == 0:
- status_emoji = etick
- elif machine_status[machine_id] == 1:
- status_emoji = ecross
- else:
- status_emoji = esoon
- machine_name = MACHINES_INFO[machine_id]
- laundry_data += '{} {}\n'.format(status_emoji, machine_name)
-
- # TODO: This should be the backend server time instead
- current_time = datetime.fromtimestamp(time.time() + 8*3600).strftime('%d %B %Y %H:%M:%S')
-
- return "Showing statuses for Level {}:\n\n" \
- "{}\n" \
- "Last updated: {}\n".format(level_number, laundry_data, current_time)
-
-# Create the status menu which contains the help command, a pinned level number, and refresh button
-def make_status_menu(level_number):
- level_buttons = []
-
- for level in LAUNDRY_LEVELS:
- label = 'L{}'.format(level)
- data = 'check_L{}'.format(level)
- if level == level_number:
- # label = u'\u2022 ' + label + u' \u2022'
- label = ebluediamond + label
-
- button = InlineKeyboardButton(text=label, callback_data=data)
- level_buttons.append(button)
-
- refresh_button = [InlineKeyboardButton(
- text='Refresh',
- callback_data='check_L{}'.format(level_number)
- )]
-
- help_button = [InlineKeyboardButton(
- text='Help',
- callback_data='Help'
- )]
-
- return build_menu(level_buttons, 5, footer_buttons=refresh_button, header_buttons=help_button)
-
-def level_status(bot, update, user_data, from_pinned_level=False, new_message=False):
- query = update.callback_query
- if from_pinned_level:
- level = user_data['pinned_level']
- else:
- level = int(re.match('^check_L(5|8|11|14|17)$', query.data).group(1))
-
- user_data['check_level'] = level
-
- if new_message:
- update.message.reply_text(text=make_status_text(level),
- reply_markup=make_status_menu(level),
- parse_mode=ParseMode.HTML)
- else:
- bot.edit_message_text(text=make_status_text(level),
- chat_id=query.message.chat_id,
- message_id=query.message.message_id,
- reply_markup=make_status_menu(level),
- parse_mode=ParseMode.HTML)
-
-
-def help_menu(bot, update, user_data, from_pinned_level=False, new_message=False):
- query = update.callback_query
- help_text = "Help\n\n" + "Washer 1 and Dryer 2 accept coins\n" + etick + "= Available / Job done\n" + esoon + "= Job finishing soon\n" + ecross + "= In use\n"
- help_text += "\nInformation not accurate or faced a problem? Please message @PakornUe or @Cpf05, thank you!"
- help_text += "\n\nThis is a project by RC4Space's Laundry Bot Team. We appreciate your feedback as we are currently still beta-testing in Ursa before launching the college-wide implementation! :)"
-
- level = user_data['check_level']
-
- help_menu_button = [InlineKeyboardButton(
- text='Back',
- callback_data='check_L{}'.format(level)
- )]
-
- bot.edit_message_text(text=help_text,
- chat_id=query.message.chat_id,
- message_id=query.message.message_id,
- reply_markup=build_menu(help_menu_button, 1),
- parse_mode=ParseMode.HTML)
-
-
-def error(bot, update, error):
- logger.warning('Update "%s" caused error "%s"', update, error)
-
-def main():
- TOKEN = os.environ['RC4LAUNDRYBOT_TOKEN']
- #NAME = 'nuscollegelaundrybot'
- #PORT = os.environ.get('PORT')
-
- updater = Updater(TOKEN)
- dp = updater.dispatcher
-
- dp.add_handler(CommandHandler('start', check_handler, pass_user_data=True))
- dp.add_handler(CallbackQueryHandler(set_pinned_level,
- pattern='^set_L(5|8|11|14|17)$',
- pass_user_data=True))
- dp.add_handler(CallbackQueryHandler(level_status,
- pattern='^check_L(5|8|11|14|17)$',
- pass_user_data=True))
- dp.add_handler(CallbackQueryHandler(help_menu,
- pattern='Help',
- pass_user_data=True))
- dp.add_error_handler(error)
-
- #updater.start_webhook(listen="0.0.0.0", port=int(PORT), url_path=TOKEN)
- #updater.bot.setWebhook("https://{}.herokuapp.com/{}".format(NAME, TOKEN))
- updater.start_polling()
- updater.idle()
-
-if __name__ == '__main__':
- main()
diff --git a/__init__.py b/main/__init__.py
similarity index 100%
rename from __init__.py
rename to main/__init__.py
diff --git a/main/data.py b/main/data.py
new file mode 100644
index 0000000..f96d666
--- /dev/null
+++ b/main/data.py
@@ -0,0 +1,45 @@
+from datetime import datetime, timedelta
+
+class MockData():
+ def getLevels(self):
+ return [5, 8, 11, 14, 17]
+
+ def getStatuses(self, level):
+ count = 4
+
+ machine_types = [
+ "washer-coin",
+ "washer-ezlink",
+ "dryer-ezlink",
+ "dryer-coin"
+ ]
+ start_times = [
+ datetime.now() - timedelta(seconds=100),
+ datetime.now() - timedelta(seconds=200),
+ datetime.now() - timedelta(seconds=300),
+ datetime.now() - timedelta(seconds=400)
+ ]
+ statuses = [0, 2, 1, 2]
+ machine_durations = [30, 30, 40, 40]
+
+ return [
+ {
+ "level": level,
+ "type": machine_types[i],
+ "start-time": start_times[i],
+ "status": statuses[i],
+ "time": datetime.now(),
+ "machine-duration": machine_durations[i]
+ } for i in range(0, count)
+ ]
+
+ def charts(self):
+ chart = {"Mon": [], "Tue": [], "Wed": [], "Thu": [], "Fri": []}
+ n = 0
+
+ for day in chart:
+ chart[day] = [x**2 + n for x in range(0, 72)]
+ n += 1
+
+ return chart
+
\ No newline at end of file
diff --git a/main/laundrybot.py b/main/laundrybot.py
new file mode 100644
index 0000000..7088322
--- /dev/null
+++ b/main/laundrybot.py
@@ -0,0 +1,518 @@
+# Laundry Bot for RC4, current telegram handle: @RC4LaundryBot
+
+import os
+import csv
+import re
+import logging
+import requests
+import threading
+from datetime import datetime
+import time
+from emoji import emojize
+from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, ReplyKeyboardMarkup, KeyboardButton
+from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, MessageHandler, ConversationHandler, Filters
+from sheets import add_response
+
+from reminders import ReminderList
+from data import MockData
+from string import Template
+import pandas as pd
+
+# Set up logging
+logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
+ level=logging.INFO)
+logger = logging.getLogger(__name__)
+logging.getLogger('googleapiclient.discovery_cache').setLevel(logging.ERROR) # Silence oauth2client ImportError
+
+# Initialize global variables
+RC_URL = "https://us-central1-rc4laundrybot.cloudfunctions.net/readData/RC4-"
+LAUNDRY_LEVELS = [5, 8, 11, 14, 17]
+MACHINES_INFO = {
+ 'washer-coin': 'Washer 1',
+ 'washer-ezlink': 'Washer 2',
+ 'dryer-ezlink': 'Dryer 1',
+ 'dryer-coin': 'Dryer 2'
+}
+user_response = ''
+DATA = MockData()
+REMINDERS = ReminderList()
+
+
+# Building menu for every occasion
+def build_menu(buttons, n_cols, header_buttons=None, row_buttons=None,
+ footer_buttons=None):
+ menu = [buttons[i:i + n_cols] for i in range(0, len(buttons), n_cols)]
+ if header_buttons:
+ menu.insert(0, header_buttons)
+ if row_buttons:
+ for b in row_buttons:
+ menu.append(b)
+ if footer_buttons:
+ menu.append(footer_buttons)
+ return InlineKeyboardMarkup(menu)
+
+
+# Checks whether 'pinned_level' is null
+def pinned_level_exists(bot, update, user_data, chat_id=None):
+ if 'pinned_level' in user_data:
+ return True
+
+ text = "The message you clicked is outdated. Please enter /start first before proceeding."
+ if chat_id:
+ bot.send_message(
+ text=text,
+ chat_id=chat_id
+ )
+ else:
+ bot.send_message(
+ text=text,
+ chat_id=update.callback_query.message.chat_id
+ )
+
+ return False
+
+
+# Building emojis for every occasion
+ebluediamond = emojize(":small_blue_diamond: ", use_aliases=True)
+etick = emojize(":white_check_mark: ", use_aliases=True)
+ecross = emojize(":x: ", use_aliases=True)
+esoon = emojize(":soon: ", use_aliases=True)
+ehourglass = emojize(":hourglass:", use_aliases=True)
+ebarchart = emojize(':bar_chart:', use_aliases=True)
+ealarm = emojize(':alarm_clock:', use_aliases=True)
+erefresh = emojize(':arrows_counterclockwise:', use_aliases=True)
+equestion = emojize(':question:', use_aliases=True)
+
+
+# start command initializes:
+def check_handler(bot, update, user_data):
+
+ logging.info("Checking handler of chat id {}".format(update.message.chat_id))
+
+ # user = update.message.from_user
+ if 'pinned_level' in user_data:
+ level_status(bot, update, user_data,
+ from_pinned_level=True, new_message=True)
+ else:
+ ask_level(bot, update)
+
+
+def ask_level(bot, update):
+
+ logging.info("Ask level of chat id {}".format(update.message.chat_id))
+
+ level_text = "Heyyo! I am RC4's Laundry Bot. As I am currently in [BETA] mode, "\
+ "I can only show details for Ursa floor.\n\nWhich laundry level do you wish to check?"
+ level_buttons = []
+ for level in LAUNDRY_LEVELS:
+ label = 'Level {}'.format(level)
+ data = 'set_L{}'.format(level)
+ buttons = InlineKeyboardButton(text=label, callback_data=data) # data callback to set_pinned_level
+ level_buttons.append(buttons)
+ update.message.reply_text(text=level_text,
+ reply_markup=build_menu(level_buttons, 1),
+ parse_mode=ParseMode.HTML)
+
+
+def set_pinned_level(bot, update, user_data):
+
+ query = update.callback_query
+ level = int(re.match('^set_L(5|8|11|14|17)$', query.data).group(1))
+ user_data['pinned_level'] = level
+
+ logging.info("Set chat id {} pinned level as {}".format(query.message.chat_id, level))
+
+ level_status(bot, update, user_data, from_pinned_level=True)
+
+
+# Generate a Hour Minute Second template
+class DeltaTemplate(Template):
+ ''' Set a template for input data '''
+ delimiter = "%"
+
+# Change from timedelta to H M S format without unecessary microsecond
+def strfdelta(tdelta, fmt):
+ ''' Format timedelta object to the format of string fmt
+
+ Parameter
+ ----------
+ tdelta: datetime.timedelta
+ The datetime.timedelta object that needs to be formatted
+ fmt: str
+ The format string (eg. %H:%M:%S)
+
+ Returns
+ -------
+ str
+ a formmated time string
+ '''
+
+ d = {"D": tdelta.days}
+ hours, rem = divmod(tdelta.seconds, 3600)
+ minutes, seconds = divmod(rem, 60)
+ d["H"] = '{:02d}'.format(hours)
+ d["M"] = '{:02d}'.format(minutes)
+ d["S"] = '{:02d}'.format(seconds)
+ t = DeltaTemplate(fmt)
+ return t.substitute(**d)
+
+
+# Carves the status text for each level
+def make_status_text(level_number):
+ laundry_data = ''
+ floor_url = RC_URL + str(level_number)
+ # TODO: This should be the backend server time instead
+ current_time = datetime.fromtimestamp(time.time() + 8*3600).strftime('%d %B %Y %H:%M:%S')
+
+ # Get Request to the database backend
+ # machine_status = requests.get(floor_url).json()
+
+ # Use mock data
+ machine_data = DATA.getStatuses(level_number)
+
+ for machine in machine_data:
+ # Get data from back end - time since request/refresh
+ remaining_time = datetime.today() - machine["start-time"]
+ remaining_time = strfdelta(remaining_time, '%H:%M:%S')
+
+ if machine["status"] == 0:
+ status_emoji = etick
+ else:
+ status_emoji = f'{ehourglass} {remaining_time} |'
+
+ machine_name = machine["type"]
+ laundry_data += '{} {}\n'.format(status_emoji, machine_name)
+
+ return "Showing statuses for Level {}:\n\n" \
+ "{}\n" \
+ "Last updated: {}\n".format(level_number, laundry_data, current_time)
+
+
+# Create the status menu which contains the help command, a pinned level number, and refresh button
+def make_status_menu(level_number):
+ level_buttons = []
+
+ for level in LAUNDRY_LEVELS:
+ label = 'L{}'.format(level)
+ data = 'check_L{}'.format(level)
+ if level == level_number:
+ # label = u'\u2022 ' + label + u' \u2022'
+ label = ebluediamond + label
+
+ button = InlineKeyboardButton(text=label, callback_data=data)
+ level_buttons.append(button)
+
+ refresh_button = [InlineKeyboardButton(
+ text='Refresh {}'.format(erefresh),
+ callback_data='check_L{}'.format(level_number)
+ )]
+
+ help_button = [InlineKeyboardButton(
+ text='Help',
+ callback_data='Help'
+ )]
+
+ report_button = [InlineKeyboardButton(
+ text='Something wrong?',
+ callback_data='Report'
+ )]
+
+ reminder_button = [InlineKeyboardButton(
+ text="Set a reminder {}".format(ealarm),
+ callback_data="remind"
+ )]
+
+ insights_button = [InlineKeyboardButton(
+ text="Get Chart Insights {}".format(ebarchart),
+ callback_data="insight"
+ )]
+
+ row_buttons = [reminder_button, insights_button]
+
+ header_buttons = help_button + report_button
+
+ logging.info("Returning menu")
+
+ return build_menu(level_buttons, 5, footer_buttons=refresh_button,
+ header_buttons=header_buttons, row_buttons=row_buttons)
+
+
+def level_status(bot, update, user_data, from_pinned_level=False, new_message=False):
+
+ if not pinned_level_exists(bot, update, user_data):
+ return
+
+ logging.info("Chat id {} checks level {} status".format(
+ update.callback_query.message.chat_id,
+ user_data['pinned_level']
+ ))
+
+ query = update.callback_query
+ if from_pinned_level:
+ level = user_data['pinned_level']
+ else:
+ level = int(re.match('^check_L(5|8|11|14|17)$', query.data).group(1))
+
+ if new_message:
+ logging.info("Chat id {} sent first status message".format(
+ update.callback_query.message.chat_id
+ ))
+ update.message.reply_text(text=make_status_text(level),
+ reply_markup=make_status_menu(level),
+ parse_mode=ParseMode.HTML)
+ else:
+ logging.info("Chat id {} edited status message".format(
+ update.callback_query.message.chat_id
+ ))
+ bot.edit_message_text(
+ text=make_status_text(level),
+ chat_id=query.message.chat_id,
+ message_id=query.message.message_id,
+ reply_markup=make_status_menu(level),
+ parse_mode=ParseMode.HTML
+ )
+
+
+def help_menu(bot, update, user_data, from_pinned_level=False, new_message=False):
+
+ if not pinned_level_exists(bot, update, user_data):
+ return
+
+ query = update.callback_query
+ help_text = "Help\n\n" + "Washer 1 and Dryer 2 accept coins\n" \
+ + etick + "= Available / Job done\n" + esoon + "= Job finishing soon\n" + ecross + "= In use\n"
+
+ help_text += "\nInformation not accurate or faced a problem? "\
+ "Please message @PakornUe or @Cpf05, thank you!"
+ help_text += "\n\nThis is a project by RC4Space's Laundry Bot Team. "\
+ "We appreciate your feedback as we are currently still beta-testing "\
+ "in Ursa before launching the college-wide implementation! :)"
+
+ level = user_data['pinned_level']
+
+ help_menu_button = [InlineKeyboardButton(
+ text='Back',
+ callback_data='check_L{}'.format(level)
+ )]
+
+ bot.edit_message_text(
+ text=help_text,
+ chat_id=query.message.chat_id,
+ message_id=query.message.message_id,
+ reply_markup=build_menu(help_menu_button, 1),
+ parse_mode=ParseMode.HTML
+ )
+
+
+def report(bot, update, user_data, from_pinned_level=False, new_message=False):
+
+ if not pinned_level_exists(bot, update, user_data):
+ return
+
+ query = update.callback_query
+
+ text = 'Is there anything wrong? Tell me in a couple of sentences.\n'
+ text += 'Elaborate on any incorrect information!'
+ bot.send_message(
+ text = text,
+ chat_id = query.message.chat_id
+ )
+ return 1
+
+
+def get_response_ask_consent(bot, update, user_data):
+ global user_response
+
+ user_input = update.message.text
+ user_response = user_input
+ query = update.callback_query
+
+ yes_button = KeyboardButton(text='Yes', callback_data='Yes')
+ no_button = KeyboardButton(text='No', call_back='No')
+ consent_button = [yes_button, no_button]
+
+ text = 'Thank you for reporting!\n'
+ text += 'Do you consent to give this information along with your telegram use ID to the developer team?'
+
+ bot.send_message(
+ text = text,
+ chat_id=update.message.chat_id,
+ reply_markup=ReplyKeyboardMarkup(keyboard=[consent_button], one_time_keyboard=True)
+ )
+
+ return 2
+
+
+def get_consent_end(bot, update, user_data):
+ user_input = update.message.text
+ query = update.callback_query
+
+ if user_input == 'Yes':
+ text = 'Huge thanks!\n'
+ username = update.message.chat.username
+ level = user_data['pinned_level']
+ add_response(username, level, user_response)
+ else:
+ text = 'Okay, your response was not recorded.\n'
+ text += 'Enter /start to restart the bot.'
+
+ bot.send_message(
+ text = text,
+ chat_id = update.message.chat_id
+ )
+
+ return ConversationHandler.END
+
+def error(bot, update, error):
+ logger.warning('Update "%s" caused error "%s"', update, error)
+
+def remind(bot, update, user_data):
+ ''' Set up reminder function interface
+
+ User will be bring to a prompt which show machines that are in used
+ and can be set a reminder for (machines that are not in used cannot
+ be set a reminder function).
+
+ When machine buttons is clicked:
+ data will be sent to add_reminder()
+ When back button is clicked:
+ user will be sent back to origianl prompt
+ '''
+
+ if not pinned_level_exists(bot, update, user_data):
+ return
+
+ query = update.callback_query
+ level = user_data['pinned_level']
+ selection = []
+
+ #Mock test
+ machine_data = DATA.getStatuses(level)
+
+ question = "Which machine on Level {} do you like to set a reminder for?\n".format(level) \
+ + "You can only set reminders for busy machines."
+
+ # Put in-use machines in selection list
+ for machine in machine_data:
+ if machine["status"] != 0:
+ label = machine['type']
+ data = machine['type']
+ button = InlineKeyboardButton(text=label, callback_data=data)
+ selection.append(button)
+
+ back_button = [InlineKeyboardButton(
+ text='Back',
+ callback_data='check_L{}'.format(level)
+ )]
+
+ bot.edit_message_text(
+ text=question,
+ chat_id=query.message.chat_id,
+ message_id=query.message.message_id,
+ reply_markup=build_menu(selection, len(selection), footer_buttons=back_button),
+ parse_mode=ParseMode.HTML
+ )
+
+def add_reminder(bot, update, user_data):
+ ''' Append current time, username, level and machine to Google sheet
+
+ Append current date, current time, username, level and machine data to Google sheet
+ to save reminders. The data is saved in Laundrybot sheet under Reminder tab.
+ '''
+ query = update.callback_query
+
+ # User inputs
+ input_data = {
+ 'current_date': datetime.fromtimestamp(time.time() + 8*3600).strftime('%d %B %Y'),
+ 'current_time': datetime.fromtimestamp(time.time() + 8*3600).strftime('%H:%M:%S'),
+ 'username': query['from_user']['username'],
+ 'level': user_data['pinned_level'],
+ 'machine-type': query['data'],
+ }
+
+ # Mock data for temporary use
+ machine_data = list(
+ filter(
+ lambda x: x['type'] == input_data['machine-type'],
+ DATA.getStatuses(input_data['level'])
+ )
+ ).pop()
+
+ notice = 'A reminder has been set for Level {} {}'.format(input_data['level'],input_data['machine-type'])
+
+ # The following is the format of reminders logging
+ REMINDERS.append({
+ 'username': query['from_user']['username'],
+ 'chat_id': query.message.chat_id,
+ 'input_data': input_data,
+ 'machine_data': machine_data
+ })
+
+ # with open('../reminder.csv', "a",newline='') as file:
+ # file_reader = csv.reader(file,delimiter=',')
+ # fieldsnames = ['current_date','current_time','username','level','machine-type']
+ # writer = csv.DictWriter(file,fieldnames=fieldsnames)
+ # writer.writerow(input_data)
+
+ back_button = [InlineKeyboardButton(
+ text='Back',
+ callback_data='check_L{}'.format(input_data['level'])
+ )]
+
+ bot.edit_message_text(
+ text=notice,
+ chat_id=query.message.chat_id,
+ message_id=query.message.message_id,
+ reply_markup=build_menu(back_button, 1),
+ parse_mode=ParseMode.HTML,
+ )
+
+def run_bot(updater):
+
+ dp = updater.dispatcher
+
+ # dp.add_handler used to receive back querry
+ # to call a function using a button, passed in pattern= callback_data
+ dp.add_handler(CommandHandler('start', check_handler, pass_user_data=True))
+ dp.add_handler(CallbackQueryHandler(set_pinned_level,
+ pattern='^set_L(5|8|11|14|17)$',
+ pass_user_data=True))
+ dp.add_handler(CallbackQueryHandler(level_status,
+ pattern='^check_L(5|8|11|14|17)$',
+ pass_user_data=True))
+ dp.add_handler(CallbackQueryHandler(help_menu,
+ pattern='Help',
+ pass_user_data=True))
+ dp.add_handler(CallbackQueryHandler(remind,
+ pattern='remind',
+ pass_user_data=True))
+ dp.add_handler(CallbackQueryHandler(add_reminder,
+ pattern='^(washer-coin|washer-ezlink|dryer-ezlink|dryer-coin)$',
+ pass_user_data=True))
+ dp.add_handler(ConversationHandler(
+ [CallbackQueryHandler(report, pattern="Report", pass_user_data=True)],
+ {
+ 1: [MessageHandler(Filters.text, get_response_ask_consent, pass_user_data=True)],
+ 2: [MessageHandler(Filters.text, get_consent_end, pass_user_data=True)]
+ },
+ []))
+ dp.add_error_handler(error)
+
+ updater.start_polling()
+ updater.idle()
+
+def main():
+
+ TOKEN = os.environ['RC4LAUNDRYBOT_TOKEN']
+
+ updater = Updater(TOKEN)
+
+ t = threading.Thread(target=REMINDERS.poll, args=(updater.bot,))
+ t.start()
+
+ run_bot(updater)
+
+
+
+if __name__ == '__main__':
+ main()
diff --git a/main/reminders.py b/main/reminders.py
new file mode 100644
index 0000000..926d711
--- /dev/null
+++ b/main/reminders.py
@@ -0,0 +1,56 @@
+import datetime
+import time
+
+from emoji import emojize
+from telegram import ParseMode
+
+REMINDER_FILE_PATH = '../reminder.csv'
+
+class ReminderList(list):
+
+ """
+ Format for each reminder is
+ {
+ 'username': query['from_user']['username'],
+ 'chat_id': query.message.chat_id,
+ 'input_data': input_data,
+ 'machine_data': machine_data
+ }
+ """
+
+ def poll(self, bot):
+ while(True):
+ for reminder in self:
+
+ remaining_time = (
+ reminder['machine_data']['start-time'] +
+ datetime.timedelta(minutes=reminder['machine_data']['machine-duration'])
+ ) - datetime.datetime.fromtimestamp(time.time() + 8*3600)
+
+ print(
+ "Reminder remaining time for user {}: ".format(reminder['username'])
+ + str(remaining_time.seconds) + "\n"
+ )
+
+ if remaining_time < datetime.timedelta(minutes=5):
+ x = self.pop(0)
+
+ remaining_time_text = " ".join([
+ str(remaining_time.seconds // 60), "minutes",
+ str(remaining_time.seconds % 60), "seconds"
+ ])
+
+ text = "The {} machine in level {} is done in {}. ".format(
+ x['machine_data']['type'],
+ x['machine_data']['level'],
+ remaining_time_text
+ ) + "Get your clothes ready!" + emojize(":basket:", use_aliases=True)
+
+ print(x)
+
+ bot.send_message(
+ text=text,
+ chat_id=x['chat_id'],
+ parse_mode=ParseMode.HTML
+ )
+ time.sleep(60)
diff --git a/main/sheets.py b/main/sheets.py
new file mode 100644
index 0000000..11b2848
--- /dev/null
+++ b/main/sheets.py
@@ -0,0 +1,67 @@
+import json
+import pickle
+import os.path
+from googleapiclient.discovery import build
+from google_auth_oauthlib.flow import InstalledAppFlow
+from google.auth.transport.requests import Request
+from datetime import datetime
+
+FILE_PATH = os.path.dirname(__file__)
+SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
+SPREADSHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo'
+RANGE_NAME = 'Error!A2:D'
+
+
+def authorize_creds():
+
+ creds = None
+
+ if os.path.exists(os.path.join(FILE_PATH, '../token.pickle')):
+ with open(os.path.join(FILE_PATH, '../token.pickle'), 'rb') as token:
+ creds = pickle.load(token)
+
+ # If there are no (valid) credentials available, let the user log in.
+ if not creds or not creds.valid:
+ if creds and creds.expired and creds.refresh_token:
+ creds.refresh(Request())
+ elif not os.path.exists(os.path.join(FILE_PATH, '../credentials.json')):
+ json_str = os.environ.get('CREDENTIALS_JSON')
+ print(json.dumps(json.loads(json_str), indent=4, sort_keys=True))
+ with open(os.path.join(FILE_PATH, '../credentials.json'), 'w') as f:
+ f.write(json_str)
+ f.close()
+
+ flow = InstalledAppFlow.from_client_secrets_file(
+ os.path.join(FILE_PATH, '../credentials.json'), SCOPES)
+ creds = flow.run_console()
+
+ # Save the credentials for the next run
+ with open(os.path.join(FILE_PATH, '../token.pickle'), 'wb') as token:
+ pickle.dump(creds, token)
+
+ return creds
+
+
+def add_response(username, level, response):
+
+ # Authorize requests to read/write
+ creds = authorize_creds()
+
+ service = build('sheets', 'v4', credentials=creds)
+
+ # Call the Sheets API
+ sheet = service.spreadsheets()
+
+ response_time = datetime.now().strftime("%d/%m/%Y %H:%M:%S")
+ values = [[response_time, username, level, response]]
+ body = {'values': values}
+
+ # Append the data on the last row
+ result = sheet.values().append(
+ spreadsheetId=SPREADSHEET_ID, range=RANGE_NAME,
+ valueInputOption='RAW', body=body).execute()
+
+
+if __name__ == '__main__':
+ print("Testing credentials...")
+ print(authorize_creds())
diff --git a/reminder.csv b/reminder.csv
new file mode 100644
index 0000000..e69de29
diff --git a/requirements.txt b/requirements.txt
index e28231c..ef05431 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,4 +1,7 @@
-python_telegram_bot==10.0.1
-telegram==0.0.1
+python_telegram_bot==12.8
emoji
-requests
\ No newline at end of file
+requests
+google-api-python-client
+google-auth-httplib2
+google-auth-oauthlib
+pandas
diff --git a/runtime.txt b/runtime.txt
new file mode 100644
index 0000000..0fd6938
--- /dev/null
+++ b/runtime.txt
@@ -0,0 +1 @@
+python-3.8.6