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/.gitignore b/.gitignore index 894a44c..88c2420 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +*.log + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] @@ -14,7 +16,7 @@ dist/ downloads/ eggs/ .eggs/ -lib/ +lib/ lib64/ parts/ sdist/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3f71350 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3.11-alpine + +COPY src /app +WORKDIR /app +RUN pip install --trusted-host pypi.python.org -r requirements.txt +EXPOSE 5000 diff --git a/LICENSE b/LICENSE index f288702..0c9c7f8 100755 --- a/LICENSE +++ b/LICENSE @@ -5,7 +5,7 @@ Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. - Preamble + Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. diff --git a/README.md b/README.md index 905d6f3..e80ae7c 100755 --- a/README.md +++ b/README.md @@ -1,40 +1,135 @@ ![demopic](https://user-images.githubusercontent.com/38849824/160300853-ef6fe753-36d6-41a9-9bd2-8a06f7add71d.png) ![](https://img.shields.io/github/license/robswc/tradingview-webhooks-bot?style=for-the-badge) -![](https://img.shields.io/github/repo-size/robswc/tradingview-webhooks-bot?style=for-the-badge) ![](https://img.shields.io/github/commit-activity/y/robswc/tradingview-webhooks-bot?style=for-the-badge) ![](https://img.shields.io/twitter/follow/robswc?style=for-the-badge) +[tvwb_demo.webm](https://user-images.githubusercontent.com/38849824/192352217-0bd08426-98b7-4188-8e5b-67d7aa93ba09.webm) +### πŸ“€ Live Demo πŸ–₯ +**There's now a live demo available at [http://tvwb.robswc.me](http://tvwb.robswc.me). +Feel free to check it out or send it some webhooks!** +[![DigitalOcean Referral Badge](https://web-platforms.sfo2.cdn.digitaloceanspaces.com/WWW/Badge%201.svg)](https://www.digitalocean.com/?refcode=2865cad8f863&utm_campaign=Referral_Invite&utm_medium=Referral_Program&utm_source=badge) -# Tradingview-webhooks-bot +### [Get Support on Discord](https://discord.gg/wrjuSaZCFh) + + +# The What πŸ”¬ + +Tradingview-webhooks-bot (TVWB) is a small, Python-based framework that allows you to extend or implement your own logic +using data from [Tradingview's webhooks](https://www.tradingview.com/support/solutions/43000529348-about-webhooks/). TVWB is not a trading library, it's a framework for building your own trading logic. + +# The How πŸ— + +TVWB is fundamentally a set of components with a webapp serving as the GUI. TVWB was built with event-driven architecture in mind that provides you with the building blocks to extend or implement your own custom logic. +TVWB uses [Flask](https://flask.palletsprojects.com/en/2.2.x/) to handle the webhooks and provides you with a simple API to interact with the data. + +# Quickstart πŸ“˜ + +### Docker compose command + +```bash +docker-compose run app start +``` + +### Installation + +* [Docker](https://github.com/robswc/tradingview-webhooks-bot/wiki/Docker) (recommended) +* [Manual](https://github.com/robswc/tradingview-webhooks-bot/wiki/Installation) + +### Hosting + +* [Deploying](https://github.com/robswc/tradingview-webhooks-bot/wiki/Hosting) + * [Cloud](https://github.com/robswc/tradingview-webhooks-bot/wiki/Hosting#cloud-hosting) (recommended) + * [Local](https://github.com/robswc/tradingview-webhooks-bot/wiki/Hosting#using-a-personal-pc) -tradingview-webhooks-bot is a trading bot, written in python that allows users to place trades with tradingview's webhook alerts. --- +Ensure you're in the `src` directory. When running the following commands, **if you installed manually**. +**If you used docker**, +start the tvwb.py shell with `docker-compose run app shell` (in the project root directory) and omit the `python3 tvwb.py` portion of the commands. + +--- +### Creating an action + +```bash +python3 tvwb.py action:create NewAction --register +``` + +This creates an action and automatically registers it with the app. [Learn more on registering here](https://github.com/robswc/tradingview-webhooks-bot/wiki/Registering). + +_Note, action and event names should **_always_** be in PascalCase._ + +You can also check out some "pre-made" [community actions](https://github.com/robswc/tradingview-webhooks-bot/tree/master/src/components/actions/community_created_actions)! + +### Linking an action to an event + +```bash +python3 tvwb.py action:link NewAction WebhookReceived +``` + +This links an action to the `WebhookReceived` event. The `WebhookReceived` event is fired when a webhook is received by the app and is currently the only default event. + +### Editing an action + +Navigate to `src/components/actions/NewAction.py` and edit the `run` method. You will see something similar to the following code. +Feel free to delete the "Custom run method" comment and replace it with your own logic. Below is an example of how you can access +the webhook data. + +```python +class NewAction(Action): + def __init__(self): + super().__init__() + + def run(self, *args, **kwargs): + super().run(*args, **kwargs) # this is required + """ + Custom run method. Add your custom logic here. + """ + data = self.validate_data() # always get data from webhook by calling this method! + print('Data from webhook:', data) +``` + +### Running the app + +```bash +python3 tvwb.py start +``` + +### Sending a webhook + +Navigate to `http://localhost:5000`. Ensure you see the `WebhookReceived` Event. Click "details" to expand the event box. +Find the "Key" field and note the value. This is the key you will use to send a webhook to the app. Copy the JSON data below, +replacing "YOUR_KEY_HERE" with the key you copied. + +```json +{ + "key": "YOUR_KEY_HERE", + "message": "I'm a webhook!" +} +``` + +The `key` field is required, as it both authenticates the webhook and tells the app which event to fire. Besides that, you can +send any data you want. The data will be available to your action via the `validate_data()` method. (see above, editing action) + +On tradingview, create a new webhook with the above JSON data and send it to `http://ipaddr:5000/webhook`. You should see the data from the webhook printed to the console. -## Quickstart Using Pipenv +### FAQs -Pipenv is a tool that helps users set virtual environments and install dependencies with ease. There are many benefits to creating a virtual environment, especially for those that haev other projects running on the same server. +#### So how do I actually trade? -### Install pipenv and initiate virtual environment +To actually submit trades, you will have to use a library like [ccxt](https://github.com/ccxt/ccxt) for crypto currency. For other brokers, usually there are +SDKs or APIs available. The general workflow would look something like: webhook signal -> tvwb (use ccxt here) -> broker. Your trade submission would take place within the `run` method of a custom action. -1. Install pipenv `sudo apt install pipenv` -2. Once pipenv is installed, I recommend that you [get familiar with it](https://github.com/pypa/pipenv). -3. Navigate to the folder where you cloned the repo. You should see `Pipfile` and `Pipfile.lock` files. -4. Run command `pipenv install` -5. The dependencies required to get started should now be installed. Check by running command `pipenv graph` - You should see flask and ccxt. -6. If you want to install any other dependencies, or if you get an error that you're missing a depedency, simply use command `pipenv install ` -7. Starting the virtual environment: `pipenv shell` -8. Starting the flask app: `python webhook-bot.py` +#### The tvwb.py shell -There you go! Nice and simple Python version and virtualenv management. +You can use the `tvwb.py shell` command to open a python shell with the app context. This allows you to interact with the app without having to enter `python3 tvwb.py` every time. -### Using ngrok for webook data retrieval +#### Running Docker on Windows/Mac? -Many people are having difficulties with their server properly receiving webhook data from TradingView. The easiest way to get started quickly without ripping your hair out from trying to figure out what's wrong, [ngrok](https://ngrok.com/) can be used to receive the signals. Create a free account, unless you want your server to go down every 8 hours. Navigate to the downloads page, and select your download to match your machine. For example, I am on Ubuntu: `wget https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip` +Thanks to @khamarr3524 for pointing out there are some docker differences when running on Windows or Mac. I've added OS-specific `docker-compose.yml` files to accomodate these differences. One should be able to run their respective OS's `docker-compose.yml` file without issue now! -### Quick Start Guide +#### How do I get more help? -[Here is a quick start guide!](https://github.com/Robswc/tradingview-webhooks-bot/wiki/Quick-Start-Guide) Once everything is set up, you can use this guide to get started! +At the moment, the wiki is under construction. However, you may still find some good info on there. For additional assistance you can DM me on [Twitter](https://twitter.com/robswc) or join the [Discord](https://discord.gg/wrjuSaZCFh). I will try my best to get back to you! diff --git a/docker-compose.mac.yml b/docker-compose.mac.yml new file mode 100644 index 0000000..18b61f4 --- /dev/null +++ b/docker-compose.mac.yml @@ -0,0 +1,16 @@ +# For Macs, there is a default program that uses port 5000, so we need to change the port to 5001. + +version: '3' + +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ./src/components:/app/components + - ./src/settings.py:/app/settings.py + ports: + - "5001:5000" + network_mode: 'host' + entrypoint: python3 tvwb.py \ No newline at end of file diff --git a/docker-compose.windows.yml b/docker-compose.windows.yml new file mode 100644 index 0000000..42e192e --- /dev/null +++ b/docker-compose.windows.yml @@ -0,0 +1,16 @@ +# For windows, we need to use network_mode: 'bridge' instead of network_mode: 'host' + +version: '3' + +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ./src/components:/app/components + - ./src/settings.py:/app/settings.py + ports: + - "5000:5000" + network_mode: 'bridge' + entrypoint: python3 tvwb.py \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..51cdb90 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile + volumes: + - ./src/components:/app/components + - ./src/settings.py:/app/settings.py + - ./src/.gui_key:/app/.gui_key + ports: + - "5000:5000" + network_mode: "host" + entrypoint: python3 tvwb.py \ No newline at end of file diff --git a/requirements.txt b/requirements.txt deleted file mode 100755 index 5524100..0000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -click==7.1.2 -colorama==0.4.5 -Flask==2.0.3 -gunicorn==20.1.0 -itsdangerous==2.1.2 -Jinja2==3.1.1 -MarkupSafe==2.1.1 -shellingham==1.5.0 -typer==0.3.2 -typer-cli==0.0.12 -Werkzeug==2.0.3 diff --git a/src/commons.py b/src/commons.py index 2f3e807..f9c2146 100755 --- a/src/commons.py +++ b/src/commons.py @@ -20,7 +20,7 @@ with open('.key', 'r') as key_file: UNIQUE_KEY = key_file.read().strip() except FileNotFoundError: - UNIQUE_KEY = uuid.uuid4() + UNIQUE_KEY = str(uuid.uuid4()) with open('.key', 'w') as key_file: - key_file.write(str(UNIQUE_KEY)) + key_file.write(UNIQUE_KEY) key_file.close() diff --git a/src/components/actions/async_demo.py b/src/components/actions/async_demo.py new file mode 100644 index 0000000..5c21e17 --- /dev/null +++ b/src/components/actions/async_demo.py @@ -0,0 +1,19 @@ +from time import sleep + +from components.actions.base.action import Action + + +class AsyncDemo(Action): + def __init__(self): + super().__init__() + + def run(self, *args, **kwargs): + super().run(*args, **kwargs) # this is required + """ + Custom run method. Add your custom logic here. + """ + print(self.name, '---> action has started...') + for i in range(5): + print(f'{self.name} ---> {i}') + sleep(1) + print(self.name, '---> action has completed!') diff --git a/src/components/actions/community_created_actions/crypto/binance_spot.py b/src/components/actions/community_created_actions/crypto/binance_spot.py new file mode 100644 index 0000000..e820be1 --- /dev/null +++ b/src/components/actions/community_created_actions/crypto/binance_spot.py @@ -0,0 +1,54 @@ +from components.actions.base.action import Action +import ccxt as ccxt + +class BinanceSpot(Action): + #Add your API_KEY from Binance Testnet or Mainnet + API_KEY = '' + #Add your API_SECRET from Binance Testnet or Mainnet + API_SECRET = '' + + exchange = ccxt.binance({ + 'rateLimit': 2000, + 'enableRateLimit': True, + 'apiKey': API_KEY, + 'secret': API_SECRET, + 'id': 'binance', + }) + + def __init__(self): + super().__init__() + #uncomment the below line to use sandbox/testnet api + # exchange = self.exchange.set_sandbox_mode(True) + + def place_order(self, symbol, side, price=None): + try: + + # Get the balance of the base currency + balance = self.exchange.fetch_balance() + if side == 'buy': + base_balance = balance['free'][symbol[-4:]] + elif side == 'sell': + base_balance = balance['free'][symbol[:-4]] + + # Calculate the amount of asset to buy or sell + if side == 'buy': + amount = base_balance / price + elif side == 'sell': + amount = base_balance + + markets = self.exchange.load_markets() + formatted_amount = self.exchange.amount_to_precision(symbol, amount) + order = self.exchange.create_market_order(symbol, side, quoteOrderQty=formatted_amount) + print(order) + # Print the order details + except ccxt.BaseError as e: + # Handle the exception + print("An error occurred while placing the order:", e) + except ValueError as e: + # Handle the exception + print("An error occurred while checking the filters or calculating the amount:", e) + + def run(self, *args, **kwargs): + super().run(*args, **kwargs) # this is required + data = self.validate_data() + self.place_order(symbol=data['symbol'], side=data['side']) diff --git a/src/components/events/base/event.py b/src/components/events/base/event.py index c67c459..fc84dbe 100755 --- a/src/components/events/base/event.py +++ b/src/components/events/base/event.py @@ -42,6 +42,7 @@ class Event: def __init__(self): self.name = self.get_name() + self.active = True self.webhook = True # all events are webhooks by default self.key = f'{self.name}:{md5(f"{self.name + UNIQUE_KEY}".encode()).hexdigest()[:6]}' self._actions = [] @@ -70,15 +71,17 @@ def register_action(self, action): self._actions.append(action) def trigger(self, *args, **kwargs): - # handle logging - logger.info(f'EVENT TRIGGERED --->\t{str(self)}') - log_event = LogEvent(self.name, 'triggered', datetime.now(), f'{self.name} was triggered') - log_event.write() - - # pass data - data = kwargs.get('data') - - self.logs.append(log_event) - for action in self._actions: - action.set_data(data) - action.run() + if self.active: + logger.info(f'EVENT TRIGGERED --->\t{str(self)}') + log_event = LogEvent(self.name, 'triggered', datetime.now(), f'{self.name} was triggered') + log_event.write() + + # pass data + data = kwargs.get('data') + + self.logs.append(log_event) + for action in self._actions: + action.set_data(data) + action.run() + else: + logger.info(f'EVENT NOT TRIGGERED (event is inactive) --->\t{str(self)}') diff --git a/src/components/events/webhook_received.py b/src/components/events/webhook_received.py new file mode 100755 index 0000000..61cb5ff --- /dev/null +++ b/src/components/events/webhook_received.py @@ -0,0 +1,6 @@ +from components.events.base.event import Event + + +class WebhookReceived(Event): + def __init__(self): + super().__init__() diff --git a/src/main.py b/src/main.py index 6acce63..b1d87a9 100755 --- a/src/main.py +++ b/src/main.py @@ -32,6 +32,22 @@ @app.route("/", methods=["GET"]) def dashboard(): if request.method == 'GET': + + # check if gui key file exists + try: + with open('.gui_key', 'r') as key_file: + gui_key = key_file.read().strip() + # check that the gui key from file matches the gui key from request + if gui_key == request.args.get('guiKey', None): + pass + else: + return 'Access Denied', 401 + + # if gui key file does not exist, the tvwb.py did not start gui in closed mode + except FileNotFoundError: + logger.warning('GUI key file not found. Open GUI mode detected.') + + # serve the dashboard action_list = am.get_all() return render_template( template_name_or_list='dashboard.html', @@ -43,14 +59,21 @@ def dashboard(): @app.route("/webhook", methods=["POST"]) -def webhook(): +async def webhook(): if request.method == 'POST': - logger.info(f'Request Data: {request.get_json()}') + data = request.get_json() + if data is None: + logger.error(f'Error getting JSON data from request...') + logger.error(f'Request data: {request.data}') + logger.error(f'Request headers: {request.headers}') + return 'Error getting JSON data from request', 400 + + logger.info(f'Request Data: {data}') triggered_events = [] for event in em.get_all(): if event.webhook: - if event.key == request.get_json()['key']: - event.trigger(data=request.get_json()) + if event.key == data['key']: + event.trigger(data=data) triggered_events.append(event.name) if not triggered_events: @@ -69,5 +92,25 @@ def get_logs(): return jsonify([log.as_json() for log in logs]) +@app.route("/event/active", methods=["POST"]) +def activate_event(): + if request.method == 'POST': + # get query parameters + event_name = request.args.get('event', None) + + # if event name is not provided, or cannot be found, 404 + if event_name is None: + return Response(f'Event name cannot be empty ({event_name})', status=404) + try: + event = em.get(event_name) + except ValueError: + return Response(f'Cannot find event with name: {event_name}', status=404) + + # set event to active or inactive, depending on current state + event.active = request.args.get('active', True) == 'true' + logger.info(f'Event {event.name} active set to: {event.active}, via POST request') + return {'active': event.active} + + if __name__ == '__main__': app.run(debug=True) diff --git a/src/requirements.txt b/src/requirements.txt old mode 100755 new mode 100644 index 5524100..92ae343 --- a/src/requirements.txt +++ b/src/requirements.txt @@ -1,11 +1,21 @@ -click==7.1.2 -colorama==0.4.5 -Flask==2.0.3 +asgiref==3.7.2 +blinker==1.6.2 +click==8.1.3 +colorama==0.4.6 +Flask==3.0.2 +Flask[async] gunicorn==20.1.0 +importlib-metadata==6.6.0 +iniconfig==2.0.0 itsdangerous==2.1.2 -Jinja2==3.1.1 -MarkupSafe==2.1.1 -shellingham==1.5.0 -typer==0.3.2 -typer-cli==0.0.12 -Werkzeug==2.0.3 +Jinja2==3.1.2 +MarkupSafe==2.1.2 +packaging==23.2 +pluggy==1.4.0 +pytest==8.0.2 +shellingham==1.4.0 +typer==0.7.0 +typer-cli==0.0.13 +typing_extensions==4.5.0 +Werkzeug==3.0.1 +zipp==3.15.0 diff --git a/src/settings.py b/src/settings.py index 46d067d..854791e 100755 --- a/src/settings.py +++ b/src/settings.py @@ -1,9 +1,9 @@ # actions -REGISTERED_ACTIONS = ['PrintData'] +REGISTERED_ACTIONS = ['PrintData', 'AsyncDemo'] # events REGISTERED_EVENTS = ['WebhookReceived'] # links -REGISTERED_LINKS = [('PrintData', 'WebhookReceived')] +REGISTERED_LINKS = [('PrintData', 'WebhookReceived'), ('AsyncDemo', 'WebhookReceived')] diff --git a/src/templates/dashboard.html b/src/templates/dashboard.html index d68b84c..bc315e0 100755 --- a/src/templates/dashboard.html +++ b/src/templates/dashboard.html @@ -117,7 +117,10 @@

{% endfor %}
-
Key
+
+
Key
+ (entire string) +
{{ event.key }}
-
- +
+
+ + +
+
@@ -216,7 +226,33 @@

Monitoring

- + > var data = [ { diff --git a/src/tests/test_tvwb.py b/src/tests/test_tvwb.py index 981e799..47d7f26 100755 --- a/src/tests/test_tvwb.py +++ b/src/tests/test_tvwb.py @@ -3,26 +3,31 @@ class TestCLI(TestCase): - # change dir before running tests - - os.chdir('..') - - def test_newevent(self): - from tvwb import newevent - assert newevent(name='TestEvent') - self.assertRaises(ValueError, newevent, name='!') - self.assertRaises(ValueError, newevent, name='@') - self.assertRaises(ValueError, newevent, name='#') - self.assertRaises(ValueError, newevent, name='test event') - self.assertRaises(ValueError, newevent, name='test-event') - self.assertRaises(ValueError, newevent, name='test_event') - - def test_newaction(self): - from tvwb import newaction - assert newaction(name='TestAction') - self.assertRaises(ValueError, newaction, name='!') - self.assertRaises(ValueError, newaction, name='@') - self.assertRaises(ValueError, newaction, name='#') - self.assertRaises(ValueError, newaction, name='test action') - self.assertRaises(ValueError, newaction, name='test-action') - self.assertRaises(ValueError, newaction, name='test_action') + + def setUp(self) -> None: + self.prefix = 'python tvwb.py' + self.initial_settings = open('settings.py', 'r').read() + + def test_create_and_register_action(self): + + # create action + cmd = f'{self.prefix} action:create CustomAction --no-register' + try: + os.system(cmd) + except Exception as e: + self.fail(e) + + # register action + cmd = f'{self.prefix} action:register CustomAction' + try: + os.system(cmd) + except Exception as e: + self.fail(e) + + def tearDown(self) -> None: + os.remove('components/actions/custom_action.py') + + # restore settings.py + with open('settings.py', 'w') as settings_file: + settings_file.write(self.initial_settings) + settings_file.close() diff --git a/src/tvwb.py b/src/tvwb.py index be5cbe8..7cf5562 100755 --- a/src/tvwb.py +++ b/src/tvwb.py @@ -1,9 +1,9 @@ -from logging import getLogger, DEBUG +import json +import os +from subprocess import run import typer -from subprocess import run -from components.events.base.event import em from utils.copy_template import copy_from_template from utils.formatting import snake_case from utils.log import get_logger @@ -16,9 +16,65 @@ logger = get_logger(__name__) -@app.command() -def start(): - run('gunicorn --bind 0.0.0.0:5000 wsgi:app'.split(' ')) +@app.command('start') +def start( + open_gui: bool = typer.Option( + default=False, + help='Determines whether the GUI should be served at the root path, or behind a unique key.', + ), + host: str = typer.Option( + default='0.0.0.0' + ), + port: int = typer.Option( + default=5000 + ), + workers: int = typer.Option( + default=1, + help='Number of workers to run the server with.', + ) +): + def clear_gui_key(): + try: + os.remove('.gui_key') + except FileNotFoundError: + pass + + def generate_gui_key(): + import secrets + if os.path.exists('.gui_key'): + pass + else: + open('.gui_key', 'w').write(secrets.token_urlsafe(24)) + + def read_gui_key(): + return open('.gui_key', 'r').read() + + def print_gui_info(): + if open_gui: + print('GUI is set to [OPEN] - it will be served at the root path.') + print(f'\n\tView GUI dashboard here: http://{host}:{port}\n') + else: + print('GUI is set to [CLOSED] - it will be served at the path /?guiKey=') + print(f'\n\tView GUI dashboard here: http://{host}:{port}?guiKey={read_gui_key()}\n') + print( + 'To run the GUI in [OPEN] mode (for development purposes only), run the following command: tvwb start --open-gui') + gui_modes_url = 'https://github.com/robswc/tradingview-webhooks-bot/discussions/43' + print(f'To learn more about GUI modes, visit: {gui_modes_url}') + + def run_server(): + print("Close server with Ctrl+C in terminal.") + run(f'gunicorn --bind {host}:{port} wsgi:app --workers {workers}'.split(' ')) + + # clear gui key if gui is set to open, else generate key + # Flask uses the existence of the key file to determine GUI mode + if open_gui: + clear_gui_key() + else: + generate_gui_key() + + # print info regarding GUI and run the server + print_gui_info() + run_server() @app.command('action:create') @@ -145,16 +201,20 @@ def trigger_event(name: str): logger.info(f'Triggering event --->\t{name}') # import event event = getattr(__import__(f'components.events.{snake_case(name)}', fromlist=['']), name)() - event.trigger_actions() + event.trigger({}) return True -@app.command('shell') -def shell(): - cmd = '--help' - while cmd not in ['exit', 'quit', 'q']: - run(f'python3 tvwb.py {cmd}'.split(' ')) - cmd = typer.prompt("Enter TVWB command (q) to exit") +@app.command('util:send-webhook') +def send_webhook(key: str): + logger.info(f'Sending webhook') + post_data = json.dumps({ + "test": "data", + "key": key}) + # send with curl + run(['curl', '-X', 'POST', '-H', 'Content-Type: application/json', '-d', post_data, + 'http://localhost:5000/webhook']) + if __name__ == "__main__": app()

AltStyle γ«γ‚ˆγ£γ¦ε€‰ζ›γ•γ‚ŒγŸγƒšγƒΌγ‚Έ (->γ‚ͺγƒͺγ‚ΈγƒŠγƒ«) /