├── ampalibe ├── __main__.py ├── old.py ├── singleton.py ├── crypt.py ├── cmd.py ├── decorators.py ├── logger.py ├── constant.py ├── payload.py ├── utils.py ├── core.py ├── admin.py ├── tools.py ├── source.py ├── __init__.py ├── model.py └── ui.py ├── _config.yml ├── tests ├── app │ ├── assets │ │ └── public │ │ │ └── hello.txt │ ├── langs.json │ ├── .env │ ├── conf.py │ └── core.py ├── deploy.sh ├── test_cli.sh ├── test.sh └── test_api_messenger.py ├── docs ├── source │ ├── _static │ │ ├── LOGO.png │ │ ├── button.png │ │ ├── logger.png │ │ ├── personas.png │ │ ├── quickrep.png │ │ ├── structure.png │ │ ├── template.png │ │ ├── ampalibe_logo.png │ │ └── product_template.png │ ├── generated │ │ ├── ampalibe.event.rst │ │ ├── ampalibe.action.rst │ │ ├── ampalibe.command.rst │ │ ├── ampalibe.crontab.rst │ │ ├── ampalibe.simulate.rst │ │ ├── ampalibe.translate.rst │ │ ├── ampalibe.download_file.rst │ │ ├── ampalibe.Payload.rst │ │ └── ampalibe.Model.rst │ ├── api.rst │ ├── command.rst │ ├── index.rst │ ├── conf.py │ ├── usage.rst │ ├── intro.rst │ ├── more.rst │ ├── quickstart.rst │ └── messenger.rst ├── Makefile └── make.bat ├── requirements.txt ├── Dockerfile ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ ├── cd-pg.yml │ ├── cd-pypi.yml │ ├── ci-python-package.yml │ └── ci.yml ├── .readthedocs.yaml ├── setup.cfg ├── LICENSE ├── bin ├── ampalibe.bat └── ampalibe ├── setup.py ├── .gitignore ├── CHANGELOG.md └── README.md /ampalibe/__main__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /tests/app/assets/public/hello.txt: -------------------------------------------------------------------------------- 1 | hello_world -------------------------------------------------------------------------------- /docs/source/_static/LOGO.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/LOGO.png -------------------------------------------------------------------------------- /docs/source/_static/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/button.png -------------------------------------------------------------------------------- /docs/source/_static/logger.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/logger.png -------------------------------------------------------------------------------- /docs/source/_static/personas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/personas.png -------------------------------------------------------------------------------- /docs/source/_static/quickrep.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/quickrep.png -------------------------------------------------------------------------------- /docs/source/_static/structure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/structure.png -------------------------------------------------------------------------------- /docs/source/_static/template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/template.png -------------------------------------------------------------------------------- /docs/source/_static/ampalibe_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/ampalibe_logo.png -------------------------------------------------------------------------------- /docs/source/_static/product_template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iTeam-S/Ampalibe/HEAD/docs/source/_static/product_template.png -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.event.rst: -------------------------------------------------------------------------------- 1 | ampalibe.event 2 | ============== 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autofunction:: event -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.action.rst: -------------------------------------------------------------------------------- 1 | ampalibe.action 2 | =============== 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autofunction:: action -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.command.rst: -------------------------------------------------------------------------------- 1 | ampalibe.command 2 | ================ 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autofunction:: command -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.crontab.rst: -------------------------------------------------------------------------------- 1 | ampalibe.crontab 2 | ================ 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autofunction:: crontab -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.simulate.rst: -------------------------------------------------------------------------------- 1 | ampalibe.simulate 2 | ================= 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autofunction:: simulate -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.translate.rst: -------------------------------------------------------------------------------- 1 | ampalibe.translate 2 | ================== 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autofunction:: translate -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | retry 4 | requests_toolbelt 5 | requests 6 | aiocron 7 | colorama 8 | tinydb 9 | httpx 10 | sqlmodel 11 | sqladmin 12 | -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.download_file.rst: -------------------------------------------------------------------------------- 1 | ampalibe.download\_file 2 | ======================= 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autofunction:: download_file -------------------------------------------------------------------------------- /tests/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Build the framework 4 | docker build . -t ampalibe 5 | 6 | # run a container 7 | docker run -d -v "${PWD}/tests/app:/usr/src/app" -p 4555:4555 --name amp ampalibe 8 | 9 | # Attente de stabilité du serveur 10 | sleep 2 11 | -------------------------------------------------------------------------------- /tests/app/langs.json: -------------------------------------------------------------------------------- 1 | { 2 | "hello_world": { 3 | "en": "Hello World", 4 | "fr": "Bonjour le monde" 5 | }, 6 | 7 | "ampalibe": { 8 | "en": "Jackfruit", 9 | "fr": "Jacquier", 10 | "mg": "Ampalibe" 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /ampalibe/old.py: -------------------------------------------------------------------------------- 1 | from .model import Model 2 | from .messenger import Messenger 3 | 4 | 5 | class Init: 6 | def __init__(self, *args): 7 | """ 8 | Leave this class for compatibility with old version 9 | """ 10 | self.query = Model() 11 | self.chat = Messenger() 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.8.10 2 | 3 | WORKDIR /usr/src/app 4 | 5 | ADD . /usr/src/app 6 | 7 | RUN pip install --no-cache-dir -r requirements.txt && python setup.py install --force && rm -rf * 8 | 9 | CMD if [ -f "requirements.txt" ]; then pip install --no-cache-dir -r requirements.txt ; fi ; \ 10 | ampalibe run -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | Api 2 | ==== 3 | 4 | .. autosummary:: 5 | :toctree: generated 6 | 7 | ampalibe.command 8 | ampalibe.action 9 | ampalibe.event 10 | ampalibe.Payload 11 | ampalibe.Model 12 | ampalibe.constant 13 | ampalibe.translate 14 | ampalibe.crontab 15 | ampalibe.simulate 16 | ampalibe.download_file -------------------------------------------------------------------------------- /ampalibe/singleton.py: -------------------------------------------------------------------------------- 1 | def singleton(cls): 2 | """ 3 | Function to create a singleton class 4 | """ 5 | instances = {} 6 | 7 | def getinstance(*args, **kwargs): 8 | if cls not in instances: 9 | instances[cls] = cls(*args, **kwargs) 10 | return instances[cls] 11 | 12 | return getinstance 13 | -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.Payload.rst: -------------------------------------------------------------------------------- 1 | ampalibe.Payload 2 | ================ 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autoclass:: Payload 7 | 8 | 9 | .. automethod:: __init__ 10 | 11 | 12 | .. rubric:: Methods 13 | 14 | .. autosummary:: 15 | 16 | ~Payload.__init__ 17 | ~Payload.trt_payload_in 18 | ~Payload.trt_payload_out 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/source/command.rst: -------------------------------------------------------------------------------- 1 | Command-line 2 | ============== 3 | 4 | .. code-block:: console 5 | 6 | $ ampalibe [ -p ] {options} 7 | 8 | options: 9 | 10 | **create** <*dir*> : create a new project in a new directory specified 11 | 12 | **init** : create a new project in current dir 13 | 14 | **version** : show the current version 15 | 16 | **env** : generate only a .env file 17 | 18 | **lang** : generate only a langs.json file 19 | 20 | **run** [*--dev*] : run the server, autoreload if --dev is specified 21 | -------------------------------------------------------------------------------- /docs/source/generated/ampalibe.Model.rst: -------------------------------------------------------------------------------- 1 | ampalibe.Model 2 | ============== 3 | 4 | .. currentmodule:: ampalibe 5 | 6 | .. autoclass:: Model 7 | 8 | 9 | .. automethod:: __init__ 10 | 11 | 12 | .. rubric:: Methods 13 | 14 | .. autosummary:: 15 | 16 | ~Model.__init__ 17 | ~Model.del_temp 18 | ~Model.get 19 | ~Model.get_action 20 | ~Model.get_lang 21 | ~Model.get_temp 22 | ~Model.set_action 23 | ~Model.set_lang 24 | ~Model.set_temp 25 | ~Model.verif_db 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ampalibe/crypt.py: -------------------------------------------------------------------------------- 1 | def encode(text, key): 2 | encoded_text = "" 3 | for i in range(len(text)): 4 | key_char = key[i % len(key)] 5 | encoded_char = chr((ord(text[i]) + ord(key_char)) % 256) 6 | encoded_text += encoded_char 7 | return encoded_text 8 | 9 | 10 | def decode(encoded_text, key): 11 | decoded_text = "" 12 | for i in range(len(encoded_text)): 13 | key_char = key[i % len(key)] 14 | decoded_char = chr((ord(encoded_text[i]) - ord(key_char) + 256) % 256) 15 | decoded_text += decoded_char 16 | return decoded_text -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | **Expected behavior** 17 | A clear and concise description of what you expected to happen. 18 | 19 | **Screenshots** 20 | If applicable, add screenshots to help explain your problem. 21 | 22 | - OS: [e.g. Windows] 23 | - Ampalibe Version [e.g. 1.0.4] 24 | 25 | 26 | **Additional context** 27 | Add any other context about the problem here. 28 | -------------------------------------------------------------------------------- /ampalibe/cmd.py: -------------------------------------------------------------------------------- 1 | class Cmd(str): 2 | """ 3 | Object for text of message 4 | """ 5 | 6 | webhook = "message" 7 | token = None 8 | __atts = [] 9 | 10 | def __init__(self, text): 11 | str.__init__(text) 12 | 13 | def set_atts(self, atts): 14 | for att in atts: 15 | self.__atts.append(att) 16 | 17 | @property 18 | def attachments(self): 19 | return self.__atts 20 | 21 | def copy(self, text): 22 | new_cmd = Cmd(text) 23 | new_cmd.__atts = self.attachments 24 | new_cmd.webhook = self.webhook 25 | new_cmd.token = self.token 26 | return new_cmd 27 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/app/.env: -------------------------------------------------------------------------------- 1 | # PAGE ACCESS TOKEN 2 | export AMP_ACCESS_TOKEN= 3 | 4 | # PAGE VERIF TOKEN 5 | export AMP_VERIF_TOKEN=AMPALIBE 6 | 7 | 8 | # DATABASE AUTHENTIFICATION 9 | export ADAPTER=SQLITE 10 | #export ADAPTER=MYSQL 11 | #export ADAPTER=POSTGRESQL 12 | 13 | ####### CASE MYSQL OR POSTGRESQL ADAPTER 14 | export DB_HOST= 15 | export DB_USER= 16 | export DB_PASSWORD= 17 | export DB_NAME= 18 | 19 | # CASE MYSQL ADAPTER 20 | # export DB_PORT=3306 21 | # CASE POSTGRESQL ADAPTER 22 | # export DB_PORT=5432 23 | 24 | ####### CASE SQLITE ADAPTER 25 | export DB_FILE=ampalibe.db 26 | 27 | 28 | # APPLICATION CONFIGURATION 29 | export AMP_HOST=0.0.0.0 30 | export AMP_PORT=4555 31 | 32 | # URL APPLICATION 33 | export AMP_URL= 34 | 35 | 36 | -------------------------------------------------------------------------------- /tests/app/conf.py: -------------------------------------------------------------------------------- 1 | from os import environ as env 2 | 3 | 4 | class Configuration: 5 | ''' 6 | Retrieves the value from the environment. 7 | Takes the default value if not defined. 8 | ''' 9 | ADAPTER = env.get('ADAPTER') 10 | 11 | DB_FILE = env.get('DB_FILE') 12 | 13 | DB_HOST = env.get('DB_HOST', 'localhost') 14 | DB_USER = env.get('DB_USER', 'root') 15 | DB_PASSWORD = env.get('DB_PASSWORD', '') 16 | DB_PORT = env.get('DB_PORT', 3306) 17 | DB_NAME = env.get('DB_NAME') 18 | 19 | ACCESS_TOKEN = env.get('AMP_ACCESS_TOKEN') 20 | VERIF_TOKEN = env.get('AMP_VERIF_TOKEN') 21 | 22 | APP_HOST = env.get('AMP_HOST', '0.0.0.0') 23 | APP_PORT = int(env.get('AMP_PORT', 4555)) 24 | APP_URL = env.get('AMP_URL') 25 | 26 | WORKERS = env.get('WORKERS', 1) 27 | 28 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Ampalibe framework 2 | ================================================ 3 | 4 | **Ampalibe** is a lightweight Python framework for building Facebook Messenger bots faster. 5 | It uses api from the `Messenger platforms `_ 6 | and offers a *simple* and *quick* functions to use it. 7 | 8 | Check out the :doc:`usage` section for further information, including 9 | how to :ref:`installation` the project. 10 | 11 | you can also see video resource links `here `_ 12 | 13 | .. note:: 14 | 15 | This project is under active development. 16 | 17 | Contents 18 | -------- 19 | 20 | .. toctree:: 21 | 22 | intro 23 | usage 24 | quickstart 25 | messenger 26 | more 27 | api 28 | command 29 | 30 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | # You can also specify other tool versions: 14 | # nodejs: "16" 15 | # rust: "1.55" 16 | # golang: "1.17" 17 | 18 | # Build documentation in the docs/ directory with Sphinx 19 | sphinx: 20 | configuration: docs/source/conf.py 21 | 22 | # If using Sphinx, optionally build your docs in additional formats such as PDF 23 | # formats: 24 | # - pdf 25 | 26 | # Optionally declare the Python requirements required to build your docs 27 | python: 28 | install: 29 | - requirements: requirements.txt -------------------------------------------------------------------------------- /.github/workflows/cd-pg.yml: -------------------------------------------------------------------------------- 1 | name: CD Package 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | env: 9 | REGISTRY: ghcr.io 10 | TAGS: iteam-s/ampalibe:latest 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Log in to the Container registry 24 | run: | 25 | docker login -u ${{ github.actor }} -p ${{ secrets.TOKEN_PACKAGE }} ${{ env.REGISTRY }} 26 | 27 | - name: Build image 28 | run: | 29 | docker build . -t ampalibe 30 | docker tag ampalibe ghcr.io/iteam-s/ampalibe 31 | docker push ghcr.io/iteam-s/ampalibe 32 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = Ampalibe 3 | version = 2.0.0.dev 4 | author = iTeam-$ 5 | author_email = contact@iteam-s.mg 6 | description = Ampalibe is a lightweight Python framework for building Facebook Messenger bots faster. 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | url = https://github.com/iTeam-S/ampalibe 10 | project_urls = 11 | Bug Tracker = https://github.com/iTeam-S/ampalibe/issues 12 | classifiers = 13 | Programming Language :: Python :: 3 14 | Programming Language :: Python :: 3.7 15 | Programming Language :: Python :: 3.8 16 | Programming Language :: Python :: 3.9 17 | Programming Language :: Python :: 3.10 18 | Programming Language :: Python :: 3.11 19 | License :: OSI Approved :: MIT License 20 | Operating System :: OS Independent 21 | 22 | [options] 23 | 24 | packages = find: 25 | python_requires = >=3.7 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /.github/workflows/cd-pypi.yml: -------------------------------------------------------------------------------- 1 | name: CD PYPI 2 | on: 3 | release: 4 | types: 5 | - created 6 | jobs: 7 | publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 11 | - uses: actions/checkout@v2 12 | 13 | # Sets up python 14 | - uses: actions/setup-python@v2 15 | with: 16 | python-version: 3.8 17 | 18 | # Install dependencies 19 | - name: "Installs dependencies" 20 | run: | 21 | python3 -m pip install --upgrade pip 22 | python3 -m pip install setuptools wheel twine 23 | # Build and upload to PyPI 24 | - name: "Builds and uploads to PyPI" 25 | run: | 26 | python3 setup.py sdist bdist_wheel 27 | python3 -m twine upload dist/* 28 | env: 29 | TWINE_USERNAME: __token__ 30 | TWINE_PASSWORD: ${{ secrets.PIP_TOKEN }} 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 iTeam-$ 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_cli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | verif_content(){ 5 | for file in core.py .env conf.py langs.json .gitignore requirements.txt models.py resources.py 6 | do 7 | test -f $1/$file || { echo "$file not found" ; exit 1; } 8 | done 9 | 10 | for dir in public private 11 | do 12 | test -d $1/assets/$dir || { echo "assets/$dir not found" ; exit 1; } 13 | done 14 | 15 | test -d $1/templates || { echo "templates not found" ; exit 1; } 16 | } 17 | 18 | #### TEST AMPALIBE CREATE ####### 19 | ampalibe create test_proj > /dev/null 20 | verif_content test_proj 21 | rm -r test_proj 22 | 23 | #### TEST AMPALIBE INIT ####### 24 | mkdir test_proj && cd test_proj 25 | ampalibe init > /dev/null 26 | verif_content . 27 | 28 | rm langs.json .env 29 | #### TEST AMPALIBE LANG ##### 30 | ampalibe lang > /dev/null 31 | test -f langs.json || { echo "ampalibe lang not working" ; exit 1; } 32 | 33 | #### TEST AMPALIBE ENV ##### 34 | ampalibe env > /dev/null 35 | test -f .env || { echo "ampalibe env not working" ; exit 1; } 36 | 37 | cd .. && rm -r test_proj 38 | 39 | #### TEST AMPALIBE VERSION 40 | ampalibe version > /dev/null || { echo "ampalibe version not working" ; exit 1; } -------------------------------------------------------------------------------- /.github/workflows/ci-python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "ci" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v3 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | - name: Install dependencies 28 | run: | 29 | python -m pip install --upgrade pip 30 | python -m pip install flake8 31 | if [ -f requirements.txt ]; then pip install -r requirements.txt; fi 32 | - name: Lint with flake8 33 | run: | 34 | # stop the build if there are Python syntax errors or undefined names 35 | flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics 36 | -------------------------------------------------------------------------------- /bin/ampalibe.bat: -------------------------------------------------------------------------------- 1 | @ echo off 2 | 3 | IF /I "%1" == "env" ( 4 | python -m ampalibe env 5 | exit /B 6 | ) 7 | 8 | IF /I "%1" == "lang" ( 9 | python -m ampalibe lang 10 | exit /B 11 | ) 12 | 13 | IF /I "%1" == "create" ( 14 | IF exist "%2" ( 15 | echo ERROR !! %2 already exists 1>&2 16 | exit /B 17 | ) 18 | python -m ampalibe create %2 19 | exit /B 20 | ) 21 | IF /I "%1" == "init" ( 22 | python -m ampalibe init 23 | exit /B 24 | ) 25 | IF /I "%1" == "run" ( 26 | 27 | IF NOT exist "core.py" ( 28 | echo ERROR !! core.py not found 1>&2 29 | echo Please, go to your dir project. 30 | exit /B 31 | ) 32 | 33 | IF NOT exist "conf.py" ( 34 | echo ERROR !! conf.py not found 1>&2 35 | exit /B 36 | ) 37 | 38 | call .env.bat 39 | python -m ampalibe run 40 | IF /I "%2" == "--dev" ( 41 | watchmedo auto-restart --patterns="*.py;langs.json" --recursive -- python -c "import core;core.ampalibe.init.run()" 42 | exit /B 43 | ) 44 | python -c "import core;core.ampalibe.init.run()" 45 | exit /B 46 | ) 47 | 48 | IF /I "%1" == "version" ( 49 | python -m ampalibe version 50 | exit /B 51 | ) 52 | 53 | python -m ampalibe usage -------------------------------------------------------------------------------- /ampalibe/decorators.py: -------------------------------------------------------------------------------- 1 | from .tools import funcs 2 | 3 | 4 | def command(*args, **kwargs): 5 | """ 6 | A decorator that registers the function as the route 7 | of a processing per command sent. 8 | """ 9 | 10 | def call_fn(function): 11 | funcs["command"][args[0]] = function 12 | 13 | return call_fn 14 | 15 | 16 | def action(*args, **kwargs): 17 | """ 18 | A decorator that registers the function as the route 19 | of a defined action handler. 20 | """ 21 | 22 | def call_fn(function): 23 | funcs["action"][args[0]] = function 24 | 25 | return call_fn 26 | 27 | 28 | def event(*args, **kwargs): 29 | """ 30 | A decorator that registers the function as the route 31 | of a defined event handler. 32 | """ 33 | 34 | def call_fn(function): 35 | funcs["event"][args[0]] = function 36 | 37 | return call_fn 38 | 39 | 40 | def before_receive(*args, **kwargs): 41 | """ 42 | A decorator that run the function before 43 | running apropriate function 44 | """ 45 | 46 | def call_fn(function): 47 | funcs["before"] = function 48 | 49 | return call_fn 50 | 51 | 52 | def after_receive(*args, **kwargs): 53 | """ 54 | A decorator that run the function after 55 | running apropriate function 56 | """ 57 | 58 | def call_fn(function): 59 | funcs["after"] = function 60 | 61 | return call_fn 62 | -------------------------------------------------------------------------------- /ampalibe/logger.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import logging 3 | 4 | 5 | class CustomFormatter(logging.Formatter): 6 | """ 7 | Custom formatter for logging, add colors to the output and custom format 8 | """ 9 | 10 | grey = "\x1b[38;20m" 11 | yellow = "\x1b[33;20m" 12 | red = "\x1b[31;20m" 13 | green = "\x1b[32;20m" 14 | bold_red = "\x1b[31;1m" 15 | reset = "\x1b[0m" 16 | _format = "%(levelname)s|%(name)s: %(message)s " 17 | 18 | # Define format of each status (DEBUG|INFO|WARNINGS|ERROR|CRITICAL) 19 | FORMATS = { 20 | logging.DEBUG: grey + _format + reset, 21 | logging.INFO: green + _format + reset, 22 | logging.WARNING: yellow + _format + reset, 23 | logging.ERROR: red + _format + reset, 24 | logging.CRITICAL: bold_red + _format + reset, 25 | } 26 | 27 | def format(self, record): 28 | log_fmt = self.FORMATS.get(record.levelno) 29 | formatter = logging.Formatter(log_fmt) 30 | return formatter.format(record) 31 | 32 | 33 | def __logger(name="Ampalibe"): 34 | 35 | log = logging.getLogger(name) 36 | log.setLevel(logging.DEBUG) 37 | # by default the logger will print on the console , in stdout 38 | handler = logging.StreamHandler(sys.stdout) 39 | handler.setLevel(logging.DEBUG) 40 | handler.setFormatter(CustomFormatter()) 41 | log.addHandler(handler) 42 | return log 43 | 44 | 45 | Logger = __logger() 46 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | import os 3 | import sys 4 | 5 | sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(".")))) 6 | 7 | 8 | current_path = os.path.dirname(os.path.abspath(__file__)) 9 | try: 10 | sys.path.remove(current_path) 11 | import ampalibe 12 | except ValueError: 13 | import ampalibe 14 | else: 15 | sys.path.insert(0, current_path) 16 | 17 | 18 | # -- Project information 19 | 20 | project = "Ampalibe" 21 | copyright = "2021, iTeam-$" 22 | author = "iTeam-$" 23 | 24 | release = ampalibe.__version__ 25 | version = ampalibe.__version__ 26 | 27 | # Html cinfiguration 28 | html_theme = "sphinx_rtd_theme" 29 | html_static_path = ["_static"] 30 | html_logo = "_static/ampalibe_logo.png" 31 | html_favicon = "_static/ampalibe_logo.png" 32 | html_theme_options = { 33 | "display_version": False, 34 | "style_nav_header_background": "#106262", 35 | } 36 | # -- General configuration 37 | 38 | extensions = [ 39 | "sphinx.ext.duration", 40 | "sphinx.ext.doctest", 41 | "sphinx.ext.autodoc", 42 | "sphinx.ext.autosummary", 43 | "sphinx.ext.intersphinx", 44 | ] 45 | 46 | intersphinx_mapping = { 47 | "python": ("https://docs.python.org/3/", None), 48 | "sphinx": ("https://www.sphinx-doc.org/en/master/", None), 49 | } 50 | intersphinx_disabled_domains = ["std"] 51 | 52 | templates_path = ["_templates"] 53 | 54 | # -- Options for HTML output 55 | 56 | html_theme = "sphinx_rtd_theme" 57 | 58 | # -- Options for EPUB output 59 | epub_show_urls = "footnote" 60 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="ampalibe", # This is the name of the package 8 | version="2.0.0.dev", # The release version 9 | author="iTeam-$", # Full name of the author 10 | description=( 11 | "Ampalibe is a lightweight Python framework for building Facebook" 12 | " Messenger bots faster." 13 | ), 14 | long_description=long_description, # Long description read from the readme 15 | long_description_content_type="text/markdown", 16 | packages=["ampalibe"], # List of all modules to be installed 17 | classifiers=[ 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.7", 20 | "Programming Language :: Python :: 3.8", 21 | "Programming Language :: Python :: 3.9", 22 | "Programming Language :: Python :: 3.10", 23 | "Programming Language :: Python :: 3.11", 24 | "License :: OSI Approved :: MIT License", 25 | "Operating System :: OS Independent", 26 | ], # Information to filter the project on PyPi website 27 | python_requires=">=3.7", # Name of the python package 28 | install_requires=[ 29 | "fastapi", 30 | "uvicorn", 31 | "retry", 32 | "requests", 33 | "colorama", 34 | "requests_toolbelt", 35 | "watchdog!=2.2.0", 36 | "aiocron", 37 | "tinydb", 38 | "httpx" 39 | ], # depandance 40 | include_package_data=True, # Include all data file with the package 41 | scripts=["bin/ampalibe", "bin/ampalibe.bat"], 42 | ) 43 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function simulate { 4 | echo $(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/?testmode=1" -d "{\"object\": \"page\", \"entry\": [{\"messaging\": [ {\"sender\": {\"id\": \"test_user\"}, \"message\": {\"text\":$1} }]}]}") 5 | } 6 | 7 | function no_simulate { 8 | echo $(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/" -d "{\"object\": \"page\", \"entry\": [{\"messaging\": [ {\"sender\": {\"id\": \"test_user\"}, \"message\": {\"text\":$1} }]}]}") 9 | 10 | } 11 | 12 | ####### PAYLOAD TEST ####### 13 | payload0=`simulate '"/set_my_name"'` 14 | myname=$(simulate $payload0) 15 | if [ "$myname" = '"Ampalibe"' ] 16 | then 17 | echo "Message: OK Payload" 18 | else 19 | echo KO 20 | exit 1 21 | fi 22 | 23 | ####### ACTION TEST && Temporary data TEST ####### 24 | simulate '"/try_action"' > /dev/null 25 | res=$(simulate '"Hello"') 26 | if [ "$res" = '"Hello Ampalibe"' ]; then 27 | echo "Message: OK Action" 28 | echo "Message: OK Temporary data" 29 | else 30 | echo KO 31 | exit 1 32 | fi 33 | 34 | ####### ACTION TEST && Payload data TEST ####### 35 | simulate '"/try_second_action"' > /dev/null 36 | res=$(simulate '"Hello"') 37 | if [ "$res" = '"Hello Ampalibe2"' ]; then 38 | echo "Message: OK Second Action" 39 | echo "Message: OK Payload data in action" 40 | else 41 | echo KO 42 | exit 1 43 | fi 44 | 45 | 46 | ### utils TEST: translate, simulate, download_file #### 47 | res0=$(simulate '"/lang"') 48 | no_simulate $res0 > /dev/null 49 | sleep 2 50 | 51 | res=$(simulate '"/lang/test"') 52 | if [ "$res" = '"Hello World"' ]; then 53 | echo "Message: OK translate - simulate - download_file" 54 | else 55 | echo KO 56 | exit 1 57 | fi 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ampalibe/constant.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | AMPALIBE_LOGO = "https://raw.githubusercontent.com/iTeam-S/Ampalibe/main/docs/source/_static/ampalibe_logo.png" 4 | 5 | 6 | class Content_type: 7 | text = "text" 8 | user_phone_number = "user_phone_number" 9 | user_email = "user_email" 10 | 11 | 12 | class Type: 13 | postback = "postback" 14 | web_url = "web_url" 15 | phone_number = "phone_number" 16 | account_link = "account_link" 17 | account_unlink = "account_unlink" 18 | 19 | 20 | class Notification_frequency(Enum): 21 | """ 22 | Message frequency for the subscription message. 23 | """ 24 | 25 | DAILY = "DAILY" 26 | WEEKLY = "WEEKLY" 27 | MONTHLY = "MONTHLY" 28 | 29 | 30 | class Notification_cta_text(Enum): 31 | """ 32 | Call to action text for the subscription message. 33 | """ 34 | 35 | ALLOW = "ALLOW" 36 | FREQUENCY = "FREQUENCY" 37 | GET = "GET" 38 | OPT_IN = "OPT_IN" 39 | SIGN_UP = "SIGN_UP" 40 | 41 | 42 | class Notification_reoptin(Enum): 43 | """ 44 | Reoptin action for the subscription message. 45 | """ 46 | 47 | ENABLED = "ENABLED" 48 | DISABLED = "DISABLED" 49 | 50 | 51 | class Action: 52 | mark_seen = "mark_seen" 53 | typing_on = "typing_on" 54 | typing_off = "typing_off" 55 | 56 | 57 | class Filetype: 58 | file = "file" 59 | video = "video" 60 | image = "image" 61 | audio = "audio" 62 | 63 | 64 | class Messaging_type: 65 | MESSAGE_TAG = "MESSAGE_TAG" 66 | RESPONSE = "RESPONSE" 67 | UPDATE = "UPDATE" 68 | 69 | 70 | class Tag: 71 | ACCOUNT_UPDATE = "ACCOUNT_UPDATE" 72 | CONFIRMED_EVENT_UPDATE = "CONFIRMED_EVENT_UPDATE" 73 | CUSTOMER_FEEDBACK = "CUSTOMER_FEEDBACK" 74 | HUMAN_AGENT = "HUMAN_AGENT" 75 | POST_PURCHASE_UPDATE = "POST_PURCHASE_UPDATE" 76 | 77 | 78 | class Notification_type: 79 | NO_PUSH = "NO_PUSH" 80 | REGULAR = "REGULAR" 81 | SILENT_PUSH = "SILENT_PUSH" 82 | -------------------------------------------------------------------------------- /ampalibe/payload.py: -------------------------------------------------------------------------------- 1 | import codecs 2 | import pickle 3 | import urllib.parse 4 | from .cmd import Cmd 5 | from conf import Configuration # type: ignore 6 | from .crypt import encode, decode 7 | 8 | 9 | class Payload: 10 | """ 11 | Object for Payload Management 12 | """ 13 | 14 | def __init__(self, payload, **kwargs) -> None: 15 | """ 16 | Object for Payload Management 17 | """ 18 | self.payload = payload 19 | self.data = kwargs 20 | 21 | def __str__(self): 22 | return Payload.trt_payload_out(self) 23 | 24 | @staticmethod 25 | def trt_payload_in(payload0): 26 | """ 27 | processing of payloads received in a sequence of structured parameters 28 | 29 | @params: payload [String] 30 | @return: payload [String] , structured parameters Dict 31 | """ 32 | 33 | payload = urllib.parse.unquote(payload0) 34 | 35 | if hasattr(Configuration, "PAYLOAD_SECRET") and Configuration.PAYLOAD_SECRET: 36 | payload = decode(payload, Configuration.PAYLOAD_SECRET) 37 | 38 | res = {} 39 | while "{{" in payload: 40 | start = payload.index("{{") 41 | end = payload.index("}}") 42 | items = payload[start + 2 : end].split("===") 43 | # result string to object 44 | res[items[0]] = pickle.loads(codecs.decode(items[1].encode(), "base64")) 45 | payload = payload.replace(payload[start : end + 2], "").strip() 46 | return ( 47 | payload0.copy(payload) if isinstance(payload0, Cmd) else payload, 48 | res, 49 | ) 50 | 51 | @staticmethod 52 | def trt_payload_out(payload): 53 | """ 54 | Processing of a Payload type as a character string 55 | 56 | @params: payload [ Payload | String ] 57 | @return: String 58 | """ 59 | if isinstance(payload, Payload): 60 | tmp = "" 61 | for key_data, val_data in payload.data.items(): 62 | # object to string 63 | val_data = codecs.encode(pickle.dumps(val_data), "base64").decode() 64 | tmp += f"{{{{{key_data}==={val_data}}}}} " 65 | 66 | final_pl = payload.payload + (" " + tmp if tmp else "") 67 | if len(final_pl) >= 2000: 68 | raise Exception("Payload data is too large") 69 | payload = final_pl 70 | 71 | if hasattr(Configuration, "PAYLOAD_SECRET") and Configuration.PAYLOAD_SECRET: 72 | payload = encode(payload, Configuration.PAYLOAD_SECRET) 73 | return urllib.parse.quote(payload) 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | _build/ 74 | 75 | # PyBuilder 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | .python-version 87 | 88 | # pipenv 89 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 90 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 91 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 92 | # install all needed dependencies. 93 | #Pipfile.lock 94 | 95 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 96 | __pypackages__/ 97 | 98 | # Celery stuff 99 | celerybeat-schedule 100 | celerybeat.pid 101 | 102 | # SageMath parsed files 103 | *.sage.py 104 | 105 | # Environments 106 | .env 107 | .venv 108 | env/ 109 | venv/ 110 | ENV/ 111 | env.bak/ 112 | venv.bak/ 113 | 114 | # Spyder project settings 115 | .spyderproject 116 | .spyproject 117 | 118 | # Rope project settings 119 | .ropeproject 120 | 121 | # mkdocs documentation 122 | /site 123 | 124 | # mypy 125 | .mypy_cache/ 126 | .dmypy.json 127 | dmypy.json 128 | 129 | # Pyre type checker 130 | .pyre/ 131 | 132 | # all bot demo 133 | demo*/ 134 | 135 | _build/.env 136 | .env.bat 137 | __pycache__/ 138 | ngrok 139 | ngrok.exe 140 | ampalibe.db 141 | _db.json 142 | 143 | generated/ -------------------------------------------------------------------------------- /tests/app/core.py: -------------------------------------------------------------------------------- 1 | import ampalibe 2 | from conf import Configuration 3 | from ampalibe import Payload 4 | from ampalibe.utils import translate 5 | from ampalibe.utils import async_simulate as simulate 6 | from ampalibe.utils import async_download_file as download_file 7 | 8 | query = ampalibe.Model() 9 | 10 | 11 | @ampalibe.command("/") 12 | def main(sender_id, **extends): 13 | print(query.get_temp(sender_id, "token")) 14 | return "Hello Ampalibe" 15 | 16 | 17 | @ampalibe.command("/set_my_name") 18 | def set_my_name(**extends): 19 | res = Payload("/get_my_name", myname="Ampalibe") 20 | return Payload.trt_payload_out(res) 21 | 22 | 23 | @ampalibe.command("/get_my_name") 24 | def get_my_name(myname, **extends): 25 | """ 26 | Verify if payload parameter work here 27 | """ 28 | return myname 29 | 30 | 31 | @ampalibe.command("/try_action") 32 | def try_action(sender_id, **extends): 33 | query.set_action(sender_id, "/action_work") 34 | query.set_temp(sender_id, "myname", "Ampalibe") 35 | 36 | 37 | @ampalibe.action("/action_work") 38 | def action_work(sender_id, cmd, **extends): 39 | query.set_action(sender_id, None) 40 | myname = query.get_temp(sender_id, "myname") 41 | query.del_temp(sender_id, "myname") 42 | if query.get_temp(sender_id, "myname") != myname: 43 | return cmd + " " + myname 44 | return "Del temp error" 45 | 46 | 47 | @ampalibe.command("/try_second_action") 48 | def try_second_action(sender_id, **extends): 49 | query.set_action( 50 | sender_id, Payload("/second_action_work", myname="Ampalibe", version=2) 51 | ) 52 | 53 | 54 | @ampalibe.action("/second_action_work") 55 | def second_action_work(sender_id, cmd, myname, version, **extends): 56 | query.set_action(sender_id, None) 57 | return cmd + " " + myname + str(version) 58 | 59 | 60 | @ampalibe.command("/receive_optin_webhook") 61 | def receive_optin_webhook(**extends): 62 | return "Optin" 63 | 64 | 65 | @ampalibe.command("/lang") 66 | async def lang(sender_id, value=None, **extends): 67 | if value: 68 | query.set_lang(sender_id, value) 69 | await simulate(sender_id, "/lang/download") 70 | return Payload.trt_payload_out(Payload("/lang", value="en")) 71 | 72 | 73 | @ampalibe.command("/lang/download") 74 | async def lang_download(lang, **extends): 75 | await download_file( 76 | f"http://127.0.0.1:{Configuration.APP_PORT}/asset/hello.txt", 77 | "assets/private/hello.txt", 78 | ) 79 | 80 | 81 | @ampalibe.command("/lang/test") 82 | def lang_test(lang, **extends): 83 | with open("assets/private/hello.txt", "r") as f: 84 | text = f.read().strip() 85 | return translate(text, lang) 86 | -------------------------------------------------------------------------------- /bin/ampalibe: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # fonction affichant un output d'aide au cas de mauvaise utilisation de la commande 3 | usage() { 4 | python3 -m ampalibe usage 5 | exit 1 6 | } 7 | 8 | which python3 > /dev/null 9 | if [ ! $? -eq 0 ]; then 10 | which python > /dev/null 11 | if [ ! $? -eq 0 ]; then 12 | echo -e "~\033[31m Oh, Give up !! \033[0m\nNo python/python3 in path" 13 | exit 1 14 | fi 15 | shopt -s expand_aliases 16 | alias python3='python' 17 | fi 18 | 19 | 20 | 21 | # Analyse des parametres entrée avec la commande 22 | while getopts "p:" option; do 23 | case "${option}" in 24 | p) 25 | port=${OPTARG} 26 | 27 | regex_number='^[0-9]+$' 28 | if ! [[ $port =~ $regex_number ]] ; then 29 | echo "Not a number $port" 30 | usage 31 | fi 32 | ;; 33 | *) 34 | usage 35 | ;; 36 | esac 37 | done 38 | shift $((OPTIND-1)) 39 | 40 | if [ "$1" == "version" ]; then 41 | python3 -m ampalibe version 42 | 43 | elif [ "$1" == "env" ]; then 44 | if ! [ -f "core.py" ]; then 45 | >&2 echo -e "~ \033[31mERROR !! \033[0m | core.py not found\n~\033[36m TIPS 👌\033[0m ~\033[0m Please, go to your dir project."; 46 | exit 1 47 | fi 48 | python3 -m ampalibe env 49 | 50 | 51 | elif [ "$1" == "lang" ]; then 52 | if ! [ -f "core.py" ]; then 53 | >&2 echo -e "~ \033[31mERROR !! \033[0m | core.py not found\n~\033[36m TIPS 👌\033[0m ~\033[0m Please, go to your dir project."; 54 | exit 1 55 | fi 56 | python3 -m ampalibe lang 57 | 58 | elif [ "$1" == "create" ]; then 59 | if [ $# -eq 2 ]; then 60 | if [ -d "$2" ]; then 61 | >&2 echo -e "~\033[31m ERROR !!\033[0m ~ A folder $2 already exists" 62 | exit 1 63 | fi 64 | python3 -m ampalibe create $2 65 | else 66 | echo -e "~\033[31m ERROR !!\033[0m | Incorrect number of args for create" 67 | usage 68 | exit 1 69 | fi 70 | elif [ "$1" == "init" ]; then 71 | if [ $# -eq 1 ]; then 72 | python3 -m ampalibe init 73 | else 74 | >&2 echo -e "~\033[31m ERROR :(\033[0m | Incorrect number of args for init" 75 | exit 1 76 | 77 | fi 78 | elif [ "$1" == "run" ]; then 79 | if ! [ -f "core.py" ]; then 80 | >&2 echo -e "~ \033[31mERROR !! \033[0m | core.py not found\n~\033[36m TIPS 👌\033[0m ~\033[0m Please, go to your dir project."; 81 | exit 1 82 | fi 83 | if ! [ -f "conf.py" ]; then 84 | >&2 echo -e "~ \033[31mERROR !! \033[0m | conf.py not found"; 85 | exit 1 86 | fi 87 | 88 | source .env 89 | 90 | if [ ! -z "${port}" ] ; then 91 | export AMP_PORT=$port; 92 | fi 93 | 94 | python3 -m ampalibe run 95 | 96 | if [ "$2" = "--dev" ]; then 97 | python3 -m uvicorn core:ampalibe.webserver --host $AMP_HOST --port $AMP_PORT --reload 98 | exit 99 | elif [ "$2" = "--prod" ]; then 100 | args=("${@:3}") 101 | python3 -m uvicorn core:ampalibe.webserver --host $AMP_HOST --port $AMP_PORT ${args[@]} 102 | exit 103 | fi 104 | 105 | python3 -m uvicorn core:ampalibe.webserver --host $AMP_HOST --port $AMP_PORT 106 | 107 | else 108 | >&2 echo -e "~\033[31m ERROR !! \033[0m | Missing knowing argument" 109 | usage; 110 | fi 111 | -------------------------------------------------------------------------------- /ampalibe/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import json 4 | import httpx 5 | import requests 6 | from .payload import Payload 7 | from conf import Configuration # type: ignore 8 | 9 | 10 | LANGS = None 11 | 12 | 13 | def download_file(url, file): 14 | """ 15 | Downloading a file from an url. 16 | 17 | Args: 18 | url: direct link for the attachment 19 | 20 | file: filename with path 21 | """ 22 | res = requests.get(url, allow_redirects=True) 23 | 24 | with open(file, "wb") as f: 25 | f.write(res.content) 26 | 27 | return file 28 | 29 | 30 | def translate(key, lang): 31 | """ 32 | this function uses the langs.json file. 33 | translate a keyword or sentence 34 | """ 35 | global LANGS 36 | 37 | if not lang: 38 | return key 39 | 40 | if not os.path.isfile("langs.json"): 41 | print("Warning! langs.json not found", file=sys.stderr) 42 | from .source import langs 43 | 44 | with open("langs.json", "w", encoding="utf-8") as lang_file: 45 | lang_file.write(langs) 46 | print("langs.json created!") 47 | return key 48 | 49 | if not LANGS: 50 | with open("langs.json", encoding="utf-8") as lang_file: 51 | LANGS = json.load(lang_file) 52 | 53 | keyword = LANGS.get(key) 54 | 55 | if keyword: 56 | if keyword.get(lang): 57 | return keyword.get(lang) 58 | return key 59 | 60 | 61 | def simulate(sender_id, text, **params): 62 | """ 63 | Simulate a message send by an user 64 | 65 | Args: 66 | sender_id: 67 | text: | 68 | """ 69 | if isinstance(text, Payload): 70 | text = Payload.trt_payload_out(text) 71 | 72 | data_json = { 73 | "object": "page", 74 | "entry": [ 75 | { 76 | "messaging": [ 77 | { 78 | "message": { 79 | "text": text, 80 | }, 81 | "sender": {"id": sender_id}, 82 | } 83 | ] 84 | } 85 | ], 86 | } 87 | header = {"content-type": "application/json; charset=utf-8"} 88 | return requests.post( 89 | f"http://127.0.0.1:{Configuration.APP_PORT}/", 90 | json=data_json, 91 | headers=header, 92 | params=params, 93 | ) 94 | 95 | 96 | async def async_simulate(sender_id, text, **params): 97 | """ 98 | Simulate a message send by an user 99 | 100 | Args: 101 | sender_id: 102 | text: | 103 | """ 104 | if isinstance(text, Payload): 105 | text = Payload.trt_payload_out(text) 106 | 107 | data_json = { 108 | "object": "page", 109 | "entry": [ 110 | { 111 | "messaging": [ 112 | { 113 | "message": { 114 | "text": text, 115 | }, 116 | "sender": {"id": sender_id}, 117 | } 118 | ] 119 | } 120 | ], 121 | } 122 | headers = {"content-type": "application/json; charset=utf-8"} 123 | async with httpx.AsyncClient() as client: 124 | response = await client.post( 125 | f"http://127.0.0.1:{Configuration.APP_PORT}/", 126 | json=data_json, 127 | headers=headers, 128 | ) 129 | 130 | return response 131 | 132 | 133 | async def async_download_file(url, file): 134 | """ 135 | Downloading a file from an url. 136 | 137 | Args: 138 | url: direct link for the attachment 139 | 140 | file: filename with path 141 | """ 142 | async with httpx.AsyncClient() as client: 143 | response = await client.get(url, follow_redirects=True) 144 | 145 | with open(file, "wb") as f: 146 | f.write(response.content) 147 | 148 | return file 149 | -------------------------------------------------------------------------------- /docs/source/usage.rst: -------------------------------------------------------------------------------- 1 | Get Started 2 | ============ 3 | 4 | .. _installation: 5 | 6 | Installation 7 | ------------ 8 | 9 | To use Ampalibe, first install it using pip: 10 | 11 | .. code-block:: console 12 | 13 | $ pip install ampalibe 14 | 15 | if you use mysql as database, you have to install `mysql-connector` or `mysql-connector-python` with ampalibe 16 | 17 | .. code-block:: console 18 | 19 | $ pip install ampalibe[mysql-connector] 20 | 21 | OR 22 | 23 | .. code-block:: console 24 | 25 | $ pip install ampalibe[mysql-connector-python] 26 | 27 | 28 | if you use postgresql as database, you have to install `psycopg2` with ampalibe 29 | 30 | .. code-block:: console 31 | 32 | $ pip install ampalibe[psycopg2] 33 | 34 | 35 | if you use mongodb as database, you have to install `pymongo` with ampalibe 36 | 37 | .. code-block:: console 38 | 39 | $ pip install ampalibe[pymongo] 40 | 41 | 42 | Creation of a new project 43 | ------------------------- 44 | 45 | After installation, an ``ampalibe`` executable is available in your system path, 46 | and we will create our project with this command. 47 | 48 | .. important:: 49 | 50 | command-line ``ampalibe`` is ``ampalibe.bat`` for **Windows** 51 | 52 | 53 | .. code-block:: console 54 | 55 | $ ampalibe create myfirstbot 56 | 57 | There will be a created directory named **myfirstbot/** and all the files contained in it. 58 | 59 | .. note:: 60 | 61 | You can directly create project without a new directory with **init** command 62 | 63 | .. code-block:: console 64 | 65 | $ cd myExistingDirectory 66 | $ ampalibe init 67 | 68 | 69 | Understanding of files 70 | ------------------------- 71 | 72 | .. image:: https://github.com/iTeam-S/Ampalibe/raw/main/docs/source/_static/structure.png 73 | 74 | .. hlist:: 75 | :columns: 1 76 | 77 | * ``assets/`` statics file folder 78 | * ``public/`` reachable via url 79 | * ``private/`` not accessible via url 80 | 81 | * ``.env`` environment variable file 82 | 83 | * ``conf.py`` configuration file that retrieves environment variables 84 | 85 | * ``core.py`` file containing the starting point of the code 86 | 87 | * ``langs.json`` file containing translated strings 88 | 89 | 90 | .. important:: 91 | 92 | .env file is env.bat in Windows 93 | 94 | 95 | Before starting 96 | ----------------- 97 | 98 | How to complete the environment variable file 99 | 100 | .. hlist:: 101 | :columns: 1 102 | 103 | * **AMP_ACCESS_TOKEN** Facebook Page access token 104 | * **AMP_VERIF_TOKEN** Token that Facebook use as part of the recall URL check. 105 | * **ADAPTER** type of database used by ampalibe (SQLITE OR MYSQL OR POSTGRESQL) 106 | * **FOR MYSQL ADAPTER OR POSTGRESQL** 107 | * *DB_HOST** 108 | * *DB_USER* 109 | * *DB_PASSWORD* 110 | * *DB_NAME* 111 | * *DB_PORT* 112 | * **FOR SQLITE ADAPTER** 113 | * *DB_FILE* 114 | * **AMP_HOST** server listening address 115 | * **AMP_PORT** server listening port 116 | * **AMP_URL** URL of the server given to Facebook 117 | 118 | 119 | 120 | 121 | Run the app 122 | ----------------- 123 | 124 | In the project folder, type 125 | 126 | .. code-block:: console 127 | 128 | $ ampalibe run 129 | 130 | 131 | for dev mode with **Hot Reload** 132 | 133 | .. code-block:: console 134 | 135 | $ ampalibe run --dev 136 | 137 | :: 138 | 139 | INFO: Started server process [26753] 140 | INFO: Waiting for application startup. 141 | INFO: Application startup complete. 142 | INFO: Uvicorn running on http://0.0.0.0:4555 (Press CTRL+C to quit) 143 | 144 | .. note:: 145 | 146 | Ampalibe use uvicorn to run server, so it is an output of uvicorn 147 | 148 | 149 | You will need to configure a Facebook application, a Facebook page, get the access to the page, link the application to the page, configure a webhook for your app before you can really start using Ampalibe. 150 | 151 | 152 | `This app setup guide `_ should help 153 | 154 | OR 155 | 156 | See `this video `_ on Youtube -------------------------------------------------------------------------------- /ampalibe/core.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | import asyncio 4 | from .model import Model 5 | from .logger import Logger 6 | from threading import Thread 7 | from .payload import Payload 8 | from conf import Configuration # type: ignore 9 | from fastapi.staticfiles import StaticFiles 10 | from fastapi import FastAPI, Request, Response 11 | from .tools import funcs, analyse, before_run, send_next, verif_event 12 | 13 | 14 | _req = Model(init=False) 15 | loop = asyncio.get_event_loop() 16 | 17 | webserver = FastAPI() 18 | if not os.path.isdir("assets/public"): 19 | os.makedirs("assets/public", exist_ok=True) 20 | 21 | webserver.mount("/asset", StaticFiles(directory="assets/public"), name="asset") 22 | if hasattr(Configuration, "ADMIN_ENABLE") and Configuration.ADMIN_ENABLE: 23 | try: 24 | import sqlmodel 25 | import sqladmin 26 | except ModuleNotFoundError: 27 | raise Exception( 28 | "You must install sqlmodel and sqladmin to use admin panel \npip install sqlmodel sqladmin" 29 | ) 30 | else: 31 | from .admin import init_admin 32 | 33 | init_admin(webserver) 34 | 35 | 36 | class Server(Request): 37 | """ 38 | Content of webhook 39 | """ 40 | 41 | @webserver.on_event("startup") 42 | def startup(): 43 | _req._start() 44 | if not loop.is_running(): 45 | Thread(target=loop.run_forever).start() 46 | 47 | @webserver.on_event("shutdown") 48 | def shutdow(): 49 | """ 50 | function that shutdown crontab server 51 | """ 52 | loop.call_soon_threadsafe(loop.stop) 53 | 54 | @webserver.get("/") 55 | async def verif(request: Request): 56 | """ 57 | Main verification for bot server is received here 58 | """ 59 | 60 | if request.query_params.get("hub.verify_token") == Configuration.VERIF_TOKEN: 61 | return Response(content=request.query_params["hub.challenge"]) 62 | return "Failed to verify token" 63 | 64 | @webserver.post("/") 65 | async def main(request: Request): 66 | """ 67 | Main Requests for bot messenger is received here. 68 | """ 69 | testmode = request.query_params.get("testmode") 70 | try: 71 | data = await request.json() 72 | except json.decoder.JSONDecodeError: 73 | return "..." 74 | 75 | # data analysis and decomposition 76 | sender_id, payload, message = analyse(data) 77 | 78 | if payload.webhook in ("read", "delivery", "reaction"): 79 | await verif_event(sender_id, payload, message, testmode) 80 | return {"status": "ok"} 81 | 82 | _req._verif_user(sender_id) 83 | action, lang = _req.get(sender_id, "action", "lang") 84 | 85 | if payload in ("/__next", "/__more"): 86 | send_next(sender_id, payload) 87 | return {"status": "ok"} 88 | 89 | if os.path.isfile(f"assets/private/.__{sender_id}"): 90 | os.remove(f"assets/private/.__{sender_id}") 91 | 92 | payload, kw = Payload.trt_payload_in(payload) 93 | kw.update( 94 | { 95 | "sender_id": sender_id, 96 | "cmd": payload, 97 | "message": message, 98 | "lang": lang, 99 | } 100 | ) 101 | 102 | command = funcs["command"].get(payload.split()[0]) 103 | 104 | if command: 105 | _req.set_action(sender_id, None) 106 | if testmode: 107 | return await before_run(command, **kw) 108 | asyncio.create_task(before_run(command, **kw)) 109 | elif action: 110 | action, kw_tmp = Payload.trt_payload_in(action) 111 | kw.update(kw_tmp) 112 | 113 | if funcs["action"].get(action): 114 | if testmode: 115 | return await before_run(funcs["action"].get(action), **kw) 116 | asyncio.create_task(before_run(funcs["action"].get(action), **kw)) 117 | else: 118 | Logger.error(f'⚠ Error! action "{action}" undeclared ') 119 | else: 120 | command = funcs["command"].get("/") 121 | if command: 122 | if testmode: 123 | return await before_run(command, **kw) 124 | asyncio.create_task(before_run(command, **kw)) 125 | else: 126 | Logger.error("⚠ Error! Default route '/' function undeclared.") 127 | return {"status": "ok"} 128 | -------------------------------------------------------------------------------- /ampalibe/admin.py: -------------------------------------------------------------------------------- 1 | import inspect 2 | import hashlib 3 | from .logger import Logger 4 | from typing import Optional 5 | from datetime import datetime 6 | from .model import DataBaseConfig 7 | from .constant import AMPALIBE_LOGO 8 | from starlette.requests import Request 9 | from sqladmin import Admin, ModelView, BaseView 10 | from sqladmin.authentication import AuthenticationBackend 11 | from sqlmodel import select, create_engine, Field, Session, SQLModel 12 | 13 | 14 | engine = create_engine( 15 | DataBaseConfig().get_db_url() 16 | ) 17 | 18 | 19 | class AdminModel(SQLModel, table=True): 20 | __tablename__ = "admin_user" 21 | id: Optional[int] = Field(default=None, primary_key=True) 22 | username: str = Field(max_length=50, unique=True, nullable=False) 23 | password: str = None 24 | created_at: datetime = Field(default=datetime.utcnow(), nullable=False) 25 | last_edited: datetime = Field(default_factory=datetime.utcnow, nullable=False) 26 | 27 | 28 | class AdminUser(ModelView, model=AdminModel): 29 | column_list = [ 30 | AdminModel.id, 31 | AdminModel.username, 32 | AdminModel.password, 33 | AdminModel.last_edited, 34 | AdminModel.created_at, 35 | ] 36 | icon = "fa-solid fa-user-secret" 37 | can_export = False 38 | 39 | def is_visible(self, request: Request) -> bool: 40 | return True 41 | 42 | def is_accessible(self, request: Request) -> bool: 43 | return True 44 | 45 | async def after_model_change(self, data, model, is_created): 46 | with Session(engine) as session: 47 | statement = select(AdminModel).where(AdminModel.id == model.id) 48 | results = session.exec(statement) 49 | admin_model = results.one() 50 | admin_model.username = data.get("username") 51 | admin_model.password = hashlib.sha256( 52 | data.get("password").encode() 53 | ).hexdigest() 54 | session.add(admin_model) 55 | session.commit() 56 | session.refresh(admin_model) 57 | 58 | 59 | class AdminAuth(AuthenticationBackend): 60 | async def login(self, request: Request) -> bool: 61 | form = await request.form() 62 | username, password = form["username"], form["password"] 63 | 64 | with Session(engine) as session: 65 | statement = ( 66 | select(AdminModel) 67 | .where(AdminModel.username == username) 68 | .where( 69 | AdminModel.password == hashlib.sha256(password.encode()).hexdigest() 70 | ) 71 | ) 72 | results = session.exec(statement) 73 | verif = len(results.all()) > 0 74 | 75 | if verif: 76 | request.session.update({"token": "..."}) 77 | return verif 78 | 79 | async def logout(self, request: Request) -> bool: 80 | request.session.clear() 81 | return True 82 | 83 | async def authenticate(self, request: Request) -> bool: 84 | token = request.session.get("token") 85 | if not token: 86 | with Session(engine) as session: 87 | statement = select(AdminModel) 88 | results = session.exec(statement) 89 | if len(results.all()) == 0: 90 | session.add( 91 | AdminModel( 92 | username="admin", 93 | password=hashlib.sha256(b"ampalibe").hexdigest(), 94 | ) 95 | ) 96 | session.commit() 97 | return False 98 | return True 99 | 100 | 101 | def init_admin(app): 102 | views = get_user_resources() 103 | SQLModel.metadata.create_all(engine) 104 | admin = Admin( 105 | app, 106 | engine, 107 | title="Ampalibe Admin", 108 | logo_url=AMPALIBE_LOGO, 109 | authentication_backend=AdminAuth(secret_key="..."), 110 | ) 111 | for view in views: 112 | admin.add_view(view) 113 | 114 | 115 | def get_user_resources(): 116 | allViews = [AdminUser] 117 | try: 118 | import resources # type: ignore 119 | 120 | clsmembers = inspect.getmembers(resources, inspect.isclass) 121 | for name, obj in clsmembers: 122 | if name in ("ModelView", "BaseView"): 123 | continue 124 | if issubclass(obj, (ModelView, BaseView)): 125 | allViews.append(obj) 126 | 127 | # Sort by their sequence attributes if they have one 128 | allViews = sorted( 129 | allViews, 130 | key=lambda view: view.sequence if hasattr(view, 'sequence') else float('inf') 131 | ) 132 | 133 | except TypeError as err: 134 | Logger.warning(f"Error while loading resources ,make sure that the sequence attribute is a number: {err}") 135 | 136 | except Exception as err: 137 | Logger.warning(f"Error while loading resources : {err}") 138 | 139 | finally: 140 | return allViews 141 | -------------------------------------------------------------------------------- /ampalibe/tools.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import inspect 4 | import asyncio 5 | from .cmd import Cmd 6 | from .messenger import Messenger 7 | 8 | funcs = { 9 | "command": {}, 10 | "action": {}, 11 | "event": {}, 12 | "before": None, 13 | "after": None, 14 | } 15 | 16 | 17 | async def verif_func(func, **kwargs): 18 | """ 19 | function verify if the function is a coroutine or not before running it 20 | """ 21 | return await func(**kwargs) if inspect.iscoroutinefunction(func) else func(**kwargs) 22 | 23 | 24 | def analyse(data): 25 | """ 26 | Function analyzing data received from Facebook 27 | The data received are of type Json . 28 | """ 29 | 30 | for event in data["entry"]: 31 | messaging = event["messaging"] 32 | 33 | for message in messaging: 34 | 35 | sender_id = message["sender"]["id"] 36 | 37 | if message.get("message"): 38 | 39 | if message["message"].get("attachments"): 40 | # Get file name 41 | data = message["message"].get("attachments") 42 | # creation de l'objet cmd personalisé 43 | atts = list(map(lambda dt: dt["payload"]["url"], data)) 44 | cmd = Cmd(atts[0]) 45 | cmd.set_atts(atts) 46 | cmd.webhook = "attachments" 47 | return sender_id, cmd, message 48 | elif message["message"].get("quick_reply"): 49 | # if the response is a quick reply 50 | return ( 51 | sender_id, 52 | Cmd(message["message"]["quick_reply"].get("payload")), 53 | message, 54 | ) 55 | elif message["message"].get("text"): 56 | # if the response is a simple text 57 | return ( 58 | sender_id, 59 | Cmd(message["message"].get("text")), 60 | message, 61 | ) 62 | 63 | if message.get("postback"): 64 | recipient_id = sender_id 65 | pst_payload = Cmd(message["postback"]["payload"]) 66 | pst_payload.webhook = "postback" 67 | return recipient_id, pst_payload, message 68 | 69 | if message.get("read"): 70 | watermark = Cmd(message["read"].get("watermark")) 71 | watermark.webhook = "read" 72 | return sender_id, watermark, message 73 | 74 | if message.get("delivery"): 75 | watermark = Cmd(message["delivery"].get("watermark")) 76 | watermark.webhook = "delivery" 77 | return sender_id, watermark, message 78 | 79 | if message.get("reaction"): 80 | reaction = Cmd(message["reaction"].get("reaction")) 81 | reaction.webhook = "reaction" 82 | return sender_id, reaction, message 83 | 84 | if message.get("optin"): 85 | optin = Cmd(message["optin"]["payload"]) 86 | optin.webhook = "optin" 87 | if message["optin"].get("type") == "one_time_notif_req": 88 | optin.token = message["optin"]["one_time_notif_token"] 89 | elif message["optin"].get("type") == "notification_messages": 90 | optin.token = message["optin"]["notification_messages_token"] 91 | return sender_id, optin, message 92 | 93 | return None, Cmd(""), None 94 | 95 | 96 | async def before_run(func, **kwargs): 97 | """ 98 | Function to be executed before running the function 99 | called by the user command 100 | """ 101 | 102 | res = None 103 | if funcs["before"] and hasattr(funcs["before"], "__call__"): 104 | if await verif_func(funcs["before"], **kwargs): 105 | res = await verif_func(func, **kwargs) 106 | else: 107 | res = await verif_func(func, **kwargs) 108 | 109 | if funcs["after"] and hasattr(funcs["after"], "__call__"): 110 | kwargs["res"] = res 111 | await verif_func(funcs["after"], **kwargs) 112 | 113 | return res 114 | 115 | 116 | def send_next(sender_id, payload): 117 | chat = Messenger() 118 | if os.path.isfile(f"assets/private/.__{sender_id}"): 119 | elements = pickle.load(open(f"assets/private/.__{sender_id}", "rb")) 120 | if payload == "/__next": 121 | chat.send_generic_template(sender_id, elements[0], next=elements[1]) 122 | else: 123 | chat.send_quick_reply(sender_id, elements[0], elements[1], next=elements[2]) 124 | 125 | 126 | async def verif_event(testmode, payload, sender_id, message): 127 | if funcs["event"].get(payload.webhook): 128 | kw = { 129 | "sender_id": sender_id, 130 | "watermark": payload, 131 | "message": message, 132 | } 133 | if testmode: 134 | await verif_func(funcs["event"][payload.webhook], **kw) 135 | else: 136 | asyncio.create_task(verif_func(funcs["event"][payload.webhook], **kw)) 137 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ "ci" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | jobs: 10 | 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | python -m pip install flake8 pytest 22 | 23 | - name: Lint with flake8 24 | run: | 25 | # stop the build if there are Python syntax errors or undefined names 26 | flake8 ampalibe --count --select=E9,F63,F7,F82 --show-source --statistics 27 | 28 | - name: Install ampalibe locally without container 29 | run: | 30 | pip install -r requirements.txt 31 | python setup.py install --force --user 32 | 33 | - name: Unit test for ampalibe CLI 34 | run: | 35 | bash tests/test_cli.sh 36 | 37 | - name: Unit test for messenger api 38 | run: SENDER_ID=${{secrets.USER_ID}} AMP_ACCESS_TOKEN=${{secrets.ACCESS_TOKEN}} pytest 39 | 40 | - name: Build the Docker image 41 | run: bash tests/deploy.sh 42 | 43 | - name: Test du serveur web 44 | run: | 45 | sudo lsof -i:4555 46 | if [ $? -eq 0 ]; then 47 | echo "Server Web Running" 48 | else 49 | echo "Server Web Not Runing" 50 | docker logs amp 51 | exit 1 52 | fi 53 | 54 | - name: Envoie du Challenge Webhook 55 | run: | 56 | response=$(curl -X GET "http://127.0.0.1:4555/?hub.verify_token=AMPALIBE&hub.challenge=CHALLENGE_ACCEPTED&hub.mode=subscribe") 57 | if [ $response = "CHALLENGE_ACCEPTED" ]; then 58 | echo "OK! : $response" 59 | else 60 | echo KO 61 | exit 1 62 | fi 63 | 64 | - name: "Test du reception de l'evenement text" 65 | run: | 66 | response=$(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/?testmode=1" -d '{"object": "page", "entry": [{"messaging": [ {"sender": {"id": "test_user"}, "message": {"text":"TEST_MESSAGE"} }]}]}') 67 | if [ "$response" = '"Hello Ampalibe"' ]; then 68 | echo "Message: OK" 69 | else 70 | echo KO 71 | exit 1 72 | fi 73 | 74 | - name: "Test du reception de l'evenement postback" 75 | run: | 76 | response=$(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/?testmode=1" -d '{"object": "page", "entry": [{"messaging": [ {"sender": {"id": "test_user"}, "postback": {"title": "TITLE-FOR-THE-CTA", "payload": "USER-DEFINED-PAYLOAD"} }]}]}') 77 | if [ "$response" = '"Hello Ampalibe"' ]; then 78 | echo "Postback: OK" 79 | else 80 | echo KO! 81 | exit 1 82 | fi 83 | 84 | - name: "Test du reception de l'evenement read" 85 | run: | 86 | response=$(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/?testmode=1" -d '{"object": "page", "entry": [{"messaging": [ {"sender": {"id": "test_user"}, "read":{"watermark":1458668856253} }]}]}') 87 | if [ "$response" = '{"status":"ok"}' ]; then 88 | echo "Read: OK" 89 | else 90 | echo KO! 91 | exit 1 92 | fi 93 | 94 | - name: "Test du reception de l'evenement delivery" 95 | run: | 96 | response=$(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/?testmode=1" -d '{"object": "page", "entry": [{"messaging": [ {"sender": {"id": "test_user"}, "delivery":{"watermark":1458668856253} }]}]}') 97 | if [ "$response" = '{"status":"ok"}' ]; then 98 | echo "Delivré: OK" 99 | else 100 | echo KO! 101 | exit 1 102 | fi 103 | 104 | - name: "Test du reception de l'evenement reaction" 105 | run: | 106 | response=$(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/?testmode=1" -d '{"object": "page", "entry": [{"messaging": [ {"sender": {"id": "test_user"}, "reaction":{"reaction": "love", "action": "react"} }]}]}') 107 | if [ "$response" = '{"status":"ok"}' ]; then 108 | echo "REACTION: OK" 109 | else 110 | echo KO! 111 | exit 1 112 | fi 113 | 114 | - name: "Test du reception d'un attachment" 115 | run: | 116 | response=$(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/?testmode=1" -d '{"object": "page", "entry": [{"messaging": [ {"sender": {"id": "test_user"}, "message": {"attachments":[{"payload":{"url": "https://i.imgflip.com/6b45bi.jpg"}}]} }]}]}') 117 | if [ "$response" = '"Hello Ampalibe"' ]; then 118 | echo "Message: OK" 119 | else 120 | echo KO 121 | exit 1 122 | fi 123 | 124 | - name: "Test du reception de l'evenement optin avec type one_time_notif_req" 125 | run: | 126 | response=$(curl -H "Content-Type: application/json" -X POST "http://127.0.0.1:4555/?testmode=1" -d '{"object": "page", "entry": [{"messaging": [ {"sender": {"id": "test_user"}, "optin":{"payload": "/receive_optin_webhook", "type": "one_time_notif_req", "one_time_notif_token": "EXAMPLE_DE_TOKEN"} }]}]}') 127 | if [ "$response" = '"Optin"' ]; then 128 | echo "Optin one_notif: OK" 129 | else 130 | echo KO! 131 | exit 1 132 | fi 133 | 134 | - name: "Run functionnal test" 135 | run: bash tests/test.sh 136 | 137 | -------------------------------------------------------------------------------- /tests/test_api_messenger.py: -------------------------------------------------------------------------------- 1 | from os import environ, makedirs 2 | from ampalibe import Messenger, Payload 3 | from ampalibe.messenger import Action, Filetype 4 | from ampalibe.ui import ( 5 | Button, 6 | Element, 7 | QuickReply, 8 | Type, 9 | ReceiptElement, 10 | Summary, 11 | Adjustment, 12 | Address, 13 | ) 14 | 15 | chat = Messenger() 16 | sender_id = environ.get("SENDER_ID") 17 | 18 | 19 | def test_send_action(): 20 | assert chat.send_action(sender_id, Action.mark_seen).status_code == 200 21 | 22 | 23 | def test_send_text(): 24 | assert chat.send_text(sender_id, "Hello world").status_code == 200 25 | 26 | 27 | def test_send_quick_reply(): 28 | makedirs("assets/private", exist_ok=True) 29 | quick_rep = [ 30 | QuickReply( 31 | title="Angela", 32 | payload="/membre", 33 | image_url="https://i.imgflip.com/6b45bi.jpg", 34 | ), 35 | QuickReply( 36 | title="Rivo", 37 | payload="/membre", 38 | image_url="https://i.imgflip.com/6b45bi.jpg", 39 | ), 40 | ] 41 | assert ( 42 | chat.send_quick_reply(sender_id, quick_rep, "who do you choose ?").status_code 43 | == 200 44 | ) 45 | 46 | quick_rep = [ 47 | QuickReply( 48 | title=f"response {i+1}", 49 | payload=Payload("/response", item=i + 1), 50 | image_url="https://i.imgflip.com/6b45bi.jpg", 51 | ) 52 | for i in range(20) 53 | ] 54 | assert ( 55 | chat.send_quick_reply( 56 | sender_id, quick_rep, "who do you choose ?", next="See More" 57 | ).status_code 58 | == 200 59 | ) 60 | 61 | 62 | def test_send_generic_template(): 63 | makedirs("assets/private", exist_ok=True) 64 | list_items = [] 65 | 66 | for i in range(30): 67 | buttons = [ 68 | Button( 69 | type=Type.postback, 70 | title="Get item", 71 | payload=Payload("/item", id_item=i + 1), 72 | ) 73 | ] 74 | 75 | list_items.append( 76 | Element( 77 | title="iTem", 78 | image_url="https://i.imgflip.com/6b45bi.jpg", 79 | buttons=buttons, 80 | ) 81 | ) 82 | 83 | assert ( 84 | chat.send_generic_template( 85 | sender_id, 86 | list_items, 87 | next=True, 88 | quick_rep=[ 89 | QuickReply(title="retour", payload="/return"), 90 | ], 91 | ).status_code 92 | == 200 93 | ) 94 | 95 | assert ( 96 | chat.send_generic_template(sender_id, list_items, next="Next page").status_code 97 | == 200 98 | ) 99 | 100 | 101 | def test_send_file_url(): 102 | assert ( 103 | chat.send_file_url( 104 | sender_id, "https://i.imgflip.com/6b45bi.jpg", filetype=Filetype.image 105 | ).status_code 106 | == 200 107 | ) 108 | 109 | 110 | def test_send_button(): 111 | buttons = [Button(title="Informations", payload="/contact")] 112 | 113 | assert ( 114 | chat.send_button(sender_id, buttons, "What do you want to do?").status_code 115 | == 200 116 | ) 117 | 118 | 119 | def test_persitent_menu(): 120 | persistent_menu = [ 121 | Button(type="postback", title="Menu", payload="/payload"), 122 | Button(type="postback", title="Logout", payload="/logout"), 123 | ] 124 | 125 | assert chat.persistent_menu(sender_id, persistent_menu).status_code == 200 126 | 127 | 128 | def test_send_receipt_template(): 129 | receipts = [ 130 | ReceiptElement(title="Tee-shirt", price=1000), 131 | ReceiptElement(title="Pants", price=2000), 132 | ] 133 | 134 | # create a summary 135 | summary = Summary(total_cost=300) 136 | 137 | # create an address 138 | address = Address( 139 | street_1="Street 1", 140 | city="City", 141 | state="State", 142 | postal_code="Postal Code", 143 | country="Country", 144 | ) 145 | 146 | # create an adjustment 147 | adjustment = Adjustment(name="Discount of 10%", amount=10) 148 | 149 | assert ( 150 | chat.send_receipt_template( 151 | sender_id, 152 | "Arleme", 153 | 123461346131, 154 | "MVOLA", 155 | summary=summary, 156 | receipt_elements=receipts, 157 | currency="MGA", 158 | address=address, 159 | adjustments=[adjustment], 160 | ).status_code 161 | ) == 200 162 | 163 | 164 | def test_personas_mangement(): 165 | # create 166 | personas_id = chat.create_personas( 167 | "Rivo Lalaina", "https://avatars.githubusercontent.com/u/59861055?v=4" 168 | ) 169 | assert personas_id.isdigit() 170 | 171 | # get personas 172 | personas = chat.get_personas(personas_id) 173 | assert personas.get("id") == personas_id 174 | 175 | # list personas 176 | list_personas = chat.list_personas() 177 | assert list_personas[0].get("id") == personas_id 178 | 179 | # delete personas 180 | assert chat.delete_personas(personas_id).status_code == 200 181 | 182 | 183 | def test_send_onetime_notification_request(): 184 | assert ( 185 | chat.send_onetime_notification_request( 186 | sender_id, "Accepter le notification", "/test" 187 | ).status_code 188 | == 200 189 | ) 190 | -------------------------------------------------------------------------------- /ampalibe/source.py: -------------------------------------------------------------------------------- 1 | ENV = """# PAGE ACCESS TOKEN 2 | export AMP_ACCESS_TOKEN= 3 | 4 | # PAGE VERIF TOKEN 5 | export AMP_VERIF_TOKEN= 6 | 7 | 8 | # DATABASE AUTHENTIFICATION 9 | export ADAPTER=SQLITE 10 | #export ADAPTER=MYSQL 11 | #export ADAPTER=POSTGRESQL 12 | #export ADAPTER=MONGODB 13 | 14 | ####### CASE MYSQL, POSTGRESQL OR MONGODB ADAPTER 15 | export DB_HOST= 16 | export DB_USER= 17 | export DB_PASSWORD= 18 | export DB_NAME= 19 | #export DB_PORT= 20 | #export SRV_PROTOCOL=1 21 | 22 | ####### CASE SQLITE ADAPTER 23 | export DB_FILE=ampalibe.db 24 | 25 | 26 | # APPLICATION CONFIGURATION 27 | export AMP_HOST=0.0.0.0 28 | export AMP_PORT=4555 29 | 30 | # URL APPLICATION 31 | export AMP_URL= 32 | 33 | # ENABLE ADMIN 34 | #export ADMIN_ENABLE=1 35 | 36 | # ENABLE PAYLOAD PROTECTION WITH KEY 37 | #export PAYLOAD_SECRET= 38 | """ 39 | 40 | ENV_CMD = """:: PAGE ACCESS TOKEN 41 | set AMP_ACCESS_TOKEN= 42 | 43 | :: PAGE VERIF TOKEN 44 | set AMP_VERIF_TOKEN= 45 | 46 | :: DATABASE AUTHENTIFICATION 47 | set ADAPTER=SQLITE 48 | :: set ADAPTER=MYSQL 49 | :: set ADAPTER=POSTGRESQL 50 | :: set ADAPTER=MONGODB 51 | 52 | ::::: CASE MYSQL, POSTGRESQL OR MONGODB ADAPTER 53 | set DB_HOST= 54 | set DB_USER= 55 | set DB_PASSWORD= 56 | set DB_NAME= 57 | :: set DB_PORT= 58 | :: set SRV_PROTOCOL=1 59 | 60 | :: CASE SQLITE ADAPTER 61 | set DB_FILE=ampalibe.db 62 | 63 | 64 | :: APPLICATION CONFIGURATION 65 | set AMP_HOST=0.0.0.0 66 | set AMP_PORT=4555 67 | 68 | :: URL APPLICATION 69 | set AMP_URL= 70 | 71 | :: ENABLE ADMIN 72 | :: set ADMIN_ENABLE=1 73 | 74 | :: ENABLE PAYLOAD PROTECTION WITH KEY 75 | :: set PAYLOAD_SECRET= 76 | """ 77 | 78 | CORE = """import ampalibe 79 | from ampalibe import Messenger 80 | 81 | chat = Messenger() 82 | 83 | # create a get started option to get permission of user. 84 | # chat.get_started() 85 | 86 | @ampalibe.command('/') 87 | def main(sender_id, cmd, **ext): 88 | ''' 89 | main function where messages received on 90 | the facebook page come in. 91 | 92 | @param sender_id String: 93 | sender facebook id 94 | @param cmd String: 95 | message content 96 | @param ext Dict: 97 | contain list of others 98 | data sent by facebook (sending time, ...) 99 | data sent by your payload if not set in parameter 100 | ''' 101 | 102 | chat.send_text(sender_id, "Hello, Ampalibe") 103 | """ 104 | 105 | CONF = """from os import environ as env 106 | 107 | 108 | class Configuration: 109 | ''' 110 | Retrieves the value from the environment. 111 | Takes the default value if not defined. 112 | ''' 113 | ADAPTER = env.get('ADAPTER') 114 | 115 | DB_FILE = env.get('DB_FILE') 116 | 117 | DB_HOST = env.get('DB_HOST', 'localhost') 118 | DB_USER = env.get('DB_USER', 'root') 119 | DB_PASSWORD = env.get('DB_PASSWORD', '') 120 | DB_PORT = env.get('DB_PORT') 121 | DB_NAME = env.get('DB_NAME') 122 | SRV_PROTOCOL = env.get('SRV_PROTOCOL') 123 | 124 | ACCESS_TOKEN = env.get('AMP_ACCESS_TOKEN') 125 | VERIF_TOKEN = env.get('AMP_VERIF_TOKEN') 126 | 127 | APP_HOST = env.get('AMP_HOST', '0.0.0.0') 128 | APP_PORT = int(env.get('AMP_PORT', 4555)) 129 | APP_URL = env.get('AMP_URL') 130 | ADMIN_ENABLE = env.get('ADMIN_ENABLE') 131 | PAYLOAD_SECRET = env.get('PAYLOAD_SECRET') 132 | 133 | """ 134 | 135 | LANGS = """{ 136 | "hello_world": { 137 | "en": "Hello World", 138 | "fr": "Bonjour le monde" 139 | }, 140 | 141 | "ampalibe": { 142 | "en": "Jackfruit", 143 | "fr": "Jacquier", 144 | "mg": "Ampalibe" 145 | } 146 | } 147 | """ 148 | 149 | MODELS = """from typing import Optional 150 | from datetime import datetime 151 | from sqlmodel import Field, SQLModel 152 | 153 | 154 | class AmpalibeUser(SQLModel, table=True): 155 | __tablename__: str = "amp_user" 156 | id: Optional[int] = Field(default=None, primary_key=True) 157 | user_id: str = Field(max_length=50, unique=True, nullable=False) 158 | action: Optional[str] = None 159 | last_use: datetime = Field(default=datetime.now(), nullable=False, index=True) 160 | lang: Optional[str] = Field(min_length=2, max_length=3) 161 | """ 162 | 163 | RESOURCES = """from sqladmin import ModelView 164 | from models import AmpalibeUser 165 | from ampalibe import __version__, __author__ 166 | from sqladmin import BaseView, expose 167 | 168 | # Declare here all class ofModelView or BaseView to put in Admin dahsboard 169 | 170 | 171 | ''' 172 | Example CRUD for a table 173 | ''' 174 | class UserAmpalibe(ModelView, model=AmpalibeUser): 175 | name = "Ampalibe User" 176 | description = "This is the view for the table ampalibe_user" 177 | icon = "fa-solid fa-user" 178 | sequence = 1 179 | column_list = [ 180 | AmpalibeUser.user_id, 181 | AmpalibeUser.action, 182 | AmpalibeUser.last_use, 183 | AmpalibeUser.lang, 184 | ] 185 | column_searchable_list = [ 186 | AmpalibeUser.user_id, 187 | AmpalibeUser.action, 188 | ] 189 | 190 | column_labels = { 191 | AmpalibeUser.user_id: "User Facebook ID", 192 | AmpalibeUser.action: "Action", 193 | AmpalibeUser.last_use: "Update at", 194 | AmpalibeUser.lang: "Language", 195 | } 196 | 197 | column_sortable_list = [ 198 | AmpalibeUser.last_use, 199 | ] 200 | 201 | # sort by last_use desc 202 | column_default_sort = (AmpalibeUser.last_use, True) 203 | 204 | can_create = True 205 | can_edit = True 206 | can_delete = True 207 | can_view_details = True 208 | 209 | 210 | ''' 211 | This is example of custom page you can make in your admin page 212 | ''' 213 | class OtherView(BaseView): 214 | name = "Other Page" 215 | icon = "fa-solid fa-list-alt" 216 | sequence = 0 217 | 218 | @expose("/other", methods=["GET"]) 219 | def other_page(self, request): 220 | return self.templates.TemplateResponse( 221 | "other.html", 222 | context={"request": request, "version": __version__, "author": __author__}, 223 | ) 224 | """ 225 | 226 | OTHER_HTML = """ 227 | {% extends "layout.html" %} 228 | {% block content %} 229 |
230 |

231 | Custom Admin Page for Ampalibe {{ version }} authored by {{ author }} 232 |

233 |
234 | {% endblock %} 235 | """ 236 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Ampalibe 1.0.1 2 | 3 | > Pypi: https://pypi.org/project/ampalibe/1.0.1/ 4 | 5 | ### FEATURES 6 | 7 | - Command & Action 8 | - Payload Managament 9 | - Messenger API functions 10 | - File Management 11 | 12 | 13 | ## Ampalibe 1.0.2 14 | 15 | > Pypi: https://pypi.org/project/ampalibe/1.0.2/ 16 | 17 | ### NEW 18 | 19 | - Works on Windows 20 | - Docs for custom endpoints 21 | - Imporve docs 22 | 23 | 24 | ## Ampalibe 1.0.4 25 | 26 | > Pypi: https://pypi.org/project/ampalibe/1.0.4/ 27 | 28 | ### FIX 29 | 30 | - Improve docs 31 | - Update readme 32 | - Remove deprecated methods 33 | - Add warning for action not found 34 | - Fix Api messenger Payload in second Page Template 35 | 36 | ### NEW FEATURES 37 | 38 | - Hot reload Support: `ampalibe run --dev` 39 | - Add python version 3.6 support 40 | - Add `download_file` options to direct download a link 41 | - Add `ampalibe env` command to generate only env 42 | 43 | 44 | ## Ampalibe 1.0.5 45 | 46 | ### FIX 47 | 48 | - Fix Ampalibe ASCII broken 49 | - Fix send_file Messenger API 50 | - Remove mysql-connector dependancy 51 | 52 | 53 | ### NEW FEATURES 54 | 55 | - Quick Reply Auto Next for item length more than 13 56 | - Help Messsage for Linux 57 | - Add QuickReply Object 58 | - Add Button Object 59 | - Add Element Object 60 | - Customize text of next quick_reply 61 | - Customize text of next generic template 62 | - Number of processes workers in production mode 63 | - Persistent Menu to Object ( List Buttons ) 64 | 65 | 66 | 67 | ## Ampalibe 1.0.6 68 | 69 | - ADD port argument in command-line for linux 70 | - Docker deployement in your own server 71 | - Docker Deployement with heroku 72 | 73 | 74 | ## Ampalibe 1.0.7 75 | 76 | - ADD `testmode` arg in the server to run without threads 77 | - ADD `langage Management` 78 | - lang managed directly by ampalibe 79 | - add 'ampalibe lang' command to generate langs.json file 80 | - add translate function to translate word or sentence 81 | - add `set_lang` method to set lang of an user 82 | 83 | - ADD simulate function to simulate a message. 84 | 85 | 86 | 87 | ## Ampalibe 1.1.0 88 | 89 | - Support for Postgres DATABASE 90 | - New Structure: All conf importation is optionnal now 91 | - New method for messenger api: `send_custom` 92 | - Make `NULL` Default for lang insted `fr` 93 | - FIX error in `get_temp` methode when data not exist 94 | - `simulate` function is ready for ampalibe 95 | - Introduction of ampalibe `crontab` 96 | - Add listenner for events: `messages_reads`, `messages_reactions`, `messages_delivery` 97 | - Continious integration for new Pull Request 98 | - Continious Delivery for Github package and Pypi 99 | - Rewrite `scripts` for Linux & Windows to same base code 100 | - Typewriting introduction for output command 101 | - Documentation updated 102 | - Remove workers 103 | 104 | 105 | ## Ampalibe 1.1.1 patch 106 | 107 | - fix port argument doesn't work on 108 | - fix error when python is python3 and not python 109 | 110 | ## Ampalibe 1.1.2 patch 111 | 112 | - [IMP] Add indication in dockerfile 113 | - [FIX] Payload object instead of dict in quick_replies when build Element 114 | - [FIX] Fix Typo in Ampalibe definition 115 | - [FIX] Missing gitignore file on create & init 116 | - [IMP] Adapt ASGI server to be supported by Heroku/python image 117 | - [IMP] Command decorators priority before action decorators in ampalibe core 118 | 119 | 120 | ## Ampalibe 1.1.3 Stable 121 | 122 | - [FIX] Cmd object & Payload in persistant menu 123 | - [FIX] route not recognized in Payload Object (#42) 124 | - [FIX][REF] Fix persistent menu Button not Jsonserialized (#45) 125 | - [ADD] Functionnal test in CI (#46) 126 | 127 | 128 | ## Ampalibe 1.1.4 129 | 130 | * [FIX] Fix send quick reply missing 13th with next argument by @rootkit7628 in https://github.com/iTeam-S/Ampalibe/pull/47 131 | * [IMP] Remove unecessary instructions by @gaetan1903 in https://github.com/iTeam-S/Ampalibe/pull/48 132 | * [IMP] Preserve the built-in type of data sent on Payload Management by @gaetan1903 in https://github.com/iTeam-S/Ampalibe/pull/50 133 | * [ADD] Create requirements on init and create by @rootkit7628 in https://github.com/iTeam-S/Ampalibe/pull/51 134 | * [FEAT] Object Payload sent in action by @gaetan1903 in https://github.com/iTeam-S/Ampalibe/pull/54 135 | * [FIX][IMP] Many fix & improvement by @gaetan1903 in https://github.com/iTeam-S/Ampalibe/pull/55 136 | * [FIX] send_buttons error by @gaetan1903 in ec865f7046af472575185635d9e4a9b8087f4dd4 137 | * [IMP][FIX] Verification db connection in b6b18ecfdadb3c324942d5b478873002d34696e2 138 | 139 | 140 | ## Ampalibe 1.1.5 141 | 142 | * [IMP] Data integrity for UI & Messenger (#56) 143 | * [FIX] quick_rep don't support in Element (f8054555e60a74d220e081bd163a2747778fb72e) 144 | * [IMP] Add specific type for attachments (#62) 145 | * [IMP] stringify Payload object to output (#62) 146 | * [ADD][IMP] Full functionnality `send_message` API (#63) 147 | * [ADD] Unit Test for messenger_api 148 | * [ADD] Unit Test for ampalibe CLI 149 | * [IMP] Documentation improved 150 | 151 | 152 | ## Ampalibe 1.1.6 153 | 154 | * [IMP] Add argument reusable in send_file & send_file_url 155 | * [ADD] send_attachment SEND API (sending reusable file) 156 | * [FEAT] Personas management send API (create, list, get, delete) by @rivo2302 157 | * [ADD] new messenger api : send receipt template by @rootkit7628 158 | * [FIX] attachments not received in function reported by @SergioDev22 159 | * [IMP] unit test for receiving attachment 160 | * [ADD] decorator before_receive & after_receive message 161 | * [ADD] get_user_profile SEND API 162 | 163 | 164 | ## Ampalibe 1.1.7 165 | 166 | * [ADD] Logger fearure for ampalibe (#71) 167 | * [IMP] Make Sqlite support type datetime (#72) 168 | * [IMP] send return data in after_receive function (#73) 169 | * [IMP] Avoid quick_rep params erase next quick_rep in (9d7a934c2a292ad3a67f3a514f3994df2b141984) 170 | * [IMP] Readability and maintainability (#74) 171 | 172 | 173 | ## Ampalibe 1.1.8 174 | 175 | * [ADD] Mongodb support (#76) 176 | * [IMP] Code quality (#87) (6a695791ab1b07f18cde89484ec41f72a8ed90c4) 177 | * [IMP] Optimize time for translate function (#77) 178 | * [IMP] Change temporary data management (#78) 179 | * [FIX] translate encoding in windows (af9e1fba065a726243b8c518157f101cd7c2de4b) 180 | * [ADD] New Messenger API: send_product_template (#79) 181 | * [FIX] --dev doesn't work anymore on windows (af7aba3fb1f860109a3aa417de7bbda880f09ffb) 182 | * [ADD] One time notification Messenger API (#80) 183 | * [IMP] fix & imrpov structure (#81) 184 | * [IMP] Improve core readability (#82) (#83) 185 | * [FIX] Logger , printing twice (#86) 186 | * [IMP] Model optimisation (#88) 187 | * [IMP] Minor syntax style (#89) 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /ampalibe/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import time 4 | import inspect 5 | import colorama 6 | import tempfile 7 | from . import source 8 | 9 | __version__ = "2.0.0.dev" 10 | __author__ = "iTeam-$" 11 | 12 | 13 | colorama.init() 14 | 15 | 16 | def typing_print(text): 17 | for character in text: 18 | sys.stdout.write(character) 19 | sys.stdout.flush() 20 | time.sleep(0.02) 21 | print() 22 | 23 | 24 | def create_env(path): 25 | if sys.platform == "win32": 26 | print(source.ENV_CMD, file=open(f"{path}/.env.bat", "w")) 27 | else: 28 | print(source.ENV, file=open(f"{path}/.env", "w")) 29 | typing_print("~\033[32m 👌 \033[0m | Env file created") 30 | 31 | 32 | def create_lang(path): 33 | print(source.LANGS, file=open(f"{path}/langs.json", "w")) 34 | typing_print("~\033[32m 👌 \033[0m | Langs file created") 35 | 36 | 37 | def create_models(path): 38 | print(source.MODELS, file=open(f"{path}/models.py", "w")) 39 | typing_print("~\033[32m 👌 \033[0m | Models file created") 40 | 41 | 42 | def create_resources(path): 43 | print(source.RESOURCES, file=open(f"{path}/resources.py", "w")) 44 | typing_print("~\033[32m 👌 \033[0m | Resources file created") 45 | os.makedirs(f"{path}/templates", exist_ok=True) 46 | print(source.OTHER_HTML, file=open(f"{path}/templates/other.html", "w")) 47 | 48 | 49 | def init_proj(path): 50 | create_env(path) 51 | create_lang(path) 52 | create_models(path) 53 | create_resources(path) 54 | print(source.CORE, file=open(f"{path}/core.py", "w")) 55 | typing_print("~\033[32m 👌 \033[0m | Core file created") 56 | 57 | print(source.CONF, file=open(f"{path}/conf.py", "w")) 58 | typing_print("~\033[32m 👌 \033[0m | Config file created") 59 | 60 | for folder in {"public", "private"}: 61 | os.makedirs(os.path.join(path, "assets", folder), exist_ok=True) 62 | 63 | print( 64 | ".env\n.env.bat\n__pycache__/\nngrok\nngrok.exe\n_db.json", 65 | file=open(f"{path}/.gitignore", "a"), 66 | ) 67 | 68 | print( 69 | "ampalibe", 70 | file=open(f"{path}/requirements.txt", "a"), 71 | ) 72 | 73 | 74 | if sys.argv[0] == "-m" and len(sys.argv) > 1: 75 | if sys.argv[1] == "version": 76 | typing_print("\033[32m" + __version__ + " ⭐ \033[0m") 77 | 78 | elif sys.argv[1] == "init": 79 | typing_print("~\033[32m 👌 \033[0m | Initiating ...") 80 | init_proj(".") 81 | typing_print( 82 | inspect.cleandoc( 83 | """ 84 | ~\033[32m 👌 \033[0m | Project Ampalibe initiated. \033[32mYoupii !!! 😎 \033[0m 85 | ~\033[36m TIPS\033[0m |\033[0m Fill in .env file. 86 | ~\033[36m TIPS\033[0m |\033[36m ampalibe run\033[0m for lauching project. 87 | """ 88 | ) 89 | ) 90 | 91 | elif sys.argv[1] == "create": 92 | proj_name = sys.argv[2] 93 | typing_print(f"~\033[32m 👌 \033[0m | Creating {proj_name} ...") 94 | os.makedirs(proj_name) 95 | init_proj(proj_name) 96 | typing_print( 97 | inspect.cleandoc( 98 | f""" 99 | ~\033[32m 👌 \033[0m | Project Ampalibe created. \033[32mYoupii !!! 😎 \033[0m 100 | ~\033[36m TIPS\033[0m |\033[0m Fill in .env file. 101 | ~\033[36m TIPS\033[0m |\033[36m cd {proj_name} && ampalibe run\033[0m for lauching project. 102 | """ 103 | ) 104 | ) 105 | 106 | elif sys.argv[1] == "env": 107 | create_env(".") 108 | 109 | elif sys.argv[1] == "lang": 110 | create_lang(".") 111 | 112 | elif sys.argv[1] == "run": 113 | print( 114 | inspect.cleandoc( 115 | "\033[36m" 116 | + r""" 117 | 0o 118 | Oo 119 | coooool 120 | looooooool 121 | loooooooooool 122 | _ __ __ ____ _ _ ___ ____ _____ looooooooooool 123 | / \ | \/ | _ \ / \ | | |_ _| __ )| ____| looooooooooool 124 | / _ \ | |\/| | |_) / _ \ | | | || _ \| _| loooooooooool 125 | / ___ \| | | | __/ ___ \| |___ | || |_) | |___ looooooool 126 | /_/ \_\_| |_|_| /_/ \_\_____|___|____/|_____| oooooo 127 | """ 128 | + "\033[0m" 129 | ) 130 | ) 131 | typing_print( 132 | "~\033[32m 👌\033[0m | Env Loaded\n~\033[32m 👌\033[0m | Ampalibe" 133 | " running..." 134 | ) 135 | 136 | elif sys.argv[1] == "usage": 137 | typing_print( 138 | inspect.cleandoc( 139 | """ 140 | Usage: ampalibe \033[32m { create, init, env, run, version, help } \033[0m 141 | ------ 142 | 👉 \033[32m create ... : \033[0m create a new project in a new directory specified 143 | 👉 \033[32m init: \033[0m create a new project in current dir 144 | 👉 \033[32m version: \033[0m show the current version 145 | 👉 \033[32m env: \033[0m generate only a .env file 146 | 👉 \033[32m lang: \033[0m generate only a langs.json file 147 | 👉 \033[32m run [--dev]: \033[0m run the server, autoreload if --dev is specified 148 | 👉 \033[32m help: \033[0m show this current help 149 | """ 150 | ) 151 | ) 152 | 153 | sys.exit(0) 154 | 155 | try: 156 | from conf import Configuration # type: ignore 157 | except ImportError: 158 | dir_tmp = os.path.join(tempfile.gettempdir(), "ampalibe_temp") 159 | os.makedirs(dir_tmp, exist_ok=True) 160 | with open(os.path.join(dir_tmp, "conf.py"), "w") as f: 161 | f.write(source.CONF) 162 | sys.path.append(dir_tmp) 163 | finally: 164 | from .constant import * 165 | from .model import Model 166 | from .logger import Logger 167 | from aiocron import crontab 168 | from .core import webserver 169 | from .payload import Payload 170 | from .old import Init as init 171 | from .messenger import Messenger 172 | from .utils import ( 173 | translate, 174 | download_file, 175 | async_download_file, 176 | simulate, 177 | async_simulate, 178 | ) 179 | from .decorators import ( 180 | event, 181 | action, 182 | command, 183 | before_receive, 184 | after_receive, 185 | ) 186 | -------------------------------------------------------------------------------- /docs/source/intro.rst: -------------------------------------------------------------------------------- 1 | Why use Ampalibe 2 | ================= 3 | 4 | Webhooks & process Management 5 | ------------------------------- 6 | 7 | **No need to manage webhooks and data** 8 | 9 | messages are received directly in a main function 10 | 11 | .. code-block:: python 12 | 13 | import ampalibe 14 | from ampalibe import Messenger 15 | 16 | chat = Messenger() 17 | 18 | @ampalibe.command('/') 19 | def main(sender_id, cmd, **extends): 20 | chat.send_text(sender_id, 'Hello world') 21 | chat.send_text(sender_id, f'This is your message: {cmd}') 22 | chat.send_text(sender_id, f'and this is your facebook id: {sender_id}') 23 | 24 | 25 | 26 | Action Management 27 | --------------------------- 28 | 29 | **Manages the actions expected by the users** 30 | 31 | define the function of the next treatment 32 | 33 | .. code-block:: python 34 | 35 | import ampalibe 36 | from ampalibe import Messenger, Model 37 | 38 | query = Model() 39 | chat = Messenger() 40 | 41 | @ampalibe.command('/') 42 | def main(sender_id, cmd, **extends): 43 | chat.send_text(sender_id, 'Enter your name') 44 | query.set_action(sender_id, '/get_name') 45 | 46 | @ampalibe.action('/get_name') 47 | def get_name(sender_id, cmd, **extends): 48 | query.set_action(sender_id, None) # clear current action 49 | chat.send_text(sender_id, f'Hello {cmd}') 50 | 51 | 52 | Temporary data Management 53 | --------------------------- 54 | 55 | Manage temporary data easily with set, get, and delete methods 56 | 57 | .. code-block:: python 58 | 59 | import ampalibe 60 | from ampalibe import Messenger 61 | 62 | query = Model() 63 | chat = Messenger() 64 | 65 | @ampalibe.command('/') 66 | def main(sender_id, cmd, **extends): 67 | chat.send_text(sender_id, 'Enter your mail') 68 | query.set_action(sender_id, '/get_mail') 69 | 70 | @ampalibe.action('/get_mail') 71 | def get_mail(sender_id, cmd, **extends): 72 | # save the mail in temporary data 73 | query.set_temp(sender_id, 'mail', cmd) 74 | 75 | chat.send_text(sender_id, f'Enter your password') 76 | query.set_action(sender_id, '/get_password') 77 | 78 | 79 | @ampalibe.action('/get_password') 80 | def get_password(sender_id, cmd, **extends): 81 | query.set_action(sender_id, None) # clear current action 82 | mail = query.get_temp(sender_id, 'mail') # get mail in temporary data 83 | 84 | chat.send_text(sender_id, f'your mail and your password are {mail} {cmd}') 85 | query.del_temp(sender_id, 'mail') # delete temporary data 86 | 87 | 88 | Payload Management 89 | --------------------------- 90 | 91 | **Manage Payload easily** 92 | 93 | send data with Payload object and get it in destination function's parameter 94 | 95 | .. code-block:: python 96 | 97 | import ampalibe 98 | from ampalibe.ui import QuickReply 99 | from ampalibe import Payload, Messenger 100 | 101 | chat = Messenger() 102 | 103 | 104 | @ampalibe.command('/') 105 | def main(sender_id, cmd, **extends): 106 | quick_rep = [ 107 | QuickReply( 108 | title='Angela', 109 | payload=Payload('/membre', name='Angela', ref='2016-sac') 110 | ), 111 | QuickReply( 112 | title='Rivo', 113 | payload=Payload('/membre', name='Rivo') 114 | ) 115 | ] 116 | chat.send_quick_reply(sender_id, quick_rep, 'Who?') 117 | 118 | 119 | @ampalibe.command('/membre') 120 | def get_membre(sender_id, cmd, name, **extends): 121 | chat.send_text(sender_id, "Hello " + name) 122 | 123 | # if the arg is not defined in the list of parameters, 124 | # it is put in the extends variable 125 | if extends.get('ref'): 126 | chat.send_text(sender_id, 'your ref is ' + extends.get('ref')) 127 | 128 | 129 | 130 | 131 | Advanced Messenger API 132 | --------------------------- 133 | 134 | No need to manage the length of the items to send: A next page button will be displayed directly 135 | 136 | .. code-block:: python 137 | 138 | import ampalibe 139 | from ampalibe import Payload 140 | from ampalibe.ui import Element, Button 141 | 142 | chat = Messenger() 143 | 144 | @ampalibe.command('/') 145 | def main(sender_id, cmd, **extends): 146 | list_items = [ 147 | Element( 148 | title=f"item n°{i+1}", 149 | subtitle=f"subtitle for this item n°{i+1}", 150 | image_url="https://i.imgflip.com/6b45bi.jpg", 151 | buttons=[ 152 | Button( 153 | type="postback", 154 | title="Get item", 155 | payload=Payload("/item", id_item=i+1) 156 | ) 157 | ] 158 | ) 159 | for i in range(30) 160 | ] 161 | # next=True for displaying directly next page button. 162 | chat.send_generic_template(sender_id, list_items, next=True) 163 | 164 | @ampalibe.command('/item') 165 | def get_item(sender_id, id_item, **extends): 166 | chat.send_text(sender_id, f"item n°{id_item} selected") 167 | 168 | 169 | 170 | Langage Management 171 | ------------------------- 172 | 173 | Language management is directly managed by Ampalibe 174 | 175 | **langs.json** 176 | 177 | .. code-block:: json 178 | 179 | { 180 | "hello_world": { 181 | "en": "Hello World", 182 | "fr": "Bonjour le monde" 183 | }, 184 | 185 | "ampalibe": { 186 | "en": "Jackfruit", 187 | "fr": "Jacquier", 188 | "mg": "Ampalibe" 189 | } 190 | } 191 | 192 | 193 | **core.py** 194 | 195 | .. code-block:: python 196 | 197 | import ampalibe 198 | from ampalibe import Model, Messenger, translate 199 | 200 | query = Model() 201 | chat = Messenger() 202 | 203 | @ampalibe.command('/') 204 | def main(sender_id, lang, cmd, **extends): 205 | chat.send_text( 206 | sender_id, 207 | translate('hello_world', lang) 208 | ) 209 | query.set_lang(sender_id, 'en') 210 | query.set_action(sender_id, '/what_my_lang') 211 | 212 | 213 | @ampalibe.action('/what_my_lang') 214 | def other_func(sender_id, lang, cmd, **extends): 215 | query.set_action(sender_id, None) 216 | 217 | chat.send_text(sender_id, 'Your lang is ' + lang + ' now') 218 | chat.send_text( 219 | sender_id, 220 | translate('hello_world', lang) 221 | ) 222 | 223 | 224 | .. important :: 225 | 226 | **Framework in constant evolution and maintained, with many other features to discover in the doc.** 227 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ampalibe 2 |

3 |
4 |

5 | Video Tutorials 6 | · 7 | Documentation 8 | · 9 | Report Bug 10 |

11 | 12 |

13 | Ampalibe is a lightweight Python framework for building Facebook Messenger bots faster. 14 | It provides a new concept, it manages webhooks, processes data sent by Facebook and provides API Messenger with advanced functions such as payload management, item length, and more. 15 |

16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |

26 | 27 | 28 |

29 | 30 |

31 | Show your support by giving a star 🌟 if this project helped you! 32 |

33 |
34 | 35 | 36 | ## Installation 37 | 38 | ```s 39 | pip install ampalibe 40 | ``` 41 | 42 | OR you can install dev version 43 | 44 | 45 | ```s 46 | pip install https://github.com/iTeam-S/Ampalibe/archive/refs/heads/main.zip 47 | ``` 48 | 49 | ------------------ 50 | 51 | if you use mysql as database, you have to install `mysql-connector` or `mysql-connector-python` with ampalibe 52 | 53 | ```s 54 | pip install ampalibe[mysql-connector] 55 | ``` 56 | 57 | ---------------------- 58 | 59 | if you use postgresql as database, you have to install `psycopg2` with ampalibe 60 | 61 | ```s 62 | pip install ampalibe[psycopg2] 63 | ``` 64 | 65 | ---------------------- 66 | 67 | if you use mongodb as database, you have to install `pymongo` with ampalibe 68 | 69 | ```s 70 | pip install ampalibe[pymongo] 71 | ``` 72 | 73 | ## Usage 74 | 75 | > command-line __ampalibe__ is __ampalibe.bat__ for _Windows_ 76 | 77 | ```s 78 | ampalibe create myproject 79 | ``` 80 | 81 | OR 82 | 83 | 84 | ```shell 85 | $ cd myproject 86 | $ ampalibe init 87 | ``` 88 | 89 | to run project, just use 90 | ```s 91 | ampalibe run 92 | ``` 93 | 94 | for dev mode with __Hot Reload__ 95 | ```s 96 | ampalibe run --dev 97 | ``` 98 | 99 | ### Register for an Access Token 100 | 101 | You will need to configure a Facebook application, a Facebook page, get the access to the page, link the application to the page, configure a webhook for your app before you can really start using __Ampalibe__. 102 | 103 | [This app setup guide](https://developers.facebook.com/docs/messenger-platform/getting-started/app-setup) should help 104 | 105 | OR 106 | 107 | See [this video](https://www.youtube.com/watch?v=Sg2P9uFJEF4&list=PL0zWFyU4-Sk5FcKJpBTp0-_nDm0kIQ5sY&index=1) on Youtube 108 | 109 | ### Minimal Application 110 | 111 | ```python 112 | import ampalibe 113 | from ampalibe import Messenger, Model 114 | from ampalibe.messenger import Action 115 | 116 | chat = Messenger() 117 | query = Model() 118 | 119 | @ampalibe.before_receive() 120 | def before_process(sender_id, **ext): 121 | # Mark as seen for each message received 122 | chat.send_action(sender_id, Action.mark_seen) 123 | return True 124 | 125 | @ampalibe.command('/') 126 | def main(sender_id, cmd, **ext): 127 | """ 128 | No need to manage weebhooks and data: messages are received directly in a main function 129 | """ 130 | chat.send_text(sender_id, 'Enter your name') 131 | 132 | # define the function of the next treatment 133 | query.set_action(sender_id, '/get_name') 134 | 135 | @ampalibe.action('/get_name') 136 | def get_name(sender_id, cmd, **ext): 137 | query.set_action(sender_id, None) # clear current action 138 | chat.send_text(sender_id, f'Hello {cmd}') # greeting with name enter by user 139 | ``` 140 | 141 | ## Documentation 142 | 143 | - [Ampalibe Readthedocs](https://ampalibe.readthedocs.io/) 144 | 145 | #### Other resource 146 | 147 | - [ [Youtube] Create a Facebook Bot Messenger with AMPALIBE Framework (EN) ](https://www.youtube.com/playlist?list=PL0zWFyU4-Sk5FcKJpBTp0-_nDm0kIQ5sY) 148 | - [ [Youtube] Tutoriel Framework Ampalibe (FR)](https://www.youtube.com/playlist?list=PLz95IHSyn29U4PA1bAUw3VT0VFFbq1LuP) 149 | - [ [Youtube] Ampalibe Framework Episode (Teny Vary Masaka) ](https://www.youtube.com/playlist?list=PLN1d8qaIQgmKmCwy3SMfndiivbgwXJZvi) 150 | 151 | 152 | ## Deployment 153 | 154 | **Using container** 155 | 156 | > Go to our dir project and run 157 | 158 | ```s 159 | $ docker run -d -v "${PWD}:/usr/src/app" -p 4555:4555 ghcr.io/iteam-s/ampalibe 160 | ``` 161 | 162 | **Using heroku container** 163 | 164 | - Go to heroku docs for [docker deploys](https://devcenter.heroku.com/articles/container-registry-and-runtime) 165 | 166 | - Change your Dockerfile like this 167 | 168 | ```dockerfile 169 | FROM ghcr.io/iteam-s/ampalibe 170 | 171 | ADD . /usr/src/app/ 172 | 173 | # RUN pip install --no-cache-dir -r requirements.txt 174 | 175 | CMD ampalibe -p $PORT run 176 | ``` 177 | - Customize your Dockerfile if necessary 178 | 179 | 180 | **Using heroku python** 181 | 182 | - Go to heroku docs for [Getting Started on Heroku with Python](https://devcenter.heroku.com/articles/getting-started-with-python?singlepage=true) 183 | 184 | 185 | - Define your Procfile like this `web: ampalibe -p $PORT run` 186 | 187 | 188 | **Other plateform ?** 189 | 190 | Maybe just run `ampalibe run` in the right directory? or specify port if needed `ampalibe -p 1774 run` 191 | 192 | ## About 193 | 194 | Ampalibe is a word of Malagasy origin designating the fruit jackfruit. 195 | 196 | We have made a promise to 197 | 198 | - keep it **light** 199 | - make it **easy to use** 200 | - do it **quickly to develop** 201 | 202 | 203 | ## Contributors 204 | 205 | ![Image des contributeurs GitHub](https://contrib.rocks/image?repo=iTeam-S/Ampalibe) 206 | 207 | ## Community 208 | 209 | ### 📌 Extension 210 | 211 | - [Ampalibe extension](https://marketplace.visualstudio.com/items?itemName=iTeam-S.ampalibe) by [Raja Rakotonirina](https://github.com/RajaRakoto) 212 | 213 | A VScode extension that allows the user to write a snippet of code using easy to remember prefixes 214 | 215 | 216 | ### 📌 Module 217 | 218 | - [Ampalibe Odoo Addons](https://apps.odoo.com/apps/modules/15.0/ampalibe/) by [Rivo Lalaina](https://github.com/rivo2302) 219 | 220 | An Odoo module to synchronize the Ampalibe Framework with Odoo ERP. 221 | 222 | ### 📌 Star History 223 | 224 | 225 | 226 | 227 | Star History Chart 228 | 229 | -------------------------------------------------------------------------------- /docs/source/more.rst: -------------------------------------------------------------------------------- 1 | Supported Database 2 | ===================== 3 | 4 | The advantage of using Ampalibe's supported databases is that we can inherit the Model object of ampalibe to make a database request. 5 | 6 | We no longer need to make the connection to the database. 7 | 8 | we use the instances received from the *Model* object as the variable **db**, **cursor**, 9 | 10 | **Example** 11 | 12 | ``model.py`` file 13 | 14 | .. code-block:: python 15 | 16 | from ampalibe import Model 17 | 18 | class CustomModel(Model): 19 | def __init__(self): 20 | super().__init__() 21 | 22 | @Model.verif_db 23 | def get_list_users(self): 24 | ''' 25 | CREATE CUSTOM method to communicate with database 26 | ''' 27 | req = "SELECT * from amp_user" 28 | self.cursor.execute(req) 29 | data = self.cursor.fetchall() 30 | self.db.commit() 31 | return data 32 | 33 | ''' 34 | Use this, in case using of mongodb database. 35 | ''' 36 | @Model.verif_db 37 | def get_list_users(self): 38 | return list(self.db.amp_user.find({})) 39 | 40 | 41 | 42 | ``core.py`` file 43 | 44 | .. code-block:: python 45 | 46 | import ampalibe 47 | from ampalibe import Messenger 48 | from model import CustomModel 49 | 50 | chat = Messenger() 51 | query = CustomModel() 52 | 53 | 54 | @ampalibe.command('/') 55 | def main(sender_id, cmd, **ext): 56 | # query.set_lang(sender_id, 'en') 57 | print(query.get_list_users()) 58 | 59 | 60 | .. important:: 61 | 62 | *Model.verif_db* is a decorator who checks that the application is well connected to the database before launching the request. 63 | 64 | 65 | .. note:: 66 | 67 | However we can still use other databases than those currently supported 68 | 69 | 70 | 71 | 72 | Logging and structure 73 | ===================== 74 | 75 | Logging 76 | -------- 77 | .. image:: https://raw.githubusercontent.com/iTeam-S/Ampalibe/main/docs/source/_static/logger.png 78 | 79 | By default, Ampalibe uses the logging module to log the application but with custom formatting,to 80 | make it easier to read and avoid using print. 81 | 82 | Messenger API methods response is a request so we can use it to view the response 83 | 84 | .. code-block:: python 85 | 86 | from ampalibe import Logger 87 | 88 | Logger.info("Info message") 89 | Logger.debug("Debug message") 90 | Logger.warning("Warning message") 91 | Logger.error("Error message") 92 | Logger.critical("Critical message") 93 | 94 | @ampalibe.command('/') 95 | def main(sender_id, cmd, **ext): 96 | Logger.info("Message received from user") 97 | res = chat.send_text(sender_id, "Hello world") 98 | if res.status_code == 200: 99 | Logger.info("Message sent to user") 100 | else: 101 | Logger.error("Error sending message to user") 102 | 103 | 104 | Structure 105 | ----------- 106 | 107 | Each developer is **free to choose the structure he wants**, by just importing the files into the core.py. 108 | 109 | We can make our functions everywhere, even as methods 110 | 111 | ``core.py`` file 112 | 113 | .. code-block:: python 114 | 115 | # importing another file contains ampalibe decorator 116 | import user 117 | import ampalibe 118 | from ampalibe import Messenger, Payload 119 | 120 | chat = Messenger() 121 | 122 | 123 | @ampalibe.command('/') 124 | def main(sender_id, cmd, **ext): 125 | buttons = [ 126 | { 127 | "type": "postback", 128 | "title": "Dashboard", 129 | "payload": '/login/admin' 130 | } 131 | ] 132 | chat.send_button(sender_id, buttons, 'What do you want to do?') 133 | 134 | 135 | class Admin: 136 | 137 | @ampalibe.command('/login/admin') 138 | def login(sender_id, **ext): 139 | ''' 140 | function is always calling when payload or message start by /login/admin 141 | ''' 142 | bot.query.set_action(sender_id, '/get_username') 143 | bot.chat.send_message(sender_id, 'Enter your username') 144 | 145 | 146 | ``user.py`` file 147 | 148 | .. code-block:: python 149 | 150 | import ampalibe 151 | from ampalibe import Messenger 152 | 153 | chat = Messenger() 154 | 155 | 156 | class User: 157 | 158 | @ampalibe.action('/get_username') 159 | def username(sender_id, cmd, **ext): 160 | bot.chat.send_message(sender_id, 'OK ' + cmd) 161 | bot.query.set_action(sender_id, None) 162 | 163 | 164 | .. note:: 165 | 166 | if you want use a MVC Pattern 167 | 168 | here is an example of an MVC template that can be used: `Ampalibe MVC Template `_ 169 | 170 | 171 | Custom endpoint 172 | ================= 173 | 174 | The web server part and the endpoints are managed directly by Ampalibe 175 | 176 | However, a custom end point can be created using the `FastAPI `_ object instance 177 | 178 | 179 | .. code-block:: python 180 | 181 | import ampalibe 182 | from ampalibe import webserver 183 | 184 | chat = ampalibe.Messenger() 185 | 186 | 187 | @webserver.get('/test') 188 | def test(): 189 | return 'Hello, test' 190 | 191 | @ampalibe.command('/') 192 | def main(sender_id, cmd, **ext): 193 | chat.send_message(sender_id, "Hello, Ampalibe") 194 | 195 | 196 | Crontab 197 | ================= 198 | 199 | Since Ampalibe v1.1.0, we can use the `Crontab `_ to schedule the execution of a task in the bot. 200 | 201 | 202 | *"run this task at this time on this date".* 203 | 204 | example of using the crontab 205 | 206 | .. code-block:: python 207 | 208 | ''' 209 | Send a weather report to everyone in the morning every day at 08:00 210 | ''' 211 | 212 | import ampalibe 213 | from ampalibe import webserver 214 | from model import CustomModel 215 | 216 | chat = ampalibe.Messenger() 217 | query = CustomModel() 218 | 219 | @ampalibe.crontab('0 8 * * *') 220 | async def say_hello(): 221 | for user in query.get_list_users(): 222 | chat.send_message(user[0], 'Good Morning') 223 | 224 | 225 | @ampalibe.command('/') 226 | def main(sender_id, cmd, **ext): 227 | ... 228 | 229 | 230 | You can too activate your crontab for later 231 | 232 | .. code-block:: python 233 | 234 | ''' 235 | Say hello to everyone in the morning every day at 08:00 236 | ''' 237 | 238 | import ampalibe 239 | ... 240 | 241 | chat = ampalibe.Messenger() 242 | 243 | def get_weather(): 244 | # Use a webservice to get the weather report 245 | ... 246 | return weather 247 | 248 | 249 | @ampalibe.crontab('0 8 * * *', start=False) 250 | async def weather_report(): 251 | weather = get_weather() 252 | for user in query.get_list_users(): 253 | chat.send_message(user[0], weather) 254 | 255 | 256 | @ampalibe.command('/') 257 | def main(sender_id, cmd, **ext): 258 | print("activate the crontab now") 259 | weather_report.start() 260 | 261 | you can also create directly in the code 262 | 263 | .. code-block:: python 264 | 265 | ''' 266 | Send everyone notification every 3 hours 267 | ''' 268 | 269 | import ampalibe 270 | from ampalibe import crontab 271 | 272 | chat = ampalibe.Messenger() 273 | 274 | async def send_notif(): 275 | for user in query.get_list_users(): 276 | chat.send_message(user[0], 'Notification for you') 277 | 278 | @ampalibe.command('/') 279 | def main(sender_id, cmd, **ext): 280 | print('Create a crontab schedule') 281 | ''' 282 | Don't forget to add argument loop=ampalibe.core.loop 283 | if the crontab is writing inside a function 284 | '''' 285 | crontab('0 */3 * * *', func=send_notif, loop=ampalibe.core.loop) 286 | 287 | 288 | .. code-block:: python 289 | 290 | ''' 291 | Send a notification to a user every 3 hours 292 | ''' 293 | 294 | import ampalibe 295 | from ampalibe import crontab 296 | 297 | chat = ampalibe.Messenger() 298 | 299 | async def send_notif(sender_id): 300 | chat.send_message(sender_id, 'Notification for you') 301 | 302 | @ampalibe.command('/') 303 | def main(sender_id, cmd, **ext): 304 | print('Create a crontabe schedule') 305 | ''' 306 | Don't forget to add argument loop=ampalibe.core.loop 307 | if the crontab is running inside decorated function 308 | like command, action, event 309 | '''' 310 | crontab('0 */3 * * *', func=send_notif, args=(sender_id,), loop=ampalibe.core.loop) 311 | 312 | .. note:: 313 | 314 | if you don't know how to create cron syntax you can check `here `_ 315 | 316 | .. important:: 317 | 318 | ampalibe **crontab** use `croniter `_ for the spec, so you can check all the possibilities of time. 319 | 320 | 321 | 322 | Event 323 | ================= 324 | 325 | Since v1.1+ 326 | You can now listening event like `message_reads`, `message_reactions` and `message_delivery` 327 | with ampalibe **event** decorator. 328 | 329 | 330 | .. code-block:: python 331 | 332 | import ampalibe 333 | 334 | chat = ampalibe.Messenger() 335 | 336 | @ampalibe.event('read') 337 | def event_read(**ext): 338 | print('message is reading') 339 | print(ext) 340 | 341 | @ampalibe.event('delivery') 342 | def event_read(**ext): 343 | print('last message is delivery') 344 | print(ext) 345 | 346 | @ampalibe.event('reaction') 347 | def event_read(**ext): 348 | print('A message received a reaction') 349 | print(ext) 350 | 351 | @ampalibe.command('/') 352 | def main(sender_id, cmd, **ext): 353 | chat.send_message(sender_id, "Hello, Ampalibe") 354 | 355 | 356 | 357 | .. note:: 358 | 359 | The are 3 arguments for `event` decorator: *read*, *delivery*, *reaction* 360 | 361 | 362 | 363 | Native event 364 | ================= 365 | 366 | Since v1.1.6+ 367 | You can now add event like `before_receive` and `after_receive` 368 | in ampalibe as decorator to execute a function before or after a received message 369 | 370 | 371 | .. code-block:: python 372 | 373 | import ampalibe 374 | from ampalibe.messenger import Action 375 | 376 | chat = ampalibe.Messenger() 377 | 378 | @ampalibe.before_receive() 379 | def before_process(sender_id, **ext): 380 | chat.send_action(sender_id, Action.mark_seen) 381 | return True 382 | 383 | 384 | @ampalibe.command("/") 385 | def main(sender_id, **ext): 386 | chat.send_text(sender_id, "Hello ampalibe") 387 | 388 | 389 | .. important:: 390 | 391 | The function decorated with before receive must return the value ``True`` to continue the process. 392 | 393 | So you can stop the process directly by returning the value ``False``. 394 | 395 | 396 | .. code-block:: python 397 | 398 | import ampalibe 399 | from ampalibe.messenger import Action 400 | 401 | chat = ampalibe.Messenger() 402 | swearing_words = ['f**k you'] 403 | 404 | @ampalibe.before_receive() 405 | def before_process(sender_id, cmd, **ext): 406 | chat.send_action(sender_id, Action.typing_on) 407 | 408 | if cmd in swearing_words: 409 | return False 410 | return True 411 | 412 | @ampalibe.after_receive() 413 | def after_process(sender_id, **ext): 414 | chat.send_action(sender_id, Action.typing_off) 415 | 416 | ''' 417 | you can also receive here the return data from the processing function. 418 | the data is in the "res" variable 419 | ''' 420 | print(ext.get('res')) # OK, There is no problem 421 | 422 | 423 | 424 | @ampalibe.command("/") 425 | def main(sender_id, **ext): 426 | chat.send_text(sender_id, "Hello ampalibe") 427 | 428 | return "OK, There is no problem" 429 | 430 | 431 | .. note:: 432 | 433 | the function decorated by **after_receive** always executes regardless 434 | of the value returned by the function decorated by **before_receive** . -------------------------------------------------------------------------------- /ampalibe/model.py: -------------------------------------------------------------------------------- 1 | # pyright: reportGeneralTypeIssues=false 2 | 3 | import os 4 | from .payload import Payload 5 | from datetime import datetime 6 | from conf import Configuration # type: ignore 7 | from .singleton import singleton 8 | from tinydb import TinyDB, Query 9 | from tinydb.operations import delete 10 | 11 | 12 | class DataBaseConfig: 13 | def standart(self, conf=Configuration): 14 | """ 15 | function to configure the standart database 16 | """ 17 | db_conf = { 18 | "host": conf.DB_HOST, 19 | "user": conf.DB_USER, 20 | "password": conf.DB_PASSWORD, 21 | "database": conf.DB_NAME, 22 | } 23 | if conf.DB_PORT: 24 | db_conf["port"] = conf.DB_PORT 25 | return db_conf 26 | 27 | def get_db_url(self, conf=Configuration): 28 | """ 29 | function to configure the standart database 30 | """ 31 | if conf.ADAPTER == "SQLITE": 32 | return f"sqlite:///{conf.DB_FILE}" 33 | elif conf.ADAPTER == "MONGODB": 34 | return self.mongodb(conf) 35 | else: 36 | url = f"{conf.ADAPTER}://{conf.DB_USER}" 37 | if conf.DB_PASSWORD: 38 | url += f":{conf.DB_PASSWORD}" 39 | url += f"@{conf.DB_HOST}" 40 | if conf.DB_PORT: 41 | url += f":{str(conf.DB_PORT)}" 42 | url += f"/{conf.DB_NAME}" 43 | return url.lower() 44 | 45 | def mongodb(self, conf=Configuration): 46 | """ 47 | function to configure the mongodb database 48 | """ 49 | 50 | url = "mongodb" 51 | if hasattr(conf, "SRV_PROTOCOL"): 52 | url += "+srv://" if not conf.SRV_PROTOCOL else "://" 53 | 54 | if conf.DB_USER and conf.DB_PASSWORD: 55 | url += f"{conf.DB_USER}:{conf.DB_PASSWORD}@" 56 | 57 | url += conf.DB_HOST 58 | 59 | if conf.DB_PORT: 60 | url += f":{str(conf.DB_PORT)}/" 61 | 62 | return url 63 | 64 | 65 | class Model: 66 | """ 67 | Object for interact with database with pre-defined function 68 | """ 69 | 70 | def __init__(self, conf=Configuration, init=True): 71 | """ 72 | object to interact with database 73 | 74 | @params: conf [ Configuration object ] 75 | init [ boolean ] 76 | """ 77 | if init: 78 | self._start(conf) 79 | 80 | 81 | def _start(self, conf=Configuration): 82 | self.ADAPTER = conf.ADAPTER 83 | 84 | if self.ADAPTER in ("MYSQL", "POSTGRESQL"): 85 | self.DB_CONF = DataBaseConfig().standart(conf) 86 | elif self.ADAPTER == "MONGODB": 87 | self.DB_CONF = DataBaseConfig().mongodb(conf) 88 | else: # SQLite is choosen by default 89 | self.DB_CONF = conf.DB_FILE 90 | 91 | self.__connect() 92 | self.__init_db() 93 | os.makedirs("assets/private/", exist_ok=True) 94 | self.tinydb = TinyDB("assets/private/_db.json") 95 | 96 | 97 | def __connect(self): 98 | """ 99 | The function which connect object to the database. 100 | """ 101 | if self.ADAPTER == "MYSQL": 102 | try: 103 | import mysql.connector 104 | except ImportError: 105 | raise ImportError( 106 | "You must install mysql-connector to use mysql database: pip install mysql-connector" 107 | ) 108 | self.db = mysql.connector.connect(**self.DB_CONF) 109 | 110 | elif self.ADAPTER == "POSTGRESQL": 111 | try: 112 | import psycopg2 113 | except ImportError: 114 | raise ImportError( 115 | "You must install psycopg2 to use postgresql database: pip install psycopg2" 116 | ) 117 | self.db = psycopg2.connect(**self.DB_CONF) 118 | 119 | elif self.ADAPTER == "MONGODB": 120 | try: 121 | import pymongo 122 | except ImportError: 123 | raise ImportError( 124 | "You must install pymongo to use mongodb database: pip install pymongo" 125 | ) 126 | self.db = pymongo.MongoClient(self.DB_CONF) 127 | self.db = self.db[Configuration.DB_NAME] 128 | return # no cursor for mongodb 129 | 130 | else: 131 | import sqlite3 132 | 133 | self.db = sqlite3.connect( 134 | self.DB_CONF, 135 | detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES, 136 | ) 137 | 138 | self.cursor = self.db.cursor() 139 | 140 | def __init_db(self): 141 | """ 142 | Creation of table if not exist 143 | Check the necessary table if exists 144 | """ 145 | 146 | if self.ADAPTER == "MYSQL": 147 | req = """ 148 | CREATE TABLE IF NOT EXISTS `amp_user` ( 149 | `id` INT NOT NULL AUTO_INCREMENT, 150 | `user_id` varchar(50) NOT NULL UNIQUE, 151 | `action` TEXT DEFAULT NULL, 152 | `last_use` datetime NOT NULL DEFAULT current_timestamp(), 153 | `lang` varchar(5) DEFAULT NULL, 154 | PRIMARY KEY (`id`), 155 | INDEX (last_use) 156 | ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 157 | """ 158 | elif self.ADAPTER == "POSTGRESQL": 159 | req = """ 160 | CREATE TABLE IF NOT EXISTS amp_user ( 161 | id SERIAL PRIMARY KEY, 162 | user_id VARCHAR UNIQUE NOT NULL, 163 | action TEXT DEFAULT NULL, 164 | last_use TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 165 | lang VARCHAR DEFAULT NULL 166 | ); 167 | CREATE INDEX IF NOT EXISTS last_use_index ON amp_user(last_use); 168 | 169 | """ 170 | elif self.ADAPTER == "MONGODB": 171 | if "amp_user" not in self.db.list_collection_names(): 172 | self.db.create_collection("amp_user") 173 | # self.db.amp_user.create_index("user_id", unique=True) 174 | return 175 | else: 176 | req = """ 177 | CREATE TABLE IF NOT EXISTS amp_user ( 178 | id INTEGER PRIMARY KEY AUTOINCREMENT, 179 | user_id TEXT NOT NULL UNIQUE, 180 | action TEXT, 181 | last_use TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 182 | lang TEXT 183 | ) 184 | """ 185 | self.cursor.execute(req) 186 | self.db.commit() 187 | 188 | def verif_db(fonction): # type: ignore 189 | """ 190 | decorator that checks if the database 191 | is connected or not before doing an operation. 192 | """ 193 | 194 | def trt_verif(*arg, **kwarg): 195 | arg[0].__connect() 196 | return fonction(*arg, **kwarg) 197 | 198 | return trt_verif 199 | 200 | @verif_db 201 | def _verif_user(self, user_id): 202 | """ 203 | method to insert new user and/or update the date 204 | of last use if the user already exists. 205 | 206 | @params : user_id 207 | 208 | """ 209 | SQL_MAP = { 210 | "MYSQL": "INSERT INTO amp_user(user_id) VALUES (%s) ON DUPLICATE KEY UPDATE last_use = NOW();", 211 | "POSTGRESQL": "INSERT INTO amp_user(user_id) VALUES (%s) ON CONFLICT (user_id) DO UPDATE SET last_use = NOW();", 212 | "SQLITE": "INSERT INTO amp_user(user_id) VALUES (?) ON CONFLICT(user_id) DO UPDATE SET last_use = CURRENT_TIMESTAMP;", 213 | } 214 | 215 | if self.ADAPTER == "MONGODB": 216 | self.db.amp_user.update_one( 217 | {"user_id": user_id}, 218 | {"$set": {"last_use": datetime.now()}}, 219 | upsert=True, 220 | ) 221 | else: 222 | sql = SQL_MAP.get(self.ADAPTER) 223 | self.cursor.execute(sql, (user_id,)) 224 | self.db.commit() 225 | 226 | @verif_db 227 | def get_action(self, sender_id): 228 | """ 229 | get current action of an user 230 | 231 | @params : sender_id 232 | @return : current action [ type of String/None ] 233 | """ 234 | return self.get(sender_id, "action")[0] 235 | 236 | @verif_db 237 | def set_action(self, sender_id, action): 238 | """ 239 | define a current action if an user 240 | 241 | @params : sender_id, action 242 | @return: None 243 | """ 244 | if isinstance(action, Payload): 245 | action = Payload.trt_payload_out(action) 246 | 247 | if self.ADAPTER in ("MYSQL", "POSTGRESQL"): 248 | req = "UPDATE amp_user set action = %s WHERE user_id = %s" 249 | elif self.ADAPTER == "MONGODB": 250 | self.db.amp_user.update_one( 251 | {"user_id": sender_id}, 252 | {"$set": {"action": action}}, 253 | ) 254 | return 255 | else: 256 | req = "UPDATE amp_user set action = ? WHERE user_id = ?" 257 | self.cursor.execute(req, (action, sender_id)) 258 | self.db.commit() 259 | 260 | @verif_db 261 | def set_temp(self, sender_id, key, value): 262 | """ 263 | set a temp parameter of an user 264 | 265 | @params: sender_id 266 | @return: None 267 | """ 268 | if self.ADAPTER == "MONGODB": 269 | self.db.amp_user.update_one( 270 | {"user_id": sender_id}, 271 | {"$set": {key: value}}, 272 | ) 273 | return 274 | if not self.tinydb.update({key: value}, Query().user_id == sender_id): 275 | self.tinydb.insert({"user_id": sender_id, key: value}) 276 | 277 | @verif_db 278 | def get_temp(self, sender_id, key): 279 | """ 280 | get one temporary data of an user 281 | 282 | @parmas : sender_id 283 | key 284 | @return: data 285 | """ 286 | if self.ADAPTER == "MONGODB": 287 | return self.db.amp_user.find({"user_id": sender_id})[0].get(key) 288 | 289 | res = self.tinydb.search(Query().user_id == sender_id) 290 | if res: 291 | return res[0].get(key) 292 | 293 | @verif_db 294 | def del_temp(self, sender_id, key): 295 | """ 296 | delete temporary parameter of an user 297 | 298 | @parameter : sender_id 299 | key 300 | @return: None 301 | """ 302 | if self.ADAPTER == "MONGODB": 303 | self.db.amp_user.update_one( 304 | {"user_id": sender_id}, 305 | {"$unset": {key: ""}}, 306 | ) 307 | return 308 | self.tinydb.update(delete(key), Query().user_id == sender_id) 309 | 310 | @verif_db 311 | def get_lang(self, sender_id): 312 | """ 313 | get current lang of an user 314 | 315 | @params: sender_id 316 | @return lang or None 317 | """ 318 | return self.get(sender_id, "lang")[0] 319 | 320 | @verif_db 321 | def set_lang(self, sender_id, lang): 322 | """ 323 | define a current lang for an user 324 | 325 | @params : sender_id 326 | @return: None 327 | """ 328 | if self.ADAPTER in ("MYSQL", "POSTGRESQL"): 329 | req = "UPDATE amp_user set lang = %s WHERE user_id = %s" 330 | elif self.ADAPTER == "MONGODB": 331 | self.db.amp_user.update_one( 332 | {"user_id": sender_id}, 333 | {"$set": {"lang": lang}}, 334 | ) 335 | return 336 | else: 337 | req = "UPDATE amp_user set lang = ? WHERE user_id = ?" 338 | self.cursor.execute(req, (lang, sender_id)) 339 | self.db.commit() 340 | 341 | @verif_db 342 | def get(self, sender_id, *args): 343 | """ 344 | get specific data of an user 345 | 346 | @params : sender_id, list of data to get 347 | @return: list of data 348 | """ 349 | if self.ADAPTER in ("MYSQL", "POSTGRESQL"): 350 | req = f"SELECT {','.join(args)} FROM amp_user WHERE user_id = %s" 351 | elif self.ADAPTER == "MONGODB": 352 | data = self.db.amp_user.find({"user_id": sender_id})[0] 353 | return [data.get(k) for k in args] 354 | else: 355 | req = f"SELECT {','.join(args)} FROM amp_user WHERE user_id = ?" 356 | self.cursor.execute(req, (sender_id,)) 357 | return self.cursor.fetchone() or (None, None) 358 | -------------------------------------------------------------------------------- /ampalibe/ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | List of All UI Widget Messenger 3 | """ 4 | from .payload import Payload 5 | from .constant import ( 6 | Type, 7 | Content_type, 8 | Notification_reoptin, 9 | Notification_cta_text, 10 | Notification_frequency, 11 | ) 12 | 13 | 14 | class QuickReply: 15 | def __init__(self, **kwargs): 16 | """ 17 | Object that can be used to generated a quick_reply 18 | """ 19 | self.content_type = kwargs.get("content_type", "text") 20 | if self.content_type not in ("text", "user_phone_number", "user_email"): 21 | raise ValueError( 22 | "content_type can only be 'text', 'user_phone_number', 'user_email'" 23 | ) 24 | 25 | self.title = kwargs.get("title") 26 | self.payload = kwargs.get("payload") 27 | 28 | if self.content_type == "text" and not self.payload: 29 | raise ValueError("payload must be present for text") 30 | 31 | if self.content_type == "text" and not self.title: 32 | raise ValueError("title must be present for text") 33 | 34 | self.image_url = kwargs.get("image_url") 35 | 36 | @property 37 | def value(self): 38 | res = {"content_type": self.content_type} 39 | 40 | if self.content_type == "text": 41 | res["title"] = self.title 42 | res["payload"] = Payload.trt_payload_out(self.payload) 43 | 44 | if self.image_url: 45 | res["image_url"] = self.image_url 46 | return res 47 | 48 | def __str__(self): 49 | return str(self.value) 50 | 51 | 52 | class Button: 53 | def __init__(self, **kwargs): 54 | 55 | self.type = kwargs.get("type", "postback") 56 | self.title = kwargs.get("title") 57 | self.url = kwargs.get("url") 58 | self.payload = kwargs.get("payload") 59 | 60 | if self.type not in ( 61 | "postback", 62 | "web_url", 63 | "phone_number", 64 | "account_link", 65 | "account_unlink", 66 | ): 67 | raise ValueError( 68 | "type must be one of 'postback', 'web_url', 'phone_number', 'account_link', 'account_unlink'" 69 | ) 70 | 71 | if self.type in ("postback", "phone_number"): 72 | 73 | if not self.payload: 74 | raise ValueError("payload must be present") 75 | 76 | if not self.title: 77 | raise ValueError("title must be present") 78 | 79 | elif self.type == "web_url": 80 | if not self.url: 81 | raise ValueError("url must be present for web_url") 82 | 83 | if not self.title: 84 | raise ValueError("title must be present for web_url") 85 | 86 | elif self.type == "account_link": 87 | if not self.url: 88 | raise ValueError("url facebook login must be present for account_link") 89 | 90 | @property 91 | def value(self): 92 | if self.payload: 93 | self.payload = Payload.trt_payload_out(self.payload) 94 | 95 | if self.type in ("postback", "phone_number"): 96 | return {"type": self.type, "title": self.title, "payload": self.payload} 97 | 98 | elif self.type == "web_url": 99 | return {"type": "web_url", "title": self.title, "url": self.url} 100 | 101 | elif self.type == "account_link": 102 | return {"type": "account_link", "url": self.url} 103 | 104 | elif self.type == "account_unlink": 105 | return {"type": "account_unlink"} 106 | 107 | def __str__(self): 108 | return str(self.value) 109 | 110 | 111 | class Element: 112 | def __init__(self, **kwargs): 113 | self.title = kwargs.get("title") 114 | self.subtitle = kwargs.get("subtitle") 115 | self.image = kwargs.get("image_url") 116 | self.buttons = kwargs.get("buttons") 117 | self.default_action = kwargs.get("default_action") 118 | 119 | if not self.title: 120 | raise ValueError("Element must be have a title") 121 | 122 | if not self.buttons: 123 | raise ValueError("Element must be have a buttons") 124 | 125 | if not isinstance(self.buttons, list): 126 | raise ValueError("buttons must be a list of Button") 127 | 128 | if len(self.buttons) > 3: 129 | raise ValueError("buttons must be three maximum") 130 | 131 | for i in range(len(self.buttons)): 132 | if not isinstance(self.buttons[i], Button): 133 | raise ValueError("buttons must a List of Button") 134 | 135 | if self.default_action: 136 | if not isinstance(self.default_action, Button): 137 | raise ValueError("default_action must be a Button") 138 | 139 | @property 140 | def value(self): 141 | res = {"title": self.title} 142 | 143 | if self.subtitle: 144 | res["subtitle"] = self.subtitle 145 | 146 | if self.image: 147 | res["image_url"] = self.image 148 | 149 | if self.default_action: 150 | res["default_action"] = self.default_action.value 151 | 152 | res["buttons"] = [button.value for button in self.buttons] # type: ignore 153 | 154 | return res 155 | 156 | def __str__(self): 157 | return str(self.value) 158 | 159 | 160 | class ReceiptElement: 161 | def __init__(self, **kwargs): 162 | self.title = kwargs.get("title") 163 | self.subtitle = kwargs.get("subtitle") 164 | self.quantity = kwargs.get("quantity") 165 | self.price = kwargs.get("price") 166 | self.currency = kwargs.get("currency") 167 | self.image = kwargs.get("image_url") 168 | 169 | if not self.title: 170 | raise ValueError("Receipt element must be have a title") 171 | 172 | if not self.price: 173 | raise ValueError("Receipt element must be have a price") 174 | 175 | @property 176 | def value(self): 177 | res = {"title": self.title} 178 | 179 | if self.subtitle: 180 | res["subtitle"] = self.subtitle 181 | 182 | if self.quantity: 183 | res["quantity"] = self.quantity 184 | 185 | res["price"] = self.price 186 | res["currency"] = self.currency 187 | 188 | if self.image: 189 | res["image_url"] = self.image 190 | 191 | return res 192 | 193 | def __str__(self): 194 | return str(self.value) 195 | 196 | 197 | class Summary: 198 | def __init__(self, **kwargs): 199 | self.subtotal = kwargs.get("subtotal") 200 | self.shipping_cost = kwargs.get("shipping_cost") 201 | self.total_tax = kwargs.get("total_tax") 202 | self.total_cost = kwargs.get("total_cost") 203 | 204 | if not self.total_cost: 205 | raise ValueError("Summary must have a total_cost") 206 | 207 | @property 208 | def value(self): 209 | res = {"total_cost": self.total_cost} 210 | 211 | if self.shipping_cost: 212 | res["shipping_cost"] = self.shipping_cost 213 | 214 | if self.total_tax: 215 | res["total_tax"] = self.total_tax 216 | 217 | if self.subtotal: 218 | res["subtotal"] = self.subtotal 219 | 220 | return res 221 | 222 | def __str__(self): 223 | return str(self.value) 224 | 225 | 226 | class Address: 227 | def __init__(self, **kwargs): 228 | self.street_1 = kwargs.get("street_1") 229 | self.street_2 = kwargs.get("street_2") 230 | self.city = kwargs.get("city") 231 | self.postal_code = kwargs.get("postal_code") 232 | self.state = kwargs.get("state") 233 | self.country = kwargs.get("country") 234 | 235 | if not self.street_1: 236 | raise ValueError("Address must have a street_1") 237 | 238 | if not self.city: 239 | raise ValueError("Address must have a city") 240 | 241 | if not self.postal_code: 242 | raise ValueError("Address must have a postal_code") 243 | 244 | if not self.state: 245 | raise ValueError("Address must have a state") 246 | 247 | if not self.country: 248 | raise ValueError("Address must have a country") 249 | 250 | @property 251 | def value(self): 252 | res = {} 253 | 254 | if self.street_1: 255 | res["street_1"] = self.street_1 256 | 257 | if self.street_2: 258 | res["street_2"] = self.street_2 259 | 260 | if self.city: 261 | res["city"] = self.city 262 | 263 | if self.postal_code: 264 | res["postal_code"] = self.postal_code 265 | 266 | if self.state: 267 | res["state"] = self.state 268 | 269 | if self.country: 270 | res["country"] = self.country 271 | 272 | return res 273 | 274 | def __str__(self): 275 | return str(self.value) 276 | 277 | 278 | class Adjustment: 279 | def __init__(self, **kwargs): 280 | self.name = kwargs.get("name") 281 | self.amount = kwargs.get("amount") 282 | 283 | if not self.name: 284 | raise ValueError("Adjustment must be have a name") 285 | 286 | if not self.amount: 287 | raise ValueError("Adjustment must be have a amount") 288 | 289 | @property 290 | def value(self): 291 | return {"name": self.name, "amount": self.amount} 292 | 293 | def __str__(self): 294 | return str(self.value) 295 | 296 | 297 | class RecurringNotificationOptin: 298 | def __init__(self, **kwargs): 299 | """ 300 | RecurringNotificationOptin ui object to be used in the send_optin 301 | Args: 302 | title (str) - Title of the optin 303 | image (str | optional) - Image url of the optin 304 | payload (str) - Payload of the optin 305 | notification_frequency (str) - Frequency of the notification messages 306 | """ 307 | self.template_type = "notification_messages" 308 | self.title = kwargs.get("title") 309 | self.image_url = kwargs.get("image_url") 310 | self.payload = kwargs.get("payload") 311 | self.notification_frequency = kwargs.get("notification_frequency") 312 | self.notification_cta_text = kwargs.get("notification_cta_text") 313 | self.notification_reoptin = kwargs.get("notification_reoptin") 314 | 315 | if not self.title: 316 | raise ValueError("RecurringNotificationOptin must have a title") 317 | 318 | if not self.payload: 319 | raise ValueError("RecurringNotificationOptin must have a payload") 320 | 321 | if not self.notification_frequency: 322 | raise ValueError( 323 | "RecurringNotificationOptin must have a notification frequency" 324 | ) 325 | 326 | if self.notification_frequency not in Notification_frequency: 327 | raise ValueError( 328 | "RecurringNotificationOptin must have a valid notification frequency" 329 | ) 330 | 331 | if self.notification_cta_text: 332 | if self.notification_cta_text not in Notification_cta_text: 333 | raise ValueError( 334 | "RecurringNotificationOptin must have a valid notification cta text" 335 | ) 336 | 337 | if self.notification_reoptin: 338 | if self.notification_reoptin not in Notification_reoptin: 339 | raise ValueError( 340 | "RecurringNotificationOptin must have a valid notification reoptin" 341 | ) 342 | 343 | @property 344 | def value(self): 345 | res = { 346 | "template_type": self.template_type, 347 | "title": self.title, 348 | "notification_messages_frequency": self.notification_frequency.value, # type: ignore 349 | } 350 | 351 | if isinstance(self.payload, Payload): 352 | res["payload"] = Payload.trt_payload_out(self.payload) 353 | else: 354 | res["payload"] = self.payload 355 | 356 | if self.image_url: 357 | res["image_url"] = self.image_url 358 | 359 | if self.notification_cta_text: 360 | res["notification_messages_cta_text"] = self.notification_cta_text.value 361 | 362 | if self.notification_reoptin: 363 | res["notification_messages_reoptin"] = self.notification_reoptin.value 364 | 365 | return res 366 | 367 | def __str__(self): 368 | return str(self.value) 369 | 370 | 371 | class Product: 372 | def __init__(self, id): 373 | self.id = id 374 | 375 | @property 376 | def value(self): 377 | return {"id": self.id} 378 | 379 | def __str__(self): 380 | return str(self.value) 381 | -------------------------------------------------------------------------------- /docs/source/quickstart.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ========= 3 | 4 | A Minimal Application 5 | ----------------------- 6 | 7 | A minimal Ampalibe application looks something like this: 8 | 9 | .. code-block:: python 10 | 11 | import ampalibe 12 | from ampalibe import Messenger 13 | 14 | chat = Messenger() 15 | 16 | @ampalibe.command('/') 17 | def main(sender_id, cmd, **ext): 18 | chat.send_text(sender_id, "Hello, Ampalibe") 19 | 20 | 21 | So what did that code do? 22 | 23 | .. hlist:: 24 | :columns: 1 25 | 26 | * ``line 1`` we import ampalibe package. 27 | 28 | * ``line 2`` we import Messenger class which contains our messenger api. 29 | 30 | * ``line 4`` we inite ampalibe Messenger api and store the result instance. 31 | 32 | * we create a function decorated by ampalibe.command('/') to say Ampalibe that it's the main function. 33 | 34 | * ``line 8`` We respond to the sender by greeting Ampalibe 35 | 36 | 37 | .. note:: 38 | 39 | messages are received directly in a main function 40 | 41 | 42 | Command 43 | --------- 44 | 45 | Ampalibe's philosophy says that all received messages, whether it is a simple or payload message, or an image, is considered to be commands 46 | 47 | The command decorator of ampalibe is used to specify the function where a specific command must launch. 48 | If no corresponding command is found, it launches the main function 49 | 50 | 51 | .. code-block:: python 52 | 53 | import ampalibe 54 | from ampalibe import Messenger 55 | from ampalibe.messenger import Action 56 | 57 | chat = Messenger() 58 | 59 | @ampalibe.command('/') 60 | def main(sender_id, cmd, **ext): 61 | chat.send_action(sender_id, Action.mark_seen) 62 | chat.send_text(sender_id, "Hello, Ampalibe") 63 | 64 | ''' 65 | if the message received start with '/eat' 66 | the code enter here, not in main 67 | ex: /eat 1 jackfruit 68 | 69 | cmd value contains /eat 1 jackfruit 70 | ''' 71 | @ampalibe.command('/eat') 72 | def spec(sender_id, cmd, **ext): 73 | print(sender_id) # 1555554455 74 | print(cmd) # '1 jackfruit' 75 | 76 | 77 | ''' 78 | ex: /drink_a_jackfruit_juice 79 | ''' 80 | @ampalibe.command('/drink_a_jackfruit_juice') 81 | def spec(sender_id, cmd, **ext): 82 | print(sender_id) # 1555554455 83 | print(cmd) # /drink_a_jackfruit_juice 84 | 85 | .. note:: 86 | 87 | When we create a function decorated by ampalibe.command, ``**ext`` parameter must be present 88 | 89 | Action 90 | ---------- 91 | 92 | At some point, we will need to point the user to a specific function, to retrieve user-specific data, for example, ask for his email, ask for the word the person wants to search for. 93 | 94 | To do this, you have to define the action expected by the user, to define what should be expected from the user. 95 | 96 | in this example, we will use two things, the **action decorator** and the **query.set_action** method 97 | 98 | **Example 1**: Ask the name of user, and greet him 99 | 100 | .. code-block:: python 101 | 102 | import ampalibe 103 | from ampalibe import Model, Messenger 104 | 105 | chat = Messenger() 106 | query = Model() 107 | 108 | @ampalibe.command('/') 109 | def main(sender_id, cmd, **ext): 110 | chat.send_text(sender_id, 'Enter your name') 111 | query.set_action(sender_id, '/get_name') 112 | 113 | @ampalibe.action('/get_name') 114 | def get_name(sender_id, cmd, **ext): 115 | query.set_action(sender_id, None) # clear current action 116 | chat.send_text(sender_id, f'Hello {cmd}') 117 | 118 | **Example 2**: Ask a number and say if it a even number or odd number 119 | 120 | .. code-block:: python 121 | 122 | import ampalibe 123 | from ampalibe import Model, Messenger 124 | 125 | chat = Messenger() 126 | query = Model() 127 | 128 | @ampalibe.command('/') 129 | def main(sender_id, cmd, **ext): 130 | chat.send_text(sender_id, 'Enter a number') 131 | query.set_action(sender_id, '/get_number') 132 | 133 | @ampalibe.action('/get_number') 134 | def get_number(sender_id, cmd, **ext): 135 | query.set_action(sender_id, None) # clear current action 136 | if cmd.isdigit(): 137 | if int(cmd) % 2 == 0: 138 | chat.send_text(sender_id, 'even number') 139 | else: 140 | chat.send_text(sender_id, 'odd number') 141 | else: 142 | chat.send_text(sender_id, f'{cmd} is not a number') 143 | 144 | 145 | We define the next function in which the user message entered and can obtain all the texts of the message in "cmd" 146 | 147 | 148 | .. important:: 149 | 150 | Remember to erase the current action to prevent the message from entering the same function each time 151 | 152 | .. note:: 153 | 154 | When we create a function decorated by ampalibe.action, ``**extends`` parameter must be present 155 | 156 | 157 | 158 | Temporary data 159 | ----------------- 160 | 161 | For each processing of each message, we will need to store information temporarily, 162 | like saving the login while waiting to ask for the password 163 | 164 | the methods used are **set_temp**, **get_temp**, **del_temp** 165 | 166 | .. code-block:: python 167 | 168 | import ampalibe 169 | from ampalibe import Model, Messenger 170 | 171 | chat = Messenger() 172 | query = Model() 173 | 174 | @ampalibe.command('/') 175 | def main(sender_id, cmd, **ext): 176 | chat.send_text(sender_id, 'Enter your mail') 177 | query.set_action(sender_id, '/get_mail') 178 | 179 | @ampalibe.action('/get_mail') 180 | def get_mail(sender_id, cmd, **ext): 181 | # save the mail in temporary data 182 | query.set_temp(sender_id, 'mail', cmd) 183 | 184 | chat.send_text(sender_id, f'Enter your password') 185 | query.set_action(sender_id, '/get_password') 186 | 187 | 188 | @ampalibe.action('/get_password') 189 | def get_password(sender_id, cmd, **ext): 190 | query.set_action(sender_id, None) # clear current action 191 | # get mail in temporary data 192 | mail = query.get_temp(sender_id, 'mail') 193 | chat.send_text(sender_id, f'your mail and your password are {mail} {cmd}') 194 | # delete mail in temporary data 195 | query.del_temp(sender_id, 'mail') 196 | 197 | 198 | Payload Management 199 | ---------------------- 200 | 201 | Ampalibe facilitates the management of payloads with the possibility of sending arguments. 202 | 203 | You can send data with ``Payload`` object and get it in destination function's parameter 204 | 205 | .. code-block:: python 206 | 207 | import ampalibe 208 | # import the Payload class 209 | from ampalibe import Messenger, Payload 210 | from ampalibe.ui import QuickReply 211 | 212 | chat = Messenger() 213 | 214 | 215 | @ampalibe.command('/') 216 | def main(sender_id, cmd, **ext): 217 | quick_rep = [ 218 | QuickReply( 219 | title='Angela', 220 | payload=Payload('/membre', name='Angela', ref='2016-sac') 221 | ), 222 | QuickReply( 223 | title='Rivo', 224 | payload=Payload('/membre', name='Rivo', ref='2016-sac') 225 | ) 226 | ] 227 | chat.send_quick_reply(sender_id, quick_rep, 'Who?') 228 | 229 | 230 | @ampalibe.command('/member') 231 | def get_membre(sender_id, cmd, name, **ext): 232 | ''' 233 | You can receive the arguments payload in extends or 234 | specifying the name of the argument in the parameters 235 | ''' 236 | chat.send_text(sender_id, "Hello " + name) 237 | 238 | # if the arg is not defined in the list of parameters, 239 | # it is put in the extends variable 240 | if ext.get('ref'): 241 | chat.send_text(sender_id, 'your ref is ' + ext.get('ref')) 242 | 243 | 244 | You can also use Payload in action, an alternative to using temporary data 245 | 246 | **Using Temporary Data** 247 | 248 | .. code-block:: python 249 | 250 | import ampalibe 251 | from ampalibe import Payload, Model 252 | from ampalibe.ui import QuickReply 253 | 254 | query = Model() 255 | 256 | @ampalibe.command("/try_action") 257 | def try_action(sender_id, **extends): 258 | query.set_action(sender_id, "/action_work") 259 | query.set_temp(sender_id, "myname", "Ampalibe") 260 | query.set_temp(sender_id, "version", "2") 261 | 262 | 263 | @ampalibe.action("/action_work") 264 | def action_work(sender_id, cmd, **extends): 265 | query.set_action(sender_id, None) 266 | myname = query.get_temp(sender_id, "myname") 267 | version = query.get_temp(sender_id, "version") 268 | print(cmd + " " + myname + version) 269 | 270 | 271 | **Using payload** (an alternative to using temporary data) 272 | 273 | .. code-block:: python 274 | 275 | import ampalibe 276 | from ampalibe import Payload, Model 277 | from ampalibe.ui import QuickReply 278 | 279 | query = Model() 280 | 281 | @ampalibe.command("/try_action") 282 | def try_action(sender_id, **extends): 283 | query.set_action(sender_id, Payload("/action_work", myname="Ampalibe", version="2")) 284 | 285 | 286 | @ampalibe.action("/action_work") 287 | def action_work(sender_id, cmd, myname, version, **extends): 288 | query.set_action(sender_id, None) 289 | print(cmd + " " + myname + version) 290 | 291 | 292 | File management 293 | ------------------- 294 | 295 | We recommand to make static file in assets folder, 296 | 297 | for files you use as a URL file, you must put assets/public, in assets/private otherwise 298 | 299 | .. code-block:: python 300 | 301 | ''' 302 | Suppose that a logo file is in "assets/public/iTeamS.png" and that we must send it via url 303 | ''' 304 | 305 | import ampalibe 306 | from ampalibe import Messenger 307 | from conf import Configuration as config 308 | from ampalibe.messenger import Filetype 309 | 310 | chat = Messenger() 311 | 312 | 313 | @ampalibe.command('/') 314 | def main(sender_id, cmd, **ext): 315 | ''' 316 | to get a file in assets/public folder, 317 | the route is /asset/ 318 | ''' 319 | chat.send_file_url( 320 | sender_id, 321 | config.APP_URL + '/asset/iTeamS.png', 322 | filetype=Filetype.image 323 | ) 324 | 325 | Langage Management 326 | ------------------------- 327 | 328 | Since Ampalibe v1.0.7, a file langs.json is generated by default. 329 | 330 | if you are using old project, you can run this command to generate manually a lang file. 331 | 332 | .. code-block:: console 333 | 334 | $ ampalibe lang 335 | 336 | you can add in this file all your words by respecting key/value format of json. 337 | 338 | .. code-block:: javascript 339 | 340 | { 341 | "" : { 342 | "" : "", 343 | "" : "", 344 | ... 345 | "" : "", 346 | }, 347 | 348 | } 349 | 350 | 351 | So you can use it in translate function 352 | 353 | **core.py** 354 | 355 | .. code-block:: python 356 | 357 | import ampalibe 358 | from ampalibe import translate 359 | from ampalibe import Model, Messenger 360 | 361 | chat = Messenger() 362 | query = Model() 363 | 364 | @ampalibe.command('/') 365 | def main(sender_id, lang, cmd, **ext): 366 | chat.send_text( 367 | sender_id, 368 | translate('hello_world', lang) 369 | ) 370 | 371 | .. note:: 372 | 373 | You can use the ``lang`` parameter in your code to get the lang of an user. 374 | 375 | you can take it in **extends** parameter too 376 | 377 | 378 | .. code-block:: python 379 | 380 | import ampalibe 381 | from ampalibe import translate 382 | 383 | @ampalibe.command('/') 384 | def main(sender_id, cmd, **ext): 385 | print(ext.get('lang')) # current lang of sender_id 386 | 387 | Use the ``set_lang`` method to set the lang of an user. 388 | 389 | .. code-block:: python 390 | 391 | import ampalibe 392 | from ampalibe import translate 393 | from ampalibe import Model, Messenger 394 | 395 | chat = Messenger() 396 | query = Model() 397 | 398 | 399 | @ampalibe.command('/') 400 | def main(sender_id, cmd, **ext): 401 | chat.send_text( 402 | sender_id, 403 | "Hello world" 404 | ) 405 | query.set_lang(sender_id, 'fr') 406 | query.set_action(sender_id, '/what_my_lang') 407 | 408 | 409 | @ampalibe.action('/what_my_lang') 410 | def other_func(sender_id, lang, cmd, **ext): 411 | query.set_action(sender_id, None) 412 | 413 | chat.send_text(sender_id, 'Your lang is ' + lang + ' now') 414 | chat.send_text( 415 | sender_id, 416 | translate('hello_world', lang) 417 | ) 418 | 419 | .. important:: 420 | 421 | if the word key is not exist in the lang file, the word is not translated. 422 | 423 | the ``translate`` function return the word key if the word is not translated. -------------------------------------------------------------------------------- /docs/source/messenger.rst: -------------------------------------------------------------------------------- 1 | Messenger API 2 | ============= 3 | 4 | list of methods for sending messages to messenger 5 | 6 | .. note:: 7 | 8 | - All returns of these functions are a POST Requests 9 | - notification_type, messaging_type and tag parameters can be sent in kwargs 10 | 11 | 12 | **Ref**: https://developers.facebook.com/docs/messenger-platform/reference/send-api/ 13 | 14 | 15 | send_text 16 | ____________ 17 | 18 | This method allows you to send a text message to the given recipient, 19 | Note that the number of characters to send is limited to 2000 characters 20 | 21 | 22 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages#sending_text 23 | 24 | **Args**: 25 | 26 | *dest_id (str)*: user id facebook for the destination 27 | 28 | *text (str)*: text want to send 29 | 30 | **Example**: 31 | 32 | .. code-block:: python 33 | 34 | chat.send_text(sender_id, "Hello world") 35 | 36 | 37 | send_action 38 | ____________ 39 | 40 | This method is used to simulate an action on messages. 41 | example: view, writing. 42 | 43 | Action available: ['mark_seen', 'typing_on', 'typing_off'] 44 | 45 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/sender-actions 46 | 47 | **Args**: 48 | 49 | *dest_id (str)*: user id facebook for the destination 50 | 51 | *action (str)*: action ['mark_seen', 'typing_on', 'typing_off'] 52 | 53 | **Example**: 54 | 55 | .. code-block:: python 56 | 57 | from ampalibe.messenger import Action 58 | ... 59 | 60 | chat.send_action(sender_id, Action.mark_seen) 61 | 62 | 63 | send_quick_reply 64 | _________________ 65 | 66 | .. image:: https://raw.githubusercontent.com/iTeam-S/Ampalibe/main/docs/source/_static/quickrep.png 67 | 68 | Quick replies provide a way to present a set of up to 13 buttons 69 | in-conversation that contain a title and optional image, and appear 70 | prominently above the composer. 71 | 72 | You can also use quick replies 73 | to request a person's location, email address, and phone number. 74 | 75 | 76 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/quick-replies 77 | 78 | **Args**: 79 | 80 | *dest_id (str)*: user id facebook for the destoination 81 | 82 | *quick_rep(list of Button)*: list of the different quick_reply to send a user 83 | 84 | *text (str)*: A text of a little description for each 85 | 86 | **Example**: 87 | 88 | .. code-block:: python 89 | 90 | from ampalibe import Payload 91 | from ampalibe.ui import QuickReply 92 | ... 93 | 94 | quick_rep = [ 95 | QuickReply( 96 | title="Angela", 97 | payload=Payload("/membre"), 98 | image_url="https://i.imgflip.com/6b45bi.jpg" 99 | ), 100 | QuickReply( 101 | title="Rivo", 102 | payload=Payload("/membre"), 103 | image_url="https://i.imgflip.com/6b45bi.jpg" 104 | ), 105 | ] 106 | 107 | # next=True in parameter for displaying directly next list quick_reply 108 | chat.send_quick_reply(sender_id, quick_rep, 'who do you choose ?') 109 | 110 | .. code-block:: python 111 | 112 | from ampalibe.ui import QuickReply, Content_type 113 | ... 114 | 115 | quick_rep = [ 116 | QuickReply( 117 | content_type=Content_type.text 118 | title=f"response {i+1}", 119 | payload= Payload("/response", item=i+1), 120 | image_url="https://i.imgflip.com/6b45bi.jpg" 121 | ) 122 | 123 | for i in range(30) 124 | ] 125 | 126 | # put a value in `next` parameter to show directly next options with the specified word. 127 | chat.send_quick_reply(sender_id, quick_rep, 'who do you choose ?', next='See More') 128 | 129 | 130 | send_generic_template 131 | ______________________ 132 | 133 | .. image:: https://raw.githubusercontent.com/iTeam-S/Ampalibe/main/docs/source/_static/template.png 134 | 135 | The method send_generic_template represent a Message templates who offer a way for you 136 | to offer a richer in-conversation experience than standard text messages by integrating 137 | buttons, images, lists, and more alongside text a single message. Templates can be use for 138 | many purposes, such as displaying product information, asking the messagerecipient to choose 139 | from a pre-determined set of options, and showing search results. 140 | 141 | For this, messenger only validates 10 templates 142 | for the first display, so we put the parameter 143 | to manage these numbers if it is a number of 144 | elements more than 10. 145 | So, there is a quick_reply which acts as a "next page" 146 | displaying all requested templates 147 | 148 | 149 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/template/generic 150 | 151 | **Args**: 152 | 153 | *dest_id (str)*: user id facebook for the destination 154 | 155 | *elements(list of Element)*: the list of the specific elements to define the structure for the template 156 | 157 | *quick_rep(list of QuickReply)*: addition quick reply at the bottom of the template 158 | 159 | *next(bool)*: this params activate the next page when elements have a length more than 10 160 | 161 | **Example**: 162 | 163 | .. code-block:: python 164 | 165 | from ampalibe import Payload 166 | from ampalibe.ui import Element, Button, Type 167 | 168 | ... 169 | 170 | list_items = [] 171 | 172 | for i in range(30): 173 | buttons = [ 174 | Button( 175 | type=Type.postback, 176 | title="Get item", 177 | payload=Payload("/item", id_item=i+1), 178 | ) 179 | ] 180 | 181 | list_items.append( 182 | Element( 183 | title="iTem", 184 | image_url="https://i.imgflip.com/6b45bi.jpg", 185 | buttons=buttons, 186 | ) 187 | ) 188 | 189 | # next=True for displaying directly next page button. 190 | chat.send_generic_template(sender_id, list_items, next=True) 191 | 192 | # next= for displaying directly next page button with custom text. 193 | # chat.send_generic_template(sender_id, list_items, next='Next page') 194 | 195 | send_file_url 196 | _____________ 197 | 198 | The Messenger Platform allows you to attach assets to messages, including audio, 199 | video, images, and files.All this is the role of this Method. The maximum attachment 200 | size is 25 MB. 201 | 202 | **Args**: 203 | 204 | *dest_id (str)*: user id facebook for destination 205 | 206 | *url (str)*: the origin url for the file 207 | 208 | *filetype (str, optional)*: type of showing file["video","image","audio","file"]. Defaults to 'file'. 209 | 210 | *reusable (bool, optional)*: if True, the file can be reused using send_attachments. Defaults to False. 211 | 212 | 213 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages#url 214 | 215 | 216 | 217 | **Example**: 218 | 219 | .. code-block:: python 220 | 221 | from ampalibe.messenger import Filetype 222 | ... 223 | 224 | chat.send_file_url(sender_id, 'https://i.imgflip.com/6b45bi.jpg', filetype=Filetype.image) 225 | 226 | 227 | 228 | send_file 229 | ____________ 230 | 231 | This method send an attachment from file 232 | 233 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages#file 234 | 235 | **Args**: 236 | 237 | *dest_id (str)*: user id facebook for the destination 238 | 239 | *file (str)*: name of the file in local folder 240 | 241 | *filetype (str, optional)*: type of the file["video","image",...]. Defaults to "file". 242 | 243 | *filename (str, optional)*: A filename received for de destination . Defaults to name of file in local. 244 | 245 | *reusable (bool, optional)*: if True, the file can be reused using send_attachments. Defaults to False. 246 | 247 | 248 | **Example**: 249 | 250 | .. code-block:: python 251 | 252 | from ampalibe.messenger import Filetype 253 | ... 254 | 255 | 256 | chat.send_file(sender_id, "mydocument.pdf") 257 | 258 | chat.send_file(sender_id, "intro.mp4", filetype=Filetype.video) 259 | 260 | chat.send_file(sender_id, "myvoice.m4a", filetype=Filetype.audio) 261 | 262 | 263 | 264 | send_attachment 265 | _________________ 266 | 267 | Method that send a attachment via attachment_id received from send_file or send_file_url when reusable=True 268 | 269 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages#attachment_reuse 270 | 271 | **Args**: 272 | 273 | *dest_id (str)*: user id facebook for the destination 274 | 275 | *attachment_id (str)*: the id of the attachment to send 276 | 277 | *filetype (str, optional)*: type of the file["video","image",...]. Defaults to "file". 278 | 279 | 280 | **Example**: 281 | 282 | .. code-block:: python 283 | 284 | import ampalibe 285 | from ampalibe.messenger import Filetype 286 | ... 287 | 288 | 289 | @ampalibe.command("/") 290 | def main(sender_id, **ext): 291 | res = chat.send_file(sender_id, "assets/private/mydocument.pdf", reusable=True) 292 | if res.status_code == 200: 293 | data = res.json() 294 | attachment_id = data.get("attachment_id") 295 | 296 | # send the attachment using attachment_id 297 | chat.send_attachment(sender_id, attachment_id, filetype=Filetype.file) 298 | 299 | 300 | 301 | send_media 302 | ____________ 303 | 304 | Method that sends files media as image and video via facebook link. 305 | This model does not allow any external URLs, only those on Facebook. 306 | 307 | 308 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/template/media 309 | 310 | **Args**: 311 | 312 | *dest_id (str)*: user id facebook for the destination 313 | 314 | *fb_url (str)*: url of the media to send on facebook 315 | 316 | *media_type (str)*: the type of the media who to want send, available["image","video"] 317 | 318 | **Example**: 319 | 320 | .. code-block:: python 321 | 322 | from ampalibe.messenger import Filetype 323 | ... 324 | 325 | chat.send_media(sender_id, "https://www.facebook.com/iTeam.Community/videos/476926027465187", Filetype.video) 326 | 327 | 328 | send_button 329 | ____________ 330 | 331 | .. image:: https://raw.githubusercontent.com/iTeam-S/Ampalibe/main/docs/source/_static/button.png 332 | 333 | The button template sends a text message with 334 | up to three buttons attached. This template gives 335 | the message recipient different options to choose from, 336 | such as predefined answers to questions or actions to take. 337 | 338 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/template/button 339 | 340 | **Args**: 341 | 342 | *dest_id (str)*: user id facebook for the destination 343 | 344 | *buttons(list of Button)*: The list of buttons who want send 345 | 346 | *text (str)*: A text to describe the fonctionnality of the buttons 347 | 348 | **Example**: 349 | 350 | .. code-block:: python 351 | 352 | from ampalibe import Payload 353 | from ampalibe.ui import Button, Type 354 | 355 | buttons = [ 356 | Button( 357 | type=Type.postback, 358 | title='Informations', 359 | payload=Payload('/contact') 360 | ) 361 | ] 362 | 363 | chat.send_button(sender_id, buttons, "What do you want to do?") 364 | 365 | 366 | get_started 367 | ____________ 368 | 369 | Method that GET STARTED button 370 | when the user talk first to the bot. 371 | 372 | 373 | **Ref**: https://developers.facebook.com/docs/messenger-platform/reference/messenger-profile-api/get-started-button 374 | 375 | **Args**: 376 | 377 | *dest_id (str)*: user id facebook for the destination 378 | 379 | *payload (str)*: payload of get started, default: '/' 380 | 381 | 382 | **Example**: 383 | 384 | .. code-block:: python 385 | 386 | chat.get_started() 387 | 388 | 389 | persistent_menu 390 | ________________ 391 | 392 | The Persistent Menu disabling the composer best practices allows you to have an always-on 393 | user interface element inside Messenger conversations. This is an easy way to help people 394 | discover and access the core functionality of your Messenger bot at any point in the conversation 395 | 396 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/persistent-menu 397 | 398 | **Args**: 399 | 400 | *dest_id (str)*: user id for destination 401 | 402 | *persistent_menu (list of dict) | (list of Button)*: the elements of the persistent menu to enable 403 | 404 | *action (str, optional)*: the action for benefit["PUT","DELETE"]. Defaults to 'PUT'. 405 | 406 | *locale [optionnel]* 407 | 408 | *composer_input_disabled [optionnel]* 409 | 410 | **Example**: 411 | 412 | .. code-block:: python 413 | 414 | from ampalibe import Payload 415 | from ampalibe.ui import Button, Type 416 | ... 417 | 418 | persistent_menu = [ 419 | Button(type=Type.postback, title='Menu', payload=Payload('/payload')), 420 | Button(type=Type.postback, title='Logout', payload=Payload('/logout')) 421 | ] 422 | 423 | chat.persistent_menu(sender_id, persistent_menu) 424 | 425 | 426 | send_custom 427 | ________________ 428 | 429 | it uses to implemend an api that not yet implemend in Ampalibe. 430 | 431 | refer to other api in this link https://developers.facebook.com/docs/messenger-platform 432 | 433 | **Args**: 434 | 435 | *custom_json (dict)*: the json who want send 436 | 437 | *endpoint (str)*: the endpoint if is not '/messages' 438 | 439 | 440 | send_receipt_template 441 | _____________________ 442 | 443 | it sends a receipt template to a customer to confirm his order. 444 | 445 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/template/receipt 446 | 447 | **Args**: 448 | *recipient_name (str)*: The name of the recipient 449 | 450 | *order_number (str)*: The order number 451 | 452 | *payment_method (str)*: The payment method 453 | 454 | *summary (Summary or dict)*: The summary of the order 455 | 456 | *currency (str)*: The currency of the order 457 | 458 | *address (Adresse or dict)*: The address of the recipient (optional) 459 | 460 | *adjustments (list)*: The adjustments of the order (optional) 461 | 462 | *order_url (str)*: The url of the order (optional) 463 | 464 | *timestamp (str)*: The timestamp of the order (optional) 465 | 466 | **Example**: 467 | 468 | .. code-block:: python 469 | 470 | from ampalibe.ui import ReceiptElement, Address, Summary, Adjustment 471 | ... 472 | 473 | # create a receipt element 474 | receipts = [ 475 | ReceiptElement(title='Tee-shirt', price=1000), 476 | ReceiptElement(title='Pants', price=2000), 477 | ] 478 | 479 | # create a summary 480 | summary = Summary(total_cost=300) 481 | 482 | # create an address 483 | address = Address(street_1='Street 1', city='City', state='State', postal_code='Postal Code', country='Country') 484 | 485 | # create an adjustment 486 | adjustment = Adjustment(name='Discount of 10%', amount=10) 487 | 488 | chat.send_receipt_template( 489 | sender_id, "Arleme", 123461346131, "MVOLA", summary=summary, receipt_elements=receipts, currency='MGA', address=address, adjustments=[adjustment]) 490 | 491 | create_personas 492 | _________________ 493 | 494 | The Messenger Platform allows you to create and manage personas for your business messaging experience. The persona may be backed by a human agent or a bot. A persona allows conversations to be passed from bots to human agents seemlessly. 495 | When a persona is introduced into a conversation, the persona's profile picture will be shown and all messages sent by the persona will be accompanied by an annotation above the message that states the persona name and business it represents. 496 | 497 | .. image:: https://raw.githubusercontent.com/iTeam-S/Ampalibe/main/docs/source/_static/personas.png 498 | 499 | Method to create personas 500 | 501 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/personas 502 | 503 | **Args**: 504 | 505 | *name (str)*: The name of the personas to create 506 | 507 | *profile_picture_url(str)*: The url of the profile picture of the personas 508 | 509 | **Response**: 510 | 511 | *srtr*: id of the personas created 512 | 513 | **Example**: 514 | 515 | .. code-block:: python 516 | 517 | from ampalibe import Messenger 518 | 519 | chat = Messenger() 520 | 521 | persona_id = chat.create_personas('Rivo Lalaina', 'https://avatars.githubusercontent.com/u/59861055?v=4') 522 | 523 | chat.send_text(sender_id, "Hello", persona_id=persona_id) 524 | 525 | 526 | get_personas 527 | _____________ 528 | 529 | Method to get specific personas 530 | 531 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/personas 532 | 533 | **Args**: 534 | 535 | *personas_id (str)*: The id of the personas 536 | 537 | **Response**: 538 | 539 | *dict*: the personas 540 | 541 | **Example**: 542 | 543 | .. code-block:: python 544 | 545 | from ampalibe import Messenger 546 | 547 | chat = Messenger() 548 | 549 | personas = chat.get_personas('123456789') 550 | 551 | print(personas) # {'name': 'Rivo Lalaina', 'profile_picture_url': 'https://avatars.githubusercontent.com/u/59861055?v=4', 'id': '123456789'} 552 | 553 | 554 | list_personas 555 | _______________ 556 | 557 | Method to get the list of personas 558 | 559 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/personas 560 | 561 | **Args**: 562 | 563 | **Response**: 564 | 565 | *list of dict*: list of personas 566 | 567 | **Example**: 568 | 569 | .. code-block:: python 570 | 571 | from ampalibe import Messenger 572 | 573 | chat = Messenger() 574 | 575 | list_personas = chat.list_personas() # return list of dict 576 | 577 | print(list_personas) # [{'name': 'Rivo Lalaina', 'profile_picture_url': 'https://avatars.githubusercontent.com/u/59861055?v=4', 'id': '123456789'}] 578 | 579 | 580 | delete_personas 581 | ________________ 582 | 583 | Method to delete personas 584 | 585 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/personas 586 | 587 | **Args**: 588 | 589 | *personas_id (str)*: The id of the personas to delete 590 | 591 | 592 | **Example**: 593 | 594 | .. code-block:: python 595 | 596 | from ampalibe import Messenger 597 | 598 | chat = Messenger() 599 | 600 | chat.delete_personas('123456789') 601 | 602 | 603 | get_user_profile 604 | _________________ 605 | 606 | Method to get specific personas 607 | 608 | **Ref**: https://developers.facebook.com/docs/messenger-platform/identity/user-profile 609 | 610 | **Args**: 611 | 612 | *dest_id (str)*: user id for destination 613 | 614 | *fields (str)*: list of field name that you need. Defaults to "first_name,last_name,profile_pic" 615 | 616 | **Response**: 617 | 618 | *dict*: user info 619 | 620 | **Example**: 621 | 622 | .. code-block:: python 623 | 624 | from ampalibe import Messenger 625 | 626 | chat = Messenger() 627 | 628 | user_info = chat.get_user_profile(sender_id) 629 | 630 | 631 | send_onetime_notification_request 632 | __________________________________ 633 | 634 | Method to send a one time notification request to the user 635 | 636 | **Ref**: https://developers.facebook.com/docs/messenger-platform/identity/one-time-notification 637 | 638 | **Args**: 639 | 640 | *dest_id (str)*: user id for destination 641 | 642 | *title (str)*: title of the notification , should be less than 65 characters 643 | 644 | *payload (str | Payload)*: payload of the notification 645 | 646 | 647 | .. code-block:: python 648 | 649 | chat.send_onetime_notification_request(sender_id, 'title', Payload('/test')) 650 | 651 | 652 | .. important :: 653 | 654 | You need one time notification permission for this message. 655 | To get that go to Advanced Messaging under your Page Settings to request the permission. 656 | 657 | And you must also subscribe to messaging_optins webhook 658 | 659 | 660 | If the user accept the notification, a *one_time_notif_token* will be sent to the webhook 661 | use the token to send notification to the user with one of those sends methods according to your needs : 662 | *send_text* 663 | *send_attachment* 664 | *send_action* 665 | *send_quick_reply* 666 | *send_generic_template* 667 | *send_file_url* 668 | *send_file* 669 | *send_media* 670 | *send_button* 671 | *send_receipt_template* 672 | 673 | **Example**: 674 | 675 | 676 | .. code-block:: python 677 | 678 | from ampalibe import Messenger, Model 679 | 680 | chat = Messenger() 681 | query = Model() 682 | 683 | @ampalibe.command('/') 684 | def main(sender_id, cmd, **ext): 685 | chat.send_onetime_notification_request(sender_id, "Notification", "/test") 686 | 687 | 688 | @ampalibe.command('/test') 689 | def test(sender_id, cmd, **ext): 690 | chat.send_text(sender_id, "You accepted to receive one notification later.") 691 | query.set_temp(sender_id, 'one_time_notif_token', cmd.token) 692 | 693 | 694 | .. code-block:: python 695 | 696 | from ampalibe import Messenger, Model 697 | 698 | chat = Messenger() 699 | query = Model() 700 | 701 | @ampalibe.command('/') 702 | def main(sender_id, cmd, **ext): 703 | """ 704 | To send a notification to the user, you need to use the token 705 | that you received in the webhook 706 | """ 707 | chat.send_text( 708 | sender_id, 709 | "Hello, notif for you even if had not answered me the last 24 hours" 710 | one_time_notif_token=query.get_temp(sender_id, 'one_time_notif_token') 711 | ) 712 | 713 | 714 | 715 | send_product_template 716 | ______________________ 717 | 718 | Method to send product template. 719 | 720 | The product template is a structured message that can be used to render products that have been uploaded to a catalog. Product details (image, title, price) will automatically be pulled from the product catalog. 721 | 722 | .. image:: https://raw.githubusercontent.com/iTeam-S/Ampalibe/main/docs/source/_static/product_template.png 723 | 724 | **Ref**: https://developers.facebook.com/docs/messenger-platform/send-messages/template/product 725 | 726 | **Args** 727 | 728 | *dest_id (str)*: user id for destination 729 | 730 | *product_ids (list of Product)*: List of product 731 | 732 | 733 | **Example**: 734 | 735 | .. code-block:: python 736 | 737 | from ampalibe import Messenger 738 | from ampalibe.ui import Product 739 | chat = Messenger() 740 | products = [ 741 | Product(p_id) 742 | for p_id in ['123456789', '987654321'] 743 | ] 744 | chat.send_product_template(sender_id, products) 745 | 746 | --------------------------------------------------------------------------------