From 1cbd11cbfb2445372fd9c2dc23b177d7f225bfb6 Mon Sep 17 00:00:00 2001 From: Ivander Jonathan <57558909+ivanderjmw@users.noreply.github.com> Date: Tue, 17 Nov 2020 17:52:23 +0800 Subject: [PATCH 01/38] Add python CI --- .github/workflows/python-app.yml | 36 ++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/python-app.yml diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml new file mode 100644 index 0000000..c7f5067 --- /dev/null +++ b/.github/workflows/python-app.yml @@ -0,0 +1,36 @@ +# 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 + - name: Test with pytest + run: | + pytest From 76c6e4272110606b4ef3c5e7c056522cf9fe8ef2 Mon Sep 17 00:00:00 2001 From: Ivander Date: Tue, 17 Nov 2020 18:14:09 +0800 Subject: [PATCH 02/38] Update gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index fabc081..e82a0d5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# virtual env +bin/ +pyvenv.cfg + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -115,4 +119,4 @@ venv.bak/ dmypy.json # Pyre type checker -.pyre/ \ No newline at end of file +.pyre/ From d607df923a7dbcc74d6854bc8a97a76b7cd89651 Mon Sep 17 00:00:00 2001 From: Ivander Date: Wed, 18 Nov 2020 11:08:50 +0800 Subject: [PATCH 03/38] Use python-telegram-bot v12.8 --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e28231c..0deb8b8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -python_telegram_bot==10.0.1 +python_telegram_bot==12.8 telegram==0.0.1 emoji requests \ No newline at end of file From 438a0008a1f54895e9915680ecad95b4024bcabb Mon Sep 17 00:00:00 2001 From: Ivander Date: Wed, 18 Nov 2020 11:39:48 +0800 Subject: [PATCH 04/38] Checkstyle with flake8 --- .flake8 | 11 ++++++ .github/workflows/python-app.yml | 9 ++--- laundrybot.py | 68 ++++++++++++++++++++------------ 3 files changed, 56 insertions(+), 32 deletions(-) create mode 100644 .flake8 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 index c7f5067..ac6a468 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,10 +16,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 + - name: Set up Python 3.7.4 uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.7.4 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -30,7 +30,4 @@ jobs: # 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 - - name: Test with pytest - run: | - pytest + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics \ No newline at end of file diff --git a/laundrybot.py b/laundrybot.py index e0c8328..9fe988a 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -25,6 +25,7 @@ '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)] @@ -34,33 +35,38 @@ def build_menu(buttons, n_cols, header_buttons=None, footer_buttons=None): 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: + +# start command initializes: def check_handler(bot, update, user_data): - user = update.message.from_user + # 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?" + +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 + 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)) @@ -68,6 +74,7 @@ def set_pinned_level(bot, update, user_data): 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 = '' @@ -93,6 +100,7 @@ def make_status_text(level_number): "{}\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 = [] @@ -101,9 +109,9 @@ def make_status_menu(level_number): label = 'L{}'.format(level) data = 'check_L{}'.format(level) if level == level_number: - # label = u'\u2022 ' + label + u' \u2022' + # label = u'\u2022 ' + label + u' \u2022' label = ebluediamond + label - + button = InlineKeyboardButton(text=label, callback_data=data) level_buttons.append(button) @@ -119,13 +127,14 @@ def make_status_menu(level_number): 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: @@ -133,19 +142,26 @@ def level_status(bot, update, user_data, from_pinned_level=False, new_message=Fa 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) + 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! :)" - + 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( @@ -153,20 +169,21 @@ def help_menu(bot, update, user_data, from_pinned_level=False, new_message=False 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) + 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 @@ -183,10 +200,9 @@ def main(): 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() From 1f9a4d508cc8a5563f18f09a4bba3896bbd5d40b Mon Sep 17 00:00:00 2001 From: Ivander Date: Wed, 18 Nov 2020 11:41:17 +0800 Subject: [PATCH 05/38] Change to available python version --- .github/workflows/python-app.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-app.yml b/.github/workflows/python-app.yml index ac6a468..91f332c 100644 --- a/.github/workflows/python-app.yml +++ b/.github/workflows/python-app.yml @@ -16,10 +16,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.7.4 + - name: Set up Python 3.8 uses: actions/setup-python@v2 with: - python-version: 3.7.4 + python-version: 3.8 - name: Install dependencies run: | python -m pip install --upgrade pip From 06d1926767f3f4ba5661affe45e60cfadd57ef80 Mon Sep 17 00:00:00 2001 From: Ivander Jonathan <57558909+ivanderjmw@users.noreply.github.com> Date: Wed, 18 Nov 2020 11:47:19 +0800 Subject: [PATCH 06/38] Create runtime.txt --- runtime.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 runtime.txt 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 From 7a1f9fbbc74e34b33c8265e5938c844a52f32391 Mon Sep 17 00:00:00 2001 From: Ivander Date: Wed, 18 Nov 2020 12:20:14 +0800 Subject: [PATCH 07/38] Remove telegram --- requirements.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 0deb8b8..ef0077a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ -python_telegram_bot==12.8 -telegram==0.0.1 +python_telegram_bot==13.0 emoji requests \ No newline at end of file From 3b8ee6fb316e2e9e8780f25ca13860d26413ba81 Mon Sep 17 00:00:00 2001 From: Ivander Jonathan <57558909+ivanderjmw@users.noreply.github.com> Date: Wed, 18 Nov 2020 12:27:59 +0800 Subject: [PATCH 08/38] Update requirements.txt Revert upgrade to 13.0 --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index ef0077a..55af6d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -python_telegram_bot==13.0 +python_telegram_bot==12.8 emoji -requests \ No newline at end of file +requests From e41cbae9f1bfe9784deb53f5fe9b4e9e6e26b1e0 Mon Sep 17 00:00:00 2001 From: Ivander Date: Wed, 18 Nov 2020 20:24:34 +0800 Subject: [PATCH 09/38] Change to Orcatech --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b77f6d..ea9d5d0 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 From a2b2b8c7511490a8b3bb92566f5edc487d59e312 Mon Sep 17 00:00:00 2001 From: Yuuki Date: Sat, 28 Nov 2020 23:30:14 +0800 Subject: [PATCH 10/38] Add remaining time to the shown status --- laundrybot.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/laundrybot.py b/laundrybot.py index 9fe988a..98b5f04 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -41,6 +41,7 @@ def build_menu(buttons, n_cols, header_buttons=None, footer_buttons=None): 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) # start command initializes: @@ -83,15 +84,21 @@ def make_status_text(level_number): # Get Request to the database backend machine_status = requests.get(floor_url).json() + for machine_id in MACHINES_INFO: + + # Get data from back end - time since request/refresh + remaining_time = 'mm:ss' + if machine_status[machine_id] == 0: status_emoji = etick elif machine_status[machine_id] == 1: status_emoji = ecross else: - status_emoji = esoon + status_emoji = f'{ehourglass} {remaining_time} |' + machine_name = MACHINES_INFO[machine_id] - laundry_data += '{} {}\n'.format(status_emoji, machine_name) + 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') From add85f81066c4fc502eabf0f6875cd538c9ec437 Mon Sep 17 00:00:00 2001 From: timotius-121 Date: Tue, 8 Dec 2020 17:15:14 +0800 Subject: [PATCH 11/38] Update requirements.txt --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index e28231c..6253eef 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ python_telegram_bot==10.0.1 -telegram==0.0.1 emoji requests \ No newline at end of file From 148cc5bd185ad67e8a359436dcb494ce26ef0592 Mon Sep 17 00:00:00 2001 From: Ivander Date: Wed, 16 Dec 2020 22:23:46 +0800 Subject: [PATCH 12/38] Add and use mock data --- data.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ laundrybot.py | 26 +++++++++++++++----------- 2 files changed, 60 insertions(+), 11 deletions(-) create mode 100644 data.py diff --git a/data.py b/data.py new file mode 100644 index 0000000..18c181a --- /dev/null +++ b/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, 0, 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/laundrybot.py b/laundrybot.py index 98b5f04..c51d099 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -10,6 +10,8 @@ from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode from telegram.ext import Updater, CommandHandler, CallbackQueryHandler +from data import MockData + # Set up logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) @@ -19,11 +21,12 @@ 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' + 'washer-coin': 'Washer 1', + 'washer-ezlink': 'Washer 2', + 'dryer-ezlink': 'Dryer 1', + 'dryer-coin': 'Dryer 2' } +DATA = MockData() # Building menu for every occasion @@ -82,22 +85,23 @@ def make_status_text(level_number): floor_url = RC_URL + str(level_number) # Get Request to the database backend - machine_status = requests.get(floor_url).json() + # machine_status = requests.get(floor_url).json() + # Use mock data + machine_data = DATA.getStatuses(level_number) - for machine_id in MACHINES_INFO: - + for machine in machine_data: # Get data from back end - time since request/refresh remaining_time = 'mm:ss' - if machine_status[machine_id] == 0: + if machine["status"] == 0: status_emoji = etick - elif machine_status[machine_id] == 1: + elif machine["status"] == 1: status_emoji = ecross else: - status_emoji = f'{ehourglass} {remaining_time} |' + status_emoji = f'{ehourglass} {remaining_time} |' - machine_name = MACHINES_INFO[machine_id] + machine_name = machine["type"] laundry_data += '{} {}\n'.format(status_emoji, machine_name) # TODO: This should be the backend server time instead From 738bf0932542107e8fe2d4d5bea480df1a4993b3 Mon Sep 17 00:00:00 2001 From: timotius-121 Date: Wed, 16 Dec 2020 22:27:32 +0800 Subject: [PATCH 13/38] Added sheets.py --- credentials.json | 1 + laundrybot.py | 1 + sheets.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ token.pickle | Bin 0 -> 703 bytes 4 files changed, 46 insertions(+) create mode 100644 credentials.json create mode 100644 sheets.py create mode 100644 token.pickle diff --git a/credentials.json b/credentials.json new file mode 100644 index 0000000..08834f1 --- /dev/null +++ b/credentials.json @@ -0,0 +1 @@ +{"installed":{"client_id":"298930412365-710dmje1v3jpfjmd7oa16kkv70p1vjn4.apps.googleusercontent.com","project_id":"laundrybot-1608120687055","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"yFtE5gVp78LVXjIG6mc_xIT9","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} \ No newline at end of file diff --git a/laundrybot.py b/laundrybot.py index e0c8328..87c16b7 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -9,6 +9,7 @@ from emoji import emojize from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode from telegram.ext import Updater, CommandHandler, CallbackQueryHandler +from sheets import add_response # Set up logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', diff --git a/sheets.py b/sheets.py new file mode 100644 index 0000000..d14531f --- /dev/null +++ b/sheets.py @@ -0,0 +1,44 @@ +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 +def add_response(username, level, response): + + # Authorize requests to read/write + SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] + SPREADSHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo' + RANGE_NAME = 'Sheet1!A2:D' + + creds = None + + if os.path.exists('token.pickle'): + with open('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()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + 'credentials.json', SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open('token.pickle', 'wb') as token: + pickle.dump(creds, token) + + 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() \ No newline at end of file diff --git a/token.pickle b/token.pickle new file mode 100644 index 0000000000000000000000000000000000000000..d7e3b8085d94bfb16c79e12639d692ffd4310476 GIT binary patch literal 703 zcmZ8f%Wl(95UmhUC>4)gSt2$dk;JdWHapvS*dcXN+jU|~$a3tP#7_LE@3m7SA;E&( z-SzS#h@ap;*z*gdZICKvHM2Np&Y3e`?tlMz@8G8J%Vm}=H=vpMC0a4fpa3CAQRHtR z!B3Ps(-nF9m3$^|@B@^sK}ztC+GoV3Pe}{4W1e;wV(B%!z?x9<86|P7Zp7R?k*6v( zR6cawk?tn5L8$Q!xyQ>{p{*;g$0=FP7>VUj3}dL)#c-m!{Bd&cp?)OA1A9U(2nj#h7y z`KeKZ#WgZnZ!H+^80Td0F&z~K?I{5R8d?Nm<9vV;OCBcuqDBN$=@u3HUEC-N$B>3G5p&Y(% zwYJ-B^EN;~k6<&%lGb&OEtnU;58(;`1c{H2ZdtsthzNfD54?~3Km6Y1kr!;D-6!lM z5UetyAhHxkGi;~b;AtvMVnA1HoG;=e`CeXRF$iv?=;gfm7h|;g AF#rGn literal 0 HcmV?d00001 From 19788dfe0f4dd4318bced93c1553a0a1ff5ae48c Mon Sep 17 00:00:00 2001 From: timotius-121 Date: Wed, 16 Dec 2020 22:47:32 +0800 Subject: [PATCH 14/38] Revert "Added sheets.py" This reverts commit 738bf0932542107e8fe2d4d5bea480df1a4993b3. --- credentials.json | 1 - laundrybot.py | 1 - sheets.py | 44 -------------------------------------------- token.pickle | Bin 703 -> 0 bytes 4 files changed, 46 deletions(-) delete mode 100644 credentials.json delete mode 100644 sheets.py delete mode 100644 token.pickle diff --git a/credentials.json b/credentials.json deleted file mode 100644 index 08834f1..0000000 --- a/credentials.json +++ /dev/null @@ -1 +0,0 @@ -{"installed":{"client_id":"298930412365-710dmje1v3jpfjmd7oa16kkv70p1vjn4.apps.googleusercontent.com","project_id":"laundrybot-1608120687055","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"yFtE5gVp78LVXjIG6mc_xIT9","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} \ No newline at end of file diff --git a/laundrybot.py b/laundrybot.py index 87c16b7..e0c8328 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -9,7 +9,6 @@ from emoji import emojize from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode from telegram.ext import Updater, CommandHandler, CallbackQueryHandler -from sheets import add_response # Set up logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', diff --git a/sheets.py b/sheets.py deleted file mode 100644 index d14531f..0000000 --- a/sheets.py +++ /dev/null @@ -1,44 +0,0 @@ -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 -def add_response(username, level, response): - - # Authorize requests to read/write - SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] - SPREADSHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo' - RANGE_NAME = 'Sheet1!A2:D' - - creds = None - - if os.path.exists('token.pickle'): - with open('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()) - else: - flow = InstalledAppFlow.from_client_secrets_file( - 'credentials.json', SCOPES) - creds = flow.run_local_server(port=0) - # Save the credentials for the next run - with open('token.pickle', 'wb') as token: - pickle.dump(creds, token) - - 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() \ No newline at end of file diff --git a/token.pickle b/token.pickle deleted file mode 100644 index d7e3b8085d94bfb16c79e12639d692ffd4310476..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 703 zcmZ8f%Wl(95UmhUC>4)gSt2$dk;JdWHapvS*dcXN+jU|~$a3tP#7_LE@3m7SA;E&( z-SzS#h@ap;*z*gdZICKvHM2Np&Y3e`?tlMz@8G8J%Vm}=H=vpMC0a4fpa3CAQRHtR z!B3Ps(-nF9m3$^|@B@^sK}ztC+GoV3Pe}{4W1e;wV(B%!z?x9<86|P7Zp7R?k*6v( zR6cawk?tn5L8$Q!xyQ>{p{*;g$0=FP7>VUj3}dL)#c-m!{Bd&cp?)OA1A9U(2nj#h7y z`KeKZ#WgZnZ!H+^80Td0F&z~K?I{5R8d?Nm<9vV;OCBcuqDBN$=@u3HUEC-N$B>3G5p&Y(% zwYJ-B^EN;~k6<&%lGb&OEtnU;58(;`1c{H2ZdtsthzNfD54?~3Km6Y1kr!;D-6!lM z5UetyAhHxkGi;~b;AtvMVnA1HoG;=e`CeXRF$iv?=;gfm7h|;g AF#rGn From cddd215adff2d0f977ac723d2c3abec5db8f32fa Mon Sep 17 00:00:00 2001 From: timotius-121 Date: Fri, 18 Dec 2020 07:20:49 +0800 Subject: [PATCH 15/38] Added the reporting feature (uncomplete) --- laundrybot.py | 75 ++++++++++++++++++++++++++++++++++++++++++++++++-- sheets.py | 45 ++++++++++++++++++++++++++++++ token.pickle | Bin 0 -> 703 bytes 3 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 sheets.py create mode 100644 token.pickle diff --git a/laundrybot.py b/laundrybot.py index e0c8328..1b17374 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -7,8 +7,9 @@ from datetime import datetime import time from emoji import emojize -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode -from telegram.ext import Updater, CommandHandler, CallbackQueryHandler +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, ParseMode, ReplyKeyboardMarkup, KeyboardButton +from telegram.ext import Updater, CommandHandler, CallbackQueryHandler, MessageHandler, ConversationHandler, Filters +from sheets import add_response # Set up logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', @@ -24,6 +25,7 @@ 'dryer1': 'Dryer 1', 'dryer2': 'Dryer 2' } +user_response = '' # Building menu for every occasion def build_menu(buttons, n_cols, header_buttons=None, footer_buttons=None): @@ -117,7 +119,14 @@ def make_status_menu(level_number): callback_data='Help' )] - return build_menu(level_buttons, 5, footer_buttons=refresh_button, header_buttons=help_button) + report_button = [InlineKeyboardButton( + text='Something wrong?', + callback_data='Report' + )] + + header_button = help_button + report_button + + return build_menu(level_buttons, 5, footer_buttons=refresh_button, header_buttons=header_button) def level_status(bot, update, user_data, from_pinned_level=False, new_message=False): query = update.callback_query @@ -159,6 +168,58 @@ def help_menu(bot, update, user_data, from_pinned_level=False, new_message=False 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): + 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): + 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_input = update.message.text + query = update.callback_query + + if user_input == 'Yes': + text = 'Huge thanks!\n' + username = update.message.chat.username + level = 'Change to the current 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) @@ -181,6 +242,14 @@ def main(): dp.add_handler(CallbackQueryHandler(help_menu, pattern='Help', pass_user_data=True)) + dp.add_handler(ConversationHandler( + [CallbackQueryHandler(report, pattern="Report", pass_user_data=True)], + { + 1 : [MessageHandler(Filters.text, get_response_ask_consent)], + 2 : [MessageHandler(Filters.text, get_consent_end)] + }, + [] + )) dp.add_error_handler(error) #updater.start_webhook(listen="0.0.0.0", port=int(PORT), url_path=TOKEN) diff --git a/sheets.py b/sheets.py new file mode 100644 index 0000000..1666be5 --- /dev/null +++ b/sheets.py @@ -0,0 +1,45 @@ +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 + +def add_response(username, level, response): + + # Authorize requests to read/write + SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] + SPREADSHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo' + RANGE_NAME = 'Sheet1!A2:D' + + creds = None + + if os.path.exists('token.pickle'): + with open('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()) + else: + flow = InstalledAppFlow.from_client_secrets_file( + 'credentials.json', SCOPES) + creds = flow.run_local_server(port=0) + # Save the credentials for the next run + with open('token.pickle', 'wb') as token: + pickle.dump(creds, token) + + 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() \ No newline at end of file diff --git a/token.pickle b/token.pickle new file mode 100644 index 0000000000000000000000000000000000000000..e284d4117ffc3a19a498819dbbbadb00e8834d06 GIT binary patch literal 703 zcmZ8f%Wl&^6h#OqlnSXkSR*8muwy%k6SH$3e&94o(>*0gjVz0t13lk`2U`-c_=wleb6^%eb zID6i_H(m30U-@VLmOZA~60SJ=xdVzO0!g1+>cv^hXvl@s8cfDX(pQAF)SlNtW7Ax= zOkqhJWtJM0xUPK;i36wr`nE0E6E`bMowIX2r+8jX$5{}JnqD?i&qkJ&SFbQ=w%m?t zx6MtjKe()upxf_8C~j9KThvPmS|1>JGK^ee)`U(@rx<6G7V2QCm6IWLlwrj{*jz}B zvQxp+I%iMd7Daf++0y_}NTU?qZ?Adt_~_5c>B*a;r$2u4Z+yVn3lGCNhGgL#e1~$W zB!!Oc=Ch03^6_OeoY?w6*Ug50(Jc(2quKaMk!H1`p-!(XT;$uHri9US8bnjtHb{?z zRy7x#Xkm;RlDYes3?eCYbkm-W)Lj|o^IEkiID6(r!C`Bc9eKph5G4E)XYUu3BJ!bB zDvCn93lN})h<=uqZgMOUgdqsX0zyhSV8?eX-Wo@oz5WM2ME)QC;PTk>lW6}5dkLIb zidNUkQbktEYE`JoQjo?_-jriBkJF%*0a;xxH#G^#n|M_b0Yc;;YfT{bvlZPl-Ke`c y=Yf~N{jX8ZUhWLqu7+b&t9Qn3Y@2H8ds};`adztE>x=>q;Vg#!t(5M1CjJEz7yMuV literal 0 HcmV?d00001 From 52e58871db1e04185f2e36fbc4ecece56a49265e Mon Sep 17 00:00:00 2001 From: timotius-121 Date: Fri, 18 Dec 2020 15:59:12 +0800 Subject: [PATCH 16/38] Added the reporting feature --- laundrybot.py | 14 +++++++------- token.pickle | Bin 703 -> 704 bytes 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/laundrybot.py b/laundrybot.py index 1b17374..71cfdc5 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -15,6 +15,7 @@ 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-" @@ -179,7 +180,7 @@ def report(bot, update, user_data, from_pinned_level=False, new_message=False): ) return 1 -def get_response_ask_consent(bot, update): +def get_response_ask_consent(bot, update, user_data): global user_response user_input = update.message.text @@ -201,14 +202,14 @@ def get_response_ask_consent(bot, update): return 2 -def get_consent_end(bot, update): +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 = 'Change to the current level' + level = user_data['check_level'] add_response(username, level, user_response) else: text = 'Okay, your response was not recorded.\n' @@ -245,11 +246,10 @@ def main(): dp.add_handler(ConversationHandler( [CallbackQueryHandler(report, pattern="Report", pass_user_data=True)], { - 1 : [MessageHandler(Filters.text, get_response_ask_consent)], - 2 : [MessageHandler(Filters.text, get_consent_end)] + 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_webhook(listen="0.0.0.0", port=int(PORT), url_path=TOKEN) diff --git a/token.pickle b/token.pickle index e284d4117ffc3a19a498819dbbbadb00e8834d06..ce17c60f608f09483a8a6583bb04c059a1f7f583 100644 GIT binary patch delta 210 zcmdnbdVrOsfo1B}i7Za_n=2EIEcFr%9Me3^f_)vGQk*ghLLAdV{k#nfl0x!*{XGm) zyoya+{o)G^%G2{QT%(-4qmn~1B3wK(iUTu)GQtAVjmkoG(|t?RbdA!I{nL{wGF-fZ z3Ij|tOf7t}!a{R0OFe>1^NrI&0}G4N9E0-80}G8yo$}H>+~S=~%ltyjk|UgqT!LNA z0&)z~inT~d;BoJz|pO(y Date: Fri, 18 Dec 2020 17:41:53 +0800 Subject: [PATCH 17/38] Make counting down time dynamic, repsonsive --- data.py | 2 +- laundrybot.py | 25 +++++++++++++++++++++---- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/data.py b/data.py index 18c181a..f96d666 100644 --- a/data.py +++ b/data.py @@ -19,7 +19,7 @@ def getStatuses(self, level): datetime.now() - timedelta(seconds=300), datetime.now() - timedelta(seconds=400) ] - statuses = [0, 0, 1, 2] + statuses = [0, 2, 1, 2] machine_durations = [30, 30, 40, 40] return [ diff --git a/laundrybot.py b/laundrybot.py index c51d099..cc421d4 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -11,6 +11,7 @@ from telegram.ext import Updater, CommandHandler, CallbackQueryHandler from data import MockData +from string import Template # Set up logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', @@ -79,10 +80,28 @@ def set_pinned_level(bot, update, user_data): level_status(bot, update, user_data, from_pinned_level=True) +# Generate a Hour Minute Second template +class DeltaTemplate(Template): + delimiter = "%" + +# Change from timedelta to H M S format without unecessary microsecond +def strfdelta(tdelta, fmt): + 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() @@ -92,7 +111,8 @@ def make_status_text(level_number): for machine in machine_data: # Get data from back end - time since request/refresh - remaining_time = 'mm:ss' + remaining_time = datetime.today() - machine["start-time"] + remaining_time = strfdelta(remaining_time, '%H:%M:%S') if machine["status"] == 0: status_emoji = etick @@ -104,9 +124,6 @@ def make_status_text(level_number): machine_name = machine["type"] 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) From 9c27d21df653349fecbe53925fed6eb668d6853e Mon Sep 17 00:00:00 2001 From: Yuuki Date: Fri, 18 Dec 2020 17:43:27 +0800 Subject: [PATCH 18/38] Remove "cross" state --- laundrybot.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/laundrybot.py b/laundrybot.py index cc421d4..6fb5b86 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -116,8 +116,6 @@ def make_status_text(level_number): if machine["status"] == 0: status_emoji = etick - elif machine["status"] == 1: - status_emoji = ecross else: status_emoji = f'{ehourglass} {remaining_time} |' From 20b662b12494f923979675f4711e93e637ea5658 Mon Sep 17 00:00:00 2001 From: timotius-121 Date: Fri, 18 Dec 2020 18:53:02 +0800 Subject: [PATCH 19/38] Removed token.pickle --- token.pickle | Bin 704 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 token.pickle diff --git a/token.pickle b/token.pickle deleted file mode 100644 index ce17c60f608f09483a8a6583bb04c059a1f7f583..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 704 zcmZ8f%Wl&^6h){|CQ-gQGhP9Fkd(-cF(uCq z?@iZq|2zFk-;+mWdI=Irel;#HGcKzHnlw34R9_9VsS;Smh-DYk^kl5D{t@o<4JT)7 zn1p>>9ogPAocHt)pCL1xokD(Pwctq=wD`aq!^Jx69hv#57>WnS(aefbrA?|-2&}Wb z2oy7^&vKzs6Q~Uwb!AK?Z?5vaNnbjRIp|1WUM);(F84HWQ5bF`T=iR{sdX-*p6|tK zRW}_<9)op;iiVOWzFUG4Ey4ZvnjY@${(1iT$^M6(m!IhmYEtsTDL_yF96DR$kZZSD zIMV0&Vv=iKaefFdbj4JZp{h)bdeAzS_2P?^IP^YvKfA+l{B*=2u##~5m2 z-Ws8BV5#h|If_lhE=S5xw=Jm=Kpu3(x~Al*gZ%AQgX}ukOEZA!7fL>cWtrhm?RH() z%w2+RhA`u$%l1u>ZJZUr^>GM5iK$EW?pU0vK$N`u2i|7>AAaj{&+%fkd4-JxN;F3~yys7!+QblpCfSbvNhS x@?x-gwvv)J&7kayaF%rsj%V{oA4*HlS?g1ol2=Y%rKRg+MH&I`R*F$An7=t@{D=Sm From e7f2909184b5c8fdfd2f3c5ef908ef336f98de9d Mon Sep 17 00:00:00 2001 From: Yuuki Date: Fri, 18 Dec 2020 21:52:06 +0800 Subject: [PATCH 20/38] Add documentation, and reminder button --- laundrybot.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/laundrybot.py b/laundrybot.py index 6fb5b86..b3e93f1 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -31,10 +31,12 @@ # Building menu for every occasion -def build_menu(buttons, n_cols, header_buttons=None, footer_buttons=None): +def build_menu(buttons, n_cols, header_buttons=None, reminder_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 reminder_buttons: + menu.append(reminder_buttons) if footer_buttons: menu.append(footer_buttons) return InlineKeyboardMarkup(menu) @@ -82,10 +84,26 @@ def set_pinned_level(bot, update, user_data): # 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) @@ -151,7 +169,13 @@ def make_status_menu(level_number): callback_data='Help' )] - return build_menu(level_buttons, 5, footer_buttons=refresh_button, header_buttons=help_button) + reminder_button = [InlineKeyboardButton( + text="Set a reminder", + callback_data="remind" + )] + + + return build_menu(level_buttons, 5, footer_buttons=refresh_button, header_buttons=help_button, reminder_buttons=reminder_button) def level_status(bot, update, user_data, from_pinned_level=False, new_message=False): From e29926113f09a54325d5e22f74089d1409ae9ba5 Mon Sep 17 00:00:00 2001 From: Yuuki Date: Fri, 18 Dec 2020 23:09:06 +0800 Subject: [PATCH 21/38] Finish front end of reminder function --- laundrybot.py | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/laundrybot.py b/laundrybot.py index b3e93f1..033adc5 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -231,6 +231,39 @@ def help_menu(bot, update, user_data, from_pinned_level=False, new_message=False def error(bot, update, error): logger.warning('Update "%s" caused error "%s"', update, error) +def remind(bot, update, user_data): + + + query = update.callback_query + level = user_data['check_level'] + in_use = [] + selection = [] + + #Mock test + machine_data = DATA.getStatuses(level) + + question = "Which machine on Level {} do you like to set a reminder for?\n".format(level) + + for machine in machine_data: + if machine["status"] != 0: + label = machine['type'] + data = MACHINES_INFO[label] + 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 main(): TOKEN = os.environ['RC4LAUNDRYBOT_TOKEN'] @@ -238,6 +271,8 @@ def main(): updater = Updater(TOKEN) 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)$', @@ -248,6 +283,9 @@ def main(): 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_error_handler(error) updater.start_polling() From d0ce1fade9db6c75c2c320a19936f7a0dc15aaf2 Mon Sep 17 00:00:00 2001 From: Yuuki Date: Sat, 19 Dec 2020 17:49:48 +0800 Subject: [PATCH 22/38] Finish add_reminder () --- Google.py | 48 ++++++++++++++++++++++ credentials.json | 1 + laundrybot.py | 90 ++++++++++++++++++++++++++++++++++++++++- token_sheets_v4.pickle | Bin 0 -> 704 bytes 4 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 Google.py create mode 100644 credentials.json create mode 100644 token_sheets_v4.pickle diff --git a/Google.py b/Google.py new file mode 100644 index 0000000..11399d9 --- /dev/null +++ b/Google.py @@ -0,0 +1,48 @@ +import pickle +import os +from google_auth_oauthlib.flow import Flow, InstalledAppFlow +from googleapiclient.discovery import build +from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload +from google.auth.transport.requests import Request +import datetime + + +def Create_Service(client_secret_file, api_name, api_version, *scopes): + print(client_secret_file, api_name, api_version, scopes, sep='-') + CLIENT_SECRET_FILE = client_secret_file + API_SERVICE_NAME = api_name + API_VERSION = api_version + SCOPES = [scope for scope in scopes[0]] + print(SCOPES) + + cred = None + + pickle_file = f'token_{API_SERVICE_NAME}_{API_VERSION}.pickle' + # print(pickle_file) + + if os.path.exists(pickle_file): + with open(pickle_file, 'rb') as token: + cred = pickle.load(token) + + if not cred or not cred.valid: + if cred and cred.expired and cred.refresh_token: + cred.refresh(Request()) + else: + flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES) + cred = flow.run_local_server() + + with open(pickle_file, 'wb') as token: + pickle.dump(cred, token) + + try: + service = build(API_SERVICE_NAME, API_VERSION, credentials=cred) + print(API_SERVICE_NAME, 'service created successfully') + return service + except Exception as e: + print('Unable to connect.') + print(e) + return None + +def convert_to_RFC_datetime(year=1900, month=1, day=1, hour=0, minute=0): + dt = datetime.datetime(year, month, day, hour, minute, 0).isoformat() + 'Z' + return dt \ No newline at end of file diff --git a/credentials.json b/credentials.json new file mode 100644 index 0000000..b028f04 --- /dev/null +++ b/credentials.json @@ -0,0 +1 @@ +{"installed":{"client_id":"298930412365-710dmje1v3jpfjmd7oa16kkv70p1vjn4.apps.googleusercontent.com","project_id":"laundrybot-1608120687055","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"yFtE5gVp78LVXjIG6mc_xIT9","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} diff --git a/laundrybot.py b/laundrybot.py index 033adc5..0142fb7 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -13,6 +13,28 @@ from data import MockData from string import Template +# This import is for communicating with google sheet###### +from Google import Create_Service + +########################################################## + +# Only modify the sheet_ID when needed, else no need to change anything + +CLIENT_SECRET_FILE = "credentials.json" +API_NAME = "sheets" +API_VERSION = "v4" +SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] + +service = Create_Service(CLIENT_SECRET_FILE, API_NAME, API_VERSION, SCOPES) + +SHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo' + +sheet = service.spreadsheets().get(spreadsheetId=SHEET_ID).execute() + +################################################################################ + + + # Set up logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.INFO) @@ -232,11 +254,20 @@ 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 + ''' query = update.callback_query level = user_data['check_level'] - in_use = [] selection = [] #Mock test @@ -244,10 +275,11 @@ def remind(bot, update, user_data): question = "Which machine on Level {} do you like to set a reminder for?\n".format(level) + # Put in-use machines in selection list for machine in machine_data: if machine["status"] != 0: label = machine['type'] - data = MACHINES_INFO[label] + data = machine['type'] button = InlineKeyboardButton(text=label, callback_data=data) selection.append(button) @@ -264,6 +296,56 @@ def remind(bot, update, user_data): 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 + level = user_data['check_level'] + username = query['from_user']['username'] + data = query['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') + + #Mock test + machine_data = DATA.getStatuses(level) + + notice = 'A reminder has been set for Level {} {}'.format(level,data) + + # Set up value to be append to google sheet + WORKSHEET_NAME = 'Reminder!' + cell_range_insert = 'A1' + values = [ + [current_date,current_time,username,level,data] + ] + value_range_body = { + 'majorDimension': 'ROWS', + 'values': values + } + + # Append to the google sheet + service.spreadsheets().values().append( + spreadsheetId=SHEET_ID, + valueInputOption= 'USER_ENTERED', + range= WORKSHEET_NAME+cell_range_insert, + body= value_range_body + ).execute() + + back_button = [InlineKeyboardButton( + text='Back', + callback_data='check_L{}'.format(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 main(): TOKEN = os.environ['RC4LAUNDRYBOT_TOKEN'] @@ -286,6 +368,10 @@ def main(): 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_error_handler(error) updater.start_polling() diff --git a/token_sheets_v4.pickle b/token_sheets_v4.pickle new file mode 100644 index 0000000000000000000000000000000000000000..e0c202a6412303663937c5640f0905d845f5192b GIT binary patch literal 704 zcmZ8f%Wl&^6h){|S`ZRqL0Pe20}^rkNbI`1#BE~dRp;$gLe|6{KVy&M8OL!YBv`QX zuEUogeuJ;zLrB{oRovCRi*xQd_uQ`sKYl%abXVnD7a?~A83Z!o@r;#18^**3D@@Dh z%7f{Ke*8wi&=2Jkfdd6P;6}R%&dzF^_z`^A-caTE@Xw3aM`!O2-u$ND>4=su%@jH*#Gbh~?sC;C z>-yTyqLww8p5>Xg3? Date: Sat, 19 Dec 2020 18:31:49 +0800 Subject: [PATCH 23/38] erased credentials --- credentials.json | 1 - 1 file changed, 1 deletion(-) diff --git a/credentials.json b/credentials.json index b028f04..e69de29 100644 --- a/credentials.json +++ b/credentials.json @@ -1 +0,0 @@ -{"installed":{"client_id":"298930412365-710dmje1v3jpfjmd7oa16kkv70p1vjn4.apps.googleusercontent.com","project_id":"laundrybot-1608120687055","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs","client_secret":"yFtE5gVp78LVXjIG6mc_xIT9","redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]}} From 31ce16ce1f8744cb504e4911dfafcc0369bfa7fb Mon Sep 17 00:00:00 2001 From: timotius-121 Date: Sat, 19 Dec 2020 19:35:44 +0800 Subject: [PATCH 24/38] Changed sheets range --- sheets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sheets.py b/sheets.py index 1666be5..a95ac68 100644 --- a/sheets.py +++ b/sheets.py @@ -10,7 +10,7 @@ def add_response(username, level, response): # Authorize requests to read/write SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] SPREADSHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo' - RANGE_NAME = 'Sheet1!A2:D' + RANGE_NAME = 'Error!A2:D' creds = None From 7b8e9a5d00c87436ad3f39e23c57a0fa04b1f399 Mon Sep 17 00:00:00 2001 From: Yuuki Date: Sat, 19 Dec 2020 19:55:45 +0800 Subject: [PATCH 25/38] Using csv for data storing --- credentials.json | 0 laundrybot.py | 66 +++++++++++------------------------------ reminder.csv | 4 +++ token_sheets_v4.pickle | Bin 704 -> 0 bytes 4 files changed, 22 insertions(+), 48 deletions(-) delete mode 100644 credentials.json create mode 100644 reminder.csv delete mode 100644 token_sheets_v4.pickle diff --git a/credentials.json b/credentials.json deleted file mode 100644 index e69de29..0000000 diff --git a/laundrybot.py b/laundrybot.py index 0142fb7..9b714c2 100644 --- a/laundrybot.py +++ b/laundrybot.py @@ -1,6 +1,7 @@ # Laundry Bot for RC4, current telegram handle: @RC4LaundryBot import os +import csv import re import logging import requests @@ -12,28 +13,7 @@ from data import MockData from string import Template - -# This import is for communicating with google sheet###### -from Google import Create_Service - -########################################################## - -# Only modify the sheet_ID when needed, else no need to change anything - -CLIENT_SECRET_FILE = "credentials.json" -API_NAME = "sheets" -API_VERSION = "v4" -SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] - -service = Create_Service(CLIENT_SECRET_FILE, API_NAME, API_VERSION, SCOPES) - -SHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo' - -sheet = service.spreadsheets().get(spreadsheetId=SHEET_ID).execute() - -################################################################################ - - +import pandas as pd # Set up logging logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', @@ -303,39 +283,29 @@ def add_reminder(bot, update, user_data): to save reminders. The data is saved in Laundrybot sheet under Reminder tab. ''' query = update.callback_query - level = user_data['check_level'] - username = query['from_user']['username'] - data = query['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') + 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['check_level'], + 'machine': query['data'], + } + #Mock test - machine_data = DATA.getStatuses(level) + machine_data = DATA.getStatuses(input_data['level']) - notice = 'A reminder has been set for Level {} {}'.format(level,data) - - # Set up value to be append to google sheet - WORKSHEET_NAME = 'Reminder!' - cell_range_insert = 'A1' - values = [ - [current_date,current_time,username,level,data] - ] - value_range_body = { - 'majorDimension': 'ROWS', - 'values': values - } + notice = 'A reminder has been set for Level {} {}'.format(input_data['level'],input_data['machine']) - # Append to the google sheet - service.spreadsheets().values().append( - spreadsheetId=SHEET_ID, - valueInputOption= 'USER_ENTERED', - range= WORKSHEET_NAME+cell_range_insert, - body= value_range_body - ).execute() + with open('reminder.csv', "a",newline='') as file: + file_reader = csv.reader(file,delimiter=',') + fieldsnames = ['current_date','current_time','username','level','machine'] + writer = csv.DictWriter(file,fieldnames=fieldsnames) + writer.writerow(input_data) back_button = [InlineKeyboardButton( text='Back', - callback_data='check_L{}'.format(level) + callback_data='check_L{}'.format(input_data['level']) )] bot.edit_message_text( diff --git a/reminder.csv b/reminder.csv new file mode 100644 index 0000000..c61eab1 --- /dev/null +++ b/reminder.csv @@ -0,0 +1,4 @@ + +20 December 2020,03:52:20,O_Yuuki_O,5,washer-ezlink +20 December 2020,03:52:33,O_Yuuki_O,14,dryer-ezlink +20 December 2020,03:52:40,O_Yuuki_O,17,dryer-coin diff --git a/token_sheets_v4.pickle b/token_sheets_v4.pickle deleted file mode 100644 index e0c202a6412303663937c5640f0905d845f5192b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 704 zcmZ8f%Wl&^6h){|S`ZRqL0Pe20}^rkNbI`1#BE~dRp;$gLe|6{KVy&M8OL!YBv`QX zuEUogeuJ;zLrB{oRovCRi*xQd_uQ`sKYl%abXVnD7a?~A83Z!o@r;#18^**3D@@Dh z%7f{Ke*8wi&=2Jkfdd6P;6}R%&dzF^_z`^A-caTE@Xw3aM`!O2-u$ND>4=su%@jH*#Gbh~?sC;C z>-yTyqLww8p5>Xg3? Date: Sat, 19 Dec 2020 19:56:53 +0800 Subject: [PATCH 26/38] Erased reminder --- reminder.csv | 4 ---- 1 file changed, 4 deletions(-) diff --git a/reminder.csv b/reminder.csv index c61eab1..e69de29 100644 --- a/reminder.csv +++ b/reminder.csv @@ -1,4 +0,0 @@ - -20 December 2020,03:52:20,O_Yuuki_O,5,washer-ezlink -20 December 2020,03:52:33,O_Yuuki_O,14,dryer-ezlink -20 December 2020,03:52:40,O_Yuuki_O,17,dryer-coin From e701e88a154bd6c83f922abf702a6ccfe0c4fc83 Mon Sep 17 00:00:00 2001 From: Ivander Date: Tue, 5 Jan 2021 14:44:10 +0800 Subject: [PATCH 27/38] Changes to gitignore --- .gitignore | 4 ++++ requirements.txt | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.gitignore b/.gitignore index e82a0d5..5567c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ +# native +token.pickle +reminder.csv + # virtual env bin/ pyvenv.cfg diff --git a/requirements.txt b/requirements.txt index 55af6d0..768873c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,7 @@ python_telegram_bot==12.8 emoji requests +google-api-python-client +google-auth-httplib2 +google-auth-oauthlib +pandas \ No newline at end of file From f9218e9513dad0742f4ec1c2b00e93b542f5618f Mon Sep 17 00:00:00 2001 From: Ivander Date: Tue, 5 Jan 2021 14:54:56 +0800 Subject: [PATCH 28/38] Refactor file structure --- .gitignore | 1 + Google.py | 48 ----------------------------- __init__.py => main/__init__.py | 0 data.py => main/data.py | 0 laundrybot.py => main/laundrybot.py | 2 +- sheets.py => main/sheets.py | 4 +-- 6 files changed, 4 insertions(+), 51 deletions(-) delete mode 100644 Google.py rename __init__.py => main/__init__.py (100%) rename data.py => main/data.py (100%) rename laundrybot.py => main/laundrybot.py (99%) rename sheets.py => main/sheets.py (93%) diff --git a/.gitignore b/.gitignore index 5567c2b..89ba654 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # native token.pickle reminder.csv +credentials.json # virtual env bin/ diff --git a/Google.py b/Google.py deleted file mode 100644 index 11399d9..0000000 --- a/Google.py +++ /dev/null @@ -1,48 +0,0 @@ -import pickle -import os -from google_auth_oauthlib.flow import Flow, InstalledAppFlow -from googleapiclient.discovery import build -from googleapiclient.http import MediaFileUpload, MediaIoBaseDownload -from google.auth.transport.requests import Request -import datetime - - -def Create_Service(client_secret_file, api_name, api_version, *scopes): - print(client_secret_file, api_name, api_version, scopes, sep='-') - CLIENT_SECRET_FILE = client_secret_file - API_SERVICE_NAME = api_name - API_VERSION = api_version - SCOPES = [scope for scope in scopes[0]] - print(SCOPES) - - cred = None - - pickle_file = f'token_{API_SERVICE_NAME}_{API_VERSION}.pickle' - # print(pickle_file) - - if os.path.exists(pickle_file): - with open(pickle_file, 'rb') as token: - cred = pickle.load(token) - - if not cred or not cred.valid: - if cred and cred.expired and cred.refresh_token: - cred.refresh(Request()) - else: - flow = InstalledAppFlow.from_client_secrets_file(CLIENT_SECRET_FILE, SCOPES) - cred = flow.run_local_server() - - with open(pickle_file, 'wb') as token: - pickle.dump(cred, token) - - try: - service = build(API_SERVICE_NAME, API_VERSION, credentials=cred) - print(API_SERVICE_NAME, 'service created successfully') - return service - except Exception as e: - print('Unable to connect.') - print(e) - return None - -def convert_to_RFC_datetime(year=1900, month=1, day=1, hour=0, minute=0): - dt = datetime.datetime(year, month, day, hour, minute, 0).isoformat() + 'Z' - return dt \ No newline at end of file diff --git a/__init__.py b/main/__init__.py similarity index 100% rename from __init__.py rename to main/__init__.py diff --git a/data.py b/main/data.py similarity index 100% rename from data.py rename to main/data.py diff --git a/laundrybot.py b/main/laundrybot.py similarity index 99% rename from laundrybot.py rename to main/laundrybot.py index 88f9fc9..0ab1343 100644 --- a/laundrybot.py +++ b/main/laundrybot.py @@ -358,7 +358,7 @@ def add_reminder(bot, update, user_data): notice = 'A reminder has been set for Level {} {}'.format(input_data['level'],input_data['machine']) - with open('reminder.csv', "a",newline='') as file: + with open('../reminder.csv', "a",newline='') as file: file_reader = csv.reader(file,delimiter=',') fieldsnames = ['current_date','current_time','username','level','machine'] writer = csv.DictWriter(file,fieldnames=fieldsnames) diff --git a/sheets.py b/main/sheets.py similarity index 93% rename from sheets.py rename to main/sheets.py index a95ac68..5e2cea5 100644 --- a/sheets.py +++ b/main/sheets.py @@ -15,7 +15,7 @@ def add_response(username, level, response): creds = None if os.path.exists('token.pickle'): - with open('token.pickle', 'rb') as token: + with open('../token.pickle', 'rb') as token: creds = pickle.load(token) # If there are no (valid) credentials available, let the user log in. @@ -27,7 +27,7 @@ def add_response(username, level, response): 'credentials.json', SCOPES) creds = flow.run_local_server(port=0) # Save the credentials for the next run - with open('token.pickle', 'wb') as token: + with open('../token.pickle', 'wb') as token: pickle.dump(creds, token) service = build('sheets', 'v4', credentials=creds) From 0c7f97a730731ddbc91a2152888ae37a20aa9231 Mon Sep 17 00:00:00 2001 From: Ivander Date: Tue, 5 Jan 2021 17:40:24 +0800 Subject: [PATCH 29/38] Create a polling system for reminders --- main/laundrybot.py | 55 +++++++++++++++++++++++++++++++++------------ main/reminders.py | 56 ++++++++++++++++++++++++++++++++++++++++++++++ main/sheets.py | 2 +- requirements.txt | 4 +++- 4 files changed, 101 insertions(+), 16 deletions(-) create mode 100644 main/reminders.py diff --git a/main/laundrybot.py b/main/laundrybot.py index 0ab1343..960146e 100644 --- a/main/laundrybot.py +++ b/main/laundrybot.py @@ -5,6 +5,7 @@ import re import logging import requests +import threading from datetime import datetime import time from emoji import emojize @@ -12,6 +13,7 @@ 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 @@ -33,6 +35,7 @@ } user_response = '' DATA = MockData() +REMINDERS = ReminderList() # Building menu for every occasion @@ -314,7 +317,8 @@ def remind(bot, update, user_data): #Mock test machine_data = DATA.getStatuses(level) - question = "Which machine on Level {} do you like to set a reminder for?\n".format(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: @@ -345,24 +349,38 @@ def add_reminder(bot, update, user_data): ''' 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['check_level'], - 'machine': query['data'], + 'machine-type': query['data'], } - #Mock test - machine_data = DATA.getStatuses(input_data['level']) + # 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']) + notice = 'A reminder has been set for Level {} {}'.format(input_data['level'],input_data['machine-type']) - with open('../reminder.csv', "a",newline='') as file: - file_reader = csv.reader(file,delimiter=',') - fieldsnames = ['current_date','current_time','username','level','machine'] - writer = csv.DictWriter(file,fieldnames=fieldsnames) - writer.writerow(input_data) + # 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', @@ -377,11 +395,8 @@ def add_reminder(bot, update, user_data): parse_mode=ParseMode.HTML, ) +def run_bot(updater): -def main(): - TOKEN = os.environ['RC4LAUNDRYBOT_TOKEN'] - - updater = Updater(TOKEN) dp = updater.dispatcher # dp.add_handler used to receive back querry @@ -414,6 +429,18 @@ def main(): 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 index 5e2cea5..aabfb07 100644 --- a/main/sheets.py +++ b/main/sheets.py @@ -14,7 +14,7 @@ def add_response(username, level, response): creds = None - if os.path.exists('token.pickle'): + if os.path.exists('../token.pickle'): with open('../token.pickle', 'rb') as token: creds = pickle.load(token) diff --git a/requirements.txt b/requirements.txt index 768873c..8fb6518 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,6 @@ requests google-api-python-client google-auth-httplib2 google-auth-oauthlib -pandas \ No newline at end of file +pandas +threading +datetime \ No newline at end of file From 70f0fd60b02d1a87cd433e078caa37ddd817d391 Mon Sep 17 00:00:00 2001 From: Ivander Jonathan <57558909+ivanderjmw@users.noreply.github.com> Date: Sat, 9 Jan 2021 22:18:06 +0800 Subject: [PATCH 30/38] Update requirements.txt Remove `threading` and `datetime` --- requirements.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8fb6518..ef05431 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,5 +5,3 @@ google-api-python-client google-auth-httplib2 google-auth-oauthlib pandas -threading -datetime \ No newline at end of file From be7fbe3b136c57477001c057e6fd95b875c4b943 Mon Sep 17 00:00:00 2001 From: Ivander Jonathan <57558909+ivanderjmw@users.noreply.github.com> Date: Sat, 9 Jan 2021 22:20:58 +0800 Subject: [PATCH 31/38] Update Procfile Cater for directory change --- Procfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From e5aaa9354938c55f64655b042a31946860ad9b47 Mon Sep 17 00:00:00 2001 From: Ivander Date: Sat, 9 Jan 2021 22:50:22 +0800 Subject: [PATCH 32/38] Update sheets.py to cater directory change --- main/sheets.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/main/sheets.py b/main/sheets.py index aabfb07..6412bf9 100644 --- a/main/sheets.py +++ b/main/sheets.py @@ -5,17 +5,15 @@ from google.auth.transport.requests import Request from datetime import datetime -def add_response(username, level, response): +FILE_PATH = os.path.dirname(__file__) - # Authorize requests to read/write - 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('../token.pickle'): - with open('../token.pickle', 'rb') as token: + + 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. @@ -27,9 +25,21 @@ def add_response(username, level, response): 'credentials.json', SCOPES) creds = flow.run_local_server(port=0) # Save the credentials for the next run - with open('../token.pickle', 'wb') as token: + 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 + SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] + SPREADSHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo' + RANGE_NAME = 'Error!A2:D' + + creds = authorize_creds() + service = build('sheets', 'v4', credentials=creds) # Call the Sheets API @@ -42,4 +52,9 @@ def add_response(username, level, response): # Append the data on the last row result = sheet.values().append( spreadsheetId=SPREADSHEET_ID, range=RANGE_NAME, - valueInputOption='RAW', body=body).execute() \ No newline at end of file + valueInputOption='RAW', body=body).execute() + + +if __name__ == '__main__': + print("Testing credentials...") + print(authorize_creds()) From 672cabcef9a30a1cfec513f9d7d9e00059fcdd51 Mon Sep 17 00:00:00 2001 From: Ivander Date: Sat, 9 Jan 2021 22:53:06 +0800 Subject: [PATCH 33/38] Fix undeclared variable --- main/sheets.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/main/sheets.py b/main/sheets.py index 6412bf9..4bbbb8c 100644 --- a/main/sheets.py +++ b/main/sheets.py @@ -6,6 +6,9 @@ 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(): @@ -34,17 +37,13 @@ def authorize_creds(): def add_response(username, level, response): # Authorize requests to read/write - SCOPES = ['https://www.googleapis.com/auth/spreadsheets'] - SPREADSHEET_ID = '1Wu2fL9DMmroz4PM7iNE-wf7IfqEAho4ArJ9L-zzxrxo' - RANGE_NAME = 'Error!A2:D' - 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} @@ -53,7 +52,7 @@ def add_response(username, level, response): result = sheet.values().append( spreadsheetId=SPREADSHEET_ID, range=RANGE_NAME, valueInputOption='RAW', body=body).execute() - + if __name__ == '__main__': print("Testing credentials...") From 24656d30d03e9fe42b3a6955e8d3ddea7667a185 Mon Sep 17 00:00:00 2001 From: Ivander Date: Sat, 9 Jan 2021 22:58:25 +0800 Subject: [PATCH 34/38] Fix hardcode --- main/sheets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/sheets.py b/main/sheets.py index 4bbbb8c..58e4faf 100644 --- a/main/sheets.py +++ b/main/sheets.py @@ -25,7 +25,7 @@ def authorize_creds(): creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file( - 'credentials.json', SCOPES) + os.path.join(FILE_PATH, '../token.pickle'), SCOPES) creds = flow.run_local_server(port=0) # Save the credentials for the next run with open(os.path.join(FILE_PATH, '../token.pickle'), 'wb') as token: From 21bad17e73e62e472cec972b390d9607ff8355cc Mon Sep 17 00:00:00 2001 From: Ivander Date: Sat, 9 Jan 2021 23:03:00 +0800 Subject: [PATCH 35/38] Fix typo Undo token.pickle in line 28 --- main/sheets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/sheets.py b/main/sheets.py index 58e4faf..e98f83c 100644 --- a/main/sheets.py +++ b/main/sheets.py @@ -25,7 +25,7 @@ def authorize_creds(): creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file( - os.path.join(FILE_PATH, '../token.pickle'), SCOPES) + os.path.join(FILE_PATH, '../credentials.json'), SCOPES) creds = flow.run_local_server(port=0) # Save the credentials for the next run with open(os.path.join(FILE_PATH, '../token.pickle'), 'wb') as token: From 61d988eb952f034315ba098c48c6797514310670 Mon Sep 17 00:00:00 2001 From: Ivander Date: Sat, 9 Jan 2021 23:27:13 +0800 Subject: [PATCH 36/38] Add config variables option for creds json --- main/sheets.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/main/sheets.py b/main/sheets.py index e98f83c..952f495 100644 --- a/main/sheets.py +++ b/main/sheets.py @@ -1,3 +1,4 @@ +import json import pickle import os.path from googleapiclient.discovery import build @@ -23,10 +24,17 @@ def authorize_creds(): if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) - else: - flow = InstalledAppFlow.from_client_secrets_file( - os.path.join(FILE_PATH, '../credentials.json'), SCOPES) - creds = flow.run_local_server(port=0) + 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_local_server(port=0) + # Save the credentials for the next run with open(os.path.join(FILE_PATH, '../token.pickle'), 'wb') as token: pickle.dump(creds, token) From cf6ea1c917451549cf0fcd65fa5e5934229c1149 Mon Sep 17 00:00:00 2001 From: Ivander Date: Sun, 10 Jan 2021 00:01:11 +0800 Subject: [PATCH 37/38] Switch to run_console --- main/sheets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main/sheets.py b/main/sheets.py index 952f495..11b2848 100644 --- a/main/sheets.py +++ b/main/sheets.py @@ -33,7 +33,7 @@ def authorize_creds(): flow = InstalledAppFlow.from_client_secrets_file( os.path.join(FILE_PATH, '../credentials.json'), SCOPES) - creds = flow.run_local_server(port=0) + creds = flow.run_console() # Save the credentials for the next run with open(os.path.join(FILE_PATH, '../token.pickle'), 'wb') as token: From 187a8df9724a1936364ba8375ad9442b2e4b1118 Mon Sep 17 00:00:00 2001 From: Ivander Date: Tue, 12 Jan 2021 19:21:09 +0800 Subject: [PATCH 38/38] fix outdated message bug --- main/laundrybot.py | 106 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 17 deletions(-) diff --git a/main/laundrybot.py b/main/laundrybot.py index 960146e..7088322 100644 --- a/main/laundrybot.py +++ b/main/laundrybot.py @@ -39,27 +39,56 @@ # Building menu for every occasion -def build_menu(buttons, n_cols, header_buttons=None, reminder_buttons=None, footer_buttons=None): +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 reminder_buttons: - menu.append(reminder_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, @@ -69,6 +98,9 @@ def check_handler(bot, update, user_data): 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 = [] @@ -83,9 +115,12 @@ def ask_level(bot, update): 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) @@ -168,7 +203,7 @@ def make_status_menu(level_number): level_buttons.append(button) refresh_button = [InlineKeyboardButton( - text='Refresh', + text='Refresh {}'.format(erefresh), callback_data='check_L{}'.format(level_number) )] @@ -177,35 +212,58 @@ def make_status_menu(level_number): callback_data='Help' )] + report_button = [InlineKeyboardButton( + text='Something wrong?', + callback_data='Report' + )] + reminder_button = [InlineKeyboardButton( - text="Set a reminder", + text="Set a reminder {}".format(ealarm), callback_data="remind" )] - report_button = [InlineKeyboardButton( - text='Something wrong?', - callback_data='Report' + insights_button = [InlineKeyboardButton( + text="Get Chart Insights {}".format(ebarchart), + callback_data="insight" )] + row_buttons = [reminder_button, insights_button] + header_buttons = help_button + report_button - return build_menu(level_buttons, 5, footer_buttons=refresh_button, header_buttons=header_buttons, reminder_buttons=reminder_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)) - user_data['check_level'] = level - 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, @@ -216,6 +274,10 @@ def level_status(bot, update, user_data, from_pinned_level=False, new_message=Fa 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" @@ -226,7 +288,7 @@ def help_menu(bot, update, user_data, from_pinned_level=False, new_message=False "We appreciate your feedback as we are currently still beta-testing "\ "in Ursa before launching the college-wide implementation! :)" - level = user_data['check_level'] + level = user_data['pinned_level'] help_menu_button = [InlineKeyboardButton( text='Back', @@ -241,7 +303,12 @@ def help_menu(bot, update, user_data, from_pinned_level=False, new_message=False 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' @@ -252,9 +319,10 @@ def report(bot, update, user_data, from_pinned_level=False, new_message=False): ) 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 @@ -274,6 +342,7 @@ def get_response_ask_consent(bot, update, user_data): return 2 + def get_consent_end(bot, update, user_data): user_input = update.message.text query = update.callback_query @@ -281,7 +350,7 @@ def get_consent_end(bot, update, user_data): if user_input == 'Yes': text = 'Huge thanks!\n' username = update.message.chat.username - level = user_data['check_level'] + level = user_data['pinned_level'] add_response(username, level, user_response) else: text = 'Okay, your response was not recorded.\n' @@ -310,8 +379,11 @@ def remind(bot, update, user_data): 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['check_level'] + level = user_data['pinned_level'] selection = [] #Mock test @@ -354,7 +426,7 @@ def add_reminder(bot, update, user_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['check_level'], + 'level': user_data['pinned_level'], 'machine-type': query['data'], } @@ -416,7 +488,7 @@ def run_bot(updater): pass_user_data=True)) dp.add_handler(CallbackQueryHandler(add_reminder, pattern='^(washer-coin|washer-ezlink|dryer-ezlink|dryer-coin)$', - pass_user_data=True)) + pass_user_data=True)) dp.add_handler(ConversationHandler( [CallbackQueryHandler(report, pattern="Report", pass_user_data=True)], {