├── .github └── dependabot.yml ├── .gitignore ├── LICENSE ├── README.md ├── opentakserver ├── EmailValidator.py ├── PasswordValidator.py ├── SocketServer.py ├── __init__.py ├── app.py ├── blueprints │ ├── __init__.py │ ├── cli.py │ ├── marti_api │ │ ├── __init__.py │ │ ├── certificate_enrollment_api.py │ │ ├── citrap_api.py │ │ ├── contacts_marti_api.py │ │ ├── cot_marti_api.py │ │ ├── data_package_marti_api.py │ │ ├── device_profile_marti_api.py │ │ ├── group_marti_api.py │ │ ├── icons │ │ │ ├── damaged.png │ │ │ ├── team_blue.png │ │ │ ├── team_cyan.png │ │ │ ├── team_darkblue.png │ │ │ ├── team_darkgreen.png │ │ │ ├── team_green.png │ │ │ ├── team_magenta.png │ │ │ ├── team_maroon.png │ │ │ ├── team_orange.png │ │ │ ├── team_purple.png │ │ │ ├── team_red.png │ │ │ ├── team_teal.png │ │ │ ├── team_white.png │ │ │ └── team_yellow.png │ │ ├── marti_api.py │ │ ├── mission_marti_api.py │ │ └── video_marti_api.py │ ├── ots_api │ │ ├── __init__.py │ │ ├── api.py │ │ ├── casevac_api.py │ │ ├── data_package_api.py │ │ ├── device_profile_api.py │ │ ├── eud_stats_api.py │ │ ├── group_api.py │ │ ├── marker_api.py │ │ ├── mediamtx_api.py │ │ ├── meshtastic_api.py │ │ ├── mission_api.py │ │ ├── package_api.py │ │ ├── scheduler_api.py │ │ ├── user_api.py │ │ └── video_api.py │ ├── ots_socketio.py │ └── scheduled_jobs.py ├── ca_config.py ├── certificate_authority.py ├── controllers │ ├── __init__.py │ ├── client_controller.py │ ├── cot_controller.py │ ├── meshtastic_controller.py │ └── rabbitmq_client.py ├── defaultconfig.py ├── extensions.py ├── forms │ ├── MediaMTXGlobalConfig.py │ ├── MediaMTXPathConfig.py │ ├── __init__.py │ ├── casevac_form.py │ ├── data_package_form.py │ ├── device_profile_form.py │ ├── marker_form.py │ ├── package_form.py │ ├── point_form.py │ └── zmist_form.py ├── functions.py ├── logo.py ├── maps │ ├── 4UMaps.xml │ ├── Bing_Hybrid.xml │ ├── Bing_Maps.xml │ ├── Chartbundle Sectional.xml │ ├── Chartbundle_IFR_High.xml │ ├── Chartbundle_IFR_Low.xml │ ├── ESRI_Clarity.xml │ ├── ESRI_Nat_Geo_World.xml │ ├── ESRI_World_Topo.xml │ ├── Google_Hybrid.xml │ ├── Google_Road_Only_(Overlay).xml │ ├── Google_Roadmap_Alt.xml │ ├── Google_Roadmap_No_POI.xml │ ├── Google_Roadmap_Standard.xml │ ├── Google_Satellite_Only.xml │ ├── Google_Terrain.xml │ ├── Google_Terrain_Shading_(Overlay).xml │ ├── Hike_Bike.xml │ ├── MML.xml │ ├── MTB_Map_(Europe).xml │ ├── OSMStandard.xml │ ├── OSM_Cycle.xml │ ├── OSM_Michelin.xml │ ├── OSM_No_Labels.xml │ ├── Stamen_Terrain.xml │ ├── Stamen_Toner.xml │ ├── Stamen_Watercolor.xml │ ├── USA_Topo_Maps.xml │ ├── USDA_FSTopo_(Overlay).xml │ ├── USGSBasemap.xml │ ├── USGSImageryOnly.xml │ ├── USGSImageryTopo.xml │ ├── USGSShadedRelief.xml │ ├── WMFLabs_Hillshading_(Overlay).xml │ ├── Waymarkedtrails_Cycle_Routes_(Overlay).xml │ └── opentopomap.xml ├── migrations │ ├── README │ ├── alembic.ini │ ├── env.py │ ├── script.py.mako │ └── versions │ │ ├── 00546817d518_added_uid_column_to_cot_table.py │ │ ├── 1041fae84708_added_eud_stats_table.py │ │ ├── 14e268184cc5_added_mission_logs_table.py │ │ ├── 21fb5a21f356_fixed_mission_changes_and_mission_uids_.py │ │ ├── 298430e7849d_fixed_video_streams_table.py │ │ ├── 2b5bddd207ae_added_some_cascades.py │ │ ├── 34d5df698b81_added_publish_time_to_packages_table.py │ │ ├── 34dc96ee805b_changed_device_profile_table.py │ │ ├── 414b87c32cc2_added_device_profile_and_package_tables.py │ │ ├── 4c7909c34d4e_initial_migration.py │ │ ├── 4f0173cdb93b_added_mission_change_id_column_to_.py │ │ ├── 5d06227dea50_added_data_sync_tables.py │ │ ├── 6af2256c568d_set_install_on_enrollment_and_.py │ │ ├── 795ebb9262d8_more_cascades.py │ │ ├── 7e9f5d2c193d_changed_phone_number_column_to_.py │ │ ├── 807c3ca8e7d0_added_mission_uids_table.py │ │ ├── 8787888a028f_fixes_to_support_mariadb.py │ │ ├── bde915ea136e_added_mission_logs_table.py │ │ ├── d1f5df78eace_more_data_sync_stuff.py │ │ ├── d31bff4a15c7_added_mission_name_to_cot_table.py │ │ ├── d4cc3d4afdb9_added_groups_table.py │ │ ├── d91957bb59a0_meshtastic_support.py │ │ ├── f107b45529ba_fixes_for_the_packages_table_added_.py │ │ └── f6dfc571d31c_added_columns_to_mission_invtations_.py ├── models │ ├── APSchedulerJobs.py │ ├── Alert.py │ ├── Base.py │ ├── CasEvac.py │ ├── Certificate.py │ ├── Chatrooms.py │ ├── ChatroomsUids.py │ ├── CoT.py │ ├── DataPackage.py │ ├── DeviceProfiles.py │ ├── EUD.py │ ├── EUDStats.py │ ├── GeoChat.py │ ├── Group.py │ ├── GroupEud.py │ ├── Icon.py │ ├── Marker.py │ ├── Meshtastic.py │ ├── Mission.py │ ├── MissionChange.py │ ├── MissionContent.py │ ├── MissionContentMission.py │ ├── MissionInvitation.py │ ├── MissionLogEntry.py │ ├── MissionRole.py │ ├── MissionUID.py │ ├── Packages.py │ ├── Point.py │ ├── RBLine.py │ ├── Team.py │ ├── VideoRecording.py │ ├── VideoStream.py │ ├── WebAuthn.py │ ├── ZMIST.py │ ├── __init__.py │ ├── role.py │ └── user.py ├── mumble │ ├── Murmur.ice │ ├── __init__.py │ ├── mumble_authenticator.py │ └── mumble_ice_app.py ├── proto │ ├── __init__.py │ ├── atak.proto │ └── atak_pb2.py └── sql_jobstore.py ├── poetry.lock ├── pyproject.toml └── tests ├── __init__.py ├── conftest.py └── tests.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | open-pull-requests-limit: 10 10 | directory: "/" 11 | schedule: 12 | interval: "daily" 13 | time: "13:00" 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | opentakserver/secret_key.py 2 | scripts/ 3 | ### Python template 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # poetry 101 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 105 | #poetry.lock 106 | 107 | # pdm 108 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 109 | #pdm.lock 110 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 111 | # in version control. 112 | # https://pdm.fming.dev/#use-with-ide 113 | .pdm.toml 114 | 115 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 116 | __pypackages__/ 117 | 118 | # Celery stuff 119 | celerybeat-schedule 120 | celerybeat.pid 121 | 122 | # SageMath parsed files 123 | *.sage.py 124 | 125 | # Environments 126 | .env 127 | .venv 128 | env/ 129 | venv/ 130 | ENV/ 131 | env.bak/ 132 | venv.bak/ 133 | 134 | # Spyder project settings 135 | .spyderproject 136 | .spyproject 137 | 138 | # Rope project settings 139 | .ropeproject 140 | 141 | # mkdocs documentation 142 | /site 143 | 144 | # mypy 145 | .mypy_cache/ 146 | .dmypy.json 147 | dmypy.json 148 | 149 | # Pyre type checker 150 | .pyre/ 151 | 152 | # pytype static type analyzer 153 | .pytype/ 154 | 155 | # Cython debug symbols 156 | cython_debug/ 157 | 158 | # PyCharm 159 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 160 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 161 | # and can be added to the global gitignore or merged into this file. For a more nuclear 162 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 163 | .idea/ 164 | 165 | ### Flask template 166 | instance/* 167 | !instance/.gitignore 168 | .webassets-cache 169 | .env 170 | 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenTAKServer 2 | 3 | ![PyPI - Downloads](https://img.shields.io/pypi/dm/opentakserver) 4 | ![PyPI - Version](https://img.shields.io/pypi/v/opentakserver) 5 | ![Discord](https://img.shields.io/discord/1183578214459777164?logo=discord&label=Discord&link=https%3A%2F%2Fdiscord.gg%2F6uaVHjtfXN) 6 | ![GitHub Release Date](https://img.shields.io/github/release-date/brian7704/OpenTAKServer) 7 | 8 | 9 | OpenTAKServer (OTS) is yet another open source TAK Server for ATAK, iTAK, and WinTAK. OTS's goal is to be easy to install and use, and to run on both servers and SBCs (ie Raspberry Pi). 10 | 11 | Join us on our [Discord server](https://discord.gg/6uaVHjtfXN) 12 | 13 | ## Current Features 14 | - Connect via TCP from ATAK, WinTAK, and iTAK 15 | - SSL 16 | - Authentication 17 | - [WebUI with a live map](https://github.com/brian7704/OpenTAKServer-UI) 18 | - Client certificate enrollment 19 | - Send and receive messages 20 | - Send and receive points 21 | - Send and receive routes 22 | - Send and receive images 23 | - Share location with other users 24 | - Save CoT messages to a database 25 | - Data Packages 26 | - Alerts 27 | - CasEvac 28 | - Optional Mumble server authentication 29 | - Use your OpenTAKServer username and password to log into your Mumble server 30 | - Video Streaming 31 | - Mission API 32 | - Data Sync plugin 33 | - Fire Area Survey plugin 34 | 35 | ## Planned Features 36 | - Federation 37 | - Groups/Channels 38 | 39 | ## Requirements 40 | - RabbitMQ 41 | - MediaMTX (Only required for video streaming) 42 | - openssl 43 | - nginx 44 | 45 | ## Installation 46 | 47 | ### Ubuntu 48 | 49 | `curl https://i.opentakserver.io/ubuntu_installer -Ls | bash -` 50 | 51 | ### Raspberry Pi 52 | 53 | `curl https://i.opentakserver.io/raspberry_pi_installer -Ls | bash -` 54 | 55 | ### Rocky 9 56 | 57 | `curl -s -L https://i.opentakserver.io/rocky_linux_installer | bash -` 58 | 59 | ### Windows 60 | 61 | Open PowerShell as an administrator and run the following command 62 | 63 | `Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://i.opentakserver.io/windows_installer'))` 64 | 65 | ### MacOS 66 | 67 | `curl -Ls https://i.opentakserver.io/macos_installer | bash -` 68 | 69 | ## Documentation 70 | 71 | https://docs.opentakserver.io 72 | 73 | ## Supporting the project 74 | 75 | If you would like to support the project you can do so [here](https://buymeacoffee.com/opentakserver) -------------------------------------------------------------------------------- /opentakserver/EmailValidator.py: -------------------------------------------------------------------------------- 1 | import tldextract 2 | from flask_security import MailUtil 3 | from opentakserver.extensions import logger 4 | 5 | 6 | class EmailValidator(MailUtil): 7 | 8 | def __init__(self, app): 9 | super().__init__(app) 10 | self.app = app 11 | 12 | def validate(self, email: str) -> str: 13 | domain_whitelist = self.app.config.get("OTS_EMAIL_DOMAIN_WHITELIST") 14 | domain_blacklist = self.app.config.get("OTS_EMAIL_DOMAIN_BLACKLIST") 15 | tld_whitelist = self.app.config.get("OTS_EMAIL_TLD_WHITELIST") 16 | tld_blacklist = self.app.config.get("OTS_EMAIL_TLD_BLACKLIST") 17 | 18 | domain = email.split("@")[-1] 19 | parsed_domain = tldextract.extract(domain) 20 | logger.debug("Got domain: {}".format(domain)) 21 | 22 | if domain_whitelist and domain not in domain_whitelist: 23 | logger.error("Domain {} is not whitelisted".format(domain)) 24 | raise ValueError("Domain {} is not whitelisted".format(domain)) 25 | 26 | if domain_blacklist and domain in domain_blacklist: 27 | logger.error("Domain {} is blacklisted".format(domain)) 28 | raise ValueError("Domain {} is blacklisted".format(domain)) 29 | 30 | if tld_whitelist and parsed_domain.suffix not in tld_whitelist: 31 | logger.error("TLD {} not whitelisted".format(parsed_domain.suffix)) 32 | raise ValueError("TLD {} not whitelisted".format(parsed_domain.suffix)) 33 | 34 | if tld_blacklist and parsed_domain.suffix in tld_blacklist: 35 | logger.error("TLD {} is blacklisted".format(parsed_domain.suffix)) 36 | raise ValueError("TLD {} is blacklisted".format(parsed_domain.suffix)) 37 | 38 | logger.info("Looks like {} is good".format(email)) 39 | return super().validate(email) 40 | -------------------------------------------------------------------------------- /opentakserver/PasswordValidator.py: -------------------------------------------------------------------------------- 1 | import typing as t 2 | 3 | from flask_security import PasswordUtil 4 | 5 | 6 | # Prevents @ and : characters in passwords. When using basic auth with RTSP streams these characters cause invalid URLs 7 | class PasswordValidator(PasswordUtil): 8 | def validate(self, password: str, is_register: bool, **kwargs: t.Any) -> t.Tuple[t.Optional[t.List], str]: 9 | if '@' in password or ':' in password: 10 | return ["Passwords should not include @ or : characters"], password 11 | return super().validate(password, is_register, **kwargs) 12 | -------------------------------------------------------------------------------- /opentakserver/SocketServer.py: -------------------------------------------------------------------------------- 1 | import os 2 | import socket 3 | import ssl 4 | from threading import Thread 5 | 6 | from opentakserver.controllers.client_controller import ClientController 7 | 8 | 9 | class SocketServer(Thread): 10 | def __init__(self, logger, app_context=None, port=8088, ssl_server=False): 11 | super().__init__() 12 | 13 | self.logger = logger 14 | self.port = port 15 | self.ssl = ssl_server 16 | self.shutdown = False 17 | self.daemon = True 18 | self.socket = None 19 | self.clients = [] 20 | self.app_context = app_context 21 | 22 | def run(self): 23 | if self.ssl: 24 | self.socket = self.launch_ssl_server() 25 | elif self.app_context.app.config.get("OTS_ENABLE_TCP_STREAMING_PORT"): 26 | self.socket = self.launch_tcp_server() 27 | else: 28 | self.logger.info("TCP connections are disabled") 29 | return 30 | 31 | self.socket.settimeout(1.0) 32 | 33 | while not self.shutdown: 34 | try: 35 | sock, addr = self.socket.accept() 36 | if self.ssl: 37 | self.logger.info("New SSL connection from {}".format(addr[0])) 38 | else: 39 | self.logger.info("New TCP connection from {}".format(addr[0])) 40 | 41 | new_thread = ClientController(addr[0], addr[1], sock, self.logger, self.app_context.app, self.ssl) 42 | new_thread.daemon = True 43 | new_thread.start() 44 | self.clients.append(new_thread) 45 | except KeyboardInterrupt: 46 | self.socket.close() 47 | break 48 | except TimeoutError: 49 | if self.shutdown: 50 | self.socket.shutdown(socket.SHUT_RDWR) 51 | self.socket.close() 52 | except (OSError, IOError) as e: 53 | if "too many open files" in str(e).lower(): 54 | self.logger.error(str(e)) 55 | self.socket.close() 56 | break 57 | else: 58 | self.logger.warning(str(e)) 59 | except BaseException as e: 60 | self.logger.warning(str(e)) 61 | continue 62 | 63 | if self.ssl: 64 | self.logger.info("SSL server has shut down") 65 | else: 66 | self.logger.info("TCP server has shut down") 67 | 68 | def launch_tcp_server(self): 69 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 70 | s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 71 | s.bind(('0.0.0.0', self.port)) 72 | s.listen(1) 73 | 74 | return s 75 | 76 | def launch_ssl_server(self): 77 | with socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) as sock: 78 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 79 | 80 | context = self.get_ssl_context() 81 | 82 | sconn = context.wrap_socket(sock, server_side=True) 83 | sconn.bind(('0.0.0.0', self.port)) 84 | sconn.listen(0) 85 | 86 | return sconn 87 | 88 | def stop(self): 89 | if self.ssl: 90 | self.logger.warning("Shutting down SSL server") 91 | else: 92 | self.logger.warning("Shutting down TCP server") 93 | 94 | self.shutdown = True 95 | for client in self.clients: 96 | self.logger.debug('Attempting to stop client {}'.format(client.address)) 97 | client.stop() 98 | 99 | def get_ssl_context(self): 100 | context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) 101 | 102 | with self.app_context: 103 | context.load_cert_chain( 104 | os.path.join(self.app_context.app.config.get("OTS_CA_FOLDER"), "certs", "opentakserver", "opentakserver.pem"), 105 | os.path.join(self.app_context.app.config.get("OTS_CA_FOLDER"), "certs", "opentakserver", "opentakserver.nopass.key")) 106 | 107 | context.verify_mode = self.app_context.app.config.get("OTS_SSL_VERIFICATION_MODE") 108 | context.load_verify_locations(cafile=os.path.join(self.app_context.app.config.get("OTS_CA_FOLDER"), 'ca.pem')) 109 | 110 | return context 111 | -------------------------------------------------------------------------------- /opentakserver/__init__.py: -------------------------------------------------------------------------------- 1 | # These version placeholders will be replaced later during substitution. 2 | __version__ = "1.4.6" 3 | __version_tuple__ = (1, 4, 6) 4 | -------------------------------------------------------------------------------- /opentakserver/blueprints/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/__init__.py -------------------------------------------------------------------------------- /opentakserver/blueprints/cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import click 4 | from flask import g, current_app as app 5 | from flask.cli import with_appcontext 6 | 7 | from opentakserver.certificate_authority import CertificateAuthority 8 | from opentakserver.extensions import logger 9 | 10 | 11 | @click.group() 12 | @click.option('-x', '--x-arg', multiple=True, 13 | help='Additional arguments consumed by custom env.py scripts') 14 | @with_appcontext 15 | def ots(x_arg): 16 | g.x_arg = x_arg 17 | 18 | 19 | @ots.command() 20 | @with_appcontext 21 | def create_ca(): 22 | ca = CertificateAuthority(logger, app) 23 | if not ca.check_if_ca_exists(): 24 | logger.info("Creating certificate authority...") 25 | ca.create_ca() 26 | else: 27 | logger.warning("Certificate authority already exists") 28 | sys.exit() 29 | -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | 3 | from opentakserver.blueprints.marti_api.certificate_enrollment_api import certificate_authority_api_blueprint 4 | from opentakserver.blueprints.marti_api.citrap_api import citrap_api_blueprint 5 | from opentakserver.blueprints.marti_api.cot_marti_api import cot_marti_api 6 | from opentakserver.blueprints.marti_api.data_package_marti_api import data_package_marti_api 7 | from opentakserver.blueprints.marti_api.mission_marti_api import mission_marti_api 8 | from opentakserver.blueprints.marti_api.device_profile_marti_api import device_profile_marti_api_blueprint 9 | from opentakserver.blueprints.marti_api.group_marti_api import group_api 10 | from opentakserver.blueprints.marti_api.marti_api import marti_api 11 | from opentakserver.blueprints.marti_api.contacts_marti_api import contacts_api 12 | from opentakserver.blueprints.marti_api.video_marti_api import video_marti_api 13 | 14 | marti_blueprint = Blueprint("marti_blueprint", __name__) 15 | 16 | marti_blueprint.register_blueprint(data_package_marti_api) 17 | marti_blueprint.register_blueprint(marti_api) 18 | marti_blueprint.register_blueprint(cot_marti_api) 19 | marti_blueprint.register_blueprint(device_profile_marti_api_blueprint) 20 | marti_blueprint.register_blueprint(citrap_api_blueprint) 21 | marti_blueprint.register_blueprint(certificate_authority_api_blueprint) 22 | marti_blueprint.register_blueprint(group_api) 23 | marti_blueprint.register_blueprint(mission_marti_api) 24 | marti_blueprint.register_blueprint(contacts_api) 25 | marti_blueprint.register_blueprint(video_marti_api) 26 | -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/citrap_api.py: -------------------------------------------------------------------------------- 1 | import bleach 2 | from flask import Blueprint, current_app as app, request, jsonify 3 | 4 | citrap_api_blueprint = Blueprint("citrap_api_blueprint", __name__) 5 | 6 | 7 | @citrap_api_blueprint.route('/Marti/api/missions/citrap/subscription', methods=['PUT']) 8 | def citrap_subscription(): 9 | uid = bleach.clean(request.args.get('uid')) 10 | response = { 11 | 'version': 3, 'type': 'com.bbn.marti.sync.model.MissionSubscription', 12 | 'data': { 13 | 14 | } 15 | } 16 | return '', 201 17 | 18 | 19 | @citrap_api_blueprint.route('/Marti/api/citrap') 20 | def citrap(): 21 | return jsonify([]) 22 | -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/contacts_marti_api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request, jsonify 2 | from opentakserver.extensions import db, logger 3 | from opentakserver.models.EUD import EUD 4 | 5 | contacts_api = Blueprint("contacts_api", __name__) 6 | 7 | 8 | @contacts_api.route('/Marti/api/contacts/all') 9 | def get_all_contacts(): 10 | logger.info(request.headers) 11 | logger.info(request.data) 12 | 13 | euds = db.session.execute(db.session.query(EUD)).all() 14 | 15 | response = [] 16 | 17 | for eud in euds: 18 | eud = eud[0] 19 | team_name = eud.team.name if eud.team else 'Cyan' 20 | team_role = eud.team_role or 'Team Member' 21 | username = eud.user.username if eud.user else '' 22 | response.append( 23 | {'filterGroups': [], 'notes': username, 'callsign': eud.callsign, 'team': team_name, 24 | 'role': team_role, 'takv': f"{eud.platform} {eud.version}", 'uid': eud.uid} 25 | ) 26 | 27 | return jsonify(response) 28 | -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/cot_marti_api.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from xml.etree.ElementTree import Element, fromstring, tostring 3 | 4 | from flask import Blueprint, request, jsonify 5 | 6 | from opentakserver.functions import datetime_from_iso8601_string 7 | from opentakserver.extensions import db, logger 8 | from opentakserver.models.CoT import CoT 9 | 10 | cot_marti_api = Blueprint('cot_api', __name__) 11 | 12 | """ 13 | Right now OpenTAKServer only uses a few of these for Data Sync. The rest were added as place holders 14 | based on the API docs until I find an example of them actually being used by a TAK client 15 | """ 16 | 17 | 18 | @cot_marti_api.route('/Marti/api/cot') 19 | def get_cots(): 20 | logger.debug(request.headers) 21 | logger.debug(request.args) 22 | 23 | return '', 200 24 | 25 | 26 | @cot_marti_api.route('/Marti/api/cot/xml/') 27 | def get_cot(uid): 28 | logger.debug(request.headers) 29 | logger.debug(request.args) 30 | 31 | cot = db.session.execute(db.session.query(CoT).filter_by(uid=uid)).first() 32 | if not cot: 33 | return jsonify({'success': False, 'error': f"No CoT found for UID {uid}"}), 404 34 | 35 | return cot[0].xml 36 | 37 | 38 | @cot_marti_api.route('/Marti/api/cot/xml//all') 39 | def get_all_cot(uid): 40 | logger.debug(request.headers) 41 | logger.debug(request.args) 42 | 43 | sec_ago = request.args.get('secago') 44 | start = request.args.get('start') 45 | end = request.args.get('end') 46 | 47 | query = db.session.query(CoT).filter_by(uid=uid) 48 | if sec_ago: 49 | try: 50 | query = query.filter(CoT.start >= datetime.timedelta(seconds=int(sec_ago))) 51 | except ValueError: 52 | return jsonify({'success': False, 'error': f'Invalid secago value: {sec_ago}'}), 400 53 | if start: 54 | query = query.filter(CoT.start >= datetime_from_iso8601_string(start)) 55 | if end: 56 | query = query.filter(CoT.stale <= datetime_from_iso8601_string(end)) 57 | 58 | cots = db.session.execute(query) 59 | 60 | events = Element("events") 61 | for cot in cots: 62 | events.append(fromstring(cot[0].xml)) 63 | 64 | return tostring(events).decode('utf-8'), 200 65 | 66 | 67 | @cot_marti_api.route('/Marti/api/cot/sa') 68 | def get_cot_by_time_and_bbox(): 69 | logger.debug(request.headers) 70 | logger.debug(request.args) 71 | 72 | start = request.args.get('start') 73 | end = request.args.get('end') 74 | left = request.args.get('left') 75 | bottom = request.args.get('bottom') 76 | right = request.args.get('right') 77 | top = request.args.get('top') 78 | 79 | return '', 200 80 | -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/damaged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/damaged.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_blue.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_cyan.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_darkblue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_darkblue.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_darkgreen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_darkgreen.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_green.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_magenta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_magenta.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_maroon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_maroon.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_orange.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_purple.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_red.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_teal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_teal.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_white.png -------------------------------------------------------------------------------- /opentakserver/blueprints/marti_api/icons/team_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/blueprints/marti_api/icons/team_yellow.png -------------------------------------------------------------------------------- /opentakserver/blueprints/ots_api/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import request, Blueprint, jsonify, current_app as app 2 | from opentakserver.blueprints.ots_api.api import api_blueprint 3 | from opentakserver.blueprints.ots_api.casevac_api import casevac_api_blueprint 4 | from opentakserver.blueprints.ots_api.data_package_api import data_package_api 5 | from opentakserver.blueprints.ots_api.device_profile_api import device_profile_api_blueprint 6 | from opentakserver.blueprints.ots_api.marker_api import marker_api_blueprint 7 | from opentakserver.blueprints.ots_api.mediamtx_api import mediamtx_api_blueprint 8 | from opentakserver.blueprints.ots_api.meshtastic_api import meshtastic_api_blueprint 9 | from opentakserver.blueprints.ots_api.package_api import packages_blueprint 10 | from opentakserver.blueprints.ots_api.scheduler_api import scheduler_api_blueprint 11 | from opentakserver.blueprints.ots_api.user_api import user_api_blueprint 12 | from opentakserver.blueprints.ots_api.video_api import video_api_blueprint 13 | from opentakserver.blueprints.ots_api.mission_api import data_sync_api 14 | from opentakserver.blueprints.ots_api.group_api import group_api 15 | from opentakserver.blueprints.ots_api.eud_stats_api import eud_stats_blueprint 16 | 17 | ots_api = Blueprint("ots_api", __name__) 18 | ots_api.register_blueprint(api_blueprint) 19 | ots_api.register_blueprint(casevac_api_blueprint) 20 | ots_api.register_blueprint(data_package_api) 21 | ots_api.register_blueprint(device_profile_api_blueprint) 22 | ots_api.register_blueprint(marker_api_blueprint) 23 | ots_api.register_blueprint(mediamtx_api_blueprint) 24 | ots_api.register_blueprint(meshtastic_api_blueprint) 25 | ots_api.register_blueprint(packages_blueprint) 26 | ots_api.register_blueprint(scheduler_api_blueprint) 27 | ots_api.register_blueprint(user_api_blueprint) 28 | ots_api.register_blueprint(video_api_blueprint) 29 | ots_api.register_blueprint(data_sync_api) 30 | ots_api.register_blueprint(group_api) 31 | ots_api.register_blueprint(eud_stats_blueprint) 32 | -------------------------------------------------------------------------------- /opentakserver/blueprints/ots_api/device_profile_api.py: -------------------------------------------------------------------------------- 1 | import traceback 2 | 3 | from flask import Blueprint, request, jsonify 4 | from flask_security import auth_required, roles_required 5 | from sqlalchemy import update 6 | from sqlalchemy.exc import IntegrityError 7 | 8 | from opentakserver.extensions import db, logger 9 | from opentakserver.forms.device_profile_form import DeviceProfileForm 10 | from opentakserver.models.DeviceProfiles import DeviceProfiles 11 | from opentakserver.blueprints.ots_api.api import search, paginate 12 | 13 | device_profile_api_blueprint = Blueprint('device_profile_api_blueprint', __name__) 14 | 15 | 16 | @device_profile_api_blueprint.route('/api/profiles') 17 | @roles_required("administrator") 18 | def get_device_profiles(): 19 | query = db.session.query(DeviceProfiles) 20 | query = search(query, DeviceProfiles, 'preference_key') 21 | query = search(query, DeviceProfiles, 'preference_value') 22 | query = search(query, DeviceProfiles, 'tool') 23 | return paginate(query) 24 | 25 | 26 | @device_profile_api_blueprint.route('/api/profiles', methods=['POST']) 27 | @auth_required() 28 | @roles_required("administrator") 29 | def add_device_profile(): 30 | form = DeviceProfileForm() 31 | if not form.validate(): 32 | return jsonify({'success': False, 'errors': form.errors}), 400 33 | 34 | device_profile = DeviceProfiles() 35 | device_profile.from_wtf(form) 36 | 37 | try: 38 | db.session.add(device_profile) 39 | db.session.commit() 40 | except IntegrityError: 41 | db.session.rollback() 42 | db.session.execute(update(DeviceProfiles).where(DeviceProfiles.preference_key == device_profile.preference_key) 43 | .values(**device_profile.serialize())) 44 | db.session.commit() 45 | 46 | return jsonify({'success': True}) 47 | 48 | 49 | @device_profile_api_blueprint.route('/api/profiles', methods=['DELETE']) 50 | @roles_required("administrator") 51 | def delete_device_profile(): 52 | preference_key = request.args.get('preference_key') 53 | if not preference_key: 54 | return jsonify({'success': False, 'error': 'Please specify the preference_key'}), 400 55 | try: 56 | query = db.session.query(DeviceProfiles) 57 | query = search(query, DeviceProfiles, 'preference_key') 58 | preference = db.session.execute(query).first() 59 | if not preference: 60 | return jsonify({'success': False, 'error': f'Unknown preference_key: {preference_key}'}), 404 61 | 62 | db.session.delete(preference[0]) 63 | db.session.commit() 64 | return jsonify({'success': True}) 65 | except BaseException as e: 66 | logger.error(f"Failed to delete device profile: {e}") 67 | logger.debug(traceback.format_exc()) 68 | return jsonify({'success': False, 'error': str(e)}), 400 69 | -------------------------------------------------------------------------------- /opentakserver/blueprints/ots_api/eud_stats_api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint 2 | from flask_security import auth_required 3 | from opentakserver.extensions import db 4 | from opentakserver.models.EUDStats import EUDStats 5 | from opentakserver.blueprints.ots_api.api import search, paginate 6 | 7 | eud_stats_blueprint = Blueprint('eud_stats_blueprint', __name__) 8 | 9 | 10 | @eud_stats_blueprint.route('/api/eud_stats') 11 | @auth_required() 12 | def get_stats(): 13 | 14 | query = db.session.query(EUDStats) 15 | query = search(query, EUDStats, 'eud_uid') 16 | # TODO: Implement date range search 17 | #query = search(query, EUDStats, 'from') 18 | #query = search(query, EUDStats, 'to') 19 | 20 | rows = db.session.execute(query.order_by(EUDStats.id.desc()).limit(50)).all() 21 | 22 | results = {'results': [], 'total_pages': 1, 'current_page': 1, 'per_page': len(rows)} 23 | 24 | for row in rows: 25 | results['results'].insert(0, row[0].to_json()) 26 | 27 | return results 28 | -------------------------------------------------------------------------------- /opentakserver/blueprints/ots_api/group_api.py: -------------------------------------------------------------------------------- 1 | from flask import Blueprint, request 2 | from flask_security import auth_required 3 | 4 | from opentakserver.extensions import db 5 | from opentakserver.blueprints.ots_api.api import search, paginate 6 | from opentakserver.models.Group import Group 7 | 8 | group_api = Blueprint("group_api", __name__) 9 | 10 | 11 | @group_api.route('/api/groups') 12 | @auth_required() 13 | def get_groups(): 14 | query = db.session.query(Group) 15 | query = search(query, Group, 'group_name') 16 | query = search(query, Group, 'direction') 17 | query = search(query, Group, 'group_type') 18 | query = search(query, Group, 'bitpos') 19 | query = search(query, Group, 'active') 20 | 21 | return paginate(query) 22 | -------------------------------------------------------------------------------- /opentakserver/blueprints/ots_api/scheduler_api.py: -------------------------------------------------------------------------------- 1 | from apscheduler.triggers.combining import AndTrigger, OrTrigger 2 | from apscheduler.triggers.cron import CronTrigger 3 | from apscheduler.triggers.date import DateTrigger 4 | from apscheduler.triggers.interval import IntervalTrigger 5 | from flask import Blueprint, request, jsonify 6 | from flask_apscheduler import api 7 | from flask_security import roles_accepted 8 | from opentakserver.extensions import apscheduler 9 | 10 | scheduler_api_blueprint = Blueprint('schedule_api_blueprint', __name__) 11 | 12 | 13 | @scheduler_api_blueprint.route("/api/scheduler/", strict_slashes=False) 14 | @roles_accepted("administrator") 15 | def scheduler_info(): 16 | return api.get_scheduler_info() 17 | 18 | 19 | @scheduler_api_blueprint.route("/api/scheduler/jobs", strict_slashes=False) 20 | @roles_accepted("administrator") 21 | def get_jobs(): 22 | return api.get_jobs() 23 | 24 | 25 | @scheduler_api_blueprint.route("/api/scheduler/job/pause", methods=['POST'], strict_slashes=False) 26 | @roles_accepted("administrator") 27 | def pause_job(): 28 | if 'job_id' not in request.json: 29 | return {'success': False, 'errors': 'Please provide a job_id'}, 400, {'Content-Type': 'application/json'} 30 | try: 31 | return api.pause_job(request.json['job_id']) 32 | except BaseException as e: 33 | return {'success': False, 'error': str(e)}, 400, {'Content-Type': 'application/json'} 34 | 35 | 36 | @scheduler_api_blueprint.route("/api/scheduler/job/resume", methods=['POST'], strict_slashes=False) 37 | @roles_accepted("administrator") 38 | def resume_job(): 39 | if 'job_id' not in request.json: 40 | return {'success': False, 'errors': 'Please provide a job_id'}, 400, {'Content-Type': 'application/json'} 41 | try: 42 | # 0 == STATE_STOPPED 43 | if apscheduler.state == 0: 44 | apscheduler.start() 45 | # 2 == STATE_PAUSED 46 | elif apscheduler.state == 2: 47 | apscheduler.resume() 48 | 49 | return api.resume_job(request.json['job_id']) 50 | except BaseException as e: 51 | return {'success': False, 'error': str(e)}, 400, {'Content-Type': 'application/json'} 52 | 53 | 54 | @scheduler_api_blueprint.route("/api/scheduler/job/run", methods=['POST'], strict_slashes=False) 55 | @roles_accepted("administrator") 56 | def run_job(): 57 | if 'job_id' not in request.json: 58 | return {'success': False, 'errors': 'Please provide a job_id'}, 400, {'Content-Type': 'application/json'} 59 | try: 60 | return api.run_job(request.json['job_id']) 61 | except BaseException as e: 62 | return {'success': False, 'error': str(e)}, 400, {'Content-Type': 'application/json'} 63 | 64 | 65 | @scheduler_api_blueprint.route("/api/scheduler/job/modify", methods=['POST'], strict_slashes=False) 66 | @roles_accepted("administrator") 67 | def modify_job(): 68 | if 'job_id' not in request.json: 69 | return {'success': False, 'errors': 'Please provide a job_id'}, 400, {'Content-Type': 'application/json'} 70 | try: 71 | job_id = request.json.pop('job_id') 72 | trigger = request.json.pop('trigger') 73 | job = apscheduler.get_job(job_id) 74 | if trigger == 'interval': 75 | trigger = IntervalTrigger(**request.json) 76 | elif trigger == 'cron': 77 | trigger = CronTrigger(**request.json) 78 | elif trigger == 'date': 79 | trigger = DateTrigger(**request.json) 80 | elif trigger == 'and': 81 | trigger = AndTrigger(**request.json) 82 | elif trigger == 'or': 83 | trigger = OrTrigger(**request.json) 84 | else: 85 | return jsonify({'success': False, 'error': 'Invalid trigger: {}'.format(trigger)}), 400 86 | 87 | job.modify(trigger=trigger) 88 | return jsonify({'success': True}) 89 | except BaseException as e: 90 | return {'success': False, 'error': str(e)}, 400, {'Content-Type': 'application/json'} 91 | -------------------------------------------------------------------------------- /opentakserver/blueprints/ots_api/video_api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import traceback 3 | 4 | import pathlib 5 | 6 | import bleach 7 | from flask import current_app as app, request, Blueprint, jsonify, send_from_directory 8 | from flask_security import auth_required 9 | 10 | from opentakserver.blueprints.ots_api.api import search, paginate 11 | from opentakserver.extensions import logger, db 12 | 13 | from opentakserver.models.VideoStream import VideoStream 14 | from opentakserver.models.VideoRecording import VideoRecording 15 | 16 | video_api_blueprint = Blueprint('video_api_blueprint', __name__) 17 | 18 | 19 | @video_api_blueprint.route('/api/videos/thumbnail', methods=['GET']) 20 | @auth_required() 21 | def thumbnail(): 22 | path = request.args.get("path") 23 | recording = request.args.get("recording") 24 | if not path: 25 | return jsonify({"success": False, "error": "Please specify a path"}), 400 26 | 27 | if recording and os.path.exists(os.path.join(app.config.get("OTS_DATA_FOLDER"), "mediamtx", "recordings", path, recording + ".png")): 28 | return send_from_directory(os.path.join(app.config.get("OTS_DATA_FOLDER"), "mediamtx", "recordings", path), 29 | recording + ".png") 30 | 31 | elif os.path.exists(os.path.join(app.config.get("OTS_DATA_FOLDER"), "mediamtx", "recordings", path)): 32 | return send_from_directory(os.path.join(app.config.get("OTS_DATA_FOLDER"), "mediamtx", "recordings", path), 33 | "thumbnail.png") 34 | 35 | return jsonify({"success": False, "error": "Please specify a valid path"}), 400 36 | 37 | 38 | @video_api_blueprint.route('/api/videos/recordings') 39 | @auth_required() 40 | def video_recordings(): 41 | query = db.session.query(VideoRecording) 42 | query = search(query, VideoRecording, 'path') 43 | 44 | return paginate(query) 45 | 46 | 47 | @video_api_blueprint.route('/api/videos/recording', methods=['GET', 'DELETE', 'HEAD']) 48 | @auth_required() 49 | def download_recording(): 50 | if not request.args.get("id"): 51 | return jsonify({'success': False, 'error': 'Please specify a recording ID'}), 400 52 | recording_id = bleach.clean(request.args.get('id')) 53 | 54 | try: 55 | recording = \ 56 | db.session.execute(db.session.query(VideoRecording).filter(VideoRecording.id == recording_id)).first()[0] 57 | 58 | if request.method == 'GET': 59 | filename = pathlib.Path(recording.segment_path) 60 | return send_from_directory(filename.parent, filename.name) 61 | elif request.method == 'DELETE': 62 | os.remove(recording.segment_path) 63 | db.session.delete(recording) 64 | db.session.commit() 65 | return jsonify({'success': True}) 66 | elif request.method == 'HEAD': 67 | return '', 200 68 | except: 69 | logger.error(traceback.format_exc()) 70 | return jsonify({'success': False, 'error': 'Recording not found'}), 404 71 | 72 | 73 | @video_api_blueprint.route('/api/video_streams') 74 | @auth_required() 75 | def get_video_streams(): 76 | query = db.session.query(VideoStream) 77 | query = search(query, VideoStream, 'username') 78 | query = search(query, VideoStream, 'protocol') 79 | query = search(query, VideoStream, 'path') 80 | query = search(query, VideoStream, 'uid') 81 | 82 | return paginate(query) 83 | -------------------------------------------------------------------------------- /opentakserver/blueprints/ots_socketio.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from flask import request, Blueprint 4 | from flask_security import current_user 5 | from flask_socketio import disconnect 6 | 7 | from opentakserver.extensions import logger, socketio 8 | 9 | ots_socketio_blueprint = Blueprint('ots_socketio_blueprint', __name__) 10 | 11 | 12 | def authenticated_only(f): 13 | @functools.wraps(f) 14 | def wrapped(*args, **kwargs): 15 | if not current_user.is_authenticated: 16 | logger.debug("Disconnecting {} from {}".format(request.sid, request.namespace)) 17 | disconnect(request.sid, namespace=request.namespace) 18 | else: 19 | return f(*args, **kwargs) 20 | 21 | return wrapped 22 | 23 | 24 | @socketio.on('connect', namespace="/socket.io") 25 | @authenticated_only 26 | def connect(data): 27 | logger.info('got a socketio connection from {}'.format(current_user.username)) 28 | 29 | 30 | @socketio.on('message', namespace="/socket.io") 31 | @authenticated_only 32 | def message(message): 33 | logger.info("Got a message".format(message)) 34 | -------------------------------------------------------------------------------- /opentakserver/ca_config.py: -------------------------------------------------------------------------------- 1 | from jinja2 import Template 2 | 3 | ca_config = """default_crl_days= 730 # how long before next CRL 4 | 5 | [ ca ] 6 | default_ca = CA_default # The default ca section 7 | 8 | [ CA_default ] 9 | dir = . # Where everything is kept 10 | certs = $dir # Where the issued certs are kept 11 | crl_dir = $dir/crl # Where the issued crl are kept 12 | database = $dir/crl_index.txt # database index file. 13 | default_md = default # use public key default MD 14 | 15 | [ req ] 16 | default_bits = 2048 17 | default_keyfile = ca.pem 18 | distinguished_name = req_distinguished_name 19 | x509_extensions = v3_ca 20 | 21 | [ req_distinguished_name ] 22 | countryName_min = 2 23 | countryName_max = 2 24 | commonName_max = 64 25 | 26 | [ v3_ca ] 27 | #basicConstraints=critical,CA:TRUE, pathlen:2 28 | basicConstraints=critical,CA:TRUE 29 | keyUsage=critical, cRLSign, keyCertSign 30 | #nameConstraints=critical,permitted;DNS:.bbn.com # this allows you to restrict a CA to only issue server certs for a particular domain 31 | 32 | [ client ] 33 | basicConstraints=critical,CA:FALSE 34 | keyUsage=critical, digitalSignature, keyEncipherment 35 | extendedKeyUsage = critical, clientAuth 36 | #extendedKeyUsage = critical, clientAuth, challengePassword 37 | #authorityInfoAccess = OCSP;URI: http://localhost:4444 38 | 39 | [ server ] 40 | basicConstraints=critical,CA:FALSE 41 | keyUsage=critical, digitalSignature, keyEncipherment 42 | extendedKeyUsage = critical, clientAuth, serverAuth 43 | #authorityInfoAccess = OCSP;URI: http://localhost:4444""" 44 | 45 | server_config = Template("""default_crl_days= 730 # how long before next CRL 46 | 47 | [ ca ] 48 | default_ca = CA_default # The default ca section 49 | 50 | [ CA_default ] 51 | dir = . # Where everything is kept 52 | certs = $dir # Where the issued certs are kept 53 | crl_dir = $dir/crl # Where the issued crl are kept 54 | database = $dir/crl_index.txt # database index file. 55 | default_md = default # use public key default MD 56 | 57 | [ req ] 58 | default_bits = 2048 59 | default_keyfile = ca.pem 60 | distinguished_name = req_distinguished_name 61 | x509_extensions = v3_ca 62 | 63 | [ req_distinguished_name ] 64 | countryName_min = 2 65 | countryName_max = 2 66 | commonName_max = 64 67 | 68 | [ v3_ca ] 69 | #basicConstraints=critical,CA:TRUE, pathlen:2 70 | basicConstraints=critical,CA:TRUE 71 | keyUsage=critical, cRLSign, keyCertSign 72 | #nameConstraints=critical,permitted;DNS:.bbn.com # this allows you to restrict a CA to only issue server certs for a particular domain 73 | 74 | [ client ] 75 | basicConstraints=critical,CA:FALSE 76 | keyUsage=critical, digitalSignature, keyEncipherment 77 | extendedKeyUsage = critical, clientAuth 78 | #extendedKeyUsage = critical, clientAuth, challengePassword 79 | #authorityInfoAccess = OCSP;URI: http://localhost:4444 80 | 81 | [ server ] 82 | basicConstraints=critical,CA:FALSE 83 | keyUsage=critical, digitalSignature, keyEncipherment 84 | extendedKeyUsage = critical, clientAuth, serverAuth 85 | #authorityInfoAccess = OCSP;URI: http://localhost:4444 86 | 87 | subjectAltName = @alt_names 88 | [alt_names] 89 | {{ alt_name_field }} = {{ common_name }} 90 | """) -------------------------------------------------------------------------------- /opentakserver/controllers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/controllers/__init__.py -------------------------------------------------------------------------------- /opentakserver/controllers/rabbitmq_client.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from threading import Thread 3 | from pika.channel import Channel 4 | 5 | import flask_sqlalchemy 6 | import pika 7 | 8 | 9 | class RabbitMQClient: 10 | def __init__(self, context, logger, db, socketio): 11 | self.context = context 12 | self.logger = logger 13 | self.db: flask_sqlalchemy.SQLAlchemy = db 14 | self.socketio = socketio 15 | 16 | self.online_euds = {} 17 | self.online_callsigns = {} 18 | self.exchanges = [] 19 | 20 | try: 21 | self.rabbit_connection = pika.SelectConnection(pika.ConnectionParameters(self.context.app.config.get("OTS_RABBITMQ_SERVER_ADDRESS")), 22 | self.on_connection_open) 23 | self.rabbit_channel: Channel = None 24 | self.iothread = Thread(target=self.rabbit_connection.ioloop.start) 25 | self.iothread.daemon = True 26 | self.iothread.start() 27 | self.is_consuming = False 28 | except BaseException as e: 29 | self.logger.error("Failed to connect to rabbitmq: {}".format(e)) 30 | return 31 | 32 | def on_connection_open(self, connection): 33 | self.rabbit_connection.channel(on_open_callback=self.on_channel_open) 34 | self.rabbit_connection.add_on_close_callback(self.on_close) 35 | 36 | def on_channel_open(self, channel): 37 | raise NotImplemented 38 | 39 | def on_close(self, channel, error): 40 | self.logger.error("cot_controller closing RabbitMQ connection: {}".format(error)) 41 | 42 | def on_message(self, unused_channel, basic_deliver, properties, body): 43 | raise NotImplemented 44 | -------------------------------------------------------------------------------- /opentakserver/extensions.py: -------------------------------------------------------------------------------- 1 | import colorlog 2 | from flask_migrate import Migrate 3 | from flask_sqlalchemy import SQLAlchemy 4 | from flask_socketio import SocketIO 5 | from opentakserver.models.Base import Base 6 | from flask_mailman import Mail 7 | from flask_apscheduler import APScheduler 8 | 9 | logger = colorlog.getLogger('OpenTAKServer') 10 | 11 | mail = Mail() 12 | 13 | apscheduler = APScheduler() 14 | 15 | db = SQLAlchemy(model_class=Base) 16 | 17 | socketio = SocketIO(async_mode='eventlet') 18 | 19 | migrate = Migrate() 20 | -------------------------------------------------------------------------------- /opentakserver/forms/MediaMTXGlobalConfig.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, IntegerField, BooleanField 3 | 4 | 5 | class MediaMTSGlobalConfig(FlaskForm): 6 | logLevel = StringField() 7 | #logDestinations 8 | logFile = StringField() 9 | readTimeout = StringField() 10 | writeTimeout = StringField() 11 | writeQueueSize = IntegerField() 12 | udpMaxPayloadSize = IntegerField() 13 | externalAuthenticationURL = StringField() 14 | api = BooleanField() 15 | apiAddress = StringField() 16 | metrics = BooleanField() 17 | metricsAddress = StringField() 18 | pprof = BooleanField() 19 | pprofAddress = StringField() 20 | runOnConnect = StringField() 21 | runOnConnectRestart = BooleanField() 22 | runOnDisconnect = StringField() 23 | rtsp = BooleanField() 24 | #protocols 25 | encryption = StringField() 26 | rtspAddress = StringField() 27 | rtspsAddress = StringField() 28 | rtpAddress = StringField() 29 | rtcpAddress = StringField() 30 | multicastIPRange = StringField() 31 | multicastRTPPort = IntegerField() 32 | multicastRTCPPort = IntegerField() 33 | serverKey = StringField() 34 | serverCert = StringField() 35 | #authMethods 36 | rtmp = BooleanField() 37 | rtmpAddress = StringField() 38 | rtmpEncryption = StringField() 39 | rtmpsAddress = StringField() 40 | rtmpServerKey = StringField() 41 | rtmpServerCert = StringField() 42 | hls = BooleanField() 43 | hlsAddress = StringField() 44 | hlsEncryption = BooleanField() 45 | hlsServerKey = StringField() 46 | hlsServerCert = StringField() 47 | hlsAlwaysRemux = BooleanField() 48 | hlsVariant = StringField() 49 | hlsSegmentCount = IntegerField() 50 | hlsSegmentDuration = StringField() 51 | hlsPartDuration = StringField() 52 | hlsSegmentMaxSize = StringField() 53 | hlsAllowOrigin = StringField() 54 | #hlsTrustedProxies 55 | hlsDirectory = StringField() 56 | webrtc = BooleanField() 57 | webrtcAddress = StringField() 58 | webrtcEncryption = BooleanField() 59 | webrtcServerKey = StringField() 60 | webrtcServerCert = StringField() 61 | webrtcAllowOrigin = StringField() 62 | #webrtcTrustedProxies 63 | webrtcLocalUDPAddress = StringField() 64 | webrtcLocalTCPAddress = StringField() 65 | webrtcIPsFromInterfaces = BooleanField() 66 | #webrtcIPsFromInterfacesList 67 | #webrtcAdditionalHosts 68 | #webrtcICEServers2 69 | srt = BooleanField() 70 | srtAddress = StringField() 71 | 72 | def serialize(self): 73 | return_value = {} 74 | for field in self._fields: 75 | if field != 'csrf_token': 76 | return_value[field] = self._fields[field].data 77 | return return_value 78 | -------------------------------------------------------------------------------- /opentakserver/forms/MediaMTXPathConfig.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, IntegerField, BooleanField 3 | 4 | 5 | class MediaMTXPathConfig(FlaskForm): 6 | def __init__(self, formdata, **kwargs): 7 | super().__init__(formdata, **kwargs) 8 | 9 | name = StringField() 10 | source = StringField() 11 | sourceFingerprint = StringField() 12 | sourceOnDemand = BooleanField(default=True) 13 | sourceOnDemandStartTimeout = StringField() 14 | sourceOnDemandCloseAfter = StringField() 15 | maxReaders = IntegerField() 16 | srtReadPassphrase = StringField() 17 | fallback = StringField() 18 | record = BooleanField(default=False) 19 | recordPath = StringField() 20 | recordFormat = StringField() 21 | recordPartDuration = StringField() 22 | recordSegmentDuration = StringField() 23 | recordDeleteAfter = StringField() 24 | publishUser = StringField() 25 | publishPass = StringField() 26 | #publishIPs 27 | readUser = StringField() 28 | readPass = StringField() 29 | #readIPs 30 | overridePublisher = BooleanField() 31 | srtPublishPassphrase = StringField() 32 | rtspTransport = StringField() 33 | rtspAnyPort = BooleanField() 34 | rtspRangeType = StringField() 35 | rtspRangeStart = StringField() 36 | sourceRedirect = StringField() 37 | rpiCameraCamID = IntegerField() 38 | rpiCameraWidth = IntegerField() 39 | rpiCameraHeight = IntegerField() 40 | rpiCameraHFlip = BooleanField() 41 | rpiCameraVFlip = BooleanField() 42 | rpiCameraBrightness = IntegerField() 43 | rpiCameraContrast = IntegerField() 44 | rpiCameraSaturation = IntegerField() 45 | rpiCameraSharpness = IntegerField() 46 | rpiCameraExposure = StringField() 47 | rpiCameraAWB = StringField() 48 | rpiCameraDenoise = StringField() 49 | rpiCameraShutter = IntegerField() 50 | rpiCameraMetering = StringField() 51 | rpiCameraGain = IntegerField() 52 | rpiCameraEV = IntegerField() 53 | rpiCameraROI = StringField() 54 | rpiCameraHDR = BooleanField() 55 | rpiCameraTuningFile = StringField() 56 | rpiCameraMode = StringField() 57 | rpiCameraFPS = IntegerField() 58 | rpiCameraIDRPeriod = IntegerField() 59 | rpiCameraBitrate = IntegerField() 60 | rpiCameraProfile = StringField() 61 | rpiCameraLevel = StringField() 62 | rpiCameraAfMode = StringField() 63 | rpiCameraAfRange = StringField() 64 | rpiCameraAfSpeed = StringField() 65 | rpiCameraLensPosition = IntegerField() 66 | rpiCameraAfWindow = StringField() 67 | rpiCameraTextOverlayEnable = BooleanField() 68 | rpiCameraTextOverlay = StringField() 69 | runOnInit = StringField() 70 | runOnInitRestart = BooleanField() 71 | runOnDemand = StringField() 72 | runOnDemandRestart = BooleanField() 73 | runOnDemandStartTimeout = StringField() 74 | runOnDemandCloseAfter = StringField() 75 | runOnUnDemand = StringField() 76 | runOnReady = StringField() 77 | runOnReadyRestart = BooleanField() 78 | runOnNotReady = StringField() 79 | runOnRead = StringField() 80 | runOnReadRestart = BooleanField() 81 | runOnUnread = StringField() 82 | runOnRecordSegmentCreate = StringField() 83 | runOnRecordSegmentComplete = StringField() 84 | 85 | def serialize(self): 86 | return_value = {} 87 | for field in self._fields: 88 | if field != 'csrf_token': 89 | return_value[field] = self._fields[field].data 90 | return return_value 91 | -------------------------------------------------------------------------------- /opentakserver/forms/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/forms/__init__.py -------------------------------------------------------------------------------- /opentakserver/forms/casevac_form.py: -------------------------------------------------------------------------------- 1 | from wtforms import StringField, IntegerField, BooleanField, FloatField, FieldList, FormField 2 | from wtforms.validators import DataRequired, UUID 3 | 4 | from opentakserver.forms.point_form import PointForm 5 | from opentakserver.forms.zmist_form import ZmistForm 6 | 7 | 8 | class CasEvacForm(PointForm): 9 | uid = StringField(validators=[DataRequired(), UUID()]) 10 | ambulatory = IntegerField() 11 | casevac = BooleanField() 12 | child = IntegerField() 13 | enemy = IntegerField() 14 | epw = IntegerField() 15 | equipment_detail = StringField() 16 | equipment_none = BooleanField() 17 | equipment_other = BooleanField() 18 | extraction_equipment = BooleanField() 19 | freq = FloatField() 20 | friendlies = StringField() 21 | hlz_marking = IntegerField() 22 | hlz_remarks = StringField() 23 | hoist = BooleanField() 24 | litter = IntegerField() 25 | marked_by = StringField() 26 | medline_remarks = StringField() 27 | nonus_civilian = IntegerField() 28 | nonus_military = IntegerField() 29 | obstacles = StringField() 30 | priority = IntegerField() 31 | routine = IntegerField() 32 | security = IntegerField() 33 | terrain_loose = BooleanField() 34 | terrain_other = BooleanField() 35 | terrain_other_detail = BooleanField() 36 | terrain_detail = StringField() 37 | terrain_none = BooleanField() 38 | terrain_rough = BooleanField() 39 | terrain_slope = BooleanField() 40 | terrain_slope_dir = StringField() 41 | title = StringField(validators=[DataRequired()]) 42 | urgent = IntegerField() 43 | us_civilian = IntegerField() 44 | us_military = IntegerField() 45 | ventilator = BooleanField() 46 | winds_are_from = StringField() 47 | zone_prot_selection = IntegerField() 48 | zmist = FieldList(FormField(ZmistForm), min_entries=0) 49 | -------------------------------------------------------------------------------- /opentakserver/forms/data_package_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, BooleanField 3 | from wtforms.validators import Optional, DataRequired 4 | 5 | from opentakserver.functions import false_values 6 | 7 | 8 | class DataPackageUpdateForm(FlaskForm): 9 | hash = StringField(validators=[DataRequired()]) 10 | install_on_enrollment = BooleanField(validators=[Optional()], false_values=false_values) 11 | install_on_connection = BooleanField(validators=[Optional()], false_values=false_values) 12 | -------------------------------------------------------------------------------- /opentakserver/forms/device_profile_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, BooleanField 3 | from wtforms.validators import Optional 4 | 5 | from opentakserver.functions import false_values 6 | 7 | 8 | class DeviceProfileForm(FlaskForm): 9 | preference_key = StringField(validators=[Optional()]) 10 | preference_value = StringField(validators=[Optional()]) 11 | value_class = StringField(validators=[Optional()]) 12 | enrollment = BooleanField(false_values=false_values) 13 | connection = BooleanField(false_values=false_values) 14 | tool = StringField(validators=[Optional()]) 15 | active = BooleanField(false_values=false_values) 16 | -------------------------------------------------------------------------------- /opentakserver/forms/marker_form.py: -------------------------------------------------------------------------------- 1 | from wtforms import StringField, IntegerField, BooleanField, DateTimeField 2 | from wtforms.validators import DataRequired, UUID 3 | 4 | from opentakserver.forms.point_form import PointForm 5 | 6 | 7 | class MarkerForm(PointForm): 8 | uid = StringField(validators=[DataRequired(), UUID()]) 9 | callsign = StringField(validators=[DataRequired()]) 10 | affiliation = StringField() 11 | battle_dimension = StringField() 12 | readiness = BooleanField() 13 | argb = IntegerField() 14 | color_hex = StringField() 15 | iconset_path = StringField() 16 | parent_callsign = StringField() 17 | production_time = DateTimeField(format="%Y-%m-%dT%H:%M:%S.%fZ") 18 | relation = StringField() 19 | relation_type = StringField() 20 | location_source = StringField() 21 | remarks = StringField() 22 | mil_std_2525c = StringField() 23 | -------------------------------------------------------------------------------- /opentakserver/forms/package_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from flask_wtf.file import FileRequired, FileAllowed 3 | from wtforms import StringField, FileField, BooleanField 4 | from wtforms.validators import Optional, DataRequired 5 | from opentakserver.functions import false_values 6 | 7 | 8 | class PackageForm(FlaskForm): 9 | platform = StringField(default="Android") 10 | plugin_type = StringField(default="plugin") 11 | apk = FileField(validators=[FileRequired(), FileAllowed(['apk'])]) 12 | icon = FileField(validators=[Optional()]) 13 | description = StringField(validators=[Optional()]) 14 | install_on_enrollment = BooleanField(false_values=false_values) 15 | install_on_connection = BooleanField(false_values=false_values) 16 | 17 | 18 | class PackageUpdateForm(FlaskForm): 19 | package_name = StringField(validators=[DataRequired()]) 20 | install_on_enrollment = BooleanField(false_values=false_values) 21 | install_on_connection = BooleanField(false_values=false_values) 22 | -------------------------------------------------------------------------------- /opentakserver/forms/point_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, IntegerField, BooleanField, FloatField, DateTimeField 3 | from wtforms.validators import DataRequired, UUID, NumberRange, Optional 4 | 5 | 6 | class PointForm(FlaskForm): 7 | latitude = FloatField(validators=[DataRequired(), NumberRange(min=-90, max=90)]) 8 | longitude = FloatField(validators=[DataRequired(), NumberRange(min=-180, max=180)]) 9 | ce = FloatField(default=9999999.0) 10 | hae = FloatField(default=9999999.0) 11 | le = FloatField(default=9999999.0) 12 | course = FloatField(default=0.0, validators=[NumberRange(min=0)]) 13 | speed = FloatField(default=0.0, validators=[NumberRange(min=0)]) 14 | location_source = StringField() 15 | battery = FloatField(validators=[NumberRange(min=0, max=100), Optional()], default=None) 16 | timestamp = DateTimeField(format="%Y-%m-%dT%H:%M:%S.%fZ", validators=[DataRequired()]) 17 | azimuth = FloatField(validators=[NumberRange(min=0, max=360), Optional()], default=None) 18 | fov = FloatField(validators=[NumberRange(min=0, max=360), Optional()], default=None) 19 | -------------------------------------------------------------------------------- /opentakserver/forms/zmist_form.py: -------------------------------------------------------------------------------- 1 | from flask_wtf import FlaskForm 2 | from wtforms import StringField, IntegerField, BooleanField, FloatField 3 | from wtforms.validators import DataRequired, UUID 4 | 5 | 6 | class ZmistForm(FlaskForm): 7 | i = StringField() 8 | m = StringField() 9 | s = StringField() 10 | t = StringField() 11 | title = StringField() 12 | z = StringField() 13 | -------------------------------------------------------------------------------- /opentakserver/logo.py: -------------------------------------------------------------------------------- 1 | ots_logo = (""" 2 | 3 | 4 | _____ _____ 5 | ( ___ ) ( ___ ) 6 | | |~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~| | 7 | | | | | 8 | | | ██████╗ ██████╗ ███████╗███╗ ██╗████████╗ █████╗ ██╗ ██╗███████╗███████╗██████╗ ██╗ ██╗███████╗██████╗ | | 9 | | | ██╔═══██╗██╔══██╗██╔════╝████╗ ██║╚══██╔══╝██╔══██╗██║ ██╔╝██╔════╝██╔════╝██╔══██╗██║ ██║██╔════╝██╔══██╗ | | 10 | | | ██║ ██║██████╔╝█████╗ ██╔██╗ ██║ ██║ ███████║█████╔╝ ███████╗█████╗ ██████╔╝██║ ██║█████╗ ██████╔╝ | | 11 | | | ██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║ ██║ ██╔══██║██╔═██╗ ╚════██║██╔══╝ ██╔══██╗╚██╗ ██╔╝██╔══╝ ██╔══██╗ | | 12 | | | ╚██████╔╝██║ ███████╗██║ ╚████║ ██║ ██║ ██║██║ ██╗███████║███████╗██║ ██║ ╚████╔╝ ███████╗██║ ██║ | | 13 | | | ╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ╚═╝ ╚═══╝ ╚══════╝╚═╝ ╚═╝ | | 14 | | | | | 15 | |___|~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~|___| 16 | (_____) (_____) 17 | 18 | 19 | """) 20 | -------------------------------------------------------------------------------- /opentakserver/maps/4UMaps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4UMaps 4 | 0 5 | 18 6 | png 7 | https://tileserver.4umaps.com/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/Bing_Hybrid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bing Hybrid 4 | 0 5 | 20 6 | png 7 | https://ecn.t2.tiles.virtualearth.net/tiles/h{$q}?g=761&mkt=en-us 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/Bing_Maps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Bing Street 4 | 0 5 | 20 6 | png 7 | None 8 | https://r0.ortho.tiles.virtualearth.net/tiles/r{$q}.png?g=45 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Chartbundle Sectional.xml: -------------------------------------------------------------------------------- 1 | 2 | Chartbundle Sectional 3 | 4 4 | 12 5 | png 6 | None 7 | http://wms.chartbundle.com/tms/v1.0/sec/{$z}/{$x}/{$y}.png?type=google 8 | #000000 9 | 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Chartbundle_IFR_High.xml: -------------------------------------------------------------------------------- 1 | 2 | Chartbundle IFR High 3 | 4 4 | 13 5 | png 6 | None 7 | http://wms.chartbundle.com/tms/v1.0/enrh/{$z}/{$x}/{$y}.png?type=google 8 | #000000 9 | 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Chartbundle_IFR_Low.xml: -------------------------------------------------------------------------------- 1 | 2 | Chartbundle IFR Low 3 | 4 4 | 13 5 | png 6 | None 7 | http://wms.chartbundle.com/tms/v1.0/enrl/{$z}/{$x}/{$y}.png?type=google 8 | #000000 9 | 10 | -------------------------------------------------------------------------------- /opentakserver/maps/ESRI_Clarity.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ESRI World Imagery (Clarity) Beta 4 | 1 5 | 20 6 | jpg 7 | None 8 | http://clarity.maptiles.arcgis.com/arcgis/rest/services/World_Imagery/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/ESRI_Nat_Geo_World.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ESRI Nat Geo World 4 | 1 5 | 20 6 | jpg 7 | None 8 | https://server.arcgisonline.com/ArcGIS/rest/services/NatGeo_World_Map/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/ESRI_World_Topo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ESRI World Topo 4 | 1 5 | 20 6 | jpg 7 | None 8 | https://server.arcgisonline.com/ArcGIS/rest/services/World_Topo_Map/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Google_Hybrid.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Hybrid 4 | 0 5 | 20 6 | jpg 7 | None 8 | http://mt1.google.com/vt/lyrs=y&x={$x}&y={$y}&z={$z} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Google_Road_Only_(Overlay).xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Road Only (Overlay) 4 | 0 5 | 20 6 | jpg 7 | None 8 | http://mt1.google.com/vt/lyrs=h&x={$x}&y={$y}&z={$z} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Google_Roadmap_Alt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Roadmap Alt 4 | 0 5 | 20 6 | jpg 7 | None 8 | http://mt1.google.com/vt/lyrs=r&x={$x}&y={$y}&z={$z} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Google_Roadmap_No_POI.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Roadmap No POI 4 | 0 5 | 20 6 | jpg 7 | None 8 | http://mt1.google.com/vt/lyrs=m&x={$x}&y={$y}&z={$z}&s=Gal&apistyle=s.t%3A2%7Cs.e%3Al%7Cp.v%3Aoff 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Google_Roadmap_Standard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Roadmap Standard 4 | 0 5 | 20 6 | jpg 7 | None 8 | http://mt1.google.com/vt/lyrs=m&x={$x}&y={$y}&z={$z} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Google_Satellite_Only.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Satellite Only 4 | 0 5 | 20 6 | jpg 7 | None 8 | http://mt1.google.com/vt/lyrs=s&x={$x}&y={$y}&z={$z} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Google_Terrain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Terrain 4 | 0 5 | 20 6 | jpg 7 | None 8 | http://mt1.google.com/vt/lyrs=p&x={$x}&y={$y}&z={$z} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Google_Terrain_Shading_(Overlay).xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Google Terrain Shading (Overlay) 4 | 0 5 | 20 6 | jpg 7 | None 8 | http://mt1.google.com/vt/lyrs=t&x={$x}&y={$y}&z={$z} 9 | #000000 10 | -------------------------------------------------------------------------------- /opentakserver/maps/Hike_Bike.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | OSM Hike Bike 4 | 0 5 | 21 6 | png 7 | https://tiles.wmflabs.org/hikebike/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/MML.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MML Topographic Map 4 | jpg 5 | https://tiles.kartat.kapsi.fi/peruskartta/{$z}/{$x}/{$y}.jpg 6 | 2 7 | 19 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/MTB_Map_(Europe).xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | MTB Map (Europe) 4 | 0 5 | 21 6 | png 7 | http://tile.mtbmap.cz/mtbmap_tiles/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/OSMStandard.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | OSM Standard 4 | 0 5 | 18 6 | png 7 | http://tile.openstreetmap.org/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/OSM_Cycle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Cycle OSM 4 | 0 5 | 21 6 | png 7 | https://a.tile-cyclosm.openstreetmap.fr/cyclosm/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/OSM_Michelin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OSM Michelin 5 | 0 6 | 19 7 | https://map{$serverpart}.viamichelin.com/map/mapdirect?map=viamichelin&z={$z}&x={$x}&y={$y}&format=png&version=201901161110&layer=background&locale=default&cs=1&protocol=https 8 | 1 2 3 4 9 | -------------------------------------------------------------------------------- /opentakserver/maps/OSM_No_Labels.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | OSM No Labels 4 | 0 5 | 21 6 | png 7 | https://tiles.wmflabs.org/osm-no-labels/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/Stamen_Terrain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Stamen Terrain 4 | 0 5 | 18 6 | png 7 | http://tile.stamen.com/terrain/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/Stamen_Toner.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Stamen Toner 4 | 0 5 | 18 6 | png 7 | http://tile.stamen.com/toner/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/Stamen_Watercolor.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Stamen Watercolor 4 | 0 5 | 18 6 | png 7 | http://tile.stamen.com/watercolor/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/USA_Topo_Maps.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | USA Topo Maps 4 | 0 5 | 15 6 | png 7 | None 8 | https://server.arcgisonline.com/ArcGIS/rest/services/USA_Topo_Maps/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | 11 | -------------------------------------------------------------------------------- /opentakserver/maps/USDA_FSTopo_(Overlay).xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | USDA FSTopo (Overlay) 4 | 0 5 | 17 6 | png 7 | None 8 | https://apps.fs.usda.gov/arcx/rest/services/EDW/EDW_FSTopo_01/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | 11 | -------------------------------------------------------------------------------- /opentakserver/maps/USGSBasemap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | USGS 4 | 0 5 | 15 6 | png 7 | None 8 | https://basemap.nationalmap.gov/ArcGIS/rest/services/USGSTopo/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | 11 | -------------------------------------------------------------------------------- /opentakserver/maps/USGSImageryOnly.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | USGSImageryOnly 4 | 0 5 | 15 6 | png 7 | None 8 | https://basemap.nationalmap.gov/ArcGIS/rest/services/USGSImageryOnly/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | 11 | -------------------------------------------------------------------------------- /opentakserver/maps/USGSImageryTopo.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | USGSImageryTopo 4 | 0 5 | 15 6 | png 7 | None 8 | https://basemap.nationalmap.gov/ArcGIS/rest/services/USGSImageryTopo/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | 11 | -------------------------------------------------------------------------------- /opentakserver/maps/USGSShadedRelief.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | USGSShadedRelief 4 | 0 5 | 15 6 | png 7 | None 8 | https://basemap.nationalmap.gov/arcgis/rest/services/USGSShadedReliefOnly/MapServer/tile/{$z}/{$y}/{$x} 9 | #000000 10 | 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/WMFLabs_Hillshading_(Overlay).xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | WMFLabs Hillshading (Overlay) 4 | 0 5 | 16 6 | png 7 | https://a.tiles.wmflabs.org/hillshading/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/Waymarkedtrails_Cycle_Routes_(Overlay).xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Waymarkedtrails Cycle Routes (Overlay) 4 | 0 5 | 18 6 | png 7 | https://tile.waymarkedtrails.org/cycling/{$z}/{$x}/{$y}.png 8 | None 9 | #000000 10 | false 11 | 12 | -------------------------------------------------------------------------------- /opentakserver/maps/opentopomap.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Open Topo Map 4 | 1 5 | 17 6 | PNG 7 | IfNoneMatch 8 | a b c 9 | https://{$serverpart}.tile.opentopomap.org/{$z}/{$x}/{$y}.png 10 | -------------------------------------------------------------------------------- /opentakserver/migrations/README: -------------------------------------------------------------------------------- 1 | Single-database configuration for Flask. 2 | -------------------------------------------------------------------------------- /opentakserver/migrations/alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # template used to generate migration files 5 | # file_template = %%(rev)s_%%(slug)s 6 | 7 | # set to 'true' to run the environment during 8 | # the 'revision' command, regardless of autogenerate 9 | # revision_environment = false 10 | 11 | 12 | # Logging configuration 13 | [loggers] 14 | keys = root,sqlalchemy,alembic,flask_migrate 15 | 16 | [handlers] 17 | keys = console 18 | 19 | [formatters] 20 | keys = generic 21 | 22 | [logger_root] 23 | level = WARN 24 | handlers = console 25 | qualname = 26 | 27 | [logger_sqlalchemy] 28 | level = WARN 29 | handlers = 30 | qualname = sqlalchemy.engine 31 | 32 | [logger_alembic] 33 | level = INFO 34 | handlers = 35 | qualname = alembic 36 | 37 | [logger_flask_migrate] 38 | level = INFO 39 | handlers = 40 | qualname = flask_migrate 41 | 42 | [handler_console] 43 | class = StreamHandler 44 | args = (sys.stderr,) 45 | level = NOTSET 46 | formatter = generic 47 | 48 | [formatter_generic] 49 | format = %(levelname)-5.5s [%(name)s] %(message)s 50 | datefmt = %H:%M:%S 51 | -------------------------------------------------------------------------------- /opentakserver/migrations/env.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from logging.config import fileConfig 3 | 4 | from flask import current_app 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | fileConfig(config.config_file_name) 15 | logger = logging.getLogger('alembic.env') 16 | 17 | 18 | def get_engine(): 19 | try: 20 | # this works with Flask-SQLAlchemy<3 and Alchemical 21 | return current_app.extensions['migrate'].db.get_engine() 22 | except (TypeError, AttributeError): 23 | # this works with Flask-SQLAlchemy>=3 24 | return current_app.extensions['migrate'].db.engine 25 | 26 | 27 | def get_engine_url(): 28 | try: 29 | return get_engine().url.render_as_string(hide_password=False).replace( 30 | '%', '%%') 31 | except AttributeError: 32 | return str(get_engine().url).replace('%', '%%') 33 | 34 | 35 | # add your model's MetaData object here 36 | # for 'autogenerate' support 37 | # from myapp import mymodel 38 | # target_metadata = mymodel.Base.metadata 39 | config.set_main_option('sqlalchemy.url', get_engine_url()) 40 | target_db = current_app.extensions['migrate'].db 41 | 42 | # other values from the config, defined by the needs of env.py, 43 | # can be acquired: 44 | # my_important_option = config.get_main_option("my_important_option") 45 | # ... etc. 46 | 47 | 48 | def get_metadata(): 49 | if hasattr(target_db, 'metadatas'): 50 | return target_db.metadatas[None] 51 | return target_db.metadata 52 | 53 | 54 | def run_migrations_offline(): 55 | """Run migrations in 'offline' mode. 56 | 57 | This configures the context with just a URL 58 | and not an Engine, though an Engine is acceptable 59 | here as well. By skipping the Engine creation 60 | we don't even need a DBAPI to be available. 61 | 62 | Calls to context.execute() here emit the given string to the 63 | script output. 64 | 65 | """ 66 | url = config.get_main_option("sqlalchemy.url") 67 | context.configure( 68 | url=url, target_metadata=get_metadata(), literal_binds=True 69 | ) 70 | 71 | with context.begin_transaction(): 72 | context.run_migrations() 73 | 74 | 75 | def run_migrations_online(): 76 | """Run migrations in 'online' mode. 77 | 78 | In this scenario we need to create an Engine 79 | and associate a connection with the context. 80 | 81 | """ 82 | 83 | # this callback is used to prevent an auto-migration from being generated 84 | # when there are no changes to the schema 85 | # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html 86 | def process_revision_directives(context, revision, directives): 87 | if getattr(config.cmd_opts, 'autogenerate', False): 88 | script = directives[0] 89 | if script.upgrade_ops.is_empty(): 90 | directives[:] = [] 91 | logger.info('No changes in schema detected.') 92 | 93 | conf_args = current_app.extensions['migrate'].configure_args 94 | if conf_args.get("process_revision_directives") is None: 95 | conf_args["process_revision_directives"] = process_revision_directives 96 | 97 | connectable = get_engine() 98 | 99 | with connectable.connect() as connection: 100 | context.configure( 101 | connection=connection, 102 | target_metadata=get_metadata(), 103 | **conf_args 104 | ) 105 | 106 | with context.begin_transaction(): 107 | context.run_migrations() 108 | 109 | 110 | if context.is_offline_mode(): 111 | run_migrations_offline() 112 | else: 113 | run_migrations_online() 114 | -------------------------------------------------------------------------------- /opentakserver/migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade(): 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade(): 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/00546817d518_added_uid_column_to_cot_table.py: -------------------------------------------------------------------------------- 1 | """Added uid column to cot table 2 | 3 | Revision ID: 00546817d518 4 | Revises: 807c3ca8e7d0 5 | Create Date: 2024-10-11 20:12:20.866452 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '00546817d518' 14 | down_revision = '807c3ca8e7d0' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('cot', schema=None) as batch_op: 22 | batch_op.add_column(sa.Column('uid', sa.String(length=255), nullable=True)) 23 | 24 | # ### end Alembic commands ### 25 | 26 | 27 | def downgrade(): 28 | # ### commands auto generated by Alembic - please adjust! ### 29 | with op.batch_alter_table('cot', schema=None) as batch_op: 30 | batch_op.drop_column('uid') 31 | 32 | # ### end Alembic commands ### 33 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/1041fae84708_added_eud_stats_table.py: -------------------------------------------------------------------------------- 1 | """Added eud_stats table 2 | 3 | Revision ID: 1041fae84708 4 | Revises: 21fb5a21f356 5 | Create Date: 2024-12-17 04:37:54.374451 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '1041fae84708' 14 | down_revision = '21fb5a21f356' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('eud_stats', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('timestamp', sa.DateTime(), nullable=True), 24 | sa.Column('eud_uid', sa.String(length=255), nullable=False), 25 | sa.Column('heap_free_size', sa.Integer(), nullable=True), 26 | sa.Column('app_framerate', sa.Integer(), nullable=True), 27 | sa.Column('storage_total', sa.Integer(), nullable=True), 28 | sa.Column('heap_current_size', sa.Integer(), nullable=True), 29 | sa.Column('battery', sa.Integer(), nullable=True), 30 | sa.Column('battery_temp', sa.Integer(), nullable=True), 31 | sa.Column('deviceDataRx', sa.Integer(), nullable=True), 32 | sa.Column('heap_max_size', sa.Integer(), nullable=True), 33 | sa.Column('storage_available', sa.Integer(), nullable=True), 34 | sa.Column('deviceDataTx', sa.Integer(), nullable=True), 35 | sa.Column('ip_address', sa.String(length=255), nullable=True), 36 | sa.Column('battery_status', sa.String(length=255), nullable=True), 37 | sa.ForeignKeyConstraint(['eud_uid'], ['euds.uid'], ondelete='CASCADE'), 38 | sa.PrimaryKeyConstraint('id') 39 | ) 40 | 41 | def downgrade(): 42 | op.drop_table('eud_stats') 43 | # ### end Alembic commands ### 44 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/14e268184cc5_added_mission_logs_table.py: -------------------------------------------------------------------------------- 1 | """Added mission_logs table 2 | 3 | Revision ID: 14e268184cc5 4 | Revises: d4cc3d4afdb9 5 | Create Date: 2024-10-15 20:28:12.426250 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '14e268184cc5' 14 | down_revision = 'd4cc3d4afdb9' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('mission_logs', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('content', sa.String(length=255), nullable=False), 24 | sa.Column('creator_uid', sa.String(length=255), nullable=False), 25 | sa.Column('entry_uid', sa.String(length=255), nullable=False), 26 | sa.Column('mission_name', sa.String(length=255), nullable=False), 27 | sa.Column('server_time', sa.DateTime(), nullable=False), 28 | sa.Column('dtg', sa.DateTime(), nullable=False), 29 | sa.Column('created', sa.DateTime(), nullable=False), 30 | sa.Column('content_hash', sa.String(length=255), nullable=True), 31 | sa.Column('keywords', sa.JSON(), nullable=False), 32 | sa.ForeignKeyConstraint(['mission_name'], ['missions.name'], name="mission_logs"), 33 | sa.PrimaryKeyConstraint('id') 34 | ) 35 | with op.batch_alter_table('mission_invitations', schema=None) as batch_op: 36 | batch_op.create_foreign_key('euds', 'euds', ['client_uid'], ['uid']) 37 | 38 | # ### end Alembic commands ### 39 | 40 | 41 | def downgrade(): 42 | # ### commands auto generated by Alembic - please adjust! ### 43 | with op.batch_alter_table('mission_invitations', schema=None) as batch_op: 44 | batch_op.drop_constraint('euds', type_='foreignkey') 45 | 46 | op.drop_table('mission_logs') 47 | # ### end Alembic commands ### 48 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/21fb5a21f356_fixed_mission_changes_and_mission_uids_.py: -------------------------------------------------------------------------------- 1 | """Fixed mission_changes and mission_uids relationship 2 | 3 | Revision ID: 21fb5a21f356 4 | Revises: 795ebb9262d8 5 | Create Date: 2024-11-08 14:09:41.129561 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '21fb5a21f356' 14 | down_revision = '795ebb9262d8' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('mission_changes', schema=None) as batch_op: 22 | batch_op.add_column(sa.Column('mission_uid', sa.String(length=255), nullable=True)) 23 | batch_op.create_foreign_key('mission_change_uid', 'mission_uids', ['mission_uid'], ['uid'], ondelete='CASCADE') 24 | 25 | with op.batch_alter_table('mission_uids', schema=None) as batch_op: 26 | batch_op.drop_constraint('mission_change', type_='foreignkey') 27 | batch_op.drop_column('mission_change_id') 28 | 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | with op.batch_alter_table('mission_uids', schema=None) as batch_op: 35 | batch_op.add_column(sa.Column('mission_change_id', sa.INTEGER(), nullable=True)) 36 | batch_op.create_foreign_key('mission_change', 'mission_changes', ['mission_change_id'], ['id']) 37 | 38 | with op.batch_alter_table('mission_changes', schema=None) as batch_op: 39 | batch_op.drop_constraint('mission_change_uid', type_='foreignkey') 40 | batch_op.drop_column('mission_uid') 41 | 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/298430e7849d_fixed_video_streams_table.py: -------------------------------------------------------------------------------- 1 | """Fixed video_streams table 2 | 3 | Revision ID: 298430e7849d 4 | Revises: f107b45529ba 5 | Create Date: 2024-10-28 19:14:27.017319 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '298430e7849d' 14 | down_revision = 'f107b45529ba' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table('video_streams', schema=None) as batch_op: 23 | batch_op.alter_column('xml', 24 | existing_type=sa.VARCHAR(length=255), 25 | type_=sa.TEXT(), 26 | existing_nullable=True) 27 | batch_op.alter_column('mediamtx_settings', 28 | existing_type=sa.VARCHAR(length=255), 29 | type_=sa.TEXT(), 30 | existing_nullable=False) 31 | 32 | # ### end Alembic commands ### 33 | 34 | 35 | def downgrade(): 36 | # ### commands auto generated by Alembic - please adjust! ### 37 | with op.batch_alter_table('video_streams', schema=None) as batch_op: 38 | batch_op.alter_column('mediamtx_settings', 39 | existing_type=sa.TEXT(), 40 | type_=sa.VARCHAR(length=255), 41 | existing_nullable=False) 42 | batch_op.alter_column('xml', 43 | existing_type=sa.TEXT(), 44 | type_=sa.VARCHAR(length=255), 45 | existing_nullable=True) 46 | 47 | # ### end Alembic commands ### 48 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/34d5df698b81_added_publish_time_to_packages_table.py: -------------------------------------------------------------------------------- 1 | """Added publish_time to packages table 2 | 3 | Revision ID: 34d5df698b81 4 | Revises: 34dc96ee805b 5 | Create Date: 2024-09-12 17:47:08.855213 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '34d5df698b81' 14 | down_revision = '34dc96ee805b' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('device_profiles', schema=None) as batch_op: 22 | batch_op.alter_column('preference_key', 23 | existing_type=sa.String(length=255), 24 | nullable=False) 25 | 26 | with op.batch_alter_table('packages', schema=None) as batch_op: 27 | batch_op.add_column(sa.Column('publish_time', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False)) 28 | 29 | # ### end Alembic commands ### 30 | 31 | 32 | def downgrade(): 33 | # ### commands auto generated by Alembic - please adjust! ### 34 | with op.batch_alter_table('packages', schema=None) as batch_op: 35 | batch_op.drop_column('publish_time') 36 | 37 | with op.batch_alter_table('device_profiles', schema=None) as batch_op: 38 | batch_op.alter_column('preference_key', 39 | existing_type=sa.String(length=255), 40 | nullable=True) 41 | 42 | # ### end Alembic commands ### 43 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/34dc96ee805b_changed_device_profile_table.py: -------------------------------------------------------------------------------- 1 | """Changed device_profile table 2 | 3 | Revision ID: 34dc96ee805b 4 | Revises: 414b87c32cc2 5 | Create Date: 2024-09-12 04:42:15.909042 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '34dc96ee805b' 14 | down_revision = '414b87c32cc2' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('device_profiles', schema=None) as batch_op: 22 | batch_op.add_column(sa.Column('id', sa.Integer(), nullable=False)) 23 | batch_op.alter_column('preference_key', 24 | existing_type=sa.String(length=255), 25 | nullable=False) 26 | batch_op.alter_column('preference_value', 27 | existing_type=sa.String(length=255), 28 | nullable=True) 29 | batch_op.alter_column('value_class', 30 | existing_type=sa.String(length=255), 31 | nullable=True) 32 | 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | with op.batch_alter_table('device_profiles', schema=None) as batch_op: 39 | batch_op.alter_column('value_class', 40 | existing_type=sa.String(length=255), 41 | nullable=False) 42 | batch_op.alter_column('preference_value', 43 | existing_type=sa.String(length=255), 44 | nullable=False) 45 | batch_op.alter_column('preference_key', 46 | existing_type=sa.String(length=255), 47 | nullable=False) 48 | batch_op.drop_column('id') 49 | 50 | # ### end Alembic commands ### 51 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/414b87c32cc2_added_device_profile_and_package_tables.py: -------------------------------------------------------------------------------- 1 | """Added device_profile and package tables 2 | 3 | Revision ID: 414b87c32cc2 4 | Revises: d91957bb59a0 5 | Create Date: 2024-09-05 19:17:20.979466 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '414b87c32cc2' 13 | down_revision = 'd91957bb59a0' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_table('device_profiles', 21 | sa.Column('preference_key', sa.String(length=255), nullable=False), 22 | sa.Column('preference_value', sa.String(length=255), nullable=False), 23 | sa.Column('value_class', sa.String(length=255), nullable=False), 24 | sa.Column('enrollment', sa.Boolean(), nullable=True), 25 | sa.Column('connection', sa.Boolean(), nullable=True), 26 | sa.Column('tool', sa.String(length=255), nullable=True), 27 | sa.Column('active', sa.Boolean(), nullable=False), 28 | sa.Column('publish_time', sa.DateTime(), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), 29 | sa.PrimaryKeyConstraint('preference_key') 30 | ) 31 | op.create_table('packages', 32 | sa.Column('id', sa.Integer(), nullable=False), 33 | sa.Column('platform', sa.String(length=255), nullable=False), 34 | sa.Column('plugin_type', sa.String(length=255), nullable=False), 35 | sa.Column('package_name', sa.String(length=255), nullable=False), 36 | sa.Column('name', sa.String(length=255), nullable=False), 37 | sa.Column('file_name', sa.String(length=255), nullable=False), 38 | sa.Column('version', sa.String(length=255), nullable=False), 39 | sa.Column('revision_code', sa.Integer(), nullable=True), 40 | sa.Column('description', sa.Integer(), nullable=True), 41 | sa.Column('apk_hash', sa.Integer(), nullable=True), 42 | sa.Column('os_requirement', sa.Integer(), nullable=True), 43 | sa.Column('tak_prereq', sa.Integer(), nullable=True), 44 | sa.Column('file_size', sa.Integer(), nullable=False), 45 | sa.Column('icon', sa.LargeBinary(), nullable=True), 46 | sa.Column('icon_filename', sa.String(length=255), nullable=True), 47 | sa.Column('install_on_enrollment', sa.Boolean(), nullable=True), 48 | sa.Column('install_on_connection', sa.Boolean(), nullable=True), 49 | sa.PrimaryKeyConstraint('id'), 50 | sa.UniqueConstraint('package_name') 51 | ) 52 | with op.batch_alter_table('data_packages', schema=None) as batch_op: 53 | batch_op.add_column(sa.Column('install_on_enrollment', sa.Boolean(), nullable=True)) 54 | batch_op.add_column(sa.Column('install_on_connection', sa.Boolean(), nullable=True)) 55 | 56 | # ### end Alembic commands ### 57 | 58 | 59 | def downgrade(): 60 | # ### commands auto generated by Alembic - please adjust! ### 61 | with op.batch_alter_table('data_packages', schema=None) as batch_op: 62 | batch_op.drop_column('install_on_connection') 63 | batch_op.drop_column('install_on_enrollment') 64 | 65 | op.drop_table('packages') 66 | op.drop_table('device_profiles') 67 | # ### end Alembic commands ### 68 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/4f0173cdb93b_added_mission_change_id_column_to_.py: -------------------------------------------------------------------------------- 1 | """Added mission_change_id column to mission_uids table 2 | 3 | Revision ID: 4f0173cdb93b 4 | Revises: 00546817d518 5 | Create Date: 2024-10-11 20:52:40.425560 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '4f0173cdb93b' 14 | down_revision = '00546817d518' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('mission_uids', schema=None) as batch_op: 22 | batch_op.add_column(sa.Column('mission_change_id', sa.Integer(), nullable=True)) 23 | batch_op.create_foreign_key("mission_change", 'mission_changes', ['mission_change_id'], ['id']) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table('mission_uids', schema=None) as batch_op: 31 | batch_op.drop_constraint(None, type_='foreignkey') 32 | batch_op.drop_column('mission_change_id') 33 | 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/6af2256c568d_set_install_on_enrollment_and_.py: -------------------------------------------------------------------------------- 1 | """Set install on enrollment and connection to nullable 2 | 3 | Revision ID: 6af2256c568d 4 | Revises: 34d5df698b81 5 | Create Date: 2024-09-20 20:17:28.926142 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '6af2256c568d' 14 | down_revision = '34d5df698b81' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('data_packages', schema=None) as batch_op: 22 | batch_op.alter_column('install_on_enrollment', 23 | existing_type=sa.BOOLEAN(), 24 | nullable=True) 25 | batch_op.alter_column('install_on_connection', 26 | existing_type=sa.BOOLEAN(), 27 | nullable=True) 28 | 29 | with op.batch_alter_table('device_profiles', schema=None) as batch_op: 30 | batch_op.alter_column('enrollment', 31 | existing_type=sa.BOOLEAN(), 32 | nullable=True) 33 | batch_op.alter_column('connection', 34 | existing_type=sa.BOOLEAN(), 35 | nullable=True) 36 | batch_op.drop_column('id') 37 | 38 | with op.batch_alter_table('packages', schema=None) as batch_op: 39 | batch_op.alter_column('install_on_enrollment', 40 | existing_type=sa.BOOLEAN(), 41 | nullable=True) 42 | batch_op.alter_column('install_on_connection', 43 | existing_type=sa.BOOLEAN(), 44 | nullable=True) 45 | 46 | # ### end Alembic commands ### 47 | 48 | 49 | def downgrade(): 50 | # ### commands auto generated by Alembic - please adjust! ### 51 | with op.batch_alter_table('packages', schema=None) as batch_op: 52 | batch_op.alter_column('install_on_connection', 53 | existing_type=sa.BOOLEAN(), 54 | nullable=False) 55 | batch_op.alter_column('install_on_enrollment', 56 | existing_type=sa.BOOLEAN(), 57 | nullable=False) 58 | 59 | with op.batch_alter_table('device_profiles', schema=None) as batch_op: 60 | batch_op.add_column(sa.Column('id', sa.INTEGER(), nullable=False)) 61 | batch_op.alter_column('connection', 62 | existing_type=sa.BOOLEAN(), 63 | nullable=False) 64 | batch_op.alter_column('enrollment', 65 | existing_type=sa.BOOLEAN(), 66 | nullable=False) 67 | 68 | with op.batch_alter_table('data_packages', schema=None) as batch_op: 69 | batch_op.alter_column('install_on_connection', 70 | existing_type=sa.BOOLEAN(), 71 | nullable=False) 72 | batch_op.alter_column('install_on_enrollment', 73 | existing_type=sa.BOOLEAN(), 74 | nullable=False) 75 | 76 | # ### end Alembic commands ### 77 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/7e9f5d2c193d_changed_phone_number_column_to_.py: -------------------------------------------------------------------------------- 1 | """Changed phone_number column to BigInteger for MySQL compatibility 2 | 3 | Revision ID: 7e9f5d2c193d 4 | Revises: 8787888a028f 5 | Create Date: 2024-10-25 17:37:42.637254 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '7e9f5d2c193d' 13 | down_revision = '8787888a028f' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | with op.batch_alter_table('euds', schema=None) as batch_op: 21 | batch_op.alter_column('callsign', 22 | existing_type=sa.String(length=255), 23 | nullable=True) 24 | batch_op.alter_column('phone_number', 25 | existing_type=sa.INTEGER(), 26 | type_=sa.BigInteger(), 27 | existing_nullable=True) 28 | with op.batch_alter_table('data_packages', schema=None) as batch_op: 29 | batch_op.alter_column('creator_uid', 30 | existing_type=sa.String(length=255), 31 | nullable=True) 32 | 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | with op.batch_alter_table('euds', schema=None) as batch_op: 39 | batch_op.alter_column('phone_number', 40 | existing_type=sa.BigInteger(), 41 | type_=sa.INTEGER(), 42 | existing_nullable=True) 43 | batch_op.alter_column('callsign', 44 | existing_type=sa.String(length=255), 45 | nullable=True) 46 | with op.batch_alter_table('data_packages', schema=None) as batch_op: 47 | batch_op.alter_column('creator_uid', 48 | existing_type=sa.String(length=255), 49 | nullable=False) 50 | 51 | # ### end Alembic commands ### 52 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/807c3ca8e7d0_added_mission_uids_table.py: -------------------------------------------------------------------------------- 1 | """Added mission_uids table 2 | 3 | Revision ID: 807c3ca8e7d0 4 | Revises: bde915ea136e 5 | Create Date: 2024-10-11 16:53:44.954118 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | from sqlalchemy.dialects import sqlite 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '807c3ca8e7d0' 14 | down_revision = 'bde915ea136e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('mission_uids', 22 | sa.Column('uid', sa.String(length=255), nullable=False), 23 | sa.Column('mission_name', sa.String(length=255), nullable=True), 24 | sa.Column('timestamp', sa.DateTime(), nullable=True), 25 | sa.Column('creator_uid', sa.String(length=255), nullable=True), 26 | sa.Column('cot_type', sa.String(length=255), nullable=True), 27 | sa.Column('callsign', sa.String(length=255), nullable=True), 28 | sa.Column('iconset_path', sa.String(length=255), nullable=True), 29 | sa.Column('color', sa.String(length=255), nullable=True), 30 | sa.Column('latitude', sa.Float(), nullable=True), 31 | sa.Column('longitude', sa.Float(), nullable=True), 32 | sa.ForeignKeyConstraint(['mission_name'], ['missions.name'], ), 33 | sa.PrimaryKeyConstraint('uid') 34 | ) 35 | op.drop_table('mission_logs') 36 | # ### end Alembic commands ### 37 | 38 | 39 | def downgrade(): 40 | # ### commands auto generated by Alembic - please adjust! ### 41 | op.create_table('mission_logs', 42 | sa.Column('id', sa.INTEGER(), nullable=False), 43 | sa.Column('content', sa.String(length=255), nullable=False), 44 | sa.Column('creator_uid', sa.String(length=255), nullable=False), 45 | sa.Column('entry_uid', sa.String(length=255), nullable=False), 46 | sa.Column('server_time', sa.DATETIME(), nullable=False), 47 | sa.Column('dtg', sa.DATETIME(), nullable=False), 48 | sa.Column('created', sa.DATETIME(), nullable=False), 49 | sa.Column('keywords', sqlite.JSON(), nullable=False), 50 | sa.Column('mission_name', sa.String(length=255), nullable=True), 51 | sa.Column('content_hash', sa.String(length=255), nullable=True), 52 | sa.PrimaryKeyConstraint('id') 53 | ) 54 | op.drop_table('mission_uids') 55 | # ### end Alembic commands ### 56 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/8787888a028f_fixes_to_support_mariadb.py: -------------------------------------------------------------------------------- 1 | """Fixes to support MariaDB 2 | 3 | Revision ID: 8787888a028f 4 | Revises: f6dfc571d31c 5 | Create Date: 2024-10-25 15:44:27.370608 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = '8787888a028f' 13 | down_revision = 'f6dfc571d31c' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | with op.batch_alter_table('cot', schema=None) as batch_op: 21 | batch_op.alter_column('xml', 22 | existing_type=sa.String(length=255), 23 | type_=sa.TEXT(), 24 | existing_nullable=False) 25 | 26 | with op.batch_alter_table('euds', schema=None) as batch_op: 27 | batch_op.alter_column('meshtastic_id', 28 | existing_type=sa.INTEGER(), 29 | type_=sa.BigInteger(), 30 | existing_nullable=True) 31 | 32 | with op.batch_alter_table('points', schema=None) as batch_op: 33 | batch_op.alter_column('device_uid', 34 | existing_type=sa.String(length=255), 35 | nullable=True) 36 | 37 | with op.batch_alter_table('missions', schema=None) as batch_op: 38 | batch_op.create_foreign_key('mission_creator', 'euds', ['creator_uid'], ['uid']) 39 | 40 | with op.batch_alter_table('groups', schema=None) as batch_op: 41 | batch_op.add_column(sa.Column('eud_uid', sa.String(length=255), nullable=False)) 42 | batch_op.create_foreign_key('eud', 'euds', ['eud_uid'], ['uid']) 43 | 44 | # ### end Alembic commands ### 45 | 46 | 47 | def downgrade(): 48 | # ### commands auto generated by Alembic - please adjust! ### 49 | with op.batch_alter_table('missions', schema=None) as batch_op: 50 | batch_op.drop_constraint('mission_creator', type_='foreignkey') 51 | 52 | with op.batch_alter_table('euds', schema=None) as batch_op: 53 | batch_op.alter_column('meshtastic_id', 54 | existing_type=sa.BigInteger(), 55 | type_=sa.INTEGER(), 56 | existing_nullable=True) 57 | 58 | with op.batch_alter_table('cot', schema=None) as batch_op: 59 | batch_op.alter_column('xml', 60 | existing_type=sa.TEXT(), 61 | type_=sa.String(length=255), 62 | existing_nullable=False) 63 | 64 | with op.batch_alter_table('groups', schema=None) as batch_op: 65 | batch_op.drop_constraint('eud', type_='foreignkey') 66 | batch_op.drop_column('eud_uid') 67 | 68 | with op.batch_alter_table('points', schema=None) as batch_op: 69 | batch_op.alter_column('device_uid', 70 | existing_type=sa.String(length=255), 71 | nullable=False) 72 | 73 | # ### end Alembic commands ### 74 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/bde915ea136e_added_mission_logs_table.py: -------------------------------------------------------------------------------- 1 | """Added mission_logs table 2 | 3 | Revision ID: bde915ea136e 4 | Revises: d31bff4a15c7 5 | Create Date: 2024-10-10 18:13:29.597437 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'bde915ea136e' 14 | down_revision = 'd31bff4a15c7' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('mission_logs', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('content', sa.String(length=255), nullable=False), 24 | sa.Column('creator_uid', sa.String(length=255), nullable=False), 25 | sa.Column('entry_uid', sa.String(length=255), nullable=False), 26 | sa.Column('mission_name', sa.String(length=255), nullable=False), 27 | sa.Column('server_time', sa.DateTime(), nullable=False), 28 | sa.Column('dtg', sa.DateTime(), nullable=False), 29 | sa.Column('created', sa.DateTime(), nullable=False), 30 | sa.Column('content_hash', sa.String(length=255), nullable=True), 31 | sa.Column('keywords', sa.JSON(), nullable=True), 32 | sa.PrimaryKeyConstraint('id') 33 | ) 34 | with op.batch_alter_table('mission_content', schema=None) as batch_op: 35 | batch_op.alter_column('hash', 36 | existing_type=sa.String(length=255), 37 | nullable=False) 38 | #batch_op.drop_index('hash_index') 39 | batch_op.create_unique_constraint("hash_index", ['hash']) 40 | 41 | # ### end Alembic commands ### 42 | 43 | 44 | def downgrade(): 45 | # ### commands auto generated by Alembic - please adjust! ### 46 | with op.batch_alter_table('mission_content', schema=None) as batch_op: 47 | batch_op.drop_constraint(None, type_='unique') 48 | batch_op.create_index('hash_index', ['hash'], unique=1) 49 | batch_op.alter_column('hash', 50 | existing_type=sa.String(length=255), 51 | nullable=True) 52 | 53 | op.drop_table('mission_logs') 54 | # ### end Alembic commands ### 55 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/d1f5df78eace_more_data_sync_stuff.py: -------------------------------------------------------------------------------- 1 | """More data sync stuff 2 | 3 | Revision ID: d1f5df78eace 4 | Revises: 4f0173cdb93b 5 | Create Date: 2024-10-15 03:02:43.059600 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd1f5df78eace' 14 | down_revision = '4f0173cdb93b' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('mission_changes', schema=None) as batch_op: 22 | batch_op.add_column(sa.Column('content_uid', sa.String(length=255), nullable=True)) 23 | batch_op.create_foreign_key("content_resource", 'mission_content', ['content_uid'], ['uid']) 24 | 25 | with op.batch_alter_table('mission_content', schema=None) as batch_op: 26 | batch_op.add_column(sa.Column('filename', sa.String(length=255), nullable=True)) 27 | batch_op.drop_column('name') 28 | 29 | with op.batch_alter_table('mission_invitations', schema=None) as batch_op: 30 | batch_op.add_column(sa.Column('callsign', sa.String(length=255), nullable=True)) 31 | batch_op.add_column(sa.Column('username', sa.String(length=255), nullable=True)) 32 | batch_op.add_column(sa.Column('group_name', sa.String(length=255), nullable=True)) 33 | batch_op.add_column(sa.Column('team_name', sa.String(length=255), nullable=True)) 34 | batch_op.alter_column('client_uid', 35 | existing_type=sa.String(length=255), 36 | nullable=True) 37 | batch_op.create_foreign_key('mission_invitation_user', 'user', ['username'], ['username']) 38 | # batch_op.create_foreign_key('groups', 'groups', ['group_name'], ['group_name']) 39 | batch_op.create_foreign_key('mission_invitation_callsign', 'euds', ['callsign'], ['callsign']) 40 | batch_op.create_foreign_key('mission_invitation_team', 'teams', ['team_name'], ['name']) 41 | batch_op.create_foreign_key('mission_invitation_eud', 'euds', ['client_uid'], ['uid']) 42 | 43 | with op.batch_alter_table('missions', schema=None) as batch_op: 44 | batch_op.add_column(sa.Column('group', sa.String(length=255), nullable=True)) 45 | # batch_op.create_foreign_key('groups', 'groups', ['group'], ['group_name']) 46 | batch_op.drop_column('group_name') 47 | 48 | # ### end Alembic commands ### 49 | 50 | 51 | def downgrade(): 52 | # ### commands auto generated by Alembic - please adjust! ### 53 | with op.batch_alter_table('missions', schema=None) as batch_op: 54 | batch_op.add_column(sa.Column('group_name', sa.String(length=255), nullable=True)) 55 | batch_op.drop_constraint(None, type_='foreignkey') 56 | batch_op.drop_column('group') 57 | 58 | with op.batch_alter_table('mission_invitations', schema=None) as batch_op: 59 | batch_op.drop_constraint('user', type_='foreignkey') 60 | batch_op.drop_constraint('eud_callsign', type_='foreignkey') 61 | batch_op.drop_constraint('eud_uid', type_='foreignkey') 62 | batch_op.drop_constraint('teams', type_='foreignkey') 63 | batch_op.alter_column('client_uid', 64 | existing_type=sa.String(length=255), 65 | nullable=False) 66 | batch_op.drop_column('team_name') 67 | batch_op.drop_column('group_name') 68 | batch_op.drop_column('username') 69 | batch_op.drop_column('callsign') 70 | 71 | with op.batch_alter_table('mission_content', schema=None) as batch_op: 72 | batch_op.add_column(sa.Column('name', sa.String(length=255), nullable=True)) 73 | batch_op.drop_column('filename') 74 | 75 | with op.batch_alter_table('mission_changes', schema=None) as batch_op: 76 | batch_op.drop_constraint(None, type_='foreignkey') 77 | batch_op.drop_column('content_uid') 78 | 79 | # ### end Alembic commands ### 80 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/d31bff4a15c7_added_mission_name_to_cot_table.py: -------------------------------------------------------------------------------- 1 | """Added mission_name to cot table 2 | 3 | Revision ID: d31bff4a15c7 4 | Revises: 5d06227dea50 5 | Create Date: 2024-10-09 15:40:49.385642 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd31bff4a15c7' 14 | down_revision = '5d06227dea50' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | with op.batch_alter_table('cot', schema=None) as batch_op: 22 | batch_op.add_column(sa.Column('mission_name', sa.String(length=255), nullable=True)) 23 | batch_op.create_foreign_key("mission_name", 'missions', ['mission_name'], ['name']) 24 | 25 | # ### end Alembic commands ### 26 | 27 | 28 | def downgrade(): 29 | # ### commands auto generated by Alembic - please adjust! ### 30 | with op.batch_alter_table('cot', schema=None) as batch_op: 31 | batch_op.drop_constraint("mission_name", type_='foreignkey') 32 | batch_op.drop_column('mission_name') 33 | 34 | # ### end Alembic commands ### 35 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/d4cc3d4afdb9_added_groups_table.py: -------------------------------------------------------------------------------- 1 | """Added groups table 2 | 3 | Revision ID: d4cc3d4afdb9 4 | Revises: d1f5df78eace 5 | Create Date: 2024-10-15 19:03:59.996651 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'd4cc3d4afdb9' 14 | down_revision = 'd1f5df78eace' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | op.create_table('groups', 22 | sa.Column('id', sa.Integer(), nullable=False), 23 | sa.Column('group_name', sa.String(length=255), nullable=False), 24 | sa.Column('direction', sa.String(length=255), nullable=False), 25 | sa.Column('created', sa.DateTime(), nullable=False), 26 | sa.Column('group_type', sa.String(length=255), nullable=False), 27 | sa.Column('bitpos', sa.Integer(), nullable=False), 28 | sa.Column('active', sa.Boolean(), nullable=False), 29 | sa.Column('description', sa.String(length=255), nullable=True), 30 | sa.PrimaryKeyConstraint('id') 31 | ) 32 | 33 | # ### end Alembic commands ### 34 | 35 | 36 | def downgrade(): 37 | # ### commands auto generated by Alembic - please adjust! ### 38 | 39 | op.drop_table('groups') 40 | # ### end Alembic commands ### 41 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/d91957bb59a0_meshtastic_support.py: -------------------------------------------------------------------------------- 1 | """Meshtastic Support 2 | 3 | Revision ID: d91957bb59a0 4 | Revises: 4c7909c34d4e 5 | Create Date: 2024-05-24 04:16:20.765489 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import logging 11 | logger = logging.getLogger('OpenTAKServer') 12 | logger.disabled = False 13 | 14 | 15 | # revision identifiers, used by Alembic. 16 | revision = 'd91957bb59a0' 17 | down_revision = '4c7909c34d4e' 18 | branch_labels = None 19 | depends_on = None 20 | 21 | 22 | def upgrade(): 23 | # ### commands auto generated by Alembic - please adjust! ### 24 | try: 25 | op.create_table('iconsets', 26 | sa.Column('id', sa.INTEGER(), nullable=False), 27 | sa.Column('name', sa.String(length=255), nullable=True), 28 | sa.Column('uid', sa.String(length=255), nullable=True), 29 | sa.Column('version', sa.Integer(), nullable=True), 30 | sa.Column('defaultFriendly', sa.String(length=255), nullable=True), 31 | sa.Column('defaultHostile', sa.String(length=255), nullable=True), 32 | sa.Column('defaultNeutral', sa.String(length=255), nullable=True), 33 | sa.Column('defaultUnknown', sa.String(length=255), nullable=True), 34 | sa.Column('selectedGroup', sa.String(length=255), nullable=True), 35 | sa.PrimaryKeyConstraint('id') 36 | ) 37 | except sa.exc.OperationalError: 38 | logger.warning('iconsets table already exists, skipping') 39 | 40 | try: 41 | op.create_table('meshtastic_channels', 42 | sa.Column('id', sa.Integer(), nullable=False), 43 | sa.Column('psk', sa.String(length=255), nullable=True), 44 | sa.Column('name', sa.String(length=255), nullable=True), 45 | sa.Column('uplink_enabled', sa.Boolean(), nullable=False), 46 | sa.Column('downlink_enabled', sa.Boolean(), nullable=False), 47 | sa.Column('position_precision', sa.Integer(), nullable=False), 48 | sa.Column('lora_region', sa.Integer(), nullable=False), 49 | sa.Column('lora_hop_limit', sa.Integer(), nullable=False), 50 | sa.Column('lora_tx_enabled', sa.Boolean(), nullable=False), 51 | sa.Column('lora_tx_power', sa.Integer(), nullable=False), 52 | sa.Column('lora_sx126x_rx_boosted_gain', sa.Boolean(), nullable=False), 53 | sa.Column('modem_preset', sa.Integer(), nullable=False), 54 | sa.Column('url', sa.String(length=255), nullable=False), 55 | sa.PrimaryKeyConstraint('id'), 56 | sa.UniqueConstraint('url') 57 | ) 58 | 59 | with op.batch_alter_table('euds', schema=None) as batch_op: 60 | batch_op.add_column(sa.Column('meshtastic_id', sa.Integer(), nullable=True)) 61 | batch_op.add_column(sa.Column('meshtastic_macaddr', sa.String(length=255), nullable=True)) 62 | except sa.exc.OperationalError: 63 | logger.warning('Meshtastic table already exists, skipping') 64 | 65 | # ### end Alembic commands ### 66 | 67 | 68 | def downgrade(): 69 | # ### commands auto generated by Alembic - please adjust! ### 70 | 71 | with op.batch_alter_table('euds', schema=None) as batch_op: 72 | batch_op.drop_column('meshtastic_macaddr') 73 | batch_op.drop_column('meshtastic_id') 74 | 75 | op.drop_table('meshtastic_channels') 76 | op.drop_table('iconsets') 77 | # ### end Alembic commands ### 78 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/f107b45529ba_fixes_for_the_packages_table_added_.py: -------------------------------------------------------------------------------- 1 | """Fixes for the packages table, added group_eud table 2 | 3 | Revision ID: f107b45529ba 4 | Revises: 7e9f5d2c193d 5 | Create Date: 2024-10-28 14:36:54.189219 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | # revision identifiers, used by Alembic. 12 | revision = 'f107b45529ba' 13 | down_revision = '7e9f5d2c193d' 14 | branch_labels = None 15 | depends_on = None 16 | 17 | 18 | def upgrade(): 19 | # ### commands auto generated by Alembic - please adjust! ### 20 | op.create_table('groups_euds', 21 | sa.Column('id', sa.Integer(), nullable=False), 22 | sa.Column('group_id', sa.Integer(), nullable=False), 23 | sa.Column('direction', sa.String(length=255), nullable=False), 24 | sa.Column('active', sa.Boolean(), nullable=False), 25 | sa.Column('description', sa.String(length=255), nullable=True), 26 | sa.Column('eud_uid', sa.String(length=255), nullable=False), 27 | sa.ForeignKeyConstraint(['eud_uid'], ['euds.uid'], name="group_eud"), 28 | sa.ForeignKeyConstraint(['group_id'], ['groups.id'], name="group"), 29 | sa.PrimaryKeyConstraint('id') 30 | ) 31 | 32 | with op.batch_alter_table('groups', schema=None) as batch_op: 33 | batch_op.drop_constraint('eud', type_='foreignkey') 34 | batch_op.drop_column('description') 35 | batch_op.drop_column('active') 36 | batch_op.drop_column('direction') 37 | batch_op.drop_column('eud_uid') 38 | 39 | with op.batch_alter_table('packages', schema=None) as batch_op: 40 | batch_op.alter_column('description', 41 | existing_type=sa.Integer(), 42 | type_=sa.String(length=255), 43 | existing_nullable=True) 44 | batch_op.alter_column('apk_hash', 45 | existing_type=sa.Integer(), 46 | type_=sa.String(length=255), 47 | existing_nullable=True) 48 | 49 | # ### end Alembic commands ### 50 | 51 | 52 | def downgrade(): 53 | # ### commands auto generated by Alembic - please adjust! ### 54 | with op.batch_alter_table('packages', schema=None) as batch_op: 55 | batch_op.alter_column('apk_hash', 56 | existing_type=sa.String(length=255), 57 | type_=sa.Integer(), 58 | existing_nullable=True) 59 | batch_op.alter_column('description', 60 | existing_type=sa.String(length=255), 61 | type_=sa.Integer(), 62 | existing_nullable=True) 63 | 64 | with op.batch_alter_table('groups', schema=None) as batch_op: 65 | batch_op.add_column(sa.Column('eud_uid', sa.String(length=255), nullable=False)) 66 | batch_op.add_column(sa.Column('direction', sa.String(length=255), nullable=False)) 67 | batch_op.add_column(sa.Column('active', sa.Boolean(), autoincrement=False, nullable=False)) 68 | batch_op.add_column(sa.Column('description', sa.String(length=255), nullable=True)) 69 | batch_op.create_foreign_key('eud', 'euds', ['eud_uid'], ['uid']) 70 | 71 | op.drop_table('groups_euds') 72 | # ### end Alembic commands ### 73 | -------------------------------------------------------------------------------- /opentakserver/migrations/versions/f6dfc571d31c_added_columns_to_mission_invtations_.py: -------------------------------------------------------------------------------- 1 | """Added columns to mission_invtations table 2 | 3 | Revision ID: f6dfc571d31c 4 | Revises: 14e268184cc5 5 | Create Date: 2024-10-17 18:53:11.141218 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'f6dfc571d31c' 14 | down_revision = '14e268184cc5' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | # ### commands auto generated by Alembic - please adjust! ### 21 | 22 | with op.batch_alter_table('mission_invitations', schema=None) as batch_op: 23 | batch_op.add_column(sa.Column('creator_uid', sa.String(length=255), nullable=True)) 24 | batch_op.add_column(sa.Column('role', sa.String(length=255), nullable=True)) 25 | 26 | # ### end Alembic commands ### 27 | 28 | 29 | def downgrade(): 30 | # ### commands auto generated by Alembic - please adjust! ### 31 | with op.batch_alter_table('mission_invitations', schema=None) as batch_op: 32 | batch_op.drop_column('role') 33 | batch_op.drop_column('creator_uid') 34 | 35 | # ### end Alembic commands ### 36 | -------------------------------------------------------------------------------- /opentakserver/models/APSchedulerJobs.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import String, Float, LargeBinary 5 | from sqlalchemy.orm import Mapped, mapped_column 6 | 7 | 8 | @dataclass 9 | class APSchedulerJobs(db.Model): 10 | __tablename__ = "apscheduler_jobs" 11 | 12 | id: Mapped[str] = mapped_column(String(255), primary_key=True) 13 | next_run_time: Mapped[float] = mapped_column(Float, nullable=True, index=True) 14 | job_state: Mapped[bytes] = mapped_column(LargeBinary, nullable=False) 15 | -------------------------------------------------------------------------------- /opentakserver/models/Alert.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import Integer, String, ForeignKey, DateTime 5 | from sqlalchemy.orm import Mapped, mapped_column, relationship 6 | 7 | from opentakserver.functions import iso8601_string_from_datetime 8 | 9 | 10 | class Alert(db.Model): 11 | __tablename__ = 'alerts' 12 | 13 | # type = ^b-a- (b-a-o-tbl, b-a-o-can) 14 | # how = ^m-g | ^h-e 15 | 16 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 17 | uid: Mapped[str] = mapped_column(String(255)) 18 | sender_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE")) 19 | start_time: Mapped[datetime] = mapped_column(DateTime) 20 | cancel_time: Mapped[datetime] = mapped_column(DateTime, nullable=True) 21 | alert_type: Mapped[str] = mapped_column(String(255)) 22 | point_id: Mapped[int] = mapped_column(Integer, ForeignKey("points.id", ondelete="CASCADE"), nullable=True) 23 | cot_id: Mapped[int] = mapped_column(Integer, ForeignKey("cot.id", ondelete="CASCADE"), nullable=True) 24 | cot = relationship("CoT", back_populates="alert") 25 | point = relationship("Point", back_populates="alert", foreign_keys=[point_id]) 26 | eud = relationship("EUD", back_populates="alert") 27 | 28 | def serialize(self): 29 | return { 30 | 'uid': self.uid, 31 | 'sender_uid': self.sender_uid, 32 | 'start_time': self.start_time, 33 | 'cancel_time': self.cancel_time, 34 | 'alert_type': self.alert_type, 35 | 'point_id': self.point_id, 36 | 'cot_id': self.cot_id 37 | } 38 | 39 | def to_json(self): 40 | return { 41 | 'uid': self.uid, 42 | 'sender_uid': self.sender_uid, 43 | 'start_time': iso8601_string_from_datetime(self.start_time), 44 | 'cancel_time': iso8601_string_from_datetime(self.cancel_time) if self.cancel_time else None, 45 | 'alert_type': self.alert_type, 46 | 'point': self.point.to_json() if self.point else None, 47 | 'callsign': self.eud.callsign if self.eud else None, 48 | } 49 | -------------------------------------------------------------------------------- /opentakserver/models/Base.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy.orm import DeclarativeBase 2 | 3 | 4 | class Base(DeclarativeBase): 5 | pass 6 | -------------------------------------------------------------------------------- /opentakserver/models/Certificate.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | 4 | from opentakserver.extensions import db 5 | from sqlalchemy import Integer, String, ForeignKey, DateTime 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from opentakserver.functions import iso8601_string_from_datetime 9 | 10 | 11 | @dataclass 12 | class Certificate(db.Model): 13 | __tablename__ = 'certificates' 14 | 15 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 16 | common_name: Mapped[str] = mapped_column(String(255)) 17 | eud_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE"), nullable=True) 18 | data_package_id: Mapped[int] = mapped_column(Integer, ForeignKey("data_packages.id"), nullable=True) 19 | callsign: Mapped[str] = mapped_column(String(255), nullable=True) 20 | username: Mapped[str] = mapped_column(String(255), ForeignKey("user.username"), nullable=True) 21 | expiration_date: Mapped[datetime] = mapped_column(DateTime) 22 | server_address: Mapped[str] = mapped_column(String(255)) 23 | server_port: Mapped[int] = mapped_column(Integer) 24 | truststore_filename: Mapped[str] = mapped_column(String(255)) 25 | user_cert_filename: Mapped[str] = mapped_column(String(255)) 26 | csr: Mapped[str] = mapped_column(String(255), nullable=True) 27 | cert_password: Mapped[str] = mapped_column(String(255)) 28 | user = relationship("User", back_populates="certificate", uselist=False) 29 | eud = relationship("EUD", cascade="all, delete", back_populates="certificate", uselist=False) 30 | data_package = relationship("DataPackage", back_populates="certificate", uselist=False) 31 | 32 | def serialize(self): 33 | return { 34 | 'callsign': self.callsign, 35 | 'expiration_date': self.expiration_date, 36 | 'server_address': self.server_address, 37 | 'server_port': self.server_port, 38 | 'truststore_filename': self.truststore_filename, 39 | 'user_cert_filename': self.user_cert_filename, 40 | 'cert_password': self.cert_password, 41 | 'eud_uid': self.eud_uid 42 | } 43 | 44 | def to_json(self): 45 | return { 46 | 'callsign': self.callsign, 47 | 'expiration_date': iso8601_string_from_datetime(self.expiration_date), 48 | 'server_address': self.server_address, 49 | 'server_port': self.server_port, 50 | 'truststore_filename': self.truststore_filename, 51 | 'user_cert_filename': self.user_cert_filename, 52 | 'data_package_filename': self.data_package.filename if self.data_package else None, 53 | 'data_package_hash': self.data_package.hash if self.data_package else None, 54 | 'eud_uid': self.eud_uid 55 | } 56 | -------------------------------------------------------------------------------- /opentakserver/models/Chatrooms.py: -------------------------------------------------------------------------------- 1 | from opentakserver.extensions import db 2 | from sqlalchemy import String, Integer, ForeignKey 3 | from sqlalchemy.orm import Mapped, mapped_column, relationship 4 | 5 | 6 | class Chatroom(db.Model): 7 | __tablename__ = 'chatrooms' 8 | 9 | id: Mapped[str] = mapped_column(String(255), primary_key=True) 10 | name: Mapped[str] = mapped_column(String(255)) 11 | group_owner: Mapped[str] = mapped_column(String(255), nullable=True) 12 | parent: Mapped[str] = mapped_column(String(255), nullable=True) 13 | geochats = relationship("GeoChat", cascade="all, delete-orphan", back_populates="chatroom") 14 | chatroom_uid = relationship("ChatroomsUids", cascade="all, delete-orphan", back_populates="chatroom") 15 | team = relationship("Team", back_populates="chatroom") 16 | 17 | def serialize(self): 18 | return { 19 | 'name': self.name, 20 | 'group_owner': self.group_owner, 21 | 'parent': self.parent, 22 | } 23 | 24 | def to_json(self): 25 | return { 26 | 'name': self.name, 27 | 'group_owner': self.group_owner, 28 | 'parent': self.parent, 29 | 'geochats': [chat.to_json() for chat in self.geochats] if self.geochats else None 30 | } 31 | -------------------------------------------------------------------------------- /opentakserver/models/ChatroomsUids.py: -------------------------------------------------------------------------------- 1 | from opentakserver.extensions import db 2 | from sqlalchemy import String, ForeignKey 3 | from sqlalchemy.orm import Mapped, mapped_column, relationship 4 | 5 | 6 | class ChatroomsUids(db.Model): 7 | __tablename__ = 'chatrooms_uids' 8 | 9 | chatroom_id: Mapped[str] = mapped_column(String(255), ForeignKey("chatrooms.id"), primary_key=True) 10 | uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE"), primary_key=True) 11 | chatroom = relationship("Chatroom", back_populates="chatroom_uid") 12 | eud = relationship("EUD", back_populates="chatroom_uid") 13 | -------------------------------------------------------------------------------- /opentakserver/models/CoT.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import Integer, String, JSON, ForeignKey, DateTime, TEXT 5 | from sqlalchemy.orm import Mapped, mapped_column, relationship 6 | 7 | from opentakserver.functions import iso8601_string_from_datetime 8 | 9 | 10 | class CoT(db.Model): 11 | __tablename__ = "cot" 12 | 13 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 14 | how: Mapped[str] = mapped_column(String(255), nullable=True) 15 | type: Mapped[str] = mapped_column(String(255), nullable=True) 16 | uid: Mapped[str] = mapped_column(String(255), nullable=True) 17 | sender_callsign: Mapped[str] = mapped_column(String(255)) 18 | sender_device_name: Mapped[str] = mapped_column(String(255), nullable=True) 19 | sender_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE"), nullable=True) 20 | recipients: Mapped[JSON] = mapped_column(JSON, nullable=True) 21 | timestamp: Mapped[datetime] = mapped_column(DateTime) 22 | start: Mapped[datetime] = mapped_column(DateTime) 23 | stale: Mapped[datetime] = mapped_column(DateTime) 24 | xml: Mapped[str] = mapped_column(TEXT) 25 | mission_name: Mapped[str] = mapped_column(String(255), ForeignKey("missions.name"), nullable=True) 26 | eud = relationship("EUD", back_populates="cots", uselist=False) 27 | alert = relationship("Alert", cascade="all, delete, delete-orphan", back_populates="cot", uselist=False) 28 | point = relationship("Point", cascade="all, delete, delete-orphan", back_populates="cot", uselist=False) 29 | casevac = relationship("CasEvac", cascade="all, delete, delete-orphan", back_populates="cot", uselist=False) 30 | video = relationship("VideoStream", back_populates="cot", cascade="all, delete, delete-orphan", uselist=False) 31 | geochat = relationship("GeoChat", back_populates="cot", cascade="all, delete, delete-orphan", uselist=False) 32 | marker = relationship("Marker", back_populates="cot", cascade="all, delete, delete-orphan", uselist=False) 33 | rb_line = relationship("RBLine", cascade="all, delete, delete-orphan", back_populates="cot") 34 | mission = relationship("Mission", back_populates="cots") 35 | 36 | def serialize(self): 37 | return { 38 | 'how': self.how, 39 | 'type': self.type, 40 | 'uid': self.uid, 41 | 'sender_callsign': self.sender_callsign, 42 | 'sender_uid': self.sender_uid, 43 | 'recipients': self.recipients, 44 | 'timestamp': self.timestamp, 45 | 'start': self.start, 46 | 'stale': self.stale, 47 | 'xml': self.xml, 48 | } 49 | 50 | def to_json(self): 51 | return { 52 | 'how': self.how, 53 | 'type': self.type, 54 | 'uid': self.uid, 55 | 'sender_callsign': self.sender_callsign, 56 | 'sender_uid': self.sender_uid, 57 | 'recipients': self.recipients, 58 | 'timestamp': self.timestamp, 59 | 'start': iso8601_string_from_datetime(self.start), 60 | 'stale': iso8601_string_from_datetime(self.stale), 61 | 'xml': self.xml, 62 | 'eud': self.eud.to_json() if self.eud else None, 63 | 'alert': self.alert.to_json() if self.alert else None, 64 | 'point': self.point.to_json() if self.point else None, 65 | 'casevac': self.casevac.to_json() if self.casevac else None, 66 | 'video': self.video.to_json() if self.video else None, 67 | 'geochat': self.geochat.to_json() if self.geochat else None, 68 | } 69 | -------------------------------------------------------------------------------- /opentakserver/models/DataPackage.py: -------------------------------------------------------------------------------- 1 | import os 2 | import uuid 3 | from dataclasses import dataclass 4 | from datetime import datetime 5 | 6 | from opentakserver.extensions import db 7 | from sqlalchemy import Integer, String, ForeignKey, DateTime, Boolean 8 | from sqlalchemy.orm import Mapped, mapped_column, relationship 9 | 10 | from opentakserver.functions import iso8601_string_from_datetime 11 | 12 | 13 | @dataclass 14 | class DataPackage(db.Model): 15 | __tablename__ = 'data_packages' 16 | 17 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 18 | filename: Mapped[str] = mapped_column(String(255), unique=True) 19 | hash: Mapped[str] = mapped_column(String(255), unique=True) 20 | creator_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE"), nullable=True) 21 | submission_time: Mapped[datetime] = mapped_column(DateTime) 22 | submission_user: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"), nullable=True) 23 | keywords: Mapped[str] = mapped_column(String(255), nullable=True) 24 | mime_type: Mapped[str] = mapped_column(String(255)) 25 | size: Mapped[int] = mapped_column(Integer) 26 | tool: Mapped[str] = mapped_column(String(255), nullable=True) 27 | expiration: Mapped[str] = mapped_column(String(255), nullable=True) 28 | install_on_enrollment: Mapped[bool] = mapped_column(Boolean, default=False, nullable=True) 29 | install_on_connection: Mapped[bool] = mapped_column(Boolean, default=False, nullable=True) 30 | eud: Mapped["EUD"] = relationship(back_populates="data_packages") 31 | certificate = relationship("Certificate", back_populates="data_package", uselist=False) 32 | user = relationship("User", back_populates="data_packages") 33 | 34 | def serialize(self): 35 | return { 36 | 'filename': self.filename, 37 | 'hash': self.hash, 38 | 'creator_uid': self.creator_uid, 39 | 'submission_time': self.submission_time, 40 | 'submission_user': self.submission_user, 41 | 'keywords': self.keywords, 42 | 'mime_type': self.mime_type, 43 | 'size': self.size, 44 | 'tool': self.tool, 45 | 'expiration': self.expiration, 46 | 'install_on_enrollment': self.install_on_enrollment, 47 | 'install_on_connection': self.install_on_connection 48 | } 49 | 50 | def to_json(self, include_eud=True): 51 | return { 52 | 'filename': self.filename, 53 | 'hash': self.hash, 54 | 'creator_uid': self.creator_uid, 55 | 'submission_time': iso8601_string_from_datetime(self.submission_time), 56 | 'submission_user': self.user.username if self.user else None, 57 | 'keywords': self.keywords, 58 | 'mime_type': self.mime_type, 59 | 'size': self.size, 60 | 'tool': self.tool, 61 | 'expiration': self.expiration, 62 | 'eud': self.eud.to_json(False) if include_eud and self.eud else None, 63 | 'install_on_enrollment': self.install_on_enrollment, 64 | 'install_on_connection': self.install_on_connection 65 | } 66 | -------------------------------------------------------------------------------- /opentakserver/models/DeviceProfiles.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from sqlalchemy import String, Boolean, DateTime, Integer 4 | from sqlalchemy.orm import Mapped, mapped_column 5 | 6 | from opentakserver.extensions import db 7 | from opentakserver.forms.device_profile_form import DeviceProfileForm 8 | from opentakserver.functions import iso8601_string_from_datetime 9 | 10 | 11 | class DeviceProfiles(db.Model): 12 | __tablename__ = "device_profiles" 13 | 14 | preference_key: Mapped[str] = mapped_column(String(255), primary_key=True, nullable=False) 15 | preference_value: Mapped[str] = mapped_column(String(255), nullable=True) 16 | value_class: Mapped[str] = mapped_column(String(255), nullable=True) 17 | enrollment: Mapped[bool] = mapped_column(Boolean, default=True, nullable=True) 18 | connection: Mapped[bool] = mapped_column(Boolean, default=False, nullable=True) 19 | tool: Mapped[str] = mapped_column(String(255), nullable=True) 20 | active: Mapped[bool] = mapped_column(Boolean, default=True) 21 | publish_time: Mapped[datetime] = mapped_column(DateTime) 22 | 23 | def from_wtf(self, form: DeviceProfileForm): 24 | self.preference_key = form.preference_key.data 25 | self.preference_value = form.preference_value.data 26 | self.value_class = f"class java.lang.{form.value_class.data.split('.')[-1]}" 27 | self.enrollment = form.enrollment.data 28 | self.connection = form.connection.data 29 | self.tool = form.tool.data 30 | self.active = form.active.data 31 | self.publish_time = datetime.now() 32 | 33 | def serialize(self): 34 | return { 35 | 'preference_key': self.preference_key, 36 | 'preference_value': self.preference_value, 37 | 'value_class': self.value_class, 38 | 'enrollment': self.enrollment, 39 | 'connection': self.connection, 40 | 'tool': self.tool, 41 | 'active': self.active, 42 | 'publish_time': self.publish_time 43 | } 44 | 45 | def to_json(self): 46 | return_value = self.serialize() 47 | return_value['publish_time'] = iso8601_string_from_datetime(self.publish_time) 48 | return return_value 49 | -------------------------------------------------------------------------------- /opentakserver/models/EUD.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | from datetime import datetime 3 | 4 | from opentakserver.extensions import db 5 | from sqlalchemy import Integer, String, ForeignKey, DateTime, BigInteger 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from opentakserver.functions import iso8601_string_from_datetime 9 | 10 | 11 | @dataclass 12 | class EUD(db.Model): 13 | __tablename__ = "euds" 14 | 15 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 16 | uid: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) 17 | callsign: Mapped[str] = mapped_column(String(255), nullable=True) 18 | device: Mapped[str] = mapped_column(String(255), nullable=True) 19 | os: Mapped[str] = mapped_column(String(255), nullable=True) 20 | platform: Mapped[str] = mapped_column(String(255), nullable=True) 21 | version: Mapped[str] = mapped_column(String(255), nullable=True) 22 | phone_number: Mapped[int] = mapped_column(BigInteger, nullable=True) 23 | last_event_time: Mapped[datetime] = mapped_column(DateTime, nullable=True) 24 | last_status: Mapped[str] = mapped_column(String(255), nullable=True) 25 | user_id: Mapped[int] = mapped_column(Integer, ForeignKey("user.id"), nullable=True) 26 | team_id: Mapped[int] = mapped_column(Integer, ForeignKey("teams.id"), nullable=True) 27 | team_role: Mapped[str] = mapped_column(String(255), nullable=True) 28 | meshtastic_id: Mapped[int] = mapped_column(BigInteger, nullable=True) 29 | meshtastic_macaddr: Mapped[String] = mapped_column(String(255), nullable=True) 30 | points = relationship("Point", cascade="all, delete-orphan", back_populates="eud") 31 | cots = relationship("CoT", cascade="all, delete-orphan", back_populates="eud") 32 | casevacs = relationship("CasEvac", cascade="all, delete-orphan", back_populates="eud") 33 | geochats = relationship("GeoChat", cascade="all, delete-orphan", back_populates="eud") 34 | chatroom_uid = relationship("ChatroomsUids", cascade="all, delete-orphan", back_populates="eud") 35 | user = relationship("User", back_populates="euds") 36 | alert = relationship("Alert", cascade="all, delete-orphan", back_populates="eud") 37 | data_packages = relationship("DataPackage", cascade="all, delete-orphan", back_populates="eud", uselist=False) 38 | certificate = relationship("Certificate", cascade="all, delete, delete-orphan", back_populates="eud", uselist=False) 39 | markers = relationship("Marker", cascade="all, delete, delete-orphan", back_populates="eud") 40 | rb_lines = relationship("RBLine", cascade="all, delete-orphan", back_populates="eud") 41 | team = relationship("Team", back_populates="euds") 42 | owned_missions = relationship("Mission", back_populates="owner") 43 | groups = relationship("Group", secondary="groups_euds", back_populates="euds") 44 | stats = relationship("EUDStats", back_populates="eud") 45 | #mission_invitations_uid = relationship("MissionInvitation", back_populates="eud_uid") 46 | #mission_invitations_callsign = relationship("MissionInvitation", back_populates="eud_callsign") 47 | 48 | def serialize(self): 49 | return { 50 | 'uid': self.uid, 51 | 'callsign': self.callsign, 52 | 'device': self.device, 53 | 'os': self.os, 54 | 'platform': self.platform, 55 | 'version': self.version, 56 | 'phone_number': self.phone_number, 57 | 'last_event_time': self.last_event_time, 58 | 'last_status': self.last_status, 59 | 'user_id': self.user_id, 60 | 'team_id': self.team_id, 61 | 'team_role': self.team_role 62 | } 63 | 64 | def to_json(self, include_data_packages=True): 65 | config_datapackage_hash = None 66 | if self.certificate and self.certificate.data_package: 67 | config_datapackage_hash = self.certificate.data_package.hash 68 | return { 69 | 'uid': self.uid, 70 | 'callsign': self.callsign, 71 | 'device': self.device, 72 | 'os': self.os, 73 | 'platform': self.platform, 74 | 'version': self.version, 75 | 'phone_number': self.phone_number, 76 | 'last_event_time': iso8601_string_from_datetime(self.last_event_time) if self.last_event_time else None, 77 | 'last_status': self.last_status, 78 | 'username': self.user.username if self.user else None, 79 | 'last_point': self.points[-1].to_json() if self.points else None, 80 | 'team': self.team.name if self.team else None, 81 | 'team_color': self.team.get_team_color() if self.team else None, 82 | 'team_role': self.team_role, 83 | 'data_packages': self.data_packages.to_json(False) if include_data_packages and self.data_packages else None, 84 | 'config_hash': config_datapackage_hash 85 | } 86 | -------------------------------------------------------------------------------- /opentakserver/models/EUDStats.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from opentakserver.extensions import db 4 | from opentakserver.functions import bytes_to_gigabytes, bytes_to_megabytes 5 | from sqlalchemy import Integer, String, ForeignKey, DateTime 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from opentakserver.functions import iso8601_string_from_datetime 9 | 10 | 11 | @dataclass 12 | class EUDStats(db.Model): 13 | __tablename__ = "eud_stats" 14 | 15 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 16 | timestamp: Mapped[DateTime] = mapped_column(DateTime, nullable=True) 17 | eud_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE"), nullable=False) 18 | heap_free_size: Mapped[int] = mapped_column(Integer, nullable=True) 19 | app_framerate: Mapped[int] = mapped_column(Integer, nullable=True) 20 | storage_total: Mapped[int] = mapped_column(Integer, nullable=True) 21 | heap_current_size: Mapped[int] = mapped_column(Integer, nullable=True) 22 | battery: Mapped[int] = mapped_column(Integer, nullable=True) 23 | battery_temp: Mapped[int] = mapped_column(Integer, nullable=True) 24 | deviceDataRx: Mapped[int] = mapped_column(Integer, nullable=True) 25 | heap_max_size: Mapped[int] = mapped_column(Integer, nullable=True) 26 | storage_available: Mapped[int] = mapped_column(Integer, nullable=True) 27 | deviceDataTx: Mapped[int] = mapped_column(Integer, nullable=True) 28 | ip_address: Mapped[str] = mapped_column(String(255), nullable=True) 29 | battery_status: Mapped[str] = mapped_column(String(255), nullable=True) 30 | eud = relationship("EUD", back_populates="stats", uselist=False) 31 | 32 | def serialize(self): 33 | return { 34 | "timestamp": self.timestamp, 35 | "eud_uid": self.eud_uid, 36 | "heap_free_size": self.heap_free_size, 37 | "app_framerate": self.app_framerate, 38 | "storage_total": self.storage_total, 39 | "heap_current_size": self.heap_current_size, 40 | "battery": self.battery, 41 | "deviceDataRx": self.deviceDataRx, 42 | "heap_max_size": self.heap_max_size, 43 | "storage_available": self.storage_available, 44 | "deviceDataTx": self.deviceDataTx, 45 | "ip_address": self.ip_address, 46 | "battery_status": self.battery_status 47 | } 48 | 49 | def to_json(self): 50 | return { 51 | "timestamp": iso8601_string_from_datetime(self.timestamp), 52 | "eud_uid": self.eud_uid, 53 | "heap_free_size": bytes_to_megabytes(self.heap_free_size), 54 | "app_framerate": self.app_framerate, 55 | "storage_total": bytes_to_gigabytes(self.storage_total), 56 | "heap_current_size": bytes_to_megabytes(self.heap_current_size), 57 | "battery": self.battery, 58 | "deviceDataRx": bytes_to_megabytes(self.deviceDataRx), 59 | "heap_max_size": bytes_to_megabytes(self.heap_max_size), 60 | "storage_available": bytes_to_gigabytes(self.storage_available), 61 | "deviceDataTx": bytes_to_megabytes(self.deviceDataTx), 62 | "ip_address": self.ip_address, 63 | "battery_status": self.battery_status 64 | } 65 | -------------------------------------------------------------------------------- /opentakserver/models/GeoChat.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import Integer, String, ForeignKey, DateTime 5 | from sqlalchemy.orm import Mapped, mapped_column, relationship 6 | 7 | 8 | class GeoChat(db.Model): 9 | __tablename__ = 'geochat' 10 | 11 | uid: Mapped[str] = mapped_column(String(255), primary_key=True) 12 | chatroom_id: Mapped[str] = mapped_column(String(255), ForeignKey("chatrooms.id")) 13 | sender_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE")) 14 | remarks: Mapped[str] = mapped_column(String(255)) 15 | timestamp: Mapped[datetime] = mapped_column(DateTime) 16 | point_id: Mapped[int] = mapped_column(Integer, ForeignKey("points.id")) 17 | cot_id: Mapped[int] = mapped_column(Integer, ForeignKey("cot.id")) 18 | point = relationship("Point", cascade="all", back_populates="geochat", uselist=False) 19 | cot = relationship("CoT", cascade="all", back_populates="geochat") 20 | chatroom = relationship("Chatroom", back_populates="geochats") 21 | eud = relationship("EUD", back_populates="geochats") 22 | 23 | def serialize(self): 24 | return { 25 | 'uid': self.uid, 26 | 'sender_uid': self.sender_uid, 27 | 'remarks': self.remarks, 28 | 'timestamp': self.timestamp, 29 | } 30 | 31 | def to_json(self): 32 | return { 33 | 'uid': self.uid, 34 | 'sender_uid': self.sender_uid, 35 | 'remarks': self.remarks, 36 | 'timestamp': self.timestamp, 37 | 'point': self.point.to_json() or None, 38 | } 39 | -------------------------------------------------------------------------------- /opentakserver/models/Group.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from dataclasses import dataclass 3 | 4 | from opentakserver.extensions import db 5 | from opentakserver.functions import iso8601_string_from_datetime 6 | from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey 7 | from sqlalchemy.orm import Mapped, mapped_column, relationship 8 | 9 | 10 | @dataclass 11 | class Group(db.Model): 12 | __tablename__ = "groups" 13 | 14 | SYSTEM = "SYSTEM" 15 | LDAP = "LDAP" 16 | 17 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 18 | group_name: Mapped[str] = mapped_column(String(255)) 19 | created: Mapped[datetime] = mapped_column(DateTime) 20 | group_type: Mapped[str] = mapped_column(String(255), default=SYSTEM) # SYSTEM, LDAP 21 | bitpos: Mapped[int] = mapped_column(Integer) 22 | euds = relationship("EUD", secondary="groups_euds", back_populates="groups") 23 | 24 | def serialize(self): 25 | return { 26 | 'group_name': self.group_name, 27 | 'created': self.created, 28 | 'group_type': self.group_type, 29 | 'bitpos': self.bitpos, 30 | } 31 | 32 | def to_json(self): 33 | return { 34 | "name": self.group_name, 35 | "direction": self.direction, 36 | "created": iso8601_string_from_datetime(self.created).split("T")[0], 37 | "type": self.group_type, 38 | "bitpos": self.bitpos, 39 | "active": self.active, 40 | "description": self.description if self.description else "" 41 | } 42 | -------------------------------------------------------------------------------- /opentakserver/models/GroupEud.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from dataclasses import dataclass 3 | 4 | from opentakserver.extensions import db 5 | from opentakserver.functions import iso8601_string_from_datetime 6 | from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey 7 | from sqlalchemy.orm import Mapped, mapped_column, relationship 8 | 9 | 10 | @dataclass 11 | class GroupEud(db.Model): 12 | __tablename__ = "groups_euds" 13 | 14 | IN = "IN" 15 | OUT = "OUT" 16 | 17 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 18 | group_id: Mapped[int] = mapped_column(Integer, ForeignKey("groups.id")) 19 | direction: Mapped[str] = mapped_column(String(255)) # IN, OUT 20 | active: Mapped[bool] = mapped_column(Boolean) 21 | description: Mapped[str] = mapped_column(String(255), nullable=True) 22 | eud_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE")) 23 | 24 | def serialize(self): 25 | return { 26 | 'group_id': self.group_id, 27 | 'direction': self.direction, 28 | 'active': self.active, 29 | 'description': self.description, 30 | 'eud_uid': self.eud_uid 31 | } 32 | 33 | def to_json(self): 34 | return { 35 | "name": self.group.group_name, 36 | "direction": self.direction, 37 | "created": iso8601_string_from_datetime(self.group.created).split("T")[0], 38 | "type": self.group.group_type, 39 | "bitpos": self.group.bitpos, 40 | "active": self.active, 41 | "description": self.description if self.description else "" 42 | } 43 | -------------------------------------------------------------------------------- /opentakserver/models/Icon.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from dataclasses import dataclass 3 | 4 | from opentakserver.extensions import db 5 | from sqlalchemy import Integer, LargeBinary, String, INTEGER 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | 9 | @dataclass 10 | class Icon(db.Model): 11 | __tablename__ = "icons" 12 | 13 | id: Mapped[int] = mapped_column(INTEGER, primary_key=True) 14 | iconset_uid: Mapped[str] = mapped_column(String(255), nullable=True) 15 | filename: Mapped[str] = mapped_column(String(255), nullable=True) 16 | groupName: Mapped[str] = mapped_column(String(255), nullable=True) 17 | type2525b: Mapped[str] = mapped_column(String(255), nullable=True) 18 | useCnt: Mapped[int] = mapped_column(Integer, nullable=True) 19 | bitmap: Mapped[bytes] = mapped_column(LargeBinary, nullable=True) 20 | shadow: Mapped[bytes] = mapped_column(LargeBinary, nullable=True) 21 | markers = relationship("Marker", back_populates="icon") 22 | 23 | def serialize(self): 24 | return { 25 | 'iconset_uid': self.iconset_uid, 26 | 'filename': self.filename, 27 | 'groupName': self.groupName, 28 | 'type2525b': self.type2525b, 29 | 'useCnt': self.useCnt, 30 | 'bitmap': 'data:image/png;base64,{}'.format(base64.b64encode(self.bitmap).decode('utf-8')) if self.bitmap else None, 31 | 'shadow': 'data:image/png;base64,{}'.format(base64.b64encode(self.shadow).decode('utf-8')) if self.shadow else None 32 | } 33 | 34 | def to_json(self): 35 | return self.serialize() 36 | 37 | 38 | class IconSets(db.Model): 39 | __tablename__ = "iconsets" 40 | 41 | id: Mapped[int] = mapped_column(INTEGER, primary_key=True) 42 | name: Mapped[str] = mapped_column(String(255), nullable=True) 43 | uid: Mapped[str] = mapped_column(String(255), nullable=True) 44 | version: Mapped[int] = mapped_column(Integer, nullable=True) 45 | defaultFriendly: Mapped[str] = mapped_column(String(255), nullable=True) 46 | defaultHostile: Mapped[str] = mapped_column(String(255), nullable=True) 47 | defaultNeutral: Mapped[str] = mapped_column(String(255), nullable=True) 48 | defaultUnknown: Mapped[str] = mapped_column(String(255), nullable=True) 49 | selectedGroup: Mapped[str] = mapped_column(String(255), nullable=True) 50 | -------------------------------------------------------------------------------- /opentakserver/models/Marker.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import Integer, String, ForeignKey, Boolean 5 | from sqlalchemy.orm import Mapped, mapped_column, relationship 6 | 7 | from opentakserver.functions import iso8601_string_from_datetime 8 | 9 | 10 | @dataclass 11 | class Marker(db.Model): 12 | __tablename__ = "markers" 13 | 14 | # type = a-[a-z]-[A-Z] 15 | # how = h-g-i-g-o 16 | 17 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 18 | uid: Mapped[str] = mapped_column(String(255), unique=True) 19 | affiliation: Mapped[str] = mapped_column(String(255), nullable=True) 20 | battle_dimension: Mapped[str] = mapped_column(String(255), nullable=True) 21 | point_id: Mapped[int] = mapped_column(Integer, ForeignKey("points.id", ondelete="CASCADE")) 22 | callsign: Mapped[str] = mapped_column(String(255), nullable=True) 23 | readiness: Mapped[bool] = mapped_column(Boolean, nullable=True) 24 | argb: Mapped[int] = mapped_column(Integer, nullable=True) 25 | color_hex: Mapped[str] = mapped_column(String(255), nullable=True) 26 | iconset_path: Mapped[str] = mapped_column(String(255), nullable=True) 27 | parent_callsign: Mapped[str] = mapped_column(String(255), nullable=True) 28 | production_time: Mapped[str] = mapped_column(String(255), nullable=True) 29 | relation: Mapped[str] = mapped_column(String(255), nullable=True) 30 | relation_type: Mapped[str] = mapped_column(String(255), nullable=True) 31 | location_source: Mapped[str] = mapped_column(String(255), nullable=True) 32 | icon_id: Mapped[int] = mapped_column(Integer, ForeignKey("icons.id"), nullable=True) 33 | 34 | # Will either be the uid attribute in the tag or 35 | # if there's no tag it's assumed that the sender is the parent 36 | parent_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE"), nullable=True) 37 | remarks: Mapped[str] = mapped_column(String(255), nullable=True) 38 | cot_id: Mapped[int] = mapped_column(Integer, ForeignKey("cot.id", ondelete="CASCADE"), nullable=True) 39 | mil_std_2525c: Mapped[str] = mapped_column(String(255), nullable=True) 40 | point = relationship("Point", cascade="all, delete", back_populates="marker") 41 | cot = relationship("CoT", back_populates="marker") 42 | eud = relationship("EUD", back_populates="markers") 43 | icon = relationship("Icon", back_populates="markers") 44 | 45 | def color_to_hex(self): 46 | if self.argb: 47 | return format(int(self.argb) & 0xFFFFFFFF, '08X') 48 | 49 | def serialize(self): 50 | return { 51 | 'uid': self.uid, 52 | 'affiliation': self.affiliation, 53 | 'battle_dimension': self.battle_dimension, 54 | 'callsign': self.callsign, 55 | 'readiness': self.readiness, 56 | 'argb': self.argb, 57 | 'color_hex': self.color_hex, 58 | 'iconset_path': self.iconset_path, 59 | 'parent_callsign': self.parent_callsign, 60 | 'relation': self.relation, 61 | 'relation_type': self.relation_type, 62 | 'production_time': self.production_time, 63 | 'location_source': self.location_source, 64 | 'mil_std_2525c': self.mil_std_2525c, 65 | } 66 | 67 | def to_json(self): 68 | return { 69 | 'uid': self.uid, 70 | 'affiliation': self.affiliation, 71 | 'battle_dimension': self.battle_dimension, 72 | 'callsign': self.callsign, 73 | 'readiness': self.readiness, 74 | 'argb': self.argb, 75 | 'color_hex': self.color_hex, 76 | 'iconset_path': self.iconset_path, 77 | 'parent_callsign': self.parent_callsign, 78 | 'relation': self.relation, 79 | 'relation_type': self.relation_type, 80 | 'production_time': self.production_time, 81 | 'location_source': self.location_source, 82 | 'icon': self.icon.to_json() if self.icon else None, 83 | 'point': self.point.to_json() if self.point else None, 84 | 'mil_std_2525c': self.mil_std_2525c, 85 | 'type': self.cot.type if self.cot else None, 86 | 'how': self.cot.how if self.cot else None, 87 | 'start': iso8601_string_from_datetime(self.cot.start) if self.cot else None, 88 | 'stale': iso8601_string_from_datetime(self.cot.stale) if self.cot else None, 89 | } 90 | -------------------------------------------------------------------------------- /opentakserver/models/Meshtastic.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import String, Integer, Boolean 5 | from sqlalchemy.orm import Mapped, mapped_column 6 | 7 | 8 | @dataclass 9 | class MeshtasticChannel(db.Model): 10 | __tablename__ = "meshtastic_channels" 11 | 12 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 13 | psk: Mapped[str] = mapped_column(String(255), nullable=True) 14 | name: Mapped[str] = mapped_column(String(255), nullable=True) 15 | uplink_enabled: Mapped[bool] = mapped_column(Boolean, default=False) 16 | downlink_enabled: Mapped[bool] = mapped_column(Boolean, default=False) 17 | position_precision: Mapped[int] = mapped_column(Integer, default=32) # LOW = 11, MED = 16, HIGH = 32, DISABLED = 0 18 | lora_region: Mapped[int] = mapped_column(Integer, default=0) 19 | lora_hop_limit: Mapped[int] = mapped_column(Integer, default=3) 20 | lora_tx_enabled: Mapped[bool] = mapped_column(Boolean, default=True) 21 | lora_tx_power: Mapped[int] = mapped_column(Integer, default=30) 22 | lora_sx126x_rx_boosted_gain: Mapped[bool] = mapped_column(Boolean, default=True) 23 | modem_preset: Mapped[int] = mapped_column(Integer, default=0) 24 | url: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) 25 | 26 | def serialize(self): 27 | return { 28 | 'psk': self.psk if self.psk else None, 29 | 'name': self.name, 30 | 'uplink_enabled': self.uplink_enabled, 31 | 'downlink_enabled': self.downlink_enabled, 32 | 'position_precision': self.position_precision, 33 | 'lora_region': self.lora_region, 34 | 'lora_hop_limit': self.lora_hop_limit, 35 | 'lora_tx_enabled': self.lora_tx_enabled, 36 | 'lora_tx_power': self.lora_tx_power, 37 | 'lora_sx126x_rx_boosted_gain': self.lora_sx126x_rx_boosted_gain, 38 | 'modem_preset': self.modem_preset, 39 | 'url': self.url 40 | } 41 | 42 | def to_json(self): 43 | preset = "LONG_FAST" 44 | if self.modem_preset == 1: 45 | preset = "LONG_SLOW" 46 | elif self.modem_preset == 2: 47 | preset = "VERY_LONG_SLOW" 48 | elif self.modem_preset == 3: 49 | preset = "MEDIUM_SLOW" 50 | elif self.modem_preset == 4: 51 | preset = "MEDIUM_FAST" 52 | elif self.modem_preset == 5: 53 | preset = "SHORT_SLOW" 54 | elif self.modem_preset == 6: 55 | preset = "SHORT_FAST" 56 | elif self.modem_preset == 7: 57 | preset = "LONG_MODERATE" 58 | elif self.modem_preset == 8: 59 | preset = "SHORT_TURBO" 60 | 61 | region = "UNSET" 62 | if self.lora_region == 1: 63 | region = "US" 64 | elif self.lora_region == 2: 65 | region = "EU_433" 66 | elif self.lora_region == 3: 67 | region = "EU_868" 68 | elif self.lora_region == 4: 69 | region = "CN" 70 | elif self.lora_region == 5: 71 | region = "JP" 72 | elif self.lora_region == 6: 73 | region = "ANZ" 74 | elif self.lora_region == 7: 75 | region = "KR" 76 | elif self.lora_region == 8: 77 | region = "TW" 78 | elif self.lora_region == 9: 79 | region = "RU" 80 | elif self.lora_region == 10: 81 | region = "IN" 82 | elif self.lora_region == 11: 83 | region = "NZ_865" 84 | elif self.lora_region == 12: 85 | region = "TH" 86 | elif self.lora_region == 13: 87 | region = "LORA_24" 88 | elif self.lora_region == 14: 89 | region = "UA_433" 90 | elif self.lora_region == 15: 91 | region = "UA_868" 92 | elif self.lora_region == 16: 93 | region = "MY_433" 94 | elif self.lora_region == 17: 95 | region = "MY_919" 96 | elif self.lora_region == 18: 97 | region = "SG_923" 98 | 99 | return { 100 | 'psk': self.psk if self.psk else None, 101 | 'name': self.name, 102 | 'uplink_enabled': self.uplink_enabled, 103 | 'downlink_enabled': self.downlink_enabled, 104 | 'position_precision': self.position_precision, 105 | 'lora_region': region, 106 | 'lora_hop_limit': self.lora_hop_limit, 107 | 'lora_tx_enabled': self.lora_tx_enabled, 108 | 'lora_tx_power': self.lora_tx_power, 109 | 'lora_sx126x_rx_boosted_gain': self.lora_sx126x_rx_boosted_gain, 110 | 'modem_preset': preset, 111 | 'url': self.url 112 | } 113 | -------------------------------------------------------------------------------- /opentakserver/models/MissionContent.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from dataclasses import dataclass 4 | 5 | from opentakserver.functions import iso8601_string_from_datetime 6 | from opentakserver.extensions import db 7 | from sqlalchemy import Integer, String, DateTime, JSON 8 | from sqlalchemy.orm import Mapped, mapped_column, relationship 9 | 10 | 11 | @dataclass 12 | class MissionContent(db.Model): 13 | __tablename__ = "mission_content" 14 | 15 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 16 | keywords: Mapped[JSON] = mapped_column(JSON, nullable=True) 17 | mime_type: Mapped[str] = mapped_column(String(255), nullable=True) 18 | filename: Mapped[str] = mapped_column(String(255), nullable=True) 19 | submission_time: Mapped[datetime] = mapped_column(DateTime, nullable=True) 20 | submitter: Mapped[str] = mapped_column(String(255), nullable=True) 21 | uid: Mapped[str] = mapped_column(String(255), unique=True, default=str(uuid.uuid4())) 22 | creator_uid: Mapped[str] = mapped_column(String(255), nullable=True) 23 | hash: Mapped[str] = mapped_column(String(255), nullable=False, unique=True) 24 | size: Mapped[int] = mapped_column(Integer, nullable=True) 25 | expiration: Mapped[int] = mapped_column(Integer, nullable=True) 26 | mission_changes = relationship("MissionChange", back_populates="content_resource") 27 | mission = relationship("Mission", secondary="mission_content_mission", back_populates="contents") 28 | 29 | def serialize(self): 30 | return { 31 | "keywords": self.keywords, 32 | "mime_type": self.mime_type, 33 | "filename": self.filename, 34 | "submission_time": self.submission_time, 35 | "submitter": self.submitter, 36 | "uid": self.uid, 37 | "creator_uid": self.creator_uid, 38 | "hash": self.hash, 39 | "size": self.size, 40 | "expiration": self.expiration 41 | } 42 | 43 | def to_json(self): 44 | return { 45 | "data": { 46 | "keywords": self.keywords if self.keywords else [], 47 | "mimeType": self.mime_type, 48 | "name": self.filename, 49 | "submissionTime": iso8601_string_from_datetime(self.submission_time), 50 | "submitter": self.submitter, 51 | "uid": self.uid, 52 | "creator_uid": self.creator_uid, 53 | "hash": self.hash, 54 | "size": self.size, 55 | "expiration": self.expiration 56 | }, 57 | "timestamp": iso8601_string_from_datetime(self.submission_time), 58 | "creatorUid": self.creator_uid 59 | } 60 | -------------------------------------------------------------------------------- /opentakserver/models/MissionContentMission.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import Integer, ForeignKey, String 5 | from sqlalchemy.orm import Mapped, mapped_column, relationship 6 | 7 | 8 | @dataclass 9 | class MissionContentMission(db.Model): 10 | __tablename__ = "mission_content_mission" 11 | 12 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 13 | mission_content_id: Mapped[int] = mapped_column(Integer, ForeignKey("mission_content.id")) 14 | mission_name: Mapped[str] = mapped_column(String(255), ForeignKey("missions.name")) 15 | 16 | def serialize(self): 17 | return { 18 | "mission_content_id": self.mission_content_id, 19 | "mission_name": self.mission_name 20 | } 21 | 22 | def to_json(self): 23 | return self.serialize() 24 | -------------------------------------------------------------------------------- /opentakserver/models/MissionInvitation.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from dataclasses import dataclass 3 | 4 | from opentakserver.extensions import db 5 | from sqlalchemy import Integer, String, Boolean, ForeignKey 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | 9 | @dataclass 10 | class MissionInvitation(db.Model): 11 | __tablename__ = "mission_invitations" 12 | 13 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 14 | mission_name: Mapped[str] = mapped_column(String(255), ForeignKey('missions.name')) 15 | client_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE"), nullable=True) 16 | callsign: Mapped[str] = mapped_column(String(255), ForeignKey("euds.callsign", ondelete="CASCADE"), nullable=True) 17 | username: Mapped[str] = mapped_column(String(255), ForeignKey("user.username"), nullable=True) 18 | group_name: Mapped[str] = mapped_column(String(255), nullable=True) 19 | team_name: Mapped[str] = mapped_column(String(255), ForeignKey("teams.name"), nullable=True) 20 | creator_uid: Mapped[str] = mapped_column(String(255), nullable=True) 21 | role: Mapped[str] = mapped_column(String(255), nullable=True) 22 | 23 | eud_uid = relationship("EUD", foreign_keys=[client_uid], uselist=False) 24 | eud_callsign = relationship("EUD", foreign_keys=[callsign], uselist=False) 25 | user = relationship("User", back_populates="mission_invitations", uselist=False) 26 | team = relationship("Team", back_populates="mission_invitations", uselist=False) 27 | mission = relationship("Mission", back_populates="invitations", uselist=False) 28 | 29 | def serialize(self): 30 | return { 31 | 'mission_name': self.mission_name, 32 | 'client_uid': self.client_uid, 33 | 'callsign': self.callsign, 34 | 'username': self.username, 35 | 'group_name': self.group, 36 | 'team_name': self.team_name, 37 | 'creator_uid': self.creator_uid, 38 | 'role': self.role 39 | } 40 | 41 | def to_json(self): 42 | return self.serialize() 43 | -------------------------------------------------------------------------------- /opentakserver/models/MissionLogEntry.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import uuid 3 | from dataclasses import dataclass 4 | 5 | from opentakserver.functions import iso8601_string_from_datetime 6 | from opentakserver.extensions import db 7 | from sqlalchemy import Integer, String, Boolean, ForeignKey, DateTime, JSON 8 | from sqlalchemy.orm import Mapped, mapped_column, relationship 9 | 10 | 11 | @dataclass 12 | class MissionLogEntry(db.Model): 13 | __tablename__ = "mission_logs" 14 | 15 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 16 | content: Mapped[str] = mapped_column(String(255)) 17 | creator_uid: Mapped[str] = mapped_column(String(255)) 18 | entry_uid: Mapped[str] = mapped_column(String(255), default=str(uuid.uuid4())) 19 | mission_name: Mapped[str] = mapped_column(String(255), ForeignKey("missions.name")) 20 | server_time: Mapped[datetime] = mapped_column(DateTime, default=datetime.datetime.now()) 21 | dtg: Mapped[datetime] = mapped_column(DateTime, default=datetime.datetime.now()) 22 | created: Mapped[datetime] = mapped_column(DateTime, default=datetime.datetime.now()) 23 | content_hash: Mapped[str] = mapped_column(String(255), nullable=True) 24 | keywords: Mapped[JSON] = mapped_column(JSON, default=[]) 25 | mission = relationship("Mission", back_populates="mission_logs") 26 | 27 | def serialize(self): 28 | return { 29 | 'content': self.content, 30 | 'creator_uid': self.creator_uid, 31 | 'entry_uid': self.entry_uid, 32 | 'mission_names': self.mission_name, 33 | 'server_time': self.server_time, 34 | 'dtg': self.dtg, 35 | 'created': self.created, 36 | 'content_hash': self.content_hash, 37 | 'keywords': self.keywords 38 | } 39 | 40 | def to_json(self): 41 | return { 42 | 'id': self.entry_uid, 43 | 'content': self.content, 44 | 'creatorUid': self.creator_uid, 45 | 'entryUid': self.entry_uid, 46 | 'missionNames': [self.mission_name], 47 | 'servertime': iso8601_string_from_datetime(self.server_time), 48 | 'dtg': iso8601_string_from_datetime(self.dtg), 49 | 'created': iso8601_string_from_datetime(self.created), 50 | 'contentHashes': [self.content_hash] if self.content_hash else [], 51 | 'keywords': self.keywords if self.keywords is not None else [] 52 | } 53 | -------------------------------------------------------------------------------- /opentakserver/models/MissionRole.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from dataclasses import dataclass 3 | 4 | from opentakserver.extensions import db 5 | from opentakserver.functions import iso8601_string_from_datetime 6 | from sqlalchemy import Integer, String, Boolean, DateTime, ForeignKey 7 | from sqlalchemy.orm import Mapped, mapped_column, relationship 8 | 9 | 10 | @dataclass 11 | class MissionRole(db.Model): 12 | __tablename__ = "mission_roles" 13 | 14 | MISSION_MANAGE_FEEDS = "MISSION_MANAGE_FEEDS" 15 | MISSION_SET_PASSWORD = "MISSION_SET_PASSWORD" 16 | MISSION_WRITE = "MISSION_WRITE" 17 | MISSION_MANAGE_LAYERS = "MISSION_MANAGE_LAYERS" 18 | MISSION_UPDATE_GROUPS = "MISSION_UPDATE_GROUPS" 19 | MISSION_DELETE = "MISSION_DELETE" 20 | MISSION_SET_ROLE = "MISSION_SET_ROLE" 21 | MISSION_READ = "MISSION_READ" 22 | MISSION_OWNER = "MISSION_OWNER" 23 | MISSION_SUBSCRIBER = "MISSION_SUBSCRIBER" 24 | MISSION_READ_ONLY = "MISSION_READ_ONLY" 25 | 26 | OWNER_ROLE = {'type': MISSION_OWNER, 'permissions': []} 27 | OWNER_ROLE['permissions'].append(MISSION_MANAGE_FEEDS) 28 | OWNER_ROLE['permissions'].append(MISSION_SET_PASSWORD) 29 | OWNER_ROLE['permissions'].append(MISSION_WRITE) 30 | OWNER_ROLE['permissions'].append(MISSION_MANAGE_LAYERS) 31 | OWNER_ROLE['permissions'].append(MISSION_UPDATE_GROUPS) 32 | OWNER_ROLE['permissions'].append(MISSION_DELETE) 33 | OWNER_ROLE['permissions'].append(MISSION_SET_ROLE) 34 | OWNER_ROLE['permissions'].append(MISSION_READ) 35 | 36 | SUBSCRIBER_ROLE = {'type': MISSION_SUBSCRIBER, 'permissions': []} 37 | SUBSCRIBER_ROLE['permissions'].append(MISSION_READ) 38 | SUBSCRIBER_ROLE['permissions'].append(MISSION_WRITE) 39 | 40 | READ_ONLY_ROLE = {'type': MISSION_READ_ONLY, 'permissions': []} 41 | READ_ONLY_ROLE['permissions'].append(MISSION_READ) 42 | 43 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 44 | clientUid: Mapped[str] = mapped_column(String(255)) 45 | username: Mapped[str] = mapped_column(String(255)) 46 | createTime: Mapped[datetime] = mapped_column(DateTime) 47 | role_type: Mapped[str] = mapped_column(String(255)) 48 | mission_name: Mapped[str] = mapped_column(String(255), ForeignKey("missions.name")) 49 | mission = relationship("Mission", back_populates="roles", uselist=False) 50 | 51 | def serialize(self): 52 | return { 53 | 'clientUid': self.clientUid, 54 | 'username': self.username, 55 | 'createTime': self.createTime, 56 | 'role_type': self.role_type 57 | } 58 | 59 | def to_json(self): 60 | json = { 61 | 'clientUid': self.clientUid, 62 | 'username': self.username, 63 | 'createTime': iso8601_string_from_datetime(self.createTime), 64 | 'role': { 65 | 'type': self.role_type, 66 | 'permissions': [] 67 | } 68 | } 69 | 70 | if self.role_type == self.MISSION_OWNER: 71 | json['role'] = self.OWNER_ROLE 72 | 73 | elif self.role_type == self.MISSION_SUBSCRIBER: 74 | json['role'] = self.SUBSCRIBER_ROLE 75 | 76 | else: 77 | json['role'] = self.READ_ONLY_ROLE 78 | 79 | return json 80 | -------------------------------------------------------------------------------- /opentakserver/models/MissionUID.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from dataclasses import dataclass 3 | 4 | from opentakserver.functions import iso8601_string_from_datetime 5 | from opentakserver.extensions import db 6 | from sqlalchemy import String, DateTime, Float, ForeignKey, Integer 7 | from sqlalchemy.orm import Mapped, mapped_column, relationship 8 | 9 | 10 | @dataclass 11 | class MissionUID(db.Model): 12 | __tablename__ = "mission_uids" 13 | 14 | uid: Mapped[str] = mapped_column(String(255), primary_key=True) # Equals the original CoT's UID 15 | mission_name: Mapped[str] = mapped_column(String(255), ForeignKey("missions.name"), nullable=True) 16 | timestamp: Mapped[datetime] = mapped_column(DateTime, nullable=True) 17 | creator_uid: Mapped[str] = mapped_column(String(255), nullable=True) 18 | cot_type: Mapped[str] = mapped_column(String(255), nullable=True) 19 | callsign: Mapped[str] = mapped_column(String(255), nullable=True) 20 | iconset_path: Mapped[str] = mapped_column(String(255), nullable=True) 21 | color: Mapped[str] = mapped_column(String(255), nullable=True) 22 | latitude: Mapped[float] = mapped_column(Float, nullable=True) 23 | longitude: Mapped[float] = mapped_column(Float, nullable=True) 24 | mission = relationship("Mission", back_populates="uids") 25 | mission_change = relationship("MissionChange", back_populates="uid", uselist=False) 26 | 27 | def serialize(self): 28 | return { 29 | 'timestamp': self.timestamp, 30 | 'creator_uid': self.creator_uid, 31 | 'cot_type': self.cot_type, 32 | 'callsign': self.callsign, 33 | 'iconset_path': self.iconset_path, 34 | 'color': self.color, 35 | 'latitude': self.latitude, 36 | 'longitude': self.longitude 37 | } 38 | 39 | def to_json(self): 40 | return { 41 | 'data': self.uid, 42 | 'timestamp': iso8601_string_from_datetime(self.timestamp), 43 | 'creatorUid': self.creator_uid if self.creator_uid else "", 44 | 'details': { 45 | 'type': self.cot_type, 46 | 'callsign': self.callsign, 47 | 'iconsetPath': self.iconset_path, 48 | 'color': self.color, 49 | 'location': { 50 | 'lat': self.latitude, 51 | 'lon': self.longitude 52 | } 53 | } 54 | } 55 | 56 | def to_details_json(self): 57 | return { 58 | 'type': self.cot_type, 59 | 'callsign': self.callsign, 60 | 'iconsetPath': self.iconset_path, 61 | 'color': self.color, 62 | 'location': { 63 | 'lat': self.latitude, 64 | 'lon': self.longitude 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /opentakserver/models/Point.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | from datetime import datetime 3 | 4 | from opentakserver.extensions import db 5 | from sqlalchemy import Integer, String, ForeignKey, Float, DateTime 6 | from sqlalchemy.orm import Mapped, mapped_column, relationship 7 | 8 | from opentakserver.forms.point_form import PointForm 9 | from opentakserver.functions import iso8601_string_from_datetime 10 | 11 | 12 | class Point(db.Model): 13 | __tablename__ = "points" 14 | 15 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 16 | uid: Mapped[str] = mapped_column(String(255)) 17 | device_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE"), nullable=True) 18 | latitude: Mapped[float] = mapped_column(Float, nullable=True) 19 | longitude: Mapped[float] = mapped_column(Float, nullable=True) 20 | ce: Mapped[float] = mapped_column(Float, nullable=True) 21 | hae: Mapped[float] = mapped_column(Float, nullable=True) 22 | le: Mapped[float] = mapped_column(Float, nullable=True) 23 | course: Mapped[float] = mapped_column(Float, nullable=True) 24 | speed: Mapped[float] = mapped_column(Float, nullable=True) 25 | location_source: Mapped[str] = mapped_column(String(255), nullable=True) 26 | battery: Mapped[float] = mapped_column(Float, nullable=True) 27 | timestamp: Mapped[datetime] = mapped_column(DateTime) 28 | azimuth: Mapped[float] = mapped_column(Float, nullable=True) 29 | # Camera field of view from TAK ICU and OpenTAK ICU 30 | fov: Mapped[float] = mapped_column(Float, nullable=True) 31 | cot_id: Mapped[int] = mapped_column(Integer, ForeignKey("cot.id", ondelete="CASCADE"), nullable=True) 32 | cot = relationship("CoT", back_populates="point") 33 | 34 | # Only populate this field of the CoT type matches ^a- and how matches either ^m-g or ^h-e 35 | eud = relationship("EUD", back_populates="points") 36 | casevac = relationship("CasEvac", cascade="all", back_populates="point") 37 | geochat = relationship("GeoChat", back_populates="point", uselist=False) 38 | alert = relationship("Alert", cascade="all", back_populates="point") 39 | marker: Mapped["Marker"] = relationship(cascade="all, delete", back_populates="point") 40 | rb_line = relationship("RBLine", cascade="all", back_populates="point") 41 | 42 | def from_wtform(self, form: PointForm): 43 | self.uid = str(uuid.uuid4()) 44 | self.latitude = form.latitude.data 45 | self.longitude = form.longitude.data 46 | self.ce = form.ce.data 47 | self.hae = form.hae.data 48 | self.le = form.le.data 49 | self.course = form.course.data 50 | self.speed = form.speed.data 51 | self.location_source = form.location_source.data 52 | self.battery = form.battery.data 53 | self.timestamp = form.timestamp.data 54 | self.azimuth = form.azimuth.data 55 | self.fov = form.fov.data 56 | 57 | def serialize(self): 58 | return { 59 | 'uid': self.uid, 60 | 'device_uid': self.device_uid, 61 | 'latitude': self.latitude, 62 | 'longitude': self.longitude, 63 | 'ce': self.ce, 64 | 'hae': self.hae, 65 | 'le': self.le, 66 | 'course': self.course, 67 | 'speed': self.speed, 68 | 'azimuth': self.azimuth, 69 | 'fov': self.fov, 70 | 'location_source': self.location_source, 71 | 'battery': self.battery, 72 | 'timestamp': self.timestamp, 73 | } 74 | 75 | def to_json(self): 76 | return { 77 | 'uid': self.uid, 78 | 'device_uid': self.device_uid, 79 | 'latitude': self.latitude, 80 | 'longitude': self.longitude, 81 | 'ce': self.ce, 82 | 'hae': self.hae, 83 | 'le': self.le, 84 | 'course': self.course, 85 | 'speed': self.speed, 86 | 'azimuth': self.azimuth, 87 | 'fov': self.fov, 88 | 'location_source': self.location_source, 89 | 'battery': self.battery, 90 | 'timestamp': iso8601_string_from_datetime(self.timestamp), 91 | 'how': self.cot.how if self.cot else None, 92 | 'type': self.cot.type if self.cot else None, 93 | 'callsign': self.eud.callsign if self.eud else None 94 | } 95 | -------------------------------------------------------------------------------- /opentakserver/models/RBLine.py: -------------------------------------------------------------------------------- 1 | import math 2 | from dataclasses import dataclass 3 | from datetime import datetime 4 | 5 | from opentakserver.extensions import db 6 | from sqlalchemy import Integer, String, ForeignKey, Float, Boolean, DateTime 7 | from sqlalchemy.orm import Mapped, mapped_column, relationship 8 | from pygc import great_circle 9 | 10 | from opentakserver.functions import iso8601_string_from_datetime 11 | 12 | 13 | @dataclass 14 | class RBLine(db.Model): 15 | __tablename__ = "rb_lines" 16 | 17 | # type = u-rb-a 18 | # how = h-e 19 | 20 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 21 | sender_uid: Mapped[str] = mapped_column(String(255), ForeignKey("euds.uid", ondelete="CASCADE")) 22 | uid: Mapped[str] = mapped_column(String(255), unique=True) 23 | timestamp: Mapped[datetime] = mapped_column(DateTime) 24 | 25 | range: Mapped[float] = mapped_column(Float) 26 | bearing: Mapped[float] = mapped_column(Float) 27 | inclination: Mapped[float] = mapped_column(Float, nullable=True) 28 | anchor_uid: Mapped[str] = mapped_column(String(255), nullable=True) 29 | range_units: Mapped[int] = mapped_column(Integer, nullable=True) 30 | bearing_units: Mapped[int] = mapped_column(Integer, nullable=True) 31 | north_ref: Mapped[int] = mapped_column(Integer, nullable=True) 32 | color: Mapped[int] = mapped_column(Integer, nullable=True) 33 | color_hex: Mapped[str] = mapped_column(String(255), nullable=True) 34 | callsign: Mapped[str] = mapped_column(String(255), nullable=True) 35 | stroke_color: Mapped[int] = mapped_column(Integer, nullable=True) 36 | stroke_weight: Mapped[float] = mapped_column(Float, nullable=True) 37 | stroke_style: Mapped[str] = mapped_column(String(255), nullable=True) 38 | labels_on: Mapped[bool] = mapped_column(Boolean, nullable=True) 39 | point_id: Mapped[int] = mapped_column(Integer, ForeignKey("points.id", ondelete="CASCADE"), nullable=True) 40 | cot_id: Mapped[int] = mapped_column(Integer, ForeignKey("cot.id", ondelete="CASCADE"), nullable=True) 41 | end_latitude: Mapped[float] = mapped_column(Float, nullable=True) 42 | end_longitude: Mapped[float] = mapped_column(Float, nullable=True) 43 | point = relationship("Point", back_populates="rb_line") 44 | cot = relationship("CoT", back_populates="rb_line") 45 | eud = relationship("EUD", back_populates="rb_lines") 46 | 47 | range_unit_names = ['standard', 'metric', 'nautical'] 48 | bearing_unit_names = ['degrees', 'mils'] 49 | north_ref_names = ['true', 'magnetic', 'grid'] 50 | 51 | def color_to_hex(self): 52 | if self.color: 53 | return format(int(self.color) & 0xFFFFFFFF, '08X') 54 | 55 | def calc_end_point(self, start_point): 56 | if not int(self.bearing_units): 57 | azimuth = float(self.bearing) 58 | else: 59 | azimuth = math.degrees(float(self.bearing)) 60 | 61 | return great_circle(distance=float(self.range), azimuth=azimuth, latitude=start_point.latitude, 62 | longitude=start_point.longitude) 63 | 64 | def serialize(self): 65 | return { 66 | 'uid': self.uid, 67 | 'timestamp': self.timestamp, 68 | 'range': self.range, 69 | 'inclination': self.inclination, 70 | 'anchor_uid': self.anchor_uid, 71 | 'range_units': self.range_units, 72 | 'bearing_units': self.bearing_units, 73 | 'north_ref': self.north_ref, 74 | 'color': self.color, 75 | 'color_hex': self.color_hex, 76 | 'stroke_weight': self.stroke_weight, 77 | 'stroke_style': self.stroke_style, 78 | 'labels_on': self.labels_on, 79 | 'end_latitude': self.end_latitude, 80 | 'end_longitude': self.end_longitude 81 | } 82 | 83 | def to_json(self): 84 | return { 85 | 'uid': self.uid, 86 | 'timestamp': iso8601_string_from_datetime(self.timestamp), 87 | 'range': self.range, 88 | 'inclination': self.inclination, 89 | 'anchor_uid': self.anchor_uid, 90 | 'range_units': self.range_units, 91 | 'range_unit_name': self.range_unit_names[int(self.range_units)] if self.range_units else None, 92 | 'bearing_units': self.bearing_units, 93 | 'bearing_unit_name': self.bearing_unit_names[int(self.bearing_units)] if self.bearing else None, 94 | 'north_ref': self.north_ref, 95 | 'north_ref_name': self.north_ref_names[int(self.north_ref)] if self.north_ref else None, 96 | 'color': self.color, 97 | 'color_hex': self.color_hex, 98 | 'stroke_weight': self.stroke_weight, 99 | 'stroke_style': self.stroke_style, 100 | 'labels_on': self.labels_on, 101 | 'point': self.point.to_json() if self.point else None, 102 | 'end_latitude': self.end_latitude, 103 | 'end_longitude': self.end_longitude 104 | } 105 | -------------------------------------------------------------------------------- /opentakserver/models/Team.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import Integer, String, ForeignKey 5 | from sqlalchemy.orm import Mapped, mapped_column, relationship 6 | 7 | 8 | @dataclass 9 | class Team(db.Model): 10 | __tablename__ = "teams" 11 | 12 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 13 | name: Mapped[str] = mapped_column(String(255), unique=True) 14 | chatroom_id: Mapped[str] = mapped_column(String(255), ForeignKey("chatrooms.id"), nullable=True) 15 | euds = relationship("EUD", back_populates="team") 16 | chatroom = relationship("Chatroom", back_populates="team") 17 | mission_invitations = relationship("MissionInvitation", back_populates="team") 18 | 19 | colors = {'Cyan': '#00FFFF', 'White': '#000000', 'Yellow': '#FFFF00', 'Orange': '#FFA500', 'Magenta': '#FF00FF', 20 | 'Red': '#FF0000', 'Maroon': '#800000', 'Purple': '#800080', 'Dark Blue': '#00008B', 'Blue': '#0000FF', 21 | 'Teal': '#008080', 'Green': '#00FF00', 'Dark Green': '#228B22', 'Brown': '#964B00'} 22 | 23 | def get_team_color(self): 24 | return self.colors[self.name] 25 | 26 | def serialize(self): 27 | return { 28 | 'name': self.name, 29 | } 30 | 31 | def to_json(self): 32 | return { 33 | 'name': self.name, 34 | 'chatroom': self.chatroom.to_json() if self.chatroom else None, 35 | 'euds': [eud.to_json() for eud in self.euds] if self.euds else None, 36 | 'color': self.colors[self.name] 37 | } 38 | -------------------------------------------------------------------------------- /opentakserver/models/VideoRecording.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | from dataclasses import dataclass 3 | from urllib.parse import urlparse 4 | 5 | from flask import request, current_app as app 6 | 7 | from opentakserver.extensions import db 8 | from sqlalchemy import Integer, String, ForeignKey, Boolean, DateTime 9 | from sqlalchemy.orm import Mapped, mapped_column, relationship 10 | 11 | from opentakserver.functions import iso8601_string_from_datetime 12 | 13 | 14 | @dataclass 15 | class VideoRecording(db.Model): 16 | __tablename__ = 'video_recordings' 17 | 18 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 19 | segment_path: Mapped[str] = mapped_column(String(255), unique=True) 20 | path: Mapped[str] = mapped_column(String(255), ForeignKey("video_streams.path")) 21 | in_progress: Mapped[bool] = mapped_column(Boolean) 22 | start_time: Mapped[DateTime] = mapped_column(DateTime, nullable=True) 23 | stop_time: Mapped[DateTime] = mapped_column(DateTime, nullable=True) 24 | duration: Mapped[int] = mapped_column(Integer, nullable=True) 25 | width: Mapped[int] = mapped_column(Integer, nullable=True) 26 | height: Mapped[int] = mapped_column(Integer, nullable=True) 27 | video_codec: Mapped[str] = mapped_column(String(255), nullable=True) 28 | video_bitrate: Mapped[int] = mapped_column(Integer, nullable=True) 29 | audio_codec: Mapped[str] = mapped_column(String(255), nullable=True) 30 | audio_bitrate: Mapped[int] = mapped_column(Integer, nullable=True) 31 | audio_samplerate: Mapped[int] = mapped_column(Integer, nullable=True) 32 | audio_channels: Mapped[int] = mapped_column(Integer, nullable=True) 33 | file_size: Mapped[int] = mapped_column(Integer, nullable=True) 34 | video_stream = relationship("VideoStream", back_populates="recordings") 35 | 36 | def serialize(self): 37 | return { 38 | 'segment_path': self.segment_path, 39 | 'path': self.path, 40 | 'in_progress': self.in_progress 41 | } 42 | 43 | def to_json(self): 44 | with app.app_context(): 45 | url = urlparse(request.url_root) 46 | protocol = url.scheme 47 | hostname = url.hostname 48 | port = url.port 49 | if not port and protocol == 'https': 50 | port = 443 51 | elif not port and protocol == 'http': 52 | port = 80 53 | 54 | return { 55 | 'id': self.id, 56 | 'segment_path': self.segment_path, 57 | 'path': self.path, 58 | 'in_progress': self.in_progress, 59 | 'start_time': iso8601_string_from_datetime(self.start_time) if self.start_time else None, 60 | 'stop_time': iso8601_string_from_datetime(self.stop_time) if self.stop_time else None, 61 | 'duration': self.duration, 62 | 'filename': pathlib.Path(self.segment_path).name, 63 | 'width': self.width, 64 | 'height': self.height, 65 | 'video_codec': self.video_codec, 66 | 'video_bitrate': self.video_bitrate, 67 | 'audio_codec': self.audio_codec, 68 | 'audio_bitrate': self.audio_bitrate, 69 | 'audio_samplerate': self.audio_samplerate, 70 | 'audio_channels': self.audio_channels, 71 | 'file_size': self.file_size, 72 | 'thumbnail': f"{protocol}://{hostname}:{port}/api/videos/thumbnail?path={self.path}&recording={pathlib.Path(self.segment_path).name}" 73 | } 74 | -------------------------------------------------------------------------------- /opentakserver/models/WebAuthn.py: -------------------------------------------------------------------------------- 1 | from opentakserver.extensions import db 2 | from flask_security.models import fsqla_v3 as fsqla 3 | 4 | 5 | class WebAuthn(db.Model, fsqla.FsWebAuthnMixin): 6 | pass 7 | -------------------------------------------------------------------------------- /opentakserver/models/ZMIST.py: -------------------------------------------------------------------------------- 1 | from opentakserver.extensions import db 2 | from sqlalchemy import Integer, String, ForeignKey, Boolean, Float 3 | from sqlalchemy.orm import Mapped, mapped_column, relationship 4 | 5 | from opentakserver.forms.zmist_form import ZmistForm 6 | 7 | 8 | class ZMIST(db.Model): 9 | __tablename__ = 'zmist' 10 | 11 | id: Mapped[int] = mapped_column(Integer, primary_key=True) 12 | i: Mapped[str] = mapped_column(String(255), nullable=True) # injury_sustained 13 | m: Mapped[str] = mapped_column(String(255), nullable=True) # mechanism_of_injury 14 | s: Mapped[str] = mapped_column(String(255), nullable=True) # symptoms_and_signs 15 | t: Mapped[str] = mapped_column(String(255), nullable=True) # treatment_given 16 | title: Mapped[str] = mapped_column(String(255), nullable=True) 17 | z: Mapped[int] = mapped_column(Integer, nullable=True) # zap_number 18 | casevac_uid: Mapped[str] = mapped_column(String(255), ForeignKey("casevac.uid", ondelete="CASCADE")) 19 | casevac = relationship("CasEvac", back_populates="zmist") 20 | 21 | def from_wtform(self, form: ZmistForm): 22 | self.i = form.i.data 23 | self.m = form.m.data 24 | self.s = form.s.data 25 | self.t = form.t.data 26 | self.title = form.title.data 27 | self.z = form.z.data 28 | 29 | def serialize(self): 30 | return { 31 | 'i': self.i, 32 | 'm': self.m, 33 | 's': self.s, 34 | 't': self.t, 35 | 'title': self.title, 36 | 'z': self.z 37 | } 38 | 39 | def to_json(self): 40 | return self.serialize() 41 | -------------------------------------------------------------------------------- /opentakserver/models/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/models/__init__.py -------------------------------------------------------------------------------- /opentakserver/models/role.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from flask_security.models import fsqla_v3 as fsqla 4 | from opentakserver.extensions import db, logger 5 | 6 | 7 | @dataclass 8 | class Role(db.Model, fsqla.FsRoleMixin): 9 | 10 | def serialize(self): 11 | return { 12 | 'name': self.name, 13 | 'description': self.description, 14 | 'permissions': self.permissions, 15 | 'update_timestamp': self.update_datetime, 16 | } -------------------------------------------------------------------------------- /opentakserver/models/user.py: -------------------------------------------------------------------------------- 1 | from dataclasses import dataclass 2 | 3 | from opentakserver.extensions import db 4 | from sqlalchemy import String 5 | from flask_security.models import fsqla_v3 as fsqla 6 | from sqlalchemy.orm import relationship 7 | 8 | 9 | @dataclass 10 | class User(db.Model, fsqla.FsUserMixin): 11 | email = db.Column(String(255), nullable=True) 12 | video_streams = relationship("VideoStream", back_populates="user") 13 | euds = relationship("EUD", back_populates="user") 14 | data_packages = relationship("DataPackage", back_populates="user") 15 | certificate = relationship("Certificate", back_populates="user") 16 | mission_invitations = relationship("MissionInvitation", back_populates="user") 17 | 18 | def serialize(self): 19 | return { 20 | 'username': self.username, 21 | 'active': self.active, 22 | 'last_login_at': self.last_login_at, 23 | 'last_login_ip': self.last_login_ip, 24 | 'current_login_at': self.current_login_at, 25 | 'current_login_ip': self.current_login_ip, 26 | 'email': self.email, 27 | 'login_count': self.login_count, 28 | 'euds': [eud.serialize() for eud in self.euds], 29 | 'video_streams': [v.serialize() for v in self.video_streams], 30 | 'roles': [role.serialize() for role in self.roles] 31 | } 32 | 33 | def to_json(self): 34 | response = self.serialize() 35 | response['token'] = self.get_auth_token() 36 | return response 37 | -------------------------------------------------------------------------------- /opentakserver/mumble/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/mumble/__init__.py -------------------------------------------------------------------------------- /opentakserver/mumble/mumble_authenticator.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import Ice 4 | 5 | from flask_security import verify_password 6 | 7 | # Load up Murmur slice file into Ice 8 | Ice.loadSlice('', ['-I' + Ice.getSliceDir(), os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Murmur.ice')]) 9 | import Murmur 10 | 11 | 12 | class MumbleAuthenticator(Murmur.ServerUpdatingAuthenticator): 13 | texture_cache = {} 14 | 15 | def __init__(self, app, logger, ice): 16 | Murmur.ServerUpdatingAuthenticator.__init__(self) 17 | self.app = app 18 | self.logger = logger 19 | self.ice = ice 20 | 21 | def authenticate(self, username, password, certlist, certhash, strong, current=None): 22 | """ 23 | This function is called to authenticate a user 24 | """ 25 | if username == 'SuperUser': 26 | return (-2, None, None) 27 | 28 | self.logger.info("Mumble auth request for {}".format(username)) 29 | 30 | with self.app.app_context(): 31 | user = self.app.security.datastore.find_user(username=username) 32 | if not user: 33 | self.logger.warning("Mumble auth: User {} not found".format(username)) 34 | return (-1, None, None) 35 | elif not user.active: 36 | self.logger.warning("Mumble auth: User {} is deactivated".format(username)) 37 | return (-1, None, None) 38 | 39 | if verify_password(password, user.password): 40 | self.logger.info("Mumble auth: {} has been authenticated".format(username)) 41 | return (user.id, user.username, None) 42 | 43 | self.logger.warning("Mumble auth: Bad password for {}".format(username)) 44 | return (-1, None, None) 45 | 46 | def idToTexture(self, id, current=None): 47 | return 48 | 49 | def getInfo(self, id, current=None): 50 | """ 51 | Gets called to fetch user specific information 52 | """ 53 | 54 | # We do not expose any additional information so always fall through 55 | return (False, None) 56 | 57 | def nameToId(self, name, current=None): 58 | """ 59 | Gets called to get the id for a given username 60 | """ 61 | pass 62 | 63 | def idToName(self, id, current=None): 64 | """ 65 | Gets called to get the username for a given id 66 | """ 67 | pass 68 | 69 | def idToTexture(self, id, current=None): 70 | """ 71 | Gets called to get the corresponding texture for a user 72 | """ 73 | # seems like it pulled a user's avatar from a phpbb DB 74 | 75 | def registerUser(self, name, current=None): 76 | """ 77 | Gets called when the server is asked to register a user. 78 | """ 79 | pass 80 | 81 | def unregisterUser(self, id, current=None): 82 | """ 83 | Gets called when the server is asked to unregister a user. 84 | """ 85 | pass 86 | 87 | def getRegisteredUsers(self, filter, current=None): 88 | """ 89 | Returns a list of usernames in the phpBB3 database which contain 90 | filter as a substring. 91 | """ 92 | pass 93 | 94 | def setInfo(self, id, info, current=None): 95 | """ 96 | Gets called when the server is supposed to save additional information 97 | about a user to his database 98 | """ 99 | pass 100 | 101 | def setTexture(self, id, texture, current=None): 102 | """ 103 | Gets called when the server is asked to update the user texture of a user 104 | """ 105 | pass -------------------------------------------------------------------------------- /opentakserver/proto/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/opentakserver/proto/__init__.py -------------------------------------------------------------------------------- /opentakserver/proto/atak.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package meshtastic; 4 | 5 | option csharp_namespace = "Meshtastic.Protobufs"; 6 | option go_package = "github.com/meshtastic/go/generated"; 7 | option java_outer_classname = "ATAKProtos"; 8 | option java_package = "com.geeksville.mesh"; 9 | option swift_prefix = ""; 10 | 11 | /* 12 | * Packets for the official ATAK Plugin 13 | */ 14 | message TAKPacket { 15 | /* 16 | * Are the payloads strings compressed for LoRA transport? 17 | */ 18 | bool is_compressed = 1; 19 | /* 20 | * The contact / callsign for ATAK user 21 | */ 22 | Contact contact = 2; 23 | /* 24 | * The group for ATAK user 25 | */ 26 | Group group = 3; 27 | /* 28 | * The status of the ATAK EUD 29 | */ 30 | Status status = 4; 31 | /* 32 | * The payload of the packet 33 | */ 34 | oneof payload_variant { 35 | /* 36 | * TAK position report 37 | */ 38 | PLI pli = 5; 39 | /* 40 | * ATAK GeoChat message 41 | */ 42 | GeoChat chat = 6; 43 | } 44 | } 45 | 46 | /* 47 | * ATAK GeoChat message 48 | */ 49 | message GeoChat { 50 | /* 51 | * The text message 52 | */ 53 | bytes message = 1; 54 | 55 | /* 56 | * Uid recipient of the message 57 | */ 58 | optional string to = 2; 59 | } 60 | 61 | /* 62 | * ATAK Group 63 | * <__group role='Team Member' name='Cyan'/> 64 | */ 65 | message Group { 66 | /* 67 | * Role of the group member 68 | */ 69 | MemberRole role = 1; 70 | /* 71 | * Team (color) 72 | * Default Cyan 73 | */ 74 | Team team = 2; 75 | } 76 | 77 | enum Team { 78 | /* 79 | * Unspecifed 80 | */ 81 | Unspecifed_Color = 0; 82 | /* 83 | * White 84 | */ 85 | White = 1; 86 | /* 87 | * Yellow 88 | */ 89 | Yellow = 2; 90 | /* 91 | * Orange 92 | */ 93 | Orange = 3; 94 | /* 95 | * Magenta 96 | */ 97 | Magenta = 4; 98 | /* 99 | * Red 100 | */ 101 | Red = 5; 102 | /* 103 | * Maroon 104 | */ 105 | Maroon = 6; 106 | /* 107 | * Purple 108 | */ 109 | Purple = 7; 110 | /* 111 | * Dark Blue 112 | */ 113 | Dark_Blue = 8; 114 | /* 115 | * Blue 116 | */ 117 | Blue = 9; 118 | /* 119 | * Cyan 120 | */ 121 | Cyan = 10; 122 | /* 123 | * Teal 124 | */ 125 | Teal = 11; 126 | /* 127 | * Green 128 | */ 129 | Green = 12; 130 | /* 131 | * Dark Green 132 | */ 133 | Dark_Green = 13; 134 | /* 135 | * Brown 136 | */ 137 | Brown = 14; 138 | } 139 | 140 | /* 141 | * Role of the group member 142 | */ 143 | enum MemberRole { 144 | /* 145 | * Unspecifed 146 | */ 147 | Unspecifed = 0; 148 | /* 149 | * Team Member 150 | */ 151 | TeamMember = 1; 152 | /* 153 | * Team Lead 154 | */ 155 | TeamLead = 2; 156 | /* 157 | * Headquarters 158 | */ 159 | HQ = 3; 160 | /* 161 | * Airsoft enthusiast 162 | */ 163 | Sniper = 4; 164 | /* 165 | * Medic 166 | */ 167 | Medic = 5; 168 | /* 169 | * ForwardObserver 170 | */ 171 | ForwardObserver = 6; 172 | /* 173 | * Radio Telephone Operator 174 | */ 175 | RTO = 7; 176 | /* 177 | * Doggo 178 | */ 179 | K9 = 8; 180 | } 181 | 182 | /* 183 | * ATAK EUD Status 184 | * 185 | */ 186 | message Status { 187 | /* 188 | * Battery level 189 | */ 190 | uint32 battery = 1; 191 | } 192 | 193 | /* 194 | * ATAK Contact 195 | * 196 | */ 197 | message Contact { 198 | /* 199 | * Callsign 200 | */ 201 | bytes callsign = 1; 202 | 203 | /* 204 | * Device callsign 205 | */ 206 | bytes device_callsign = 2; 207 | /* 208 | * IP address of endpoint in integer form (0.0.0.0 default) 209 | */ 210 | // fixed32 enpoint_address = 3; 211 | /* 212 | * Port of endpoint (4242 default) 213 | */ 214 | // uint32 endpoint_port = 4; 215 | /* 216 | * Phone represented as integer 217 | * Terrible practice, but we really need the wire savings 218 | */ 219 | // uint32 phone = 4; 220 | } 221 | 222 | /* 223 | * Position Location Information from ATAK 224 | */ 225 | message PLI { 226 | /* 227 | * The new preferred location encoding, multiply by 1e-7 to get degrees 228 | * in floating point 229 | */ 230 | sfixed32 latitude_i = 1; 231 | 232 | /* 233 | * The new preferred location encoding, multiply by 1e-7 to get degrees 234 | * in floating point 235 | */ 236 | sfixed32 longitude_i = 2; 237 | 238 | /* 239 | * Altitude (ATAK prefers HAE) 240 | */ 241 | int32 altitude = 3; 242 | 243 | /* 244 | * Speed 245 | */ 246 | uint32 speed = 4; 247 | 248 | /* 249 | * Course in degrees 250 | */ 251 | uint32 course = 5; 252 | } 253 | -------------------------------------------------------------------------------- /opentakserver/sql_jobstore.py: -------------------------------------------------------------------------------- 1 | from apscheduler.jobstores.base import ConflictingIdError 2 | from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore 3 | 4 | 5 | class SQLJobStore(SQLAlchemyJobStore): 6 | def add_job(self, job): 7 | try: 8 | super().add_job(job) 9 | except ConflictingIdError: 10 | pass 11 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "OpenTAKServer" 3 | version = "0.0.0" 4 | description = "A server for ATAK, WinTAK, and iTAK" 5 | authors = ["OpenTAKServer "] 6 | readme = "README.md" 7 | license = "GPL-3.0-or-later" 8 | repository = "https://github.com/brian7704/OpenTAKServer" 9 | documentation = "https://docs.opentakserver.io" 10 | 11 | [tool.poetry.dependencies] 12 | adsbxcot = "6.0.4" 13 | androguard = "4.1.2" 14 | aiscot = "5.2.1" 15 | beautifulsoup4 = "4.12.3" 16 | colorlog = "6.9.0" 17 | datetime = "5.5" 18 | python-ffmpeg = "2.0.12" 19 | flask = "3.1.0" 20 | flask-apscheduler = "1.13.1" 21 | flask-cors = "5.0.0" 22 | flask-migrate = "4.0.7" 23 | Flask-Security-Too = {version="5.5.2", extras=["common", "mfa"]} 24 | flask-socketio = "5.4.1" 25 | flask-sqlalchemy = "3.1.1" 26 | eventlet = "0.40.0" 27 | gevent = "24.11.1" 28 | lastversion = "*" 29 | lxml = "5.3.0" 30 | meshtastic = "2.5.4" 31 | pika = "1.3.2" 32 | poetry-dynamic-versioning = {version = "1.4.1", extras = ["plugin"]} 33 | protobuf = "5.28.3" 34 | psutil = "6.1.0" 35 | pyfiglet = "1.0.2" 36 | pygc = "1.3.0" 37 | PyJWT = "2.9.0" 38 | pytak = "6.3.2" 39 | pytest = "8.3.3" 40 | pytest-cov = "6.0.0" 41 | python = ">=3.10" 42 | python-socketio = {extras = ["client", "websocket_client", "asyncio_client"], version = "5.11.4"} 43 | pyOpenSSL = "24.2.1" 44 | pyotp = "2.9.0" 45 | PyYAML = "6.0.2" 46 | simplekml = "1.3.6" 47 | sqlalchemy = "2.0.36" 48 | sqlalchemy-utils = "0.41.2" 49 | tldextract = "5.1.2" 50 | unishox2-py3 = "1.0.0" 51 | 52 | [tool.poetry-dynamic-versioning] 53 | enable = true 54 | vcs = "git" 55 | style = "semver" 56 | dirty = false 57 | pattern = "((?P\\d+)!)?(?P\\d+(\\.\\d+)*)" 58 | 59 | [tool.poetry-dynamic-versioning.files."opentakserver/__init__.py"] 60 | persistent-substitution = true 61 | initial-content = """ 62 | # These version placeholders will be replaced later during substitution. 63 | __version__ = "0.0.0" 64 | __version_tuple__ = (0, 0, 0) 65 | """ 66 | 67 | [build-system] 68 | requires = ["poetry-core>=1.0.0", "poetry-dynamic-versioning>=1.0.0,<2.0.0"] 69 | build-backend = "poetry_dynamic_versioning.backend" 70 | 71 | [tool.poetry.scripts] 72 | opentakserver = "opentakserver.app:main" -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brian7704/OpenTAKServer/96b8d7d6f683f6b933bbe1f6ad4f5168792c84ba/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import sqlalchemy 3 | from flask_security import hash_password 4 | 5 | from opentakserver.app import create_app 6 | from opentakserver.extensions import logger, db 7 | 8 | 9 | class AuthActions: 10 | def __init__(self, app, client, username='TestUser', password='TestPass'): 11 | self.app = app 12 | self.client = client 13 | self.username = username 14 | self.password = password 15 | self.headers = {} 16 | self.create() 17 | self.login() 18 | 19 | def create(self): 20 | try: 21 | with self.client.application.app_context(): 22 | self.app.security.datastore.create_user(username=self.username, 23 | password=hash_password(self.password), 24 | roles=["administrator"]) 25 | db.session.commit() 26 | except sqlalchemy.exc.IntegrityError: 27 | logger.warning("{} already exists".format(self.username)) 28 | 29 | def login(self): 30 | response = self.get('/api/login') 31 | csrf_token = response.json['response']['csrf_token'] 32 | self.headers['X-CSRFToken'] = csrf_token 33 | response = self.client.post( 34 | '/api/login', 35 | json={'username': self.username, 'password': self.password} 36 | ) 37 | 38 | return response 39 | 40 | def logout(self): 41 | return self.client.get('/api/logout') 42 | 43 | def get(self, path, headers=None, json=None): 44 | if json is None: 45 | json = {} 46 | 47 | if headers is None: 48 | headers = self.headers 49 | return self.client.get(path, headers=headers, json=json) 50 | 51 | def post(self, path, headers=None): 52 | if headers is None: 53 | headers = self.headers 54 | return self.client.post(path, headers=headers) 55 | 56 | 57 | @pytest.fixture 58 | def app(): 59 | app = create_app() 60 | app.config["TESTING"] = True 61 | app.config['PRESERVE_CONTEXT_ON_EXCEPTION'] = False 62 | return app 63 | 64 | 65 | @pytest.fixture() 66 | def client(app): 67 | with app.test_client() as client: 68 | with client.session_transaction() as session: 69 | session['Authorization'] = 'redacted' 70 | print(session) # will be populated SecureCookieSession 71 | yield client 72 | 73 | 74 | @pytest.fixture 75 | def auth(app, client): 76 | return AuthActions(app, client) 77 | -------------------------------------------------------------------------------- /tests/tests.py: -------------------------------------------------------------------------------- 1 | import base64 2 | 3 | 4 | def test_marti_api_clientendpoints(client): 5 | response = client.get('/Marti/api/clientEndPoints') 6 | assert response.status_code == 200 7 | 8 | 9 | def test_mart_api_tls_config(auth): 10 | creds = base64.b64encode(b'TestUser:TestPass').decode('utf-8') 11 | response = auth.get('/Marti/api/tls/config', headers={'Authorization': 'Basic {}'.format(creds)}) 12 | assert response.status_code == 200 13 | 14 | 15 | def test_points(auth): 16 | response = auth.get('/api/point') 17 | assert response.status_code == 200 18 | 19 | 20 | def test_me(auth): 21 | response = auth.get('/api/me') 22 | assert response.json['username'] == 'TestUser' 23 | --------------------------------------------------------------------------------