Google calendar support
This commit is contained in:
parent
dc2139df24
commit
c851b9d7e2
3
.gitignore
vendored
3
.gitignore
vendored
@ -6,3 +6,6 @@ chromedriver.exe
|
|||||||
chromedriver
|
chromedriver
|
||||||
data.db
|
data.db
|
||||||
.envrc
|
.envrc
|
||||||
|
google_client.json
|
||||||
|
credentials.json
|
||||||
|
pyrightconfig.json
|
||||||
|
|||||||
@ -10,9 +10,6 @@ WORKDIR /app
|
|||||||
# Copy the local requirements file to the container
|
# Copy the local requirements file to the container
|
||||||
COPY requirements.txt /app/requirements.txt
|
COPY requirements.txt /app/requirements.txt
|
||||||
|
|
||||||
# Install the required Python packages
|
|
||||||
RUN pip install --no-cache-dir -r requirements.txt
|
|
||||||
|
|
||||||
# Install Selenium and its required drivers
|
# Install Selenium and its required drivers
|
||||||
RUN apt-get update && \
|
RUN apt-get update && \
|
||||||
apt-get install -y --no-install-recommends \
|
apt-get install -y --no-install-recommends \
|
||||||
@ -20,9 +17,15 @@ RUN apt-get update && \
|
|||||||
curl \
|
curl \
|
||||||
unzip \
|
unzip \
|
||||||
chromium-driver \
|
chromium-driver \
|
||||||
|
build-essential \
|
||||||
|
libsqlite3-dev \
|
||||||
|
sqlite3 \
|
||||||
&& apt-get clean \
|
&& apt-get clean \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Install the required Python packages
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# Set up Chrome WebDriver for Selenium
|
# Set up Chrome WebDriver for Selenium
|
||||||
RUN wget -q -O /app/chromedriver.zip https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip && \
|
RUN wget -q -O /app/chromedriver.zip https://chromedriver.storage.googleapis.com/114.0.5735.90/chromedriver_linux64.zip && \
|
||||||
unzip /app/chromedriver.zip && \
|
unzip /app/chromedriver.zip && \
|
||||||
|
|||||||
@ -4,6 +4,12 @@ TELEGRAM_TOKEN = os.environ.get("SECRETARX_TG_TOKEN", None)
|
|||||||
DB_PATH = "data.db"
|
DB_PATH = "data.db"
|
||||||
CHROMEDRIVER_PATH = "chromedriver"
|
CHROMEDRIVER_PATH = "chromedriver"
|
||||||
|
|
||||||
|
|
||||||
|
LISTEN_PORT = 9090
|
||||||
|
GOOGLE_CLIENT_SECRET_FILE = "credentials.json"
|
||||||
|
REDIRECT_URL = "http://localhost:9090/"
|
||||||
|
CALENDAR_NAME = "secretarx"
|
||||||
|
|
||||||
INTERVALS = {
|
INTERVALS = {
|
||||||
1800 + 3600 * 0.0: 1800,
|
1800 + 3600 * 0.0: 1800,
|
||||||
1800 + 3600 * 0.5: 30,
|
1800 + 3600 * 0.5: 30,
|
||||||
|
|||||||
215
google_calendar.py
Normal file
215
google_calendar.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
import datetime
|
||||||
|
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
|
||||||
|
import os.path
|
||||||
|
import socketserver
|
||||||
|
from threading import Thread
|
||||||
|
from time import sleep
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
import multiprocessing
|
||||||
|
|
||||||
|
from google.auth.transport.requests import Request
|
||||||
|
from google.oauth2.credentials import Credentials
|
||||||
|
from google_auth_oauthlib.flow import InstalledAppFlow
|
||||||
|
from googleapiclient.discovery import build
|
||||||
|
from googleapiclient.errors import HttpError
|
||||||
|
from config import GOOGLE_CLIENT_SECRET_FILE
|
||||||
|
|
||||||
|
# If modifying these scopes, delete the file token.json.
|
||||||
|
SCOPES = ["https://www.googleapis.com/auth/calendar"]
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleCalendar:
|
||||||
|
class TCPServer(socketserver.ForkingTCPServer):
|
||||||
|
flow_callback = None
|
||||||
|
|
||||||
|
class HTTPHandler(BaseHTTPRequestHandler):
|
||||||
|
def do_GET(self):
|
||||||
|
url_parts = urlparse(self.path)
|
||||||
|
query_params = parse_qs(url_parts.query)
|
||||||
|
|
||||||
|
if "code" not in query_params:
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write("<h1>Missing OAuth Code</h1>".encode("utf-8"))
|
||||||
|
return
|
||||||
|
|
||||||
|
if "state" not in query_params:
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write("<h1>Missing OAuth State</h1>".encode("utf-8"))
|
||||||
|
return
|
||||||
|
|
||||||
|
code = query_params["code"][0]
|
||||||
|
state = query_params["state"][0]
|
||||||
|
|
||||||
|
result = self.server.flow_callback(code=code, state=state)
|
||||||
|
|
||||||
|
if result is None:
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write("<h1>OAuth Success</h1>".encode("utf-8"))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.send_response(500)
|
||||||
|
self.send_header("Content-type", "text/html")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(f"<h1>{result}</h1>".encode("utf-8"))
|
||||||
|
return
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
http_port: int,
|
||||||
|
client_secret_file: str,
|
||||||
|
redirect_uri: str,
|
||||||
|
flow_callback: callable,
|
||||||
|
calendar_summary: str,
|
||||||
|
token_refresh_callback: callable = None,
|
||||||
|
) -> None:
|
||||||
|
self.flow = InstalledAppFlow.from_client_secrets_file(
|
||||||
|
client_secret_file, SCOPES, redirect_uri=redirect_uri
|
||||||
|
)
|
||||||
|
self.httpd = ThreadingHTTPServer(("", http_port), self.HTTPHandler)
|
||||||
|
|
||||||
|
self.httpd.flow_callback = self.finish_flow
|
||||||
|
|
||||||
|
manager = multiprocessing.Manager()
|
||||||
|
self.state_callback_kwargs = manager.dict()
|
||||||
|
self.flow_callback = flow_callback
|
||||||
|
self.calendar_summary = calendar_summary
|
||||||
|
self.token_refresh_callback = token_refresh_callback
|
||||||
|
|
||||||
|
def finish_flow(self, state: str, code: str):
|
||||||
|
if (kwargs := self.state_callback_kwargs.get(state, None)) is None:
|
||||||
|
return "Invalid OAuth State"
|
||||||
|
|
||||||
|
token = self.flow.fetch_token(code=code)
|
||||||
|
|
||||||
|
self.flow_callback(token, **kwargs)
|
||||||
|
|
||||||
|
def flow_url(self, callback_kwargs={}):
|
||||||
|
url, state = self.flow.authorization_url(prompt="consent")
|
||||||
|
self.state_callback_kwargs[state] = callback_kwargs
|
||||||
|
return url
|
||||||
|
|
||||||
|
def service(self, token: Mapping[str, str]):
|
||||||
|
# Convert the token dict into a Credentials object
|
||||||
|
creds = Credentials(
|
||||||
|
token=token.get("access_token"),
|
||||||
|
refresh_token=token.get("refresh_token"),
|
||||||
|
token_uri=self.flow.client_config["token_uri"],
|
||||||
|
client_id=self.flow.client_config["client_id"],
|
||||||
|
client_secret=self.flow.client_config["client_secret"],
|
||||||
|
scopes=SCOPES,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not creds or not creds.valid:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if creds and creds.expired and creds.refresh_token:
|
||||||
|
creds.refresh(Request())
|
||||||
|
self.token_refresh_callback(
|
||||||
|
token,
|
||||||
|
{
|
||||||
|
"access_token": creds.token,
|
||||||
|
"expires_in": creds.expiry,
|
||||||
|
"refresh_token": creds.refresh_token,
|
||||||
|
"scope": creds.scopes,
|
||||||
|
"token_type": creds.token,
|
||||||
|
"expires_at": creds.expiry,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
service = build("calendar", "v3", credentials=creds)
|
||||||
|
|
||||||
|
# Return the credentials object
|
||||||
|
return service
|
||||||
|
|
||||||
|
def get_calendar_id(self, token: Mapping[str, str]):
|
||||||
|
# Create calendar with id
|
||||||
|
service = self.service(token)
|
||||||
|
|
||||||
|
if not service:
|
||||||
|
return None
|
||||||
|
|
||||||
|
calendars = service.calendarList().list().execute().get("items")
|
||||||
|
|
||||||
|
for calendar in calendars:
|
||||||
|
if calendar["summary"] == self.calendar_summary:
|
||||||
|
return calendar["id"]
|
||||||
|
|
||||||
|
calendar = (
|
||||||
|
service.calendars()
|
||||||
|
.insert(
|
||||||
|
body={
|
||||||
|
"summary": self.calendar_summary,
|
||||||
|
"timeZone": "Europe/Amsterdam",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
|
||||||
|
return calendar["id"]
|
||||||
|
|
||||||
|
def get_first_event(self, token: Mapping[str, str], start: datetime.datetime):
|
||||||
|
service = self.service(token)
|
||||||
|
|
||||||
|
if not service:
|
||||||
|
return None
|
||||||
|
|
||||||
|
calendar_id = self.get_calendar_id(token)
|
||||||
|
print(start.isoformat() + "Z")
|
||||||
|
|
||||||
|
try:
|
||||||
|
events_result = (
|
||||||
|
service.events()
|
||||||
|
.list(
|
||||||
|
calendarId=calendar_id,
|
||||||
|
timeMin=start.isoformat(),
|
||||||
|
maxResults=1,
|
||||||
|
singleEvents=True,
|
||||||
|
orderBy="startTime",
|
||||||
|
)
|
||||||
|
.execute()
|
||||||
|
)
|
||||||
|
except HttpError as e:
|
||||||
|
if e.resp.status == 404:
|
||||||
|
return []
|
||||||
|
print(e)
|
||||||
|
|
||||||
|
items = events_result.get("items", [])
|
||||||
|
|
||||||
|
if items:
|
||||||
|
return items[0]
|
||||||
|
|
||||||
|
return []
|
||||||
|
|
||||||
|
def create_event(self, token: Mapping[str, str], event: dict):
|
||||||
|
service = self.service(token)
|
||||||
|
if not service:
|
||||||
|
return None
|
||||||
|
|
||||||
|
calendar_id = self.get_calendar_id(token)
|
||||||
|
|
||||||
|
event = service.events().insert(calendarId=calendar_id, body=event).execute()
|
||||||
|
|
||||||
|
return event
|
||||||
|
|
||||||
|
def delete_event(self, token: Mapping[str, str], event_id: str):
|
||||||
|
service = self.service(token)
|
||||||
|
|
||||||
|
if not service:
|
||||||
|
return None
|
||||||
|
|
||||||
|
calendar_id = self.get_calendar_id(token)
|
||||||
|
|
||||||
|
service.events().delete(calendarId=calendar_id, eventId=event_id).execute()
|
||||||
|
|
||||||
|
def httpd_listen(self):
|
||||||
|
# Start httpd as a daemon thread
|
||||||
|
thread = Thread(target=self.httpd.serve_forever, args=())
|
||||||
|
thread.daemon = True
|
||||||
|
thread.start()
|
||||||
11
models.py
11
models.py
@ -1,5 +1,6 @@
|
|||||||
from datetime import datetime, timedelta, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
|
|
||||||
class Booking:
|
class Booking:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@ -12,15 +13,18 @@ class Booking:
|
|||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.booking_id = bookingId
|
self.booking_id = bookingId
|
||||||
self.start = datetime.strptime(startDate, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
|
self.start = datetime.strptime(startDate, "%Y-%m-%dT%H:%M:%S.%fZ").replace(
|
||||||
self.end = datetime.strptime(endDate, "%Y-%m-%dT%H:%M:%S.%fZ").replace(tzinfo=timezone.utc)
|
tzinfo=timezone.utc
|
||||||
|
)
|
||||||
|
self.end = datetime.strptime(endDate, "%Y-%m-%dT%H:%M:%S.%fZ").replace(
|
||||||
|
tzinfo=timezone.utc
|
||||||
|
)
|
||||||
self.bookable_product_id = bookableProductId
|
self.bookable_product_id = bookableProductId
|
||||||
self.linked_product_id = linkedProductId
|
self.linked_product_id = linkedProductId
|
||||||
self.available = isAvailable
|
self.available = isAvailable
|
||||||
|
|
||||||
self.booking_dict = kwargs
|
self.booking_dict = kwargs
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def start_stamp(self):
|
def start_stamp(self):
|
||||||
## Get string representation of the timestamp in GMT +2 with only Day of week and month, hour and minute
|
## Get string representation of the timestamp in GMT +2 with only Day of week and month, hour and minute
|
||||||
@ -59,3 +63,4 @@ class Booking:
|
|||||||
return time_left
|
return time_left
|
||||||
else:
|
else:
|
||||||
return timedelta(0) # No time left if the booking has already started
|
return timedelta(0) # No time left if the booking has already started
|
||||||
|
|
||||||
|
|||||||
@ -2,3 +2,9 @@ pysqlite3
|
|||||||
selenium==4.24.0
|
selenium==4.24.0
|
||||||
requests==2.32.3
|
requests==2.32.3
|
||||||
telebot==0.0.5
|
telebot==0.0.5
|
||||||
|
google-api-core==2.19.2
|
||||||
|
google-api-python-client==2.145.0
|
||||||
|
google-auth==2.34.0
|
||||||
|
google-auth-httplib2==0.2.0
|
||||||
|
google-auth-oauthlib==1.2.1
|
||||||
|
googleapis-common-protos==1.65.0
|
||||||
|
|||||||
159
tgbot.py
159
tgbot.py
@ -7,10 +7,13 @@ from threading import Thread
|
|||||||
import sqlite3
|
import sqlite3
|
||||||
from xclient import XClient
|
from xclient import XClient
|
||||||
import config
|
import config
|
||||||
|
from google_calendar import GoogleCalendar
|
||||||
|
import json
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TelegramBot:
|
class TelegramBot:
|
||||||
def __init__(self, bot_token, db_path="bot_database.db"):
|
def __init__(self, bot_token, db_path="bot_database.db"):
|
||||||
self.bot = telebot.TeleBot(bot_token)
|
self.bot = telebot.TeleBot(bot_token)
|
||||||
@ -19,6 +22,14 @@ class TelegramBot:
|
|||||||
self.user_bookings = {} # Store user bookings by chat ID
|
self.user_bookings = {} # Store user bookings by chat ID
|
||||||
self.watchlist = {} # Watchlist to store full slots by chat ID
|
self.watchlist = {} # Watchlist to store full slots by chat ID
|
||||||
self.db_path = db_path
|
self.db_path = db_path
|
||||||
|
self.calendar = GoogleCalendar(
|
||||||
|
config.LISTEN_PORT,
|
||||||
|
config.GOOGLE_CLIENT_SECRET_FILE,
|
||||||
|
config.REDIRECT_URL,
|
||||||
|
self.calendar_connect_callback,
|
||||||
|
config.CALENDAR_NAME,
|
||||||
|
self.calendar_token_update_callback,
|
||||||
|
)
|
||||||
self.init_db()
|
self.init_db()
|
||||||
|
|
||||||
# Set up the command handlers
|
# Set up the command handlers
|
||||||
@ -56,6 +67,52 @@ class TelegramBot:
|
|||||||
def callback_remove_slot(call):
|
def callback_remove_slot(call):
|
||||||
self.callback_remove_slot(call)
|
self.callback_remove_slot(call)
|
||||||
|
|
||||||
|
@self.bot.message_handler(commands=["calendar"])
|
||||||
|
def calendar_auth(message):
|
||||||
|
self.calendar_auth(message)
|
||||||
|
|
||||||
|
def init_db(self):
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
# Create tables
|
||||||
|
cursor.execute("""
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
chat_id INTEGER UNIQUE,
|
||||||
|
username TEXT,
|
||||||
|
password TEXT,
|
||||||
|
access_token TEXT,
|
||||||
|
calendar_token TEXT
|
||||||
|
)
|
||||||
|
""")
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
def calendar_connect_callback(self, calendar_token, chat_id):
|
||||||
|
calendar_token_str = json.dumps(calendar_token, sort_keys=True)
|
||||||
|
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE users SET calendar_token = ? WHERE chat_id = ?",
|
||||||
|
(calendar_token_str, chat_id),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Send message to user
|
||||||
|
self.bot.send_message(chat_id, "Calendar connected successfully!")
|
||||||
|
|
||||||
|
def calendar_token_update_callback(self, old_token, new_token):
|
||||||
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(
|
||||||
|
"UPDATE users SET calendar_token = ? WHERE calendar_token = ?",
|
||||||
|
(
|
||||||
|
json.dumps(new_token, sort_keys=True),
|
||||||
|
json.dumps(old_token, sort_keys=True),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
def update_user_access_token_callback(self, chat_id):
|
def update_user_access_token_callback(self, chat_id):
|
||||||
def callback(access_token):
|
def callback(access_token):
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
@ -68,32 +125,37 @@ class TelegramBot:
|
|||||||
|
|
||||||
return callback
|
return callback
|
||||||
|
|
||||||
def init_db(self):
|
def get_calendar_token(self, chat_id):
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
# Create tables
|
cursor.execute(
|
||||||
cursor.execute("""
|
"SELECT calendar_token FROM users WHERE chat_id = ?", (chat_id,)
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
chat_id INTEGER UNIQUE,
|
|
||||||
username TEXT,
|
|
||||||
password TEXT,
|
|
||||||
access_token TEXT
|
|
||||||
)
|
)
|
||||||
""")
|
result = cursor.fetchone()
|
||||||
conn.commit()
|
|
||||||
|
if result:
|
||||||
|
return json.loads(result[0])
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
def get_xclient(self, chat_id):
|
def get_xclient(self, chat_id):
|
||||||
if chat_id not in self.xclients:
|
if chat_id not in self.xclients:
|
||||||
with sqlite3.connect(self.db_path) as conn:
|
with sqlite3.connect(self.db_path) as conn:
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
cursor.execute(
|
cursor.execute(
|
||||||
"SELECT username, password, access_token FROM users WHERE chat_id = ?", (chat_id,)
|
"SELECT username, password, access_token FROM users WHERE chat_id = ?",
|
||||||
|
(chat_id,),
|
||||||
)
|
)
|
||||||
result = cursor.fetchone()
|
result = cursor.fetchone()
|
||||||
if result:
|
if result:
|
||||||
username, password, access_token = result
|
username, password, access_token = result
|
||||||
self.xclients[chat_id] = XClient(username, password, on_access_token_change=self.update_user_access_token_callback(chat_id))
|
self.xclients[chat_id] = XClient(
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
on_access_token_change=self.update_user_access_token_callback(
|
||||||
|
chat_id
|
||||||
|
),
|
||||||
|
)
|
||||||
self.xclients[chat_id].access_token = access_token
|
self.xclients[chat_id].access_token = access_token
|
||||||
else:
|
else:
|
||||||
self.xclients[chat_id] = None
|
self.xclients[chat_id] = None
|
||||||
@ -150,6 +212,12 @@ class TelegramBot:
|
|||||||
message, "Invalid format. Please use `/login username password`."
|
message, "Invalid format. Please use `/login username password`."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def calendar_auth(self, message):
|
||||||
|
url = self.calendar.flow_url({"chat_id": message.chat.id})
|
||||||
|
|
||||||
|
# Send url to user
|
||||||
|
self.bot.reply_to(message, f"Please visit this URL to authenticate: {url}")
|
||||||
|
|
||||||
def send_welcome(self, message):
|
def send_welcome(self, message):
|
||||||
self.bot.reply_to(
|
self.bot.reply_to(
|
||||||
message,
|
message,
|
||||||
@ -217,6 +285,29 @@ class TelegramBot:
|
|||||||
# Attempt to book the available slot
|
# Attempt to book the available slot
|
||||||
try:
|
try:
|
||||||
xclient.make_booking(selected_slot)
|
xclient.make_booking(selected_slot)
|
||||||
|
|
||||||
|
if (
|
||||||
|
token := self.get_calendar_token(call.message.chat.id)
|
||||||
|
) is not None:
|
||||||
|
self.calendar.create_event(
|
||||||
|
token,
|
||||||
|
{
|
||||||
|
"summary": "Gym Booking",
|
||||||
|
"start": {
|
||||||
|
"dateTime": selected_slot.start.strftime(
|
||||||
|
"%Y-%m-%dT%H:%M:%S.%fZ"
|
||||||
|
),
|
||||||
|
"timeZone": "Europe/Amsterdam",
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"dateTime": selected_slot.end.strftime(
|
||||||
|
"%Y-%m-%dT%H:%M:%S.%fZ"
|
||||||
|
),
|
||||||
|
"timeZone": "Europe/Amsterdam",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
self.bot.answer_callback_query(
|
self.bot.answer_callback_query(
|
||||||
call.id, "Slot booked successfully!"
|
call.id, "Slot booked successfully!"
|
||||||
)
|
)
|
||||||
@ -313,6 +404,19 @@ class TelegramBot:
|
|||||||
call.message.chat.id,
|
call.message.chat.id,
|
||||||
f"Booking for {selected_booking.start_stamp} has been canceled.",
|
f"Booking for {selected_booking.start_stamp} has been canceled.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (
|
||||||
|
token := self.get_calendar_token(call.message.chat.id)
|
||||||
|
) is not None:
|
||||||
|
gmt_plus_2 = timezone(timedelta(hours=2))
|
||||||
|
|
||||||
|
event = self.calendar.get_first_event(
|
||||||
|
token, selected_booking.start.astimezone(gmt_plus_2)
|
||||||
|
)
|
||||||
|
|
||||||
|
if event:
|
||||||
|
self.calendar.delete_event(token, event["id"])
|
||||||
|
|
||||||
else:
|
else:
|
||||||
self.bot.answer_callback_query(
|
self.bot.answer_callback_query(
|
||||||
call.id, "Failed to cancel booking."
|
call.id, "Failed to cancel booking."
|
||||||
@ -407,6 +511,25 @@ class TelegramBot:
|
|||||||
# If the slot becomes available
|
# If the slot becomes available
|
||||||
try:
|
try:
|
||||||
xclient.make_booking(slot)
|
xclient.make_booking(slot)
|
||||||
|
if (token := self.get_calendar_token(chat_id)) is not None:
|
||||||
|
self.calendar.create_event(
|
||||||
|
token,
|
||||||
|
{
|
||||||
|
"summary": "Gym Booking",
|
||||||
|
"start": {
|
||||||
|
"dateTime": slot.start.strftime(
|
||||||
|
"%Y-%m-%dT%H:%M:%S.%fZ"
|
||||||
|
),
|
||||||
|
"timeZone": "Europe/Amsterdam",
|
||||||
|
},
|
||||||
|
"end": {
|
||||||
|
"dateTime": slot.end.strftime(
|
||||||
|
"%Y-%m-%dT%H:%M:%S.%fZ"
|
||||||
|
),
|
||||||
|
"timeZone": "Europe/Amsterdam",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
self.bot.send_message(
|
self.bot.send_message(
|
||||||
chat_id,
|
chat_id,
|
||||||
f"Slot {slot.start_stamp} is now available and has been booked for you.",
|
f"Slot {slot.start_stamp} is now available and has been booked for you.",
|
||||||
@ -424,16 +547,17 @@ class TelegramBot:
|
|||||||
if not self.watchlist[chat_id]:
|
if not self.watchlist[chat_id]:
|
||||||
del self.watchlist[chat_id] # Remove chat_id from watchlist if empty
|
del self.watchlist[chat_id] # Remove chat_id from watchlist if empty
|
||||||
|
|
||||||
|
|
||||||
def calculate_polling_interval(self, slot_time):
|
def calculate_polling_interval(self, slot_time):
|
||||||
now = datetime.now().replace(tzinfo=timezone.utc)
|
now = datetime.now().replace(tzinfo=timezone.utc)
|
||||||
time_until_slot = (slot_time - now).total_seconds()
|
time_until_slot = (slot_time - now).total_seconds()
|
||||||
|
|
||||||
intervals = sorted([(k,v) for k,v in config.INTERVALS.items()], key=lambda x: x[0])
|
intervals = sorted(
|
||||||
|
[(k, v) for k, v in config.INTERVALS.items()], key=lambda x: x[0]
|
||||||
|
)
|
||||||
|
|
||||||
last_wait = 1800
|
last_wait = 1800
|
||||||
|
|
||||||
for (threshold, wait) in intervals:
|
for threshold, wait in intervals:
|
||||||
if time_until_slot < threshold:
|
if time_until_slot < threshold:
|
||||||
return wait
|
return wait
|
||||||
|
|
||||||
@ -462,7 +586,6 @@ class TelegramBot:
|
|||||||
|
|
||||||
logger.info(f"POLLING {self.watchlist}. Sleeping {polling_interval}")
|
logger.info(f"POLLING {self.watchlist}. Sleeping {polling_interval}")
|
||||||
|
|
||||||
|
|
||||||
time.sleep(polling_interval) # Wait before checking the watchlist again
|
time.sleep(polling_interval) # Wait before checking the watchlist again
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
@ -471,4 +594,6 @@ class TelegramBot:
|
|||||||
thread.daemon = True
|
thread.daemon = True
|
||||||
thread.start()
|
thread.start()
|
||||||
|
|
||||||
|
self.calendar.httpd_listen()
|
||||||
|
|
||||||
self.bot.polling()
|
self.bot.polling()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user