Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,12 @@ class ConversationRestGet(OutputSchema):
project_uuid: ProjectID | None
user_group_id: GroupID
type: ConversationType
fogbugz_case_id: str | None
created: datetime
modified: datetime
extra_context: dict[str, str]
is_read_by_user: bool
is_read_by_support: bool

@classmethod
def from_domain_model(cls, domain: ConversationGetDB) -> Self:
Expand All @@ -39,15 +42,20 @@ def from_domain_model(cls, domain: ConversationGetDB) -> Self:
project_uuid=domain.project_uuid,
user_group_id=domain.user_group_id,
type=domain.type,
fogbugz_case_id=domain.fogbugz_case_id,
created=domain.created,
modified=domain.modified,
extra_context=domain.extra_context,
is_read_by_user=domain.is_read_by_user,
is_read_by_support=domain.is_read_by_support,
)


class ConversationPatch(InputSchema):
name: str | None = None
extra_context: dict[str, Any] | None = None
is_read_by_user: bool | None = None
is_read_by_support: bool | None = None


### CONVERSATION MESSAGES ---------------------------------------------------------------
Expand Down
9 changes: 9 additions & 0 deletions packages/models-library/src/models_library/conversations.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ class ConversationMessageType(StrAutoEnum):
#


IsSupportUser: TypeAlias = bool


class ConversationGetDB(BaseModel):
conversation_id: ConversationID
product_name: ProductName
Expand All @@ -46,6 +49,9 @@ class ConversationGetDB(BaseModel):
user_group_id: GroupID
type: ConversationType
extra_context: dict[str, Any]
fogbugz_case_id: str | None
is_read_by_user: bool
is_read_by_support: bool

# states
created: datetime
Expand All @@ -71,6 +77,9 @@ class ConversationMessageGetDB(BaseModel):
class ConversationPatchDB(BaseModel):
name: ConversationName | None = None
extra_context: dict[str, Any] | None = None
fogbugz_case_id: str | None = None
is_read_by_user: bool | None = None
is_read_by_support: bool | None = None


class ConversationMessagePatchDB(BaseModel):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
"""add read by user/support columns

Revision ID: dfdd4f8d4870
Revises: 9dddb16914a4
Create Date: 2025-10-10 12:07:12.014847+00:00

"""

import sqlalchemy as sa
from alembic import op

# revision identifiers, used by Alembic.
revision = "dfdd4f8d4870"
down_revision = "9dddb16914a4"
branch_labels = None
depends_on = None


def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column(
"conversations", sa.Column("fogbugz_case_id", sa.String(), nullable=True)
)
op.add_column(
"conversations",
sa.Column(
"is_read_by_user",
sa.Boolean(),
server_default=sa.text("true"),
nullable=False,
),
)
op.add_column(
"conversations",
sa.Column(
"is_read_by_support",
sa.Boolean(),
server_default=sa.text("true"),
nullable=False,
),
)
# ### end Alembic commands ###


def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column("conversations", "is_read_by_support")
op.drop_column("conversations", "is_read_by_user")
op.drop_column("conversations", "fogbugz_case_id")
# ### end Alembic commands ###
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,26 @@ class ConversationType(enum.Enum):
server_default=sa.text("'{}'::jsonb"),
doc="Free JSON to store extra context",
),
sa.Column(
"fogbugz_case_id",
sa.String,
nullable=True,
doc="Fogbugz case ID associated with the conversation",
),
sa.Column(
"is_read_by_user",
sa.Boolean,
nullable=False,
server_default=sa.text("true"),
doc="Indicates if the message has been read by the user (true) or not (false)",
),
sa.Column(
"is_read_by_support",
sa.Boolean,
nullable=False,
server_default=sa.text("true"),
doc="Indicates if the message has been read by the support user (true) or not (false)",
),
column_created_datetime(timezone=True),
column_modified_datetime(timezone=True),
)
Original file line number Diff line number Diff line change
Expand Up @@ -10327,6 +10327,16 @@ components:
type: object
- type: 'null'
title: Extracontext
isReadByUser:
anyOf:
- type: boolean
- type: 'null'
title: Isreadbyuser
isReadBySupport:
anyOf:
- type: boolean
- type: 'null'
title: Isreadbysupport
type: object
title: ConversationPatch
ConversationRestGet:
Expand Down Expand Up @@ -10355,6 +10365,11 @@ components:
minimum: 0
type:
$ref: '#/components/schemas/ConversationType'
fogbugzCaseId:
anyOf:
- type: string
- type: 'null'
title: Fogbugzcaseid
created:
type: string
format: date-time
Expand All @@ -10368,6 +10383,12 @@ components:
type: string
type: object
title: Extracontext
isReadByUser:
type: boolean
title: Isreadbyuser
isReadBySupport:
type: boolean
title: Isreadbysupport
type: object
required:
- conversationId
Expand All @@ -10376,9 +10397,12 @@ components:
- projectUuid
- userGroupId
- type
- fogbugzCaseId
- created
- modified
- extraContext
- isReadByUser
- isReadBySupport
title: ConversationRestGet
ConversationType:
type: string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,8 +175,8 @@ def create_application(tracing_config: TracingConfig) -> web.Application:
setup_projects(app)

# conversations
setup_conversations(app)
setup_fogbugz(app) # Needed for support conversations
setup_conversations(app)

# licenses
setup_licenses(app)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import functools
import logging
from typing import Any

Expand Down Expand Up @@ -31,13 +30,8 @@
from servicelib.rest_constants import RESPONSE_MODEL_POLICY

from ..._meta import API_VTAG as VTAG
from ...application_keys import APP_SETTINGS_APPKEY
from ...email import email_service
from ...fogbugz.settings import FogbugzSettings
from ...login.decorators import login_required
from ...models import AuthenticatedRequestContext
from ...products import products_web
from ...users import users_service
from ...utils_aiohttp import envelope_json_response
from .. import _conversation_message_service, _conversation_service
from ._common import ConversationPathParams, raise_unsupported_type
Expand Down Expand Up @@ -89,103 +83,25 @@ async def create_conversation_message(request: web.Request):
raise_unsupported_type(_conversation.type)

# This function takes care of granting support user access to the message
await _conversation_service.get_support_conversation_for_user(
_, is_support_user = await _conversation_service.get_support_conversation_for_user(
app=request.app,
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
conversation_id=path_params.conversation_id,
)

message, is_first_message = (
await _conversation_message_service.create_support_message_with_first_check(
app=request.app,
product_name=req_ctx.product_name,
user_id=req_ctx.user_id,
project_id=None, # Support conversations don't use project_id
conversation_id=path_params.conversation_id,
content=body_params.content,
type_=body_params.type,
)
message = await _conversation_message_service.create_support_message(
app=request.app,
product_name=req_ctx.product_name,
user_id=req_ctx.user_id,
is_support_user=is_support_user,
conversation=_conversation,
request_url=request.url,
request_host=request.host,
content=body_params.content,
type_=body_params.type,
)

# NOTE: This is done here in the Controller layer, as the interface around email currently needs request
product = products_web.get_current_product(request)
fogbugz_settings_or_none: FogbugzSettings | None = request.app[
APP_SETTINGS_APPKEY
].WEBSERVER_FOGBUGZ
if (
product.support_standard_group_id
and fogbugz_settings_or_none is not None
and is_first_message
):
_logger.debug(
"Support settings available and FogBugz client configured, creating FogBugz case."
)
assert product.support_assigned_fogbugz_project_id # nosec

try:
_url = request.url
_conversation_url = f"{_url.scheme}://{_url.host}/#/conversation/{path_params.conversation_id}"

await _conversation_service.create_fogbugz_case_for_support_conversation(
request.app,
conversation=_conversation,
user_id=req_ctx.user_id,
message_content=message.content,
conversation_url=_conversation_url,
host=request.host,
product_support_assigned_fogbugz_project_id=product.support_assigned_fogbugz_project_id,
fogbugz_url=str(fogbugz_settings_or_none.FOGBUGZ_URL),
)
except Exception: # pylint: disable=broad-except
_logger.exception(
"Failed to create support request FogBugz case for conversation %s.",
_conversation.conversation_id,
)

elif (
product.support_standard_group_id
and fogbugz_settings_or_none is None
and is_first_message
):
_logger.debug(
"Support settings available, but no FogBugz client configured, sending email instead to create FogBugz case."
)
try:
user = await users_service.get_user(request.app, req_ctx.user_id)
template_name = "request_support.jinja2"
destination_email = product.support_email
email_template_path = await products_web.get_product_template_path(
request, template_name
)
_url = request.url
_conversation_url = f"{_url.scheme}://{_url.host}/#/conversation/{path_params.conversation_id}"
_extra_context = _conversation.extra_context
await email_service.send_email_from_template(
request,
from_=product.support_email,
to=destination_email,
template=email_template_path,
context={
"host": request.host,
"first_name": user["first_name"],
"last_name": user["last_name"],
"user_email": user["email"],
"conversation_url": _conversation_url,
"message_content": message.content,
"extra_context": _extra_context,
"dumps": functools.partial(_json_encoder_and_dumps, indent=1),
},
)
except Exception: # pylint: disable=broad-except
_logger.exception(
"Failed to send '%s' email to %s (this means the FogBugz case for the request was not created).",
template_name,
destination_email,
)
else:
_logger.debug("No support settings available, skipping FogBugz case creation.")

data = ConversationMessageRestGet.from_domain_model(message)
return envelope_json_response(data, web.HTTPCreated)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ async def get_conversation(request: web.Request):
if conversation.type != ConversationType.SUPPORT:
raise_unsupported_type(conversation.type)

conversation = await _conversation_service.get_support_conversation_for_user(
conversation, _ = await _conversation_service.get_support_conversation_for_user(
app=request.app,
user_id=req_ctx.user_id,
product_name=req_ctx.product_name,
Expand Down
Loading
Loading