├── src ├── requirements.txt ├── db │ └── wolt-checker.db ├── notify.py ├── database.py └── main.py ├── Dockerfile ├── README.md └── docker-compose.yml /src/requirements.txt: -------------------------------------------------------------------------------- 1 | requests 2 | python-telegram-bot -------------------------------------------------------------------------------- /src/db/wolt-checker.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dFurman/wolt-checker-bot/HEAD/src/db/wolt-checker.db -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | WORKDIR /app 3 | COPY ./src/requirements.txt ./requirements.txt 4 | RUN pip install --upgrade pip && \ 5 | pip install -r requirements.txt 6 | COPY ./src/* ./ 7 | RUN chmod +x ./main.py 8 | CMD python ./main.py 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Wolt Checker Bot 2 | 3 | Wolt Restaurant Checker Telegram Bot will check if the restaurant you want to order from is closed and 4 | will let you the option to register for a notification when the restaurant is back to get some orders. 5 | These situations of restaurants being closed on their opening hours are caused sometimes due the restaurant being too busy at the moment. 6 | 7 | This bot is using telegram as the chat application and the backend is written in python. 8 | Dockerfile is ready for you. -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | wolt-checker: 4 | build: ./ 5 | container_name: wolt-checker 6 | image: furman/wolt-checker 7 | environment: 8 | - TZ=Asia/Jerusalem 9 | - TELEGRAM_BOT_TOKEN= 10 | volumes: 11 | - ./db:/app/db 12 | restart: unless-stopped 13 | 14 | wolt-notifier: 15 | build: ./ 16 | container_name: wolt-notifier 17 | image: furman/wolt-checker 18 | command: "python notify.py" 19 | volumes: 20 | - ./db:/app/db 21 | environment: 22 | - TZ=Asia/Jerusalem 23 | - TELEGRAM_BOT_TOKEN= 24 | restart: unless-stopped 25 | -------------------------------------------------------------------------------- /src/notify.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import time 3 | import json 4 | import telegram 5 | import logging 6 | from database import Database 7 | from multiprocessing import Process 8 | 9 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 10 | level=logging.INFO) 11 | logger = logging.getLogger(__name__) 12 | 13 | def notify(userId, slug, rest_name, rest_link): 14 | bot.send_message(chat_id=userId, parse_mode=telegram.ParseMode.HTML, 15 | text=f"""Hey there, 16 | {rest_name} is now ONLINE!!! Enjoy :) 17 | 18 | Click here to order from {rest_name} 19 | ‌ 20 | """) ## ‌ - zero-width non-joiner (ZWNJ) 21 | db = Database() 22 | db.removeNotification(userId=userId, slug=slug, reason="Notified") 23 | db.close() 24 | 25 | def check_restaurant(slug, notifications): 26 | result = requests.get(f"{wolt_api}/{slug}").json()["results"][0] 27 | is_online = result["online"] 28 | if is_online: 29 | try: 30 | rest_name = list(filter(lambda x: x["lang"] == "en", result["name"]))[0]["value"] 31 | except: 32 | rest_name = list(filter(lambda x: x["lang"] == "he", result["name"]))[0]["value"] 33 | 34 | rest_link = result["public_url"] 35 | users2Notify = set(map(lambda x: x["userId"], filter(lambda x: x["slug"] == slug, notifications))) 36 | notifiers = [Process(target=notify, args=(user, slug, rest_name, rest_link)) for user in users2Notify] 37 | 38 | for notifier in notifiers: 39 | notifier.start() 40 | 41 | def main(): 42 | db = Database() 43 | notifications = db.getAllActiveNotifications() 44 | db.close() 45 | uniqueSlugs = set(map(lambda x: x["slug"], notifications)) 46 | rest_checkers = [Process(target=check_restaurant, args=(slug, notifications)) for slug in uniqueSlugs] 47 | 48 | for rest_checker in rest_checkers: 49 | rest_checker.start() 50 | 51 | 52 | if __name__ == '__main__': 53 | wolt_api = "https://restaurant-api.wolt.com/v3/venues/slug" 54 | telegram_bot_token = os.environ.get('TELEGRAM_BOT_TOKEN') 55 | bot = telegram.Bot(token=telegram_bot_token) 56 | 57 | while True: 58 | main() 59 | time.sleep(10) 60 | -------------------------------------------------------------------------------- /src/database.py: -------------------------------------------------------------------------------- 1 | import sqlite3 2 | from datetime import datetime 3 | class Database: 4 | def __init__(self, name="./db/wolt-checker.db"): 5 | 6 | self.conn = None 7 | self.cursor = None 8 | 9 | if name: 10 | self.open(name) 11 | 12 | def __enter__(self): 13 | return self 14 | 15 | def __exit__(self,exc_type,exc_value,traceback): 16 | 17 | self.close() 18 | 19 | def open(self,name): 20 | 21 | try: 22 | self.conn = sqlite3.connect(name); 23 | self.cursor = self.conn.cursor() 24 | 25 | except sqlite3.Error as e: 26 | print("Error connecting to database!") 27 | 28 | def close(self): 29 | 30 | if self.conn: 31 | self.conn.commit() 32 | self.cursor.close() 33 | self.conn.close() 34 | 35 | 36 | 37 | def write(self,table,columns,data): 38 | 39 | query = "INSERT INTO {0} ({1}) VALUES ({2});".format(table,columns,data) 40 | 41 | self.cursor.execute(query) 42 | 43 | def query(self,sql): 44 | self.cursor.execute(sql) 45 | 46 | def addNewNotification(self, userId, slug): 47 | existing = self.getUserActiveNotifications(userId=userId) 48 | existingSlugs = list(map(lambda x: x["slug"], existing)) 49 | 50 | if slug not in existingSlugs: 51 | while True: 52 | try: 53 | nowstr = datetime.now().strftime("%d/%m/%Y %H:%M:%S") 54 | query = f"INSERT INTO Notifications ('userId', 'slug', 'registered') VALUES ({userId}, '{slug}', '{nowstr}');" 55 | self.cursor.execute(query) 56 | break 57 | except: 58 | pass 59 | 60 | def removeNotification(self, userId, slug, reason): 61 | while True: 62 | try: 63 | nowstr = datetime.now().strftime("%d/%m/%Y %H:%M:%S") 64 | query = f"UPDATE Notifications SET removed = '{nowstr}', removedReason = '{reason}', active = '0' WHERE userId={userId} AND slug='{slug}' AND active='1';" 65 | self.cursor.execute(query) 66 | break 67 | except: 68 | pass 69 | 70 | def getAllActiveNotifications(self): 71 | query = f"SELECT userId,slug FROM Notifications WHERE active='1';" 72 | 73 | self.query(query) 74 | notifications = [] 75 | for row in self.cursor.fetchall(): 76 | notifications.append({"userId": row[0], "slug": row[1]}) 77 | 78 | return notifications 79 | 80 | def getUserActiveNotifications(self, userId): 81 | query = f"SELECT slug FROM Notifications WHERE userId='{userId}' AND active='1';" 82 | 83 | self.query(query) 84 | notifications = [] 85 | for row in self.cursor.fetchall(): 86 | notifications.append(row[0]) 87 | 88 | return notifications -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import json 3 | import csv 4 | import telegram 5 | from telegram import (InlineKeyboardButton, InlineKeyboardMarkup) 6 | from telegram.ext import (Updater, CommandHandler, MessageHandler, Filters, 7 | ConversationHandler, CallbackQueryHandler) 8 | import logging 9 | from database import Database 10 | import os 11 | from multiprocessing import Process 12 | 13 | logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 14 | level=logging.INFO) 15 | logger = logging.getLogger(__name__) 16 | 17 | SEARCH, CHECKER = range(2) 18 | 19 | def start(update, context): 20 | userId = update.message.from_user.id 21 | userName = update.message.from_user.name 22 | update.message.reply_text(f""" 23 | Hello :) 24 | Please Enter the restaurant name you want to check. 25 | It can be in English/Hebrew. 26 | To show current notification registrations please write: /show 27 | """) 28 | return SEARCH 29 | 30 | def search_restaurant(update, context): 31 | bot.send_chat_action(chat_id=update.message.chat_id, action=telegram.ChatAction.TYPING) 32 | wolt_api = "https://restaurant-api.wolt.com/v1" 33 | query = update.message.text 34 | 35 | results = requests.get(f"{wolt_api}/search?sort=relevancy&q={query}").json()["results"] 36 | button_list = [] 37 | rest_name = None 38 | if results: 39 | for result in results[:10]: 40 | try: 41 | rest_name = list(filter(lambda x: x["lang"] == "he", result["value"]["name"]))[0]["value"] 42 | except: 43 | rest_name = list(filter(lambda x: x["lang"] == "en", result["value"]["name"]))[0]["value"] 44 | 45 | button_list.append( 46 | [ 47 | InlineKeyboardButton(rest_name, callback_data=result["value"]["slug"]) 48 | ] 49 | ) 50 | reply_markup = InlineKeyboardMarkup(button_list) 51 | 52 | update.message.reply_text("Please Select the wanted restaurant", reply_markup=reply_markup) 53 | else: 54 | update.message.reply_text("I'm Sorry! No restaurants were found.\n\ 55 | you can search for another one if you like.") 56 | 57 | return CHECKER 58 | 59 | def checker_query_handler(update, context): 60 | wolt_api = "https://restaurant-api.wolt.com/v3/venues/slug" 61 | query = update.callback_query 62 | query.answer() 63 | 64 | slug = query.data 65 | userId = update.callback_query.from_user["id"] 66 | 67 | result = requests.get(f"{wolt_api}/{slug}").json()["results"][0] 68 | try: 69 | rest_name = list(filter(lambda x: x["lang"] == "en", result["name"]))[0]["value"] 70 | except: 71 | rest_name = list(filter(lambda x: x["lang"] == "he", result["name"]))[0]["value"] 72 | 73 | is_online = result["online"] 74 | rest_link = result["public_url"] 75 | if is_online: 76 | query.edit_message_text(parse_mode=telegram.ParseMode.HTML, 77 | text=f"{rest_name} is OPEN :)\n\n\ 78 | Click here to order from {rest_name}\n\n\ 79 | Thank you for using Wolt Checker Bot :)\n\ 80 | To re-run the bot, please write /start") 81 | return cancel(update, context) 82 | else: 83 | button_list = [ 84 | [InlineKeyboardButton("Yes", callback_data=f"REGISTER_{slug}_{rest_name}")], 85 | [InlineKeyboardButton("No", callback_data=f"NO")], 86 | 87 | ] 88 | reply_markup = InlineKeyboardMarkup(button_list) 89 | 90 | query.edit_message_text(f"{rest_name} is CLOSED :(\n\n\ 91 | Do you want to register for an update when the restaurant will be open again ?", 92 | reply_markup=reply_markup) 93 | 94 | return SEARCH 95 | 96 | def register_handling(update, context): 97 | query = update.callback_query 98 | query.answer() 99 | 100 | data = query.data.split("_") 101 | action = data[0] 102 | 103 | if action == "NO": 104 | query.edit_message_text("Thank you for using Wolt Checker Bot :)\n\n\ 105 | To re-run the bot, please write /start") 106 | return ConversationHandler.END 107 | 108 | if action == "REMOVE": 109 | userId = update.callback_query.from_user["id"] 110 | slug = data[1] 111 | db = Database() 112 | db.removeNotification(userId=userId, slug=slug, reason="UserManually") 113 | db.close() 114 | query.edit_message_text("No Problem, you are removed from being notified.") 115 | 116 | if action == "REGISTER": 117 | userId = update.callback_query.from_user["id"] 118 | slug = data[1] 119 | rest_name = "_".join(data[2:]) 120 | 121 | Process(target=addNewNotification, args=(userId, slug)).start() 122 | 123 | query.edit_message_text(f"No Problem, you will be notified once {rest_name} is open.\n\n\ 124 | Thank you for using Wolt Checker Bot :)\n\n\ 125 | To re-run the bot, please write /start") 126 | 127 | return ConversationHandler.END 128 | 129 | def addNewNotification(userId, slug): 130 | db = Database() 131 | db.addNewNotification(userId=userId, slug=slug) 132 | db.close() 133 | 134 | def list_registrations(update, context): 135 | wolt_api = "https://restaurant-api.wolt.com/v3/venues/slug" 136 | userId = update.message.chat_id 137 | db = Database() 138 | relevant = db.getUserActiveNotifications(userId=userId) 139 | db.close() 140 | if not relevant: 141 | update.message.reply_text("You are not registered for any notification right now.") 142 | return cancel(update, context) 143 | 144 | for slug in set(relevant): 145 | button_list = [ 146 | [InlineKeyboardButton("Remove", callback_data=f"REMOVE_{slug}")] 147 | ] 148 | result = requests.get(f"{wolt_api}/{slug}").json()["results"][0] 149 | try: 150 | rest_name = list(filter(lambda x: x["lang"] == "en", result["name"]))[0]["value"] 151 | except: 152 | rest_name = list(filter(lambda x: x["lang"] == "he", result["name"]))[0]["value"] 153 | 154 | reply_markup = InlineKeyboardMarkup(button_list) 155 | update.message.reply_text(rest_name, reply_markup=reply_markup) 156 | 157 | 158 | def cancel(update, context): 159 | try: 160 | update.message.reply_text("Thank you for using Wolt Checker Bot :)\n\n\ 161 | To re-run the bot, please write /start") 162 | except: 163 | pass 164 | return ConversationHandler.END 165 | 166 | def main(): 167 | # Create the Updater and pass it your bot's token. 168 | # Make sure to set use_context=True to use the new context based callbacks 169 | # Post version 12 this will no longer be necessary 170 | updater = Updater(telegram_bot_token, use_context=True) 171 | 172 | # Get the dispatcher to register handlers 173 | dp = updater.dispatcher 174 | 175 | conv_handler = ConversationHandler( 176 | entry_points=[CommandHandler('start', start)], 177 | 178 | states={ 179 | SEARCH: [ 180 | CommandHandler('show', list_registrations), 181 | CommandHandler('start', start), 182 | MessageHandler(Filters.text, search_restaurant), 183 | CallbackQueryHandler(register_handling) 184 | ], 185 | CHECKER: [ 186 | CallbackQueryHandler(checker_query_handler), 187 | CommandHandler('show', list_registrations), 188 | CommandHandler('start', start), 189 | MessageHandler(Filters.text, search_restaurant) 190 | ] 191 | }, 192 | 193 | fallbacks=[CommandHandler('cancel', cancel)] 194 | ) 195 | 196 | dp.add_handler(conv_handler) 197 | 198 | # Start the Bot 199 | updater.start_polling() 200 | 201 | # Run the bot until you press Ctrl-C or the process receives SIGINT, 202 | # SIGTERM or SIGABRT. This should be used most of the time, since 203 | # start_polling() is non-blocking and will stop the bot gracefully. 204 | updater.idle() 205 | 206 | 207 | if __name__ == '__main__': 208 | telegram_bot_token = os.environ.get('TELEGRAM_BOT_TOKEN') 209 | bot = telegram.Bot(token=telegram_bot_token) 210 | main() --------------------------------------------------------------------------------