├── T3SF ├── __init__.py ├── gui │ ├── __init__.py │ ├── t3sf.sqlite3 │ ├── static │ │ ├── imgs │ │ │ ├── icon.png │ │ │ ├── logo-dark.png │ │ │ └── logo-light.png │ │ └── js │ │ │ ├── vanilla-jsoneditor │ │ │ ├── SECURITY.md │ │ │ ├── LICENSE.md │ │ │ ├── package.json │ │ │ ├── themes │ │ │ │ ├── jse-theme-dark.css │ │ │ │ └── jse-theme-default.css │ │ │ └── README.md │ │ │ ├── copy.js │ │ │ ├── bootstrap_toasts.js │ │ │ └── theme_switcher.js │ └── templates │ │ ├── env_creation.html │ │ └── index.html ├── logger │ ├── __init__.py │ └── logger.py ├── slack │ ├── __init__.py │ ├── bot.py │ └── slack.py ├── discord │ ├── __init__.py │ ├── bot.py │ └── discord.py ├── utils.py └── T3SF.py ├── examples ├── Discord │ ├── .env │ ├── gui_view.png │ ├── injects_arrived.png │ ├── main.py │ └── README.md ├── Telegram │ ├── .env │ ├── injects_arrived.png │ ├── requirements.txt │ ├── README.md │ └── bot.py ├── Slack │ ├── .env │ ├── gui_view.png │ ├── injects_arrived.png │ ├── main.py │ ├── README.md │ └── bot_manifest.yml ├── MSEL.xlsx ├── WhatsApp │ ├── injects_arrived.png │ ├── requirements.txt │ ├── README.md │ └── bot.py ├── main.py ├── README.md └── MSEL_EXAMPLE.json ├── docs ├── requirements.txt ├── images │ ├── logo.png │ ├── schema.jpeg │ └── gui │ │ ├── index.png │ │ ├── env_creation.png │ │ └── msel_playground.png ├── .readthedocs.yaml ├── conf.py ├── T3SF.Logger.rst ├── index.rst ├── WhatsApp.rst ├── Telegram.rst ├── T3SF.Installation.rst ├── T3SF.CoreFunctions.rst ├── T3SF.Usage.rst ├── T3SF.Handlers.rst ├── Discord.rst └── Slack.rst ├── docker ├── main-slack.py └── main-discord.py ├── TODO.md ├── dockerfile-slack ├── dockerfile-discord ├── .github ├── workflows │ ├── publish_slack.yml │ └── publish_discord.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── CONTRIBUTING.md ├── README.md └── CHANGELOG.md /T3SF/__init__.py: -------------------------------------------------------------------------------- 1 | from .T3SF import * -------------------------------------------------------------------------------- /T3SF/gui/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import * -------------------------------------------------------------------------------- /T3SF/logger/__init__.py: -------------------------------------------------------------------------------- 1 | from .logger import * -------------------------------------------------------------------------------- /examples/Discord/.env: -------------------------------------------------------------------------------- 1 | DISCORD_TOKEN=XXXXXXXXX -------------------------------------------------------------------------------- /examples/Telegram/.env: -------------------------------------------------------------------------------- 1 | TELEGRAM_TOKEN=XXXXXXXXX -------------------------------------------------------------------------------- /T3SF/slack/__init__.py: -------------------------------------------------------------------------------- 1 | from .slack import Slack 2 | from .bot import * -------------------------------------------------------------------------------- /T3SF/discord/__init__.py: -------------------------------------------------------------------------------- 1 | from .discord import Discord 2 | from .bot import * -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | myst_parser 2 | Sphinx 3 | sphinx-rtd-theme 4 | sphinx-toolbox -------------------------------------------------------------------------------- /examples/Slack/.env: -------------------------------------------------------------------------------- 1 | SLACK_BOT_TOKEN=xoxb-XXXXXX 2 | 3 | SLACK_APP_TOKEN=xapp-XXXXXX -------------------------------------------------------------------------------- /examples/MSEL.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/examples/MSEL.xlsx -------------------------------------------------------------------------------- /T3SF/gui/t3sf.sqlite3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/T3SF/gui/t3sf.sqlite3 -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/docs/images/logo.png -------------------------------------------------------------------------------- /docs/images/schema.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/docs/images/schema.jpeg -------------------------------------------------------------------------------- /docs/images/gui/index.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/docs/images/gui/index.png -------------------------------------------------------------------------------- /examples/Slack/gui_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/examples/Slack/gui_view.png -------------------------------------------------------------------------------- /T3SF/gui/static/imgs/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/T3SF/gui/static/imgs/icon.png -------------------------------------------------------------------------------- /examples/Discord/gui_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/examples/Discord/gui_view.png -------------------------------------------------------------------------------- /docs/images/gui/env_creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/docs/images/gui/env_creation.png -------------------------------------------------------------------------------- /T3SF/gui/static/imgs/logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/T3SF/gui/static/imgs/logo-dark.png -------------------------------------------------------------------------------- /examples/Slack/injects_arrived.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/examples/Slack/injects_arrived.png -------------------------------------------------------------------------------- /T3SF/gui/static/imgs/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/T3SF/gui/static/imgs/logo-light.png -------------------------------------------------------------------------------- /docs/images/gui/msel_playground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/docs/images/gui/msel_playground.png -------------------------------------------------------------------------------- /examples/Discord/injects_arrived.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/examples/Discord/injects_arrived.png -------------------------------------------------------------------------------- /examples/Telegram/injects_arrived.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/examples/Telegram/injects_arrived.png -------------------------------------------------------------------------------- /examples/WhatsApp/injects_arrived.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Base4Security/T3SF/HEAD/examples/WhatsApp/injects_arrived.png -------------------------------------------------------------------------------- /examples/Slack/main.py: -------------------------------------------------------------------------------- 1 | from T3SF import T3SF 2 | import asyncio 3 | 4 | async def main(): 5 | await T3SF.start(MSEL="../MSEL_EXAMPLE.json", platform="Slack", gui=True) 6 | 7 | if __name__ == '__main__': 8 | asyncio.run(main()) -------------------------------------------------------------------------------- /examples/Discord/main.py: -------------------------------------------------------------------------------- 1 | from T3SF import T3SF 2 | import asyncio 3 | 4 | async def main(): 5 | await T3SF.start(MSEL="../MSEL_EXAMPLE.json", platform="Discord", gui=True) 6 | 7 | if __name__ == '__main__': 8 | asyncio.run(main()) -------------------------------------------------------------------------------- /T3SF/gui/static/js/vanilla-jsoneditor/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report (suspected) security vulnerabilities privately to one of the maintainers of the library, for example to Jos de Jong: https://github.com/josdejong. 6 | -------------------------------------------------------------------------------- /docker/main-slack.py: -------------------------------------------------------------------------------- 1 | from T3SF import T3SF 2 | import asyncio, os 3 | from dotenv import load_dotenv 4 | load_dotenv() 5 | 6 | MSEL_PATH = os.environ['MSEL_PATH'] 7 | 8 | async def main(): 9 | await T3SF.start(MSEL=MSEL_PATH, platform="slack", gui=True) 10 | 11 | if __name__ == '__main__': 12 | asyncio.run(main()) -------------------------------------------------------------------------------- /docker/main-discord.py: -------------------------------------------------------------------------------- 1 | from T3SF import T3SF 2 | import asyncio, os 3 | from dotenv import load_dotenv 4 | load_dotenv() 5 | 6 | MSEL_PATH = os.environ['MSEL_PATH'] 7 | 8 | async def main(): 9 | await T3SF.start(MSEL=MSEL_PATH, platform="discord", gui=True) 10 | 11 | if __name__ == '__main__': 12 | asyncio.run(main()) -------------------------------------------------------------------------------- /examples/Telegram/requirements.txt: -------------------------------------------------------------------------------- 1 | aiogram==2.22.2 2 | aiohttp==3.8.3 3 | aiosignal==1.2.0 4 | async-timeout==4.0.2 5 | attrs==22.1.0 6 | Babel==2.9.1 7 | certifi==2022.9.24 8 | charset-normalizer==2.1.1 9 | frozenlist==1.3.1 10 | idna==3.4 11 | multidict==6.0.2 12 | python-dotenv==0.21.0 13 | pytz==2022.5 14 | yarl==1.8.1 15 | t3sf==1.1 -------------------------------------------------------------------------------- /examples/main.py: -------------------------------------------------------------------------------- 1 | from T3SF import T3SF 2 | import asyncio 3 | 4 | MSEL = "path/to/your/MSEL.json" # Indicate where's your MSEL stored. 5 | 6 | platform = "Slack" or "Discord" # Choose your platform 7 | 8 | gui = True or False # Do you want a GUI? 9 | 10 | async def main(): 11 | await T3SF.start(MSEL=MSEL, platform=platform, gui=gui) 12 | 13 | if __name__ == '__main__': 14 | asyncio.run(main()) -------------------------------------------------------------------------------- /examples/WhatsApp/requirements.txt: -------------------------------------------------------------------------------- 1 | async-generator==1.10 2 | attrs==21.4.0 3 | beautifulsoup4==4.10.0 4 | bs4==0.0.1 5 | certifi==2021.10.8 6 | cffi==1.15.0 7 | cryptography==36.0.2 8 | h11==0.13.0 9 | idna==3.3 10 | outcome==1.1.0 11 | Pillow==9.1.0 12 | pycparser==2.21 13 | pyOpenSSL==22.0.0 14 | PySocks==1.7.1 15 | selenium==4.1.3 16 | sniffio==1.2.0 17 | sortedcontainers==2.4.0 18 | soupsieve==2.3.1 19 | trio==0.20.0 20 | trio-websocket==0.9.2 21 | urllib3==1.26.9 22 | wsproto==1.1.0 23 | WhaBot==1.1.1 24 | t3sf==1.0 -------------------------------------------------------------------------------- /docs/.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-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | 19 | # Optionally declare the Python requirements required to build your docs 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | import re 4 | import sphinx_rtd_theme 5 | from sphinx.locale import _ 6 | 7 | 8 | project = u'T3SF' 9 | slug = re.sub(r'\W+', '-', project.lower()) 10 | version = "2.5" 11 | release = "2.5" 12 | language = 'en' 13 | 14 | extensions = ['sphinx_rtd_theme', 'sphinx_toolbox.confval',] 15 | 16 | html_theme = "sphinx_rtd_theme" 17 | html_theme_options = { 18 | 'logo_only': True, 19 | 'navigation_depth': 5, 20 | 'style_nav_header_background': 'white', 21 | } 22 | 23 | html_logo = "images/logo.png" 24 | html_show_sourcelink = True 25 | html_show_copyright = False -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ### TODO 2 | 3 | --- 4 | 5 | - [x] Adding and formating the README.md file. 6 | - [ ] Add support to other platforms (Teams, Signal) 7 | - [x] Add buttons inside the injects for analytics. 8 | - [x] Stop/Resume/Pause Buttons for game masters [Slack/Telegram/Discord]. 9 | - [x] Multi company messages sender. 10 | - [x] Send information for debug purposes to #log channels. PD: Modified the log channel to the GUI IRT logs. 11 | - [ ] Option to fetch injects from online resources. 12 | - [ ] Upgrade the Telegram and WhatsApp's modules to work with the newest update. 13 | - [x] Upgrade the MSEL Viewer to a MSEL Editor -------------------------------------------------------------------------------- /dockerfile-slack: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as the base image 2 | FROM python:3.9-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Install the required dependencies 8 | # RUN pip3 install "T3SF[Slack]" 9 | 10 | RUN pip3 install "flask[async]" slack_bolt python-dotenv 11 | 12 | # Copy the main.py script to the container 13 | COPY ./docker/main-slack.py main.py 14 | 15 | # Copy the framework to the container 16 | COPY ./T3SF/ T3SF/ 17 | 18 | # Set the MSEL default location 19 | ENV MSEL_PATH=/app/MSEL.json 20 | 21 | STOPSIGNAL SIGINT 22 | 23 | # Run the main.py script 24 | ENTRYPOINT ["python", "main.py"] -------------------------------------------------------------------------------- /dockerfile-discord: -------------------------------------------------------------------------------- 1 | # Use an official Python runtime as the base image 2 | FROM python:3.9-slim 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Install the required dependencies 8 | # RUN pip3 install "T3SF[Discord]" 9 | 10 | RUN pip3 install "flask[async]" discord.py python-dotenv 11 | 12 | # Copy the main.py script to the container 13 | COPY ./docker/main-discord.py main.py 14 | 15 | # Copy the framework to the container 16 | COPY ./T3SF/ T3SF/ 17 | 18 | # Set the MSEL default location 19 | ENV MSEL_PATH=/app/MSEL.json 20 | 21 | STOPSIGNAL SIGINT 22 | 23 | # Run the main.py script 24 | ENTRYPOINT ["python", "main.py"] -------------------------------------------------------------------------------- /.github/workflows/publish_slack.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Image for Slack 2 | 3 | on: 4 | push: 5 | branches: 6 | [ "main" ] 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v2 14 | 15 | - name: Login to Docker Hub 16 | uses: docker/login-action@v1 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | 21 | - name: Build and Publish Docker Image 22 | uses: docker/build-push-action@v2 23 | with: 24 | context: . 25 | file: dockerfile-slack 26 | push: true 27 | tags: base4sec/t3sf:slack 28 | -------------------------------------------------------------------------------- /T3SF/gui/static/js/vanilla-jsoneditor/LICENSE.md: -------------------------------------------------------------------------------- 1 | The ISC License 2 | 3 | Copyright (c) 2020-2023 by Jos de Jong 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 8 | -------------------------------------------------------------------------------- /.github/workflows/publish_discord.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker Image for Discord 2 | 3 | on: 4 | push: 5 | branches: 6 | [ "main" ] 7 | 8 | jobs: 9 | build-and-publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v3 14 | 15 | - name: Login to Docker Hub 16 | uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a 17 | with: 18 | username: ${{ secrets.DOCKER_USERNAME }} 19 | password: ${{ secrets.DOCKER_PASSWORD }} 20 | 21 | - name: Build and Publish Docker Image 22 | uses: docker/build-push-action@v4 23 | with: 24 | context: . 25 | file: dockerfile-discord 26 | push: true 27 | tags: base4sec/t3sf:discord 28 | -------------------------------------------------------------------------------- /.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: lanfranB4 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 | 1. Run this bot '...' 16 | 2. Use the function '....' 17 | 3. See error 18 | 19 | **Expected behavior** 20 | A clear and concise description of what you expected to happen. 21 | 22 | **Screenshots** 23 | If applicable, add screenshots to help explain your problem. 24 | 25 | **Environment (please complete the following information):** 26 | - OS: [e.g. Linux] 27 | - Platform [e.g. discord, slack] 28 | - Version [e.g. 1.0,1.2] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[Feature]" 5 | labels: enhancement 6 | assignees: lanfranB4 7 | 8 | --- 9 | 10 | **Your feature is based is platform-based? Or is it platform-independent?** 11 | In the case of platform-based, specify which one. 12 | 13 | **Is your feature request related to a problem? Please describe.** 14 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 15 | 16 | **Describe the solution you'd like** 17 | A clear and concise description of what you want to happen. 18 | 19 | **Describe alternatives you've considered** 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | Add any other context or screenshots about the feature request here. 24 | -------------------------------------------------------------------------------- /T3SF/utils.py: -------------------------------------------------------------------------------- 1 | from difflib import SequenceMatcher 2 | from collections import Counter 3 | import os 4 | 5 | process_wait = False 6 | process_quit = False 7 | process_started = False 8 | 9 | def is_docker(): 10 | """ 11 | Detects if the script is running inside a docker environment. 12 | """ 13 | path = '/proc/self/cgroup' 14 | return ( 15 | os.path.exists('/.dockerenv') or 16 | os.path.isfile(path) and any('docker' in line for line in open(path)) 17 | ) 18 | 19 | def similar(a, b): 20 | """ 21 | Based in graphics, find the similarity between 2 strings. 22 | """ 23 | return SequenceMatcher(None, a, b).ratio() 24 | 25 | def regex_finder(input): 26 | """ 27 | Matches repeated words counting the 28 | amount of times the word is being repeated. 29 | """ 30 | words = input.split('-') 31 | dict = Counter(words) 32 | for key in words: 33 | if dict[key]>1: 34 | return key 35 | return False -------------------------------------------------------------------------------- /T3SF/gui/static/js/copy.js: -------------------------------------------------------------------------------- 1 | export default function copyToClipBoard(text) { 2 | if (navigator.clipboard) { 3 | return navigator.clipboard.writeText(text) 4 | } 5 | // Compatible with old browsers such as Chrome <=65, Edge <=18 & IE 6 | // Compatible with HTTP 7 | else if (document.queryCommandSupported?.("copy")) { 8 | const textarea = document.createElement("textarea") 9 | textarea.value = text 10 | 11 | textarea.style.position = "fixed" // Avoid scrolling to bottom 12 | textarea.style.opacity = "0" 13 | 14 | document.body.appendChild(textarea) 15 | textarea.select() 16 | 17 | // Security exception may be thrown by some browsers 18 | try { 19 | document.execCommand("copy") 20 | } catch (e) { 21 | console.error(e) 22 | } finally { 23 | document.body.removeChild(textarea) 24 | } 25 | } else { 26 | console.error("Copy failed.") 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | - Contributing to this framework is straight forward. This document shows you how to get started 4 | 5 | ## Submitting changes 6 | 7 | - Fork the repo 8 | - 9 | - Check out a new branch based and name it to what you intend to do: 10 | - Example: 11 | ```` 12 | $ git checkout -b BRANCH_NAME 13 | ```` 14 | If you get an error, you may need to fetch main first by using 15 | ```` 16 | $ git remote update && git fetch 17 | ```` 18 | - Use one branch per fix / feature 19 | - Commit your changes 20 | - Please provide a git message that explains what you've done 21 | - Commit to the forked repository 22 | - Example: 23 | ```` 24 | $ git commit -am 'Adding Signal support' 25 | ```` 26 | - Push to the branch 27 | - Example: 28 | ```` 29 | $ git push origin BRANCH_NAME 30 | ```` 31 | - Make a pull request 32 | - Make sure you send the PR to the main branch 33 | 34 | If you follow these instructions, your PR will land pretty safely in the main repo! 35 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # T3SF Examples 2 | This folder contains examples of the frameowk in action on various platforms, including Discord, Slack, Telegram, and WhatsApp. The framework uses a modular structure that allows for easy customization and extension. 3 | 4 | ## Examples 5 | Inside each platform folder, there are example scripts and screenshots that demonstrate how T3SF can be used. These examples are intended to help you get started with the framework and show how it can be customized and extended for your own projects. 6 | 7 | ## Other Files 8 | In addition to the platform examples, this folder also includes an example Excel file and JSON file for the MSEL (Master Scenario Events List) project. These files are intended to demonstrate how T3SF can be used for the execution of a TTX. 9 | 10 | ## Getting Started 11 | To get started with T3SF, you can start by exploring the examples provided in this folder. Each platform folder contains a `README.md` file that explains how to run the example scripts and use the framework on that platform. 12 | 13 | For more detailed documentation and information about the framework, please refer to the T3SF documentation page. -------------------------------------------------------------------------------- /T3SF/gui/static/js/vanilla-jsoneditor/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vanilla-jsoneditor", 3 | "description": "A web-based tool to view, edit, format, transform, and validate JSON", 4 | "version": "0.17.8", 5 | "homepage": "https://jsoneditoronline.org", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/josdejong/svelte-jsoneditor.git" 9 | }, 10 | "type": "module", 11 | "svelte": "./index.js", 12 | "module": "./index.js", 13 | "main": "./index.js", 14 | "types": "./index.d.ts", 15 | "sideEffects": false, 16 | "license": "ISC", 17 | "exports": { 18 | ".": "./index.js", 19 | "./CHANGELOG.md": "./CHANGELOG.md", 20 | "./index.d.ts": "./index.d.ts", 21 | "./index.js": "./index.js", 22 | "./index.js.map": "./index.js.map", 23 | "./LICENSE.md": "./LICENSE.md", 24 | "./README.md": "./README.md", 25 | "./SECURITY.md": "./SECURITY.md", 26 | "./themes/jse-theme-dark.css": "./themes/jse-theme-dark.css", 27 | "./themes/jse-theme-default.css": "./themes/jse-theme-default.css", 28 | "./package.json": "./package.json" 29 | }, 30 | "files": [ 31 | "*" 32 | ], 33 | "scripts": {}, 34 | "dependencies": {}, 35 | "devDependencies": {} 36 | } -------------------------------------------------------------------------------- /examples/Telegram/README.md: -------------------------------------------------------------------------------- 1 | # Telegram Examples 2 | This framework offers a range of features and capabilities when using it with Telegram, including: 3 | 4 | - Comprehensive support for Telegram's API, allowing you to interact with channels, messages, users, and more. 5 | - A modular architecture that enables you to easily extend and customize the platform's specified bot. 6 | - A user-friendly interface that makes it easy to manage your bot and monitor its status. 7 | - A special designed bot to interact directly with Telegram. 8 | 9 | 10 | ## Getting Started 11 | 12 | To get started with T3SF, you will only need to use as example, our `bot.py` file that starts the framework with the desired platform, in this case, Telegram. You can find detailed documentation on these topics on the documentation page. 13 | 14 | The framework is easy to install and configure and comes with a range of pre-built modules and templates to help you get started. 15 | 16 | ## Example Screenshots 17 | Here are some example screenshots of T3SF in action: 18 | 19 | ### Messages Arrive 20 | ![injects_arrived](injects_arrived.png) 21 | 22 | This screenshot shows how messages are received and processed by T3SF. The framework provides comprehensive support for interacting with Telegram's messaging system, allowing you to send messages, images, polls, and detect actions based on specific message content or user behavior. -------------------------------------------------------------------------------- /examples/WhatsApp/README.md: -------------------------------------------------------------------------------- 1 | # WhatsApp Examples 2 | This framework offers a range of features and capabilities when using it with WhatsApp, including: 3 | 4 | - Comprehensive support for WhatsApp's API, allowing you to interact with channels, messages, users, and more. 5 | - A modular architecture that enables you to easily extend and customize the platform's specified bot. 6 | - A user-friendly interface that makes it easy to manage your bot and monitor its status. 7 | - A special designed bot to interact directly with WhatsApp. 8 | 9 | 10 | ## Getting Started 11 | 12 | To get started with the T3SF, you will only need to use as example, our `bot.py` file that starts the framework with the desired platform, in this case, WhatsApp. You can find detailed documentation on these topics on the documentation page. 13 | 14 | The framework is easy to install and configure and comes with a range of pre-built modules and templates to help you get started. 15 | 16 | ## Example Screenshots 17 | Here are some example screenshots of T3SF in action: 18 | 19 | ### Messages Arrive 20 | ![injects_arrived](injects_arrived.png) 21 | 22 | This screenshot shows how messages are received and processed by T3SF. The framework provides comprehensive support for interacting with WhatsApp's messaging system, allowing you to send messages, images, polls, and detect actions based on specific message content or user behavior. -------------------------------------------------------------------------------- /T3SF/gui/static/js/bootstrap_toasts.js: -------------------------------------------------------------------------------- 1 | const b5toastContainerElement = document.getElementById("toast-container"); 2 | 3 | const b5toast = { 4 | delayInMilliseconds: 5000, 5 | htmlToElement: function (html) { 6 | const template = document.createElement("template"); 7 | html = html.trim(); 8 | template.innerHTML = html; 9 | return template.content.firstChild; 10 | }, 11 | show: function (color, title, message, delay) { 12 | title = title ? title : ""; 13 | if (color == "warning") { 14 | txt_color = "black"; 15 | } 16 | else{ 17 | txt_color = "white"; 18 | } 19 | const html = ` 20 | `; 29 | const toastElement = b5toast.htmlToElement(html); 30 | b5toastContainerElement.appendChild(toastElement); 31 | const toast = new bootstrap.Toast(toastElement, { 32 | delay: delay?delay:b5toastdelayInMilliseconds, 33 | animation: true 34 | }); 35 | toast.show(); 36 | setTimeout(() => toastElement.remove(), delay?delay:b5toastdelayInMilliseconds + 3000); // let a certain margin to allow the "hiding toast animation" 37 | }, 38 | }; -------------------------------------------------------------------------------- /T3SF/logger/logger.py: -------------------------------------------------------------------------------- 1 | from T3SF.gui.core import MessageAnnouncer 2 | from datetime import datetime 3 | import json 4 | import uuid 5 | 6 | class T3SF_Logger: 7 | @staticmethod 8 | def emit(message,message_type="DEBUG"): 9 | try: 10 | try: 11 | message = message.replace("\n"," ") 12 | except Exception: 13 | # We are trying to show a triggered exception 14 | message = f"An exception of type {type(message).__name__} occurred. Arguments:\n{message.args!r}" 15 | 16 | # Create a dictionary with the message data 17 | message_data = { 18 | "id" : str(uuid.uuid4()), 19 | "type" : message_type, 20 | "content": message, 21 | "timestamp": datetime.now().strftime("%H:%M:%S") 22 | } 23 | 24 | # Convert the dictionary to a JSON string 25 | sse_msg = f"data: {json.dumps(message_data)}\n\n" 26 | 27 | announcer = MessageAnnouncer() 28 | announcer.announce(msg=sse_msg) 29 | 30 | # Do something with the EventSource-formatted message, such as writing it to a file 31 | with open('logs.txt', 'a+') as f: 32 | f.write(sse_msg) 33 | 34 | # Print some critical information to the terminal 35 | if message_type in ['WARN', 'ERROR']: 36 | red = '\033[91m\033[5m' 37 | yellow = '\033[93m\033[5m' 38 | nc = '\033[0m' 39 | 40 | if message_type == "WARN": 41 | text = yellow + "[!] " + nc + message 42 | else: 43 | text = red + "[✗] " + nc + message 44 | print(text) 45 | 46 | except Exception as e: 47 | print(f"We could not print this message on the webpage:\n{message}") 48 | print(e) -------------------------------------------------------------------------------- /examples/Slack/README.md: -------------------------------------------------------------------------------- 1 | # Slack Examples 2 | This framework offers a range of features and capabilities when using it with Slack, including: 3 | 4 | - Comprehensive support for Slack's API, allowing you to interact with channels, messages, users, and more. 5 | - A modular architecture that enables you to easily extend and customize the platform's specified bot. 6 | - A user-friendly interface that makes it easy to manage your bot and monitor its status. 7 | - An integrated bot that interacts directly with the platform. 8 | 9 | 10 | ## Getting Started 11 | 12 | To get started with T3SF, you will only need to create a file that starts the framework with the desired platform, in this case, Slack. You can find detailed documentation on these topics on the documentation page. 13 | 14 | The framework is easy to install and configure and comes with a range of pre-built modules and templates to help you get started. 15 | 16 | ## Example Screenshots 17 | Here are some example screenshots of T3SF in action: 18 | 19 | ### Messages Arrive 20 | ![injects_arrived](injects_arrived.png) 21 | 22 | This screenshot shows how messages are received and processed by T3SF. The framework provides comprehensive support for interacting with Slack's messaging system, allowing you to send messages, images, polls, and detect actions based on specific message content or user behavior. 23 | 24 | ### GUI Example 25 | ![gui_view](gui_view.png) 26 | 27 | This screenshot shows an example of the T3SF's user interface. The interface provides a range of options and features for managing your bot, including real-time status updates and error reporting. -------------------------------------------------------------------------------- /examples/Discord/README.md: -------------------------------------------------------------------------------- 1 | # Discord Examples 2 | This framework offers a range of features and capabilities when using it with Discord, including: 3 | 4 | - Comprehensive support for Discord's API, allowing you to interact with channels, messages, users, and more. 5 | - A modular architecture that enables you to easily extend and customize the platform's specified bot. 6 | - A user-friendly interface that makes it easy to manage your bot and monitor its status. 7 | - An integrated bot that interacts directly with the platform. 8 | 9 | 10 | ## Getting Started 11 | 12 | To get started with T3SF, you will only need to create a file that starts the framework with the desired platform, in this case, Discord. You can find detailed documentation on these topics on the documentation page. 13 | 14 | The framework is easy to install and configure, and comes with a range of pre-built modules and templates to help you get started. 15 | 16 | ## Example Screenshots 17 | Here are some example screenshots of T3SF in action: 18 | 19 | ### Messages Arrive 20 | ![injects_arrived](injects_arrived.png) 21 | 22 | This screenshot shows how messages are received and processed by T3SF. The framework provides comprehensive support for interacting with Discord's messaging system, allowing you to send messages, images, polls, and detect actions based on specific message content or user behavior. 23 | 24 | ### GUI Example 25 | ![gui_view](gui_view.png) 26 | 27 | This screenshot shows an example of T3SF's user interface. The interface provides a range of options and features for managing your bot, including real-time status updates and error reporting. -------------------------------------------------------------------------------- /examples/WhatsApp/bot.py: -------------------------------------------------------------------------------- 1 | from WhaBot import * 2 | from T3SF import T3SF 3 | import asyncio 4 | import time 5 | 6 | 7 | whatsapp = WhaBot(reloaded=False, 8 | binary_location = '/Applications/Brave Browser.app/Contents/MacOS/Brave Browser', 9 | driver_location = "/Users/XXXXXX/Desktop/chromedriver", 10 | ) 11 | 12 | 13 | T3SF = T3SF(bot=whatsapp) 14 | 15 | async def handle_commands(ctx): 16 | for contact in ctx: 17 | if whatsapp.CommandHandler(ctx=contact, command="!ping"): 18 | description = "🏓 Pong!\n\nPING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=113 time=37.758 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=113 time=50.650 ms\n64 bytes from 127.0.0.1: icmp_seq=2 ttl=113 time=42.493 ms\n64 bytes from 127.0.0.1: icmp_seq=3 ttl=113 time=37.637 ms\n--- localhost ping statistics ---\n4 packets transmitted, 4 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 37.637/42.135/50.650/5.292 ms\n\n_This is not real xD_" 19 | whatsapp.SendMessage(chat=contact["Chat_Name"], message=description) 20 | 21 | elif whatsapp.CommandHandler(ctx=contact, command="!start"): 22 | await T3SF.ProcessIncidents(function_type = "start", ctx=contact) 23 | 24 | elif whatsapp.CommandHandler(ctx=contact, command="!resume"): 25 | itinerator = int(contact["Last_Message"].split(" ")[1]) 26 | await T3SF.ProcessIncidents(function_type = "resume", ctx=contact, itinerator=itinerator) 27 | 28 | elif whatsapp.CommandHandler(ctx=contact, command="!add"): 29 | await T3SF.RegexHandler(inbox=contact) 30 | 31 | async def main(): 32 | while True: 33 | unreads = whatsapp.GetUnreadChats(scrolls=10) 34 | await handle_commands(ctx=unreads) 35 | await asyncio.sleep(0.5) 36 | 37 | 38 | loop = asyncio.new_event_loop() 39 | asyncio.set_event_loop(loop) 40 | try: 41 | loop.run_until_complete(main()) 42 | finally: 43 | loop.run_until_complete(loop.shutdown_asyncgens()) 44 | loop.close() -------------------------------------------------------------------------------- /examples/Slack/bot_manifest.yml: -------------------------------------------------------------------------------- 1 | display_information: 2 | name: Incidents Bot 3 | description: I'm here, but I'm broken 4 | background_color: "#d32600" 5 | long_description: Hey! I'm moving my home from Discord to this new lovely place named Slack! So let's see if it's that good. :D This should be a long description, so I have to be at least 175 characters. 6 | features: 7 | app_home: 8 | home_tab_enabled: true 9 | messages_tab_enabled: false 10 | messages_tab_read_only_enabled: true 11 | bot_user: 12 | display_name: Incidents Bot 13 | always_online: true 14 | slash_commands: 15 | - command: /info 16 | description: information about the bot 17 | should_escape: false 18 | - command: /resume 19 | description: Resumes the Game from the desired Incident Id 20 | usage_hint: "4" 21 | should_escape: false 22 | - command: /start 23 | description: Starts the Incidents Game. 24 | should_escape: false 25 | oauth_config: 26 | scopes: 27 | user: 28 | - chat:write 29 | - channels:write 30 | - groups:write 31 | - im:write 32 | - mpim:write 33 | bot: 34 | - app_mentions:read 35 | - channels:history 36 | - channels:join 37 | - channels:manage 38 | - channels:read 39 | - chat:write 40 | - chat:write.customize 41 | - commands 42 | - groups:history 43 | - groups:write 44 | - im:history 45 | - im:write 46 | - mpim:history 47 | - mpim:write 48 | - reactions:read 49 | - team:read 50 | - users.profile:read 51 | - users:read 52 | - groups:read 53 | settings: 54 | event_subscriptions: 55 | bot_events: 56 | - app_home_opened 57 | - app_mention 58 | - message.channels 59 | - message.groups 60 | - reaction_added 61 | interactivity: 62 | is_enabled: true 63 | org_deploy_enabled: false 64 | socket_mode_enabled: true 65 | token_rotation_enabled: false 66 | -------------------------------------------------------------------------------- /docs/T3SF.Logger.rst: -------------------------------------------------------------------------------- 1 | ************************ 2 | T3SF Logger 3 | ************************ 4 | 5 | The T3SF_Logger class is a Python class used for logging messages in the T3SF application. It provides a method for emitting log messages and sending them to a message announcer object, which then broadcasts the message to other components in the application. 6 | 7 | The message is also written to a file called ``logs.txt`` for future reference. If the ``message_type`` is set to "WARN" or "ERROR", a critical information message is printed to the terminal using color codes to highlight the message type. 8 | 9 | The purpose of the T3SF_Logger class is to provide a centralized logging system for T3SF, allowing developers to easily log messages and display them to administrators or game masters in an easy way. 10 | 11 | 12 | Module 13 | ====== 14 | 15 | To ensure a clean and organized logging system in the T3SF framework, we implemented the T3SF_Logger class. This class is responsible for handling all logging events and formatting them in a standardized way. By utilizing this class, we can easily track the events and activities of the framework and ensure efficient debugging when needed. 16 | 17 | The file structure is shown below: 18 | 19 | .. code-block:: bash 20 | 21 | logger 22 | ├── __init__.py 23 | └── logger.py 24 | 25 | The Class 26 | ---------- 27 | 28 | .. py:class:: T3SF_Logger() 29 | 30 | The class has only one method, which is ``emit``. It can be called directly without the need to initialize the class. 31 | 32 | .. py:method:: emit(message,message_type="DEBUG") 33 | 34 | With this method, we can format the message to an SSE format, store it in the ``logs.txt`` file and also print critical information messages on the terminal using color codes to highlight the type of message. 35 | 36 | .. confval:: message 37 | 38 | The content of the message 39 | 40 | :type: ``str`` 41 | :required: ``True`` 42 | 43 | .. confval:: message_type 44 | 45 | The level/type of the message. 46 | 47 | :type: ``str`` 48 | :required: ``False`` -------------------------------------------------------------------------------- /examples/Telegram/bot.py: -------------------------------------------------------------------------------- 1 | from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton 2 | from aiogram import Bot, Dispatcher, executor, types 3 | from aiogram.types import ChatType 4 | from dotenv import load_dotenv 5 | import logging 6 | 7 | from T3SF import * 8 | 9 | load_dotenv() 10 | 11 | # Configure logging 12 | logging.basicConfig(level=logging.WARNING) 13 | 14 | bot = Bot(token=os.environ['TELEGRAM_TOKEN']) 15 | dp = Dispatcher(bot) 16 | 17 | T3SF = T3SF(bot=bot) 18 | 19 | @dp.callback_query_handler() 20 | async def inline_query_handler(query: types.CallbackQuery): 21 | await T3SF.PollAnswerHandler(query=query) 22 | 23 | @dp.message_handler(commands="ping") 24 | async def ping(message): 25 | """ 26 | Retrieves the !ping command and replies with this message. 27 | """ 28 | description = """``` 29 | PING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=113 time=37.758 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=113 time=50.650 ms\n64 bytes from 127.0.0.1: icmp_seq=2 ttl=113 time=42.493 ms\n64 bytes from 127.0.0.1: icmp_seq=3 ttl=113 time=37.637 ms\n--- localhost ping statistics ---\n4 packets transmitted, 4 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 37.637/42.135/50.650/5.292 ms```\n\n_This is not real xD_""" 30 | response = await message.reply(description, parse_mode = 'Markdown') 31 | 32 | @dp.message_handler(commands='start') 33 | async def start(message: types.Message): 34 | """ 35 | Retrieves the !start command and starts to fetch and send the incidents. 36 | """ 37 | await T3SF.ProcessIncidents(function_type = "start", ctx=message) 38 | 39 | @dp.message_handler(commands='resume') 40 | async def resume(message: types.Message): 41 | """ 42 | Retrieves the !resume command and starts to fetch and 43 | send incidents from the desired starting point. 44 | """ 45 | itinerator = int(message['text'].split(" ")[1]) 46 | 47 | await T3SF.ProcessIncidents(function_type = "resume", ctx=message, itinerator=itinerator) 48 | 49 | @dp.channel_post_handler() 50 | async def inboxes_fetcher(message): 51 | """ 52 | This handler will be called when user sends a message in a channel 53 | to add it to the inboxes list. 54 | """ 55 | await T3SF.InboxesAuto(message=message) 56 | 57 | if __name__ == '__main__': 58 | executor.start_polling(dp, skip_updates=True) -------------------------------------------------------------------------------- /T3SF/gui/static/js/theme_switcher.js: -------------------------------------------------------------------------------- 1 | // Check the initial theme preference and set it 2 | const themePreference = localStorage.getItem('theme'); 3 | const prefersDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; 4 | 5 | if (themePreference === 'dark' || (themePreference === null && prefersDarkMode)) { 6 | enableDarkMode(); 7 | } else { 8 | enableLightMode(); 9 | } 10 | 11 | // Toggle the theme when the button is clicked 12 | const themeToggle = document.getElementById('theme-toggle'); 13 | 14 | themeToggle.addEventListener('click', () => { 15 | if (document.documentElement.getAttribute('data-bs-theme') === 'dark') { 16 | enableLightMode(); 17 | } else { 18 | enableDarkMode(); 19 | } 20 | }); 21 | 22 | // Enable dark mode 23 | function enableDarkMode() { 24 | document.documentElement.setAttribute('data-bs-theme', 'dark'); 25 | localStorage.setItem('theme', 'dark'); 26 | replaceClassNames('bg-light', 'bg-darkmode'); 27 | replaceClassNames('bg-white', 'bg-dark'); 28 | document.getElementById('theme-icon').classList.replace('bi-moon-fill', 'bi-sun-fill'); 29 | document.getElementById('theme-text').textContent = 'Light mode'; 30 | document.querySelectorAll('#logo-image').forEach((image) => {image.src = "/static/imgs/logo-dark.png";}); 31 | document.getElementById('json-editor-container')?.classList.replace('jse-theme-light', 'jse-theme-dark'); 32 | } 33 | 34 | // Enable light mode 35 | function enableLightMode() { 36 | document.documentElement.setAttribute('data-bs-theme', 'light'); 37 | localStorage.setItem('theme', 'light'); 38 | replaceClassNames('bg-darkmode', 'bg-light'); 39 | replaceClassNames('bg-dark', 'bg-white'); 40 | document.getElementById('theme-icon').classList.replace('bi-sun-fill', 'bi-moon-fill'); 41 | document.getElementById('theme-text').textContent = 'Dark mode'; 42 | document.querySelectorAll('#logo-image').forEach((image) => {image.src = "/static/imgs/logo-light.png"; }); 43 | document.getElementById('json-editor-container')?.classList.replace('jse-theme-dark', 'jse-theme-light'); 44 | } 45 | 46 | // Replace class names 47 | function replaceClassNames(oldClassName, newClassName) { 48 | const elements = document.querySelectorAll(`.${oldClassName}`); 49 | elements.forEach((element) => { 50 | element.classList.remove(oldClassName); 51 | element.classList.add(newClassName); 52 | }); 53 | } -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | ********************************************************** 2 | T3SF - Technical Tabletop Exercises Simulation Framework 3 | ********************************************************** 4 | 5 | .. contents:: Table of Contents 6 | 7 | T3SF is a framework that offers a modular structure for the orchestration of events from a master scenario events list (MSEL) together with a set of rules defined for each exercise (optional) and a configuration that allows defining the parameters of the platform used. The main module performs the communication with the specific platform module (Discord, Slack, Telegram, etc.) which allows the events to be presented in the input channels as injects corresponding to each platform. 8 | 9 | Schematic 10 | ========== 11 | 12 | .. image:: ./images/schema.jpeg 13 | 14 | 15 | Supported platforms 16 | ==================== 17 | 18 | - :doc:`./Discord` 19 | - Start / Resume functions for incidents. 20 | - Incidents can have a picture attached and also the profile picture from the sender. 21 | - Automatic regular expression to match players and channels. 22 | - Core functionalities such as time difference, ping command and injects fetcher. 23 | 24 | - :doc:`./Slack` 25 | - Start / Resume functions for incidents. 26 | - Incidents can have a picture attached and also the profile picture from the sender. 27 | - Automatic regular expression to match players and channels. 28 | - Core functionalities such as time difference, ping command and injects fetcher. 29 | 30 | - :doc:`./Telegram` 31 | - Start / Resume functions for incidents. 32 | - Incidents are only capable to have an attached picture, no profile picture from the sender. 33 | - Manual Inbox fetcher with the command `!add`, due to the lack of options from Telegram. 34 | - Core functionalities such as time difference, ping command and injects fetcher. 35 | 36 | - :doc:`./WhatsApp` 37 | - Start / Resume functions for incidents. 38 | - Incidents are only capable to have an attached picture, no profile picture from the sender. 39 | - Manual Inbox fetcher with the command `!add`, due to the lack of options from WhatsApp. 40 | - Core functionalities such as time difference, ping command and injects fetcher. 41 | 42 | 43 | .. toctree:: 44 | :caption: T3SF Core 45 | :maxdepth: 3 46 | :hidden: 47 | 48 | T3SF.Installation 49 | T3SF.Usage 50 | T3SF.CoreFunctions 51 | T3SF.Handlers 52 | T3SF.Gui 53 | T3SF.Logger 54 | 55 | .. toctree:: 56 | :caption: Supported Platforms 57 | :maxdepth: 3 58 | :hidden: 59 | 60 | Discord 61 | Slack 62 | Telegram 63 | WhatsApp 64 | 65 | 66 | -------------------------------------------------------------------------------- /docs/WhatsApp.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | WhatsApp 3 | ******************* 4 | 5 | .. warning:: We are currently working to give WhatsApp support to latest version of T3SF. Please use ``Version 1.1`` if you want to run an exercise on this platform. 6 | 7 | .. contents:: Table of Contents 8 | 9 | We are expaning our framework's support to this new platform as clients requested us. Not all the functions/features from the main platforms (Discord/Slack) could be migrated due to WhatsApp limitations. 10 | 11 | Functions 12 | =============== 13 | 14 | .. py:function:: SendMessage(title, description, ctx=None, player=None, image=None) 15 | 16 | Message sending controller. 17 | 18 | .. confval:: title 19 | 20 | The title of the message. 21 | 22 | :type: ``str`` 23 | :required: ``True`` 24 | 25 | .. confval:: description 26 | 27 | The description/main text of the message. 28 | 29 | :type: ``str`` 30 | :required: ``True`` 31 | 32 | .. confval:: ctx 33 | 34 | :type: ``ctx`` 35 | :required: ``True`` 36 | 37 | .. confval:: player 38 | 39 | The player's inbox id to send the message. 40 | 41 | :type: ``int`` 42 | :required: ``False`` 43 | 44 | .. confval:: image 45 | 46 | Attach an image to the message. 47 | 48 | :type: ``str`` 49 | :required: ``False`` 50 | 51 | 52 | .. py:function:: InboxFetcher(inbox) 53 | 54 | Fetches half manual, half automatically the inboxes, based in a command (``!add``) from the game master in the inbox channel, notifies the Game masters about differents parts of this process. 55 | 56 | .. confval:: inbox 57 | 58 | Parameter containing the Chat Name. 59 | 60 | :type: ``array`` 61 | :required: ``True`` 62 | 63 | 64 | .. py:function:: InboxesAuto(message=None) 65 | 66 | Checks the amount of players and the amount of inboxes to start/resume the simulation. Based in the function :py:meth:`InboxFetcher` 67 | 68 | .. confval:: message 69 | 70 | The message from the game master, to add an inbox to the list. 71 | 72 | :type: ``str`` 73 | :required: ``False`` 74 | 75 | 76 | .. py:function:: InjectHandler(self) 77 | 78 | Gives the format to the inject and sends it to the correct player's inbox. 79 | 80 | 81 | Bot 82 | =============== 83 | 84 | Installation 85 | ------------------ 86 | 87 | 1. Git clone this repository. 88 | 2. Go inside the WhatsApp version folder with ``cd T3SF/Whatsapp/`` 89 | 3. Install requirements. 90 | ``pip3 install -r requirements.txt`` 91 | 92 | (Optional) Create a virtual envirnoment 93 | ``python3 -m venv venv`` 94 | 4. Run the bot with ``python3 bot.py`` 95 | (Optional) Scan the QR code to login. 96 | 97 | We recommend using a business WhatsApp account and a non-everyday phone number. 98 | 5. Add the Bot to every group, such as Inboxes group, GM-Chat, etc. 99 | 6. Done! -------------------------------------------------------------------------------- /docs/Telegram.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Telegram 3 | ******************* 4 | 5 | .. warning:: We are currently working to give Telegram support to latest version of T3SF. Please use ``Version 1.1`` if you want to run an exercise on this platform. 6 | 7 | .. contents:: Table of Contents 8 | 9 | We are expaning our framework's support to this new platform as clients requested us. Not all the functions/features from the main platforms (Discord/Slack) could be migrated due to Telegram limitations. 10 | 11 | Functions 12 | =============== 13 | 14 | .. py:function:: SendMessage(title, description, ctx=None, player=None, image=None) 15 | 16 | Message sending controller. 17 | 18 | .. confval:: title 19 | 20 | The title of the message. 21 | 22 | :type: ``str`` 23 | :required: ``True`` 24 | 25 | .. confval:: description 26 | 27 | The description/main text of the message. 28 | 29 | :type: ``str`` 30 | :required: ``True`` 31 | 32 | .. confval:: ctx 33 | 34 | :type: ``ctx`` 35 | :required: ``False`` 36 | 37 | .. confval:: player 38 | 39 | The player's inbox id to send the message. 40 | 41 | :type: ``int`` 42 | :required: ``False`` 43 | 44 | .. confval:: image 45 | 46 | Attach an image to the message. 47 | 48 | :type: ``str`` 49 | :required: ``False`` 50 | 51 | 52 | .. py:function:: EditMessage(title, description, response) 53 | 54 | Message editing controller. 55 | 56 | .. confval:: title 57 | 58 | The title of the message. 59 | 60 | :type: ``str`` 61 | :required: ``True`` 62 | 63 | .. confval:: description 64 | 65 | The description/main text of the message. 66 | 67 | :type: ``str`` 68 | :required: ``True`` 69 | 70 | .. confval:: response 71 | 72 | The previous response message's object containing the edit message function. 73 | 74 | :type: ``object`` 75 | :required: ``True`` 76 | 77 | 78 | .. py:function:: InboxesAuto(message=None) 79 | 80 | Fetches half manual, half automatically the inboxes, based in a command (``!add``) from the game master in the inbox channel, notifies the Game masters about differents parts of this process. 81 | 82 | .. confval:: message 83 | 84 | The message from the game master, to add an inbox to the list. 85 | 86 | :type: ``str`` 87 | :required: ``False`` 88 | 89 | 90 | .. py:function:: InjectHandler(self) 91 | 92 | Gives the format to the inject and sends it to the correct player's inbox. 93 | 94 | 95 | Bot 96 | =============== 97 | 98 | Installation 99 | ------------------ 100 | 101 | 1. Git clone this repository. 102 | 2. Go inside the Telegram version folder with ``cd T3SF/Telegram/`` 103 | 3. Install requirements. 104 | ``pip3 install -r requirements.txt`` 105 | 106 | (Optional) Create a virtual environment 107 | ``python3 -m venv venv`` 108 | 4. Create/Get the Bot's token from `@BotFather `_. 109 | 5. Add the token to en ``.env`` file. 110 | 6. Run the bot with ``python3 bot.py`` 111 | 7. Add the Bot to every channel, such as Inboxes channel, GM-Chat, etc. 112 | 8. Done! -------------------------------------------------------------------------------- /docs/T3SF.Installation.rst: -------------------------------------------------------------------------------- 1 | ************************ 2 | Installation 3 | ************************ 4 | 5 | To use the framework with the platform you want, either Slack or Discord, you will have to install the necessary modules and external libraries for that platform. But don't worry, installing these requirements is easy and simple. 6 | 7 | Getting things ready 8 | ======================= 9 | 10 | The installation of the framework itself it's really easy! 11 | 12 | You just have to use ``pip`` and voila! 13 | 14 | 15 | .. note:: 16 | You can create a virtual environment to avoid dependencies issues: 17 | 18 | ``python3 -m venv venv`` 19 | 20 | ``pip install T3SF`` 21 | 22 | 23 | Platform-based installation 24 | ======================= 25 | 26 | Even though we have installed the core framework, we still need to install the additional libraries for each of the platforms. 27 | 28 | Discord 29 | --------- 30 | 31 | In case you want to use Discord, the installation of the necessary libraries is done as follows 32 | 33 | ``pip install "T3SF[Discord]"`` 34 | 35 | *For more information on how to create the Bot and obtain the tokens, please go to the* `specific page <./Discord.html#bot>`_ *of the platform.* 36 | 37 | 38 | Slack 39 | --------- 40 | 41 | On the other hand, if you want to use Slack, you can install them as follows 42 | 43 | ``pip install "T3SF[Slack]"`` 44 | 45 | *For more information on how to create the Bot and obtain the tokens, please go to the* `specific page <./Slack.html#bot>`_ *of the platform.* 46 | 47 | 48 | Using Docker 49 | ======================= 50 | 51 | To simplify the setup process and avoid any configuration headaches, we provide Docker images that come pre-packaged with all the necessary components to run your TTX exercise seamlessly. 52 | 53 | Slack 54 | --------- 55 | 56 | For Slack users, our Docker image has everything you need to perform your exercise effortlessly. Just run the following command: 57 | 58 | ``$ docker run --rm -t --env-file .env -v $(pwd)/MSEL.json:/app/MSEL.json base4sec/t3sf:slack`` 59 | 60 | 61 | Make sure to update your `.env` file with the required ``SLACK_BOT_TOKEN`` and ``SLACK_APP_TOKEN`` tokens. You can find more information on providing the tokens `here <./Slack.html#providing-the-tokens>`_. 62 | 63 | Also, remember to set the `MSEL_PATH` environment variable to specify the location of your MSEL file. By default, the container path is `/app/MSEL.json`. Adjust the variable accordingly if you change the volume mount location. 64 | 65 | 66 | Discord 67 | --------- 68 | 69 | If you prefer Discord, our Docker image has got you covered. Simply execute the following command: 70 | 71 | 72 | ``$ docker run --rm -t --env-file .env -v $(pwd)/MSEL.json:/app/MSEL.json base4sec/t3sf:discord`` 73 | 74 | Update your .env file with the required ``DISCORD_TOKEN``. You can find detailed instructions on providing the token `here <./Discord.html#providing-the-token>`_. 75 | 76 | Similarly, set the `MSEL_PATH` environment variable to specify the location of your MSEL file. The default container path is `/app/MSEL.json`. Adjust the variable accordingly if you change the volume mount location. -------------------------------------------------------------------------------- /docs/T3SF.CoreFunctions.rst: -------------------------------------------------------------------------------- 1 | ************************ 2 | CORE Functions 3 | ************************ 4 | 5 | CORE functions are those functions that are to be used regardless of the selected platform. Basically they are functions for all platforms. 6 | 7 | .. py:function:: TimeDifference(actual_real_time:int, previous_real_time:int, itinerator:int, resumed:bool) 8 | :async: 9 | 10 | Get the difference between two injects. It will make the bot sleep and inform the Game Masters. 11 | 12 | .. confval:: actual_real_time 13 | 14 | The actual inject's time. 15 | 16 | :type: ``int`` 17 | :required: ``True`` 18 | 19 | .. confval:: previous_real_time 20 | 21 | The previous inject's time. 22 | 23 | :type: ``int`` 24 | :required: ``True`` 25 | 26 | 27 | .. confval:: itinerator 28 | 29 | The inject's number. Used when :confval:`resumed` is ``True``. 30 | 31 | :type: ``int`` 32 | :default: ``None`` 33 | :required: ``False`` 34 | 35 | 36 | .. confval:: resumed 37 | 38 | :type: ``bool`` 39 | :default: ``None`` 40 | :required: ``False`` 41 | 42 | .. py:function:: NotifyGameMasters(type_info:str) 43 | :async: 44 | 45 | Notify the Game Masters of the different states of the bot, through messages. 46 | 47 | .. confval:: type_info 48 | 49 | :type: ``str`` 50 | :required: ``True`` 51 | 52 | .. py:function:: ProcessIncidents(ctx, function_type:str=None, itinerator:int=0) 53 | :async: 54 | 55 | Process the incidents from the MSEL file. 56 | 57 | .. confval:: ctx 58 | 59 | :type: ``object|array`` 60 | :required: ``True`` 61 | 62 | .. confval:: function_type 63 | 64 | Depending the command sent (Start/Resume). 65 | 66 | :type: ``str`` 67 | :required: ``True`` 68 | 69 | 70 | .. confval:: itinerator 71 | 72 | Inject number retrieved from the Game Master, used when :confval:`function_type` equals ``"resume"``. 73 | 74 | :type: ``int`` 75 | :default: ``None`` 76 | :required: ``False`` 77 | 78 | .. py:function:: IncidentsFetcher(self) 79 | 80 | Retrieves the incidents from the desired source, chosen in the config file. 81 | 82 | .. py:function:: start(MSEL:str, platform, gui=False) 83 | :async: 84 | 85 | Start the framework. This function takes care of starting the platform bot and also the GUI. 86 | 87 | .. confval:: MSEL 88 | 89 | The location of the MSEL. 90 | 91 | :type: ``str`` 92 | :required: ``True`` 93 | 94 | .. confval:: platform 95 | 96 | The platform selected for the exercise. 97 | 98 | :type: ``str`` 99 | :required: ``True`` 100 | 101 | .. confval:: gui 102 | 103 | A boolean to determine if we should start the visual interface. 104 | 105 | :type: ``bool`` 106 | :required: ``False`` 107 | 108 | .. py:function:: check_status(reset: bool = False) -> Union[bool, str] 109 | :async: 110 | 111 | Monitors the framework's status. It can reset flags, handle framework breaks, and wait until the framework is ready to proceed. Its purpose is to ensure smooth operation and synchronization within the framework. 112 | 113 | .. confval:: reset 114 | 115 | Reset flag to indicate whether to reset the process_wait and process_quit flags. 116 | 117 | :type: ``bool`` 118 | :default: ``False`` 119 | :required: ``False`` 120 | 121 | :return: Returns a boolean indicating the status of the framework or 'break' if process_quit is True. -------------------------------------------------------------------------------- /T3SF/gui/static/js/vanilla-jsoneditor/themes/jse-theme-dark.css: -------------------------------------------------------------------------------- 1 | .jse-theme-dark { 2 | --jse-theme: dark; 3 | 4 | /* over all fonts, sizes, and colors */ 5 | --jse-theme-color: #2f6dd0; 6 | --jse-theme-color-highlight: #467cd2; 7 | /* --jse-background-color: #1e1e1e; */ 8 | /* Changed the defaul background color */ 9 | --jse-background-color: var(--bs-dark); 10 | --jse-text-color: #d4d4d4; 11 | 12 | /* main, menu, modal */ 13 | --jse-main-border: 1px solid #4f4f4f; 14 | --jse-menu-color: #fff; 15 | --jse-modal-background: #2f2f2f; 16 | --jse-modal-overlay-background: rgba(0, 0, 0, 0.5); 17 | --jse-modal-code-background: #2f2f2f; 18 | 19 | /* tooltip in text mode */ 20 | --jse-tooltip-color: var(--jse-text-color); 21 | --jse-tooltip-background: #4b4b4b; 22 | --jse-tooltip-border: 1px solid #737373; 23 | --jse-tooltip-action-button-color: inherit; 24 | --jse-tooltip-action-button-background: #737373; 25 | 26 | /* panels: navigation bar, gutter, search box */ 27 | --jse-panel-background: #333333; 28 | --jse-panel-background-border: 1px solid #464646; 29 | --jse-panel-color: var(--jse-text-color); 30 | --jse-panel-color-readonly: #737373; 31 | --jse-panel-border: 1px solid #3c3c3c; 32 | --jse-panel-button-color-highlight: #e5e5e5; 33 | --jse-panel-button-background-highlight: #464646; 34 | 35 | /* navigation-bar */ 36 | --jse-navigation-bar-background: #656565; 37 | --jse-navigation-bar-background-highlight: #7e7e7e; 38 | --jse-navigation-bar-dropdown-color: var(--jse-text-color); 39 | 40 | /* context menu */ 41 | --jse-context-menu-background: #4b4b4b; 42 | --jse-context-menu-background-highlight: #595959; 43 | --jse-context-menu-separator-color: #595959; 44 | --jse-context-menu-color: var(--jse-text-color); 45 | --jse-context-menu-pointer-background: #737373; 46 | --jse-context-menu-pointer-background-highlight: #818181; 47 | --jse-context-menu-pointer-color: var(--jse-context-menu-color); 48 | 49 | /* contents: json key and values */ 50 | --jse-key-color: #9cdcfe; 51 | --jse-value-color: var(--jse-text-color); 52 | --jse-value-color-number: #b5cea8; 53 | --jse-value-color-boolean: #569cd6; 54 | --jse-value-color-null: #569cd6; 55 | --jse-value-color-string: #ce9178; 56 | --jse-value-color-url: #ce9178; 57 | --jse-delimiter-color: #949494; 58 | --jse-edit-outline: 2px solid var(--jse-text-color); 59 | 60 | /* contents: selected or hovered */ 61 | --jse-selection-background-color: #464646; 62 | --jse-selection-background-inactive-color: #333333; 63 | --jse-hover-background-color: #343434; 64 | --jse-active-line-background-color: rgba(255, 255, 255, 0.06); 65 | --jse-search-match-background-color: #343434; 66 | 67 | /* contents: section of collapsed items in an array */ 68 | --jse-collapsed-items-background-color: #333333; 69 | --jse-collapsed-items-selected-background-color: #565656; 70 | --jse-collapsed-items-link-color: #b2b2b2; 71 | --jse-collapsed-items-link-color-highlight: #ec8477; 72 | 73 | /* contents: highlighting of search results */ 74 | --jse-search-match-color: #724c27; 75 | --jse-search-match-outline: 1px solid #966535; 76 | --jse-search-match-active-color: #9f6c39; 77 | --jse-search-match-active-outline: 1px solid #bb7f43; 78 | 79 | /* contents: inline tags inside the JSON document */ 80 | --jse-tag-background: #444444; 81 | --jse-tag-color: #bdbdbd; 82 | 83 | /* contents: table */ 84 | --jse-table-header-background: #333333; 85 | --jse-table-header-background-highlight: #424242; 86 | --jse-table-row-odd-background: rgba(255, 255, 255, 0.1); 87 | 88 | /* controls in modals: inputs, buttons, and `a` */ 89 | --jse-input-background: #3d3d3d; 90 | --jse-input-border: var(--jse-main-border); 91 | --jse-button-background: #808080; 92 | --jse-button-background-highlight: #7a7a7a; 93 | --jse-button-color: #e0e0e0; 94 | --jse-button-secondary-background: #494949; 95 | --jse-button-secondary-background-highlight: #5d5d5d; 96 | --jse-button-secondary-background-disabled: #9d9d9d; 97 | --jse-button-secondary-color: var(--jse-text-color); 98 | --jse-a-color: #55abff; 99 | --jse-a-color-highlight: #4387c9; 100 | 101 | /* svelte-select */ 102 | --background: #3d3d3d; 103 | --border: 1px solid #4f4f4f; 104 | --list-background: #3d3d3d; 105 | --item-hover-bg: #505050; 106 | --multi-item-bg: #5b5b5b; 107 | --input-color: #d4d4d4; 108 | --multi-clear-bg: #8a8a8a; 109 | --multi-item-clear-icon-color: #d4d4d4; 110 | --multi-item-outline: 1px solid #696969; 111 | --list-shadow: 0 2px 8px 0 rgba(0, 0, 0, 0.4); 112 | 113 | /* color picker */ 114 | --jse-color-picker-background: #656565; 115 | --jse-color-picker-border-box-shadow: #8c8c8c 0 0 0 1px; 116 | } 117 | -------------------------------------------------------------------------------- /docs/T3SF.Usage.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Usage 3 | ******************* 4 | 5 | .. contents:: Table of Contents 6 | 7 | Basically we have created the framework to be quick and easy to set up, so if you want to run the bot in Discord or Slack you just need to start T3SF! 8 | 9 | 10 | Initializing T3SF 11 | ========================= 12 | 13 | To start the framework, you have to set 3 parameters. Depending on the platform and your preferences the following arguments will be set: 14 | 15 | .. confval:: MSEL 16 | 17 | The location of the MSEL. It accepts the complete file path. 18 | 19 | :type: ``str`` 20 | :required: ``True`` 21 | 22 | .. confval:: platform 23 | 24 | The platform you want to use. 25 | 26 | :type: ``str`` 27 | :required: ``True`` 28 | :values: ``Slack`` or ``Discord`` 29 | 30 | .. confval:: gui 31 | 32 | Starts the GUI of the framework. 33 | 34 | :type: ``bool`` 35 | :required: ``False`` 36 | :default: ``True`` 37 | 38 | 39 | This example is for an exercise using the platform Slack with a GUI: 40 | 41 | .. code-block:: python3 42 | 43 | from T3SF import T3SF 44 | import asyncio 45 | 46 | async def main(): 47 | await T3SF.start(MSEL="MSEL_Company.json", platform="Slack", gui=True) 48 | 49 | if __name__ == '__main__': 50 | asyncio.run(main()) 51 | 52 | And that's it! 53 | 54 | 55 | MSEL Configuration 56 | =================== 57 | 58 | The file where you have all injects stored is the Master Scenario Events List (MSEL). From this file, the framework is going to retrieve all the messages and players, so it's like the Heart of the exercise! 59 | 60 | Format 61 | --------- 62 | 63 | Inside the repo you have an example of a common MSEL, but we will be explaining in a short and easy way the format of it. 64 | 65 | Here is the first inject from the example in the repo. 66 | 67 | .. code-block:: json 68 | 69 | { 70 | "#": 1, 71 | "Real Time": "07:30 PM", 72 | "Date": "Monday 9:40 AM", 73 | "Subject": "[URGENT] Ransom Request!", 74 | "From": "SOC - BASE4", 75 | "Player": "Legal", 76 | "Script": "Team, we received a ransom request. What should we do?", 77 | "Picture Name": "Base_4_SOC.jpg", 78 | "Photo": "https://img2.helpnetsecurity.com/posts2018/aws-s3-buckets-public.jpg", 79 | "Profile": "https://foreseeti.com/wp-content/uploads/2021/09/Ska%CC%88rmavbild-2021-09-02-kl.-15.44.24.png", 80 | "Poll": "We are checking on it | It is a false positive" 81 | } 82 | 83 | .. confval:: # 84 | 85 | The inject/incident number. 86 | 87 | :type: ``int`` 88 | :required: ``True`` 89 | 90 | .. confval:: Real Time 91 | 92 | The actual time by which the incident should arrive in the player's inbox. This will not be shown to the player. 93 | 94 | .. note:: 95 | We are mainly using the minutes of this key to make things work. 96 | 97 | :type: ``str`` 98 | :required: ``True`` 99 | 100 | .. confval:: Date 101 | 102 | The simulated date of the incident. This will be displayed to the player. 103 | 104 | :type: ``str`` 105 | :required: ``True`` 106 | 107 | .. confval:: Subject 108 | 109 | The Subject from the incident. 110 | 111 | :type: ``str`` 112 | :required: ``True`` 113 | 114 | .. confval:: From 115 | 116 | The sender of the incident/message. 117 | 118 | :type: ``str`` 119 | :required: ``True`` 120 | 121 | .. confval:: Player 122 | 123 | The player's name, eg. ``"Information Security"``, ``"Legal"``, ``"SRE"``. 124 | 125 | :type: ``str`` 126 | :required: ``True`` 127 | 128 | .. confval:: Script 129 | 130 | The main text and the incident body of the message. 131 | 132 | :type: ``str`` 133 | :required: ``True`` 134 | 135 | .. confval:: Picture Name 136 | 137 | The attachment's name. 138 | 139 | .. note:: 140 | This key is used in :doc:`./Slack`. 141 | 142 | :type: ``str`` -> Web URL 143 | :required: ``False`` -> ``True`` if the platform is :doc:`./Slack`. 144 | 145 | .. confval:: Photo 146 | 147 | An attached photo for the incident. 148 | 149 | .. note:: 150 | In WhatsApp the photo **should be a local PATH**. In other platforms, you can use the image url from internet. 151 | 152 | :type: ``str`` -> Web URL 153 | :required: ``False`` 154 | 155 | .. confval:: Profile 156 | 157 | The profile picture of the sender. If no profile picture is set for an incident, a default user avatar will be used. 158 | 159 | .. note:: 160 | This key is only valid in :doc:`./Discord` and :doc:`./Slack`, due to platform restrictions. 161 | 162 | :type: ``str`` -> Web URL 163 | :required: ``False`` -> ``True`` if the platform is :doc:`./Discord` or :doc:`./Slack`. 164 | 165 | 166 | .. confval:: Poll 167 | 168 | Set up a survey to be sent to the players, where they have time to answer depending on the options. 169 | 170 | .. note:: 171 | This key is only valid in :doc:`./Discord` and :doc:`./Slack`, due to platform restrictions. 172 | 173 | :type: ``str`` 174 | :required: ``False`` 175 | 176 | .. note:: 177 | The options should be separated with a pipe (|) symbol. 178 | -------------------------------------------------------------------------------- /T3SF/gui/templates/env_creation.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} Environment creation {% endblock %} 4 | 5 | {% block extra_head %} 6 | 38 | 39 | {% endblock %} 40 | {% block content %} 41 |
42 |
43 |
44 |

Environment creation

45 |
46 |
47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |

Areas: {{ T3SF.players_list|count }}

56 |
57 |
58 |

Channels to create per Area:

59 |
60 |
61 |

Channels for Game Masters:

62 |
63 |
64 |
65 |
66 | {%- for player in T3SF.players_list -%} 67 |
{{player}}
68 | {%- endfor -%} 69 |
70 |
71 |
Internal chat
72 |
Inbox
73 |
Decision log
74 | {%- if T3SF.platform == "discord" -%} 75 |
Voice channel
76 | {%- endif -%} 77 |
78 |
79 |
Internal chat
80 |
Logs
81 | {%- if T3SF.platform == "discord" -%} 82 |
Voice channel
83 | {%- endif -%} 84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 | 144 | 145 | 199 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | T3SF Logo 4 |

5 | 6 |

T3SF

7 | 8 |
9 | 10 | [![Status](https://img.shields.io/badge/status-active-success.svg)]() 11 | [![PyPI version](https://badge.fury.io/py/T3SF.svg)](https://badge.fury.io/py/T3SF) 12 | [![Documentation Status](https://readthedocs.org/projects/t3sf/badge/?version=latest)](https://t3sf.readthedocs.io/en/latest/?badge=latest) 13 | [![License](https://img.shields.io/badge/license-GPL-blue.svg)](/LICENSE) 14 | [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.6519221.svg)](https://doi.org/10.5281/zenodo.6519221) 15 |
16 |
17 | 18 | [![Docker Image for Discord](https://github.com/Base4Security/T3SF/actions/workflows/publish_discord.yml/badge.svg)](https://hub.docker.com/r/base4sec/t3sf) 19 | [![Docker Image for Slack](https://github.com/Base4Security/T3SF/actions/workflows/publish_slack.yml/badge.svg)](https://hub.docker.com/r/base4sec/t3sf) 20 |
21 | 22 |

Technical Tabletop Exercises Simulation Framework 23 |
24 |

25 | 26 | ## Table of Contents 27 | - [About](#About) 28 | - [Getting Things Ready](#Starting) 29 | - [TODO](./TODO.md) 30 | - [CHANGELOG](./CHANGELOG.md) 31 | - [Contributing](./CONTRIBUTING.md) 32 | 33 | ## About 34 | T3SF is a framework that offers a modular structure for the orchestration of events based on a master scenario events list (MSEL) together with a set of rules defined for each exercise (optional) and a configuration that allows defining the parameters of the corresponding platform. The main module performs the communication with the specific module (Discord, Slack, Telegram, etc.) that allows the events to present the events in the input channels as injects for each platform. In addition, the framework supports different use cases: "single organization, multiple areas", "multiple organization, single area" and "multiple organization, multiple areas". 35 | 36 | ## Getting Things Ready 37 | To use the framework with your desired platform, whether it's Slack or Discord, you will need to install the required modules for that platform. But don't worry, installing these modules is easy and straightforward. 38 | 39 | To do this, you can follow this simple step-by-step guide, or if you're already comfortable installing packages with `pip`, you can skip to the last step! 40 | 41 | ```bash 42 | # Python 3.6+ required 43 | python -m venv .venv # We will create a python virtual environment 44 | source .venv/bin/activate # Let's get inside it 45 | 46 | pip install -U pip # Upgrade pip 47 | ``` 48 | 49 | Once you have created a Python virtual environment and activated it, you can install the T3SF framework for your desired platform by running the following command: 50 | 51 | ```bash 52 | pip install "T3SF[Discord]" # Install the framework to work with Discord 53 | ``` 54 | or 55 | 56 | ```bash 57 | pip install "T3SF[Slack]" # Install the framework to work with Slack 58 | ``` 59 | 60 | This will install the T3SF framework along with the required dependencies for your chosen platform. Once the installation is complete, you can start using the framework with your platform of choice. 61 | 62 | We strongly recommend following the platform-specific guidance within our Read The Docs! Here are the links: 63 | 64 | - [Discord](https://t3sf.readthedocs.io/en/latest/Discord.html#installation) 65 | - [Slack](https://t3sf.readthedocs.io/en/latest/Slack.html#installation) 66 | - [Telegram](https://t3sf.readthedocs.io/en/latest/Telegram.html#installation) 67 | - [WhatsApp](https://t3sf.readthedocs.io/en/latest/WhatsApp.html#installation) 68 | 69 | ## Usage 70 | We created this framework to simplify all your work! 71 | 72 | 73 |
74 | Using Docker 75 | 76 | ### Supported Tags 77 | - slack → This image has all the requirements to perform an exercise in Slack. 78 | - discord → This image has all the requirements to perform an exercise in Discord. 79 | 80 | 81 | #### Using it with Slack 82 | 83 | ```bash 84 | $ docker run --rm -t --env-file .env -v $(pwd)/MSEL.json:/app/MSEL.json base4sec/t3sf:slack 85 | ``` 86 | 87 | Inside your `.env` file you have to provide the `SLACK_BOT_TOKEN` and `SLACK_APP_TOKEN` tokens. Read more about it [here](https://t3sf.readthedocs.io/en/latest/Slack.html#providing-the-tokens). 88 | 89 | There is another environment variable to set, `MSEL_PATH`. This variable tells the framework in which path the MSEL is located. By default, the container path is `/app/MSEL.json`. If you change the mount location of the volume then also change the variable. 90 | 91 | 92 | #### Using it with Discord 93 | 94 | ```bash 95 | $ docker run --rm -t --env-file .env -v $(pwd)/MSEL.json:/app/MSEL.json base4sec/t3sf:discord 96 | ``` 97 | 98 | Inside your `.env` file you have to provide the `DISCORD_TOKEN` token. Read more about it [here](https://t3sf.readthedocs.io/en/latest/Discord.html#providing-the-token). 99 | 100 | There is another environment variable to set, `MSEL_PATH`. This variable tells the framework in which path the MSEL is located. By default, the container path is `/app/MSEL.json`. If you change the mount location of the volume then also change the variable. 101 | 102 | --------------------- 103 |
104 | 105 | Once you have everything ready, use our template for the `main.py`, or modify the following code: 106 | 107 | Here is an example if you want to run the framework with the `Discord` bot and a `GUI`. 108 | 109 | ```python 110 | from T3SF import T3SF 111 | import asyncio 112 | 113 | async def main(): 114 | await T3SF.start(MSEL="MSEL_TTX.json", platform="Discord", gui=True) 115 | 116 | if __name__ == '__main__': 117 | asyncio.run(main()) 118 | ``` 119 | 120 | Or if you prefer to run the framework without `GUI` and with `Slack` instead, you can modify the arguments, and that's it! 121 | 122 | Yes, that simple! 123 | 124 | ```python 125 | await T3SF.start(MSEL="MSEL_TTX.json", platform="Slack", gui=False) 126 | ``` 127 | 128 | If you need more help, you can always check our documentation [here](https://t3sf.readthedocs.io/en/latest/)! 129 | -------------------------------------------------------------------------------- /examples/MSEL_EXAMPLE.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "#": 1, 4 | "Real Time": "07:29 PM", 5 | "Date": "Monday 9:30 AM", 6 | "Subject": "Anomalous Files Detected", 7 | "From": "Amazon Web Services", 8 | "Player": "Information Security", 9 | "Script": "We detected some anomalous files in your S3 Bucket.", 10 | "Photo": "https://img2.helpnetsecurity.com/posts2018/aws-s3-buckets-public.jpg", 11 | "Picture Name": "S3_Bucket.png", 12 | "Profile": "https://upload.wikimedia.org/wikipedia/commons/thumb/9/93/Amazon_Web_Services_Logo.svg/1024px-Amazon_Web_Services_Logo.svg.png", 13 | "Poll": "" 14 | }, 15 | { 16 | "#": 2, 17 | "Real Time": "07:30 PM", 18 | "Date": "Monday 9:40 AM", 19 | "Subject": "SOC - Internal Phising Alert", 20 | "From": "SOC - BASE4", 21 | "Player": "SRE", 22 | "Script": "Please note that we detected a large number of phishing emails coming from different email addresses.\n\nIgnore messages from emails with the address @notanemail.net", 23 | "Photo": "", 24 | "Picture Name": "Base_4_SOC.jpg", 25 | "Profile": "https://sdn.signalhire.co/storage/company/67a2/3012/f549/4d22/ce41/5f01/f4b4/820e.webp", 26 | "Poll": "" 27 | }, 28 | { 29 | "#": 3, 30 | "Real Time": "07:31 PM", 31 | "Date": "Monday 10:00 AM", 32 | "Subject": "New possible data breach", 33 | "From": "Internal", 34 | "Player": "Data Governance", 35 | "Script": "Hey Guys!\n\nI found some files from the company exposed online, please take a look!", 36 | "Photo": "https://assets-global.website-files.com/5efc3ccdb72aaa7480ec8179/60dc1c3c984ef85123cb0f7b_LinkedIn-Data-Breach-700-million-.png", 37 | "Picture Name": "attachment.jpg", 38 | "Profile": "", 39 | "Poll": "" 40 | }, 41 | { 42 | "#": 4, 43 | "Real Time": "07:31 PM", 44 | "Date": "Monday 10:00 AM", 45 | "Subject": "MIT vs GPL", 46 | "From": "Joaquin Lanfranconi", 47 | "Player": "Legal", 48 | "Script": "Hey folks!\n\nI'm about to release a new tool, but I'm not sure which license is better, can you help with this one?\n\nThanks in advance!", 49 | "Photo": "", 50 | "Picture Name": "", 51 | "Profile": "", 52 | "Poll": "MIT | GPL" 53 | }, 54 | { 55 | "#": 5, 56 | "Real Time": "07:32 PM", 57 | "Date": "Monday 11:00 AM", 58 | "Subject": "I've encrypted all your files!", 59 | "From": "h4x0r@notanemail.net", 60 | "Player": "PR/Comm", 61 | "Script": "Hello, \n\nI'm inside your company, I've encrypted all your files, if you want to recover them, check the attached file.", 62 | "Picture Name": "attachment.jpg", 63 | "Profile": "", 64 | "Photo": "https://cdn2.hubspot.net/hubfs/486579/lp/academy/malware/wannacry_screenshot.png", 65 | "Poll": "" 66 | }, 67 | { 68 | "#": 6, 69 | "Real Time": "07:32 PM", 70 | "Date": "Monday 11:00 AM", 71 | "Subject": "I've encrypted all your files!", 72 | "From": "h4x0r@notanemail.net", 73 | "Player": "Information Security", 74 | "Script": "Hello, \n\nI'm inside your company, I've encrypted all your files, if you want to recover them, check the attached file.", 75 | "Picture Name": "attachment.jpg", 76 | "Profile": "", 77 | "Photo": "https://cdn2.hubspot.net/hubfs/486579/lp/academy/malware/wannacry_screenshot.png", 78 | "Poll": "" 79 | }, 80 | { 81 | "#": 7, 82 | "Real Time": "07:32 PM", 83 | "Date": "Monday 11:00 AM", 84 | "Subject": "I've encrypted all your files!", 85 | "From": "h4x0r@notanemail.net", 86 | "Player": "SRE", 87 | "Script": "Hello, \n\nI'm inside your company, I've encrypted all your files, if you want to recover them, check the attached file.", 88 | "Picture Name": "attachment.jpg", 89 | "Profile": "", 90 | "Photo": "https://cdn2.hubspot.net/hubfs/486579/lp/academy/malware/wannacry_screenshot.png", 91 | "Poll": "" 92 | }, 93 | { 94 | "#": 8, 95 | "Real Time": "07:32 PM", 96 | "Date": "Monday 11:00 AM", 97 | "Subject": "I've encrypted all your files!", 98 | "From": "h4x0r@notanemail.net", 99 | "Player": "Data Governance", 100 | "Script": "Hello, \n\nI'm inside your company, I've encrypted all your files, if you want to recover them, check the attached file.", 101 | "Picture Name": "attachment.jpg", 102 | "Profile": "", 103 | "Photo": "https://cdn2.hubspot.net/hubfs/486579/lp/academy/malware/wannacry_screenshot.png", 104 | "Poll": "" 105 | }, 106 | { 107 | "#": 9, 108 | "Real Time": "07:32 PM", 109 | "Date": "Monday 11:00 AM", 110 | "Subject": "I've encrypted all your files!", 111 | "From": "h4x0r@notanemail.net", 112 | "Player": "Legal", 113 | "Script": "Hello, \n\nI'm inside your company, I've encrypted all your files, if you want to recover them, check the attached file.", 114 | "Picture Name": "attachment.jpg", 115 | "Profile": "", 116 | "Photo": "https://cdn2.hubspot.net/hubfs/486579/lp/academy/malware/wannacry_screenshot.png", 117 | "Poll": "" 118 | }, 119 | { 120 | "#": 10, 121 | "Real Time": "07:32 PM", 122 | "Date": "Monday 11:00 AM", 123 | "Subject": "I've encrypted all your files!", 124 | "From": "h4x0r@notanemail.net", 125 | "Player": "People", 126 | "Script": "Hello, \n\nI'm inside your company, I've encrypted all your files, if you want to recover them, check the attached file.", 127 | "Picture Name": "attachment.jpg", 128 | "Profile": "", 129 | "Photo": "https://cdn2.hubspot.net/hubfs/486579/lp/academy/malware/wannacry_screenshot.png", 130 | "Poll": "" 131 | }, 132 | { 133 | "#": 11, 134 | "Real Time": "07:33 PM", 135 | "Date": "Monday 11:10 AM", 136 | "Subject": "Encrypted Files!", 137 | "From": "Internal", 138 | "Player": "Data Governance", 139 | "Script": "Guys, we have a major issue!\nWhat should we do in this case, they are asking for a ransom of 2 BTC!\n\nInfoSec confirmed the encrypted files and everything!\nShould we pay them?", 140 | "Picture Name": "encrypted_files.jpg", 141 | "Profile": "", 142 | "Photo": "https://cdn2.hubspot.net/hubfs/486579/lp/academy/malware/wannacry_screenshot.png", 143 | "Poll": "Pay the ransom | Don’t pay it" 144 | }, 145 | { 146 | "#": 12, 147 | "Real Time": "07:35 PM", 148 | "Date": "Monday 11:15 AM", 149 | "Subject": "Data breach in your company", 150 | "From": "New York Times", 151 | "Player": "PR/Comm", 152 | "Script": "Hello, \nWe have information about a data breach in your company.\nCould you please confirm, what kind of data is compromised?\nThanks!", 153 | "Picture Name": "", 154 | "Profile": "https://1000logos.net/wp-content/uploads/2017/04/Symbol-New-York-Times.png", 155 | "Photo": "", 156 | "Poll": "" 157 | } 158 | ] -------------------------------------------------------------------------------- /T3SF/gui/static/js/vanilla-jsoneditor/themes/jse-theme-default.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --jse-theme: light; 3 | 4 | /* over all fonts, sizes, and colors */ 5 | --jse-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, 6 | Cantarell, 'Helvetica Neue', sans-serif; 7 | /* "consolas" for Windows, "menlo" for Mac with fallback to "monaco", 'Ubuntu Mono' for Ubuntu */ 8 | /* (at Mac this font looks too large at 14px, but 13px is too small for the font on Windows) */ 9 | --jse-font-family-mono: consolas, menlo, monaco, 'Ubuntu Mono', 'source-code-pro', monospace; 10 | --jse-font-size-mono: 14px; 11 | --jse-font-size: 16px; 12 | --jse-font-size-text-mode-search: 80%; 13 | --jse-line-height: calc(1em + 4px); 14 | --jse-indent-size: calc(1em + 4px); 15 | --jse-color-picker-button-size: 1em; 16 | --jse-padding: 10px; 17 | --jse-theme-color: #3883fa; 18 | --jse-theme-color-highlight: #5f9dff; 19 | --jse-background-color: #fff; 20 | --jse-text-color: #4d4d4d; 21 | --jse-text-readonly: #8d8d8d; 22 | --jse-text-color-inverse: #fff; 23 | --jse-error-color: #ee5341; 24 | --jse-warning-color: #fdc539; 25 | 26 | /* main, menu, modal */ 27 | --jse-main-border: 1px solid #d7d7d7; 28 | --jse-menu-color: var(--jse-text-color-inverse); 29 | --jse-menu-button-size: 32px; 30 | --jse-modal-background: #f5f5f5; 31 | --jse-modal-overlay-background: rgba(0, 0, 0, 0.3); 32 | --jse-modal-code-background: rgba(0, 0, 0, 0.05); 33 | 34 | /* jsoneditor modal */ 35 | --jse-modal-theme-color: #707070; 36 | --jse-modal-theme-color-highlight: #646464; 37 | 38 | /* tooltip in text mode */ 39 | --jse-tooltip-color: var(--jse-text-color); 40 | --jse-tooltip-background: var(--jse-modal-background); 41 | --jse-tooltip-border: var(--jse-main-border); 42 | --jse-tooltip-action-button-color: var(--jse-text-color-inverse); 43 | --jse-tooltip-action-button-background: #4d4d4d; 44 | 45 | /* panels: navigation bar, gutter, search box */ 46 | --jse-panel-background: #ebebeb; 47 | --jse-panel-color: var(--jse-text-color); 48 | --jse-panel-color-readonly: #b2b2b2; 49 | --jse-panel-border: var(--jse-main-border); 50 | --jse-panel-button-color: inherit; 51 | --jse-panel-button-background: transparent; 52 | --jse-panel-button-color-highlight: var(--jse-text-color); 53 | --jse-panel-button-background-highlight: #e0e0e0; 54 | 55 | /* navigation-bar */ 56 | --jse-navigation-bar-background: var(--jse-background-color); 57 | --jse-navigation-bar-background-highlight: #e5e5e5; 58 | --jse-navigation-bar-dropdown-color: #656565; 59 | 60 | /* context menu */ 61 | --jse-context-menu-background: #656565; 62 | --jse-context-menu-background-highlight: #7a7a7a; 63 | --jse-context-menu-color: var(--jse-text-color-inverse); 64 | --jse-context-menu-color-disabled: #9d9d9d; 65 | --jse-context-menu-separator-color: #7a7a7a; 66 | --jse-context-menu-pointer-hover-background: #b2b2b2; 67 | --jse-context-menu-pointer-background: var(--jse-context-menu-background); 68 | --jse-context-menu-pointer-background-highlight: var(--jse-context-menu-background-highlight); 69 | --jse-context-menu-pointer-color: var(--jse-context-menu-color); 70 | --jse-context-menu-pointer-size: calc(1em + 4px); 71 | --jse-context-menu-tip-background: rgba(255, 255, 255, 0.2); 72 | --jse-context-menu-tip-color: inherit; 73 | 74 | /* contents: json key and values */ 75 | --jse-key-color: #1a1a1a; 76 | --jse-value-color: #1a1a1a; 77 | --jse-value-color-number: #ee422e; 78 | --jse-value-color-boolean: #ff8c00; 79 | --jse-value-color-null: #004ed0; 80 | --jse-value-color-string: #008000; 81 | --jse-value-color-url: #008000; 82 | --jse-delimiter-color: rgba(0, 0, 0, 0.38); 83 | --jse-edit-outline: 2px solid #656565; 84 | 85 | /* contents: selected or hovered */ 86 | --jse-contents-background-color: transparent; 87 | --jse-contents-cursor: pointer; 88 | --jse-contents-selected-cursor: grab; 89 | --jse-selection-background-color: #d3d3d3; 90 | --jse-selection-background-inactive-color: #e8e8e8; 91 | --jse-hover-background-color: rgba(0, 0, 0, 0.06); 92 | --jse-active-line-background-color: rgba(0, 0, 0, 0.06); 93 | --jse-search-match-background-color: #99ff7780; 94 | 95 | /* contents: section of collapsed items in an array */ 96 | --jse-collapsed-items-background-color: #f5f5f5; 97 | --jse-collapsed-items-selected-background-color: #c2c2c2; 98 | --jse-collapsed-items-link-color: rgba(0, 0, 0, 0.38); 99 | --jse-collapsed-items-link-color-highlight: #ee5341; 100 | 101 | /* contents: highlighting of search matches */ 102 | --jse-search-match-color: #ffe665; 103 | --jse-search-match-outline: 1px solid #ffd700; 104 | --jse-search-match-active-color: #ffd700; 105 | --jse-search-match-active-outline: 1px solid #e1be00; 106 | 107 | /* contents: inline tags inside the JSON document */ 108 | --jse-tag-background: rgba(0, 0, 0, 0.2); 109 | --jse-tag-color: var(--jse-text-color-inverse); 110 | 111 | /* contents: table */ 112 | --jse-table-header-background: #f5f5f5; 113 | --jse-table-header-background-highlight: #e8e8e8; 114 | --jse-table-row-odd-background: rgba(0, 0, 0, 0.05); 115 | 116 | /* controls in modals: inputs, buttons, and `a` */ 117 | --jse-controls-box-shadow: 0 2px 6px 0 rgba(0, 0, 0, 0.24); 118 | --jse-input-background: var(--jse-background-color); 119 | --jse-input-background-readonly: transparent; 120 | --jse-input-border: 1px solid #d8dbdf; 121 | --jse-input-border-focus: 1px solid var(--jse-theme-color); 122 | --jse-input-radius: 3px; 123 | --jse-button-background: #e0e0e0; 124 | --jse-button-background-highlight: #e7e7e7; 125 | --jse-button-color: var(--jse-text-color); 126 | --jse-button-primary-background: var(--jse-theme-color); 127 | --jse-button-primary-background-highlight: var(--jse-theme-color-highlight); 128 | --jse-button-primary-background-disabled: #9d9d9d; 129 | --jse-button-primary-color: var(--jse-text-color-inverse); 130 | --jse-button-secondary-background: #d3d3d3; 131 | --jse-button-secondary-background-highlight: #e1e1e1; 132 | --jse-button-secondary-background-disabled: #9d9d9d; 133 | --jse-button-secondary-color: var(--jse-text-color); 134 | --jse-a-color: #156fc5; 135 | --jse-a-color-highlight: #0f508d; 136 | 137 | /* messages */ 138 | --jse-message-error-background: var(--jse-error-color); 139 | --jse-message-error-color: var(--jse-text-color-inverse); 140 | --jse-message-warning-background: #ffde5c; 141 | --jse-message-warning-color: var(--jse-text-color); 142 | --jse-message-success-background: #9ac45d; 143 | --jse-message-success-color: var(--jse-text-color-inverse); 144 | --jse-message-info-background: #4f91ff; 145 | --jse-message-info-color: var(--jse-text-color-inverse); 146 | --jse-message-action-background: rgba(255, 255, 255, 0.2); 147 | --jse-message-action-background-highlight: rgba(255, 255, 255, 0.3); 148 | 149 | /* svelte-select */ 150 | --item-is-active-bg: #3883fa; 151 | --border: 1px solid #d8dbdf; 152 | --border-radius: 3px; 153 | --background: #fff; 154 | --padding: 0 10px; 155 | --multi-select-padding: 0 10px; 156 | 157 | /* color picker */ 158 | --jse-color-picker-background: var(--jse-panel-background); 159 | --jse-color-picker-border-box-shadow: #cbcbcb 0 0 0 1px; 160 | } 161 | -------------------------------------------------------------------------------- /docs/T3SF.Handlers.rst: -------------------------------------------------------------------------------- 1 | ************************ 2 | Multi Platform Handlers 3 | ************************ 4 | 5 | These functions, unlike the CORE ones, will be like handlers, calling other platform-based functions depending on the platform selected in the ``platform`` variable when starting the framework. 6 | 7 | So if we need to send a message, we'll use :py:meth:`SendMessage` and in there we'll handle the correct platform to send the message. 8 | 9 | .. py:function:: SendMessage(title:str=None, description:str=None, color_ds:str=None, color_sl:str=None, channel=None, image=None, author=None, buttons=None, text_input=None, checkboxes=None) 10 | :async: 11 | 12 | Message sending controller for all platforms. 13 | 14 | .. confval:: title 15 | 16 | The title of the message. 17 | 18 | :type: ``str`` 19 | :required: ``True`` 20 | 21 | .. confval:: description 22 | 23 | The description/main text of the message. 24 | 25 | :type: ``str`` 26 | :required: ``True`` 27 | 28 | .. confval:: color_ds 29 | 30 | Parameter with the color of the embedded message. 31 | 32 | .. note:: 33 | This parameter is only used for Discord. 34 | 35 | :type: ``str`` 36 | :required: ``False`` 37 | 38 | .. confval:: color_sl 39 | 40 | Parameter with the color of the embedded message. 41 | 42 | .. note:: 43 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 44 | 45 | :type: ``str`` 46 | :required: ``False`` 47 | 48 | 49 | .. confval:: channel 50 | 51 | Parameter with the desired destination channel. 52 | 53 | .. note:: 54 | This parameter is only used for Slack. 55 | 56 | :type: ``str`` 57 | :required: ``False`` 58 | 59 | .. confval:: image 60 | 61 | .. note:: 62 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 63 | 64 | :type: ``array`` 65 | :required: ``False`` 66 | 67 | .. confval:: author 68 | 69 | .. note:: 70 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 71 | 72 | :type: ``array`` 73 | :required: ``False`` 74 | 75 | .. confval:: buttons 76 | 77 | .. note:: 78 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 79 | 80 | :type: ``array`` 81 | :required: ``False`` 82 | 83 | .. confval:: text_input 84 | 85 | .. note:: 86 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 87 | 88 | :type: ``array`` 89 | :required: ``False`` 90 | 91 | .. confval:: checkboxes 92 | 93 | .. note:: 94 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 95 | 96 | :type: ``array`` 97 | :required: ``False`` 98 | 99 | .. py:function:: EditMessage(title:str=None, description:str=None, color_ds:str=None, color_sl:str=None, response=None, variable=None, image=None, author=None, buttons=None, text_input=None, checkboxes=None) 100 | :async: 101 | 102 | Message editing controller for all platforms (which allow editing messages). 103 | 104 | .. confval:: title 105 | 106 | The title of the message. 107 | 108 | :type: ``str`` 109 | :required: ``True`` 110 | 111 | .. confval:: description 112 | 113 | The description/main text of the message. 114 | 115 | :type: ``str`` 116 | :required: ``True`` 117 | 118 | .. confval:: color_ds 119 | 120 | Parameter with the color of the embedded message. 121 | 122 | .. note:: 123 | This parameter is only used for Discord. 124 | 125 | :type: ``str`` 126 | :required: ``False`` 127 | 128 | .. confval:: color_sl 129 | 130 | Parameter with the color of the embedded message. 131 | 132 | .. note:: 133 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 134 | 135 | :type: ``str`` 136 | :required: ``False`` 137 | 138 | 139 | .. confval:: response 140 | 141 | Parameter with the previous response. 142 | 143 | .. note:: 144 | This parameter is only used for Slack. 145 | 146 | :type: ``array`` 147 | :required: ``False`` 148 | 149 | .. confval:: variable 150 | 151 | Parameter with the previous response, containing the method to edit messages. 152 | 153 | .. note:: 154 | This parameter is only used for Discord. 155 | 156 | :type: ``str`` 157 | :required: ``False`` 158 | 159 | .. confval:: image 160 | 161 | .. note:: 162 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 163 | 164 | :type: ``array`` 165 | :required: ``False`` 166 | 167 | .. confval:: author 168 | 169 | .. note:: 170 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 171 | 172 | :type: ``array`` 173 | :required: ``False`` 174 | 175 | .. confval:: buttons 176 | 177 | .. note:: 178 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 179 | 180 | :type: ``array`` 181 | :required: ``False`` 182 | 183 | .. confval:: text_input 184 | 185 | .. note:: 186 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 187 | 188 | :type: ``array`` 189 | :required: ``False`` 190 | 191 | .. confval:: checkboxes 192 | 193 | .. note:: 194 | This parameter is only used for Slack. Go to `Slack.Formatter `_ for references. 195 | 196 | :type: ``array`` 197 | :required: ``False`` 198 | 199 | .. py:function:: SendIncident(inject) 200 | :async: 201 | 202 | Send the current incident to the correct player. 203 | 204 | .. confval:: inject 205 | 206 | :type: ``array`` 207 | :required: ``True`` 208 | 209 | .. py:function:: RegexHandler(ack=None, body=None, payload=None, inbox=None) 210 | :async: 211 | 212 | In charge of the inboxes gathering part. 213 | 214 | .. note:: 215 | This function is just used by WhatsApp and Slack. 216 | 217 | .. confval:: ack 218 | 219 | :type: ``object`` 220 | :required: ``False`` 221 | 222 | .. confval:: body 223 | 224 | :type: ``array`` 225 | :required: ``false`` 226 | 227 | .. confval:: payload 228 | 229 | :type: ``array`` 230 | :required: ``false`` 231 | 232 | .. confval:: inbox 233 | 234 | :type: ``str`` 235 | :required: ``false`` 236 | 237 | .. py:function:: InboxesAuto(message=None) 238 | :async: 239 | 240 | Handler for the Automatic gathering of inboxes. 241 | 242 | .. confval:: message 243 | 244 | :type: ``str`` 245 | :required: ``False`` 246 | 247 | .. py:function:: SendPoll(inject) 248 | :async: 249 | 250 | Send the current poll to the correct player. 251 | 252 | .. confval:: inject 253 | 254 | :type: ``array`` 255 | :required: ``True`` 256 | 257 | .. py:function:: PollAnswerHandler(ack=None, body=None, payload=None, query=None) 258 | :async: 259 | 260 | Detects the answer in the poll sent. Modifies the poll message and notifies the game master about the selected option. 261 | 262 | .. confval:: ack 263 | 264 | Acknowledge object to inform Slack that we have received the interaction. 265 | 266 | :type: ``obj`` 267 | :required: ``False`` 268 | .. note:: 269 | This parameter is only used for Slack. 270 | 271 | .. confval:: body 272 | 273 | The body of the interaction. 274 | 275 | :type: ``obj`` 276 | :required: ``False`` 277 | .. note:: 278 | This parameter is only used for Slack. 279 | 280 | .. confval:: payload 281 | 282 | The user's input. 283 | 284 | :type: ``obj`` 285 | :required: ``False`` 286 | .. note:: 287 | This parameter is only used for Slack. 288 | 289 | .. confval:: query 290 | 291 | The query of the message. 292 | 293 | :type: ``obj`` 294 | :required: ``False`` 295 | .. note:: 296 | This parameter is only used for Discord. -------------------------------------------------------------------------------- /T3SF/gui/static/js/vanilla-jsoneditor/README.md: -------------------------------------------------------------------------------- 1 | # vanilla-jsoneditor 2 | 3 | A web-based tool to view, edit, format, transform, and validate JSON. 4 | 5 | Try it out: https://jsoneditoronline.org 6 | 7 | This is the vanilla variant of `svelte-jsoneditor`, which can be used in vanilla JavaScript or frameworks like SolidJS, React, Vue, Angular. 8 | 9 | ![JSONEditor tree mode screenshot](https://raw.githubusercontent.com/josdejong/svelte-jsoneditor/main/misc/jsoneditor_tree_mode_screenshot.png) 10 | ![JSONEditor text mode screenshot](https://raw.githubusercontent.com/josdejong/svelte-jsoneditor/main/misc/jsoneditor_text_mode_screenshot.png) 11 | ![JSONEditor table mode screenshot](https://raw.githubusercontent.com/josdejong/svelte-jsoneditor/main/misc/jsoneditor_table_mode_screenshot.png) 12 | 13 | ## Features 14 | 15 | - View and edit JSON 16 | - Has a low level text editor and high level tree view and table view 17 | - Format (beautify) and compact JSON 18 | - Sort, query, filter, and transform JSON 19 | - Repair JSON 20 | - JSON schema validation and pluggable custom validation 21 | - Color highlighting, undo/redo, search and replace 22 | - Utilities like a color picker and timestamp tag 23 | - Handles large JSON documents up to 512 MB 24 | 25 | ## Install 26 | 27 | Install using npm: 28 | 29 | ``` 30 | npm install vanilla-jsoneditor 31 | ``` 32 | 33 | Remark: for usage in a Svelte project, install `svelte-jsoneditor` instead. 34 | 35 | ## Use (Browser example loading the ES module): 36 | 37 | ```html 38 | 39 | 40 | 41 | JSONEditor 42 | 43 | 44 |
45 | 46 | 75 | 76 | 77 | ``` 78 | 79 | ## Use (React example, including NextJS) 80 | 81 | ### First, create a React component to wrap the vanilla-jsoneditor 82 | 83 | Depending on whether you are using JavaScript of TypeScript, create either a JSX or TSX file: 84 | 85 | ### TypeScript: 86 | 87 | ```typescript 88 | // 89 | // JSONEditorReact.tsx 90 | // 91 | import { useEffect, useRef } from 'react' 92 | import { JSONEditor, JSONEditorPropsOptional } from 'vanilla-jsoneditor' 93 | 94 | const JSONEditorReact: React.FC = (props) => { 95 | const refContainer = useRef(null) 96 | const refEditor = useRef(null) 97 | 98 | useEffect(() => { 99 | // create editor 100 | refEditor.current = new JSONEditor({ 101 | target: refContainer.current!, 102 | props: {} 103 | }) 104 | 105 | return () => { 106 | // destroy editor 107 | if (refEditor.current) { 108 | refEditor.current.destroy() 109 | refEditor.current = null 110 | } 111 | } 112 | }, []) 113 | 114 | useEffect(() => { 115 | // update props 116 | if (refEditor.current) { 117 | refEditor.current.updateProps(props) 118 | } 119 | }, [props]) 120 | 121 | return
122 | } 123 | 124 | export default JSONEditorReact 125 | ``` 126 | 127 | ### JavaScript 128 | 129 | ```javascript 130 | // 131 | // JSONEditorReact.jsx 132 | // 133 | import { useEffect, useRef } from 'react' 134 | import { JSONEditor, JSONEditorPropsOptional } from 'vanilla-jsoneditor' 135 | 136 | const JSONEditorReact = (props) => { 137 | const refContainer = useRef(null) 138 | const refEditor = useRef(null) 139 | 140 | useEffect(() => { 141 | // create editor 142 | refEditor.current = new JSONEditor({ 143 | target: refContainer.current, 144 | props: {} 145 | }) 146 | 147 | return () => { 148 | // destroy editor 149 | if (refEditor.current) { 150 | refEditor.current.destroy() 151 | refEditor.current = null 152 | } 153 | } 154 | }, []) 155 | 156 | // update props 157 | useEffect(() => { 158 | if (refEditor.current) { 159 | refEditor.current.updateProps(props) 160 | } 161 | }, [props]) 162 | 163 | return
164 | } 165 | 166 | export default JSONEditorReact 167 | ``` 168 | 169 | ### Import and use the React component 170 | 171 | If you are using NextJS, you will need to use a dynamic import to only render the component in the browser (disabling server-side rendering of the wrapper), as shown below in a NextJS TypeScript example. 172 | 173 | If you are using React in an conventional non-NextJS browser app, you can import the component using a standard import statement like `import JSONEditorReact from '../JSONEditorReact'` 174 | 175 | ```typescript 176 | // 177 | // demo.tsx for use with NextJS 178 | // 179 | import dynamic from 'next/dynamic' 180 | import { useCallback, useState } from 'react' 181 | 182 | // 183 | // In NextJS, when using TypeScript, type definitions 184 | // can be imported from 'vanilla-jsoneditor' using a 185 | // conventional import statement (prefixed with 'type', 186 | // as shown below), but only types can be imported this 187 | // way. When using NextJS, React components and helper 188 | // functions must be imported dynamically using { ssr: false } 189 | // as shown elsewhere in this example. 190 | // 191 | import type { Content, OnChangeStatus } from 'vanilla-jsoneditor' 192 | 193 | // 194 | // In NextJS, the JSONEditor component must be wrapped in 195 | // a component that is dynamically in order to turn off 196 | // server-side rendering of the component. This is neccessary 197 | // because the vanilla-jsoneditor code attempts to use 198 | // browser-only JavaScript capabilities not available 199 | // during server-side rendering. Any helper functions 200 | // provided by vanilla-jsoneditor, such as toTextContent, 201 | // must also only be used in dynamically imported, 202 | // ssr: false components when using NextJS. 203 | // 204 | const JSONEditorReact = dynamic(() => import('../JSONEditorReact'), { ssr: false }) 205 | const TextContent = dynamic(() => import('../TextContent'), { ssr: false }) 206 | 207 | const initialContent = { 208 | hello: 'world', 209 | count: 1, 210 | foo: ['bar', 'car'] 211 | } 212 | 213 | export default function Demo() { 214 | const [jsonContent, setJsonContent] = useState({ json: initialContent }) 215 | const handler = useCallback( 216 | (content: Content, previousContent: Content, status: OnChangeStatus) => { 217 | setJsonContent(content) 218 | }, 219 | [jsonContent] 220 | ) 221 | 222 | return ( 223 |
224 | 225 | 226 |
227 | ) 228 | } 229 | ``` 230 | 231 | ```typescript 232 | // 233 | // TextContent.tsx 234 | // 235 | // (wrapper around toTextContent for use with NextJS) 236 | // 237 | import { Content, toTextContent } from 'vanilla-jsoneditor' 238 | 239 | interface IOwnProps { 240 | content: Content 241 | } 242 | const TextContent = (props: IOwnProps) => { 243 | const { content } = props 244 | 245 | return ( 246 |

247 | The contents of the editor, converted to a text string, are: {toTextContent(content).text} 248 |

249 | ) 250 | } 251 | 252 | export default TextContent 253 | ``` 254 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [2.5.1] - 2023-10-30 5 | Version 2.5.1 includes minor bug fixes and some new commands for platform-specific bots. 6 | 7 | ### New 8 | - New management commands for Discord's Bot 9 | - !pause / !resume / !stop are the new commands for the platform. They work exactly as the buttons on the GUI. 10 | - New management commands for Slack's Bot 11 | - !pause / !resume / !stop are the new commands for the platform. They work exactly as the buttons on the GUI. 12 | 13 | ### Fixes 14 | - Fixed a bug where if upper case was used when setting the platform to "Discord", the GUI would not send the server ID. 15 | - Resolved an issue related to the framework's status in the GUI. When changing the log verbose level, the only button that appeared was the "Start" button. 16 | 17 | ## [2.5] - 2023-08-02 18 | Upgrade to version 2.5 of the T3SF framework for exciting new features, bug fixes, and an enhanced user experience. This release introduces the MSEL Playground, Dark mode, a Database with AI-generated Events, and much more! 19 | 20 | ### New 21 | - Introducing the MSEL Playground, providing real-time editing capabilities for MSEL files. 22 | - Added support for uploading .xls/.xlsx files and converting them to JSON format. 23 | - Implemented the ability to save modified or converted MSEL files. 24 | - Refreshed the nav-bar style for an improved visual experience. 25 | - Created a new static folder for local web resources. 26 | - Modified the source for certain images used in the application. 27 | - Added a function to set the Discord server ID. 28 | - Introduced a Dark mode option. 29 | - Implemented a Database with AI-generated Events. 30 | - Included a FAQ section addressing common TTX design questions. 31 | - Added a Platform indicator on the GUI. 32 | - Expanded the poll options to support more choices. 33 | - Added "Resume," "Pause," and "Abort" buttons on the GUI. 34 | - Swapped the functionality of the "Stop" and "Abort" buttons. 35 | - Included a random data generator button for the MSEL. 36 | - Added a Database with comprehensive example scenarios. 37 | - Improved clarity of script execution explanations. 38 | 39 | ### Fixes 40 | - Fixed a bug in the log viewer where the "Framework status" text wouldn't display without available logs. 41 | - Rewrote the log display to address visual issues when selecting or when logs were too long. 42 | - Resolved an issue where exception messages were not properly displayed on the GUI. 43 | 44 | ## [2.1] - 2023-06-02 45 | Upgrade to version 2.1 of the T3SF framework to benefit from an array of new features, bug fixes, and enhanced stability. This release brings important updates and automation to streamline various processes within the framework. 46 | 47 | ### Fixes: 48 | - Slack: 49 | -Addressed a bug related to the usage of regular expressions by users. 50 | - Discord: 51 | - Resolved an issue with the automatic environment creation where the Game Master (GM) chat was incorrectly labeled as "chat" instead of "gm-chat". 52 | - GUI: 53 | - The Start button now correctly re-enables after restarting the framework. 54 | - The default logging level has been set to "INFO" to resolve a recursion error that occurred when no logging level was selected. 55 | 56 | ### New Additions: 57 | - Docker Images: 58 | - Docker images with specific tags are now available for each currently supported platform, facilitating deployment and management. 59 | - Slack: 60 | - Introduced a new sanitization function to ensure proper formatting of channel names. 61 | - Discord: 62 | - Implemented a fail-safe validation for server ID inputs. 63 | - Added a new alert to notify users when attempting to start the bot without the Game Master (GM) role from the Discord platform. 64 | 65 | ## [2.0] - 2023-05-05 66 | 67 | Upgrade to version 2.0 for new features, bug fixes, and improved stability. In this version we have automated some important processes. 68 | 69 | ### Added 70 | - New navigation bar added to the GUI. 71 | - Now you can see the status of the framework from the logs viewer. 72 | - New MSEL Viewer! You can now load a MSEL in JSON format to check every inject's detail. 73 | - Automatic Environment creation, starting from this version you can automatically create the exercise's environment. 74 | 75 | ### Updated 76 | - Updated documentation to match all the functions, features, classes and odules of version 2.0. 77 | 78 | ### Fixed 79 | - The communication between the SSE client and server was poorly performed, which generated an infinite loop every 3 seconds. 80 | 81 | ## [1.2] - 2023-04-05 82 | 83 | Official release of v1.2 - New features, bug fixes, and stability improvements included! 84 | 85 | ### Newly added 86 | - Now for **Slack** and **Discord**, you no longer need to create a bot, now the framework handles completely that part with integrated bots. 87 | - Guided User Interface (GUI) for management purposes. Now you can control, see the logs and exercise status in real time in a newly designed Web interface. 88 | - New framework's modular structure. 89 | - New simplified user's input, now you can run a TTX with just a few lines of code, less than 10! 90 | - New class to log all the output from the bots and framework, such as status, errors, warnings and more. 91 | 92 | ### Updated 93 | - Updated the code to work with all updates to the specified platform libraries. 94 | - Requirements are now fullfilled when installing the framework with the specified module, as `pip3 install "T3SF[Platform]"`. 95 | 96 | ### Deprecated 97 | - This versions is not ready for platforms as **Telegram** and **WhatsApp**, we are constantly working to update the framework for all the platforms. If you want to use the framework with those platform, use the previous `Version 1.1`. 98 | 99 | ## [1.1] - 2022-10-28 100 | 101 | Version 1.1 has been officially released, with new features, bug fixes and stability issues resolved! 102 | 103 | ### Added 104 | - An option has been added for players to respond to polls for analytical purposes. Available for **Slack**, **Discord** and **Telegram**! 105 | 106 | ### Fixed 107 | - A problem with the Discord bot has been fixed. 108 | - We have changed the way to check options such as "Photos" and "Profile picture" in the injects. 109 | 110 | ### Updated 111 | - We have updated all the bots dependencies and made some changes to make them work with the new versions. 112 | 113 | 114 | ## [1.0] - 2022-04-20 115 | 116 | Releasing the official public version of the framework! 117 | 118 | ### Added 119 | - Added the proper project's documentation. 120 | - Added a better and nicer README. 121 | - Added contributing guidelines. 122 | - Added platform-dependat files, such as `.env` `requirements.txt` and `bot.py`. 123 | - Added support for more platforms: **WhatsApp** and **Telegram**! 124 | - Added a To-Do list with ideas for next versions. 125 | - Added the templates for issues and feature request. 126 | - Added a few examples of the injects per platform as illustrative manner. 127 | - New config file added as a way to configurate the framework. 128 | - Framework added to PyPi for easier download. 129 | 130 | ### Changed 131 | - Now the framework has a module for each platform's functions. 132 | - There are new platform-wide handlers. 133 | 134 | 135 | ## [0.5] - 2022-04-03 [Private] 136 | 137 | Now the framework supports Slack! 138 | 139 | ### Added 140 | - Adding the Slack version of the bot. 141 | - Main features migrated. 142 | - Few functions exclusives to Slack created, such as format and inboxes fetcher. 143 | - Adding a better way to separate Platforms with Folders. 144 | - Adding a README with a How to for installation an a brief description of the bot and platforms. 145 | 146 | 147 | ## [0.4] - 2022-02-02 [Private] 148 | 149 | Launching the latest _and better_ version of the framework supporting Discord! 150 | 151 | ### Added 152 | - Auto regex finder to match the categories' names 153 | - Auto match with player's name from the MSEL.json and actual inbox's name 154 | - Inbox now stored in a text file after getting the correct ones! 155 | - New format for the injects! Now they have a profile picture for the sender 156 | 157 | ### Changed 158 | -Now the start and resume function are merged into 1 process function! 159 | 160 | ### Fixed 161 | - Fixed a bug within the resume function, injects not being chosen correctly! 162 | 163 | 164 | ## [0.3] - 2022-01-29 [Private] 165 | 166 | Releasing a new feature for our unique platform, Discord, to add a new and better way to present injects 167 | 168 | ### Added 169 | - Accepting new key from the JSON ("Photo") so we can attach a picture to the injects. 170 | - Added exceptions handling for better debug. 171 | 172 | ### Remarkable tests 173 | - Tested on education environment, with real case scenarios. 174 | 175 | 176 | ## [0.2] - 2021-12-13 [Private] 177 | 178 | Releasing the second version, after many tests where new ideas emerged. 179 | 180 | ### Added 181 | - New "Resume" function, to restart the bot in a certain Incident Id. 182 | - New messages to inform the steps of the bots, such as remaining incidents, waiting time. 183 | - New function to obtain automatically the channel's IDs [Discord]. 184 | 185 | ### Changed 186 | - Commented code. 187 | - Code cleanup. 188 | 189 | ### Remarkable tests 190 | - Tested on education environment, with real case scenarios. 191 | 192 | 193 | ## [0.1] - 2021-09-19 [Private] 194 | 195 | Releasing the very first early version of the framework for internal testing. 196 | 197 | ### Added 198 | - Basics functions suchs as framework's "start" and incidents handling. 199 | - Adding Discord support. [Only platform]. 200 | 201 | ### Special Thanks 202 | Based on the We Learn Cybersecurity team idea (Matías Sliafertas, Gastón Ureta and Federico Pacheco) 203 | -------------------------------------------------------------------------------- /T3SF/slack/bot.py: -------------------------------------------------------------------------------- 1 | from slack_bolt.adapter.socket_mode.async_handler import AsyncSocketModeHandler 2 | from slack_bolt.app.async_app import AsyncApp 3 | from slack_sdk.errors import SlackApiError 4 | from dotenv import load_dotenv 5 | from .slack import Slack 6 | from T3SF import utils 7 | import warnings 8 | import asyncio 9 | import os, re 10 | import json 11 | import T3SF 12 | 13 | load_dotenv() 14 | 15 | # Disable the UserWarning message 16 | warnings.filterwarnings("ignore", category=UserWarning) 17 | 18 | app_slack = None 19 | T3SF_instance = None 20 | config_MSEL = None 21 | 22 | 23 | class create_bot(): 24 | def __init__(self, MSEL=None): 25 | global config_MSEL 26 | config_MSEL = MSEL 27 | pass 28 | 29 | async def slack_main(self): 30 | global app_slack 31 | app_slack = self.app_slack = AsyncApp(token=os.environ["SLACK_BOT_TOKEN"]) 32 | 33 | handler = AsyncSocketModeHandler(self.app_slack, os.environ["SLACK_APP_TOKEN"]) 34 | 35 | @app_slack.action(re.compile("regex")) 36 | async def regex_handler(ack, body, payload): 37 | await T3SF.T3SF.RegexHandler(self=T3SF_instance,ack=ack,body=body,payload=payload) 38 | 39 | @app_slack.action(re.compile("option")) 40 | async def poll_handler(ack, body, payload): 41 | await T3SF.T3SF.PollAnswerHandler(self=T3SF_instance,ack=ack,body=body,payload=payload) 42 | 43 | @app_slack.message("!start") 44 | async def start(message, say): 45 | """ 46 | Retrieves the !start command and starts to fetch and send the incidents. 47 | """ 48 | try: 49 | global T3SF_instance 50 | 51 | # Setting variables for GUI 52 | utils.process_quit = False 53 | utils.process_wait = False 54 | utils.process_started = True 55 | 56 | T3SF_instance = T3SF.T3SF(platform="slack", app=app_slack) 57 | await T3SF_instance.ProcessIncidents(MSEL = config_MSEL, function_type = "start", ctx=message) 58 | 59 | except Exception as e: 60 | print("ERROR - Start function") 61 | print(e) 62 | raise 63 | 64 | @app_slack.message("!stop") 65 | async def stop(message, say): 66 | """ 67 | Retrieves the !stop command and stops the incidents. 68 | """ 69 | try: 70 | await say(attachments=Slack.Formatter(color="RED", title=":black_square_for_stop: Exercise stopped", description="The exercise has stopped, start it again when you are ready!\n*Please note that maybe one remaining inject will be sent.*")) 71 | # Setting variables for GUI 72 | utils.process_quit = True 73 | 74 | except Exception as e: 75 | print("ERROR - Stop function") 76 | print(e) 77 | raise 78 | 79 | @app_slack.message("!pause") 80 | async def pause(message, say): 81 | """ 82 | Retrieves the !pause command and pauses the incidents. 83 | """ 84 | try: 85 | # Setting variables for GUI 86 | await say(attachments=Slack.Formatter(color="YELLOW", title=":double_vertical_bar: Exercise paused", description="The exercise is now paused, resume it when you are ready!\n*Please note that maybe one remaining inject will be sent.*")) 87 | utils.process_wait = True 88 | 89 | except Exception as e: 90 | print("ERROR - Pause function") 91 | print(e) 92 | raise 93 | 94 | @app_slack.message("!resume") 95 | async def resume(message, say): 96 | """ 97 | Retrieves the !resume command and resumes to fetch and send the incidents. 98 | """ 99 | try: 100 | await say(attachments=Slack.Formatter(color="GREEN", title=":black_right_pointing_triangle_with_double_vertical_bar: Exercise resumed", description="The exercise has been resumed at the point where it was interrupted.")) 101 | # Setting variables for GUI 102 | utils.process_wait = False 103 | utils.process_quit = False 104 | utils.process_started = True 105 | 106 | except Exception as e: 107 | print("ERROR - Resume function") 108 | print(e) 109 | raise 110 | 111 | @app_slack.message('!ping') 112 | async def ping(message, say): 113 | """ 114 | Retrieves the !ping command and replies with this message. 115 | """ 116 | description = """ 117 | PING localhost (127.0.0.1): 56 data bytes\n64 bytes from 127.0.0.1: icmp_seq=0 ttl=113 time=37.758 ms\n64 bytes from 127.0.0.1: icmp_seq=1 ttl=113 time=50.650 ms\n64 bytes from 127.0.0.1: icmp_seq=2 ttl=113 time=42.493 ms\n64 bytes from 127.0.0.1: icmp_seq=3 ttl=113 time=37.637 ms\n--- localhost ping statistics ---\n4 packets transmitted, 4 packets received, 0.0% packet loss\nround-trip min/avg/max/stddev = 37.637/42.135/50.650/5.292 ms\n\n_This is not real xD_""" 118 | await app_slack.client.chat_postMessage(channel = message['channel'], attachments = Slack.Formatter(color="GREEN", title="🏓 Pong!", description=description)) 119 | 120 | @app_slack.event("message") 121 | async def not_interesting_messages(body, logger): 122 | pass 123 | 124 | await handler.start_async() 125 | 126 | async def start_bot(): 127 | task1 = asyncio.create_task(create_bot(config_MSEL).slack_main()) 128 | T3SF.T3SF_Logger.emit(message=f'Slack Bot is ready!', message_type="DEBUG") 129 | await asyncio.gather(task1) 130 | 131 | async def run_async_incidents(): 132 | global T3SF_instance 133 | T3SF_instance = T3SF.T3SF(platform="slack", app=app_slack) 134 | await T3SF_instance.ProcessIncidents(MSEL = config_MSEL, function_type = "start", ctx={"channel":"gm-chat"}) 135 | 136 | async def create_environment(): 137 | global T3SF_instance 138 | 139 | T3SF_instance = T3SF.T3SF(platform="slack", app=app_slack) 140 | areas_msel = T3SF_instance.IncidentsFetcher(MSEL = config_MSEL) 141 | 142 | admins_users = await get_admins() 143 | 144 | await create_gm_channels(admins=admins_users) 145 | 146 | try: 147 | inbox_name = "inbox" 148 | extra_chnls = ['chat','decision-log'] 149 | players_list_local = areas_msel 150 | 151 | channels_ids = [] 152 | 153 | for player in players_list_local: 154 | inbox = inbox_name + "-" + sanitize_channel_name(player) 155 | try: 156 | channel_id_inbox = await create_channel_if_not_exists(channel_name=inbox, private=True) 157 | 158 | if channel_id_inbox: 159 | await app_slack.client.conversations_invite(channel=channel_id_inbox, users=admins_users) 160 | 161 | channels_ids.append(channel_id_inbox) 162 | 163 | T3SF.T3SF_Logger.emit(message=f'Player [{player}] - Channel {inbox_name} created', message_type="DEBUG") 164 | 165 | for extra in extra_chnls: 166 | channel = extra + "-" + sanitize_channel_name(player) 167 | 168 | channel_id_extra = await create_channel_if_not_exists(channel_name=channel, private=True) 169 | if channel_id_extra: 170 | await app_slack.client.conversations_invite(channel=channel_id_extra, users=admins_users) 171 | 172 | channels_ids.append(channel_id_extra) 173 | 174 | T3SF.T3SF_Logger.emit(message=f'Player [{player}] - Channel {channel} created', message_type="DEBUG") 175 | 176 | except Exception as e: 177 | print("ERROR - Create function") 178 | print(e) 179 | pass 180 | 181 | T3SF.T3SF_Logger.emit(message=f"Player's Channels created!", message_type="INFO") 182 | print(channels_ids) 183 | T3SF.T3SF_Logger.emit(message=f'Channels IDs: {channels_ids}', message_type="DEBUG") 184 | 185 | except Exception as e: 186 | print("ERROR - Create function") 187 | print(e) 188 | raise 189 | 190 | async def get_admins(): 191 | admins_users = [] 192 | try: 193 | response = await app_slack.client.users_list() 194 | # Loop through the list of users and check if they're an admin 195 | for user in response["members"]: 196 | try: 197 | if user["is_admin"]: 198 | # Store the user ID of the workspace admin 199 | admins_users.append(user['id']) 200 | except KeyError: 201 | pass 202 | return admins_users 203 | 204 | except SlackApiError as e: 205 | if e.response["error"] == "ratelimited": 206 | T3SF.T3SF_Logger.emit(message=f'Slack is rate limiting us. Please wait a few seconds and try again.', message_type="ERROR") 207 | 208 | except Exception as e: 209 | print("ERROR - Get Admins function") 210 | print(e) 211 | raise 212 | 213 | async def create_channel_if_not_exists(channel_name, private=True): 214 | try: 215 | # Call the conversations_list method using the app client 216 | response = await app_slack.client.conversations_list() 217 | 218 | # Check if a channel with the given name already exists 219 | channel_id = None 220 | for channel in response["channels"]: 221 | if channel["name"] == channel_name: 222 | channel_id = channel["id"] 223 | break 224 | 225 | # If the channel doesn't exist, create it using the conversations_create method 226 | if not channel_id: 227 | response = await app_slack.client.conversations_create(name=channel_name ,is_private=private) 228 | channel_id = response["channel"]["id"] 229 | 230 | # Return the ID of the channel 231 | return channel_id 232 | 233 | except SlackApiError as e: 234 | if e.response["error"] == "name_taken": 235 | T3SF.T3SF_Logger.emit(message=f'Channel {channel_name} already taken.', message_type="WARN") 236 | else: 237 | print(f"Error: {e}") 238 | return False 239 | 240 | async def create_gm_channels(admins): 241 | channels = ['gm-chat','gm-logs'] 242 | for channel in channels: 243 | channel_id = await create_channel_if_not_exists(channel) 244 | if channel_id: 245 | await app_slack.client.conversations_invite(channel=channel_id, users=admins) 246 | 247 | T3SF.T3SF_Logger.emit(message=f'Game Master channels created.', message_type="INFO") 248 | 249 | return True 250 | 251 | def sanitize_channel_name(name): 252 | # Replace whitespace with a hyphen 253 | sanitized_name = re.sub(r"\s", "-", name) 254 | 255 | # Remove special characters except for hyphens and periods 256 | sanitized_name = re.sub(r"[^\w.-]", "-", sanitized_name) 257 | 258 | # Replace consecutive hyphens and periods with a single hyphen 259 | sanitized_name = re.sub(r"[-.]+", "-", sanitized_name) 260 | 261 | # Remove leading and trailing hyphens 262 | sanitized_name = sanitized_name.strip("-") 263 | 264 | # Convert to lowercase 265 | sanitized_name = sanitized_name.lower() 266 | 267 | # Truncate to a maximum length of 80 characters 268 | sanitized_name = sanitized_name[:80] 269 | 270 | return sanitized_name -------------------------------------------------------------------------------- /T3SF/discord/bot.py: -------------------------------------------------------------------------------- 1 | from discord.ext import commands 2 | from dotenv import load_dotenv 3 | from T3SF import utils 4 | import discord 5 | import asyncio 6 | import re,os 7 | import T3SF 8 | 9 | load_dotenv() 10 | 11 | TOKEN = os.environ['DISCORD_TOKEN'] 12 | 13 | intents = discord.Intents.default() 14 | intents.message_content = True 15 | bot_discord = commands.Bot(command_prefix='!', intents=intents) 16 | 17 | # Defining global variables and events 18 | T3SF_instance = None 19 | start_incidents_gui = asyncio.Event() 20 | create_env = asyncio.Event() 21 | config_MSEL = None 22 | 23 | class create_bot(): 24 | def __init__(self, MSEL): 25 | global config_MSEL 26 | config_MSEL = MSEL 27 | self.define_commands() 28 | 29 | def define_commands(self): 30 | global bot_discord 31 | 32 | @bot_discord.event 33 | async def on_ready(): 34 | """ 35 | Print some infomation about the bot when it connects succesfully. 36 | """ 37 | print(f'\n * Discord Bot - {bot_discord.user} is ready!') 38 | T3SF.T3SF_Logger.emit(message=f'Discord Bot - {bot_discord.user} is ready!', message_type="DEBUG") 39 | 40 | @bot_discord.event 41 | async def on_interaction(interaction): 42 | await T3SF.T3SF.PollAnswerHandler(self=T3SF_instance, payload=interaction) 43 | 44 | @bot_discord.command(name='ping', help='Responds with pong to know if the bot is up! Usage -> !ping') 45 | async def ping_command(ctx): 46 | """ 47 | Retrieves the !ping command and replies with the latency. 48 | """ 49 | description = f""" 50 | **Pong!** 51 | 52 | :stopwatch: `{round(bot_discord.latency*1000)}ms` 53 | """ 54 | response = await ctx.send(embed=discord.Embed(colour=discord.Colour.blue(), description=description)) 55 | 56 | @bot_discord.command(name="start", help='Starts the Incidents Game. Usage -> !start') 57 | @commands.has_role("Game Master") 58 | async def start_command(ctx, *, query=None): 59 | """ 60 | Retrieves the !start command and starts to fetch and send the incidents. 61 | """ 62 | try: 63 | global T3SF_instance 64 | 65 | # Setting variables for GUI 66 | utils.process_quit = False 67 | utils.process_wait = False 68 | utils.process_started = True 69 | 70 | T3SF_instance = T3SF.T3SF(platform="discord", bot=bot_discord) 71 | await T3SF_instance.ProcessIncidents(MSEL = config_MSEL, function_type = "start", ctx=ctx) 72 | 73 | except Exception as e: 74 | print("ERROR - Start function") 75 | print(e) 76 | raise 77 | 78 | @bot_discord.command(name="stop", help='Stops the Incidents Game. Usage -> !stop') 79 | @commands.has_role("Game Master") 80 | async def stop_command(ctx, *, query=None): 81 | """ 82 | Retrieves the !stop command and stops the incidents. 83 | """ 84 | try: 85 | response = await ctx.send(embed=discord.Embed(colour=discord.Colour.red(), title=":stop_button: Exercise stopped", description="The exercise has stopped, start it again when you are ready!\n**Please note that maybe one remaining inject will be sent.**")) 86 | # Setting variables for GUI 87 | utils.process_quit = True 88 | 89 | except Exception as e: 90 | print("ERROR - Stop function") 91 | print(e) 92 | raise 93 | 94 | @bot_discord.command(name="pause", help='Pauses the Incidents Game. Usage -> !pause') 95 | @commands.has_role("Game Master") 96 | async def pause_command(ctx, *, query=None): 97 | """ 98 | Retrieves the !pause command and stops the incidents. 99 | """ 100 | try: 101 | response = await ctx.send(embed=discord.Embed(colour=discord.Colour.yellow(), title=":pause_button: Exercise paused", description="The exercise is now paused, resume it when you are ready!\n**Please note that maybe one remaining inject will be sent.**")) 102 | # Setting variables for GUI 103 | utils.process_wait = True 104 | 105 | except Exception as e: 106 | print("ERROR - Pause function") 107 | print(e) 108 | raise 109 | 110 | @bot_discord.command(name="resume", help='Resumes the Incidents Game. Usage -> !resume') 111 | @commands.has_role("Game Master") 112 | async def resume_command(ctx, *, query=None): 113 | """ 114 | Retrieves the !resume command and stops the incidents. 115 | """ 116 | try: 117 | response = await ctx.send(embed=discord.Embed(colour=discord.Colour.green(), title=":play_pause: Exercise resumed", description="The exercise has been resumed at the point where it was interrupted.")) 118 | # Setting variables for GUI 119 | utils.process_wait = False 120 | utils.process_quit = False 121 | utils.process_started = True 122 | 123 | except Exception as e: 124 | print("ERROR - Resume function") 125 | print(e) 126 | raise 127 | 128 | @bot_discord.event 129 | async def on_command_error(ctx, error): 130 | """ 131 | Inform if an error with the commands ocurred. 132 | """ 133 | if isinstance(error, commands.errors.CheckFailure): 134 | await ctx.send(embed=discord.Embed(colour=discord.Colour.red(), title="Error 401", description='You do not have the "Game Master" role to use this command.')) 135 | 136 | elif isinstance(error, discord.HTTPException): 137 | await ctx.send(embed=discord.Embed(colour=discord.Colour.red(), title="Error 5xx", description="We got ratelimited by Discord. Please wait a few seconds and try again.")) 138 | 139 | async def start_bot(): 140 | T3SF_instance = T3SF.T3SF(platform="discord", bot=bot_discord) 141 | task1 = asyncio.create_task(bot_discord.start(TOKEN)) 142 | task2 = asyncio.create_task(async_handler_exercise()) 143 | task3 = asyncio.create_task(create_environment_task()) 144 | 145 | # wait for the coroutines to finish 146 | await asyncio.gather(task1, task2, task3) 147 | 148 | async def async_handler_exercise(): 149 | global T3SF_instance 150 | while True: 151 | try: 152 | await asyncio.wait_for(start_incidents_gui.wait(), timeout=0.1) 153 | except asyncio.TimeoutError: 154 | pass 155 | else: 156 | T3SF_instance = T3SF.T3SF(platform="discord", bot=bot_discord) 157 | T3SF_instance.guild_id = server_id 158 | asyncio.create_task(T3SF_instance.ProcessIncidents(MSEL = config_MSEL, function_type="start", ctx=None)) 159 | 160 | start_incidents_gui.clear() 161 | 162 | async def run_async_incidents(server): 163 | global server_id 164 | server_id = server 165 | start_incidents_gui.set() 166 | 167 | async def create_environment(server): 168 | global server_id 169 | server_id = server 170 | create_env.set() 171 | 172 | async def create_environment_task(): 173 | global T3SF_instance 174 | while True: 175 | try: 176 | await asyncio.wait_for(create_env.wait(), timeout=0.1) 177 | except asyncio.TimeoutError: 178 | pass 179 | else: 180 | create_env.clear() 181 | 182 | T3SF_instance = T3SF.T3SF(platform="discord", bot=bot_discord) 183 | areas_msel = T3SF_instance.IncidentsFetcher(MSEL = config_MSEL) 184 | 185 | # Set the GUILD To the user's input server 186 | guild = bot_discord.get_guild(int(server_id)) 187 | 188 | inbox_name = "inbox" 189 | extra_chnls = ['chat','decision-log'] 190 | players_list_local = areas_msel 191 | 192 | await create_gm_channels(guild=guild) 193 | 194 | for player in players_list_local: 195 | 196 | player_normalized = player.lower().replace(" ", "-") 197 | 198 | role = await create_role_if_not_exists(guild=guild, name=player_normalized) 199 | 200 | if role: 201 | category_name = "Group - " + player 202 | category = await create_category_if_not_exists(guild=guild, name=category_name, private=True, role=role) 203 | 204 | if category: 205 | await create_channel_if_not_exists(category=category, name=inbox_name) 206 | T3SF.T3SF_Logger.emit(message=f'Player [{player}] - Channel {inbox_name} created', message_type="DEBUG") 207 | 208 | for channel in extra_chnls: 209 | await create_channel_if_not_exists(category=category, name=channel) 210 | T3SF.T3SF_Logger.emit(message=f'Player [{player}] - Channel {channel} created', message_type="DEBUG") 211 | 212 | await create_voice_if_not_exists(category=category, name="office") 213 | T3SF.T3SF_Logger.emit(message=f'Player [{player}] - Voice Channel created', message_type="DEBUG") 214 | 215 | T3SF.T3SF_Logger.emit(message=f"Player's Channels created!", message_type="INFO") 216 | 217 | async def create_role_if_not_exists(guild, name): 218 | try: 219 | role = discord.utils.get(guild.roles, name=name) 220 | 221 | if role is None: 222 | general = discord.Permissions(view_channel=True) 223 | text = discord.Permissions.text() 224 | voice = discord.Permissions.voice() 225 | role = await guild.create_role(name=name, permissions=general | text | voice, colour=discord.Colour.red()) 226 | return role 227 | else: 228 | return role 229 | 230 | except Exception as e: 231 | print(e) 232 | raise 233 | 234 | async def create_category_if_not_exists(guild, name, private=False, role=None): 235 | try: 236 | category = discord.utils.get(guild.categories, name=name) 237 | 238 | if category is None: 239 | category = await guild.create_category(name) 240 | 241 | if private: 242 | await category.set_permissions(guild.default_role, read_messages=False) 243 | await category.set_permissions(role, read_messages=True) 244 | 245 | return category 246 | else: 247 | return category 248 | 249 | except Exception as e: 250 | print(e) 251 | raise 252 | 253 | async def create_channel_if_not_exists(category, name): 254 | try: 255 | channel = discord.utils.get(category.channels, name=name) 256 | if channel is None: 257 | channel = await category.create_text_channel(name) 258 | return channel 259 | else: 260 | return channel 261 | except Exception as e: 262 | print(e) 263 | raise 264 | 265 | async def create_voice_if_not_exists(category, name): 266 | try: 267 | channel = discord.utils.get(category.channels, name=name) 268 | if channel is None: 269 | channel = await category.create_voice_channel(name) 270 | return channel 271 | else: 272 | return channel 273 | except Exception as e: 274 | print(e) 275 | raise 276 | 277 | async def create_gm_channels(guild): 278 | 279 | role = discord.utils.get(guild.roles, name="Game Master") 280 | 281 | if role is None: 282 | all_perms = discord.Permissions.all() 283 | role = await guild.create_role(name="Game Master", permissions=all_perms, colour=discord.Colour.green()) 284 | 285 | category = await create_category_if_not_exists(guild=guild, name="Control Room", private=True, role=role) 286 | channels = ['gm-chat','logs'] 287 | 288 | for channel in channels: 289 | await create_channel_if_not_exists(category=category, name=channel) 290 | 291 | await create_voice_if_not_exists(category=category, name="Game Masters") 292 | 293 | T3SF.T3SF_Logger.emit(message=f'Game Master channels created.', message_type="INFO") 294 | 295 | return True -------------------------------------------------------------------------------- /T3SF/discord/discord.py: -------------------------------------------------------------------------------- 1 | import discord 2 | import asyncio 3 | import random 4 | import json 5 | import re 6 | from T3SF import utils, T3SF_Logger 7 | 8 | class Discord(object): 9 | def __init__(self, bot): 10 | self.bot = bot 11 | 12 | async def InboxesAuto(self, T3SF_instance): 13 | mensaje_inboxes = "" 14 | image_example = "https://i.ibb.co/NCrPD3Y/discord-exp.png" 15 | 16 | if T3SF_instance.fetch_inboxes == True: 17 | if T3SF_instance._ctx == None: 18 | # Exercise started from the GUI 19 | guild = self.bot.get_guild(int(T3SF_instance.guild_id)) 20 | channels = guild.text_channels 21 | 22 | for channel in channels: 23 | if "chat" in channel.name: 24 | accuracy = utils.similar(str(channel.name).lower(),"gm-chat") 25 | if accuracy >= 0.8: 26 | inbox = channel.id 27 | T3SF_instance._ctx = self.bot.get_channel(channel.id) 28 | 29 | T3SF_instance.response_auto = await self.SendMessage(T3SF_instance=T3SF_instance, title="⚙️ Fetching inboxes...", description=f"Please wait while we fetch all the inboxes in this server!", color="BLUE") 30 | 31 | channels_itinerator = 0 32 | player_itinerator = 0 33 | regex = "" 34 | 35 | try: 36 | channels = T3SF_instance._ctx.message.guild.channels 37 | categories = T3SF_instance._ctx.message.guild.categories 38 | started_from_gui = False 39 | 40 | except Exception: 41 | channels = T3SF_instance._ctx.guild.channels 42 | categories = T3SF_instance._ctx.guild.categories 43 | started_from_gui = True 44 | 45 | while regex == "" and len(T3SF_instance.players_list) > player_itinerator: 46 | for category in categories: 47 | if channels_itinerator == 0: 48 | past_channel = category.name 49 | pass 50 | 51 | match_channel = utils.similar(str(category).lower(), str(T3SF_instance.players_list[0]).lower()) 52 | 53 | if match_channel >= 0.4: 54 | for character in past_channel: 55 | if character in category.name: 56 | regex += character 57 | else: 58 | break 59 | else: 60 | player_itinerator += 1 61 | channels_itinerator += 1 62 | past_channel = category.name 63 | pass 64 | 65 | T3SF_Logger.emit(message=f'Please confirm the Regular Expression for the inboxes on the gm-chat!', message_type="INFO") 66 | 67 | await T3SF_instance.response_auto.edit(embed=discord.Embed( 68 | title = "ℹ️ Regex detected!", 69 | description = f"Please confirm if the regex detected for the channels, is correct so we can get the inboxes!\n\nExample:\nGroup - Legal\nThe regex should be `Group -`\n\nDetected regex: `{regex}`", 70 | color = 0x77B255).set_image(url=image_example).set_footer(text="Please answer with [Yes(Y)/No(N)]")) 71 | 72 | 73 | def check_regex_channels(msg): 74 | if started_from_gui: 75 | return msg.content.lower() in ["y", "yes", "n", "no"] 76 | 77 | else: 78 | return msg.author == T3SF_instance._ctx.author and msg.channel == T3SF_instance._ctx.channel and msg.content.lower() in ["y", "yes", "n", "no"] 79 | 80 | try: 81 | msg = await self.bot.wait_for("message", check=check_regex_channels, timeout=50) 82 | 83 | if msg.content.lower() in ["y", "yes"]: 84 | await self.EditMessage(T3SF_instance=T3SF_instance, style="custom", variable="T3SF_instance.response_auto", color="GREEN", title = "✨ Regex detected succesfully! ✨", description = f"Thanks for confirming the regex detected for the channels (I'm going to tell my creator he is so good coding :D ), we are going to use `{regex}` to match the inboxes") 85 | 86 | elif msg.content.lower() in ["n", "no"]: 87 | await T3SF_instance.response_auto.edit(embed=discord.Embed( 88 | title = "ℹ️ Regex needed!", 89 | description = "Got it!\n Unluckily, but here we go...\nPlease send me the regex for the channels, so we can get the inboxes!\n\nExample:\nGroup - Legal\nThe regex should be `Group -`", 90 | color = discord.Colour.red()).set_image(url=image_example).set_footer(text="Please answer with the desired regex. EG: `Groups -`")) 91 | 92 | def get_regex_channels(msg_regex_user): 93 | # return msg_regex_user.author == T3SF_instance._ctx.author and msg_regex_user.channel == T3SF_instance._ctx.channel and 94 | 95 | if started_from_gui: 96 | return msg_regex_user.content != "" 97 | 98 | else: 99 | return msg_regex_user.author == T3SF_instance._ctx.author and msg_regex_user.channel == T3SF_instance._ctx.channel and msg_regex_user.content != "" 100 | 101 | msg_regex_user = await self.bot.wait_for("message", check=get_regex_channels, timeout=50) 102 | 103 | if msg_regex_user.content != "": 104 | regex = msg_regex_user 105 | await self.EditMessage(T3SF_instance=T3SF_instance, style="custom", variable="T3SF_instance.response_auto", color="GREEN", title="✅ Regex accepted!", description=f"Thanks for confirming the regex for the channels, we are going to use `{msg_regex_user.content}` to match the inboxes!") 106 | 107 | except asyncio.TimeoutError: 108 | await self.EditMessage(T3SF_instance=T3SF_instance, style="custom", variable="T3SF_instance.response_auto", color="RED", title = ":x: Sorry, you didn't reply on time", description = "Please start the process again.") 109 | raise RuntimeError("We didn't detect any regex, time's up.") 110 | 111 | for player in T3SF_instance.players_list: 112 | for channel in channels: 113 | category = channel.category 114 | if "inbox" in channel.name : 115 | accuracy = utils.similar(re.sub(f"({regex})", "",str(category)).lower(),str(player).lower()) 116 | if accuracy >= 0.4: 117 | T3SF_instance.inboxes_all[player] = channel.id 118 | 119 | json.dump(T3SF_instance.inboxes_all,open(f"inboxes_{T3SF_instance.platform}.json", "w")) 120 | 121 | for player in T3SF_instance.inboxes_all: 122 | mensaje_inboxes += f"**Inbox** {player}[{T3SF_instance.inboxes_all[player]}]\n" 123 | 124 | await self.EditMessage(T3SF_instance=T3SF_instance, style="custom", variable="T3SF_instance.response_auto", color="GREEN", title=f"📩 Inboxes fetched! [{len(T3SF_instance.inboxes_all)}]", description=mensaje_inboxes) 125 | 126 | T3SF_Logger.emit(message=f'Confirmed! Inboxes ready', message_type="INFO") 127 | 128 | return True 129 | 130 | async def InjectHandler(self, T3SF_instance): 131 | all_data = f'Date: {T3SF_instance._inject["Date"]}\n\n{T3SF_instance._inject["Script"]}' 132 | 133 | player = T3SF_instance._inject['Player'] 134 | 135 | inbox = self.bot.get_channel(T3SF_instance.inboxes_all[player]) 136 | 137 | embed = discord.Embed(title = T3SF_instance._inject['Subject'], description = all_data, color = discord.Colour.blue()) 138 | 139 | if "Photo" in T3SF_instance._inject and T3SF_instance._inject['Photo'] != '': 140 | embed.set_image(url=T3SF_instance._inject['Photo']) 141 | 142 | if "Profile" in T3SF_instance._inject and T3SF_instance._inject['Profile'] != '': 143 | profile_pic = T3SF_instance._inject['Profile'] 144 | else: 145 | profile_pic = random.choice([ 146 | "https://ssl.gstatic.com/ui/v1/icons/mail/profile_mask2.png", 147 | "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg", 148 | "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSTt8Dg9RL4IGOjsJ2Fr-lXThf-DGM5YgPB6j5rD8tHQ9RLrU-03H4dYeskL01FNajqL_0&usqp=CAU" 149 | ]) 150 | 151 | embed.set_author(name=T3SF_instance._inject["From"], icon_url=profile_pic) 152 | 153 | T3SF_instance.response_poll = await inbox.send(embed = embed) 154 | 155 | return embed 156 | 157 | async def PollHandler(self, T3SF_instance): 158 | T3SF_instance.poll_answered = False 159 | 160 | all_data = T3SF_instance._inject["Script"] 161 | 162 | poll_options = T3SF_instance._inject['Poll'].split('|') 163 | 164 | actual_real_time = re.sub("([^0-9])", "", T3SF_instance._inject['Real Time'])[-2:] 165 | 166 | next_real_time = re.sub("([^0-9])", "", T3SF_instance.data[int(T3SF_instance._inject['#'])]['Real Time'])[-2:] 167 | 168 | diff = int(next_real_time) - int(actual_real_time) 169 | if diff < 0: 170 | diff_no_real = int(actual_real_time) - int(next_real_time) 171 | diff = 60 - diff_no_real 172 | 173 | diff_secs = diff * 60 174 | 175 | view = discord.ui.View() 176 | for option in poll_options: 177 | view.add_item(discord.ui.Button(style=discord.ButtonStyle.primary, label=option, custom_id="poll|" + option)) 178 | 179 | all_data = all_data+ f"\n\nYou have {diff} minute(s) to answer this poll!" 180 | 181 | player = T3SF_instance._inject['Player'] 182 | 183 | inbox = self.bot.get_channel(T3SF_instance.inboxes_all[player]) 184 | 185 | embed = discord.Embed(title = T3SF_instance._inject['Subject'], description = all_data, color = discord.Colour.yellow()) 186 | 187 | if "Photo" in T3SF_instance._inject and T3SF_instance._inject['Photo'] != '': 188 | embed.set_image(url=T3SF_instance._inject['Photo']) 189 | 190 | T3SF_instance.response_poll = await inbox.send(embed = embed, view=view) 191 | 192 | return embed 193 | 194 | async def PollAnswerHandler(self, T3SF_instance, interaction=None): 195 | if "poll" in interaction.data['custom_id']: 196 | poll_msg_og = interaction.message.embeds.copy()[0] 197 | 198 | title = poll_msg_og.title 199 | 200 | poll_msg = poll_msg_og.description 201 | poll_msg = poll_msg[: poll_msg.rfind('\n')] 202 | 203 | action_user = interaction.user 204 | 205 | selected_option = interaction.data['custom_id'].split('|')[1] 206 | description = f'{poll_msg}\n\n@{action_user} selected: {selected_option}' 207 | 208 | T3SF_instance.poll_answered = True 209 | T3SF_instance.response_poll = await interaction.response.edit_message(embed=discord.Embed(colour=discord.Colour.green(), title=title, description=description),view=None) 210 | await T3SF_instance.NotifyGameMasters(type_info="poll_answered", data={'msg_poll':poll_msg,'answer':selected_option,'user':action_user}) 211 | return True 212 | else: 213 | pass 214 | 215 | async def SendMessage(self, T3SF_instance, color="CYAN", title:str=None, description:str=None, view=None, unique=False): 216 | colors = {'BLUE' : discord.Colour.dark_blue(), 'RED' : discord.Colour.red(), 'CYAN' : discord.Colour.blue(), 'GREEN' : discord.Colour.green(), 'YELLOW' : discord.Colour.yellow()} 217 | 218 | if unique == True: 219 | T3SF_instance.gm_poll_msg = await T3SF_instance._ctx.send(embed=discord.Embed(color=colors[color], title = title, description = description), view=view) 220 | return T3SF_instance.gm_poll_msg 221 | 222 | else: 223 | T3SF_instance.response = await T3SF_instance._ctx.send(embed=discord.Embed(color=colors[color], title = title, description = description), view=view) 224 | return T3SF_instance.response 225 | 226 | async def EditMessage(self, T3SF_instance=None, color="CYAN", title:str=None, description:str=None, view=None, style="simple", variable=None): 227 | 228 | if style == "simple": 229 | T3SF_instance.response = T3SF_instance.response.edit(embed=discord.Embed(color=colors[color], title = title, description = description), view=view) 230 | return T3SF_instance.response 231 | else: 232 | colors = {'BLUE' : discord.Colour.dark_blue(), 'RED' : discord.Colour.red(), 'CYAN' : discord.Colour.blue(), 'GREEN' : discord.Colour.green(), 'YELLOW' : discord.Colour.yellow()} 233 | variable = eval(variable) 234 | await variable.edit(embed=discord.Embed(color=colors[color], title = title, description = description), view=view) -------------------------------------------------------------------------------- /docs/Discord.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Discord 3 | ******************* 4 | 5 | .. contents:: Table of Contents 6 | 7 | Discord was the main platform for the bot! Many new features, bug fixes and tests since the first release! 8 | 9 | Bot 10 | =============== 11 | 12 | To perform an exercise based on the Discord platform, you will need to provide a Bot token. If you already have a bot created, you can skip the creation part and go straight to the provisioning part. 13 | 14 | Bot creation 15 | ------------------ 16 | 17 | 1. Navigate to `Discord Developers `_ 18 | 2. Create a new application 19 | 3. Go to the *Bot* tab 20 | #. On the *Privileged Gateway Intents* enable all the intents 21 | #. Enable *PRESENCE INTENT* 22 | #. Enable *SERVER MEMBERS INTENT* 23 | #. Enable *MESSAGE CONTENT INTENT* 24 | 4. Reset the bot token 25 | 5. Copy it and keep it in a safe place, because you will only see it once. 26 | 27 | 28 | Inviting the bot to a server 29 | ------------------------------ 30 | 31 | 1. Navigate to `Discord Developers `_ 32 | 2. Select you recently created Application 33 | 3. Go now to the *OAuth2* tab 34 | 4. Select the URL Generator 35 | 5. On *SCOPES*, select *"bot"* and on *BOT PERMISSIONS*, select *"Administrator"* 36 | 6. Navigate to the generate URL 37 | 7. You will be asked for the name of the server you want to add the bot to. *(You must be a user with server privileges or the server owner)* 38 | 8. Once the server is selected, authorize the bot to get Administrator privileges. 39 | 9. Done! 40 | 41 | Starting the Framework 42 | ======================== 43 | 44 | Once you have installed in all the `libraries dependent on the platform `_, created your bot and added it to your server, you will need to provide the token to the framework for it to work and choose the Discord platform on ``T3SF.start``. 45 | 46 | Providing the token 47 | ------------------------ 48 | 49 | The framework expects a bot token with the name ``DISCORD_TOKEN``. 50 | 51 | You have two common options for this: 52 | 53 | 1. Create a ``.env`` file 54 | #. On the same path as your ``main.py`` file create a ``.env`` file 55 | #. Inside of it, add the variable ``DISCORD_TOKEN`` and your bot's token, as following: ``DISCORD_TOKEN=MTEpMDU2OTc9MjQ0NDE4ODc2Mw.GP0bxK.5J2xWb3D40zSIRxYiJgGlNiTSq8OkSR4xCcvpY`` 56 | 57 | .. note:: Note that the token will be stored and everyone with read access to the file will be able to read it. 58 | 59 | 2. Export the variable to your shell environment 60 | #. Create a variable with the name ``DISCORD_TOKEN`` as following: ``export DISCORD_TOKEN=MTEpMDU2OTc9MjQ0NDE4ODc2Mw.GP0bxK.5J2xWb3D40zSIRxYiJgGlNiTSq8OkSR4xCcvpY`` 61 | 62 | Initializing the framework 63 | ---------------------------- 64 | 65 | As explained in the `Initializing T3SF `_ page, you will need to set 3 variables inside your ``main.py`` file. 66 | 67 | This example is for an exercise using Discord with a GUI: 68 | 69 | .. code-block:: python3 70 | 71 | from T3SF import T3SF 72 | import asyncio 73 | 74 | async def main(): 75 | await T3SF.start(MSEL="MSEL_Company.json", platform="Discord", gui=True) 76 | 77 | if __name__ == '__main__': 78 | asyncio.run(main()) 79 | 80 | And that's it! 81 | 82 | Module 83 | ====== 84 | 85 | To maintain the modular structure of the framework, we developed a module with all the platform specific functions inside. Including the integrated bot and the functions to contact the Discord API. 86 | 87 | The file structure is shown below: 88 | 89 | .. code-block:: bash 90 | 91 | Discord 92 | ├── bot.py 93 | ├── discord.py 94 | └── __init__.py 95 | 96 | Class Functions 97 | ----------------- 98 | 99 | .. py:function:: SendMessage(color=None, style:str="simple", title:str=None, description:str=None) 100 | :async: 101 | 102 | Message sending controller. 103 | 104 | .. confval:: color 105 | 106 | Parameter with the color of the embedded message. 107 | 108 | :type: ``str`` 109 | :required: ``False`` 110 | 111 | .. confval:: title 112 | 113 | The title of the message. 114 | 115 | :type: ``str`` 116 | :required: ``True`` 117 | 118 | .. confval:: description 119 | 120 | The description/main text of the message. 121 | 122 | :type: ``str`` 123 | :required: ``True`` 124 | 125 | .. py:function:: EditMessage(color=None, style:str="simple", title:str=None, description:str=None) 126 | :async: 127 | 128 | Message editing controller. 129 | 130 | .. confval:: color 131 | 132 | Parameter with the color of the embedded message. 133 | 134 | :type: ``str`` 135 | :required: ``False`` 136 | 137 | .. confval:: title 138 | 139 | The title of the message. 140 | 141 | :type: ``str`` 142 | :required: ``True`` 143 | 144 | .. confval:: description 145 | 146 | The description/main text of the message. 147 | 148 | :type: ``str`` 149 | :required: ``True`` 150 | 151 | .. py:function:: InboxesAuto(self) 152 | :async: 153 | 154 | Fetches automatically all the inboxes, based in a regular expression (RegEx), notifies the Game masters about differents parts of this process. 155 | 156 | .. py:function:: InjectHandler(T3SF_instance) 157 | :async: 158 | 159 | This method handles the injection of a message in a specific channel using a Discord bot. 160 | 161 | .. confval:: T3SF_instance 162 | 163 | An instance of the T3SF class. 164 | 165 | :type: ``obj`` 166 | :required: ``True`` 167 | 168 | .. py:function:: PollHandler(T3SF_instance) 169 | :async: 170 | 171 | Handles the injects with polls. Creates the poll with the two options and sends it to the player's channel. 172 | 173 | .. confval:: T3SF_instance 174 | 175 | An instance of the T3SF class. 176 | 177 | :type: ``obj`` 178 | :required: ``True`` 179 | 180 | .. py:function:: PollAnswerHandler(T3SF_instance, interaction=None) 181 | :async: 182 | 183 | Detects the answer in the poll sent. Modifies the poll message and notifies the game master about the selected option. 184 | 185 | .. confval:: T3SF_instance 186 | 187 | An instance of the T3SF class. 188 | 189 | :type: ``obj`` 190 | :required: ``True`` 191 | 192 | .. confval:: interaction 193 | 194 | The received interaction. 195 | 196 | :type: ``obj`` 197 | :required: ``False`` 198 | 199 | .. py:function:: similar(a, b) 200 | 201 | Based in graphics, find the similarity between 2 strings. 202 | 203 | .. confval:: a 204 | 205 | :type: ``str`` 206 | :required: ``True`` 207 | 208 | .. confval:: b 209 | 210 | :type: ``str`` 211 | :required: ``True`` 212 | 213 | 214 | Integrated bot 215 | ----------------- 216 | 217 | We integrated the bot to fully manage the platform from within the framework. The bot handles poll responses, commands and environment creation. 218 | 219 | .. py:class:: create_bot(MSEL) 220 | 221 | This class creates the bot, will handle the commands, messages and interactions with it. 222 | 223 | .. confval:: MSEL 224 | 225 | The location of the MSEL. 226 | 227 | :type: ``str`` 228 | :required: ``True`` 229 | 230 | .. py:method:: define_commands() 231 | 232 | Within this method, we will create the following command management functions 233 | 234 | .. py:function:: on_ready() 235 | :async: 236 | 237 | Detects when the bot is ready to receive commands and process messages. 238 | 239 | .. py:function:: on_interaction(interaction) 240 | :async: 241 | 242 | Detects when the bot receives an interaction (as a response to a poll). 243 | 244 | .. py:function:: ping_command(ctx) 245 | :async: 246 | 247 | Handles the ``!ping`` command and returns a `pong` message. 248 | 249 | .. confval:: ctx 250 | 251 | The context of the received message. 252 | 253 | :type: ``obj`` 254 | :required: ``True`` 255 | 256 | .. py:function:: start_command(ctx, *, query=None) 257 | :async: 258 | 259 | Handles the ``!start`` command and starts the exercise. 260 | 261 | .. confval:: ctx 262 | 263 | The context of the received message. 264 | 265 | :type: ``obj`` 266 | :required: ``True`` 267 | 268 | .. confval:: query 269 | 270 | The query of the message. 271 | 272 | :type: ``obj`` 273 | :required: ``False`` 274 | 275 | .. py:function:: start_bot() 276 | :async: 277 | 278 | This functions will start the bot, but also generate tasks for the `async_handler_exercise <#async_handler_exercise>`_ `create_environment_task <#create_environment_task>`_ listeners. 279 | 280 | .. note:: Because the library we are using is asynchronous and the exercise can be started directly from the GUI, we need to add this *"listeners"* to start it without problems. 281 | 282 | .. py:function:: async_handler_exercise() 283 | :async: 284 | 285 | This function waits for the ``start_incidents_gui`` global event to be triggered and starts the exercise. 286 | 287 | .. py:function:: run_async_incidents() 288 | :async: 289 | 290 | This function sets the ``start_incidents_gui`` global event to start the exercise. 291 | 292 | .. py:function:: create_environment_task() 293 | :async: 294 | 295 | This function waits for the ``create_env`` global event to be triggered and creates the environment. 296 | 297 | .. py:function:: create_environment(server) 298 | :async: 299 | 300 | This function sets the ``create_env`` global event to create the environment. 301 | 302 | .. confval:: server 303 | 304 | The server/guild identifier. 305 | 306 | :type: ``int`` 307 | :required: ``True`` 308 | 309 | .. py:function:: create_role_if_not_exists(guild, name) 310 | :async: 311 | 312 | Create a role within the guild if it does not already exist. 313 | 314 | .. confval:: guild 315 | 316 | The guild identifier. 317 | 318 | :type: ``int`` 319 | :required: ``True`` 320 | 321 | .. confval:: name 322 | 323 | The role's name. 324 | 325 | :type: ``str`` 326 | :required: ``True`` 327 | 328 | .. py:function:: create_category_if_not_exists(guild, name, private=False, role=None) 329 | :async: 330 | 331 | Create a category within the guild if it does not already exist. 332 | 333 | .. confval:: guild 334 | 335 | The guild identifier. 336 | 337 | :type: ``str`` 338 | :required: ``True`` 339 | 340 | .. confval:: name 341 | 342 | The category's name. 343 | 344 | :type: ``str`` 345 | :required: ``True`` 346 | 347 | .. confval:: private 348 | 349 | Determines if the category should be private and only available to a specific role 350 | 351 | :type: ``bool`` 352 | :required: ``False`` 353 | 354 | .. confval:: role 355 | 356 | The role's name. 357 | 358 | :type: ``str`` 359 | :required: ``False`` | ``True`` if ``private`` is set to ``True`` 360 | 361 | .. py:function:: create_channel_if_not_exists(category, name) 362 | :async: 363 | 364 | Create a channel within the given category if it does not already exist. 365 | 366 | .. confval:: category 367 | 368 | The category in which the channel should be created. 369 | 370 | :type: ``str`` 371 | :required: ``True`` 372 | 373 | .. confval:: name 374 | 375 | The channel's name. 376 | 377 | :type: ``str`` 378 | :required: ``True`` 379 | 380 | .. py:function:: create_voice_if_not_exists(category, name) 381 | :async: 382 | 383 | Create a voice channel within the given category if it does not already exist. 384 | 385 | .. confval:: category 386 | 387 | The category in which the channel should be created. 388 | 389 | :type: ``str`` 390 | :required: ``True`` 391 | 392 | .. confval:: name 393 | 394 | The channel's name. 395 | 396 | :type: ``str`` 397 | :required: ``True`` 398 | 399 | .. py:function:: create_gm_channels(guild) 400 | :async: 401 | 402 | Creates the Game Masters section, text channels and voice channel. 403 | 404 | .. confval:: guild 405 | 406 | The guild identifier. 407 | 408 | :type: ``int`` 409 | :required: ``True`` 410 | 411 | 412 | -------------------------------------------------------------------------------- /T3SF/slack/slack.py: -------------------------------------------------------------------------------- 1 | from collections import Counter 2 | from T3SF import T3SF_Logger, utils 3 | import random 4 | import json 5 | import re 6 | 7 | class Slack(object): 8 | def __init__(self, app): 9 | self.app = app 10 | 11 | def Formatter(self=None, title=None, description=None, color="CYAN", image=None, author=None, buttons=None, text_input=None, checkboxes=None): 12 | colors = {'BLUE' : '#428bca', 'RED' : '#d9534f', 'WHITE' : '#f9f9f9', 'CYAN' : '#5bc0de', 'GREEN' : '#5cb85c', 'ORANGE' : '#ffa700', 'YELLOW' : '#ffff00'} 13 | 14 | fallback_text = "" 15 | color = colors[color] 16 | result =[ 17 | { 18 | "color": color, 19 | "blocks": [] 20 | } 21 | ] 22 | 23 | if title != None: 24 | title_list = [ 25 | { 26 | "type": "header", 27 | "text": { 28 | "type": "plain_text", 29 | "text": title 30 | } 31 | }, 32 | { 33 | "type": "divider" 34 | } 35 | ] 36 | 37 | result[0]["blocks"] = title_list 38 | 39 | if description != None: 40 | desc_list = { 41 | "type": "section", 42 | "text": { 43 | "type": "mrkdwn", 44 | "text": description 45 | } 46 | } 47 | result[0]["blocks"].append(desc_list) 48 | 49 | else: 50 | description = "Preview not available." 51 | desc_list = { 52 | "type": "section", 53 | "text": { 54 | "type": "mrkdwn", 55 | "text": description 56 | } 57 | } 58 | result[0]["blocks"].append(desc_list) 59 | 60 | if image != None: 61 | fallback_text = "🖼 " 62 | image_list = { 63 | "type": "image", 64 | "title": { 65 | "type": "plain_text", 66 | "text": image["name"], 67 | "emoji": True 68 | }, 69 | "image_url": image["image_url"], 70 | "alt_text": "image" 71 | } 72 | result[0]["blocks"].append(image_list) 73 | 74 | if author != None: 75 | author_list = [{ 76 | "type": "context", 77 | "elements": [ 78 | { 79 | "type": "image", 80 | "image_url": author["image_url"], 81 | "alt_text": author["name"] 82 | }, 83 | { 84 | "type": "mrkdwn", 85 | "text": author["name"], 86 | }, 87 | { 88 | "type": "mrkdwn", 89 | "text": " | *" + author["date"] + "*", 90 | } 91 | ] 92 | }] 93 | result[0]["blocks"][1:1] = author_list 94 | 95 | if text_input != None: 96 | input_list = { 97 | "block_id": "input_texto", 98 | "dispatch_action": text_input['dispatch_action'], 99 | "type": "input", 100 | "element": { 101 | "type": "plain_text_input", 102 | "action_id": text_input["action_id"] 103 | }, 104 | "label": { 105 | "type": "plain_text", 106 | "text": text_input["label"] 107 | } 108 | } 109 | result[0]["blocks"].append(input_list) 110 | 111 | if checkboxes != None: 112 | chk_boxes_list = { 113 | "block_id": "checkboxes", 114 | "type": "input", 115 | "label": { 116 | "type": "plain_text", 117 | "text": checkboxes['title'], 118 | "emoji": True 119 | }, 120 | "element": { 121 | "type": "checkboxes", 122 | "options": [] 123 | } 124 | } 125 | if 'action_id' in checkboxes: 126 | chk_boxes_list['element']["action_id"] = checkboxes['action_id'] 127 | 128 | for checkbox in checkboxes['checkboxes']: 129 | boxes_list = { 130 | "text": { 131 | "type": "plain_text", 132 | "text": checkbox['text'], 133 | "emoji": True 134 | }, 135 | "value": checkbox['value'] 136 | } 137 | chk_boxes_list['element']['options'].append(boxes_list) 138 | 139 | result[0]["blocks"].append(chk_boxes_list) 140 | 141 | if buttons != None: 142 | actions_list = { 143 | "type": "actions", 144 | "elements": [] 145 | } 146 | 147 | for button in buttons: 148 | button_list = { 149 | "type": "button", 150 | "text": { 151 | "type": "plain_text", 152 | "text": button['text'] 153 | }, 154 | "style": button['style'], 155 | "value": button['value'], 156 | "action_id": button['action_id'] 157 | } 158 | actions_list['elements'].append(button_list) 159 | 160 | result[0]["blocks"].append(actions_list) 161 | 162 | result[0]["fallback"] = (fallback_text + description) 163 | 164 | return result 165 | 166 | async def InboxesAuto(self, T3SF_instance, regex=None): 167 | if regex != None: 168 | mensaje_inboxes = "" 169 | for player in T3SF_instance.players_list: 170 | for channel in T3SF_instance.ch_names_list: 171 | if regex in channel: 172 | accuracy = utils.similar(re.sub(f"({regex})", "", str(channel)).lower(), str(player).lower().replace(" ", "-")) 173 | if accuracy >= 0.4: 174 | T3SF_instance.inboxes_all[player] = channel 175 | 176 | json.dump(T3SF_instance.inboxes_all,open(f"inboxes_{T3SF_instance.platform}.json", "w")) 177 | 178 | for player in T3SF_instance.inboxes_all: 179 | mensaje_inboxes += f"Inbox {player} [{T3SF_instance.inboxes_all[player]}]\n" 180 | 181 | T3SF_instance.response_auto = await self.EditMessage(response=T3SF_instance.response_auto, color="YELLOW", title = f"📩 Inboxes fetched! [{len(T3SF_instance.inboxes_all)}]", description=mensaje_inboxes) 182 | 183 | T3SF_instance.regex_ready = True 184 | 185 | T3SF_Logger.emit(message=f'Confirmed! Inboxes ready', message_type="INFO") 186 | 187 | elif T3SF_instance.fetch_inboxes == True: 188 | 189 | T3SF_instance.response_auto = await self.SendMessage(channel = T3SF_instance._ctx['channel'], color="CYAN", title="💬 Fetching inboxes...", description=f"Please wait while we fetch all the inboxes in this server!") 190 | 191 | channels = await T3SF_instance.app.client.conversations_list(types="public_channel,private_channel") 192 | 193 | for channel in channels['channels']: 194 | T3SF_instance.ch_names_list.append(channel['name']) 195 | 196 | channels_itinerator = 0 197 | regex = "" 198 | past_channel = None 199 | 200 | while regex == "": 201 | for channel in T3SF_instance.ch_names_list: 202 | if channels_itinerator == 0 and channel == past_channel: 203 | past_channel = channel 204 | channels_itinerator += 1 205 | continue 206 | 207 | match_channel = utils.regex_finder(str(channel).lower() + "-" + str(past_channel).lower()) 208 | 209 | if match_channel != False: 210 | for character in past_channel: 211 | if character in channel: 212 | regex += character 213 | else: 214 | break 215 | break 216 | else: 217 | channels_itinerator += 1 218 | past_channel = channel 219 | continue 220 | 221 | image = {"image_url":"https://i.ibb.co/34rTqMH/image.png", "name": "regex"} 222 | buttons = [{"text":"Yes!", "style": "primary", "value": regex, "action_id": "regex_yes"},{"text":"No.", "style": "danger", "value": "click_me_456", "action_id": "regex_no"}] 223 | 224 | T3SF_instance.response_auto = await self.EditMessage(response=T3SF_instance.response_auto, color="GREEN", title = "ℹ️ Regex detected!", description = f"Please confirm if the regex detected for the channels, is correct so we can get the inboxes!\n\nExample:\ninbox-legal\nThe regex should be `inbox-`\n\n*Detected regex:* `{regex}`\n\n\nPlease select your answer below.", image=image, buttons = buttons) 225 | T3SF_instance.regex_ready = False 226 | T3SF_Logger.emit(message=f'Please confirm the Regular Expression for the inboxes on the gm-chat!', message_type="INFO") 227 | 228 | else: 229 | mensaje_inboxes = "" 230 | T3SF_instance.response_auto = await self.SendMessage(channel = T3SF_instance._ctx['channel'], color="CYAN", title="💬 Fetching inboxes...", description=f"Please wait while we fetch all the inboxes in this server!") 231 | 232 | for player in T3SF_instance.inboxes_all: 233 | mensaje_inboxes += f"Inbox {player} [{T3SF_instance.inboxes_all[player]}]\n" 234 | 235 | T3SF_instance.response_auto = await self.EditMessage(response=T3SF_instance.response_auto, color="YELLOW", title = f"📩 Inboxes fetched! [{len(T3SF_instance.inboxes_all)}]", description=mensaje_inboxes) 236 | 237 | T3SF_instance.regex_ready = True 238 | 239 | async def InjectHandler(self, T3SF_instance): 240 | image = None 241 | 242 | author = {"name": T3SF_instance._inject["From"], "date": T3SF_instance._inject["Date"]} 243 | 244 | player = T3SF_instance._inject['Player'] 245 | 246 | if "Photo" in T3SF_instance._inject and T3SF_instance._inject['Photo'] != '': 247 | if "Picture Name" in T3SF_instance._inject and T3SF_instance._inject['Picture Name'] == '' or "Photo" not in T3SF_instance._inject: 248 | attachment_name = "attachment.jpg" 249 | else: 250 | attachment_name = T3SF_instance._inject['Picture Name'] 251 | image = {"name": attachment_name, "image_url": T3SF_instance._inject['Photo']} 252 | 253 | if "Profile" in T3SF_instance._inject and T3SF_instance._inject['Profile'] != '': 254 | author["image_url"] = T3SF_instance._inject['Profile'] 255 | 256 | else: 257 | profile_pic = random.choice([ 258 | "https://ssl.gstatic.com/ui/v1/icons/mail/profile_mask2.png", 259 | "https://lh3.googleusercontent.com/-XdUIqdMkCWA/AAAAAAAAAAI/AAAAAAAAAAA/4252rscbv5M/photo.jpg", 260 | "https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcSTt8Dg9RL4IGOjsJ2Fr-lXThf-DGM5YgPB6j5rD8tHQ9RLrU-03H4dYeskL01FNajqL_0&usqp=CAU" 261 | ]) 262 | author["image_url"] = profile_pic 263 | 264 | await self.SendMessage(channel = T3SF_instance.inboxes_all[player], title=T3SF_instance._inject['Subject'], description=T3SF_instance._inject["Script"], image=image, author=author, color="CYAN") 265 | 266 | return True 267 | 268 | async def PollHandler(self, T3SF_instance): 269 | T3SF_instance.poll_answered = False 270 | 271 | image = None 272 | 273 | player = T3SF_instance._inject['Player'] 274 | 275 | if "Photo" in T3SF_instance._inject and T3SF_instance._inject['Photo'] != '': 276 | if "Picture Name" in T3SF_instance._inject and T3SF_instance._inject['Picture Name'] == '' or "Photo" not in T3SF_instance._inject: 277 | attachment_name = "attachment.jpg" 278 | else: 279 | attachment_name = T3SF_instance._inject['Picture Name'] 280 | image = {"name": attachment_name, "image_url": T3SF_instance._inject['Photo']} 281 | 282 | poll_options = T3SF_instance._inject['Poll'].split('|') 283 | 284 | actual_real_time = re.sub("([^0-9])", "", T3SF_instance._inject['Real Time'])[-2:] 285 | 286 | next_real_time = re.sub("([^0-9])", "", T3SF_instance.data[int(T3SF_instance._inject['#'])]['Real Time'])[-2:] 287 | 288 | diff = int(next_real_time) - int(actual_real_time) 289 | if diff < 0: 290 | diff_no_real = int(actual_real_time) - int(next_real_time) 291 | diff = 60 - diff_no_real 292 | 293 | diff_secs = diff * 60 294 | 295 | description = T3SF_instance._inject["Script"] + f"\n\nYou have {diff} minute(s) to answer this poll!" 296 | 297 | buttons = [] 298 | 299 | for i, option in enumerate(poll_options, start=1): 300 | buttons.append({ 301 | "style": "primary", 302 | "text": option, 303 | "value": f"option{i}", 304 | "action_id": f"option{i}" 305 | }) 306 | 307 | T3SF_instance.response_poll = await self.SendMessage(channel = T3SF_instance.inboxes_all[player], title=T3SF_instance._inject['Subject'], description=description, image=image, buttons=buttons, color="YELLOW") 308 | 309 | return True 310 | 311 | async def PollAnswerHandler(self, T3SF_instance, body=None, payload=None): 312 | poll_msg = body['message']['attachments'][0]['fallback'] 313 | poll_msg = poll_msg[: poll_msg.rfind('\n')] 314 | action_user = body['user']['username'] 315 | selected_option = payload['text']['text'] 316 | description = f'{poll_msg}\n\n@{action_user} selected: {selected_option}' 317 | 318 | T3SF_instance.poll_answered = True 319 | T3SF_instance.response_poll = await self.EditMessage(color = "GREEN", title="Poll Answered!", description=description, response=T3SF_instance.response_poll) 320 | await T3SF_instance.NotifyGameMasters(type_info="poll_answered", data={'msg_poll':poll_msg,'answer':selected_option,'user':action_user}) 321 | return True 322 | 323 | async def SendMessage(self, T3SF_instance=None, color=None, title=None, description:str=None, channel=None, image=None, author=None, buttons=None, text_input=None, checkboxes=None): 324 | if channel == None: 325 | channel = self._ctx['channel'] 326 | 327 | try: 328 | attachments = self.Formatter(title=title, description=description, color=color, image=image, author=author, buttons=buttons, text_input=text_input, checkboxes=checkboxes) 329 | self.response = await self.app.client.chat_postMessage(channel = channel, attachments = attachments) 330 | return self.response 331 | 332 | except Exception as e: 333 | raise 334 | 335 | async def EditMessage(self, T3SF_instance=None, color=None, title:str=None, description:str=None, response=None, image=None, author=None, buttons=None, text_input=None, checkboxes=None): 336 | self.response = await self.app.client.chat_update(channel=response['channel'], ts=response['ts'], attachments = self.Formatter(title=title, description=description, color=color, image=image, author=author, buttons=buttons, text_input=text_input, checkboxes=checkboxes)) 337 | return self.response -------------------------------------------------------------------------------- /docs/Slack.rst: -------------------------------------------------------------------------------- 1 | ******************* 2 | Slack 3 | ******************* 4 | 5 | .. contents:: Table of Contents 6 | 7 | Slack is the new platform we are expanding to given the needs of our clients! The heart of the bot is still the same, we just made some improvements to it so it can walk around smoothly! 8 | 9 | Bot 10 | =============== 11 | 12 | To perform an exercise based on the Slack platform, you will need to provide an *APP* and a *BOT* token. If you already have an app created, you can skip the creation part and go straight to the provisioning part. 13 | 14 | App/Bot setup 15 | ------------------ 16 | #. Create a Workspace in slack (You can skip this step if you have already a workspace). 17 | #. Navigate to `Slack Apps `_. 18 | #. Select "Create New App". 19 | #. Select the option "From an app manifest". 20 | #. Select your workspace. 21 | #. Selecting the format "YAML", paste the code inside ``bot_manifest.yml`` located in the following `link `_ 22 | #. Create the App. 23 | #. With the recently created app, and in the Basic Information menu, scroll to ``App-Level Tokens``, Generate a token and Scopes. 24 | #. You can use any Token name, the important thing is that you add both scopes to the token: ``connections:write`` and ``authorizations:read``. 25 | #. Generate it, copy it and keep it in a safe place, because you will only see it once. 26 | #. Now navigate to the "OAuth & permissions" sub-menu inside the Feautures sidebar menu. 27 | #. Copy the ``Bot User OAuth Token``. 28 | #. For the bot to be able to respond to your messages, you must add the App within that channel. You can do this by tagging the App name or adding it manually. 29 | #. Yeah! You are ready to go now! 30 | 31 | Starting the Framework 32 | ======================== 33 | 34 | Once you have installed in all the `libraries dependent on the platform `_, and added it to your workspace, you will need to provide the app and bot token to the framework for it to work and choose the Slack platform on ``T3SF.start``. 35 | 36 | Providing the tokens 37 | ------------------------------ 38 | 39 | The framework expects an *APP* and *BOT* token with the names ``SLACK_BOT_TOKEN`` and ``SLACK_APP_TOKEN``. 40 | 41 | You have two common options for this: 42 | 43 | 1. Create a ``.env`` file 44 | #. On the same path as your ``main.py`` file create a ``.env`` file 45 | #. Inside of it, add the variable ``SLACK_APP_TOKEN`` and your app's token, as following: ``SLACK_APP_TOKEN=xapp-1-Z03ZJ58JUTF-3463422570419-p11no1l1q9po6qq96p1n383378q17032p08l7n8015mp1mn067q075n9q48m8434`` 46 | #. Also, add the variable ``SLACK_BOT_TOKEN`` and the bot's token, as following: ``SLACK_BOT_TOKEN=xoxb-4239546374990-4236264338677-jQqt0XeIMVgDAGNNnJaydQkk`` 47 | #. Your file should look like this 48 | .. code-block:: bash 49 | 50 | SLACK_APP_TOKEN=xapp-1-Z03ZJ58JUTF-3463422570419-p11no1l1q9po6qq96p1n383378q17032p08l7n8015mp1mn067q075n9q48m8434 51 | SLACK_BOT_TOKEN=xoxb-4239546374990-4236264338677-jQqt0XeIMVgDAGNNnJaydQkk 52 | 53 | .. note:: Note that the tokens will be stored and everyone with read access to the file will be able to read them. 54 | 55 | 2. Export the variables to your shell environment 56 | #. Create a variable with the name ``SLACK_APP_TOKEN`` as following: ``export SLACK_APP_TOKEN=xapp-1-Z03ZJ58JUTF-3463422570419-p11no1l1q9po6qq96p1n383378q17032p08l7n8015mp1mn067q075n9q48m8434`` 57 | #. Create another variable with the name ``SLACK_BOT_TOKEN`` as following: ``export SLACK_BOT_TOKEN=xoxb-4239546374990-4236264338677-jQqt0XeIMVgDAGNNnJaydQkk`` 58 | #. Your ``env`` should look like this 59 | .. code-block:: bash 60 | 61 | [...] 62 | SLACK_APP_TOKEN=xapp-1-Z03ZJ58JUTF-3463422570419-p11no1l1q9po6qq96p1n383378q17032p08l7n8015mp1mn067q075n9q48m8434 63 | SLACK_BOT_TOKEN=xoxb-4239546374990-4236264338677-jQqt0XeIMVgDAGNNnJaydQkk 64 | [...] 65 | 66 | Initializing the framework 67 | ---------------------------- 68 | 69 | As explained in the `Initializing T3SF `_ page, you will need to set 3 variables inside your ``main.py`` file. 70 | 71 | This example is for an exercise using Slack with a GUI: 72 | 73 | .. code-block:: python3 74 | 75 | from T3SF import T3SF 76 | import asyncio 77 | 78 | async def main(): 79 | await T3SF.start(MSEL="MSEL_Company.json", platform="Slack", gui=True) 80 | 81 | if __name__ == '__main__': 82 | asyncio.run(main()) 83 | 84 | And that's it! 85 | 86 | Module 87 | ====== 88 | 89 | To maintain the modular structure of the framework, we developed a module with all the platform specific functions inside. Including the integrated bot and the functions to contact the Slack API. 90 | 91 | The file structure is shown below: 92 | 93 | .. code-block:: bash 94 | 95 | Slack 96 | ├── bot.py 97 | ├── __init__.py 98 | └── slack.py 99 | 100 | Class Functions 101 | ----------------- 102 | 103 | .. py:function:: Formatter(title=None, description=None, color="#5bc0de", image=None, author=None, buttons=None, text_input=None, checkboxes=None) 104 | 105 | Creates the embed messages, with a different set of options. 106 | 107 | .. confval:: title 108 | 109 | The title of the message. 110 | 111 | :type: ``str`` 112 | :required: ``False`` 113 | 114 | .. confval:: description 115 | 116 | The description/main text of the message. 117 | 118 | :type: ``str`` 119 | :required: ``False`` 120 | 121 | .. confval:: color 122 | 123 | Parameter with the color of the embedded message. 124 | 125 | :type: ``str`` 126 | :required: ``False`` 127 | :default: `"#5bc0de"` 128 | 129 | .. confval:: image 130 | 131 | Attach an image to the message. 132 | 133 | :type: ``array`` 134 | :required: ``False`` 135 | 136 | .. confval:: author 137 | 138 | Attach the author of the message. 139 | 140 | :type: ``array`` 141 | :required: ``False`` 142 | 143 | .. confval:: buttons 144 | 145 | Attach buttons to the message. 146 | 147 | :type: ``array`` 148 | :required: ``False`` 149 | 150 | 151 | .. confval:: text_input 152 | 153 | Attach a text area input to the message. 154 | 155 | :type: ``array`` 156 | :required: ``False`` 157 | 158 | .. confval:: checkboxes 159 | 160 | Attach textboxes to the message 161 | 162 | :type: ``array`` 163 | :required: ``False`` 164 | 165 | .. py:function:: SendMessage(title:str=None, description:str=None, color_sl=None, channel=None, image=None, author=None, buttons=None, text_input=None, checkboxes=None) 166 | :async: 167 | 168 | Message sending controller. 169 | 170 | .. confval:: title 171 | 172 | The title of the message. 173 | 174 | :type: ``str`` 175 | :required: ``True`` 176 | 177 | .. confval:: description 178 | 179 | The description/main text of the message. 180 | 181 | :type: ``str`` 182 | :required: ``True`` 183 | 184 | .. confval:: color_sl 185 | 186 | Parameter with the color of the embedded message. 187 | 188 | :type: ``str`` 189 | :required: ``False`` 190 | 191 | .. confval:: channel 192 | 193 | Parameter with the desired destination channel. 194 | 195 | :type: ``str`` 196 | :required: ``False`` 197 | 198 | .. confval:: image 199 | 200 | :type: ``array`` 201 | :required: ``False`` 202 | 203 | .. confval:: author 204 | 205 | :type: ``array`` 206 | :required: ``False`` 207 | 208 | .. confval:: buttons 209 | 210 | 211 | :type: ``array`` 212 | :required: ``False`` 213 | 214 | .. confval:: text_input 215 | 216 | :type: ``array`` 217 | :required: ``False`` 218 | 219 | .. confval:: checkboxes 220 | 221 | :type: ``array`` 222 | :required: ``False`` 223 | 224 | .. py:function:: EditMessage(title:str=None, description:str=None, color_sl=None, response=None, image=None, author=None, buttons=None, text_input=None, checkboxes=None) 225 | :async: 226 | 227 | Message editing controller. 228 | 229 | .. confval:: title 230 | 231 | The title of the message. 232 | 233 | :type: ``str`` 234 | :required: ``True`` 235 | 236 | .. confval:: description 237 | 238 | The description/main text of the message. 239 | 240 | :type: ``str`` 241 | :required: ``True`` 242 | 243 | .. confval:: color_sl 244 | 245 | Parameter with the color of the embedded message. 246 | 247 | :type: ``str`` 248 | :required: ``False`` 249 | 250 | .. confval:: response 251 | 252 | Parameter with the previous response. 253 | 254 | :type: ``array`` 255 | :required: ``False`` 256 | 257 | .. confval:: image 258 | 259 | :type: ``array`` 260 | :required: ``False`` 261 | 262 | .. confval:: author 263 | 264 | :type: ``array`` 265 | :required: ``False`` 266 | 267 | .. confval:: buttons 268 | 269 | :type: ``array`` 270 | :required: ``False`` 271 | 272 | .. confval:: text_input 273 | 274 | :type: ``array`` 275 | :required: ``False`` 276 | 277 | .. confval:: checkboxes 278 | 279 | :type: ``array`` 280 | :required: ``False`` 281 | 282 | .. py:function:: InboxesAuto(self) 283 | :async: 284 | 285 | Fetches automatically all the inboxes, based in a regular expression (RegEx), notifies the Game masters about differents parts of this process. 286 | 287 | .. py:function:: InjectHandler(self) 288 | :async: 289 | 290 | Gives the format to the inject and sends it to the correct player's inbox. 291 | 292 | .. py:function:: regex_finder(input) 293 | 294 | Tries to get a regular expresion on one string. 295 | 296 | .. confval:: input 297 | 298 | The input to find the regular expression. 299 | 300 | :type: ``str`` 301 | :required: ``True`` 302 | 303 | .. py:function:: PollHandler(T3SF_instance) 304 | :async: 305 | 306 | Handles the injects with polls. Creates the poll with the two options and sends it to the player's channel. 307 | 308 | .. confval:: T3SF_instance 309 | 310 | An instance of the T3SF class. 311 | 312 | :type: ``obj`` 313 | :required: ``True`` 314 | 315 | .. py:function:: PollAnswerHandler(T3SF_instance, body=None, payload=None) 316 | :async: 317 | 318 | Detects the answer in the poll sent. Modifies the poll message and notifies the game master about the selected option. 319 | 320 | .. confval:: T3SF_instance 321 | 322 | An instance of the T3SF class. 323 | 324 | :type: ``obj`` 325 | :required: ``True`` 326 | 327 | .. confval:: body 328 | 329 | The body of the interaction. 330 | 331 | :type: ``obj`` 332 | :required: ``False`` 333 | 334 | .. confval:: payload 335 | 336 | The user's input. 337 | 338 | :type: ``obj`` 339 | :required: ``False`` 340 | 341 | .. py:function:: similar(a, b) 342 | 343 | Based in graphics, find the similarity between 2 strings. 344 | 345 | .. confval:: a 346 | 347 | :type: ``str`` 348 | :required: ``True`` 349 | 350 | .. confval:: b 351 | 352 | :type: ``str`` 353 | :required: ``True`` 354 | 355 | 356 | Integrated bot 357 | ----------------- 358 | 359 | We integrated the bot to fully manage the platform from within the framework. The bot handles poll responses, commands and environment creation. 360 | 361 | .. py:class:: create_bot(MSEL) 362 | 363 | This class creates the bot, will handle the commands, messages and interactions with it. 364 | 365 | .. confval:: MSEL 366 | 367 | The location of the MSEL. 368 | 369 | :type: ``str`` 370 | :required: ``True`` 371 | 372 | .. py:method:: slack_main() 373 | 374 | Within this method, we will create the following command management functions. 375 | 376 | .. py:function:: regex_handler(ack, body, payload) 377 | :async: 378 | 379 | Handles the user's regular expression, at the start of the exercise. 380 | 381 | .. confval:: ack 382 | 383 | Acknowledge object to inform Slack that we have received the interaction. 384 | 385 | :type: ``obj`` 386 | :required: ``True`` 387 | 388 | .. confval:: body 389 | 390 | The body of the interaction. 391 | 392 | :type: ``obj`` 393 | :required: ``True`` 394 | 395 | .. confval:: payload 396 | 397 | The user's input. 398 | 399 | :type: ``obj`` 400 | :required: ``True`` 401 | 402 | .. py:function:: poll_handler(ack, body, payload) 403 | :async: 404 | 405 | Detects when the bot receives an interaction (as a response to a poll). 406 | 407 | .. confval:: ack 408 | 409 | Acknowledge object to inform Slack that we have received the interaction. 410 | 411 | :type: ``obj`` 412 | :required: ``True`` 413 | 414 | .. confval:: body 415 | 416 | The body of the interaction. 417 | 418 | :type: ``obj`` 419 | :required: ``True`` 420 | 421 | .. confval:: payload 422 | 423 | The user's input. 424 | 425 | :type: ``obj`` 426 | :required: ``True`` 427 | 428 | .. py:function:: ping(message, say) 429 | :async: 430 | 431 | Handles the ``!ping`` command and returns a `pong` message. 432 | 433 | .. confval:: message 434 | 435 | The content of the message, incluiding information about the workspace, channel, user, etc. 436 | 437 | :type: ``obj`` 438 | :required: ``True`` 439 | 440 | .. confval:: say 441 | 442 | A method to directly reply on the same channel to the message/command. 443 | 444 | :type: ``obj`` 445 | :required: ``True`` 446 | 447 | .. py:function:: start(message, say) 448 | :async: 449 | 450 | Handles the ``!start`` command and starts the exercise. 451 | 452 | .. confval:: message 453 | 454 | The content of the message, incluiding information about the workspace, channel, user, etc. 455 | 456 | :type: ``obj`` 457 | :required: ``True`` 458 | 459 | .. confval:: say 460 | 461 | A method to directly reply on the same channel to the message/command. 462 | 463 | :type: ``obj`` 464 | :required: ``True`` 465 | 466 | .. py:function:: not_interesting_messages(body, logger) 467 | :async: 468 | 469 | Handles all other messages, avoiding any noise in the logs. 470 | 471 | .. confval:: body 472 | 473 | The body of the message. 474 | 475 | :type: ``obj`` 476 | :required: ``True`` 477 | 478 | .. confval:: logger 479 | 480 | A method to log the message. 481 | 482 | :type: ``obj`` 483 | :required: ``True`` 484 | 485 | .. py:function:: start_bot() 486 | :async: 487 | 488 | This function will create a task to start the bot immediately. 489 | 490 | .. py:function:: run_async_incidents() 491 | :async: 492 | 493 | This function intancies the T3SF class and starts the exercise making use of `T3SF.ProcessIncidents <./T3SF.CoreFunctions.html#ProcessIncidents>`_. 494 | 495 | .. py:function:: create_environment() 496 | :async: 497 | 498 | This function creates the environment for the exercise. 499 | 500 | .. py:function:: get_admins() 501 | :async: 502 | 503 | It will get all the administrators in the workspace and return an array with their IDs. 504 | 505 | .. py:function:: create_channel_if_not_exists(channel_name, private=True) 506 | :async: 507 | 508 | Create a channel if it does not already exist. 509 | 510 | .. confval:: channel_name 511 | 512 | The channel's name. 513 | 514 | :type: ``str`` 515 | :required: ``True`` 516 | 517 | .. confval:: private 518 | 519 | Determines if the channel should be private and only available to the members and admins. 520 | 521 | :type: ``bool`` 522 | :required: ``False`` 523 | 524 | .. py:function:: create_gm_channels(admins) 525 | :async: 526 | 527 | Creates the Game Masters text channels. 528 | 529 | .. confval:: admins 530 | 531 | List of administrators to invite to the channels. 532 | 533 | :type: ``list`` 534 | :required: ``True`` 535 | 536 | 537 | -------------------------------------------------------------------------------- /T3SF/T3SF.py: -------------------------------------------------------------------------------- 1 | import importlib 2 | import asyncio 3 | import signal 4 | import json 5 | import time 6 | import re 7 | import os 8 | 9 | # Internal logging 10 | from .logger import T3SF_Logger 11 | # / Internal logging 12 | 13 | def keyboard_interrupt_handler(signal_num, frame): 14 | T3SF_Logger.emit(message=f"KeyboardInterrupt (ID: {signal_num}) has been caught. Exiting...", message_type="WARN") 15 | os._exit(1) 16 | 17 | # Associate the signal handler function with SIGINT (keyboard interrupt) 18 | signal.signal(signal.SIGINT, keyboard_interrupt_handler) 19 | 20 | class T3SF(object): 21 | def __init__(self, bot = None, app = None, platform = None): 22 | self.response = None 23 | self.regex_ready = None 24 | self.incidents_running = False 25 | self.poll_answered = False 26 | self.ch_names_list = [] 27 | self.players_list = [] 28 | self.platform = platform.lower() 29 | self.guild_id = None 30 | 31 | try: 32 | if os.path.isfile(os.getcwd() + f"/inboxes_{self.platform}.json"): 33 | self.inboxes_all = json.load(open(f"inboxes_{self.platform}.json", encoding='utf-8-sig')) 34 | self.fetch_inboxes = False 35 | 36 | T3SF_Logger.emit(message="Locally retrieved Inboxes", message_type="DEBUG") 37 | else: 38 | self.fetch_inboxes = True 39 | self.inboxes_all = {} 40 | 41 | except Exception: 42 | self.fetch_inboxes = True 43 | self.inboxes_all = {} 44 | 45 | if self.platform == "discord": 46 | from .discord import Discord 47 | self.bot = bot 48 | self.discord = Discord(bot) 49 | 50 | elif self.platform == "slack": 51 | from .slack import Slack 52 | self.app = app 53 | self.slack = Slack(app) 54 | 55 | async def TimeDifference(self, actual_real_time, previous_real_time, itinerator=int, resumed=bool): 56 | """ 57 | This function is used to Get the difference between two injects. It will make the bot sleep and inform the Game Masters. 58 | """ 59 | try: 60 | self.diff = int(actual_real_time) - int(previous_real_time) 61 | 62 | if self.diff < 0: 63 | self.diff_no_real = int(previous_real_time) - int(actual_real_time) 64 | self.diff = 60 - self.diff_no_real 65 | 66 | self.diff_secs = self.diff * 60 67 | 68 | if resumed != True: 69 | itinerator = itinerator + 1 70 | 71 | description = f"The bot is Up and running!\n\nIncident: {itinerator}/{len(self.data)}\n\nWaiting {self.diff} minute(s) ({self.diff_secs} sec.) to send it." 72 | 73 | # await self.EditMessage(style="simple", color = "CYAN", title="⚙️ Bot running...", description=description, response=self.msg_gm) 74 | 75 | T3SF_Logger.emit(f'We have a difference of {self.diff} minute(s) - {self.diff_secs} seconds', message_type="INFO") 76 | 77 | await asyncio.sleep(self.diff_secs) 78 | 79 | if "Poll" in self._inject and self._inject['Poll'] != '' and self.poll_answered == False: 80 | description = self._inject["Script"] + f"\n\n@channel Poll not answered within {self.diff} minute(s), Time's Up!" 81 | await self.EditMessage(style="custom", variable="T3SF_instance.response_poll", color = "RED", title="Poll time ended!", description=description, response=self.response_poll) 82 | await self.NotifyGameMasters(type_info="poll_unanswered", data={'msg_poll':self._inject["Script"]}) 83 | 84 | except Exception as e: 85 | T3SF_Logger.emit("Get Time Difference", message_type="ERROR") 86 | T3SF_Logger.emit(e, message_type="ERROR") 87 | raise 88 | 89 | def IncidentsFetcher(self, MSEL:str): 90 | """ 91 | Retrieves the incidents from the desired source, chosen in the config file. 92 | """ 93 | T3SF_Logger.emit(message="Reading MSEL", message_type="DEBUG") 94 | if MSEL: 95 | self.data = json.load(open(MSEL, encoding='utf-8-sig')) 96 | 97 | for inject in self.data: 98 | player = inject['Player'] 99 | if player not in self.players_list: 100 | self.players_list.append(player) 101 | T3SF_Logger.emit(message="We have the inboxes right now", message_type="DEBUG") 102 | T3SF_Logger.emit(message="Incidents ready", message_type="DEBUG") 103 | return self.players_list 104 | else: 105 | raise RuntimeError("Please set a method to retrieve the TTXs with the argument `MSEL` inside the `start` function.") 106 | 107 | async def start(MSEL:str, platform, gui=False): 108 | if gui == True: 109 | T3SF_Logger.emit(message="Starting GUI", message_type="DEBUG") 110 | gui_module = importlib.import_module("T3SF.gui.core") 111 | gui_module.GUI(platform_run=platform, MSEL=MSEL, static_url_path='/static', static_folder='static') 112 | 113 | if platform.lower() == "slack": 114 | bot_module = importlib.import_module("T3SF.slack.bot") 115 | 116 | elif platform.lower() == "discord": 117 | bot_module = importlib.import_module("T3SF.discord.bot") 118 | 119 | else: 120 | raise ValueError("Invalid platform") 121 | 122 | T3SF_Logger.emit(message="Starting BOT", message_type="DEBUG") 123 | bot_module.create_bot(MSEL=MSEL) 124 | await bot_module.start_bot() 125 | 126 | async def NotifyGameMasters(self, type_info=str, data=None): 127 | """ 128 | Notify the Game Masters of the different states of the bot, through messages. 129 | """ 130 | try: 131 | if type_info == "start_normal": 132 | title = "⚙ Starting bot..." 133 | description = "The bot it's heating up!\n\nGive us just a second!!" 134 | # self.msg_gm = await self.SendMessage(title = title, description = description, color="YELLOW") 135 | 136 | elif type_info == "started_normal": 137 | title = "Bot succesfully started! 🎈" 138 | description = "The bot is Up and running!\n\nLets the game begin!!" 139 | # self.msg_gm = await self.EditMessage(title = title, description = description, color="GREEN", response=self.msg_gm) 140 | 141 | elif type_info == "start_resumed": 142 | title = "⚙ Resuming bot..." 143 | description = "The bot it's trying to resume from the desired point!\n\nGive us just a few seconds!!" 144 | # self.msg_gm = await self.SendMessage(title = title, description = description, color="YELLOW") 145 | 146 | elif type_info == "started_resumed": 147 | title = "Bot succesfully started! 🎈" 148 | description = "The bot is Up and running!\n\nLets the game begin!!" 149 | # self.msg_gm = await self.EditMessage(title = title, description = description, color="GREEN", response=self.msg_gm) 150 | 151 | elif type_info == "finished_incidents": 152 | title = "🎉 Bot Finished succesfully! 🎉" 153 | description = "The bot've just completed the entire game!\n\nHope to see you again soon!!" 154 | # self.msg_gm = await self.EditMessage(title = title, description = description, color="GREEN", response=self.msg_gm) 155 | 156 | elif type_info == "poll_answered": 157 | title = "📊 Poll Answered" 158 | description = f"Poll Question: {data['msg_poll']}\nSelected Answer: {data['answer']}\nBy: @{data['user']}" 159 | # await self.SendMessage(title = title, description = description, color="GREEN", unique=True) 160 | 161 | elif type_info == "poll_unanswered": 162 | title = "📊 Poll Not Answered" 163 | description = f"Poll Question: {data['msg_poll']}\nNot answered by anyone." 164 | # await self.SendMessage(title = title, description = description, color="RED", unique=True) 165 | 166 | T3SF_Logger.emit(message=description, message_type="INFO") 167 | return True 168 | 169 | except Exception as e: 170 | T3SF_Logger.emit("NotifyGameMasters", message_type="ERROR") 171 | T3SF_Logger.emit(e, message_type="ERROR") 172 | raise 173 | 174 | async def ProcessIncidents(self, MSEL:str, ctx, function_type:str=None, itinerator:int=0): 175 | """ 176 | Process the incidents from the MSEL file. 177 | """ 178 | self.IncidentsFetcher(MSEL) 179 | try: 180 | self._ctx = ctx 181 | 182 | await self.InboxesAuto() 183 | 184 | while self.regex_ready == False: 185 | await asyncio.sleep(2) 186 | 187 | if function_type == "start": 188 | await self.NotifyGameMasters(type_info="start_normal") # Sends a message regarding the bot's start procedure. 189 | else: 190 | await self.NotifyGameMasters(type_info="start_resumed") # Sends a message regarding the bot's restart procedure. 191 | 192 | bypass_time = True 193 | 194 | for information in self.data: 195 | process_status = await self.check_status() 196 | 197 | if process_status == 'break': 198 | break 199 | 200 | if itinerator == 0: # Set a variable to get the actual timestamp and the past one, after that checks for differences. 201 | itinerator_loop = itinerator 202 | else: 203 | if function_type == "resume": 204 | itinerator_loop = itinerator - 2 205 | else: 206 | itinerator_loop = itinerator - 1 207 | 208 | if int(information["#"]) != itinerator and function_type == "resume": # Checks if the incident ID is the same as the desired starting point. 209 | pass 210 | 211 | else: 212 | actual_real_time = re.sub("([^0-9])", "", information["Real Time"])[-2:] 213 | 214 | previous_real_time = re.sub("([^0-9])", "", self.data[itinerator_loop]["Real Time"])[-2:] 215 | 216 | T3SF_Logger.emit(f"Previous {previous_real_time} - Actual {actual_real_time}", message_type="DEBUG") 217 | 218 | if previous_real_time != actual_real_time and function_type == "start": 219 | await self.TimeDifference(actual_real_time, previous_real_time, resumed=False, itinerator=itinerator) # Check the amount of seconds between both timestamps. 220 | 221 | elif previous_real_time != actual_real_time and bypass_time != True: 222 | await self.TimeDifference(actual_real_time, previous_real_time, resumed=True, itinerator=itinerator) # Check the amount of seconds between both timestamps. 223 | 224 | T3SF_Logger.emit(f'Inject {information["#"]}/{len(self.data)}', message_type="INFO") 225 | 226 | if "Poll" in information and information['Poll'] != '': 227 | await self.SendPoll(inject = information) 228 | 229 | else: 230 | await self.SendIncident(inject = information) # Sends the incident to the desired chats. 231 | 232 | if function_type == "start": 233 | if itinerator == 0: 234 | await self.NotifyGameMasters(type_info="started_normal") # Informs that the bot succesfully started. 235 | else: 236 | if bypass_time == True: 237 | await self.NotifyGameMasters(type_info="started_resumed") # Informs that the bot succesfully restarted. 238 | bypass_time = False 239 | 240 | itinerator += 1 241 | 242 | await self.check_status(reset=True) 243 | await self.NotifyGameMasters(type_info="finished_incidents") # Informs that the script is completed and there's no remaining incidents. 244 | self.incidents_running = False 245 | 246 | except Exception as e: 247 | T3SF_Logger.emit("ProcessIncidents function", message_type="ERROR") 248 | T3SF_Logger.emit(e, message_type="ERROR") 249 | raise 250 | 251 | async def check_status(self, reset:bool = False): 252 | from .utils import process_wait, process_quit 253 | 254 | if reset: 255 | process_quit = False 256 | process_wait = False 257 | return True 258 | 259 | if process_quit == True: 260 | return 'break' 261 | 262 | if process_wait == True: 263 | # print(f"We are waiting, because the variable {process_wait} is set to True") 264 | await asyncio.sleep(5) 265 | await self.check_status() 266 | return False 267 | 268 | return True 269 | 270 | async def SendIncident(self, inject): 271 | try: 272 | self._inject = inject 273 | 274 | if self.platform == "discord": 275 | await self.discord.InjectHandler(T3SF_instance=self) 276 | 277 | elif self.platform == "slack": 278 | await self.slack.InjectHandler(T3SF_instance=self) 279 | 280 | return True 281 | 282 | except Exception as e: 283 | T3SF_Logger.emit("SendIncident", message_type="ERROR") 284 | T3SF_Logger.emit(e, message_type="ERROR") 285 | raise 286 | 287 | async def SendPoll(self, inject): 288 | try: 289 | self._inject = inject 290 | 291 | if self.platform == "discord": 292 | await self.discord.PollHandler(T3SF_instance=self) 293 | 294 | elif self.platform == "slack": 295 | await self.slack.PollHandler(T3SF_instance=self) 296 | 297 | return True 298 | 299 | except Exception as e: 300 | T3SF_Logger.emit("SendPoll", message_type="ERROR") 301 | T3SF_Logger.emit(e, message_type="ERROR") 302 | raise 303 | 304 | async def InboxesAuto(self, message=None): 305 | if self.platform == "discord": 306 | await self.discord.InboxesAuto(T3SF_instance=self) 307 | 308 | elif self.platform == "slack": 309 | await self.slack.InboxesAuto(T3SF_instance=self, regex=None) 310 | 311 | async def RegexHandler(self, ack=None, body=None, payload=None, inbox=None): 312 | if self.platform == "slack": 313 | await ack() 314 | text_input = None 315 | image = None 316 | regex = None 317 | 318 | if payload['action_id'] == "regex_yes": 319 | regex = body['actions'][0]['value'] 320 | color="GREEN" 321 | title = "✨ Regex detected succesfully! ✨" 322 | description = f"Thanks for confirming the regex detected for the channels (I'm going to tell my creator he is so good coding :D ), we are going to use `{regex}` to match the inboxes" 323 | 324 | elif payload['action_id'] == "regex_no": 325 | color="RED" 326 | title = "ℹ️ Regex needed!" 327 | description = "Got it!\n Unluckily, but here we go...\nPlease send me the regex for the channels, so we can get the inboxes!\n\nExample:\ninbox-legal\nThe regex should be `inbox-`" 328 | text_input = {"action_id": "regex_custom", "label": "Please type the desired regex. EG: inbox-", "dispatch_action": True} 329 | image = {"image_url":"https://i.ibb.co/34rTqMH/image.png", "name": "regex"} 330 | 331 | elif payload['action_id'] == "regex_custom": 332 | regex = body['actions'][0]['value'] 333 | color="GREEN" 334 | title="✅ Regex accepted!" 335 | description=f"Thanks for confirming the regex for the channels, we are going to use `{regex}` to match the inboxes!" 336 | 337 | self.response_auto = await self.EditMessage(title = title, description = description, color=color, image=image, text_input=text_input, response=self.response_auto) 338 | 339 | if regex != None: 340 | await self.slack.InboxesAuto(T3SF_instance=self, regex=regex) 341 | 342 | async def PollAnswerHandler(self, ack=None, body=None, payload=None, query=None): 343 | if self.platform == "discord": 344 | await self.discord.PollAnswerHandler(T3SF_instance=self, interaction=payload) 345 | return True 346 | 347 | elif self.platform == "slack": 348 | await ack() 349 | await self.slack.PollAnswerHandler(T3SF_instance=self, body=body, payload=payload) 350 | return True 351 | 352 | async def SendMessage(self, 353 | color=None, 354 | title:str=None, 355 | description:str=None, 356 | channel=None, 357 | image=None, 358 | author=None, 359 | buttons=None, 360 | text_input=None, 361 | checkboxes=None, 362 | view=None, 363 | unique=False, 364 | reply_markup=None): 365 | 366 | if self.platform == "discord": 367 | if unique == True: 368 | self.gm_poll_msg = await self.discord.SendMessage(T3SF_instance=self, color=color, title=title, description=description, view=view, unique=unique) 369 | return self.gm_poll_msg 370 | else: 371 | self.response = await self.discord.SendMessage(T3SF_instance=self, color=color, title=title, description=description, view=view) 372 | return self.response 373 | 374 | elif self.platform == "slack": 375 | self.response = await Slack.SendMessage(self=self.slack, channel = channel, title=title, description=description, color=color, image=image, author=author, buttons=buttons, text_input=text_input, checkboxes=checkboxes) 376 | return self.response 377 | 378 | async def EditMessage(self, 379 | color=None, 380 | style:str="simple", 381 | title:str=None, 382 | description:str=None, 383 | response=None, 384 | variable=None, 385 | image=None, 386 | author=None, 387 | buttons=None, 388 | text_input=None, 389 | checkboxes=None, 390 | view=None, 391 | reply_markup=None): 392 | 393 | if self.platform == "discord": 394 | if style == "simple": 395 | self.response = await self.discord.EditMessage(T3SF_instance=self, color=color, title=title, description=description, view=view) 396 | return self.response 397 | else: 398 | self.response = await self.discord.EditMessage(T3SF_instance=self, color=color, title=title, description=description, view=view, variable=variable, style="custom") 399 | return self.response 400 | 401 | elif self.platform == "slack": 402 | self.response = await self.slack.EditMessage(response=response, title=title, description=description, color=color, image=image, author=author, buttons=buttons, text_input=text_input, checkboxes=checkboxes) 403 | return self.response -------------------------------------------------------------------------------- /T3SF/gui/templates/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'base.html' %} 2 | 3 | {% block title %} Logs Viewer {% endblock %} 4 | 5 | {% block extra_head %} 6 | 118 | 119 | {% endblock %} 120 | 121 | {% block content %} 122 |
123 |
124 |
125 |

Logs

126 |
127 |
128 |
129 | 130 | 136 |
137 |
138 |
139 | 140 | 141 | 142 | 143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | 155 |
156 |
157 |
158 |
159 | 160 | 161 |
162 |
163 |
164 | 165 | 545 | {% endblock %} --------------------------------------------------------------------------------