├── 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()
--------------------------------------------------------------------------------