import aiohttp
import asyncio
import json
import sys
from typing import Dict, List, Union
from time import time
import base64


async def json_print(thing):
    print(json.dumps(thing, indent=2, default=lambda o: o.to_json()))


async def obj_to_json(thing):
    return json.loads(json.dumps(thing, default=lambda o: o.to_json()))


class APIToken:
    def __init__(
        self,
        token_type: str = None,
        token_value: str = None,
        creation_time: str = None,
        active: bool = None,
        id: int = None,
        operator: Union["Operator", str] = None,
    ):
        self._token_type = token_type
        self._token_value = token_value
        self._creation_time = creation_time
        self._active = active
        self._id = id
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, APIToken):
            return self._token_value == other.token_value
        return False

    @property
    def token_type(self) -> str:
        return self._token_type

    @token_type.setter
    def token_type(self, token_type):
        self._token_type = token_type

    @property
    def token_value(self) -> str:
        return self._token_value

    @token_value.setter
    def token_value(self, token_value):
        self._token_value = token_value

    @property
    def creation_time(self) -> str:
        return self._creation_time

    @creation_time.setter
    def creation_time(self, creation_time):
        self._creation_time = creation_time

    @property
    def active(self) -> bool:
        return self._active

    @active.setter
    def active(self, active):
        self._active = active

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def operator(self) -> "Operator":
        return self._operator

    @operator.setter
    def operator(self, operator):
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)


class Operation:
    def __init__(
        self,
        name: str = None,
        admin: Union["Operator", str] = None,
        complete: bool = None,
        AESPSK: str = None,
        webhook: str = None,
        id: int = None,
        members: List[Union["Operator", Dict[str, str], str]] = None,
    ):
        self._name = name
        if isinstance(admin, Operator) or admin is None:
            self._admin = admin
        else:
            self._admin = Operator(username=admin)
        self._complete = complete
        self._AESPSK = AESPSK
        self._webhook = webhook
        self._id = id
        if members is not None:
            if isinstance(members, list):
                self._members = [
                    Operator(username=x) if isinstance(x, str) else Operator(**x) if isinstance(x, Dict) else x for x in members
                ]
            else:
                raise ValueError("members must be a list")
        else:
            self._members = members

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Operation):
            return self._name == other.name or (
                self._id is not None and other.id is not None and self._id == other.id
            )
        return False

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

    @property
    def admin(self) -> "Operator":
        return self._admin

    @admin.setter
    def admin(self, admin):
        if isinstance(admin, Operator) or admin is None:
            self._admin = admin
        else:
            self._admin = Operator(username=admin)

    @property
    def complete(self) -> bool:
        return self._complete

    @complete.setter
    def complete(self, complete):
        self._complete = complete

    @property
    def AESPSK(self) -> str:
        return self._AESPSK

    @AESPSK.setter
    def AESPSK(self, AESPSK):
        self._AESPSK = AESPSK

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def webhook(self) -> str:
        return self._webhook

    @webhook.setter
    def webhook(self, webhook):
        self._webhook = webhook

    @property
    def members(self) -> List["Operator"]:
        return self._members

    @members.setter
    def members(self, members):
        if members is not None:
            if isinstance(members, list):
                self._members = [
                    Operator(username=x) if isinstance(x, str) else Operator(**x) if isinstance(x, Dict) else x for x in members
                ]
            else:
                raise ValueError("members must be a list")
        else:
            self._members = members


class Operator:
    def __init__(
        self,
        username: str = None,
        password: str = None,
        admin: bool = None,
        creation_time: str = None,
        last_login: str = None,
        active: bool = None,
        current_operation: Union[Operation, str] = None,
        current_operation_id: int = None,
        ui_config: str = None,
        id: int = None,
        view_utc_time: bool = None,
        deleted: bool = None,
        view_mode: str = None,
        base_disabled_commands: str = None,
    ):
        self._username = username
        self._admin = admin
        self._creation_time = creation_time
        self._last_login = last_login
        self._active = active
        if isinstance(current_operation, Operation) or current_operation is None:
            self._current_operation = current_operation
        else:
            self._current_operation = Operation(name=current_operation)
        self._ui_config = ui_config
        self._id = id
        self._password = password
        self._view_utc_time = view_utc_time
        self._deleted = deleted
        if self._current_operation is not None:
            self._current_operation.id = current_operation_id
        if view_mode in ["spectator", "operator", "developer", None]:
            self._view_mode = view_mode
        else:
            raise Exception("Bad value for view_mode")
        self._base_disabled_commands = base_disabled_commands

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Operator):
            return self._username == other.username or (
                self._id is not None and other.id is not None and self._id == other.id
            )
        return False

    @property
    def username(self) -> str:
        return self._username

    @username.setter
    def username(self, username):
        self._username = username

    @property
    def admin(self) -> bool:
        return self._admin

    @admin.setter
    def admin(self, admin):
        self._admin = admin

    @property
    def creation_time(self) -> str:
        return self._creation_time

    @creation_time.setter
    def creation_time(self, creation_time):
        self._creation_time = creation_time

    @property
    def last_login(self) -> str:
        return self._last_login

    @last_login.setter
    def last_login(self, last_login):
        self._last_login = last_login

    @property
    def active(self) -> bool:
        return self._active

    @active.setter
    def active(self, active):
        self._active = active

    @property
    def current_operation(self) -> Operation:
        return self._current_operation

    @current_operation.setter
    def current_operation(self, current_operation):
        if isinstance(current_operation, Operation) or current_operation is None:
            self._current_operation = current_operation
        else:
            self._current_operation = Operation(name=current_operation)

    @property
    def ui_config(self) -> str:
        return self._ui_config

    @ui_config.setter
    def ui_config(self, ui_config):
        self._ui_config = ui_config

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def password(self) -> str:
        return self._password

    @password.setter
    def password(self, password):
        self._password = password

    @property
    def view_utc_time(self) -> bool:
        return self._view_utc_time

    @view_utc_time.setter
    def view_utc_time(self, view_utc_time):
        self._view_utc_time = view_utc_time

    @property
    def deleted(self) -> bool:
        return self._deleted

    @deleted.setter
    def deleted(self, deleted):
        self._deleted = deleted

    @property
    def view_mode(self) -> str:
        return self._view_mode

    @view_mode.setter
    def view_mode(self, view_mode):
        if view_mode in ["spectator", "operator", "developer", None]:
            self._view_mode = view_mode
        else:
            raise Exception("Bad value for view_mode")

    @property
    def base_disabled_commands(self) -> str:
        return self._base_disabled_commands

    @base_disabled_commands.setter
    def base_disabled_commands(self, base_disabled_commands):
        self._base_disabled_commands = base_disabled_commands


class PayloadType:
    def __init__(
        self,
        ptype: str = None,
        creation_time: str = None,
        file_extension: str = None,
        wrapper: bool = None,
        wrapped: Union["PayloadType", str] = None,
        supported_os: str = None,
        last_heartbeat: str = None,
        container_running: bool = None,
        service: str = None,
        author: str = None,
        note: str = None,
        supports_dynamic_loading: bool = None,
        deleted: bool = None,
        build_parameters: List[Dict] = None,
        id: int = None,
        c2_profiles: List[Union["C2Profile", Dict]] = None,
        commands: List[Union["Command", str, Dict]] = None,
    ):
        self._ptype = ptype
        self._creation_time = creation_time
        self._file_extension = file_extension
        self._wrapper = wrapper
        if isinstance(wrapped, PayloadType) or wrapped is None:
            self._wrapped = wrapped
        else:
            self._wrapped_ = PayloadType(ptype=wrapped)
        self._supported_os = supported_os
        self._last_heartbeat = last_heartbeat
        self._container_running = container_running
        self._service = service
        self._id = id
        self._author = author
        self._note = note
        self._build_parameters = build_parameters
        self._supports_dynamic_loading = supports_dynamic_loading
        self._deleted = deleted
        if isinstance(c2_profiles, List):
            self._c2_profiles = [
                C2Profile(**x) if isinstance(x, Dict) else x for x in c2_profiles
            ]
        else:
            self._c2_profiles = c2_profiles
        if isinstance(commands, List):
            self._commands = [
                Command(**x)
                if isinstance(x, Dict)
                else Command(cmd=x)
                if isinstance(x, str)
                else x
                for x in commands
            ]
        else:
            self._commands = commands

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, PayloadType):
            return self._ptype == other.ptype or (
                self._id is not None and other.id is not None and self._id == other.id
            )
        return False

    @property
    def ptype(self) -> str:
        return self._ptype

    @ptype.setter
    def ptype(self, ptype):
        self._ptype = ptype

    @property
    def operator(self) -> Operator:
        return self._operator

    @operator.setter
    def operator(self, operator):
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)

    @property
    def creation_time(self) -> str:
        return self._creation_time

    @creation_time.setter
    def creation_time(self, creation_time):
        self._creation_time = creation_time

    @property
    def file_extension(self) -> str:
        return self._file_extension

    @file_extension.setter
    def file_extension(self, file_extension):
        self._file_extension = file_extension

    @property
    def wrapper(self) -> bool:
        return self._wrapper

    @wrapper.setter
    def wrapper(self, wrapper):
        self._wrapper = wrapper

    @property
    def wrapped(self) -> "PayloadType":
        return self._wrapped

    @wrapped.setter
    def wrapped(self, wrapped):
        if isinstance(wrapped, PayloadType) or wrapped is None:
            self._wrapped = wrapped
        else:
            self._wrapped_ = PayloadType(ptype=wrapped)

    @property
    def supported_os(self) -> str:
        return self._supported_os

    @supported_os.setter
    def supported_os(self, supported_os):
        self._supported_os = supported_os

    @property
    def last_heartbeat(self) -> str:
        return self._last_heartbeat

    @last_heartbeat.setter
    def last_heartbeat(self, last_heartbeat):
        self._last_heartbeat = last_heartbeat

    @property
    def container_running(self) -> bool:
        return self._container_running

    @container_running.setter
    def container_running(self, container_running):
        self._container_running = container_running

    @property
    def service(self) -> str:
        return self._service

    @service.setter
    def service(self, service):
        self._service = service

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def author(self) -> str:
        return self._author

    @author.setter
    def author(self, author):
        self._author = author

    @property
    def note(self) -> str:
        return self._note

    @note.setter
    def note(self, note):
        self._note = note

    @property
    def supports_dynamic_loading(self) -> bool:
        return self._supports_dynamic_loading

    @supports_dynamic_loading.setter
    def supports_dynamic_loading(self, supports_dynamic_loading):
        self._supports_dynamic_loading = supports_dynamic_loading

    @property
    def deleted(self) -> bool:
        return self._deleted

    @deleted.setter
    def deleted(self, deleted):
        self._deleted = deleted

    @property
    def build_parameters(self) -> List[Dict]:
        return self._build_parameters

    @build_parameters.setter
    def build_parameters(self, build_parameters):
        self._build_parameters = build_parameters

    @property
    def c2_profiles(self) -> List["C2Profile"]:
        return self._c2_profiles

    @c2_profiles.setter
    def c2_profiles(self, c2_profiles):
        if isinstance(c2_profiles, List):
            self._c2_profiles = [
                C2Profile(**x) if isinstance(x, Dict) else x for x in c2_profiles
            ]
        else:
            self._c2_profiles = c2_profiles

    @property
    def commands(self) -> List["Command"]:
        return self._commands

    @commands.setter
    def commands(self, commands):
        if isinstance(commands, List):
            self._commands = [
                Command(**x)
                if isinstance(x, Dict)
                else Command(cmd=x)
                if isinstance(x, str)
                else x
                for x in commands
            ]
        else:
            self._commands = commands


class Command:
    def __init__(
        self,
        needs_admin: bool = None,
        help_cmd: str = None,
        description: str = None,
        cmd: str = None,
        payload_type: Union[PayloadType, str] = None,
        creation_time: str = None,
        version: int = None,
        is_exit: bool = None,
        is_file_browse: bool = None,
        is_process_list: bool = None,
        is_download_file: bool = None,
        is_remove_file: bool = None,
        is_upload_file: bool = None,
        author: str = None,
        mythic_version: int = None,
        deleted: bool = None,
        id: int = None,
        params: List[Union["CommandParameters", Dict[str, str]]] = None,
    ):
        self._needs_admin = needs_admin
        self._help_cmd = help_cmd
        self._description = description
        self._cmd = cmd
        if isinstance(payload_type, PayloadType) or payload_type is None:
            self._payload_type = payload_type
        else:
            self._payload_type = PayloadType(ptype=payload_type)
        self._creation_time = creation_time
        self._version = version
        self._is_exit = is_exit
        self._is_file_browse = is_file_browse
        self._is_process_list = is_process_list
        self._is_download_file = is_download_file
        self._is_remove_file = is_remove_file
        self._is_upload_file = is_upload_file
        self._author = author
        self._delted = deleted
        self._mythic_version = mythic_version
        self._id = id
        if params is not None and params != []:
            if isinstance(params, list):
                self._params = [
                    CommandParameters(**x) if isinstance(x, Dict) else x for x in params
                ]
            else:
                raise ValueError("params must be a list")
        else:
            self._params = None

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Command):
            return (
                self._cmd == other.cmd
                and self._payload_type.ptype == other.payload_type.ptype
            ) or (
                self._id is not None and other.id is not None and self._id == other.id
            )
        return False

    @property
    def needs_admin(self) -> bool:
        return self._needs_admin

    @needs_admin.setter
    def needs_admin(self, needs_admin):
        self._needs_admin = needs_admin

    @property
    def help_cmd(self) -> str:
        return self._help_cmd

    @help_cmd.setter
    def help_cmd(self, help_cmd):
        self._help_cmd = help_cmd

    @property
    def description(self) -> str:
        return self._description

    @description.setter
    def description(self, description):
        self._description = description

    @property
    def cmd(self) -> str:
        return self._cmd

    @cmd.setter
    def cmd(self, cmd):
        self._cmd = cmd

    @property
    def payload_type(self) -> PayloadType:
        return self._payload_type

    @payload_type.setter
    def payload_type(self, payload_type):
        if isinstance(payload_type, PayloadType) or payload_type is None:
            self._payload_type = payload_type
        else:
            self._payload_type = PayloadType(ptype=payload_type)

    @property
    def creation_time(self) -> str:
        return self._creation_time

    @creation_time.setter
    def creation_time(self, creation_time):
        self._creation_time = creation_time

    @property
    def version(self) -> int:
        return self._version

    @version.setter
    def version(self, version):
        self._version = version

    @property
    def is_exit(self) -> bool:
        return self._is_exit

    @is_exit.setter
    def is_exit(self, is_exit):
        self._is_exit = is_exit

    @property
    def is_file_browse(self) -> bool:
        return self._is_file_browse

    @is_file_browse.setter
    def is_file_browse(self, is_file_browse):
        self._is_file_browse = is_file_browse

    @property
    def is_process_list(self) -> bool:
        return self._is_process_list

    @is_process_list.setter
    def is_process_list(self, is_process_list):
        self._is_process_list = is_process_list

    @property
    def is_download_file(self) -> bool:
        return self._is_download_file

    @is_download_file.setter
    def is_download_file(self, is_download_file):
        self._is_download_file = is_download_file

    @property
    def is_remove_file(self) -> bool:
        return self._is_remove_file

    @is_remove_file.setter
    def is_remove_file(self, is_remove_file):
        self._is_remove_file = is_remove_file

    @property
    def is_upload_file(self) -> bool:
        return self._is_upload_file

    @is_upload_file.setter
    def is_upload_file(self, is_upload_file):
        self._is_upload_file = is_upload_file

    @property
    def deleted(self) -> bool:
        return self._deleted

    @deleted.setter
    def deleted(self, deleted):
        self._deleted = deleted

    @property
    def author(self) -> str:
        return self._author

    @author.setter
    def author(self, author):
        self._author = author

    @property
    def mythic_version(self) -> int:
        return self._mythic_version

    @mythic_version.setter
    def mythic_version(self, mythic_version):
        self._mythic_version = mythic_version

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def params(self) -> List["CommandParameters"]:
        return self._params

    @params.setter
    def params(self, params):
        if isinstance(params, list):
            self._params = [
                CommandParameters(**x) if isinstance(x, Dict) else x for x in params
            ]
        elif params is None or params == []:
            self._params = None
        else:
            raise ValueError("params must be a list")


class CommandParameters:
    def __init__(
        self,
        command: Union[
            Command, int
        ] = None,  # database ID for the corresponding command
        cmd: str = None,  # cmd string the command refers to (like shell)
        payload_type: Union[PayloadType, str] = None,
        name: str = None,
        type: str = None,
        default_value: str = None,
        description: str = None,
        supported_agents: str = None,
        choices: Union[List[str], str] = None,
        required: bool = None,
        id: int = None,
    ):
        if isinstance(command, Command) or command is None:
            self._command = command
        else:
            self._command = Command(id=command)
        self._cmd = cmd
        if isinstance(payload_type, PayloadType) or payload_type is None:
            self._payload_type = payload_type
        else:
            self._payload_type = PayloadType(ptype=payload_type)
        self._name = name
        self._type = type
        self._description = description
        self._supported_agents = supported_agents
        self._default_value = default_value
        if isinstance(choices, List) or choices is None:
            self._choices = choices
        else:
            self._choices = choices.split("\n")
        self._required = required
        self._id = id

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, CommandParameters):
            return (
                self._name == other.name
                and (self._command == other.command)
                or (self._cmd == other.cmd)
            ) or (
                self._id is not None and other.id is not None and self._id == other.id
            )
        return False

    @property
    def command(self) -> Command:
        return self._command

    @command.setter
    def command(self, command):
        if isinstance(command, Command) or command is None:
            self._command = command
        else:
            self._command = Command(id=command)

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

    @property
    def type(self) -> str:
        return self._type

    @type.setter
    def type(self, type):
        self._type = type

    @property
    def description(self) -> str:
        return self._description

    @description.setter
    def description(self, description):
        self._description = description

    @property
    def supported_agents(self) -> str:
        return self._supported_agents

    @supported_agents.setter
    def supported_agents(self, supported_agents):
        self._supported_agents = supported_agents

    @property
    def default_value(self) -> str:
        return self._default_value

    @default_value.setter
    def default_value(self, default_value):
        self._default_value = default_value

    @property
    def choices(self) -> List[str]:
        return self._choices

    @choices.setter
    def choices(self, choices):
        if isinstance(choices, List) or choices is None:
            self._choices = choices
        else:
            self._choices = choices.split("\n")

    @property
    def required(self) -> bool:
        return self._required

    @required.setter
    def required(self, required):
        self._required = required

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def cmd(self) -> str:
        return self._cmd

    @cmd.setter
    def cmd(self, cmd):
        self._cmd = cmd

    @property
    def payload_type(self) -> PayloadType:
        return self._payload_type

    @payload_type.setter
    def payload_type(self, payload_type):
        if isinstance(payload_type, PayloadType) or payload_type is None:
            self._payload_type = payload_type
        else:
            self._payload_type = PayloadType(ptype=payload_type)


class C2Profile:
    def __init__(
        self,
        name: str = None,
        description: str = None,
        creation_time: str = None,
        running: bool = None,
        last_heartbeat: str = None,
        container_running: bool = None,
        author: str = None,
        is_p2p: bool = None,
        is_server_routed: bool = None,
        mythic_encrypts: bool = None,
        deleted: bool = None,
        id: int = None,
        ptype: List[Union[PayloadType, str]] = None,
        parameters: Dict = None,
    ):  # list of payload types that support this c2 profile
        self._name = name
        self._description = description
        self._creation_time = creation_time
        self._running = running
        self._last_heartbeat = last_heartbeat
        self._container_running = container_running
        self._id = id
        self._author = author
        self._is_p2p = is_p2p
        self._is_server_routed = is_server_routed
        self._mythic_encrypts = mythic_encrypts
        self._deleted = deleted
        if ptype is not None:
            if isinstance(ptype, list):
                self._ptype = [
                    PayloadType(ptype=x) if isinstance(x, str) else x for x in ptype
                ]
            else:
                raise ValueError("ptype must be a list")
        else:
            self._ptype = ptype
        self._parameters = parameters

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, C2Profile):
            return self._name == other.name or (
                self._id is not None and other.id is not None and self._id == other.id
            )
        return False

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

    @property
    def description(self) -> str:
        return self._description

    @description.setter
    def description(self, description):
        self._description = description

    @property
    def creation_time(self) -> str:
        return self._creation_time

    @creation_time.setter
    def creation_time(self, creation_time):
        self._creation_time = creation_time

    @property
    def running(self) -> bool:
        return self._running

    @running.setter
    def running(self, running):
        self._running = running

    @property
    def last_heartbeat(self) -> str:
        return self._last_heartbeat

    @last_heartbeat.setter
    def last_heartbeat(self, last_heartbeat):
        self._last_heartbeat = last_heartbeat

    @property
    def container_running(self) -> bool:
        return self._container_running

    @container_running.setter
    def container_running(self, container_running):
        self._container_running = container_running

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def ptype(self) -> List[PayloadType]:
        return self._ptype

    @ptype.setter
    def ptype(self, ptype):
        if isinstance(ptype, list):
            self._ptype = [
                PayloadType(ptype=x) if isinstance(x, str) else x for x in ptype
            ]
        elif ptype is None:
            self._ptype = ptype
        else:
            raise ValueError("ptype must be a list")

    @property
    def author(self) -> str:
        return self._author

    @author.setter
    def author(self, author):
        self._author = author

    @property
    def is_p2p(self) -> bool:
        return self._is_p2p

    @is_p2p.setter
    def is_p2p(self, is_p2p):
        self._is_p2p = is_p2p

    @property
    def is_server_routed(self) -> bool:
        return self._iis_server_routed

    @is_server_routed.setter
    def is_server_routed(self, is_server_routed):
        self._is_server_routed = is_server_routed

    @property
    def mythic_encrypts(self) -> bool:
        return self._mythic_encrypts

    @mythic_encrypts.setter
    def is_server_routed(self, mythic_encrypts):
        self._mythic_encrypts = mythic_encrypts

    @property
    def deleted(self) -> bool:
        return self._deleted

    @deleted.setter
    def deleted(self, deleted):
        self._deleted = deleted
    @property
    def parameters(self) -> Dict:
        return self._parameters

    @parameters.setter
    def parameters(self, parameters):
        self._parameters = parameters


class C2ProfileParameters:
    """
    This class combines C2ProfileParameters and C2ProfileParametersInstance
    """

    def __init__(
        self,
        c2_profile: Union[C2Profile, str] = None,
        name: str = None,
        default_value: any = None,
        required: bool = None,
        verifier_regex: str = None,
        randomize: bool = None,
        parameter_type: str = None,
        description: str = None,
        id: int = None,
        value: any = None,
        instance_name: str = None,
        operation: Union[Operation, str] = None,
        callback: Union["Callback", int] = None,
        payload: Union["Payload", str] = None,
    ):
        if isinstance(c2_profile, C2Profile) or c2_profile is None:
            self._c2_profile = c2_profile
        else:
            self._c2_profile = C2Profile(name=c2_profile)
        self._name = name
        self._default_value = default_value
        self._required = required
        self._verifier_regex = verifier_regex
        self._parameter_type = parameter_type
        self._description = description
        self._instance_name = instance_name
        self._value = value
        self._randomize = randomize
        self._id = id
        if isinstance(payload, Payload) or payload is None:
            self._payload = payload
        else:
            self._payload = Payload(uuid=payload)
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)
        if isinstance(callback, Callback) or callback is None:
            self._callback = callback
        else:
            self._callback = Callback(id=callback)

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, C2ProfileParameters):
            return self._name == other.name and self._c2_profile == other.c2_profile
        return False

    @property
    def c2_profile(self) -> C2Profile:
        return self._c2_profile

    @c2_profile.setter
    def c2_profile(self, c2_profile):
        if isinstance(c2_profile, C2Profile) or c2_profile is None:
            self._c2_profile = c2_profile
        else:
            self._c2_profile = C2Profile(name=c2_profile)

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

    @property
    def verifier_regex(self) -> str:
        return self._verifier_regex

    @verifier_regex.setter
    def verifier_regex(self, verifier_regex):
        self._verifier_regex = verifier_regex

    @property
    def parameter_type(self) -> str:
        return self._parameter_type

    @parameter_type.setter
    def parameter_type(self, parameter_type):
        self._parameter_type = parameter_type

    @property
    def description(self) -> str:
        return self._description

    @description.setter
    def description(self, description):
        self._description = description

    @property
    def instance_name(self) -> str:
        return self._instance_name

    @instance_name.setter
    def instance_name(self, instance_name):
        self._instance_name = instance_name

    @property
    def default_value(self) -> any:
        return self._default_value

    @default_value.setter
    def default_value(self, default_value):
        self._default_value = default_value

    @property
    def required(self) -> bool:
        return self._required

    @required.setter
    def required(self, required):
        self._required = required

    @property
    def randomize(self) -> bool:
        return self._randomize

    @randomize.setter
    def randomize(self, randomize):
        self._randomize = randomize

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def value(self) -> any:
        return self._value

    @value.setter
    def value(self, value):
        self._value = value

    @property
    def payload(self) -> "Payload":
        return self._payload

    @payload.setter
    def payload(self, payload):
        if isinstance(payload, Payload) or payload is None:
            self._payload = payload
        else:
            self._payload = Payload(uuid=payload)

    @property
    def operation(self) -> Operation:
        return self._operation

    @operation.setter
    def operation(self, operation):
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)

    @property
    def callback(self) -> "Callback":
        return self._callback

    @callback.setter
    def callback(self, callback):
        if isinstance(callback, Callback) or callback is None:
            self._callback = callback
        else:
            self._callback = Callback(id=callback)


class Callback:
    def __init__(
        self,
        init_callback: str = None,
        last_checkin: str = None,
        user: str = None,
        host: str = None,
        pid: int = None,
        ip: str = None,
        os: str = None,
        domain: str = None,
        architecture: str = None,
        description: str = None,
        operator: Union[Operator, str] = None,
        active: bool = None,
        port: int = None,
        socks_task: int = None,
        pcallback: Union["Callback", int] = None,
        registered_payload: str = None,  # corresponding payload's UUID
        payload_type: Union[PayloadType, str] = None,  # corresponding payload's type
        c2_profile: Union[C2Profile, str] = None,  # corresponding payload's c2 profile
        payload_description: str = None,  # corresponding payload's description
        integrity_level: int = None,
        operation: Union[Operation, str] = None,
        encryption_type: str = None,
        decryption_key: str = None,
        encryption_key: str = None,
        locked: bool = None,
        locked_operator: str = None,
        tasks: List[Union["Task", Dict]] = None,
        id: int = None,
        agent_callback_id: str = None,
        extra_info: str = None,
        sleep_info: str = None,
        external_ip: str = None,
        payload_type_id: int = None,
        supported_profiles: List[Union[C2Profile, Dict]] = None,
    ):
        self._init_callback = init_callback
        self._last_checkin = last_checkin
        self._user = user
        self._host = host
        self._pid = pid
        self._ip = ip
        self._port = port
        self._socks_task = socks_task
        self._domain = domain
        self._description = description
        self._agent_callback_id = agent_callback_id
        self._external_ip = external_ip
        self._payload_type_id = payload_type_id
        self._locked_operator = locked_operator
        self._os = os
        self._architecture = architecture
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)
        self._active = active
        if isinstance(pcallback, Callback) or pcallback is None:
            self._pcallback = pcallback
        elif pcallback == "null":
            self._pcallback = None
        else:
            self._pcallback = Callback(id=pcallback)
        if registered_payload is None:
            self._registered_payload = registered_payload
        else:
            self._registered_payload = Payload(uuid=registered_payload)
        if isinstance(payload_type, PayloadType) or payload_type is None:
            self._payload_type = payload_type
        else:
            self._payload_type = PayloadType(ptype=payload_type)
        if isinstance(c2_profile, C2Profile) or c2_profile is None:
            self._c2_profile = c2_profile
        else:
            self._c2_profile = C2Profile(name=c2_profile)
        self._payload_description = payload_description
        self._integrity_level = integrity_level
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)
        self._encryption_type = encryption_type
        self._decryption_key = decryption_key
        self._encryption_key = encryption_key
        if isinstance(tasks, List):
            self._tasks = [Task(**x) if isinstance(x, Dict) else x for x in tasks]
        elif tasks is None:
            self._tasks = tasks
        else:
            self._tasks = [Task(**tasks) if isinstance(tasks, Dict) else tasks]
        self._id = id
        if supported_profiles is None:
            self._supported_profiles = supported_profiles
        else:
            self._supported_profiles = [x if isinstance(x, C2Profile) else C2Profile(**x) for x in supported_profiles]

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Callback):
            return self._id == other.id
        return False

    @property
    def init_callback(self) -> str:
        return self._init_callback

    @init_callback.setter
    def init_callback(self, init_callback):
        self._init_callback = init_callback

    @property
    def last_checkin(self) -> str:
        return self._last_checkin

    @last_checkin.setter
    def last_checkin(self, last_checkin):
        self._last_checkin = last_checkin

    @property
    def user(self) -> str:
        return self._user

    @user.setter
    def user(self, user):
        self._user = user

    @property
    def host(self) -> str:
        return self._host

    @host.setter
    def host(self, host):
        self._host = host

    @property
    def pid(self) -> int:
        return self._pid

    @pid.setter
    def pid(self, pid):
        self._pid = pid

    @property
    def ip(self) -> str:
        return self._ip

    @ip.setter
    def ip(self, ip):
        self._ip = ip

    @property
    def description(self) -> str:
        return self._description

    @description.setter
    def description(self, description):
        self._description = description

    @property
    def operator(self) -> Operator:
        return self._operator

    @operator.setter
    def operator(self, operator):
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)

    @property
    def active(self) -> bool:
        return self._active

    @active.setter
    def active(self, active):
        self._active = active

    @property
    def pcallback(self) -> "Callback":
        return self._pcallback

    @pcallback.setter
    def pcallback(self, pcallback):
        if isinstance(pcallback, Callback) or pcallback is None:
            self._pcallback = pcallback
        elif pcallback == "null":
            self._pcallback = None
        else:
            self._pcallback = Callback(id=pcallback)

    @property
    def registered_payload(self) -> "Payload":
        return self._registered_payload

    @registered_payload.setter
    def registered_payload(self, registered_payload):
        if isinstance(registered_payload, Payload) or registered_payload is None:
            self._registered_payload = registered_payload
        else:
            self._registered_payload = Payload(uuid=registered_payload)

    @property
    def payload_type(self) -> PayloadType:
        return self._payload_type

    @payload_type.setter
    def payload_type(self, payload_type):
        if isinstance(payload_type, PayloadType) or payload_type is None:
            self._payload_type = payload_type
        else:
            self._payload_type = PayloadType(ptype=payload_type)

    @property
    def c2_profile(self) -> C2Profile:
        return self._c2_profile

    @c2_profile.setter
    def c2_profile(self, c2_profile):
        if isinstance(c2_profile, C2Profile) or c2_profile is None:
            self._c2_profile = c2_profile
        else:
            self._c2_profile = C2Profile(name=c2_profile)

    @property
    def payload_description(self) -> str:
        return self._payload_description

    @payload_description.setter
    def payload_description(self, payload_description):
        self._payload_description = payload_description

    @property
    def integrity_level(self) -> int:
        return self._integrity_level

    @integrity_level.setter
    def integrity_level(self, integrity_level):
        self._integrity_level = integrity_level

    @property
    def operation(self) -> Operation:
        return self._operation

    @operation.setter
    def operation(self, operation):
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)

    @property
    def encryption_type(self) -> str:
        return self._encryption_type

    @encryption_type.setter
    def encryption_type(self, encryption_type):
        self._encryption_type = encryption_type

    @property
    def decryption_key(self) -> str:
        return self._decryption_key

    @decryption_key.setter
    def decryption_key(self, decryption_key):
        self._decryption_key = decryption_key

    @property
    def encryption_key(self) -> str:
        return self._encryption_key

    @encryption_key.setter
    def encryption_key(self, encryption_key):
        self._encryption_key = encryption_key

    @property
    def tasks(self) -> List["Task"]:
        return self._tasks

    @tasks.setter
    def tasks(self, tasks):
        if isinstance(tasks, List):
            self._tasks = [Task(**x) if isinstance(x, Dict) else x for x in tasks]
        elif tasks is None:
            self._tasks = tasks
        else:
            self._tasks = [Task(**tasks) if isinstance(tasks, Dict) else tasks]

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def supported_profiles(self) -> List[C2Profile]:
        return self._supported_profiles

    @supported_profiles.setter
    def supported_profiles(self, supported_profiles):
        if supported_profiles is None:
            self._supported_profiles = supported_profiles
        else:
            self._supported_profiles = [x if isinstance(x, C2Profile) else C2Profile(**x) for x in supported_profiles]


class TaskFile:
    def __init__(self, content: Union[bytes, str], filename: str, param_name: str):
        self._filename = filename
        if isinstance(content, bytes):
            self._content = content
        else:
            self._content = base64.b64decode(content)
        self._param_name = param_name

    @property
    def filename(self):
        return self._filename

    @filename.setter
    def filename(self, filename):
        self._filename = filename

    @property
    def param_name(self):
        return self._param_name

    @param_name.setter
    def param_name(self, param_name):
        self._param_name = param_name

    @property
    def content(self):
        return self._content

    @content.setter
    def content(self, content):
        if isinstance(content, bytes):
            self._content = content
        else:
            self._content = base64.b64decode(content)

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())


class Task:
    def __init__(
        self,
        command: Union[Command, str] = None,
        agent_task_id: str = None,
        command_id: str = None,
        params: str = None,
        files: List[TaskFile] = None,
        timestamp: str = None,
        callback: Union[Callback, int, Dict] = None,
        operator: Union[Operator, str] = None,
        payload_type: str = None,
        status: str = None,
        task_status: str = None,  # sometimes this is set to not conflict with overall status message
        original_params: str = None,
        comment: str = None,
        comment_operator: Union[Operator, str] = None,
        completed: bool = None,
        id: int = None,
        status_timestamp_preprocessing: str = None,
        status_timestamp_processed: str = None,
        status_timestamp_submitted: str = None,
        status_timestamp_processing: str = None,
        operation: str = None,
        responses: List[Union["Response", Dict]] = None,
    ):
        if isinstance(command, Command) or command is None:
            self._command = command
        else:
            self._command = Command(cmd=command)
        self.params = params
        self.timestamp = timestamp
        self.agent_task_id = agent_task_id
        self.command_id = command_id
        self.status_timestamp_preprocessing = status_timestamp_preprocessing
        self.status_timestamp_processed = status_timestamp_processed
        self.status_timestamp_submitted = status_timestamp_submitted
        self.status_timestamp_processing = status_timestamp_processing
        self.operation = operation
        self.completed = completed
        self.payload_type = payload_type
        if isinstance(callback, Callback) or callback is None:
            self._callback = callback
        elif isinstance(callback, Dict):
            self._callback = Callback(**callback)
        else:
            self._callback = Callback(id=callback)
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)
        self.status = status
        self._original_params = original_params
        if comment == "":
            self._comment = None
        else:
            self._comment = comment
        if isinstance(comment_operator, Operator) or comment_operator is None:
            self._comment_operator = comment_operator
        elif comment_operator == "null":
            self._comment_operator = None
        else:
            self._comment_operator = Operator(username=comment_operator)
        self._id = id
        if isinstance(responses, List):
            self._responses = [
                Response(**x) if isinstance(x, Dict) else x for x in responses
            ]
        elif responses is None:
            self._responses = responses
        else:
            self._responses = [
                Response(**responses)
                if isinstance(responses, Dict)
                else Response(response=responses)
            ]
        if self._status is None:
            self._status = task_status
        if isinstance(files, List):
            self._files = files
        elif isinstance(files, TaskFile):
            self._files = [files]
        elif files is None:
            self._files = None
        else:
            raise Exception("Invalid value for files parameter")

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Task):
            return self._id == other.id
        return False

    @property
    def command(self) -> Command:
        return self._command

    @command.setter
    def command(self, command):
        if isinstance(command, Command) or command is None:
            self._command = command
        else:
            self._command = Command(cmd=command)

    @property
    def params(self) -> str:
        return self._params

    @params.setter
    def params(self, params):
        if params is None:
            self._params = ""
        else:
            self._params = params
    @property
    def payload_type(self) -> str:
        return self._payload_type

    @payload_type.setter
    def payload_type(self, payload_type):
        self._payload_type = payload_type

    @property
    def files(self) -> List[TaskFile]:
        return self._files

    @files.setter
    def files(self, files):
        if isinstance(files, List):
            self._files = files
        elif isinstance(files, TaskFile):
            self._files = [files]
        elif files is None:
            self._files = None
        else:
            raise Exception("Invalid value for files parameter")

    @property
    def timestamp(self) -> str:
        return self._timestamp

    @timestamp.setter
    def timestamp(self, timestamp):
        self._timestamp = timestamp

    @property
    def callback(self) -> Callback:
        return self._callback

    @callback.setter
    def callback(self, callback):
        if isinstance(callback, Callback):
            self._callback = callback
        else:
            self._callback = Callback(id=callback)

    @property
    def operator(self) -> Operator:
        return self._operator

    @operator.setter
    def operator(self, operator):
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)

    @property
    def status(self) -> str:
        return self._status

    @status.setter
    def status(self, status):
        self._status = status

    @property
    def original_params(self) -> str:
        return self._original_params

    @original_params.setter
    def original_params(self, original_params):
        self._original_params = original_params

    @property
    def comment(self) -> str:
        return self._comment

    @comment.setter
    def comment(self, comment):
        if comment == "":
            self._comment = None
        else:
            self._comment = comment

    @property
    def comment_operator(self) -> Operator:
        return self._comment_operator

    @comment_operator.setter
    def comment_operator(self, comment_operator):
        if isinstance(comment_operator, Operator) or comment_operator is None:
            self._comment_operator = comment_operator
        elif comment_operator == "null":
            self._comment_operator = None
        else:
            self._comment_operator = Operator(username=comment_operator)

    @property
    def responses(self) -> List["Response"]:
        return self._responses

    @responses.setter
    def responses(self, responses):
        if isinstance(responses, List):
            self._responses = [
                Response(**x) if isinstance(x, Dict) else x for x in responses
            ]
        elif responses is None:
            self._responses = responses
        else:
            self._responses = [
                Response(**responses)
                if isinstance(responses, Dict)
                else Response(response=responses)
            ]

    @property
    def id(self):
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def task_status(self) -> str:
        return self._status

    @task_status.setter
    def task_status(self, task_status):
        self._status = task_status

    @property
    def completed(self) -> bool:
        return self._completed

    @completed.setter
    def completed(self, completed):
        self._completed = completed
        
    @property
    def agent_task_id(self) -> str:
        return self._agent_task_id

    @agent_task_id.setter
    def agent_task_id(self, agent_task_id):
        self._agent_task_id = agent_task_id
        
    @property
    def command_id(self) -> int:
        return self._command_id

    @command_id.setter
    def command_id(self, command_id):
        self._command_id = command_id
        
    @property
    def status_timestamp_preprocessing(self) -> str:
        return self._status_timestamp_preprocessing

    @status_timestamp_preprocessing.setter
    def status_timestamp_preprocessing(self, status_timestamp_preprocessing):
        self._status_timestamp_preprocessing = status_timestamp_preprocessing
        
    @property
    def status_timestamp_submitted(self) -> str:
        return self._status_timestamp_submitted

    @status_timestamp_submitted.setter
    def status_timestamp_submitted(self, status_timestamp_submitted):
        self._status_timestamp_submitted = status_timestamp_submitted
        
    @property
    def status_timestamp_processed(self) -> str:
        return self._status_timestamp_processed

    @status_timestamp_processed.setter
    def status_timestamp_processed(self, status_timestamp_processed):
        self._status_timestamp_processed = status_timestamp_processed
        
    @property
    def status_timestamp_processing(self) -> str:
        return self._status_timestamp_processing

    @status_timestamp_processing.setter
    def status_timestamp_processing(self, status_timestamp_processing):
        self._status_timestamp_processing = status_timestamp_processing
        
    @property
    def operation(self) -> str:
        return self._operation

    @operation.setter
    def operation(self, operation):
        self._operation = operation


class Payload:
    def __init__(
        self,
        uuid: str = None,
        tag: str = None,
        operator: Union[Operator, str] = None,
        creation_time: str = None,
        payload_type: Union[PayloadType, str] = None,
        pcallback: Union["Callback", int] = None,
        c2_profiles: Dict[
            Union[C2Profile, str, Dict], List[Union[C2ProfileParameters, Dict]]
        ] = None,
        operation: Union[Operation, str] = None,
        wrapped_payload: Union["Payload", str] = None,
        deleted: bool = None,
        build_container: str = None,
        build_phase: str = None,
        build_message: str = None,
        callback_alert: bool = None,
        auto_generated: bool = None,
        task: Union[Task, Dict] = None,
        file_id: Union["FileMeta", Dict] = None,
        id: int = None,
        build_parameters: List[Dict] = None,
        commands: List = None,
        filename: str = None,
    ):
        self._uuid = uuid
        self._tag = tag
        self._build_container = build_container
        self._callback_alert = callback_alert
        self._auto_generated = auto_generated
        self._build_parameters = build_parameters
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)
        self._creation_time = creation_time
        if isinstance(payload_type, PayloadType) or payload_type is None:
            self._payload_type = payload_type
        else:
            self._payload_type = PayloadType(ptype=payload_type)
        if isinstance(pcallback, Callback) or pcallback is None:
            self._pcallback = pcallback
        else:
            self._pcallback = Callback(id=pcallback)
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)
        if isinstance(task, Task) or task is None:
            self._task = task
        else:
            self._task = Task(**task)
        if isinstance(file_id, FileMeta) or file_id is None:
            self._file_id = file_id
        else:
            self._file_id = FileMeta(**file_id)
        if isinstance(wrapped_payload, Payload) or wrapped_payload is None:
            self._wrapped_payload = wrapped_payload
        else:
            self._wrapped_payload = Payload(uuid=wrapped_payload)
        self._deleted = deleted
        self._build_phase = build_phase
        self._build_message = build_message
        self._id = id
        if isinstance(commands, List) and len(commands) > 0:
            if isinstance(commands[0], Command):
                self._commands = commands
            elif isinstance(commands[0], Dict):
                self._commands = [Command(**x) for x in commands]
            else:
                self._commands = [Command(cmd=x) for x in commands]
        else:
            self._commands = None
        if isinstance(c2_profiles, Dict):
            self._c2_profiles = {}
            for k, v in c2_profiles.items():
                key = (
                    k["name"]
                    if isinstance(k, Dict)
                    else k.name
                    if isinstance(k, C2Profile)
                    else k
                )
                self._c2_profiles[key] = []
                for i in v:
                    # now iterate over each list of parameters for the profile
                    if isinstance(i, C2ProfileParameters):
                        self._c2_profiles[key].append(i)
                    elif isinstance(i, Dict):
                        self._c2_profiles[key].append(C2ProfileParameters(**i))
        else:
            self._c2_profiles = None
        self._filename = filename

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Payload):
            return self._uuid == other.uuid
        return False

    @property
    def uuid(self) -> str:
        return self._uuid

    @uuid.setter
    def uuid(self, uuid):
        self._uuid = uuid

    @property
    def tag(self) -> str:
        return self._tag

    @tag.setter
    def tag(self, tag):
        self._tag = tag

    @property
    def operator(self) -> Operator:
        return self._operator

    @operator.setter
    def operator(self, operator):
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)

    @property
    def creation_time(self) -> str:
        return self._creation_time

    @creation_time.setter
    def creation_time(self, creation_time):
        self._creation_time = creation_time

    @property
    def payload_type(self) -> PayloadType:
        return self._payload_type

    @payload_type.setter
    def payload_type(self, payload_type):
        if isinstance(payload_type, PayloadType) or payload_type is None:
            self._payload_type = payload_type
        else:
            self._payload_type = PayloadType(ptype=payload_type)

    @property
    def pcallback(self) -> "Callback":
        return self._pcallback

    @pcallback.setter
    def pcallback(self, pcallback):
        if isinstance(pcallback, Callback) or pcallback is None:
            self._pcallback = pcallback
        else:
            self._pcallback = Callback(id=pcallback)

    @property
    def c2_profiles(self) -> Dict:
        return self._c2_profiles

    @c2_profiles.setter
    def c2_profiles(self, c2_profiles):
        if isinstance(c2_profiles, Dict):
            self._c2_profiles = {}
            for k, v in c2_profiles.items():
                key = (
                    k["name"]
                    if isinstance(k, Dict)
                    else k.name
                    if isinstance(k, C2Profile)
                    else k
                )
                self._c2_profiles[key] = []
                for i in v:
                    # now iterate over each list of parameters for the profile
                    if isinstance(i, C2ProfileParameters):
                        self._c2_profiles[key].append(i)
                    elif isinstance(i, Dict):
                        self._c2_profiles[key].append(C2ProfileParameters(**i))
        else:
            self._c2_profiles = None

    @property
    def operation(self) -> Operation:
        return self._operation

    @operation.setter
    def operation(self, operation):
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)

    @property
    def wrapped_payload(self) -> "Payload":
        return self._wrapped_payload

    @wrapped_payload.setter
    def wrapped_payload(self, wrapped_payload):
        if isinstance(wrapped_payload, Payload) or wrapped_payload is None:
            self._wrapped_payload = wrapped_payload
        else:
            self._wrapped_payload = Payload(uuid=payload)

    @property
    def deleted(self) -> bool:
        return self._deleted

    @deleted.setter
    def deleted(self, deleted):
        self._deleted = deleted

    @property
    def build_phase(self) -> str:
        return self._build_phase

    @build_phase.setter
    def build_phase(self, build_phase):
        self._build_phase = build_phase

    @property
    def build_message(self) -> str:
        return self._build_message

    @build_message.setter
    def build_message(self, build_message):
        self._build_message = build_message

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def build_container(self) -> str:
        return self._build_container

    @build_container.setter
    def build_container(self, build_container):
        self._build_container = build_container

    @property
    def commands(self) -> List[Command]:
        return self._commands

    @commands.setter
    def commands(self, commands):
        if isinstance(commands, List):
            self._commands = [
                Command(**x) if isinstance(x, Dict) else x for x in commands
            ]
        else:
            self._commands = None

    @property
    def build_parameters(self) -> List[Dict]:
        return self._build_parameters

    @build_parameters.setter
    def build_parameters(self, build_parameters):
        self._build_parameters = build_parameters

    @property
    def file_id(self) -> "FileMeta":
        return self._file_id

    @file_id.setter
    def file_id(self, file_id):
        if isinstance(file_id, "FileMeta") or file_id is None:
            self._file_id = file_id
        else:
            self._file_id = FileMeta(**file_id)

    @property
    def filename(self) -> str:
        return self._filename

    @filename.setter
    def filename(self, filename):
        self._filename = filename


class FileMeta:
    def __init__(
        self,
        agent_file_id: str = None,
        total_chunks: int = None,
        chunks_received: int = None,
        chunk_size: int = None,
        task: Union[Task, Dict] = None,
        complete: bool = None,
        path: str = None,
        full_remote_path: str = None,
        host: str = None,
        is_payload: bool = None,
        is_screenshot: bool = None,
        is_download_from_agent: bool = None,
        file_browser: Dict = None,
        filename: str = None,
        delete_after_fetch: bool = None,
        operation: Union[Operation, str] = None,
        timestamp: str = None,
        deleted: bool = None,
        operator: Union[Operator, str] = None,
        md5: str = None,
        sha1: str = None,
        id: int = None,
        cmd: str = None,
        comment: str = None,
        upload: dict = None,
        params: dict = None,
    ):
        self._agent_file_id = agent_file_id
        self._total_chunks = total_chunks
        self._chunks_received = chunks_received
        self._chunk_size = chunk_size
        if isinstance(task, Task) or task is None:
            self._task = task
        else:
            self._task = Task(id=task)
        self._complete = complete
        self._path = path
        self._full_remote_path = full_remote_path
        self._host = host
        self._is_payload = is_payload
        self._is_screenshot = is_screenshot
        self._is_download_from_agent = is_download_from_agent
        self._file_browser = file_browser
        self._filename = filename
        self._delete_after_fetch = delete_after_fetch
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)
        self._timestamp = timestamp
        self._deleted = deleted
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)
        self._md5 = md5
        self._sha1 = sha1
        self._id = id
        self._cmd = cmd
        self._comment = comment
        self._upload = upload
        self._params = params

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, FileMeta):
            return self._id == other.id
        return False

    @property
    def agent_file_id(self):
        return self._agent_file_id

    @agent_file_id.setter
    def total_chunks(self, agent_file_id):
        self._agent_file_id = agent_file_id

    @property
    def total_chunks(self):
        return self._total_chunks

    @total_chunks.setter
    def total_chunks(self, total_chunks):
        self._total_chunks = total_chunks

    @property
    def chunks_received(self):
        return self._chunks_received

    @chunks_received.setter
    def chunks_received(self, chunks_received):
        self._chunks_received = chunks_received

    @property
    def chunk_size(self):
        return self._chunk_size

    @chunk_size.setter
    def chunk_size(self, chunk_size):
        self._chunk_size = chunk_size

    @property
    def task(self):
        return self._task

    @task.setter
    def task(self, task):
        if isinstance(task, Task) or task is None:
            self._task = task
        else:
            self._task = Task(id=task)

    @property
    def complete(self):
        return self._complete

    @complete.setter
    def complete(self, complete):
        self._complete = complete

    @property
    def path(self):
        return self._path

    @path.setter
    def path(self, path):
        self._path = path

    @property
    def full_remote_path(self):
        return self._full_remote_path

    @full_remote_path.setter
    def full_remote_path(self, full_remote_path):
        self._full_remote_path = full_remote_path

    @property
    def host(self):
        return self._host

    @host.setter
    def host(self, host):
        self._host = host

    @property
    def is_payload(self):
        return self._is_payload

    @is_payload.setter
    def is_payload(self, is_payload):
        self._is_payload = is_payload

    @property
    def is_screenshot(self):
        return self._is_screenshot

    @is_screenshot.setter
    def is_screenshot(self, is_screenshot):
        self._is_screenshot = is_screenshot

    @property
    def is_download_from_agent(self):
        return self._is_download_from_agent

    @is_download_from_agent.setter
    def is_download_from_agent(self, is_download_from_agent):
        self._is_download_from_agent = is_download_from_agent

    @property
    def file_browser(self):
        return self._file_browser

    @file_browser.setter
    def file_browser(self, file_browser):
        self._file_browser = file_browser

    @property
    def filename(self):
        return self._filename

    @filename.setter
    def filename(self, filename):
        self._filename = filename

    @property
    def delete_after_fetch(self):
        return self._delete_after_fetch

    @delete_after_fetch.setter
    def delete_after_fetch(self, delete_after_fetch):
        self._delete_after_fetch = delete_after_fetch

    @property
    def operation(self):
        return self._operation

    @operation.setter
    def operation(self, operation):
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)

    @property
    def timestamp(self):
        return self._timestamp

    @timestamp.setter
    def timestamp(self, timestamp):
        self._timestamp = timestamp

    @property
    def deleted(self):
        return self._deleted

    @deleted.setter
    def deleted(self, deleted):
        self._deleted = deleted

    @property
    def operator(self):
        return self._operator

    @operator.setter
    def operator(self, operator):
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)

    @property
    def md5(self):
        return self._md5

    @md5.setter
    def md5(self, md5):
        self._md5 = md5

    @property
    def sha1(self):
        return self._sha1

    @sha1.setter
    def sha1(self, sha1):
        self._sha1 = sha1

    @property
    def id(self):
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def cmd(self):
        return self._cmd

    @cmd.setter
    def cmd(self, cmd):
        self._cmd = cmd

    @property
    def comment(self):
        return self._comment

    @comment.setter
    def comment(self, comment):
        self._comment = comment

    @property
    def upload(self):
        return self._upload

    @upload.setter
    def upload(self, upload):
        self._upload = upload

    @property
    def params(self):
        return self._params

    @params.setter
    def params(self, params):
        self._params = params


class Response:
    def __init__(
        self,
        response: str = None,
        timestamp: str = None,
        task: Union[Task, int, Dict] = None,  # JSON string of the corresponding task
        id: int = None,
    ):
        self._response = response
        self._timestamp = timestamp
        if isinstance(task, Task) or task is None:
            self._task = task
        elif isinstance(task, Dict):
            self._task = Task(**task)
        else:
            self._task = Task(id=task)
        self._id = id

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Response):
            return self._id == other.id
        return False

    @property
    def response(self) -> str:
        return self._response

    @response.setter
    def response(self, response):
        self._response = response

    @property
    def timestamp(self) -> str:
        return self._timestamp

    @timestamp.setter
    def timestamp(self, timestamp):
        self._timestamp = timestamp

    @property
    def task(self) -> Task:
        return self._task

    @task.setter
    def task(self, task):
        if isinstance(task, Task) or task is None:
            self._task = task
        elif isinstance(task, Dict):
            self._task = Task(**task)
        else:
            self._task = Task(id=task)

    @property
    def id(self):
        return self._id

    @id.setter
    def id(self, id):
        self._id = id


class Credential:
    def __init__(
        self,
        type: str = None,
        task: Union[Task, int] = None,
        task_command: Union[Command, str] = None,
        account: str = None,
        realm: str = None,
        id: int = None,
        operator: Union[Operator, str] = None,
        operation: Union[Operation, str] = None,
        timestamp: str = None,
        credential: bytes = None,
        comment: str = None,
        deleted: bool = None,
        new: bool = None,
    ):
        self._type = type
        if isinstance(task, Task) or task is None:
            self._task = task
        else:
            self._task = Task(id=task)
        if isinstance(task_command, Command) or task_command is None:
            self._task_command = task_command
        else:
            self._task_command = Command(cmd=task_command)
        self._account = account
        self._realm = realm
        self._id = id
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)
        self._timestamp = timestamp
        self._credential = credential
        self._comment = comment
        self._deleted = deleted
        self._new = new

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Credential):
            return self._id == other.id
        return False

    @property
    def type(self) -> str:
        return self._type

    @type.setter
    def type(self, type):
        self._type = type

    @property
    def task(self) -> Task:
        return self._task

    @task.setter
    def task(self, task):
        if isinstance(task, Task) or task is None:
            self._task = task
        else:
            self._task = Task(id=task)

    @property
    def task_command(self) -> Command:
        return self._task_command

    @task_command.setter
    def task_command(self, task_command):
        if isinstance(task_command, Command) or task_command is None:
            self._task_command = task_command
        else:
            self._task_command = Command(cmd=task_command)

    @property
    def account(self) -> str:
        return self._account

    @account.setter
    def account(self, account):
        self._account = account

    @property
    def realm(self) -> str:
        return self._realm

    @realm.setter
    def realm(self, realm):
        self._realm = realm

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def operator(self) -> Operator:
        return self._operator

    @operator.setter
    def operator(self, operator):
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)

    @property
    def operation(self) -> Operation:
        return self._operation

    @operation.setter
    def operation(self, operation):
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)

    @property
    def timestamp(self) -> str:
        return self._timestamp

    @timestamp.setter
    def timestamp(self, timestamp):
        self._timestamp = timestamp

    @property
    def credential(self) -> bytes:
        return self._credential

    @credential.setter
    def credential(self, credential):
        self._credential = credential

    @property
    def comment(self) -> str:
        return self._comment

    @comment.setter
    def comment(self, comment):
        self._comment = comment

    @property
    def deleted(self) -> bool:
        return self._deleted

    @deleted.setter
    def deleted(self, deleted):
        self._deleted = deleted

    @property
    def new(self) -> bool:
        return self._new

    @new.setter
    def new(self, new):
        self._new = new


class Keylog:
    def __init__(
        self,
        task: Union[Task, int] = None,
        keystrokes: bytes = None,
        window: str = None,
        timestamp: str = None,
        operation: Union[Operation, str] = None,
        user: str = None,
        host: str = None,
        id: int = None,
        callback: Union[Callback, Dict] = None,
    ):
        self._keystrokes = keystrokes
        self._window = window
        self._timestamp = timestamp
        self._user = user
        self._host = host
        if isinstance(task, Task) or task is None:
            self._task = task
        else:
            self._task = Task(id=int)
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)
        if isinstance(callback, Callback) or callback is None:
            self._callback = callback
        else:
            self._callback = Callback(**callback)

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, Keylog):
            return self._id == other.id

    @property
    def keystrokes(self) -> bytes:
        return self._keystrokes

    @keystrokes.setter
    def keystrokes(self, keystrokes):
        self._keystrokes = keystrokes

    @property
    def window(self) -> str:
        return self._window

    @window.setter
    def window(self, window):
        self._window = window

    @property
    def timestamp(self) -> str:
        return self._timestamp

    @timestamp.setter
    def timestamp(self, timestamp):
        self._timestamp = timestamp

    @property
    def user(self) -> str:
        return self._user

    @user.setter
    def user(self, user):
        self._user = user

    @property
    def host(self) -> str:
        return self._host

    @host.setter
    def host(self, host):
        self._host = host

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def task(self) -> Task:
        return self._task

    @task.setter
    def task(self, task):
        if isinstance(task, Task) or task is None:
            self._task = task
        else:
            self._task = Task(id=int)

    @property
    def operation(self) -> Operation:
        return self._operation

    @operation.setter
    def operation(self, operation):
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)

    @property
    def callback(self) -> Callback:
        return self._callback

    @callback.setter
    def callback(self, callback):
        if isinstance(callback, Callback) or callback is None:
            self._callback = callback
        else:
            self._callback = Callback(**callback)


class DisabledCommandsProfile:
    def __init__(
        self,
        payload_types: List[Union[PayloadType, str, Dict]] = None,
        name: str = None,
    ):
        self._name = name
        if isinstance(payload_types, List):
            self._payload_types = [
                PayloadType(ptype=x)
                if isinstance(x, str)
                else PayloadType(**x)
                if isinstance(x, Dict)
                else x
                for x in payload_types
            ]
        else:
            self._payload_types = payload_types

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, DisabledCommandsProfile):
            return self._name == other.name

    @property
    def name(self) -> str:
        return self._name

    @name.setter
    def name(self, name):
        self._name = name

    @property
    def payload_types(self) -> List[PayloadType]:
        return self._payload_types

    @payload_types.setter
    def payload_types(self, payload_types):
        if isinstance(payload_types, List):
            self._payload_types = [
                PayloadType(ptype=x)
                if isinstance(x, str)
                else PayloadType(**x)
                if isinstance(x, Dict)
                else x
                for x in payload_types
            ]
        else:
            self._payload_types = payload_types


class EventMessage:
    def __init__(
        self,
        operator: Union[Operator, str] = None,
        timestamp: str = None,
        message: str = None,
        operation: Union[Operation, str] = None,
        level: str = None,
        deleted: bool = None,
        resolved: bool = None,
        id: int = None,
        channel: str = None,
        alerts: List[Dict] = None,
    ):
        self._timestamp = timestamp
        self._message = message
        self._level = level
        self._deleted = deleted
        self._resolved = resolved
        self._id = id
        self._channel = channel
        self._alerts = alerts
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)

    def to_json(self):
        r = {}
        for k in vars(self):
            if getattr(self, k) is not None:
                try:
                    r[k[1:]] = getattr(self, k)
                except:
                    r[k[1:]] = json.dumps(
                        getattr(self, k), default=lambda o: o.to_json()
                    )
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    def __eq__(self, other):
        """Overrides the default implementation"""
        if isinstance(other, EventMessage):
            return self._id == other.id

    @property
    def operator(self) -> Operator:
        return self._operator

    @operator.setter
    def operator(self, operator):
        if isinstance(operator, Operator) or operator is None:
            self._operator = operator
        else:
            self._operator = Operator(username=operator)

    @property
    def timestamp(self) -> str:
        return self._timestamp

    @timestamp.setter
    def timestamp(self, timestamp):
        self._timestamp = timestamp

    @property
    def message(self) -> str:
        return self._message

    @message.setter
    def message(self, message):
        self._message = message

    @property
    def operation(self) -> Operation:
        return self._operation

    @operation.setter
    def operation(self, operation):
        if isinstance(operation, Operation) or operation is None:
            self._operation = operation
        else:
            self._operation = Operation(name=operation)

    @property
    def level(self) -> str:
        return self._level

    @level.setter
    def level(self, level):
        self._level = level

    @property
    def deleted(self) -> bool:
        return self._deleted

    @deleted.setter
    def deleted(self, deleted):
        self._deleted = deleted

    @property
    def resolved(self) -> bool:
        return self._resolved

    @resolved.setter
    def resolved(self, resolved):
        self._resolved = resolved

    @property
    def id(self) -> int:
        return self._id

    @id.setter
    def id(self, id):
        self._id = id

    @property
    def channel(self) -> str:
        return self._channel

    @channel.setter
    def channel(self, channel):
        self._channel = channel

    @property
    def alerts(self) -> List[Dict]:
        return self._alerts

    @alerts.setter
    def alerts(self, alerts):
        self._alerts = alerts


class MythicResponse:
    def __init__(
        self,
        response=None,
        raw_response: Dict[str, str] = None,
        response_code: int = None,
        status: str = None,
    ):
        # set the response_code and raw_response automatically
        self.response_code = response_code
        self.raw_response = raw_response
        # determine and set status if it's not explicitly specified
        if status is None and "status" in raw_response:
            self.status = raw_response["status"]
        elif status is None and self.response_code != 200:
            self.status = "error"
        else:
            self.status = status
        # if the raw_response has a status indicator, remove it and set the response
        #   otherwise just set response to raw_response and process later
        if "status" in raw_response and response is None:
            del raw_response["status"]
            self.response = raw_response
        elif response is None:
            self.response = raw_response

    def to_json(self):
        r = {}
        for k in vars(self):
            try:
                r[k] = getattr(self, k)
            except:
                r[k] = json.dumps(getattr(self, k))
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    @property
    def response(self):
        return self.__response

    @property
    def status(self):
        return self.__status

    @property
    def response_code(self):
        return self.__response_code

    @property
    def raw_response(self):
        return self.__raw_response

    @response.setter
    def response(self, response):
        self.__response = response

    @response_code.setter
    def response_code(self, response_code):
        self.__response_code = response_code

    @status.setter
    def status(self, status):
        self.__status = status

    @raw_response.setter
    def raw_response(self, raw_response):
        self.__raw_response = raw_response


class Mythic:
    def __init__(
        self,
        username: str = None,
        password: str = None,
        apitoken: Union[APIToken, str] = None,
        access_token: str = None,
        refresh_token: str = None,
        server_ip: str = None,
        ssl: bool = False,
        server_port: str = None,
        server_api_version: int = 1.4,
        operator: Operator = None,
        global_timeout: int = None,
    ):
        self._username = username
        self._password = password
        if isinstance(apitoken, APIToken) or apitoken is None:
            self._apitoken = apitoken
        else:
            self._apitoken = APIToken(token_value=apitoken)
        self._access_token = access_token
        self._refresh_token = refresh_token
        self._server_ip = server_ip
        self._server_port = server_port
        self._server_api_version = server_api_version
        self._operator = operator
        self._ssl = ssl
        self._http = "http://" if not ssl else "https://"
        self._ws = "ws://" if not ssl else "wss://"
        self._global_timeout = global_timeout if global_timeout is not None else -1

    def to_json(self):
        r = {}
        for k in vars(self):
            try:
                r[k[1:]] = getattr(self, k)
            except:
                r[k[1:]] = json.dumps(getattr(self, k))
        return r

    def __str__(self):
        return json.dumps(self.to_json())

    # ======== GETTING INTERNAL VALUES ==================
    @property
    def username(self):
        return self._username

    @property
    def password(self):
        return self._password

    @property
    def apitoken(self):
        return self._apitoken

    @property
    def access_token(self):
        return self._access_token

    @property
    def refresh_token(self):
        return self._refresh_token

    @property
    def server_ip(self):
        return self._server_ip

    @property
    def server_port(self):
        return self._server_port

    @property
    def operator(self):
        return self._operator

    @property
    def server_api_version(self):
        return self._server_api_version

    @property
    def ssl(self):
        return self._ssl

    @property
    def global_timeout(self):
        return self._global_timeout

    # ========== SETTING INTERNAL VALUES ===============
    @username.setter
    def username(self, username=None):
        self._username = username

    @password.setter
    def password(self, password=None):
        self._password = password

    @apitoken.setter
    def apitoken(self, apitoken=None):
        if isinstance(apitoken, APIToken) or apitoken is None:
            self._apitoken = apitoken
        else:
            self._apitoken = APIToken(token_value=apitoken)

    @access_token.setter
    def access_token(self, access_token=None):
        self._access_token = access_token

    @refresh_token.setter
    def refresh_token(self, refresh_token=None):
        self._refresh_token = refresh_token

    @server_ip.setter
    def server_ip(self, server_ip=None):
        self._server_ip = server_ip

    @server_port.setter
    def server_port(self, server_port=None):
        self._server_port = server_port

    @operator.setter
    def operator(self, operator=None):
        self._operator = operator

    @server_api_version.setter
    def server_api_version(self, server_api_version=None):
        self._server_api_version = server_api_version

    @ssl.setter
    def ssl(self, ssl=False):
        self._ssl = ssl
        self._http = "http://" if not ssl else "https://"
        self._ws = "ws://" if not ssl else "wss://"

    # ======== BASIC GET/POST/PUT/DELETE JSON WEB REQUESTS =========

    def get_headers(self) -> dict:
        if self._apitoken is not None:
            return {"apitoken": self._apitoken.token_value}
        elif self._access_token is not None:
            return {"Authorization": "Bearer {}".format(self._access_token)}
        else:
            return {}

    async def get_json(self, url) -> MythicResponse:
        headers = self.get_headers()
        try:
            async with aiohttp.ClientSession() as session:
                async with session.get(url, headers=headers, ssl=False) as resp:
                    return MythicResponse(
                        response_code=resp.status, raw_response=await resp.json()
                    )
        except OSError as o:
            #print(o)
            return MythicResponse(
                response_code=0, raw_response={"status": "error", "error": str(o)}
            )
        except Exception as e:
            #print(e)
            return MythicResponse(
                response_code=0, raw_response={"status": "error", "error": str(e)}
            )

    async def get_file(self, url) -> bytes:
        headers = self.get_headers()
        async with aiohttp.ClientSession() as session:
            async with session.get(url, headers=headers, ssl=False) as resp:
                data = await resp.read()
                return data


    async def put_json(self, url, data) -> MythicResponse:
        headers = self.get_headers()
        try:
            async with aiohttp.ClientSession() as session:
                async with session.put(
                    url, json=data, headers=headers, ssl=False
                ) as resp:
                    return MythicResponse(
                        response_code=resp.status, raw_response=await resp.json()
                    )
        except OSError as o:
            return MythicResponse(
                response_code=0, raw_response={"status": "error", "error": str(o)}
            )
        except Exception as e:
            return MythicResponse(
                response_code=0, raw_response={"status": "error", "error": str(e)}
            )

    async def post_json(self, url, data) -> MythicResponse:
        headers = self.get_headers()
        try:
            async with aiohttp.ClientSession() as session:
                async with session.post(
                    url, json=data, headers=headers, ssl=False
                ) as resp:
                    return MythicResponse(
                        response_code=resp.status, raw_response=await resp.json()
                    )
        except OSError as o:
            return MythicResponse(
                response_code=0, raw_response={"status": "error", "error": str(o)}
            )
        except Exception as e:
            return MythicResponse(
                response_code=0, raw_response={"status": "error", "error": str(e)}
            )

    async def delete_json(self, url) -> MythicResponse:
        headers = self.get_headers()
        try:
            async with aiohttp.ClientSession() as session:
                async with session.delete(url, headers=headers, ssl=False) as resp:
                    return MythicResponse(
                        response_code=resp.status, raw_response=await resp.json()
                    )
        except OSError as o:
            return MythicResponse(
                response_code=0, raw_response={"status": "error", "error": str(o)}
            )
        except Exception as e:
            return MythicResponse(
                response_code=0, raw_response={"status": "error", "error": str(e)}
            )

    # ======== WEBSOCKET BASED HELPER ENDPOINTS ========================

    async def print_websocket_output(self, mythic, data) -> None:
        try:
            await json_print(data)
        except Exception as e:
            raise Exception("Failed to decode json data: " + str(e))

    async def cast_data(self, data):
        try:
            json_data = json.loads(data)
            if "channel" in json_data:
                if "callback" in json_data["channel"]:
                    del json_data["channel"]
                    return Callback(**json_data)
                elif "task" in json_data["channel"]:
                    del json_data["channel"]
                    return Task(**json_data)
                elif "response" in json_data["channel"]:
                    del json_data["channel"]
                    return Response(**json_data)
                elif "historic" in json_data["channel"]:
                    return EventMessage(**json_data)
                elif "event" in json_data["channel"]:
                    return EventMessage(**json_data)
            elif "chunks_received" in json_data:
                return FileMeta(**json_data)
            elif "build_phase" in json_data:
                return Payload(**json_data)
            elif "agent_task_id" in json_data:
                return Task(**json_data)
            elif "response" in json_data:
                return Response(**json_data)
            elif "realm" in json_data:
                return Credential(**json_data)
            elif "level" in json_data:
                return EventMessage(**json_data)
            elif "agent_callback_id" in json_data:
                return Callback(**json_data)
            else:
                raise Exception("Unknown Mythic Object: " + json.dumps(json_data, indent=2))
        except Exception as e:
            raise Exception("Failed to decode json data: " + str(e))

    async def thread_output_helper(
        self, url, callback_function=None, timeout=None
    ) -> None:
        headers = self.get_headers()
        if timeout is None:
            timeout = self.global_timeout
        try:
            async with aiohttp.ClientSession() as session:
                ws = await session.ws_connect(url, headers=headers, ssl=False)
                start = time()
                while True:
                    try:
                        if timeout > 0 and (time() - start >= timeout):
                            raise Exception(
                                "Timeout in listening on websocket endpoint: {}".format(
                                    url
                                )
                            )
                        msg = await ws.receive()
                        if msg.data is None:
                            raise Exception(
                                "Got no data from websocket: {}".format(str(msg))
                            )
                        if msg.data != "":
                            print(msg.data)
                            task = asyncio.get_event_loop().create_task(
                                callback_function(self, await self.cast_data(msg.data))
                            )
                            asyncio.ensure_future(task)
                    except Exception as e:
                        raise Exception("Got exception reading from websocket, exiting websocket: " + str(e))
        except Exception as e:
            raise Exception("Failed to get websocket connection: " + str(e))

    async def stream_output(self, url, callback_function, timeout) -> asyncio.Task:
        task = asyncio.get_event_loop().create_task(
            self.thread_output_helper(url, callback_function, timeout)
        )
        asyncio.ensure_future(task)
        return task

    # ================== OPERATION ENDPOINTS ======================

    async def get_current_operation_info(self) -> MythicResponse:
        """
        Gets information about the current operation for the user
        """
        if self.operator is None:
            await self.get_self()
        url = "{}{}:{}/api/v{}/operations/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            self.operator.current_operation.id,
        )
        resp = await self.get_json(url)
        if resp.response_code == 200 and resp.status == "success":
            resp.response = Operation(**resp.response)
        return resp

    async def get_all_operations(self) -> MythicResponse:
        """
        Gets information about all operations your operator can see
        """
        url = "{}{}:{}/api/v{}/operations".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200 and resp.status == "success":
            operations = []
            for o in resp.response["output"]:
                operations.append(Operation(**o))
            resp.response = operations
        return resp

    async def get_operation(self, operation: Operation) -> MythicResponse:
        """
        Gets information about the current user
        """
        if operation.id is None:
            resp = await self.get_all_operations()
            if resp.response_code == 200 and resp.status == "success":
                for o in resp.response:
                    if o.name == operation.name:
                        resp.response = o
                        return resp
            raise Exception("Failed to find operation: " + json.dumps(resp, indent=2, default=lambda o: o.to_json()))
        else:
            url = "{}{}:{}/api/v{}/operations/{}".format(
                self._http,
                self.server_ip,
                self._server_port,
                self._server_api_version,
                str(operation.id),
            )
            resp = await self.get_json(url)
            if resp.response_code == 200:
                resp.response = Operation(**resp.response)
            return resp

    async def add_or_update_operator_for_operation(
        self, operation: Operation, operator: Operator
    ) -> MythicResponse:
        """
        Adds an operator to an operation or updates an operator's view/block lists in an operation
        """
        resp = await self.get_operation(operation)
        if resp.status == "success":
            operation = resp.response
        else:
            raise Exception(
                "failed to get operation in add_or_update_operator_for_operation"
            )
        data = {"add_members": [await obj_to_json(operator)]}
        if operator.base_disabled_commands is not None:
            data["add_disabled_commands"] = [await obj_to_json(operator)]
        url = "{}{}:{}/api/v{}/operations/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(operation.id),
        )
        resp = await self.put_json(url, data=data)
        if resp.response_code == 200 and resp.status == "success":
            resp.response = Operation(**resp.response)
        return resp

    async def remove_operator_from_operation(
        self, operation: Operation, operator: Operator
    ) -> MythicResponse:
        """
        Removes an operator from an operation
        """
        resp = await self.get_operation(operation)
        if resp.status == "success":
            operation = resp.response
        else:
            raise Exception("failed to get operation in remove_operator_for_operation")
        data = {"remove_members": [operator.username]}
        url = "{}{}:{}/api/v{}/operations/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(operation.id),
        )
        resp = await self.put_json(url, data=data)
        if resp.response_code == 200 and resp.status == "success":
            resp.response = Operation(**resp.response)
        return resp

    async def update_operation(self, operation: Operation) -> MythicResponse:
        """
        Updates information about an operation such as webhook and completion status
        """
        if operation.id is None:
            resp = await self.get_operation(operation)
            if resp.status == "error":
                raise Exception("Failed to get_operation in update_operation")
            operation.id = resp.response.id
        url = "{}{}:{}/api/v{}/operations/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(operation.id),
        )
        resp = await self.put_json(url, data=await obj_to_json(operation))
        if resp.response_code == 200 and resp.status == "success":
            resp.response = Operation(**resp.response)
        return resp

    async def create_operation(self, operation: Operation) -> MythicResponse:
        """
        Creates a new operation and specifies the admin of the operation
        """
        url = "{}{}:{}/api/v{}/operations/".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
        )
        data = {
            "name": operation.name,
            "admin": operation.admin.username
        }
        resp = await self.post_json(url, data=data)
        if resp.response_code == 200 and resp.status == "success":
            resp.response = Operation(**resp.response)
        return resp

    # ================== OPERATOR ENDPOINTS ======================

    async def get_self(self) -> MythicResponse:
        """
        Gets information about the current user
        """
        url = "{}{}:{}/api/v{}/operators/me".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200 and resp.status == "success":
            self.operator = Operator(**resp.response)
            resp.response = Operator(**resp.response)
        return resp

    async def get_operator(self, operator: Operator) -> MythicResponse:
        """
        Gets information about the current user
        """
        if operator.id is None:
            # need to get the operator's ID first, which means we need to get all operators and match the username
            url = "{}{}:{}/api/v{}/operators/".format(
                self._http, self.server_ip, self._server_port, self._server_api_version
            )
            resp = await self.get_json(url)
            if resp.response_code == 200:
                if resp.status is None:
                    resp.status = "success"
                for o in resp.response:
                    if o["username"] == operator.username:
                        resp.response = Operator(**o)
                        return resp
                raise Exception("Operator not found: " + json.dumps(resp, indent=2, default=lambda o: o.to_json()))
            return resp
        else:
            url = "{}{}:{}/api/v{}/operators/{}".format(
                self._http,
                self.server_ip,
                self._server_port,
                self._server_api_version,
                str(operator.id),
            )
            resp = await self.get_json(url)
            if resp.response_code == 200:
                resp.response = Operator(**resp.response)
            return resp

    async def create_operator(self, operator: Operator) -> MythicResponse:
        """
        Creates a new operator with the specified username and password.
        If the operator name already exists, just returns information about that operator.
        """
        url = "{}{}:{}/api/v{}/operators".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.post_json(
            url, data={"username": operator.username, "password": operator.password}
        )
        if resp.response_code == 200 and resp.status == "success":
            resp.response = Operator(**resp.response)
        elif resp.status == "error":
            resp = await self.get_operator(operator)
            if resp.status == "success":
                return resp
            raise Exception("Unable to create operator and no active operator found: " + json.dumps(resp, indent=2, default=lambda o: o.to_json()))
        return resp

    async def update_operator(self, operator: Operator) -> MythicResponse:
        """
        Updates information about the specified operator.
        """
        if operator.id is None:
            resp = await self.get_operator(operator)
            if resp.status == "error":
                raise Exception("Failed to get_operator in update_operator")
            operator.id = resp.response.id
        url = "{}{}:{}/api/v{}/operators/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(operator.id),
        )
        resp = await self.put_json(url, data=await obj_to_json(operator))
        if resp.response_code == 200 and resp.status == "success":
            resp.response = Operator(**resp.response)
        return resp

    # ================== APITOKEN ENDPOINTS ======================

    async def get_apitokens(self) -> MythicResponse:
        """
        Gets all of the user's API tokens in a List
        :return:
        """
        url = "{}{}:{}/api/v{}/apitokens".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200 and resp.status == "success":
            # update the response with APIToken objects instead of just a dictionary
            resp.response = [APIToken(**x) for x in resp.response["apitokens"]]
        return resp

    async def create_apitoken(self, token_type="User") -> MythicResponse:
        """
        Creates an API token for the user
        :param token_type:
            must be either "User" or "C2"
        :return:
        """
        # token_type should be C2 or User
        url = "{}{}:{}/api/v{}/apitokens".format(
            self._http, self._server_ip, self._server_port, self._server_api_version
        )
        resp = await self.post_json(url, data={"token_type": token_type})
        if resp.response_code == 200 and resp.status == "success":
            # update the response to be an object
            resp.response = APIToken(**resp.response)
        return resp

    async def remove_apitoken(self, apitoken: Union[APIToken, Dict]) -> MythicResponse:
        """
        Removes the specified API token and invalidates it going forward
        :param apitoken:
            if using the APIToken class, the following must be set:
                id
        :return:
        """
        # take in an object and parse it if the value isn't explicitly given
        url = "{}{}:{}/api/v{}/apitokens/{}".format(
            self._http,
            self._server_ip,
            self._server_port,
            self._server_api_version,
            str(apitoken.id if isinstance(apitoken, APIToken) else apitoken["id"]),
        )
        resp = await self.delete_json(url)
        if resp.response_code == 200 and resp.status == "success":
            # update the response to ben an object
            resp.response = APIToken(**resp.response)
        return resp

    # ================= PAYLOAD ENDPOINTS =======================

    async def get_payloads(self) -> MythicResponse:
        """
        Get all the payloads for the current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/payloads/current_operation".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            resp.response = [Payload(**x) for x in resp.response]
        return resp

    async def remove_payload(self, payload: Union[Payload, Dict]) -> MythicResponse:
        """
        Mark a payload as deleted in the database and remove it from disk
        Truly removing it from the database would delete any corresponding tasks/callbacks, so we don't do that
        :param payload:
        :return:
        """
        url = "{}{}:{}/api/v{}/payloads/{}".format(
            self._http,
            self._server_ip,
            self._server_port,
            self._server_api_version,
            str(payload.uuid if isinstance(payload, Payload) else payload["uuid"]),
        )
        resp = await self.delete_json(url)
        if resp.response_code == 200 and resp.status == "success":
            # update the response to ben an object
            resp.response = Payload(**resp.response)
        return resp

    async def create_payload(
        self,
        payload: Payload,
        all_commands: bool = None,
        timeout=None,
        wait_for_build: bool = None,
    ) -> MythicResponse:
        """
        :param payload:

        :return:
        {"payload_type":"poseidon",
        "c2_profiles":[
          {"c2_profile_parameters":
            {
              "AESPSK":"ElhUTijQn2klOtjlGyxs2uU6oq4PWD2Tboc5qaKzKCg=",
              "USER_AGENT":"Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko",
              "callback_host":"https://domain.com",
              "callback_interval":"10",
              "callback_jitter":"23",
              "callback_port":"80",
              "domain_front":"",
              "encrypted_exchange_check":"T",
              "killdate":"yyyy-mm-dd"
            },
          "c2_profile":"HTTP"
          }],
        "filename":"poseidon.bin",
        "tag":"this is my tag yo for initial access",
        "commands":["cat","cd","cp","curl","download","drives","exit","getenv","getuser","jobkill","jobs","jxa","keylog","keys","kill","libinject","listtasks","ls","mkdir","mv","portscan","ps","pwd","rm","screencapture","setenv","shell","sleep","socks","sshauth","triagedirectory","unsetenv","upload","xpc"],
        "build_parameters":[
          {"name":"mode","value":"default"},
          {"name":"os","value":"darwin"}
          ]
        }"
        """
        data = {}
        data["payload_type"] = payload.payload_type.ptype
        data["filename"] = payload.filename
        data["tag"] = payload.tag
        if payload.wrapped_payload is None:
            data["c2_profiles"] = []
            for k, v in payload.c2_profiles.items():
                parameters = {i.name: i.value for i in v}
                data["c2_profiles"].append(
                    {"c2_profile": k, "c2_profile_parameters": parameters}
                )
        data["build_parameters"] = []
        if all_commands:
            if payload.payload_type.id is None:
                resp = await self.get_payloadtypes()
                for p in resp.response:
                    if p.ptype == payload.payload_type.ptype:
                        payload.payload_type = p
            resp = await self.get_payloadtype_commands(payload.payload_type)
            payload.commands = resp.response
        if payload.commands is not None:
            data["commands"] = [c.cmd for c in payload.commands]
        else:
            data["commands"] = []
        if payload.build_parameters is not None:
            data['build_parameters'] = payload.build_parameters
        if payload.wrapped_payload is not None:
            data['wrapped_payload'] = payload.wrapped_payload.uuid
        url = "{}{}:{}/api/v{}/payloads/create".format(
            self._http, self._server_ip, self._server_port, self._server_api_version
        )
        resp = await self.post_json(url, data=data)
        if resp.response_code == 200 and resp.status == "success":
            # update the response to be an object
            # this will be a very basic payload with just the payload UUID
            resp.response = Payload(**resp.response)
            if wait_for_build is not None and wait_for_build:
                status = await self.wait_for_payload_status_change(
                    resp.response.uuid, "success", timeout
                )
                if status is None:
                    raise Exception(
                        "Failed to get final payload status from wait_for_payload_status_change in creat_payload"
                    )
                else:
                    resp.response = status
        return resp

    async def get_one_payload_info(
        self, payload: Union[Payload, Dict]
    ) -> MythicResponse:
        """
        Get information about a specific payload
        :param payload:
            if using the Payload class, the following must be set:
                uuid
        :return:
        """
        url = "{}{}:{}/api/v{}/payloads/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(payload.uuid if isinstance(payload, Payload) else payload["uuid"]),
        )
        resp = await self.get_json(url)
        if resp.response_code == 200 and resp.status == "success":
            # update the response to ben an object
            resp.response = Payload(**resp.response)
        return resp

    async def download_payload(self, payload: Union[Payload, Dict]) -> bytes:
        """
        Get the final payload for a specified payload
        :param payload:
            if using Payload class, the following must be set:
                uuid
        :return:
        """
        url = "{}{}:{}/api/v{}/payloads/download/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(payload.uuid if isinstance(payload, Payload) else payload["uuid"]),
        )
        resp = await self.get_file(url)
        return resp

    # ================= FILE ENDPOINTS =======================

    async def download_file(self, file: FileMeta) -> bytes:
        """
        Download a file that is either scheduled for upload or is finished downloading
        """
        url = "{}{}:{}/api/v{}/files/download/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            file.agent_file_id,
        )
        resp = await self.get_file(url)
        return resp

    # ================ PAYLOAD TYPE ENDPOINTS ====================

    async def get_payloadtypes(self) -> MythicResponse:
        """
        Get all payload types registered with Apfell
        :return:
        """
        url = "{}{}:{}/api/v{}/payloadtypes/".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            tmp = []
            for x in resp.response["payloads"]:
                tmp.append(PayloadType(**x))
            for x in resp.response["wrappers"]:
                tmp.append(PayloadType(**x))
            resp.response = tmp
        return resp

    async def get_payloadtype(
        self, payload_type: Union[PayloadType, Dict]
    ) -> MythicResponse:
        """
        Get information about a specific payload type
        :param payload_type:
            if using PayloadType class, the following must be set:
                ptype
        :return:
        """
        url = "{}{}:{}/api/v{}/payloadtypes/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(
                payload_type.id
                if isinstance(payload_type, PayloadType)
                else payload_type["id"]
            ),
        )
        resp = await self.get_json(url)
        if resp.response_code == 200 and resp.status == "success":
            # update the response with APIToken objects instead of just a dictionary
            resp.response = PayloadType(**resp.response)
        return resp

    async def get_payloadtype_commands(
        self, payload_type: Union[PayloadType, Dict]
    ) -> MythicResponse:
        """
        Get the commands registered for a specific payload type
        :param payload_type:
            if using PayloadType class, the following must be set:
                ptype
        :return:
        """
        url = "{}{}:{}/api/v{}/payloadtypes/{}/commands".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(
                payload_type.id
                if isinstance(payload_type, PayloadType)
                else payload_type["id"]
            ),
        )
        resp = await self.get_json(url)
        if resp.response_code == 200 and resp.status == "success":
            resp.response = [Command(**x) for x in resp.response["commands"]]
        return resp

    # ================ TASKING ENDPOINTS ========================

    async def get_all_tasks(self) -> MythicResponse:
        """
        Get all of the tasks associated with the user's current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/tasks/".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            resp.response = [Task(**x) for x in resp.response]
        return resp

    async def get_all_tasks_for_callback(
        self, callback: Union[Callback, Dict]
    ) -> MythicResponse:
        """
        Get the tasks (no responses) for a specific callback
        :param callback:
            if using the Callback class, the following must be set:
                id
        :return:
        """
        url = "{}{}:{}/api/v{}/tasks/callback/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            callback.id if isinstance(callback, Callback) else callback["id"],
        )
        resp = await self.get_json(url)
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            resp.response = [Task(**x) for x in resp.response]
        return resp

    async def get_all_responses_for_task(
        self, task: Union[Task, Dict]
    ) -> MythicResponse:
        """
        For the specified task, get all the responses
        :param task:
            if using the Task class, the following must be set:
                id
        :return:
        """
        url = "{}{}:{}/api/v{}/tasks/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            task.id if isinstance(task, Task) else task["id"],
        )
        resp = await self.get_json(url)
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            tsk = Task(**resp.response["task"])
            tsk.callback = Callback(**resp.response["callback"])
            tsk.responses = [Response(**x) for x in resp.response["responses"]]
            resp.response = tsk
        return resp

    async def get_all_tasks_and_responses_grouped_by_callback(self) -> MythicResponse:
        """
        Get all tasks and responses for all callbacks in the current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/task_report_by_callback".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            resp.response = [Callback(**x) for x in resp.response["output"]]
        return resp

    async def create_task(
        self, task: Task, return_on="preprocessing", timeout=None
    ) -> MythicResponse:
        """
        Create a new task for a callback
        :param task:
            if using the Task class, the following must be set:
                callback: id
                command: cmd
                params
        :return:
        """
        url = "{}{}:{}/api/v{}/tasks/callback/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            task.callback.id if isinstance(task, Task) else task["callback"],
        )
        headers = self.get_headers()
        if task.files is None:
            data = {"command": task.command.cmd}
            if isinstance(task.params, str):
                data["params"] = task.params
            else:
                data["params"] = json.dumps(task.params)
            try:
                async with aiohttp.ClientSession() as session:
                    async with session.post(
                        url, json=data, headers=headers, ssl=False
                    ) as resp:
                        resp = MythicResponse(
                            response_code=resp.status, raw_response=await resp.json()
                        )
            except OSError as o:
                return MythicResponse(
                    response_code=0, raw_response={"status": "error", "error": str(o)}
                )
            except Exception as e:
                return MythicResponse(
                    response_code=0, raw_response={"status": "error", "error": str(e)}
                )
        else:
            form = aiohttp.FormData()
            data = {"command": task.command.cmd, "params": task.params}
            for f in task.files:
                data["params"][f.param_name] = "FILEUPLOAD"
                form.add_field("file" + f.param_name, f.content, filename=f.filename)
            data["params"] = json.dumps(data["params"])
            form.add_field("json", json.dumps(data))
            try:
                async with aiohttp.ClientSession() as session:
                    async with session.post(
                        url, data=form, headers=headers, ssl=False
                    ) as resp:
                        resp = MythicResponse(
                            response_code=resp.status, raw_response=await resp.json()
                        )
            except OSError as o:
                return MythicResponse(
                    response_code=0, raw_response={"status": "error", "error": str(o)}
                )
            except Exception as e:
                return MythicResponse(
                    response_code=0, raw_response={"status": "error", "error": str(e)}
                )
        if resp.response_code == 200 and resp.status == "success":
            resp.response = Task(**resp.response)
            if return_on == "preprocessing":
                return resp.response
            else:
                # we need to loop and wait for the status of the task to change
                resp.response = await self.wait_for_task_status_change(
                    resp.response.id, return_on, timeout
                )
        return resp

    async def set_comment_on_task(self, task:Task) -> MythicResponse:
        """
        Get all of the credentials associated with the user's current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/tasks/comments/{}".format(
            self._http, self.server_ip, self._server_port, self._server_api_version,
            task.id
        )
        if task.comment == "" or task.comment is None:
            resp = await self.delete_json(url)
        else:
            resp = await self.post_json(url, data={"comment": task.comment})
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            resp.response = Task(**resp.response['task'])
        return resp

    # ============== CREDENTIAL ENDPOINTS ========================

    async def get_all_credentials(self) -> MythicResponse:
        """
        Get all of the credentials associated with the user's current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/credentials/current_operation".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            resp.response = [Credential(**x) for x in resp.response["credentials"]]
        return resp

    async def create_credential(self, credential: Credential) -> MythicResponse:
        """
        Create a new credential associated with the user's current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/credentials".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.post_json(url, data=await obj_to_json(credential))
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            resp.response = Credential(**resp.response)
        return resp

    async def update_credential(self, credential: Credential) -> MythicResponse:
        """
        Create a new credential associated with the user's current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/credentials/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(credential.id),
        )
        resp = await self.put_json(url, data=await obj_to_json(credential))
        if resp.response_code == 200:
            # update the response with APIToken objects instead of just a dictionary
            resp.response = Credential(**resp.response)
        return resp

    # =============== DISABLED COMMANDS PROFILES ENDPOINTS =======

    async def get_all_disabled_commands_profiles(self) -> MythicResponse:
        """
        Get all of the disabled command profiles associated with Mythic
        :return:
        """
        url = "{}{}:{}/api/v{}/operations/disabled_commands_profiles".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200:
            profile_entries = []
            for name, ptypes in resp.response["disabled_command_profiles"].items():
                new_entry = DisabledCommandsProfile(name=name, payload_types=[])
                for ptype, commands in ptypes.items():
                    payload_type = PayloadType(ptype=ptype, commands=[])
                    for command in commands:
                        payload_type.commands.append(
                            Command(cmd=command["command"], id=command["command_id"])
                        )
                    new_entry.payload_types.append(payload_type)
                profile_entries.append(new_entry)
            resp.response = profile_entries
        return resp

    async def create_disabled_commands_profile(
        self, profile: DisabledCommandsProfile
    ) -> MythicResponse:
        """
        Create a new disabled command profiles associated with Mythic
        :return:
        """
        url = "{}{}:{}/api/v{}/operations/disabled_commands_profile".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        data = {profile.name: {}}
        for payload_type in profile.payload_types:
            data[profile.name][payload_type.ptype] = []
            for command in payload_type.commands:
                data[profile.name][payload_type.ptype].append(command.cmd)
        resp = await self.post_json(url, data=data)
        if resp.response_code == 200 and resp.status == "success":
            profile_entries = []
            for entry in resp.response["disabled_command_profile"]:
                # first check if we have a profile for this
                found = False
                for p in profile_entries:
                    if p.name == entry["name"]:
                        found = True
                        ptype_found = False
                        for payload_type in p.payload_types:
                            if payload_type.ptype == entry["payload_type"]:
                                ptype_found = True
                                payload_type.commands.append(
                                    Command(
                                        cmd=entry["command"], id=entry["command_id"]
                                    )
                                )
                        if not ptype_found:
                            p.payload_types.append(
                                PayloadType(
                                    ptype=entry["payload_type"],
                                    commands=[
                                        Command(
                                            cmd=entry["command"], id=entry["command_id"]
                                        )
                                    ],
                                )
                            )
                if not found:
                    dcp = DisabledCommandsProfile(name=entry["name"], payload_types=[])
                    dcp.payload_types.append(
                        PayloadType(
                            ptype=entry["payload_type"],
                            commands=[
                                Command(cmd=entry["command"], id=entry["command_id"])
                            ],
                        )
                    )
                    profile_entries.append(dcp)
            resp.response = profile_entries
        return resp

    async def update_disabled_commands_profile(
        self, profile: DisabledCommandsProfile
    ) -> MythicResponse:
        """
        Create a new disabled command profiles associated with Mythic
        :return:
        """
        url = "{}{}:{}/api/v{}/operations/disabled_commands_profile".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        data = {profile.name: {}}
        for payload_type in profile.payload_types:
            data[profile.name][payload_type.ptype] = []
            for command in payload_type.commands:
                data[profile.name][payload_type.ptype].append(command.cmd)
        resp = await self.put_json(url, data=data)
        if resp.response_code == 200 and resp.status == "success":
            profile_entries = []
            for entry in resp.response["disabled_command_profile"]:
                # first check if we have a profile for this
                found = False
                for p in profile_entries:
                    if p.name == entry["name"]:
                        found = True
                        ptype_found = False
                        for payload_type in p.payload_types:
                            if payload_type.ptype == entry["payload_type"]:
                                ptype_found = True
                                payload_type.commands.append(
                                    Command(
                                        cmd=entry["command"], id=entry["command_id"]
                                    )
                                )
                        if not ptype_found:
                            p.payload_types.append(
                                PayloadType(
                                    ptype=entry["payload_type"],
                                    commands=[
                                        Command(
                                            cmd=entry["command"], id=entry["command_id"]
                                        )
                                    ],
                                )
                            )
                if not found:
                    dcp = DisabledCommandsProfile(name=entry["name"], payload_types=[])
                    dcp.payload_types.append(
                        PayloadType(
                            ptype=entry["payload_type"],
                            commands=[
                                Command(cmd=entry["command"], id=entry["command_id"])
                            ],
                        )
                    )
                    profile_entries.append(dcp)
            resp.response = profile_entries
        return resp

    async def update_disabled_commands_profile_for_operator(
        self,
        profile: Union[DisabledCommandsProfile, str],
        operator: Operator,
        operation: Operation,
    ) -> MythicResponse:
        # async def add_or_update_operator_for_operation(self, operation: Operation, operator: Operator)
        if isinstance(profile, DisabledCommandsProfile):
            operator.base_disabled_commands = profile.name
        else:
            operator.base_disabled_commands = profile
        resp = await self.add_or_update_operator_for_operation(operation, operator)
        return resp

    # =============== EVENT LOG MESSAGES ========================

    async def get_all_event_messages(self) -> MythicResponse:
        """
        Get all of the event messages associated with Mythic for the current operation that are not deleted
        :return:
        """
        url = "{}{}:{}/api/v{}/event_message".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.get_json(url)
        if resp.response_code == 200 and resp.status == "success":
            resp.response = [EventMessage(**x) for x in resp.response["alerts"]]
        return resp

    async def create_event_message(self, message: EventMessage) -> MythicResponse:
        """
        Create new event message for the current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/event_message".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.post_json(url, data=await obj_to_json(message))
        if resp.response_code == 200 and resp.status == "success":
            resp.response = EventMessage(resp.response)
        return resp

    async def update_event_message(self, message: EventMessage) -> MythicResponse:
        """
        Update event message for the current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/event_message/{}".format(
            self._http,
            self.server_ip,
            self._server_port,
            self._server_api_version,
            str(message.id),
        )
        resp = await self.put_json(url, data=await obj_to_json(message))
        if resp.response_code == 200 and resp.status == "success":
            resp.response = EventMessage(resp.response)
        return resp

    async def remove_event_message(self, message: EventMessage) -> MythicResponse:
        """
        Update event message for the current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/event_message/delete".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        resp = await self.post_json(url, data={"messages": [message.id]})
        if resp.response_code == 200 and resp.status == "success":
            resp.response = EventMessage(resp.response)
        return resp

    async def remove_event_messages(self, messages: List) -> MythicResponse:
        """
        Update event message for the current operation
        :return:
        """
        url = "{}{}:{}/api/v{}/event_message/delete".format(
            self._http, self.server_ip, self._server_port, self._server_api_version
        )
        msgs = [m.id for m in messages]
        resp = await self.post_json(url, data={"messages": msgs})
        if resp.response_code == 200 and resp.status == "success":
            resp.response = EventMessage(resp.response)
        return resp

    # ============= CUSTOM HELPER FUNCTIONS ======================

    async def login(self):
        """
        Login with username/password and store resulting access_token and refresh_token
        """
        url = "{}{}:{}/auth".format(self._http, self._server_ip, self._server_port)
        data = {"username": self.username, "password": self.password}
        resp = await self.post_json(url, data)
        if resp.response_code == 200:
            self._access_token = resp.response["access_token"]
            self._refresh_token = resp.response["refresh_token"]
            return resp
        else:
            raise Exception("Failed to log in: " + json.dumps(resp, indent=2, default=lambda o: o.to_json()))
            sys.exit(1)

    async def set_or_create_apitoken(self, token_type="User"):
        """
        Use current auth to check if there are any user tokens. Either get one or create a new user one
        """
        resp = await self.get_apitokens()
        if resp.status == "success":
            for x in resp.response:
                if x.token_type == token_type:
                    self._apitoken = x
                    resp.response = x
                    return resp
        # if we get here, then we don't have a token of the right type for us to just leverage, so we need to get one
        token_resp = await self.create_apitoken(token_type=token_type)
        if token_resp.response_code == 200:
            self._apitoken = token_resp.response
        return token_resp

    async def wait_for_task_status_change(self, task_id, status, timeout=None):
        """
        Uses websockets to listen for notifications related to the specified task within a certain period of time
        if self.timeout is -1, then wait indefinitely
        :param task_id:
        :param status: the status we're waiting for (error is always included)
        :return:
        """
        if timeout is None:
            timeout = self.global_timeout
        url = "{}{}:{}/ws/task/{}".format(
            self._ws, self._server_ip, self._server_port, str(task_id)
        )
        headers = self.get_headers()
        try:
            async with aiohttp.ClientSession() as session:
                ws = await session.ws_connect(url, headers=headers, ssl=False)
                start = time()
                while True:
                    try:
                        if timeout > 0 and (time() - start >= timeout):
                            raise Exception("wait_for_task_status_change has timed out")
                        msg = await ws.receive()
                        if msg.data is None:
                            return None
                        if msg.data != "":
                            task = Task(**json.loads(msg.data))
                            if (
                                task.status == "error"
                                or task.completed == True
                                or task.status.lower() == status.lower()
                            ):
                                return task
                    except Exception as e:
                        raise Exception("Exception while waiting for task status change: " + str(e))
        except Exception as e:
            raise Exception("Exception in outer try/catch while waiting for task status change: " + str(e))

    async def wait_for_payload_status_change(self, payload_uuid, status, timeout=None):
        """
        Uses websockets to listen for notifications related to the specified pyaload within a certain period of time
        if self.timeout is -1, then wait indefinitely
        :param payload_uuid:
        :param status: the status we're waiting for (error is always included)
        :return:
        """
        if timeout is None:
            timeout = self.global_timeout
        url = "{}{}:{}/ws/payloads/{}".format(
            self._ws, self._server_ip, self._server_port, str(payload_uuid)
        )
        headers = self.get_headers()
        try:
            async with aiohttp.ClientSession() as session:
                ws = await session.ws_connect(url, headers=headers, ssl=False)
                start = time()
                while True:
                    try:
                        if timeout > 0 and (time() - start >= timeout):
                            raise Exception(
                                "wait_for_payload_status_change has timed out"
                            )
                        msg = await ws.receive()
                        if msg.data is None:
                            return None
                        if msg.data != "":
                            payload = Payload(**json.loads(msg.data))
                            if (
                                payload.build_phase == "error"
                                or payload.deleted == True
                                or payload.build_phase == status
                            ):
                                return payload
                    except Exception as e:
                        raise Exception("Exception while waiting for payload status change: " + str(e))
        except Exception as e:
            raise Exception("Exception in outer try/catch while waiting for payload status change: " + str(e))

    # ============= WEBSOCKET NOTIFICATION FUNCTIONS ===============

    async def listen_for_all_notifications_on_one_callback(
        self, callback_id, callback_function=None, timeout=None
    ):
        """
        Uses websockets to listen for all notifications related to a specific callback and prints to the screen.
        To stop listening, call cancel() on the result from this function call
        :param callback_id:
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/unified_callback/{}".format(
            self._ws, self._server_ip, self._server_port, str(callback_id)
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_new_callbacks(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for all notifications related new callbacks.
        To stop listening, call cancel() on the result from this function call
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/new_callbacks/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_responses_for_task(
        self, task_id, callback_function=None, timeout=None
    ):
        """
        Uses websockets to listen for all responses on a given task
        To stop listening, call cancel() on the result from this function call
        :param callback_id:
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/responses/by_task/{}".format(
            self._ws, self._server_ip, self._server_port, str(task_id)
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def gather_task_responses(self, task_id, timeout=None) -> List:
        """
        Uses websockets to listen for all responses related to task_id and gather them together into an array until the task is completed or errored.
        :param callback_id:
        :param callback_function: gets called on each notification
        :return:
        """
        if timeout is None:
            timeout = self.global_timeout
        url = "{}{}:{}/ws/responses/by_task/{}".format(
            self._ws, self._server_ip, self._server_port, str(task_id)
        )
        headers = self.get_headers()
        responses = []
        try:
            async with aiohttp.ClientSession() as session:
                ws = await session.ws_connect(url, headers=headers, ssl=False)
                start = time()
                while True:
                    try:
                        if timeout > 0 and (time() - start >= timeout):
                            raise Exception("gather_task_responses has timed out")
                        msg = await ws.receive()
                        if msg.data is None:
                            return responses
                        if msg.data != "":
                            rsp = Response(**json.loads(msg.data))
                            # await json_print(rsp)
                            responses.append(rsp)
                            if rsp.task.status == "error" or rsp.task.completed == True:
                                return responses
                    except Exception as e:
                        raise Exception("Exception while gathering responses: " + str(e))
        except Exception as e:
            raise Exception("Exception in our try/catch while gathering responses: " + str(e))

    async def listen_for_all_files(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for all file notifications within mythic for the current operation.
        This includes payloads, uploads, downloads, screenshots.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/files/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_new_files(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for all file notifications within mythic for the current operation.
        This includes uploads, downloads.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/files/new/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_all_responses(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for all response notifications within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/responses/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_new_responses(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for all new response notifications within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/responses/new/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_all_tasks(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for all tasks within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/tasks/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_new_tasks(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for all new tasks within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/tasks/new/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_all_payloads(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for all payloads within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/payloads/info/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_all_credentials(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for credentials within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/credentials/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_new_credentials(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for new credentials within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/credentials/new/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_all_event_messages(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for event messages within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/events_all/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task

    async def listen_for_new_event_messages(self, callback_function=None, timeout=None):
        """
        Uses websockets to listen for new event messages within mythic for the current operation.
        :param callback_function: gets called on each notification
        :return:
        """
        url = "{}{}:{}/ws/events_notifier/current_operation".format(
            self._ws, self._server_ip, self._server_port
        )
        if callback_function:
            task = await self.stream_output(url, callback_function, timeout)
        else:
            task = await self.stream_output(url, self.print_websocket_output, timeout)
        return task
