diff --git a/alembic/versions/094dd28a1d8d_add_thread_association.py b/alembic/versions/094dd28a1d8d_add_thread_association.py new file mode 100644 index 00000000..437b3272 --- /dev/null +++ b/alembic/versions/094dd28a1d8d_add_thread_association.py @@ -0,0 +1,50 @@ +"""Add Thread association + +Revision ID: 094dd28a1d8d +Revises: d02bf381e3ed +Create Date: 2025-11-15 19:07:22.805825 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '094dd28a1d8d' +down_revision = 'd02bf381e3ed' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('inbox_mail_thread_association', + sa.Column('id', postgresql.UUID(as_uuid=True), nullable=False), + sa.Column('thread_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.Column('user_id', postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint(['thread_id'], ['global.inbox_mail_thread.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['user.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + schema='global' + ) + op.create_index(op.f('ix_global_inbox_mail_thread_association_thread_id'), 'inbox_mail_thread_association', ['thread_id'], unique=False, schema='global') + op.create_index(op.f('ix_global_inbox_mail_thread_association_user_id'), 'inbox_mail_thread_association', ['user_id'], unique=False, schema='global') + op.add_column('inbox_mail_thread', sa.Column('created_by', postgresql.UUID(as_uuid=True), nullable=True), schema='global') + op.drop_index('idx_inbox_thread_participants_gin', table_name='inbox_mail_thread', schema='global') + op.create_index(op.f('ix_global_inbox_mail_thread_created_by'), 'inbox_mail_thread', ['created_by'], unique=False, schema='global') + op.create_foreign_key(None, 'inbox_mail_thread', 'user', ['created_by'], ['id'], source_schema='global', ondelete='CASCADE') + op.drop_column('inbox_mail_thread', 'participant_ids', schema='global') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail_thread', sa.Column('participant_ids', postgresql.ARRAY(sa.VARCHAR()), autoincrement=False, nullable=True), schema='global') + op.drop_constraint(None, 'inbox_mail_thread', schema='global', type_='foreignkey') + op.drop_index(op.f('ix_global_inbox_mail_thread_created_by'), table_name='inbox_mail_thread', schema='global') + op.create_index('idx_inbox_thread_participants_gin', 'inbox_mail_thread', ['participant_ids'], unique=False, schema='global') + op.drop_column('inbox_mail_thread', 'created_by', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_thread_association_user_id'), table_name='inbox_mail_thread_association', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_thread_association_thread_id'), table_name='inbox_mail_thread_association', schema='global') + op.drop_table('inbox_mail_thread_association', schema='global') + # ### end Alembic commands ### diff --git a/alembic/versions/8b89db0d41fd_unread_mail_count_thread.py b/alembic/versions/8b89db0d41fd_unread_mail_count_thread.py new file mode 100644 index 00000000..c0afaff5 --- /dev/null +++ b/alembic/versions/8b89db0d41fd_unread_mail_count_thread.py @@ -0,0 +1,28 @@ +"""Unread mail count thread + +Revision ID: 8b89db0d41fd +Revises: c95b049b0779 +Create Date: 2025-11-17 14:28:02.843729 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '8b89db0d41fd' +down_revision = 'c95b049b0779' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail_thread_association', sa.Column('unread_mail_count', sa.Integer(), nullable=True), schema='global') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('inbox_mail_thread_association', 'unread_mail_count', schema='global') + # ### end Alembic commands ### diff --git a/alembic/versions/b59188cf5526_change_inbox_model.py b/alembic/versions/b59188cf5526_change_inbox_model.py new file mode 100644 index 00000000..83cf240b --- /dev/null +++ b/alembic/versions/b59188cf5526_change_inbox_model.py @@ -0,0 +1,62 @@ +"""Change inbox model + +Revision ID: b59188cf5526 +Revises: ebf4435d83f8 +Create Date: 2025-11-12 10:01:36.389914 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'b59188cf5526' +down_revision = 'ebf4435d83f8' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail', sa.Column('sender_id', postgresql.UUID(as_uuid=True), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('recipient_id', postgresql.UUID(as_uuid=True), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('other_recipient_ids', sa.JSON(), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('thread_id', postgresql.UUID(as_uuid=True), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('is_important', sa.Boolean(), nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('being_working_on', sa.Boolean(), nullable=True), schema='global') + op.drop_index('ix_global_inbox_mail_child_id', table_name='inbox_mail', schema='global') + op.create_index(op.f('ix_global_inbox_mail_recipient_id'), 'inbox_mail', ['recipient_id'], unique=False, schema='global') + op.create_index(op.f('ix_global_inbox_mail_sender_id'), 'inbox_mail', ['sender_id'], unique=False, schema='global') + op.create_index(op.f('ix_global_inbox_mail_thread_id'), 'inbox_mail', ['thread_id'], unique=False, schema='global') + op.drop_constraint('inbox_mail_child_id_fkey', 'inbox_mail', schema='global', type_='foreignkey') + op.create_foreign_key(None, 'inbox_mail', 'user', ['sender_id'], ['id'], source_schema='global', ondelete='SET NULL') + op.create_foreign_key(None, 'inbox_mail', 'user', ['recipient_id'], ['id'], source_schema='global', ondelete='CASCADE') + op.drop_column('inbox_mail', 'mark_as_important', schema='global') + op.drop_column('inbox_mail', 'being_worked_on', schema='global') + op.drop_column('inbox_mail', 'child_id', schema='global') + op.drop_column('inbox_mail', 'send_from', schema='global') + op.drop_column('inbox_mail', 'send_to', schema='global') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail', sa.Column('send_to', postgresql.JSON(astext_type=sa.Text()), autoincrement=False, nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('send_from', sa.VARCHAR(), autoincrement=False, nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('child_id', postgresql.UUID(), autoincrement=False, nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('being_worked_on', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='global') + op.add_column('inbox_mail', sa.Column('mark_as_important', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='global') + op.drop_constraint(None, 'inbox_mail', schema='global', type_='foreignkey') + op.drop_constraint(None, 'inbox_mail', schema='global', type_='foreignkey') + op.create_foreign_key('inbox_mail_child_id_fkey', 'inbox_mail', 'inbox_mail', ['child_id'], ['id'], source_schema='global', referent_schema='global', ondelete='SET NULL') + op.drop_index(op.f('ix_global_inbox_mail_thread_id'), table_name='inbox_mail', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_sender_id'), table_name='inbox_mail', schema='global') + op.drop_index(op.f('ix_global_inbox_mail_recipient_id'), table_name='inbox_mail', schema='global') + op.create_index('ix_global_inbox_mail_child_id', 'inbox_mail', ['child_id'], unique=False, schema='global') + op.drop_column('inbox_mail', 'being_working_on', schema='global') + op.drop_column('inbox_mail', 'is_important', schema='global') + op.drop_column('inbox_mail', 'thread_id', schema='global') + op.drop_column('inbox_mail', 'other_recipient_ids', schema='global') + op.drop_column('inbox_mail', 'recipient_id', schema='global') + op.drop_column('inbox_mail', 'sender_id', schema='global') + # ### end Alembic commands ### diff --git a/alembic/versions/c95b049b0779_rename_working_on.py b/alembic/versions/c95b049b0779_rename_working_on.py new file mode 100644 index 00000000..f50d0d92 --- /dev/null +++ b/alembic/versions/c95b049b0779_rename_working_on.py @@ -0,0 +1,30 @@ +"""Rename working on + +Revision ID: c95b049b0779 +Revises: 094dd28a1d8d +Create Date: 2025-11-17 10:58:16.971432 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = 'c95b049b0779' +down_revision = '094dd28a1d8d' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail_thread', sa.Column('is_in_progress', sa.Boolean(), nullable=True), schema='global') + op.drop_column('inbox_mail_thread', 'being_working_on', schema='global') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('inbox_mail_thread', sa.Column('being_working_on', sa.BOOLEAN(), autoincrement=False, nullable=True), schema='global') + op.drop_column('inbox_mail_thread', 'is_in_progress', schema='global') + # ### end Alembic commands ### diff --git a/alembic/versions/d02bf381e3ed_restructure_inbox_mail_threads.py b/alembic/versions/d02bf381e3ed_restructure_inbox_mail_threads.py new file mode 100644 index 00000000..b3b62c05 --- /dev/null +++ b/alembic/versions/d02bf381e3ed_restructure_inbox_mail_threads.py @@ -0,0 +1,240 @@ +"""Restructure inbox mail threads + +Revision ID: d02bf381e3ed +Revises: b59188cf5526 +Create Date: 2025-11-15 18:44:47.229926 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "d02bf381e3ed" +down_revision = "b59188cf5526" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "inbox_mail_thread", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("organization_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("subject", sa.String(), nullable=True), + sa.Column("participant_ids", sa.ARRAY(sa.String()), nullable=True), + sa.Column("meta_data", sa.JSON(), nullable=True), + sa.Column("is_important", sa.Boolean(), nullable=True), + sa.Column("being_working_on", sa.Boolean(), nullable=True), + sa.Column("is_admin_support_thread", sa.Boolean(), nullable=True), + sa.ForeignKeyConstraint( + ["organization_id"], ["organization.id"], ondelete="CASCADE" + ), + sa.PrimaryKeyConstraint("id"), + schema="global", + ) + op.create_index( + "idx_inbox_thread_participants_gin", + "inbox_mail_thread", + ["participant_ids"], + unique=False, + schema="global", + postgresql_using="gin", + postgresql_ops={"participant_ids": "array_ops"}, + ) + op.create_index( + op.f("ix_global_inbox_mail_thread_organization_id"), + "inbox_mail_thread", + ["organization_id"], + unique=False, + schema="global", + ) + op.drop_index( + "ix_global_inbox_mail_reference_inbox_mail_id", + table_name="inbox_mail_reference", + schema="global", + ) + op.drop_index( + "ix_global_inbox_mail_reference_user_id", + table_name="inbox_mail_reference", + schema="global", + ) + op.drop_table("inbox_mail_reference", schema="global") + op.drop_index( + "ix_global_inbox_mail_organization_id", table_name="inbox_mail", schema="global" + ) + op.drop_index( + "ix_global_inbox_mail_parent_id", table_name="inbox_mail", schema="global" + ) + op.drop_constraint( + "inbox_mail_parent_id_fkey", "inbox_mail", schema="global", type_="foreignkey" + ) + op.drop_constraint( + "inbox_mail_organization_id_fkey", + "inbox_mail", + schema="global", + type_="foreignkey", + ) + op.create_foreign_key( + None, + "inbox_mail", + "inbox_mail_thread", + ["thread_id"], + ["id"], + source_schema="global", + referent_schema="global", + ondelete="CASCADE", + ) + op.drop_column("inbox_mail", "being_working_on", schema="global") + op.drop_column("inbox_mail", "original_recipient_ids", schema="global") + op.drop_column("inbox_mail", "organization_id", schema="global") + op.drop_column("inbox_mail", "parent_id", schema="global") + op.drop_column("inbox_mail", "subject", schema="global") + op.drop_column("inbox_mail", "is_important", schema="global") + op.drop_column("inbox_mail", "meta_data", schema="global") + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "inbox_mail", + sa.Column( + "meta_data", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column("is_important", sa.BOOLEAN(), autoincrement=False, nullable=True), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column("subject", sa.VARCHAR(), autoincrement=False, nullable=True), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column("parent_id", postgresql.UUID(), autoincrement=False, nullable=True), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column( + "organization_id", postgresql.UUID(), autoincrement=False, nullable=True + ), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column( + "original_recipient_ids", + postgresql.JSON(astext_type=sa.Text()), + autoincrement=False, + nullable=True, + ), + schema="global", + ) + op.add_column( + "inbox_mail", + sa.Column("being_working_on", sa.BOOLEAN(), autoincrement=False, nullable=True), + schema="global", + ) + op.drop_constraint( + "inbox_mail_thread_id_fkey", + "inbox_mail", + schema="global", + type_="foreignkey", + ) + op.create_foreign_key( + "inbox_mail_organization_id_fkey", + "inbox_mail", + "organization", + ["organization_id"], + ["id"], + source_schema="global", + ondelete="CASCADE", + ) + op.create_foreign_key( + "inbox_mail_parent_id_fkey", + "inbox_mail", + "inbox_mail", + ["parent_id"], + ["id"], + source_schema="global", + referent_schema="global", + ondelete="SET NULL", + ) + op.create_index( + "ix_global_inbox_mail_parent_id", + "inbox_mail", + ["parent_id"], + unique=False, + schema="global", + ) + op.create_index( + "ix_global_inbox_mail_organization_id", + "inbox_mail", + ["organization_id"], + unique=False, + schema="global", + ) + op.create_table( + "inbox_mail_reference", + sa.Column("id", postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column( + "inbox_mail_id", postgresql.UUID(), autoincrement=False, nullable=False + ), + sa.Column("scope", sa.VARCHAR(), autoincrement=False, nullable=False), + sa.Column("user_id", postgresql.UUID(), autoincrement=False, nullable=False), + sa.Column("is_seen", sa.BOOLEAN(), autoincrement=False, nullable=True), + sa.ForeignKeyConstraint( + ["inbox_mail_id"], + ["global.inbox_mail.id"], + name="inbox_mail_reference_inbox_mail_id_fkey", + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["user_id"], + ["user.id"], + name="inbox_mail_reference_user_id_fkey", + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("id", name="inbox_mail_reference_pkey"), + schema="global", + ) + op.create_index( + "ix_global_inbox_mail_reference_user_id", + "inbox_mail_reference", + ["user_id"], + unique=False, + schema="global", + ) + op.create_index( + "ix_global_inbox_mail_reference_inbox_mail_id", + "inbox_mail_reference", + ["inbox_mail_id"], + unique=False, + schema="global", + ) + op.drop_index( + op.f("ix_global_inbox_mail_thread_organization_id"), + table_name="inbox_mail_thread", + schema="global", + ) + op.drop_index( + "idx_inbox_thread_participants_gin", + table_name="inbox_mail_thread", + schema="global", + postgresql_using="gin", + postgresql_ops={"participant_ids": "array_ops"}, + ) + op.drop_table("inbox_mail_thread", schema="global") + # ### end Alembic commands ### diff --git a/alembic/versions/ebf4435d83f8_inbox_mail_table.py b/alembic/versions/ebf4435d83f8_inbox_mail_table.py new file mode 100644 index 00000000..f4f4ad67 --- /dev/null +++ b/alembic/versions/ebf4435d83f8_inbox_mail_table.py @@ -0,0 +1,87 @@ +"""Inbox mail table + +Revision ID: ebf4435d83f8 +Revises: 85bb3ebee137 +Create Date: 2025-11-06 13:44:57.938408 + +""" + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "ebf4435d83f8" +down_revision = "85bb3ebee137" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "inbox_mail", + sa.Column("id", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("organization_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("created_at", sa.DateTime(), nullable=True), + sa.Column("send_from", sa.String(), nullable=True), + sa.Column("send_to", sa.JSON(), nullable=True), + sa.Column("subject", sa.String(), nullable=True), + sa.Column("content", sa.String(), nullable=True), + sa.Column("mark_as_important", sa.Boolean(), nullable=True), + sa.Column("meta_data", sa.JSON(), nullable=True), + sa.Column("is_seen", sa.Boolean(), nullable=True), + sa.Column("being_worked_on", sa.Boolean(), nullable=True), + sa.Column("parent_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.Column("child_id", postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint( + ["child_id"], ["global.inbox_mail.id"], ondelete="SET NULL" + ), + sa.ForeignKeyConstraint( + ["organization_id"], ["organization.id"], ondelete="CASCADE" + ), + sa.ForeignKeyConstraint( + ["parent_id"], ["global.inbox_mail.id"], ondelete="SET NULL" + ), + sa.PrimaryKeyConstraint("id"), + schema="global", + ) + op.create_index( + op.f("ix_global_inbox_mail_child_id"), + "inbox_mail", + ["child_id"], + unique=False, + schema="global", + ) + op.create_index( + op.f("ix_global_inbox_mail_organization_id"), + "inbox_mail", + ["organization_id"], + unique=False, + schema="global", + ) + op.create_index( + op.f("ix_global_inbox_mail_parent_id"), + "inbox_mail", + ["parent_id"], + unique=False, + schema="global", + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index( + op.f("ix_global_inbox_mail_parent_id"), table_name="inbox_mail", schema="global" + ) + op.drop_index( + op.f("ix_global_inbox_mail_organization_id"), + table_name="inbox_mail", + schema="global", + ) + op.drop_index( + op.f("ix_global_inbox_mail_child_id"), table_name="inbox_mail", schema="global" + ) + op.drop_table("inbox_mail", schema="global") + # ### end Alembic commands ### diff --git a/app.py b/app.py index 6c973e29..a631713b 100644 --- a/app.py +++ b/app.py @@ -34,6 +34,7 @@ from fast_api.routes.task_execution import router as task_execution_router from fast_api.routes.record_internal import router as record_internal_router from fast_api.routes.playground import router as playground_router +from fast_api.routes.inbox_mail import router as inbox_mail_router from middleware.database_session import handle_db_session from middleware.starlette_tmp_middleware import DatabaseSessionHandler from starlette.applications import Starlette @@ -62,6 +63,7 @@ PREFIX_LABELING_TASKS, PREFIX_TASK_EXECUTION, PREFIX_PLAYGROUND, + PREFIX_INBOX_MAIL, ) from util import security, clean_up from middleware import log_storage @@ -121,6 +123,10 @@ playground_router, prefix=PREFIX_PLAYGROUND, tags=["playground"] ) +fastapi_app.include_router( + inbox_mail_router, prefix=PREFIX_INBOX_MAIL, tags=["inbox_mail"] +) + app_name_internal = app_name + "-i" fastapi_app_internal = FastAPI(title=app_name_internal) diff --git a/controller/auth/kratos.py b/controller/auth/kratos.py index bd7b7b31..473ec364 100644 --- a/controller/auth/kratos.py +++ b/controller/auth/kratos.py @@ -7,7 +7,8 @@ from datetime import datetime, timedelta from urllib.parse import quote -from controller.user import manager +from submodules.model import daemon +from submodules.model.business_objects import general, user logging.basicConfig(level=logging.INFO) @@ -76,7 +77,7 @@ def __refresh_identity_cache(update_db_users: bool = True) -> None: KRATOS_IDENTITY_CACHE = {} if update_db_users: - manager.migrate_kratos_users() + migrate_kratos_users() def __get_link_from_kratos_request(request: requests.Response) -> str: @@ -293,3 +294,49 @@ def check_user_exists(email: str) -> bool: if i["traits"]["email"].lower() == email.lower(): return True return False + + +def migrate_kratos_users() -> None: + daemon.run_with_db_token(__migrate_kratos_users) + + +def __migrate_kratos_users(): + users_kratos = get_cached_values(False) + users_database = user.get_all() + + for user_database in users_database: + user_id = str(user_database.id) + if user_id not in users_kratos or users_kratos[user_id] is None: + continue + user_identity = users_kratos[user_id]["identity"] + if user_database.email != user_identity["traits"]["email"]: + user_database.email = user_identity["traits"]["email"] + if ( + user_database.verified + != user_identity["verifiable_addresses"][0]["verified"] + ): + user_database.verified = user_identity["verifiable_addresses"][0][ + "verified" + ] + if ( + user_database.created_at + != user_identity["verifiable_addresses"][0]["created_at"] + ): + user_database.created_at = user_identity["verifiable_addresses"][0][ + "created_at" + ] + if user_database.metadata_public != user_identity["metadata_public"]: + user_database.metadata_public = user_identity["metadata_public"] + sso_provider = ( + ( + user_identity["metadata_public"] + .get("registration_scope", {}) + .get("provider_id", None) + ) + if user_identity["metadata_public"] + else None + ) + if user_database.sso_provider != sso_provider: + user_database.sso_provider = sso_provider + + general.commit() diff --git a/controller/inbox_mail/__init__.py b/controller/inbox_mail/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/controller/inbox_mail/manager.py b/controller/inbox_mail/manager.py new file mode 100644 index 00000000..0289e941 --- /dev/null +++ b/controller/inbox_mail/manager.py @@ -0,0 +1,196 @@ +from typing import Any, Dict, List, Optional +from submodules.model.business_objects import general, user +from submodules.model.global_objects import inbox_mail +from submodules.model.util import sql_alchemy_to_dict +from controller.auth import kratos +from sqlalchemy.orm.attributes import flag_modified + +DEFAULT_KERN_AI_ADMIN_NAME = {"first": "Kern AI Support", "last": ""} +DEFAULT_SYSTEM_AUTOGENERATED_NAME = {"first": "System", "last": ""} + + +def create_inbox_mail_by_thread( + org_id: str, + sender_id: str, + recipient_ids: List[str], + subject: str, + content: str, + is_important: bool = False, + is_admin_support_thread: bool = False, + meta_data: Optional[Dict[str, Any]] = None, + thread_id: Optional[str] = None, +) -> Dict[str, Any]: + + return inbox_mail.create_by_thread( + org_id=org_id, + sender_id=sender_id, + content=content, + recipient_ids=recipient_ids, + subject=subject, + meta_data=meta_data, + thread_id=thread_id, + is_important=is_important, + is_admin_support_thread=is_admin_support_thread, + ) + + +def get_inbox_mails_by_thread( + org_id: str, + user_id: str, + thread_id: Optional[str] = None, + user_is_admin: bool = False, +) -> List[Dict[str, Any]]: + mails = inbox_mail.get_by_thread( + org_id, user_id, thread_id, user_is_admin=user_is_admin + ) + participant_ids = [ + str(pid) for pid in inbox_mail.get_participant_ids_by_thread_id(thread_id) + ] + admin_user_ids = [str(u.id) for u in user.get_admin_users()] + + mail_dicts = [sql_alchemy_to_dict(mail) for mail in mails] + thread_entity = inbox_mail.get_inbox_mail_thread_by_id(thread_id) + thread_association = ( + inbox_mail.get_inbox_mail_thread_association_by_thread_id_and_user_id( + thread_id, user_id + ) + ) + if thread_association: + thread_association.unread_mail_count = 0 + general.flush_or_commit(True) + if thread_entity and thread_entity.is_admin_support_thread: + meta_data = thread_entity.meta_data or {} + if user_id in admin_user_ids: + if meta_data.get("unreadMailCountAdmin", 0) > 0: + meta_data["unreadMailCountAdmin"] = 0 + thread_entity.meta_data = meta_data + flag_modified(thread_entity, "meta_data") + general.flush_or_commit(True) + for mail_dict in mail_dicts: + extend_inbox_mail_sender_receiver_names( + participant_ids=participant_ids, + mail_dict=mail_dict, + user_id=user_id, + admin_user_ids=admin_user_ids, + is_admin_support_thread=( + thread_entity.is_admin_support_thread if thread_entity else False + ), + is_auto_generated=thread_entity.meta_data.get("autoGenerated", False), + ) + return mail_dicts + + +def get_inbox_mail_threads_overview( + org_id: str, + user_id: str, + page: int = 1, + limit: int = 10, + user_is_admin: bool = False, +) -> Dict[str, Any]: + overview_by_threads = inbox_mail.get_overview_by_threads( + org_id=org_id, + user_id=user_id, + page=page, + limit=limit, + user_is_admin=user_is_admin, + ) + + admin_user_ids = [str(u.id) for u in user.get_admin_users()] + + for thread in overview_by_threads["threads"]: + if not thread.get("latest_mail"): + continue + extend_inbox_mail_sender_receiver_names( + participant_ids=thread["participant_ids"], + mail_dict=thread["latest_mail"], + user_id=user_id, + admin_user_ids=admin_user_ids, + is_admin_support_thread=thread.get("is_admin_support_thread", False), + is_auto_generated=thread.get("meta_data", {}).get("autoGenerated", False), + ) + + return overview_by_threads + + +def create_auto_generated_inbox_mail_thread_for_error( + org_id: str, + created_by: str, + subject: str, + content: str, + is_important: bool = False, + meta_data: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + + inbox_mail.create_by_thread( + org_id=org_id, + sender_id=None, + content=content, + recipient_ids=[], + subject=subject, + meta_data=meta_data, + thread_id=None, + is_important=is_important, + created_by=created_by, + is_admin_support_thread=True, + ) + + +def get_new_inbox_mails_info( + org_id: str, user_id: str, user_is_admin: bool +) -> Dict[str, int]: + return inbox_mail.get_new_inbox_mails( + org_id=org_id, user_id=user_id, user_is_admin=user_is_admin + ) + + +def extend_inbox_mail_sender_receiver_names( + participant_ids: List[str], + mail_dict: Dict[str, Any], + user_id: str, + admin_user_ids: list[str], + is_admin_support_thread: bool = False, + is_auto_generated: bool = False, +) -> None: + + recipient_ids = [ + rid for rid in participant_ids if str(rid) != str(mail_dict.get("sender_id")) + ] + if not is_admin_support_thread: + mail_dict["senderName"] = kratos.resolve_user_name_by_id(mail_dict["sender_id"]) + mail_dict["recipientNames"] = [ + kratos.resolve_user_name_by_id(rid) for rid in recipient_ids + ] + return + + user_is_admin = str(user_id) in admin_user_ids + user_is_sender = str(user_id) == str(mail_dict["sender_id"]) + sender_is_admin = str(mail_dict["sender_id"]) in admin_user_ids + print(is_auto_generated, user_is_admin, user_is_sender, sender_is_admin, flush=True) + if is_auto_generated and user_is_admin: + + if not (mail_dict.get("sender_id")): + mail_dict["senderName"] = DEFAULT_SYSTEM_AUTOGENERATED_NAME + mail_dict["recipientNames"] = [DEFAULT_KERN_AI_ADMIN_NAME] + else: + mail_dict["senderName"] = DEFAULT_KERN_AI_ADMIN_NAME + mail_dict["recipientNames"] = [DEFAULT_SYSTEM_AUTOGENERATED_NAME] + return + if (user_is_sender and not user_is_admin) or ( + user_is_admin and not sender_is_admin + ): + mail_dict["senderName"] = kratos.resolve_user_name_by_id(mail_dict["sender_id"]) + mail_dict["recipientNames"] = [DEFAULT_KERN_AI_ADMIN_NAME] + + elif not user_is_sender and not user_is_admin: + mail_dict["senderName"] = DEFAULT_KERN_AI_ADMIN_NAME + mail_dict["recipientNames"] = [ + kratos.resolve_user_name_by_id(rid) for rid in recipient_ids + ] + elif user_is_admin and user_is_sender and user_id in participant_ids: + mail_dict["senderName"] = kratos.resolve_user_name_by_id(mail_dict["sender_id"]) + mail_dict["recipientNames"] = [DEFAULT_KERN_AI_ADMIN_NAME] + elif user_is_admin and sender_is_admin: + mail_dict["senderName"] = DEFAULT_KERN_AI_ADMIN_NAME + mail_dict["recipientNames"] = [ + kratos.resolve_user_name_by_id(rid) for rid in recipient_ids + ] diff --git a/fast_api/models.py b/fast_api/models.py index e777496d..4e4878b4 100644 --- a/fast_api/models.py +++ b/fast_api/models.py @@ -536,3 +536,17 @@ class GetEmbeddingNameBody(BaseModel): class CreateUpdateReleaseNotificationBody(BaseModel): link: StrictStr config: Dict[str, Any] + + +class InboxMailCreateRequest(BaseModel): + recipientIds: List[str] + subject: str + content: str + isImportant: bool = False + isAdminSupportThread: bool = False + metaData: Optional[Dict[str, Any]] = None + threadId: Optional[str] = None + + +class UpdateInboxMailThreadProgressRequest(BaseModel): + progress: bool diff --git a/fast_api/routes/inbox_mail.py b/fast_api/routes/inbox_mail.py new file mode 100644 index 00000000..1d164b22 --- /dev/null +++ b/fast_api/routes/inbox_mail.py @@ -0,0 +1,145 @@ +from typing import List, Dict, Any +from controller.auth import manager as auth_manager +from fast_api.models import ( + InboxMailCreateRequest, + UpdateInboxMailThreadProgressRequest, +) +from controller.inbox_mail import manager as inbox_mail_manager +from submodules.model.global_objects import inbox_mail as inbox_mail_go +from submodules.model.business_objects import user +from fastapi import APIRouter, HTTPException, Request +from fast_api.routes.client_response import ( + get_silent_success, + pack_json_result, +) + +router = APIRouter() + + +@router.post("") +def create_inbox_mail_by_thread(request: Request, inbox_mail: InboxMailCreateRequest): + user_is_admin = auth_manager.check_is_admin(request) + user = auth_manager.get_user_by_info(request.state.info) + + if inbox_mail.threadId: + inbox_mail_thread = inbox_mail_go.get_inbox_mail_thread_by_id( + thread_id=inbox_mail.threadId, + ) + if not inbox_mail_thread: + raise HTTPException(status_code=404, detail="Thread not found") + + if not user_is_admin and ( + (str(inbox_mail_thread.organization_id) != str(user.organization_id)) + or not inbox_mail_go.get_inbox_mail_thread_association_by_thread_id_and_user_id( + thread_id=inbox_mail.threadId, + user_id=user.id, + ) + ): + raise HTTPException(status_code=403, detail="Not authorized") + + mail = inbox_mail_manager.create_inbox_mail_by_thread( + org_id=user.organization_id, + sender_id=user.id, + recipient_ids=inbox_mail.recipientIds, + subject=inbox_mail.subject, + content=inbox_mail.content, + meta_data=inbox_mail.metaData, + thread_id=inbox_mail.threadId, + is_important=inbox_mail.isImportant, + is_admin_support_thread=inbox_mail.isAdminSupportThread, + ) + return pack_json_result(mail) + + +@router.get("/thread/{thread_id}") +def get_inbox_mails_by_thread(request: Request, thread_id: str) -> List[Dict[str, Any]]: + user_is_admin = auth_manager.check_is_admin(request) + + user = auth_manager.get_user_by_info(request.state.info) + + mails = inbox_mail_manager.get_inbox_mails_by_thread( + org_id=user.organization_id, + user_id=user.id, + thread_id=thread_id, + user_is_admin=user_is_admin, + ) + return pack_json_result(mails) + + +@router.get("/overview") +def get_inbox_mail_thread_overview_paginated( + request: Request, page: int = 1, limit: int = 10 +): + user_is_admin = auth_manager.check_is_admin(request) + user = auth_manager.get_user_by_info(request.state.info) + print("is admin", user_is_admin, flush=True) + + mail = inbox_mail_manager.get_inbox_mail_threads_overview( + org_id=user.organization_id, + user_id=user.id, + page=page, + limit=limit, + user_is_admin=user_is_admin, + ) + return pack_json_result(mail) + + +@router.put("/thread/{thread_id}/progress") +def update_inbox_mail_thread_being_worked_on( + request: Request, + thread_id: str, + inbox_mail_thread_update: UpdateInboxMailThreadProgressRequest, +): + user_is_admin = auth_manager.check_is_admin(request) + user = auth_manager.get_user_by_info(request.state.info) + + inbox_mail_thread = inbox_mail_go.get_inbox_mail_thread_by_id(thread_id=thread_id) + if not inbox_mail_thread: + raise HTTPException(status_code=404, detail="Thread not found") + + if not user_is_admin: + raise HTTPException(status_code=403, detail="Not authorized") + + inbox_mail_go.update_thread_progress( + thread_id=thread_id, + is_in_progress=inbox_mail_thread_update.progress, + with_commit=True, + ) + return get_silent_success() + + +@router.delete("/{mail_id}") +def delete_inbox_mail_by_id(request: Request, mail_id: str): + inbox_mail_entity = inbox_mail_go.get(inbox_mail_id=mail_id) + user = auth_manager.get_user_by_info(request.state.info) + if not inbox_mail_entity: + raise HTTPException(status_code=404, detail="Mail not found") + + if str(inbox_mail_entity.sender_id) != str(user.id): + raise HTTPException(status_code=403, detail="Not authorized") + + thread_id = inbox_mail_entity.thread_id + inbox_mail_go.delete(inbox_mail_id=mail_id, with_commit=True) + # If no mails are left in the thread, delete the thread as well + remaining_mails = inbox_mail_go.get_inbox_mail_thread_length(thread_id=thread_id) + if remaining_mails == 0: + inbox_mail_go.delete_thread_by_id(thread_id) + return get_silent_success() + + +@router.get("/new") +def has_new_inbox_mails(request: Request): + + user_is_admin = auth_manager.check_is_admin(request) + user = auth_manager.get_user_by_info(request.state.info) + + total_new_inbox_mails = inbox_mail_manager.get_new_inbox_mails_info( + org_id=user.organization_id, + user_id=user.id, + user_is_admin=user_is_admin, + ) + return pack_json_result( + { + "totalNewInboxMails": total_new_inbox_mails, + } + ) diff --git a/route_prefix.py b/route_prefix.py index 890fab2d..ba5c6857 100644 --- a/route_prefix.py +++ b/route_prefix.py @@ -19,3 +19,4 @@ PREFIX_LABELING_TASKS = PREFIX + "/labeling-tasks" PREFIX_TASK_EXECUTION = PREFIX + "/task-execution" PREFIX_PLAYGROUND = PREFIX + "/playground" +PREFIX_INBOX_MAIL = PREFIX + "/inbox-mail" diff --git a/submodules/model b/submodules/model index b02850d1..b186fc81 160000 --- a/submodules/model +++ b/submodules/model @@ -1 +1 @@ -Subproject commit b02850d1417c7d29604c774a4a0d5904bb309687 +Subproject commit b186fc81c08576cc82423969517ee4c4ebac48a5