diff --git a/api/chalicelib/core/assist.py b/api/chalicelib/core/assist.py index ebf1b7ab8..0a5e5e59d 100644 --- a/api/chalicelib/core/assist.py +++ b/api/chalicelib/core/assist.py @@ -61,10 +61,10 @@ def __get_live_sessions_ws(project_id, data): return {"total": 0, "sessions": []} live_peers = results.json().get("data", []) except requests.exceptions.Timeout: - print("Timeout getting Assist response") + print("!! Timeout getting Assist response") live_peers = {"total": 0, "sessions": []} except Exception as e: - print("issue getting Live-Assist response") + print("!! Issue getting Live-Assist response") print(str(e)) print("expected JSON, received:") try: @@ -116,7 +116,7 @@ def get_live_session_by_id(project_id, session_id): print("!! Timeout getting Assist response") return None except Exception as e: - print("issue getting Assist response") + print("!! Issue getting Assist response") print(str(e)) print("expected JSON, received:") try: @@ -139,10 +139,10 @@ def is_live(project_id, session_id, project_key=None): return False results = results.json().get("data") except requests.exceptions.Timeout: - print("Timeout getting Assist response") + print("!! Timeout getting Assist response") return False except Exception as e: - print("issue getting Assist response") + print("!! Issue getting Assist response") print(str(e)) print("expected JSON, received:") try: @@ -168,10 +168,10 @@ def autocomplete(project_id, q: str, key: str = None): return {"errors": [f"Something went wrong wile calling assist:{results.text}"]} results = results.json().get("data", []) except requests.exceptions.Timeout: - print("Timeout getting Assist response") + print("!! Timeout getting Assist response") return {"errors": ["Assist request timeout"]} except Exception as e: - print("issue getting Assist response") + print("!! Issue getting Assist response") print(str(e)) print("expected JSON, received:") try: @@ -250,7 +250,7 @@ def session_exists(project_id, session_id): print("!! Timeout getting Assist response") return False except Exception as e: - print("issue getting Assist response") + print("!! Issue getting Assist response") print(str(e)) print("expected JSON, received:") try: diff --git a/api/chalicelib/core/collaboration_slack.py b/api/chalicelib/core/collaboration_slack.py index bd0ae7f21..15f090f5d 100644 --- a/api/chalicelib/core/collaboration_slack.py +++ b/api/chalicelib/core/collaboration_slack.py @@ -35,24 +35,57 @@ class Slack: return True @classmethod - def send_text(cls, tenant_id, webhook_id, text, **args): + def send_text_attachments(cls, tenant_id, webhook_id, text, **args): integration = cls.__get(tenant_id=tenant_id, integration_id=webhook_id) if integration is None: return {"errors": ["slack integration not found"]} - print("====> sending slack notification") - r = requests.post( - url=integration["endpoint"], - json={ - "attachments": [ - { - "text": text, - "ts": datetime.now().timestamp(), - **args - } - ] - }) - print(r) - print(r.text) + try: + r = requests.post( + url=integration["endpoint"], + json={ + "attachments": [ + { + "text": text, + "ts": datetime.now().timestamp(), + **args + } + ] + }, + timeout=5) + if r.status_code != 200: + print(f"!! issue sending slack text attachments; webhookId:{webhook_id} code:{r.status_code}") + print(r.text) + return None + except requests.exceptions.Timeout: + print(f"!! Timeout sending slack text attachments webhookId:{webhook_id}") + return None + except Exception as e: + print(f"!! Issue sending slack text attachments webhookId:{webhook_id}") + print(str(e)) + return None + return {"data": r.text} + + @classmethod + def send_raw(cls, tenant_id, webhook_id, body): + integration = cls.__get(tenant_id=tenant_id, integration_id=webhook_id) + if integration is None: + return {"errors": ["slack integration not found"]} + try: + r = requests.post( + url=integration["endpoint"], + json=body, + timeout=5) + if r.status_code != 200: + print(f"!! issue sending slack raw; webhookId:{webhook_id} code:{r.status_code}") + print(r.text) + return None + except requests.exceptions.Timeout: + print(f"!! Timeout sending slack raw webhookId:{webhook_id}") + return None + except Exception as e: + print(f"!! Issue sending slack raw webhookId:{webhook_id}") + print(str(e)) + return None return {"data": r.text} @classmethod diff --git a/api/chalicelib/core/sessions_notes.py b/api/chalicelib/core/sessions_notes.py index ecf2ddd3d..420cbdf11 100644 --- a/api/chalicelib/core/sessions_notes.py +++ b/api/chalicelib/core/sessions_notes.py @@ -1,9 +1,34 @@ +from urllib.parse import urljoin + +from decouple import config + import schemas from chalicelib.core import sessions +from chalicelib.core.collaboration_slack import Slack from chalicelib.utils import pg_client, helper from chalicelib.utils.TimeUTC import TimeUTC +def get_note(tenant_id, project_id, user_id, note_id, share=None): + with pg_client.PostgresClient() as cur: + query = cur.mogrify(f"""SELECT sessions_notes.*, users.name AS creator_name + {",(SELECT name FROM users WHERE user_id=%(share)s AND deleted_at ISNULL) AS share_name" if share else ""} + FROM sessions_notes INNER JOIN users USING (user_id) + WHERE sessions_notes.project_id = %(project_id)s + AND sessions_notes.note_id = %(note_id)s + AND sessions_notes.deleted_at IS NULL + AND (sessions_notes.user_id = %(user_id)s OR sessions_notes.is_public);""", + {"project_id": project_id, "user_id": user_id, "tenant_id": tenant_id, + "note_id": note_id, "share": share}) + + cur.execute(query=query) + row = cur.fetchone() + row = helper.dict_to_camel_case(row) + if row: + row["createdAt"] = TimeUTC.datetime_to_timestamp(row["createdAt"]) + return row + + def get_session_notes(tenant_id, project_id, session_id, user_id): with pg_client.PostgresClient() as cur: query = cur.mogrify(f"""SELECT sessions_notes.* @@ -80,8 +105,7 @@ def edit(tenant_id, user_id, project_id, note_id, data: schemas.SessionUpdateNot sub_query.append("timestamp = %(timestamp)s") with pg_client.PostgresClient() as cur: cur.execute( - cur.mogrify(f"""\ - UPDATE public.sessions_notes + cur.mogrify(f"""UPDATE public.sessions_notes SET {" ,".join(sub_query)} WHERE @@ -101,14 +125,42 @@ def edit(tenant_id, user_id, project_id, note_id, data: schemas.SessionUpdateNot def delete(tenant_id, user_id, project_id, note_id): with pg_client.PostgresClient() as cur: cur.execute( - cur.mogrify("""\ - UPDATE public.sessions_notes + cur.mogrify(""" UPDATE public.sessions_notes SET deleted_at = timezone('utc'::text, now()) - WHERE - note_id = %(note_id)s - AND project_id = %(project_id)s\ + WHERE note_id = %(note_id)s + AND project_id = %(project_id)s AND user_id = %(user_id)s AND deleted_at ISNULL;""", {"project_id": project_id, "user_id": user_id, "note_id": note_id}) ) return {"data": {"state": "success"}} + + +def share_to_slack(tenant_id, user_id, project_id, note_id, webhook_id): + note = get_note(tenant_id=tenant_id, project_id=project_id, user_id=user_id, note_id=note_id, share=user_id) + if note is None: + return {"errors": ["Note not found"]} + session_url = urljoin(config('SITE_URL'), f"{note['projectId']}/sessions/{note['sessionId']}") + title = f"<{session_url}|Note for session {note['sessionId']}>" + + blocks = [{"type": "section", + "fields": [{"type": "mrkdwn", + "text": title}]}, + {"type": "section", + "fields": [{"type": "plain_text", + "text": note["message"]}]}] + if note["tag"]: + blocks.append({"type": "context", + "elements": [{"type": "plain_text", + "text": f"Tag: *{note['tag']}*"}]}) + bottom = f"Created by {note['creatorName'].capitalize()}" + if user_id != note["userId"]: + bottom += f"\nSent by {note['shareName']}: " + blocks.append({"type": "context", + "elements": [{"type": "plain_text", + "text": bottom}]}) + return Slack.send_raw( + tenant_id=tenant_id, + webhook_id=webhook_id, + body={"blocks": blocks} + ) diff --git a/api/chalicelib/core/slack.py b/api/chalicelib/core/slack.py index 0bd715f5e..76bf40163 100644 --- a/api/chalicelib/core/slack.py +++ b/api/chalicelib/core/slack.py @@ -4,17 +4,6 @@ from decouple import config from chalicelib.core.collaboration_slack import Slack -def send(notification, destination): - if notification is None: - return - return Slack.send_text(tenant_id=notification["tenantId"], - webhook_id=destination, - text=notification["description"] \ - + f"\n<{config('SITE_URL')}{notification['buttonUrl']}|{notification['buttonText']}>", - title=notification["title"], - title_link=notification["buttonUrl"], ) - - def send_batch(notifications_list): if notifications_list is None or len(notifications_list) == 0: return diff --git a/api/routers/core.py b/api/routers/core.py index 18e459dd2..935eac873 100644 --- a/api/routers/core.py +++ b/api/routers/core.py @@ -1,22 +1,19 @@ from typing import Union from decouple import config -from fastapi import Depends, Body, BackgroundTasks, HTTPException -from fastapi.responses import FileResponse +from fastapi import Depends, Body, HTTPException from starlette import status import schemas from chalicelib.core import log_tool_rollbar, sourcemaps, events, sessions_assignments, projects, \ alerts, funnels, issues, integrations_manager, metadata, \ log_tool_elasticsearch, log_tool_datadog, \ - log_tool_stackdriver, reset_password, sessions_favorite, \ - log_tool_cloudwatch, log_tool_sentry, log_tool_sumologic, log_tools, errors, sessions, \ + log_tool_stackdriver, reset_password, log_tool_cloudwatch, log_tool_sentry, log_tool_sumologic, log_tools, sessions, \ log_tool_newrelic, announcements, log_tool_bugsnag, weekly_report, integration_jira_cloud, integration_github, \ - assist, heatmaps, mobile, signup, tenants, errors_viewed, boarding, notifications, webhook, users, \ - custom_metrics, saved_search, integrations_global, sessions_viewed, errors_favorite + assist, mobile, signup, tenants, boarding, notifications, webhook, users, \ + custom_metrics, saved_search, integrations_global from chalicelib.core.collaboration_slack import Slack -from chalicelib.utils import email_helper, helper, captcha -from chalicelib.utils.TimeUTC import TimeUTC +from chalicelib.utils import helper, captcha from or_dependencies import OR_context from routers.base import get_routers @@ -52,7 +49,6 @@ def login(data: schemas.UserLoginSchema = Body(...)): @app.post('/{projectId}/sessions/search', tags=["sessions"]) -@app.post('/{projectId}/sessions/search2', tags=["sessions"]) def sessions_search(projectId: int, data: schemas.FlatSessionsSearchPayloadSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): data = sessions.search_sessions(data=data, project_id=projectId, user_id=context.user_id) @@ -100,7 +96,6 @@ def get_integrations_status(projectId: int, context: schemas.CurrentContext = De @app.post('/{projectId}/integrations/{integration}/notify/{integrationId}/{source}/{sourceId}', tags=["integrations"]) -@app.put('/{projectId}/integrations/{integration}/notify/{integrationId}/{source}/{sourceId}', tags=["integrations"]) def integration_notify(projectId: int, integration: str, integrationId: int, source: str, sourceId: str, data: schemas.IntegrationNotificationSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -129,7 +124,6 @@ def get_sentry(projectId: int, context: schemas.CurrentContext = Depends(OR_cont @app.post('/{projectId}/integrations/sentry', tags=["integrations"]) -@app.put('/{projectId}/integrations/sentry', tags=["integrations"]) def add_edit_sentry(projectId: int, data: schemas.SentrySchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_sentry.add_edit(tenant_id=context.tenant_id, project_id=projectId, data=data.dict())} @@ -156,7 +150,6 @@ def get_datadog(projectId: int, context: schemas.CurrentContext = Depends(OR_con @app.post('/{projectId}/integrations/datadog', tags=["integrations"]) -@app.put('/{projectId}/integrations/datadog', tags=["integrations"]) def add_edit_datadog(projectId: int, data: schemas.DatadogSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_datadog.add_edit(tenant_id=context.tenant_id, project_id=projectId, data=data.dict())} @@ -178,7 +171,6 @@ def get_stackdriver(projectId: int, context: schemas.CurrentContext = Depends(OR @app.post('/{projectId}/integrations/stackdriver', tags=["integrations"]) -@app.put('/{projectId}/integrations/stackdriver', tags=["integrations"]) def add_edit_stackdriver(projectId: int, data: schemas.StackdriverSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_stackdriver.add_edit(tenant_id=context.tenant_id, project_id=projectId, data=data.dict())} @@ -200,7 +192,6 @@ def get_newrelic(projectId: int, context: schemas.CurrentContext = Depends(OR_co @app.post('/{projectId}/integrations/newrelic', tags=["integrations"]) -@app.put('/{projectId}/integrations/newrelic', tags=["integrations"]) def add_edit_newrelic(projectId: int, data: schemas.NewrelicSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_newrelic.add_edit(tenant_id=context.tenant_id, project_id=projectId, data=data.dict())} @@ -222,7 +213,6 @@ def get_rollbar(projectId: int, context: schemas.CurrentContext = Depends(OR_con @app.post('/{projectId}/integrations/rollbar', tags=["integrations"]) -@app.put('/{projectId}/integrations/rollbar', tags=["integrations"]) def add_edit_rollbar(projectId: int, data: schemas.RollbarSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_rollbar.add_edit(tenant_id=context.tenant_id, project_id=projectId, data=data.dict())} @@ -250,7 +240,6 @@ def get_bugsnag(projectId: int, context: schemas.CurrentContext = Depends(OR_con @app.post('/{projectId}/integrations/bugsnag', tags=["integrations"]) -@app.put('/{projectId}/integrations/bugsnag', tags=["integrations"]) def add_edit_bugsnag(projectId: int, data: schemas.BugsnagSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_bugsnag.add_edit(tenant_id=context.tenant_id, project_id=projectId, data=data.dict())} @@ -280,7 +269,6 @@ def get_cloudwatch(projectId: int, context: schemas.CurrentContext = Depends(OR_ @app.post('/{projectId}/integrations/cloudwatch', tags=["integrations"]) -@app.put('/{projectId}/integrations/cloudwatch', tags=["integrations"]) def add_edit_cloudwatch(projectId: int, data: schemas.CloudwatchSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_cloudwatch.add_edit(tenant_id=context.tenant_id, project_id=projectId, data=data.dict())} @@ -308,7 +296,6 @@ def test_elasticsearch_connection(data: schemas.ElasticsearchBasicSchema = Body( @app.post('/{projectId}/integrations/elasticsearch', tags=["integrations"]) -@app.put('/{projectId}/integrations/elasticsearch', tags=["integrations"]) def add_edit_elasticsearch(projectId: int, data: schemas.ElasticsearchSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return { @@ -331,7 +318,6 @@ def get_sumologic(projectId: int, context: schemas.CurrentContext = Depends(OR_c @app.post('/{projectId}/integrations/sumologic', tags=["integrations"]) -@app.put('/{projectId}/integrations/sumologic', tags=["integrations"]) def add_edit_sumologic(projectId: int, data: schemas.SumologicSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": log_tool_sumologic.add_edit(tenant_id=context.tenant_id, project_id=projectId, data=data.dict())} @@ -372,7 +358,6 @@ def get_integration_status_github(context: schemas.CurrentContext = Depends(OR_c @app.post('/integrations/jira', tags=["integrations"]) -@app.put('/integrations/jira', tags=["integrations"]) def add_edit_jira_cloud(data: schemas.JiraSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): if not data.url.endswith('atlassian.net'): @@ -386,7 +371,6 @@ def add_edit_jira_cloud(data: schemas.JiraSchema = Body(...), @app.post('/integrations/github', tags=["integrations"]) -@app.put('/integrations/github', tags=["integrations"]) def add_edit_github(data: schemas.GithubSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): error, integration = integrations_manager.get_integration(tool=integration_github.PROVIDER, @@ -461,7 +445,6 @@ def get_all_assignments(projectId: int, context: schemas.CurrentContext = Depend @app.post('/{projectId}/sessions2/{sessionId}/assign/projects/{integrationProjectId}', tags=["assignment"]) -@app.put('/{projectId}/sessions2/{sessionId}/assign/projects/{integrationProjectId}', tags=["assignment"]) def create_issue_assignment(projectId: int, sessionId: int, integrationProjectId, data: schemas.AssignmentSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -484,14 +467,12 @@ def get_gdpr(projectId: int, context: schemas.CurrentContext = Depends(OR_contex @app.post('/{projectId}/gdpr', tags=["projects", "gdpr"]) -@app.put('/{projectId}/gdpr', tags=["projects", "gdpr"]) def edit_gdpr(projectId: int, data: schemas.GdprSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": projects.edit_gdpr(project_id=projectId, gdpr=data.dict())} @public_app.post('/password/reset-link', tags=["reset password"]) -@public_app.put('/password/reset-link', tags=["reset password"]) def reset_password_handler(data: schemas.ForgetPasswordPayloadSchema = Body(...)): if len(data.email) < 5: return {"errors": ["please provide a valid email address"]} @@ -504,21 +485,18 @@ def get_metadata(projectId: int, context: schemas.CurrentContext = Depends(OR_co @app.post('/{projectId}/metadata/list', tags=["metadata"]) -@app.put('/{projectId}/metadata/list', tags=["metadata"]) def add_edit_delete_metadata(projectId: int, data: schemas.MetadataListSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return metadata.add_edit_delete(tenant_id=context.tenant_id, project_id=projectId, new_metas=data.list) @app.post('/{projectId}/metadata', tags=["metadata"]) -@app.put('/{projectId}/metadata', tags=["metadata"]) def add_metadata(projectId: int, data: schemas.MetadataBasicSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return metadata.add(tenant_id=context.tenant_id, project_id=projectId, new_name=data.key) @app.post('/{projectId}/metadata/{index}', tags=["metadata"]) -@app.put('/{projectId}/metadata/{index}', tags=["metadata"]) def edit_metadata(projectId: int, index: int, data: schemas.MetadataBasicSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return metadata.edit(tenant_id=context.tenant_id, project_id=projectId, index=index, @@ -552,7 +530,6 @@ def get_capture_status(projectId: int, context: schemas.CurrentContext = Depends @app.post('/{projectId}/sample_rate', tags=["projects"]) -@app.put('/{projectId}/sample_rate', tags=["projects"]) def update_capture_status(projectId: int, data: schemas.SampleRateSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": projects.update_capture_status(project_id=projectId, changes=data.dict())} @@ -574,7 +551,6 @@ def errors_merge(context: schemas.CurrentContext = Depends(OR_context)): @app.post('/{projectId}/alerts', tags=["alerts"]) -@app.put('/{projectId}/alerts', tags=["alerts"]) def create_alert(projectId: int, data: schemas.AlertSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return alerts.create(projectId, data) @@ -597,7 +573,6 @@ def get_alert(projectId: int, alertId: int, context: schemas.CurrentContext = De @app.post('/{projectId}/alerts/{alertId}', tags=["alerts"]) -@app.put('/{projectId}/alerts/{alertId}', tags=["alerts"]) def update_alert(projectId: int, alertId: int, data: schemas.AlertSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return alerts.update(alertId, data) @@ -609,7 +584,6 @@ def delete_alert(projectId: int, alertId: int, context: schemas.CurrentContext = @app.post('/{projectId}/funnels', tags=["funnels"]) -@app.put('/{projectId}/funnels', tags=["funnels"]) def add_funnel(projectId: int, data: schemas.FunnelSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return funnels.create(project_id=projectId, @@ -653,7 +627,6 @@ def get_funnel_insights(projectId: int, funnelId: int, rangeValue: str = None, s @app.post('/{projectId}/funnels/{funnelId}/insights', tags=["funnels"]) -@app.put('/{projectId}/funnels/{funnelId}/insights', tags=["funnels"]) def get_funnel_insights_on_the_fly(projectId: int, funnelId: int, data: schemas.FunnelInsightsPayloadSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return funnels.get_top_insights_on_the_fly(funnel_id=funnelId, user_id=context.user_id, project_id=projectId, @@ -668,7 +641,6 @@ def get_funnel_issues(projectId: int, funnelId, rangeValue: str = None, startDat @app.post('/{projectId}/funnels/{funnelId}/issues', tags=["funnels"]) -@app.put('/{projectId}/funnels/{funnelId}/issues', tags=["funnels"]) def get_funnel_issues_on_the_fly(projectId: int, funnelId: int, data: schemas.FunnelSearchPayloadSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": funnels.get_issues_on_the_fly(funnel_id=funnelId, user_id=context.user_id, project_id=projectId, @@ -685,7 +657,6 @@ def get_funnel_sessions(projectId: int, funnelId: int, rangeValue: str = None, s @app.post('/{projectId}/funnels/{funnelId}/sessions', tags=["funnels"]) -@app.put('/{projectId}/funnels/{funnelId}/sessions', tags=["funnels"]) def get_funnel_sessions_on_the_fly(projectId: int, funnelId: int, data: schemas.FunnelSearchPayloadSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": funnels.get_sessions_on_the_fly(funnel_id=funnelId, user_id=context.user_id, project_id=projectId, @@ -705,7 +676,6 @@ def get_funnel_issue_sessions(projectId: int, issueId: str, startDate: int = Non @app.post('/{projectId}/funnels/{funnelId}/issues/{issueId}/sessions', tags=["funnels"]) -@app.put('/{projectId}/funnels/{funnelId}/issues/{issueId}/sessions', tags=["funnels"]) def get_funnel_issue_sessions(projectId: int, funnelId: int, issueId: str, data: schemas.FunnelSearchPayloadSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -729,7 +699,6 @@ def get_funnel(projectId: int, funnelId: int, context: schemas.CurrentContext = @app.post('/{projectId}/funnels/{funnelId}', tags=["funnels"]) -@app.put('/{projectId}/funnels/{funnelId}', tags=["funnels"]) def edit_funnel(projectId: int, funnelId: int, data: schemas.UpdateFunnelSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return funnels.update(funnel_id=funnelId, @@ -762,7 +731,6 @@ def get_weekly_report_config(context: schemas.CurrentContext = Depends(OR_contex @app.post('/config/weekly_report', tags=["weekly report config"]) -@app.put('/config/weekly_report', tags=["weekly report config"]) def edit_weekly_report_config(data: schemas.WeeklyReportConfigSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": weekly_report.edit_config(user_id=context.user_id, weekly_report=data.weekly_report)} @@ -797,21 +765,19 @@ def mobile_signe(projectId: int, sessionId: int, data: schemas.MobileSignPayload return {"data": mobile.sign_keys(project_id=projectId, session_id=sessionId, keys=data.keys)} -@public_app.put('/signup', tags=['signup']) @public_app.post('/signup', tags=['signup']) +@public_app.put('/signup', tags=['signup']) def signup_handler(data: schemas.UserSignupSchema = Body(...)): return signup.create_step1(data) @app.post('/projects', tags=['projects']) -@app.put('/projects', tags=['projects']) def create_project(data: schemas.CreateProjectSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return projects.create(tenant_id=context.tenant_id, user_id=context.user_id, data=data) @app.post('/projects/{projectId}', tags=['projects']) -@app.put('/projects/{projectId}', tags=['projects']) def edit_project(projectId: int, data: schemas.CreateProjectSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return projects.edit(tenant_id=context.tenant_id, user_id=context.user_id, data=data, project_id=projectId) @@ -829,8 +795,8 @@ def generate_new_tenant_token(context: schemas.CurrentContext = Depends(OR_conte } -@app.put('/client', tags=['client']) @app.post('/client', tags=['client']) +@app.put('/client', tags=['client']) def edit_client(data: schemas.UpdateTenantSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return tenants.update(tenant_id=context.tenant_id, user_id=context.user_id, data=data) @@ -852,7 +818,6 @@ def view_notifications(notificationId: int, context: schemas.CurrentContext = De @app.post('/notifications/view', tags=['notifications']) -@app.put('/notifications/view', tags=['notifications']) def batch_view_notifications(data: schemas.NotificationsViewSchema, context: schemas.CurrentContext = Depends(OR_context)): return {"data": notifications.view_notification(notification_ids=data.ids, @@ -903,7 +868,6 @@ def delete_slack_integration(integrationId: int, context: schemas.CurrentContext @app.post('/webhooks', tags=["webhooks"]) -@app.put('/webhooks', tags=["webhooks"]) def add_edit_webhook(data: schemas.CreateEditWebhookSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": webhook.add_edit(tenant_id=context.tenant_id, data=data.dict(), replace_none=True)} @@ -940,7 +904,6 @@ def generate_new_user_token(context: schemas.CurrentContext = Depends(OR_context @app.post('/account/password', tags=["account"]) -@app.put('/account/password', tags=["account"]) def change_client_password(data: schemas.EditUserPasswordSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return users.change_password(email=context.email, old_password=data.old_password, @@ -949,7 +912,6 @@ def change_client_password(data: schemas.EditUserPasswordSchema = Body(...), @app.post('/{projectId}/saved_search', tags=["savedSearch"]) -@app.put('/{projectId}/saved_search', tags=["savedSearch"]) def add_saved_search(projectId: int, data: schemas.SavedSearchSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return saved_search.create(project_id=projectId, user_id=context.user_id, data=data) @@ -966,7 +928,6 @@ def get_saved_search(projectId: int, search_id: int, context: schemas.CurrentCon @app.post('/{projectId}/saved_search/{search_id}', tags=["savedSearch"]) -@app.put('/{projectId}/saved_search/{search_id}', tags=["savedSearch"]) def update_saved_search(projectId: int, search_id: int, data: schemas.SavedSearchSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return {"data": saved_search.update(user_id=context.user_id, search_id=search_id, data=data, project_id=projectId)} diff --git a/api/routers/core_dynamic.py b/api/routers/core_dynamic.py index 93664a4ff..f8c602faf 100644 --- a/api/routers/core_dynamic.py +++ b/api/routers/core_dynamic.py @@ -46,7 +46,6 @@ def get_account(context: schemas.CurrentContext = Depends(OR_context)): @app.post('/account', tags=["account"]) -@app.put('/account', tags=["account"]) def edit_account(data: schemas.EditUserSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return users.edit(tenant_id=context.tenant_id, user_id_to_update=context.user_id, changes=data, @@ -70,8 +69,8 @@ def get_project(projectId: int, context: schemas.CurrentContext = Depends(OR_con return {"data": data} -@app.put('/integrations/slack', tags=['integrations']) @app.post('/integrations/slack', tags=['integrations']) +@app.put('/integrations/slack', tags=['integrations']) def add_slack_client(data: schemas.AddSlackSchema, context: schemas.CurrentContext = Depends(OR_context)): n = Slack.add_channel(tenant_id=context.tenant_id, url=data.url, name=data.name) if n is None: @@ -81,7 +80,6 @@ def add_slack_client(data: schemas.AddSlackSchema, context: schemas.CurrentConte return {"data": n} -@app.put('/integrations/slack/{integrationId}', tags=['integrations']) @app.post('/integrations/slack/{integrationId}', tags=['integrations']) def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -98,7 +96,6 @@ def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = B @app.post('/client/members', tags=["client"]) -@app.put('/client/members', tags=["client"]) def add_member(background_tasks: BackgroundTasks, data: schemas.CreateMemberSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return users.create_member(tenant_id=context.tenant_id, user_id=context.user_id, data=data.dict(), @@ -123,7 +120,6 @@ def process_invitation_link(token: str): @public_app.post('/password/reset', tags=["users"]) -@public_app.put('/password/reset', tags=["users"]) def change_password_by_invitation(data: schemas.EditPasswordByInvitationSchema = Body(...)): if data is None or len(data.invitation) < 64 or len(data.passphrase) < 8: return {"errors": ["please provide a valid invitation & pass"]} @@ -136,7 +132,6 @@ def change_password_by_invitation(data: schemas.EditPasswordByInvitationSchema = return users.set_password_invitation(new_password=data.password, user_id=user["userId"]) -@app.put('/client/members/{memberId}', tags=["client"]) @app.post('/client/members/{memberId}', tags=["client"]) def edit_member(memberId: int, data: schemas.EditMemberSchema, context: schemas.CurrentContext = Depends(OR_context)): @@ -358,9 +353,7 @@ def assign_session(projectId: int, sessionId: int, issueId: str, @app.post('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) -@app.put('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) @app.post('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) -@app.put('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"]) def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schemas.CommentAssignmentSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): data = sessions_assignments.comment(tenant_id=context.tenant_id, project_id=projectId, @@ -374,9 +367,10 @@ def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schem @app.post('/{projectId}/sessions/{sessionId}/notes', tags=["sessions", "notes"]) -@app.put('/{projectId}/sessions/{sessionId}/notes', tags=["sessions", "notes"]) def create_note(projectId: int, sessionId: int, data: schemas.SessionNoteSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): + if not sessions.session_exists(project_id=projectId, session_id=sessionId): + return {"errors": ["Session not found"]} data = sessions_notes.create(tenant_id=context.tenant_id, project_id=projectId, session_id=sessionId, user_id=context.user_id, data=data) if "errors" in data.keys(): @@ -398,7 +392,6 @@ def get_session_notes(projectId: int, sessionId: int, context: schemas.CurrentCo @app.post('/{projectId}/notes/{noteId}', tags=["sessions", "notes"]) -@app.put('/{projectId}/notes/{noteId}', tags=["sessions", "notes"]) def edit_note(projectId: int, noteId: int, data: schemas.SessionUpdateNoteSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): data = sessions_notes.edit(tenant_id=context.tenant_id, project_id=projectId, user_id=context.user_id, @@ -417,6 +410,13 @@ def delete_note(projectId: int, noteId: int, context: schemas.CurrentContext = D return data +@app.post('/{projectId}/notes/{noteId}/slack/{webhookId}', tags=["sessions", "notes"]) +def share_note_to_slack(projectId: int, noteId: int, webhookId: int, + context: schemas.CurrentContext = Depends(OR_context)): + return sessions_notes.share_to_slack(tenant_id=context.tenant_id, project_id=projectId, user_id=context.user_id, + note_id=noteId, webhook_id=webhookId) + + @app.post('/{projectId}/notes', tags=["sessions", "notes"]) def get_all_notes(projectId: int, data: schemas.SearchNoteSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -424,6 +424,4 @@ def get_all_notes(projectId: int, data: schemas.SearchNoteSchema = Body(...), user_id=context.user_id, data=data) if "errors" in data: return data - return { - 'data': data - } + return {'data': data} diff --git a/ee/api/chalicelib/core/sessions_notes.py b/ee/api/chalicelib/core/sessions_notes.py index f5ac05722..ce0420023 100644 --- a/ee/api/chalicelib/core/sessions_notes.py +++ b/ee/api/chalicelib/core/sessions_notes.py @@ -1,9 +1,35 @@ +from urllib.parse import urljoin + +from decouple import config + import schemas from chalicelib.core import sessions +from chalicelib.core.collaboration_slack import Slack from chalicelib.utils import pg_client, helper from chalicelib.utils.TimeUTC import TimeUTC +def get_note(tenant_id, project_id, user_id, note_id, share=None): + with pg_client.PostgresClient() as cur: + query = cur.mogrify(f"""SELECT sessions_notes.*, users.name AS creator_name + {",(SELECT name FROM users WHERE tenant_id=%(tenant_id)s AND user_id=%(share)s) AS share_name" if share else ""} + FROM sessions_notes INNER JOIN users USING (user_id) + WHERE sessions_notes.project_id = %(project_id)s + AND sessions_notes.note_id = %(note_id)s + AND sessions_notes.deleted_at IS NULL + AND (sessions_notes.user_id = %(user_id)s + OR sessions_notes.is_public AND users.tenant_id = %(tenant_id)s);""", + {"project_id": project_id, "user_id": user_id, "tenant_id": tenant_id, + "note_id": note_id, "share": share}) + + cur.execute(query=query) + row = cur.fetchone() + row = helper.dict_to_camel_case(row) + if row: + row["createdAt"] = TimeUTC.datetime_to_timestamp(row["createdAt"]) + return row + + def get_session_notes(tenant_id, project_id, session_id, user_id): with pg_client.PostgresClient() as cur: query = cur.mogrify(f"""SELECT sessions_notes.* @@ -83,8 +109,7 @@ def edit(tenant_id, user_id, project_id, note_id, data: schemas.SessionUpdateNot sub_query.append("timestamp = %(timestamp)s") with pg_client.PostgresClient() as cur: cur.execute( - cur.mogrify(f"""\ - UPDATE public.sessions_notes + cur.mogrify(f"""UPDATE public.sessions_notes SET {" ,".join(sub_query)} WHERE @@ -104,14 +129,42 @@ def edit(tenant_id, user_id, project_id, note_id, data: schemas.SessionUpdateNot def delete(tenant_id, user_id, project_id, note_id): with pg_client.PostgresClient() as cur: cur.execute( - cur.mogrify("""\ - UPDATE public.sessions_notes + cur.mogrify(""" UPDATE public.sessions_notes SET deleted_at = timezone('utc'::text, now()) - WHERE - note_id = %(note_id)s - AND project_id = %(project_id)s\ + WHERE note_id = %(note_id)s + AND project_id = %(project_id)s AND user_id = %(user_id)s AND deleted_at ISNULL;""", {"project_id": project_id, "user_id": user_id, "note_id": note_id}) ) return {"data": {"state": "success"}} + + +def share_to_slack(tenant_id, user_id, project_id, note_id, webhook_id): + note = get_note(tenant_id=tenant_id, project_id=project_id, user_id=user_id, note_id=note_id, share=user_id) + if note is None: + return {"errors": ["Note not found"]} + session_url = urljoin(config('SITE_URL'), f"{note['projectId']}/sessions/{note['sessionId']}") + title = f"<{session_url}|Note for session {note['sessionId']}>" + + blocks = [{"type": "section", + "fields": [{"type": "mrkdwn", + "text": title}]}, + {"type": "section", + "fields": [{"type": "plain_text", + "text": note["message"]}]}] + if note["tag"]: + blocks.append({"type": "context", + "elements": [{"type": "plain_text", + "text": f"Tag: *{note['tag']}*"}]}) + bottom = f"Created by {note['creatorName'].capitalize()}" + if user_id != note["userId"]: + bottom += f"\nSent by {note['shareName']}: " + blocks.append({"type": "context", + "elements": [{"type": "plain_text", + "text": bottom}]}) + return Slack.send_raw( + tenant_id=tenant_id, + webhook_id=webhook_id, + body={"blocks": blocks} + ) diff --git a/ee/api/routers/core_dynamic.py b/ee/api/routers/core_dynamic.py index 1ce471c23..9734a9e2c 100644 --- a/ee/api/routers/core_dynamic.py +++ b/ee/api/routers/core_dynamic.py @@ -50,7 +50,6 @@ def get_account(context: schemas.CurrentContext = Depends(OR_context)): @app.post('/account', tags=["account"]) -@app.put('/account', tags=["account"]) def edit_account(data: schemas_ee.EditUserSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return users.edit(tenant_id=context.tenant_id, user_id_to_update=context.user_id, changes=data, @@ -74,8 +73,8 @@ def get_project(projectId: int, context: schemas.CurrentContext = Depends(OR_con return {"data": data} -@app.put('/integrations/slack', tags=['integrations']) @app.post('/integrations/slack', tags=['integrations']) +@app.put('/integrations/slack', tags=['integrations']) def add_slack_client(data: schemas.AddSlackSchema, context: schemas.CurrentContext = Depends(OR_context)): n = Slack.add_channel(tenant_id=context.tenant_id, url=data.url, name=data.name) if n is None: @@ -85,7 +84,6 @@ def add_slack_client(data: schemas.AddSlackSchema, context: schemas.CurrentConte return {"data": n} -@app.put('/integrations/slack/{integrationId}', tags=['integrations']) @app.post('/integrations/slack/{integrationId}', tags=['integrations']) def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -102,7 +100,6 @@ def edit_slack_integration(integrationId: int, data: schemas.EditSlackSchema = B @app.post('/client/members', tags=["client"]) -@app.put('/client/members', tags=["client"]) def add_member(background_tasks: BackgroundTasks, data: schemas_ee.CreateMemberSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): return users.create_member(tenant_id=context.tenant_id, user_id=context.user_id, data=data.dict(), @@ -127,7 +124,6 @@ def process_invitation_link(token: str): @public_app.post('/password/reset', tags=["users"]) -@public_app.put('/password/reset', tags=["users"]) def change_password_by_invitation(data: schemas.EditPasswordByInvitationSchema = Body(...)): if data is None or len(data.invitation) < 64 or len(data.passphrase) < 8: return {"errors": ["please provide a valid invitation & pass"]} @@ -140,7 +136,6 @@ def change_password_by_invitation(data: schemas.EditPasswordByInvitationSchema = return users.set_password_invitation(new_password=data.password, user_id=user["userId"], tenant_id=user["tenantId"]) -@app.put('/client/members/{memberId}', tags=["client"]) @app.post('/client/members/{memberId}', tags=["client"]) def edit_member(memberId: int, data: schemas_ee.EditMemberSchema, context: schemas.CurrentContext = Depends(OR_context)): @@ -375,12 +370,8 @@ def assign_session(projectId: int, sessionId: int, issueId: str, @app.post('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"], dependencies=[OR_scope(Permissions.session_replay)]) -@app.put('/{projectId}/sessions/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"], - dependencies=[OR_scope(Permissions.session_replay)]) @app.post('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"], dependencies=[OR_scope(Permissions.session_replay)]) -@app.put('/{projectId}/sessions2/{sessionId}/assign/{issueId}/comment', tags=["sessions", "issueTracking"], - dependencies=[OR_scope(Permissions.session_replay)]) def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schemas.CommentAssignmentSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): data = sessions_assignments.comment(tenant_id=context.tenant_id, project_id=projectId, @@ -395,10 +386,10 @@ def comment_assignment(projectId: int, sessionId: int, issueId: str, data: schem @app.post('/{projectId}/sessions/{sessionId}/notes', tags=["sessions", "notes"], dependencies=[OR_scope(Permissions.session_replay)]) -@app.put('/{projectId}/sessions/{sessionId}/notes', tags=["sessions", "notes"], - dependencies=[OR_scope(Permissions.session_replay)]) def create_note(projectId: int, sessionId: int, data: schemas.SessionNoteSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): + if not sessions.session_exists(project_id=projectId, session_id=sessionId): + return {"errors": ["Session not found"]} data = sessions_notes.create(tenant_id=context.tenant_id, project_id=projectId, session_id=sessionId, user_id=context.user_id, data=data) if "errors" in data.keys(): @@ -422,7 +413,6 @@ def get_session_notes(projectId: int, sessionId: int, context: schemas.CurrentCo @app.post('/{projectId}/notes/{noteId}', tags=["sessions", "notes"], dependencies=[OR_scope(Permissions.session_replay)]) -@app.put('/{projectId}/notes/{noteId}', tags=["sessions", "notes"], dependencies=[OR_scope(Permissions.session_replay)]) def edit_note(projectId: int, noteId: int, data: schemas.SessionUpdateNoteSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): data = sessions_notes.edit(tenant_id=context.tenant_id, project_id=projectId, user_id=context.user_id, @@ -442,6 +432,13 @@ def delete_note(projectId: int, noteId: int, context: schemas.CurrentContext = D return data +@app.post('/{projectId}/notes/{noteId}/slack/{webhookId}', tags=["sessions", "notes"]) +def share_note_to_slack(projectId: int, noteId: int, webhookId: int, + context: schemas.CurrentContext = Depends(OR_context)): + return sessions_notes.share_to_slack(tenant_id=context.tenant_id, project_id=projectId, user_id=context.user_id, + note_id=noteId, webhook_id=webhookId) + + @app.post('/{projectId}/notes', tags=["sessions", "notes"], dependencies=[OR_scope(Permissions.session_replay)]) def get_all_notes(projectId: int, data: schemas.SearchNoteSchema = Body(...), context: schemas.CurrentContext = Depends(OR_context)): @@ -449,6 +446,4 @@ def get_all_notes(projectId: int, data: schemas.SearchNoteSchema = Body(...), user_id=context.user_id) if "errors" in data: return data - return { - 'data': data - } + return {'data': data}