From 918e7a1943125168392f939980577f1a7e47678c Mon Sep 17 00:00:00 2001 From: yosefabush Date: Thu, 1 Aug 2024 19:33:35 +0300 Subject: [PATCH] first commit --- .gitignore | 133 ++++++ Dockerfile_ | 39 ++ Procfile | 1 + README.md | 2 + app/DatabaseConnection.py | 36 ++ app/Model/models.py | 331 ++++++++++++++ app/Model/moses_api.py | 142 ++++++ app/app.py | 799 ++++++++++++++++++++++++++++++++++ app/dict/user_requests.pickle | Bin 0 -> 1692 bytes app/requirements.txt | 41 ++ app/test_app.py | 16 + docker-compose.yml | 29 ++ 12 files changed, 1569 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile_ create mode 100644 Procfile create mode 100644 README.md create mode 100644 app/DatabaseConnection.py create mode 100644 app/Model/models.py create mode 100644 app/Model/moses_api.py create mode 100644 app/app.py create mode 100644 app/dict/user_requests.pickle create mode 100644 app/requirements.txt create mode 100644 app/test_app.py create mode 100644 docker-compose.yml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3860cd1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.env + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ +./.idea/ +./app/.idea/ +.idea/ +./app/dict/ \ No newline at end of file diff --git a/Dockerfile_ b/Dockerfile_ new file mode 100644 index 0000000..565f0ed --- /dev/null +++ b/Dockerfile_ @@ -0,0 +1,39 @@ +# Use the official Python image as the base image +FROM python:3.10-slim + +# Set the working directory in the container +WORKDIR /app + +COPY ./app /app +# +ENV PYTHONDONTWRITEBYTECODE 1 +ENV PYTHONUNBUFFERED 1 + +# Set environment variables for the MySQL database +ENV MYSQL_DATABASE=moses +ENV MYSQL_USER=root +ENV MYSQL_PASSWORD=root +ENV MYSQL_HOST=localhost +ENV MYSQL_PORT=3306 + +# Set env +ENV VERIFY_TOKEN=WHATSAPP_VERIFY_TOKEN +ENV TOKEN=EAAMtxuKXXtgBAEAmVLQguVMihtke2jI1PfyhJkCN8RxZBtppe0O0RqiNdGSKvQ4kpajIymqXQ6ZBrz4IoEWNLMdKYMI3JI5wf4XyJ2qDxq4f0DkVN2cKw3d32XEtMU0niU41gvco4izqfkf1BZBo4Vf5yacLVixtj5GldozVE2tmqf4oTT2fh6H4o7pjjLF7sdHskfWNgZDZD +ENV NUMBER_ID_PROVIDER=103476326016925 +ENV PORT=5000 + +# Copy the requirements file into the container +# COPY requirements.txt . + +# Install the required packages in the container +RUN pip install --no-cache-dir -r requirements.txt + +# Copy the rest of the application code into the container +# COPY . . +# COPY ./app /app +# Expose port 8000 for the app to listen on +EXPOSE 80 + +# Start the app with Uvicorn +# CMD ["uvicorn", "app:app", "--host", "0.0.0.0"] +CMD ["uvicorn", "app:app", "--host", "0.0.0.0"] diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..52ec8bd --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python app.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..c267d2a --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# fast_api_whatsappbot +new diff --git a/app/DatabaseConnection.py b/app/DatabaseConnection.py new file mode 100644 index 0000000..c90f316 --- /dev/null +++ b/app/DatabaseConnection.py @@ -0,0 +1,36 @@ +from dotenv import load_dotenv +import os +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy_utils import database_exists, create_database + +load_dotenv() +user = os.getenv("MYSQLUSER", default="root") +password = os.getenv("MYSQLPASSWORD", default="root") +host = os.getenv("MYSQLHOST", default="localhost") +port = os.getenv("MYSQLPORT", default="3306") +db = os.getenv("MYSQLDATABASE", default="moses") +MYSQL_URL = os.getenv("MYSQL_URL", default=None) +print(f"MYSQL_URL: '{MYSQL_URL}'") + +SQLALCHEMY_DATABASE_URL = f'mysql+pymysql://{user}:{password}@{host}:{port}/{db}' + +if MYSQL_URL is None: + print("Local mysql database") +else: + print("From Server database mysql") + +print(f"Full Sql alchemy connection string: '{SQLALCHEMY_DATABASE_URL}'") +engine = create_engine(SQLALCHEMY_DATABASE_URL) + +if not database_exists(engine.url): + print("New Data Base were created!") + create_database(engine.url) +else: + # Connect the database if exists. + print("Data Base exist!") + engine.connect() + +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) +Base = declarative_base() diff --git a/app/Model/models.py b/app/Model/models.py new file mode 100644 index 0000000..854f90c --- /dev/null +++ b/app/Model/models.py @@ -0,0 +1,331 @@ +import os +import re +import json +from typing import List +from dotenv import load_dotenv +from pydantic import BaseModel +from sqlalchemy.orm import Session +from datetime import datetime, timedelta +from sqlalchemy.dialects.mysql import LONGTEXT +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy import Boolean, Column, Integer, String, Text, DateTime + +from Model import moses_api +load_dotenv() +Base = declarative_base() +WORKING_HOURS_START_END = (float(os.getenv('START_TIME', default=None)), float(os.getenv('END_TIME', default=None))) +start_hours = str(timedelta(hours=WORKING_HOURS_START_END[0])).rsplit(':', 1)[0] +end_hour = str(timedelta(hours=WORKING_HOURS_START_END[1])).rsplit(':', 1)[0] +str_working_hours = f"{start_hours} - {end_hour}" + + +class User(Base): + __tablename__ = "users" + id = Column(Integer, primary_key=True, index=True) + name = Column(String(255), unique=False, index=True) + password = Column(String(255), unique=False, index=True) + phone = Column(String(255), unique=True, index=True) + + def __init__(self, db: Session, **kwargs): + self.db = db + super().__init__(**kwargs) + + @classmethod + def log_in_user(self, user, password): + return self.db.query(self).filter(self.name == user, self.password == password).first() + + +class Items(Base): + __tablename__ = "items" + id = Column(Integer, primary_key=True, index=True) + name = Column(String(255), unique=True, index=True) + + +class Issues(Base): + __tablename__ = "issues" + id = Column(Integer, primary_key=True, index=True) + conversation_id = Column(String(255), unique=False, index=True) + item_id = Column(Text) + issue_data = Column(String(255), unique=False) + issue_sent_status = Column(Boolean, default=False) + + def set_issue_status(self, db, status): + session = db.query(Issues).filter(Issues.id == self.id).first() + session.issue_sent_status = status + db.commit() + # self.session_active = status + + +class ConversationSession(Base): + conversation_steps_in_class = { + "1": "אנא הזינו שם משתמש", + "2": "אנא הזינו סיסמא", + "3": "תודה שפנית אלינו פרטיך נקלטו במערכת\nבאיזה נושא נוכל להעניק לך שירות?\n(לפתיחת קריאה ללא נושא רשום 'אחר')", + "4": "אנא בחרו קוד מוצר", + "5": "אם ברצונכם שנחזור למספר פלאפון זה לחץ כאן, אחרת נא הקישו כעת את המספר לחזרה", + "6": "מה שם פותח הקריאה?", + "7": "נא רשמו בקצרה את תיאור הפנייה", + "8": "תודה רבה על פנייתכם!\n הקריאה נכנסה למערכת שלנו ותטופל בהקדם האפשרי\n" + "אנא הישארו זמינים למענה טלפוני חוזר ממחלקת התמיכה 📞\n" + "על מנת לפתוח קריאות שירות נוספת שלחו אלינו הודעה חדשה\n" + "המשך יום טוב!" + } + MAX_LOGING_ATTEMPTS = 3 + __tablename__ = 'conversation' + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(String(255), unique=False, index=True) + password = Column(String(255), unique=False, index=True) + login_attempts = Column(Integer) + call_flow_location = Column(Integer) + all_client_products_in_service = Column(LONGTEXT) + start_data = Column(DateTime, nullable=False, default=datetime.now) + session_active = Column(Boolean, default=True) + convers_step_resp = Column(String(1500), unique=False) + + def __init__(self, user_id, db: Session): + self.user_id = user_id + self.login_attempts = 0 + self.call_flow_location = 0 + self.all_client_products_in_service = None + # self.start_data = None + self.session_active = True + self.convers_step_resp = json.dumps({"1": "", + "2": "", + "3": "", + "4": "", + "5": "", + "6": "", + "7": "" + }) + # self.db = db + + def increment_call_flow(self, db): + session = db.query(ConversationSession).filter(ConversationSession.id == self.id).first() + session.call_flow_location += 1 + db.commit() + print(f"call flow inc to: '{self.call_flow_location}'") + + def set_call_flow(self, db, flow_location): + session = db.query(ConversationSession).filter(ConversationSession.id == self.id).first() + session.call_flow_location = flow_location + db.commit() + print(f"call flow SET to: '{self.call_flow_location}'") + + def get_conversation_session_id(self): + return self.user_id + + def get_user_id(self): + return self.user_id + + def get_call_flow_location(self): + sees = self.db.query(ConversationSession).filter(ConversationSession.id == self.id).first() + if sees is None: + raise Exception(f"Could not find user {self.id}, {self.user_id}") + return sees.call_flow_location + + # def all_validation(self, db, step, answer): + # match step: + # case 1: + # print(f"Check if user name '{answer}' valid") + # case 2: + # print(f"Log in with password '{answer}'") + # print( + # f"Search for user with user name '{self.get_conversation_step_json('1')}' and password '{answer}'") + # # user_db = moses_api.get_product_by_user(self.get_converstion_step('1'), self.password) + # user_db = db.query(User).filter(User.name == self.get_conversation_step_json('1'), + # User.password == answer).first() + # if user_db is None: + # print("user not found!") + # return False + # print("User found!") + # self.password = answer + # db.commit() + # case 3: + # print(f"check if chosen '{answer}' valid") + # # choises = {a.name: a.id for a in db.query(Items).all()} + # choises = moses_api.get_product_by_user(self.user_id, self.password) + # if answer not in choises: + # return False + # res = db.query(Items.id).filter(Items.name == answer).first() + # if len(res) == 0: + # print(f"Item not exist {answer}") + # return False + # case 4: + # print(f"Check if product '{answer}' exist") + # if answer not in moses_api.get_product_number_by_user(self.user_id, self.password): + # return False + # case 5: + # print(f"Check if phone number '{answer}' is valid") + # if answer != "1": + # # rule = re.compile(r'(^[+0-9]{1,3})*([0-9]{10,11}$)') + # rule = re.compile(r'(^\+?(972|0)(\-)?0?(([23489]{1}\d{7})|[5]{1}\d{8})$)') + # if not rule.search(answer): + # msg = "המספר שהוקש איננו תקין" + # print(msg) + # return False + # case 6: + # print(f"NO NEED TO VALIDATE ISSUE") + # return True + + def validation_switch_step(self, db, case, answer, select_by_button): + try: + if case == 1: + print(f"Check if user name '{answer}' valid") + elif case == 2: + print(f"Log in user name '{self.get_conversation_step_json('1')}' password '{answer}'") + client_data = moses_api.login_whatsapp(self.get_conversation_step_json('1'), answer) + if client_data is None: + return False + client_name = client_data.get('clientName', None) + if client_name is None: + print(f"No clientName!") + self.password = f"{answer};{client_data['UserId']};{None}" + else: + print(f"clientName found! {client_name}") + self.password = f"{answer};{client_data['UserId']};{client_data['clientName']}" + db.commit() + elif case == 3: + if self.all_client_products_in_service is None: + print(f"no product found") + return False + print(f"check if chosen '{answer}' valid") + choices = json.loads(self.all_client_products_in_service) + print(f"subjects {list(choices.keys())}") + if answer == "אחר": + print("found אחר") + return True + if not select_by_button: + return False + elif case == 4: + print(f"Check if product '{answer}' exist") + if not select_by_button: + return False + choices = json.loads(self.all_client_products_in_service) + res = choices.get(self.get_conversation_step_json("3"), None) + found = False + for d in res: + an = d.get(answer, None) + if an is not None: + # print(an[0]) + found = True + break + return found + elif case == 5: + print(f"Check if phone number '{answer}' is valid") + if answer != "חזרו אלי למספר זה": + rule = re.compile(r'(^\+?(972|0)(\-)?0?(([23489]{1}\d{7})|[5]{1}\d{8})$)') + if not rule.search(answer): + msg = "המספר שהוקש איננו תקין" + print(msg) + return False + elif case == 6: + print(f"Opening issue name {answer}") + elif case == 7: + print(f"NO NEED TO VALIDATE ISSUE") + else: + return False + return True + except Exception as ex: + print(f"step {case} {ex}") + return False + + def validate_and_set_answer(self, db, step, response, is_button_selected): + step = int(step) + if self.validation_switch_step(db, step, response, is_button_selected): + if step == 2: + client = self.password.split(';')[-1] + if client != 'None': + self.set_conversion_step(step, client, db) + else: + print(f"Save login name") + self.set_conversion_step(step, response, db) + elif step == 3 and response == "אחר": + self.set_conversion_step(step, "None", db) + self.set_conversion_step(4, "None", db) + self.call_flow_location = 4 + db.commit() + elif step == 4: + choices = json.loads(self.all_client_products_in_service) + res = choices.get(self.get_conversation_step_json("3"), None) + for d in res: + an = d.get(response, None) + if an is not None: + print(an[0]) + self.set_conversion_step(step, an[0], db) + break + elif step == 5: + # if response == "1": + if response == "חזרו אלי למספר זה": + # user_id is phone number in conversation + self.set_conversion_step(step, self.user_id, db) + else: + # new phone number + self.set_conversion_step(step, response, db) + else: + self.set_conversion_step(step, response, db) + print(f"session object step {self.get_conversation_step_json(str(step))}") + result = f"{self.conversation_steps_in_class[str(step)]}: {response}" + return True, result + else: + if self.call_flow_location == 1: + result = "שם משתמש או ססמא שגויים אנא נסו שוב" + elif self.call_flow_location == 2: + result = f"לצערינו לא ניתן להמשיך את הליך ההזדהות 😌\nרוצים לנסות שוב? שלחו הודעה נוספת\n\nאם אין לכם פרטים תוכלו ליצור קשר עם שירות הלקוחות בטלפון או בwhatsapp במספר 02-6430010 ולקבל פרטי גישה עדכניים, שירות הלקוחות זמין בימים א-ה בין השעות {str_working_hours}" + elif self.call_flow_location in [3, 4]: + if self.all_client_products_in_service is None: + result = "אין פריטים" + else: + result = "אנא בחרו פריטים מהרשימה" + elif self.call_flow_location == 5: + result = "מספר הטלפון שהוקש אינו תקין, אנא הזינו שוב" + else: + result = f" ערך לא חוקי '{response}' " + print(f"Not valid response {response} for step {step}") + return False, result + + def set_status(self, db, status): + session = db.query(ConversationSession).filter(ConversationSession.id == self.id).first() + session.session_active = status + db.commit() + + def get_all_client_product_and_save_db_subjects(self, db): + choices = moses_api.get_sorted_product_by_user_and_password(self.password.split(";")[1]) + if choices is None: + print("get_all_client_product error (check user password and client Id or user does not have products)") + return None + print(f"Allowed values: '{choices}'") + self.all_client_products_in_service = json.dumps(choices) + db.commit() + print("saved to DB!") + return list(choices.keys()) + + def get_products(self, selected_category): + choices = json.loads(self.all_client_products_in_service) + distinct_values = choices.get(selected_category, None) + return distinct_values + + def is_product_more_then_max(self, max_product): + choices = json.loads(self.all_client_products_in_service) + if len(choices) > max_product: + return True + return False + + def get_all_responses(self): + return self.convers_step_resp + + def set_conversion_step(self, step, value, db): + print(f"set_conversion_step {step} value {value} ") + temp = json.loads(self.convers_step_resp) + temp[str(step)] = value + self.convers_step_resp = json.dumps(temp) + db.commit() + + def get_conversation_step_json(self, step): + return json.loads(self.convers_step_resp)[step] + + +# Request Models. +class WebhookRequestData(BaseModel): + object: str = "" + entry: List = [] diff --git a/app/Model/moses_api.py b/app/Model/moses_api.py new file mode 100644 index 0000000..5fd6b4c --- /dev/null +++ b/app/Model/moses_api.py @@ -0,0 +1,142 @@ +import json +import base64 +import requests +import unicodedata +import xml.etree.ElementTree as ET + +END_POINT = "https://026430010.co.il/MosesTechWebService/Service1.asmx" +# encode code +PERFIX_USER_ID = 'Njgy' +# PERFIX_USER_ID = 'NDQ4' +PERFIX_PASSWORD = 'NDU2Nzg5' + +base64_bytes = PERFIX_USER_ID.encode('ascii') +message_bytes = base64.b64decode(base64_bytes) +PERFIX_USER_ID = int(message_bytes.decode('ascii')) + +base64_bytes = PERFIX_PASSWORD.encode('ascii') +message_bytes = base64.b64decode(base64_bytes) +PERFIX_PASSWORD = int(message_bytes.decode('ascii')) + + +def create_kria(data): + print("creating kria...") + data["id"] = PERFIX_USER_ID + data["password"] = PERFIX_PASSWORD + # return True + url = END_POINT + f"/InsertNewCall" + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + print(f"json '{data}' ") + print(url) + response = requests.post(url, data=data, headers=headers) + if response.ok: + print(f"kria created! '{response}'") + return True + return False + + +def get_sorted_product_by_user_and_password(client_id): + print("get_sorted_products.. using perfix_user_id, and perfix_password") + url = END_POINT + f"/GetClientProductsInServiceForWhatsapp?clientId={client_id}&Id={PERFIX_USER_ID}&password={PERFIX_PASSWORD}" + print(url) + response = requests.get(url, verify=False) + if response.ok: + root = ET.fromstring(response.content) + # data = json.loads(root.text) + try: + data = json.loads(root.text, strict=False) + # return data["table"] + # Group all products by: {category name: [value_number or value_string]} + distinct_product_values = dict() + for row in data["table"]: + if row['ProductSherotName'] not in distinct_product_values.keys(): + # distinct_product_values[row['ProductSherotName']] = [row['NumComp']] + # distinct_product_values[row['ProductSherotName']] = [{f"{row['NumComp']}-{row['Description']}":[row['NumComp']]}] + if row["NumComp"] != "": + distinct_product_values[row['ProductSherotName']] = [ + {f"{row['NumComp']}-{row['Description']}": [row['NumComp']]}] + else: + distinct_product_values[row['ProductSherotName']] = [ + {f"{row['Description']}": [row['Description']]}] + # print("new product") + else: + # distinct_product_values[row['ProductSherotName']].append(row['NumComp']) + # distinct_product_values[row['ProductSherotName']].append({f"{row['NumComp']}-{row['Description']}":[row['NumComp']]}) + if row["NumComp"] != "": + distinct_product_values[row['ProductSherotName']].append( + {f"{row['NumComp']}-{row['Description']}": [row['NumComp']]}) + else: + distinct_product_values[row['ProductSherotName']].append( + {f"{row['Description']}": [row['Description']]}) + # print("exist product") + # Rename english product name to hebrew name (only Routers) + for key, value in distinct_product_values.items(): + _language = "en" if 'HEBREW' not in unicodedata.name(key.strip()[0]) else "he" + if _language == "en" and key == "Routers": + distinct_product_values['ראוטרים ואינטרנט'] = distinct_product_values['Routers'] + del distinct_product_values['Routers'] + break + + #fix_value_over_max_length(distinct_product_values) + #fix_key_over_max_length(distinct_product_values) + return distinct_product_values + except Exception as ex: + print(f"get_sorted_product Exception {ex,root.text}") + return None + return None + + +def login_whatsapp(user, password): + print("LoginWhatsapp..") + data = {"username": user, "password": password} + url = END_POINT + f"/LoginWhatsapp" + headers = {'Content-Type': 'application/x-www-form-urlencoded'} + print(f"json '{data}' ") + print(url) + response = requests.post(url, data=data, headers=headers) + if response.ok: + root = ET.fromstring(response.content) + try: + data = json.loads(root.text) + error = data['table'][0].get('error', '') + if len(error) == 0: + print(f"LoginWhatsapp data: {data['table']}") + return data["table"][0] + #else: + #error.process_bot_response() + except Exception as ex: + print(f"login_whatsapp Exception {ex}") + return None + + +def fix_value_over_max_length(distinct_product_values): + for key, item in distinct_product_values.items(): + for row in item: + for insideKey, value in row.items(): + for index, trimValue in enumerate(value): + if len(trimValue) > 16: + print("Value were changed") + temp_dict = dict() + temp_dict[insideKey] = trimValue[:16] + distinct_product_values[key][index] = temp_dict + + +def fix_key_over_max_length(distinct_product_values): + temp_dict = dict() + for key, item in distinct_product_values.items(): + for index, row in enumerate(item): + for insideKey, value in row.items(): + if len(insideKey) > 16: + print("Key were changed") + #distinct_product_values[key][index][insideKey[:16]] = row.pop(insideKey) + temp_dict[insideKey[:16]]=value + keys_to_change = list(temp_dict.keys()) + + # Update the dictionary keys + for old_key in keys_to_change: + for key, item in distinct_product_values.items(): + for index, row in enumerate(item): + if old_key in row.keys(): + new_key = temp_dict[old_key] + distinct_product_values[new_key] = distinct_product_values.pop(old_key) + print(temp_dict) \ No newline at end of file diff --git a/app/app.py b/app/app.py new file mode 100644 index 0000000..ef76437 --- /dev/null +++ b/app/app.py @@ -0,0 +1,799 @@ +from dotenv import load_dotenv +import os +import time +import pytz +import pickle +import uvicorn +import requests +import threading +from pathlib import Path +from Model.models import * +from datetime import datetime, timedelta +from json import JSONDecodeError +from sqlalchemy.orm import Session +from starlette.responses import JSONResponse +from slowapi.util import get_remote_address +from slowapi.errors import RateLimitExceeded +from slowapi import Limiter, _rate_limit_exceeded_handler +from requests.structures import CaseInsensitiveDict +from DatabaseConnection import SessionLocal, engine +from fastapi.middleware.cors import CORSMiddleware +from apscheduler.schedulers.background import BackgroundScheduler +from fastapi import Depends, FastAPI, HTTPException, Request, Response, status + +requests.packages.urllib3.disable_warnings() +load_dotenv() + +PORT = os.getenv("PORT", default=8000) +TOKEN = os.getenv('TOKEN', default=None) +VERIFY_TOKEN = os.getenv("VERIFY_TOKEN", default=None) +PHONE_NUMBER_ID_PROVIDER = os.getenv("NUMBER_ID_PROVIDER", default="104091002619024") +FACEBOOK_API_URL = 'https://graph.facebook.com/v16.0' +WHATS_API_URL = 'https://api.whatsapp.com/v3' +TIMER_FOR_SEARCH_OPEN_SESSION_MINUTES = 10 +MAX_NOT_RESPONDING_TIMEOUT_MINUETS = 4 +TIME_PASS_FROM_LAST_SESSION = 2 +MINIMUM_SUSPENDED_TIME_SECONDS = 60 +EXCEEDED_REQUEST_REQUEST_LIMIT = 10 +MAX_ALLOW_PRODUCTS = 10 # LIMITED to 10 products in list by whatsapp api +if None in [TOKEN, VERIFY_TOKEN]: + raise Exception(f"Error on env var '{TOKEN, VERIFY_TOKEN}' ") + +sender = None +language_support = {"he": "he_IL", "en": "en_US"} + +headers = CaseInsensitiveDict() +headers["Accept"] = "application/json" +headers["Authorization"] = f"Bearer {TOKEN}" +session_open = False + +WORKING_HOURS_START_END = (float(os.getenv('START_TIME', default=None)), float( + os.getenv('END_TIME', default=None))) # the float number multiplied by 6 - > 17.4 = 17:24 etc.. +start_hours = str(timedelta(hours=WORKING_HOURS_START_END[0])).rsplit(':', 1)[0] +end_hour = str(timedelta(hours=WORKING_HOURS_START_END[1])).rsplit(':', 1)[0] +str_working_hours = f"{start_hours} - {end_hour}" +non_working_hours_msg = """שלום, שירותי התמיכה פתוחים בימים א-ה בין השעות {}\n + כאן ניתן לפתוח קריאה ונחזור אליכם בזמני הפעילות.\n\n +במידה והנך מעונין בקריאת חרום מעבר לזמני הפעילות יש לפתוח קריאה דרך האתר בלבד\n בכתובת https://026430010.co.il/ +""".format(str_working_hours) + +conversation = { + "Greeting": "הי! ברוכים הבאים לבוט השירות של קבוצת מוזס!\n תודה שפנית אלינו 😊\n אנו כאן כדי לעזור!\n" + "ניתן להתקשר למשרדנו בשעות הפעילות למספר 02-6430010,\n" + "על מנת לפתוח קריאת שירות עלינו לבצע הליך זיהוי קצר,\n" + "בכל שלב תוכלו לרשום 'יציאה' להתחלה מחדש\n\n" + "אם בא לכם לדבר עם נציג אנושי תמיד תוכלו לחייג לשירות הלקוחות בטלפון 02-6430010\n\n" + "*תנאי שירות https://go.mosesnet.net/wa", + "Greeting_after_working_hours": non_working_hours_msg + +} + +# Define a list of predefined conversation steps +conversation_steps = ConversationSession.conversation_steps_in_class + +limiter = Limiter(key_func=get_remote_address) + +# to enble API swagger docs +# app = FastAPI(debug=False) +app = FastAPI(docs_url=None, redoc_url=None) +app.state.limiter = limiter +app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + +# Create a new scheduler +scheduler = BackgroundScheduler() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_methods=["*"], + allow_headers=["*"], + allow_credentials=True +) + +# Create a dictionary to store the request count and timestamp for each user +user_requests = dict() + +after_working_hours_flag = False + + +def save_file_as_pickle(data, filename, path=os.path.join(os.getcwd(), "dict")): + Path(path).mkdir(parents=True, exist_ok=True) + file_path = os.path.join(path, str(filename + ".pickle")) + # save dictionary to pickle file + with open(file_path, "wb") as file: + pickle.dump(data, file, pickle.HIGHEST_PROTOCOL) + + +def load_pickle(filename, path=os.path.join(os.getcwd(), "dict")): + global user_requests + file_path = os.path.join(path, str(filename + ".pickle")) + try: + # load a pickle file + with open(file_path, "rb") as file: + user_requests = pickle.load(file) + print(f"pickle {user_requests}") + except: + print("Error loading load_pickle") + pass + + +@app.middleware("http") +async def add_process_time_header(request: Request, call_next): + global user_requests + print("~" * 100) + print(f"Received request inside middleware: {request.method} {request.url}") + + async def set_body(req: Request): + receive_ = await req._receive() + + async def receive(): + return receive_ + + request._receive = receive + + start_time = time.time() + await set_body(request) + try: + data = await request.json() + if data['object'] == "whatsapp_business_account": + entry = data['entry'][0] + messaging_events = [msg for msg in entry.get("changes", []) if msg.get("field", None) == "messages"] + event = messaging_events[0] + user_id = event['value']['messages'][0]['from'] + # print(user_id) + if user_id in user_requests: + # Get the request count and timestamp for the user + count, timestamp = user_requests[user_id] + + # Calculate the elapsed time since the last request + elapsed_time = time.time() - timestamp + + # If the elapsed time is greater than 60 seconds, reset the count and timestamp + if elapsed_time > MINIMUM_SUSPENDED_TIME_SECONDS: + count = 0 + timestamp = time.time() + + # If the user has exceeded the request limit (5 requests), raise an HTTPException + if count >= EXCEEDED_REQUEST_REQUEST_LIMIT: + response = await suspend_session_after_too_meny_request(user_id) + return Response(content=response, status_code=status.HTTP_429_TOO_MANY_REQUESTS) + + # Increment the request count and update the dictionary + count += 1 + user_requests[user_id] = (count, timestamp) + else: + # If the user is not in the dictionary, add a new entry with a count of 1 and the current timestamp + user_requests[user_id] = (1, time.time()) + # print(f'********** user_requests {user_requests} **********') + print("~" * 100) + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + else: + print("invalid object") + return Response(content="invalid object (from middleware)") + except: + response = await call_next(request) + process_time = time.time() - start_time + response.headers["X-Process-Time"] = str(process_time) + return response + + +@app.on_event("startup") +def startup(): + print("startup DB create_all..") + Base.metadata.create_all(bind=engine) + print(f"starting scheduler..") + # Add your function to the scheduler to run every x minutes + scheduler.add_job(check_for_afk_sessions, 'interval', minutes=TIMER_FOR_SEARCH_OPEN_SESSION_MINUTES) + scheduler.start() + print(f"loading user_requests from file..") + load_pickle("user_requests") + # schedule_search_for_inactive_sessions() + + +@app.on_event("shutdown") +def shutdown(): + print("shutdown DB dispose..") + engine.dispose() + print("shutdown scheduler..") + scheduler.shutdown() + print("Write active users to file..") + save_file_as_pickle(user_requests, "user_requests") + + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + + +def check_for_afk_sessions(): + try: + db_connection = next(get_db()) + results = db_connection.query(ConversationSession).filter(ConversationSession.session_active == True).all() + print(f"Active sessions: '{len(results)}' (interval: '{TIMER_FOR_SEARCH_OPEN_SESSION_MINUTES}' minutes)") + for open_session in results: + now = datetime.now() + diff = now - open_session.start_data + min = diff.total_seconds() / 60 + print(f"session opened: '{min}' ago") + if min < MAX_NOT_RESPONDING_TIMEOUT_MINUETS: + print(f"session id: '{open_session.id}' remains '{MAX_NOT_RESPONDING_TIMEOUT_MINUETS - min}' minutes") + return + try: + # print(f"end session phone: '{open_session.user_id}' id {open_session.id}") + open_session.session_active = False + db_connection.commit() + db_connection.refresh(open_session) + send_response_using_whatsapp_api( + f"יכול להיות שזה כבר לא זמן נוח עבורך להמשיך את השיחה שלנו..\nכדי שלא נפריע, ניתן לפנות אלינו שוב בשליחת הודעה כשמתאפשר לך,\nכאן או בכל אחד מערוצי התקשורת שלנו, המשך יום נעים 😊", + _specific_sendr=open_session.user_id) + print("session closed!") + except Exception as er: + print(er) + continue + except Exception: + pass + + +async def suspend_session_after_too_meny_request(user): + try: + print("Suspending session") + msg = f"יכול להיות שזה כבר לא זמן נוח עבורך להמשיך את השיחה שלנו..\nכדי שלא נפריע, ניתן לפנות אלינו שוב בשליחת הודעה כשמתאפשר לך,\n כאן או בכל אחד מערוצי התקשורת שלנו, nהמשך יום נעים 😊" + db_connection = next(get_db()) + result = db_connection.query(ConversationSession).filter(ConversationSession.user_id == user, + ConversationSession.session_active == True).first() + if result is None: + msg = f"אין באפשרותך לשלוח הודעות, אנא המתן מספר דקות" + send_response_using_whatsapp_api(msg, _specific_sendr=user) + return msg + result.session_active = False + db_connection.commit() + db_connection.refresh(result) + send_response_using_whatsapp_api(msg, _specific_sendr=result.user_id) + print("too meny request!, session closed!") + return msg + except Exception as er: + print(er) + + +def schedule_search_for_inactive_sessions(): + print(f"Searching for sessions over {MAX_NOT_RESPONDING_TIMEOUT_MINUETS} minutes...") + threading.Timer(TIMER_FOR_SEARCH_OPEN_SESSION_MINUTES, schedule_search_for_inactive_sessions).start() + check_for_afk_sessions() + + +@app.get("/") +@limiter.limit('5/minute') +async def root(request: Request): + print("root router 1!") + return {"Hello": "FastAPI"} + + +@app.get("/user/{user}", status_code=200) +def get_user(user, response: Response, db: Session = Depends(get_db)): + db_user = db.query(User).filter(User.name == user).first() + if db_user: + return {"Result": f"Found {db_user.name}"} + # response.status_code = status.HTTP_204_NO_CONTENT + return {"Result": f"{user} not Found"} + + +@app.get("/get_all_created_issues/{user}") +def get_conversations(user, db: Session = Depends(get_db)): + results = list() + if user.lower() not in ["admin", "servercheck"]: + raise HTTPException(status_code=400, detail="Only admin can get all created issues") + created_issues_history = db.query(Issues).filter(Issues.issue_sent_status == True).all() + for issue in created_issues_history: + results.append({"ID": issue.conversation_id, "Message": issue.issue_data}) + return JSONResponse(content={"sessions": results}) + + +@app.get("/reset/{user}") +def reset_all_sessions(user, db: Session = Depends(get_db)): + if user.lower() not in ["admin"]: + raise HTTPException(status_code=400, detail="Only admin can reset all sessions") + conversation_history = db.query(ConversationSession).filter(ConversationSession.session_active == True).all() + for session in conversation_history: + # session.set_status(db, False) + session.session_active = False + db.commit() + print("All conversation were reset") + return "All conversation were reset" + # user = User(db=db, name="Alice", password="password", phone="555-1234") + # # user_ = User(db, user) + # existing_user = db.query(User).filter(User.phone == user.phone).first() + # if existing_user: + # raise HTTPException(status_code=400, detail="Phone number already registered") + # db.add(user) + # db.commit() + # db.refresh(user) + # return f"'{user}' Created" + + +@app.get("/all_users") +async def get_users(db=Depends(get_db)): + # result = db.execute(text("SELECT * FROM users")) + # rows = result.fetchall() + result = db.query(User).all() + return {"Results": result} + + +@app.post("/webhook") +@limiter.limit('100/minute') +async def handle_message_with_request_scheme(request: Request, data: WebhookRequestData, db: Session = Depends(get_db)): + try: + # print("handle_message webhook") + global sender + message = "ok" + if data.object == "whatsapp_business_account": + for entry in data.entry: + messaging_events = [msg for msg in entry.get("changes", []) if msg.get("field", None) == "messages"] + # print(f"total events: '{len(messaging_events)}'") + for event in messaging_events: + if event['value'].get('messages', None) is None: + print(f"event is not a messages") + return Response(content="Event is not a messages") + type = event['value']['messages'][0].get('type', None) + if type is None: + print("None type") + return Response(content="None type found") + # return Response(content="None type found", status_code=status.HTTP_400_BAD_REQUEST) + if type == "text": + # print("text") + text = event['value']['messages'][0]['text']['body'] + sender = event['value']['messages'][0]['from'] + message = process_bot_response(db, text) + elif type == "button": + print("Button") + text = event['value']['messages'][0]['button']['text'] + sender = event['value']['messages'][0]['from'] + message = process_bot_response(db, text, button_selected=True) + elif type == "interactive": + print("interactive") + if event['value']['messages'][0]['interactive']["type"] == "button_reply": + text = event['value']['messages'][0]['interactive']['button_reply']["title"] + elif event['value']['messages'][0]['interactive']["type"] == "list_reply": + text = event['value']['messages'][0]['interactive']['list_reply']['title'] + else: + raise Exception("unknown type {event['value']['messages'][0]['interactive']}") + sender = event['value']['messages'][0]['from'] + message = process_bot_response(db, text, button_selected=True) + # res = f"Json: '{event['value']['messages'][0]}'" + # print(res) + else: + print(f"Type '{type}' is not valid") + message = f"Json: '{event['value']['messages'][0]}'" + print(message) + return Response(content=message) + else: + print(data.object) + return Response(content=message) + except JSONDecodeError: + message = "Received data is not a valid JSON (JSONDecodeError)" + except Exception as ex: + print(f"Exception: '{ex}'") + message = str(ex) + return Response(content=message) + + +@app.get("/webhook") +async def verify(request: Request): + """ + On webhook verification VERIFY_TOKEN has to match the token at the + configuration and send back "hub.challenge" as success. + """ + print("verify token") + if request.query_params.get("hub.mode") == "subscribe" and request.query_params.get("hub.challenge"): + if not request.query_params.get("hub.verify_token") == os.environ["VERIFY_TOKEN"]: + return Response(content="Verification token mismatch", status_code=403) + return Response(content=request.query_params["hub.challenge"]) + + return Response(content="Required arguments haven't passed.", status_code=400) + + +def check_for_timeout(db, sender): + """ + Check if last session of the user was at least x minutes old + return True if yes, False otherwise + """ + # return False + print("Checking for timeout to prevent abuse of issues creation..") + session = db.query(ConversationSession).filter(ConversationSession.user_id == sender, + ConversationSession.session_active == False).order_by( + ConversationSession.start_data.desc()).first() + if session is None: + return False + now = datetime.now() + diff = now - session.start_data + minutes = diff.total_seconds() / 60 + if TIME_PASS_FROM_LAST_SESSION > minutes: + return True + return False + + +def process_bot_response(db, user_msg: str, button_selected=False) -> str: + after_working_hours() + # if after_working_hours(): + # print("after working hours") + # send_response_using_whatsapp_api(non_working_hours_msg) + # return non_working_hours_msg + # log = "" + # if user_msg in ["אדמין", "servercheck"]: + # created_issues_history = db.query(Issues).filter(Issues.issue_sent_status == True).all() + # for issue in created_issues_history: + # log += f"Conversation ID: {issue.conversation_id} Sent message: {issue.issue_data}\n" + # send_response_using_whatsapp_api(log) + # return log + if user_msg.lower() in ["reset"]: + conversation_history = db.query(ConversationSession).filter(ConversationSession.session_active == True).all() + for session in conversation_history: + # session.set_status(db, False) + session.session_active = False + db.commit() + print("All conversation were reset") + return "All conversation were reset" + next_step_after_increment = "" + session = check_if_session_exist(db, sender) + if session is None or session.call_flow_location == 0: + print(f"Hi {sender} You are new!:") + steps_message = "" + for key, value in conversation_steps.items(): + steps_message += f"{value} - {key}\n" + print(f"{steps_message}") + + if after_working_hours_flag: + send_response_using_whatsapp_api(conversation["Greeting_after_working_hours"]) + else: + send_response_using_whatsapp_api(conversation["Greeting"]) + + # Handling session after restart dou to max login attempts + if session is None: + session = ConversationSession(user_id=sender, db=db) + db.add(session) + db.commit() + session.increment_call_flow(db) + send_response_using_whatsapp_api(conversation_steps[str(session.call_flow_location)]) + return conversation_steps[str(session.call_flow_location)] + else: + if user_msg.lower() in ["יציאה"]: + session.session_active = False + db.commit() + print("Your session end") + send_response_using_whatsapp_api("השיחה הסתיימה, על מנת לחדש את השיחה אנא שלח הודעה") + return "השיחה הסתיימה, על מנת לחדש את השיחה אנא שלח הודעה" + current_conversation_step = str(session.call_flow_location) + print(f"Current step is: {current_conversation_step}") + is_answer_valid, message_in_error = session.validate_and_set_answer(db, current_conversation_step, user_msg, + button_selected) + if is_answer_valid: + if not button_selected: + session.increment_call_flow(db) + next_step_after_increment = str(session.call_flow_location) + if current_conversation_step == "2": # log in + subject_groups = session.get_all_client_product_and_save_db_subjects(db) + if subject_groups is None: + message = f"שלום '{session.get_conversation_step_json('2')}' !" + send_response_using_whatsapp_api(message) + print("\nאין מוצרים!") + session.set_call_flow(db, 5) + next_step_after_increment = str(session.call_flow_location) + message = conversation_steps[next_step_after_increment] + return send_interactive_response(message, ["חזור למספר זה"]) + # message += "\nאין מוצרים!" + # session.session_active = False + # db.commit() + # print("Your session end") + # send_response_using_whatsapp_api(message) + # send_response_using_whatsapp_api("השיחה הסתיימה, על מנת לחדש את השיחה אנא שלח הודעה") + # return f"השיחה הסתיימה, על מנת לחדש את השיחה אנא שלח הודעה\n{message}" + else: + if session.is_product_more_then_max(MAX_ALLOW_PRODUCTS): + print(f"subject_groups more then max {MAX_ALLOW_PRODUCTS}") + message = f"שלום '{session.get_conversation_step_json('2')}' !\n{conversation_steps[next_step_after_increment]}" + # show buttons for step 3 + return send_interactive_response(message, subject_groups) + elif current_conversation_step in ["3", "4", "5"]: + if button_selected: + print(f"selected button: '{user_msg}'") + session.increment_call_flow(db) + next_step_after_increment = str(session.call_flow_location) + if current_conversation_step == "3": + # show buttons for step 4 + products = session.get_products(user_msg) + if products: + products_2 = list() + for p in products: + for k, v in p.items(): + products_2.append(k) + if len(products_2) > MAX_ALLOW_PRODUCTS: + print(f"products more then max {MAX_ALLOW_PRODUCTS}") + session.set_call_flow(db, 5) # skip on step 4 + next_step_after_increment = str(session.call_flow_location) + message = conversation_steps[next_step_after_increment] + return send_interactive_response(message, ["חזרו אלי למספר זה"]) + return send_interactive_response(conversation_steps[next_step_after_increment], + products_2) + else: + print("product return empty") + return send_interactive_response(conversation_steps[next_step_after_increment], + ["חזרו אלי למספר זה"]) + # send_response_using_whatsapp_api(conversation_steps[next_step_after_increment]) + # return conversation_steps[next_step_after_increment] + # return "Choose product..." + elif current_conversation_step == "4": + return send_interactive_response(conversation_steps[next_step_after_increment], + ["חזרו אלי למספר זה"]) + else: + send_response_using_whatsapp_api(conversation_steps[next_step_after_increment]) + return conversation_steps[next_step_after_increment] + else: + send_response_using_whatsapp_api(conversation_steps[next_step_after_increment]) + # check if is last step + if next_step_after_increment != str(len(conversation_steps)): + return conversation_steps[next_step_after_increment] + # Check if conversation reach to last step + if next_step_after_increment == str(len(conversation_steps)): # 8 + summary = json.loads(session.convers_step_resp) + client_id = session.password.split(";")[1] + _phone_number_with_0 = summary['5'].replace('972', '0') + kria_header = f"מספר מדבקה: {summary['4']}" if summary[ + '4'].isdigit() else f"שים לב למוצר אין מדבקה: {summary['4']}" + keria_body = summary['7'] + kria_footer = f"המספר ממנו נפתחה הקריאה: {session.user_id.replace('972', '0')}" + data = {"technicianName": f"{_phone_number_with_0} {summary['6']}", + # product name and phone + "kria": f"{keria_body}\n{kria_header}\n{kria_footer}", + # issue details and orig phone number + "clientCode": f"{client_id}"} # client code + if len(data["technicianName"]) > 20: + print("technicianName is Over 20 character! Set technicianName only phone number without name") + data["technicianName"] = f"{summary['5']}" + new_issue = Issues(conversation_id=session.id, + item_id=session.get_conversation_step_json("4"), + issue_data=data["kria"] + ) + db.add(new_issue) + # db.commit() + print(f"Issue successfully created! {new_issue}") + data["notePayment"] = f"נכנס מהוואצפ על ידי הלקוח: {session.get_conversation_step_json('2')}" + if moses_api.create_kria(data): + print(f"Kria successfully created! {data}") + # new_issue.set_issue_status(db, True) + new_issue.issue_sent_status = True + else: + print(f"Failed to create Kria {data}") + print("Conversation ends!") + # session.set_status(db, False) + session.session_active = False + db.commit() + return "Conversation ends!" + else: + raise Exception("Unknown step after check for end conversation") + else: + print("Invalid response try again") + if current_conversation_step in ["2", "3"]: + session.session_active = False + db.commit() + print("Your session end") + send_response_using_whatsapp_api(message_in_error) + return message_in_error + send_response_using_whatsapp_api(message_in_error) + return conversation_steps[current_conversation_step] + + +def trigger_bot_response_after_wrong_auth(): + """Trigger bot response after wrong auth.""" + print(f"Trigger bot response after wrong auth") + json_data = { + "object": "whatsapp_business_account", + "entry": [ + { + "id": "8856996819413533", + "changes": [ + { + "value": { + "messaging_product": "whatsapp", + "metadata": { + "display_phone_number": "16505553333", + "phone_number_id": "27681414235104944" + }, + "contacts": [ + { + "profile": { + "name": "Kerry Fisher" + }, + "wa_id": "16315551234" + } + ], + "messages": [ + { + "from": f"{sender}", + "id": "wamid.ABGGFlCGg0cvAgo-sJQh43L5Pe4W", + "timestamp": "1603059201", + "text": { + "body": "retry login" + }, + "type": "text" + } + ] + }, + "field": "messages" + } + ] + } + ] + } + headers = {'Content-type': 'application/json', 'Accept': 'application/json'} + res = requests.post(f'http://localhost:{PORT}/webhook', json=json.dumps(json_data), headers=headers) + print(res) + + +def send_response_using_whatsapp_api(message, debug=False, _specific_sendr=None): + """Send a message using the WhatsApp Business API.""" + # Todo: handle exceptions, after sending message failure, call get incremented should not be incremented + try: + print(f"Sending message: '{message}' ") + url = f"{FACEBOOK_API_URL}/{PHONE_NUMBER_ID_PROVIDER}/messages" + + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": f"{sender if _specific_sendr is None else _specific_sendr}", + "type": "text", + "text": { + "preview_url": False, + "body": f"{message}" + } + } + + if debug: + print(f"Payload '{payload}' ") + print(f"Headers '{headers}' ") + print(f"URL '{url}' ") + response = requests.post(url, json=payload, headers=headers) + if not response.ok: + raise Exception(f"Error on sending message, json: {payload}") + print(f"Message sent successfully to :'{sender if _specific_sendr is None else _specific_sendr}'!") + return f"Message sent successfully to :'{sender if _specific_sendr is None else _specific_sendr}'!" + except Exception as EX: + print(f"Error send whatsapp : '{EX}'") + raise EX + + +def send_interactive_response(message, chooses, debug=False): + try: + print(f"Sending interactive message: '{chooses}' ") + url = f"{FACEBOOK_API_URL}/{PHONE_NUMBER_ID_PROVIDER}/messages" + buttons = [{ + "type": "reply", + "reply": { + "id": i, + "title": msg + }} for i, msg in enumerate(chooses)] + + if len(chooses) == 1: + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": f"{sender}", + "type": "interactive", + "interactive": { + "type": "button", + "body": { + "text": f"{message}" + }, + "action": { + "buttons": json.dumps(buttons) + } + } + } + else: + if len(chooses) > 10: + print("More then 10, use only 10 first!!") + buttons = [{ + "id": i, + "title": msg, + "description": "", + } for i, msg in enumerate(chooses[:10])] + else: + buttons = [{ + "id": i, + "title": msg, + "description": "", + } for i, msg in enumerate(chooses)] + print("Multiple options") + payload = { + "messaging_product": "whatsapp", + "recipient_type": "individual", + "to": f"{sender}", + "type": "interactive", + "interactive": { + "type": "list", + "body": { + "text": f"{message}" + }, + "action": { + "button": "לחץ לבחירה", + "sections": [ + { + "title": "בחר פריט מהרשימה", + "rows": json.dumps(buttons) + }] + } + } + } + # Todo: Fix right to left on body for list + if debug: + print(f"Payload '{payload}' ") + print(f"Headers '{headers}' ") + print(f"URL '{url}' ") + response = requests.post(url, json=payload, headers=headers, verify=False) + if not response.ok: + return f"Failed send interactive message, response: '{response}'" + print(f"Interactive message sent successfully to :'{sender}'!") + # return f"Interactive sent successfully to :'{sender}'!" + return f"{message}\n{chooses}" + except Exception as EX: + print(f"Error send interactive whatsapp : '{EX}'") + raise EX + + +def check_if_session_exist(db, user_id): + session = db.query(ConversationSession).filter(ConversationSession.user_id == user_id, + ConversationSession.session_active == True).all() + if len(session) > 1: + print(f"There is more then one active call for {user_id}, returning last one") + return session[-1] + if len(session) == 1: + # print("SESSION exist!") + return session[0] + return None + + +def after_working_hours(): + global after_working_hours_flag + # Get Day Number from weekday + # weekday: Sunday is 6 + # Monday is 0 + # tuesday is 1 + # wednesday is 2 + # thursday is 3 + # friday is 4 + # saturday is 5 + # Todo: remove False + # return False + week_num = datetime.today().weekday() + + if week_num in [4, 5]: + # print("Today is a Weekend") + after_working_hours_flag = True + return True + else: + pass + # print(f"Today is weekday {week_num}") + + current_time = datetime.now(pytz.timezone('Israel')) + if current_time.hour > WORKING_HOURS_START_END[1] or current_time.hour < WORKING_HOURS_START_END[0]: + # print("Now is NOT working hours") + after_working_hours_flag = True + return True + else: + pass + # print("Now is working hours") + after_working_hours_flag = False + return False + + +if __name__ == "__main__": + print("From main") + uvicorn.run(app, + host="0.0.0.0", + port=int(PORT), + log_level="info") diff --git a/app/dict/user_requests.pickle b/app/dict/user_requests.pickle new file mode 100644 index 0000000000000000000000000000000000000000..b803cbef6dde137ba7d94365dd5fc81e234f3008 GIT binary patch literal 1692 zcmZ9MYlu}<6vwrBNKQ~GI6gu#j)xSL{oZTSr!+=uq^LU@w>#HYFfw8qQFGH~C?y9Z z&77`CVOCT0!AF?}jt&g?L1#v1jQpTTDfyTWgMnD0kdm%_&R%EAbMLnw|Gocf?f+UI zR_)wTJu3Oy%Fk?l%vqs@2IsgG<@(VJp1ZL3&#xvl9B(iGkLi?+`?~jLoqmW}Vy6pt z&pgB=v4Noj*NHK!j4>>`ueY=33Nay>U`DD`H)+mp_`8vqGS&*AgO|yjdkWL)h=JBz zYh%jwk6;Zm?oV$z+f9rKF0?Xu-xy^1*1lJ_zmqXz0GtSCPK_q)?7TrNz0bOGeHH5B z6$f)Q&BUYwBP9f%v+w%Q1rc|!$eiK;@zJqYa z;6Jn_YB=Avsg-0{wy^?;EgtUOUX?M)lu;I~_|oGg4GRygC&m<47QfU=v}Ang@h6B0 z$&E!RG4}lGl^HX@IdIY)cY52^uU9B^0;b9_KQq zwE_h|tm%#7C&Z)x3Xo{Deh6tUX>kl1{MJC5iCqjDBiuS-70e#M(KNJ}rrt zgC{5bn@xi-%rF4*a%iaWV%LbyNQQQTm!JRH-2E~!ZNM<=s4qv_x_W?^a(=!t*5C2= z%pDoiPGCMm;-mi0wl!TQhG&9u8f*78%)ay82b{q^G`fFB14^3 z!Ae}ctNQAbK_(>wbcwh5ZpO=+r?1&ZGG?{K0Sldu7d+cDP#`8P7lPCNh!@THXt*U~ z+6#3^OT6gzvRyu$=?(-O2K<%lEB$@BBcq!8iQzosTJo>ptBZ5Pdpoin!N3~BIi8vL zTwHr=_#Ej3HcT)Uk{cIu%{P9`nAF%Y6}lN0XMOq005MQpaKponm{VDKc;9_uLJ20N z2ww8Dn(yCzG_$u$Kl)N9)EDOLwQkbozdl27miw-X>yFD2-1M7FKES&gRbb J?d5mpJ^=5ZE9L+I literal 0 HcmV?d00001 diff --git a/app/requirements.txt b/app/requirements.txt new file mode 100644 index 0000000..374b186 --- /dev/null +++ b/app/requirements.txt @@ -0,0 +1,41 @@ +anyio==3.6.2 +APScheduler==3.10.1 +attrs==22.2.0 +certifi==2022.12.7 +cffi==1.15.1 +charset-normalizer==3.0.1 +click==8.1.3 +colorama==0.4.6 +cryptography==39.0.2 +Deprecated==1.2.13 +exceptiongroup==1.1.1 +fastapi==0.92.0 +greenlet==2.0.2 +gunicorn==20.1.0 +h11==0.14.0 +idna==3.4 +iniconfig==2.0.0 +limits==2.8.0 +packaging==22.0 +pluggy==1.0.0 +pycparser==2.21 +pydantic==1.10.5 +PyMySQL==1.0.2 +pytest==7.2.2 +python-dotenv==1.0.0 +pytz==2022.7.1 +pytz-deprecation-shim==0.1.0.post0 +requests==2.28.2 +six==1.16.0 +slowapi==0.1.7 +sniffio==1.3.0 +SQLAlchemy==2.0.4 +SQLAlchemy-Utils==0.40.0 +starlette==0.25.0 +tomli==2.0.1 +typing_extensions==4.5.0 +tzdata==2023.3 +tzlocal==4.3 +urllib3==1.26.14 +uvicorn==0.20.0 +wrapt==1.15.0 diff --git a/app/test_app.py b/app/test_app.py new file mode 100644 index 0000000..498bfc9 --- /dev/null +++ b/app/test_app.py @@ -0,0 +1,16 @@ +import os +from app import app +from fastapi import status +from fastapi.testclient import TestClient +# TOKEN = os.getenv('TOKEN', default=None) +TOKEN = os.environ["TOKEN"] +VERIFY_TOKEN = os.environ["VERIFY_TOKEN"] + +client = TestClient(app=app) + + +def test_root(): + print("test root") + response = client.get("/") + assert response.status_code == status.HTTP_200_OK + assert response.json() == {"Hello": "FastAPI"} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d25d5ce --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.8' + +services: + app: + build: . + command: uvicorn app:app --host 0.0.0.0 + ports: + - "8000:8000" +# depends_on: +# - db + +# db: +# container_name: mysql-db +# image: mysql:latest +# restart: always +# environment: +# MYSQL_DATABASE: moses +# MYSQL_ROOT_PASSWORD: root +# MYSQL_USER: root +# MYSQL_PASSWORD: root +# MYSQL_HOST: db +# MYSQL_PORT: 3306 +# ports: +# - "3307:3306" +# volumes: +# - db-data:/var/lib/mysql + +# volumes: +# db-data: \ No newline at end of file