commit 903229cccb1ad06313fb0b0d645d3693b95ce441 Author: Andrew Williams Date: Wed Jun 8 15:51:29 2022 +0100 Initial import diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..dd84ea7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,38 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..1ded03d --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} \ No newline at end of file diff --git a/.github/workflows/build-container.yaml b/.github/workflows/build-container.yaml new file mode 100644 index 0000000..ae164a9 --- /dev/null +++ b/.github/workflows/build-container.yaml @@ -0,0 +1,32 @@ +name: Build Container + +"on": + push: + branches: + - main + tags: + - "[0-9]+.[0-9]+.[0-9]+" + +jobs: + docker: + runs-on: ubuntu-latest + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to GHCR + uses: docker/login-action@v1 + if: github.event_name != 'pull_request' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push + id: docker_build + uses: docker/build-push-action@v2 + with: + push: true + tags: | + ghcr.io/${{ github.repository_owner }}/ohayodash:${{ github.ref_name }} + ghcr.io/${{ github.repository_owner }}/ohayodash:latest diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml new file mode 100644 index 0000000..66dd3d6 --- /dev/null +++ b/.github/workflows/lint.yaml @@ -0,0 +1,21 @@ +name: Lint + +'on': + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + run-linters: + name: Run linters + runs-on: ubuntu-latest + + steps: + - name: Check out Git repository + uses: actions/checkout@v2 + + - name: wemake-python-styleguide + uses: wemake-services/wemake-python-styleguide@0.16.0 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..420fec9 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,27 @@ +--- +name: Release +on: + push: + tags: + - "[0-9]+.[0-9]+.[0-9]+" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + - run: pip install -r requirements-dev.txt + + - name: Build Assets + run: python setup.py sdist bdist_wheel + + - name: Release + uses: softprops/action-gh-release@v1 + with: + name: "Version ${{ github.ref_name }}" + generate_release_notes: true + files: | + dist/* \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..894a44c --- /dev/null +++ b/.gitignore @@ -0,0 +1,104 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..95db8ac --- /dev/null +++ b/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.8-alpine +WORKDIR /app +COPY . /app +RUN pip install -r requirements.txt +ENTRYPOINT python smsbot.py \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4e37b7c --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2019 Five B +Copyright (c) 2022 Andrew Williams + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..704185e --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# SMSBot + +A simple Telegram bot fo receive SMS messages. + +Forked from [FiveBoroughs/Twilio2Telegram](https://github.com/FiveBoroughs/Twilio2Telegram). + +## Functionality + +This simple tool acts as a webhook receiver for the Twilio API, taking messages sent over and posting them on Telegram to a target chat or channel. This is useful for forwarding on 2FA messages, notification text messages for services that don't support international numbers (**cough** Disney, of all people). + +The bot is designed to run within a Kubernetes environment, but can be operated as a individual container, or as a hand ran service. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..e0f2822 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +flask +waitress +twilio +python-telegram-bot \ No newline at end of file diff --git a/smsbot.py b/smsbot.py new file mode 100644 index 0000000..20a812d --- /dev/null +++ b/smsbot.py @@ -0,0 +1,147 @@ +try: + import os + import logging + from functools import wraps + from flask import Flask, request, abort + from waitress import serve + from twilio.request_validator import RequestValidator + from telegram import ParseMode + from telegram.ext import Updater, CommandHandler +except ImportError as err: + print(f"Failed to import required modules: {err}") + +# FB - Enable logging +logging.basicConfig( + format='%(asctime)s - %(levelname)s - %(message)s', level=logging.INFO) +logger = logging.getLogger(__name__) + +webhook_listener = Flask(__name__) + + +def validate_twilio_request(f): + """Validates that incoming requests genuinely originated from Twilio""" + @wraps(f) + def decorated_function(*args, **kwargs): + # FB - Create an instance of the RequestValidator class + validator = RequestValidator(os.environ.get('TWILIO_AUTH_TOKEN')) + + # FB - Validate the request using its URL, POST data, and X-TWILIO-SIGNATURE header + request_valid = validator.validate( + request.url, + request.form, + request.headers.get('X-TWILIO-SIGNATURE', '')) + + # FB - Continue processing the request if it's valid, return a 403 error if it's not + if request_valid: + return f(*args, **kwargs) + else: + logger.error('Invalid twilio request, aborting') + return abort(403) + return decorated_function + + +def tg_help_handler(update, context): + """Send a message when the command /help is issued.""" + logger.info('/help command received in chat: %s', update.message.chat) + update.message.reply_markdown( + 'Find out more on [Github](https://github.com/FiveBoroughs/Twilio2Telegram)') + + +def tg_error_handler(update, context): + """Log Errors caused by Updates.""" + logger.warning('Update "%s" caused error "%s"', update, context.error) + + +def tg_bot_start(): + """Start the telegram bot.""" + # FB - Create the Updater + updater = Updater(os.environ.get('TELEGRAM_BOT_TOKEN'), use_context=True) + + # FB - Get the dispatcher to register handlers + dispatcher = updater.dispatcher + + # FB - on /help command + dispatcher.add_handler(CommandHandler("help", tg_help_handler)) + + # FB - log all errors + dispatcher.add_error_handler(tg_error_handler) + + # FB - Start the Bot + updater.start_polling() + + return updater.bot + + +def tg_send_owner_message(message): + """Send telegram message to owner.""" + telegram_bot.sendMessage(text=message, chat_id=os.environ.get('TELEGRAM_OWNER'), parse_mode=ParseMode.MARKDOWN) + + +def tg_send_subscribers_message(message): + """Send telegram messages to subscribers.""" + for telegram_destination in os.environ.get('TELEGRAM_SUBSCRIBERS').split(','): + telegram_bot.sendMessage( + text=message, chat_id=telegram_destination, parse_mode=ParseMode.MARKDOWN) + + +@webhook_listener.route('/', methods=['GET']) +def index(): + """Upon call of homepage.""" + logger.info('"/" reached, IP: %s', request.remote_addr) + + return webhook_listener.send_static_file('Index.html') + + +@webhook_listener.route('/message', methods=['POST']) +@validate_twilio_request +def recv_message(): + """Upon reception of a SMS.""" + logger.info(' "/message" reached, IP: %s', request.remote_addr) + # FB - Format telegram Message + telegram_message = 'Text from `{From}` ({Country}, {State}) :``` {Body}```'.format( + From=request.values.get('From', 'unknown'), + Country=request.values.get('FromCountry', 'unknown'), + State=request.values.get('FromState', 'unknown'), + Body=request.values.get('Body', 'unknown') + ) + + logger.info(telegram_message) + # FB - Send telegram alerts + tg_send_owner_message('Twilio ID : `{Id}`\n'.format( + Id=request.values.get('MessageSid', 'unknown')) + telegram_message) + tg_send_subscribers_message(telegram_message) + + # FB - return empty response to avoid further Twilio fees + return '' + + + +@webhook_listener.route('/call', methods=['POST']) +@validate_twilio_request +def recv_call(): + """Upon reception of a call.""" + logger.info(' "/call" reached, IP: %s', request.remote_addr) + # FB - Format telegram Message + telegram_message = 'Call from `{From}` ({Country}, {State}) :``` {Status}```'.format( + From=request.values.get('From', 'unknown'), + Country=request.values.get('FromCountry', 'unknown'), + State=request.values.get('FromState', 'unknown'), + Status=request.values.get('CallStatus', 'unknown') + ) + + logger.info(telegram_message) + # FB - Send telegram alerts + tg_send_owner_message('Twilio ID : `{Id}`\n'.format( + Id=request.values.get('CallSid', 'unknown')) + telegram_message) + tg_send_subscribers_message(telegram_message) + + # FB - reject the call without being billed + return '' + + +if __name__ == "__main__": + logger.info('Starting bot') + # FB - Start the telegram bot + telegram_bot = tg_bot_start() + # FB - Start the website + serve(webhook_listener, host='0.0.0.0', port=80)