We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Add Zoom support for audio-only calls 
Following #25922, we now have support for audio calls in Zulip, but the Zoom integration doesn't yet support it.

We should figure out what parameters we need to pass when creating a Zoom call to support an "Audio call" button for Zoom users as well.

Tagged as an 8.0 release goal, since we'll want to document the audio calls feature as available for all call providers.

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `version.py`
Content:
```
1 import os
2 
3 ZULIP_VERSION = "8.0-dev+git"
4 
5 # Add information on number of commits and commit hash to version, if available
6 zulip_git_version_file = os.path.join(
7     os.path.dirname(os.path.abspath(__file__)), "zulip-git-version"
8 )
9 lines = [ZULIP_VERSION, ""]
10 if os.path.exists(zulip_git_version_file):
11     with open(zulip_git_version_file) as f:
12         lines = [*f, "", ""]
13 ZULIP_VERSION = lines.pop(0).strip()
14 ZULIP_MERGE_BASE = lines.pop(0).strip()
15 
16 LATEST_MAJOR_VERSION = "7.0"
17 LATEST_RELEASE_VERSION = "7.3"
18 LATEST_RELEASE_ANNOUNCEMENT = "https://blog.zulip.com/2023/05/31/zulip-7-0-released/"
19 
20 # Versions of the desktop app below DESKTOP_MINIMUM_VERSION will be
21 # prevented from connecting to the Zulip server.  Versions above
22 # DESKTOP_MINIMUM_VERSION but below DESKTOP_WARNING_VERSION will have
23 # a banner at the top of the page asking the user to upgrade.
24 DESKTOP_MINIMUM_VERSION = "5.4.3"
25 DESKTOP_WARNING_VERSION = "5.9.3"
26 
27 # Bump the API_FEATURE_LEVEL whenever an API change is made
28 # that clients might want to condition on.  If we forget at
29 # the time we make the change, then bump it later as soon
30 # as we notice; clients using API_FEATURE_LEVEL will just not
31 # use the new feature/API until the bump.
32 #
33 # Changes should be accompanied by documentation explaining what the
34 # new level means in api_docs/changelog.md, as well as "**Changes**"
35 # entries in the endpoint's documentation in `zulip.yaml`.
36 API_FEATURE_LEVEL = 205
37 
38 # Bump the minor PROVISION_VERSION to indicate that folks should provision
39 # only when going from an old version of the code to a newer version. Bump
40 # the major version to indicate that folks should provision in both
41 # directions.
42 
43 # Typically,
44 # * adding a dependency only requires a minor version bump;
45 # * removing a dependency requires a major version bump;
46 # * upgrading a dependency requires a major version bump, unless the
47 #   upgraded dependency is backwards compatible with all of our
48 #   historical commits sharing the same major version, in which case a
49 #   minor version bump suffices.
50 
51 PROVISION_VERSION = (247, 6)
52
```

Path: `zerver/views/video_calls.py`
Content:
```
1 import hashlib
2 import json
3 import random
4 import secrets
5 from base64 import b32encode
6 from functools import partial
7 from typing import Dict
8 from urllib.parse import quote, urlencode, urljoin
9 
10 import requests
11 from defusedxml import ElementTree
12 from django.conf import settings
13 from django.core.signing import Signer
14 from django.http import HttpRequest, HttpResponse
15 from django.middleware import csrf
16 from django.shortcuts import redirect, render
17 from django.utils.crypto import constant_time_compare, salted_hmac
18 from django.utils.translation import gettext as _
19 from django.views.decorators.cache import never_cache
20 from django.views.decorators.csrf import csrf_exempt
21 from django.views.decorators.http import require_POST
22 from oauthlib.oauth2 import OAuth2Error
23 from requests_oauthlib import OAuth2Session
24 
25 from zerver.actions.video_calls import do_set_zoom_token
26 from zerver.decorator import zulip_login_required
27 from zerver.lib.exceptions import ErrorCode, JsonableError
28 from zerver.lib.outgoing_http import OutgoingSession
29 from zerver.lib.pysa import mark_sanitized
30 from zerver.lib.request import REQ, has_request_variables
31 from zerver.lib.response import json_success
32 from zerver.lib.subdomains import get_subdomain
33 from zerver.lib.url_encoding import append_url_query_string
34 from zerver.lib.validator import check_dict, check_string
35 from zerver.models import UserProfile, get_realm
36 
37 
38 class VideoCallSession(OutgoingSession):
39     def __init__(self) -> None:
40         super().__init__(role="video_calls", timeout=5)
41 
42 
43 class InvalidZoomTokenError(JsonableError):
44     code = ErrorCode.INVALID_ZOOM_TOKEN
45 
46     def __init__(self) -> None:
47         super().__init__(_("Invalid Zoom access token"))
48 
49 
50 def get_zoom_session(user: UserProfile) -> OAuth2Session:
51     if settings.VIDEO_ZOOM_CLIENT_ID is None:
52         raise JsonableError(_("Zoom credentials have not been configured"))
53 
54     client_id = settings.VIDEO_ZOOM_CLIENT_ID
55     client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET
56 
57     return OAuth2Session(
58         client_id,
59         redirect_uri=urljoin(settings.ROOT_DOMAIN_URI, "/calls/zoom/complete"),
60         auto_refresh_url="https://zoom.us/oauth/token",
61         auto_refresh_kwargs={
62             "client_id": client_id,
63             "client_secret": client_secret,
64         },
65         token=user.zoom_token,
66         token_updater=partial(do_set_zoom_token, user),
67     )
68 
69 
70 def get_zoom_sid(request: HttpRequest) -> str:
71     # This is used to prevent CSRF attacks on the Zoom OAuth
72     # authentication flow.  We want this value to be unpredictable and
73     # tied to the session, but we don’t want to expose the main CSRF
74     # token directly to the Zoom server.
75 
76     csrf.get_token(request)
77     # Use 'mark_sanitized' to cause Pysa to ignore the flow of user controlled
78     # data out of this function. 'request.META' is indeed user controlled, but
79     # post-HMAC output is no longer meaningfully controllable.
80     return mark_sanitized(
81         ""
82         if getattr(request, "_dont_enforce_csrf_checks", False)
83         else salted_hmac("Zulip Zoom sid", request.META["CSRF_COOKIE"]).hexdigest()
84     )
85 
86 
87 @zulip_login_required
88 @never_cache
89 def register_zoom_user(request: HttpRequest) -> HttpResponse:
90     assert request.user.is_authenticated
91 
92     oauth = get_zoom_session(request.user)
93     authorization_url, state = oauth.authorization_url(
94         "https://zoom.us/oauth/authorize",
95         state=json.dumps(
96             {"realm": get_subdomain(request), "sid": get_zoom_sid(request)},
97         ),
98     )
99     return redirect(authorization_url)
100 
101 
102 @never_cache
103 @has_request_variables
104 def complete_zoom_user(
105     request: HttpRequest,
106     state: Dict[str, str] = REQ(
107         json_validator=check_dict([("realm", check_string)], value_validator=check_string)
108     ),
109 ) -> HttpResponse:
110     if get_subdomain(request) != state["realm"]:
111         return redirect(urljoin(get_realm(state["realm"]).uri, request.get_full_path()))
112     return complete_zoom_user_in_realm(request)
113 
114 
115 @zulip_login_required
116 @has_request_variables
117 def complete_zoom_user_in_realm(
118     request: HttpRequest,
119     code: str = REQ(),
120     state: Dict[str, str] = REQ(
121         json_validator=check_dict([("sid", check_string)], value_validator=check_string)
122     ),
123 ) -> HttpResponse:
124     assert request.user.is_authenticated
125 
126     if not constant_time_compare(state["sid"], get_zoom_sid(request)):
127         raise JsonableError(_("Invalid Zoom session identifier"))
128 
129     client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET
130 
131     oauth = get_zoom_session(request.user)
132     try:
133         token = oauth.fetch_token(
134             "https://zoom.us/oauth/token",
135             code=code,
136             client_secret=client_secret,
137         )
138     except OAuth2Error:
139         raise JsonableError(_("Invalid Zoom credentials"))
140 
141     do_set_zoom_token(request.user, token)
142     return render(request, "zerver/close_window.html")
143 
144 
145 def make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpResponse:
146     oauth = get_zoom_session(user)
147     if not oauth.authorized:
148         raise InvalidZoomTokenError
149 
150     try:
151         res = oauth.post("https://api.zoom.us/v2/users/me/meetings", json={})
152     except OAuth2Error:
153         do_set_zoom_token(user, None)
154         raise InvalidZoomTokenError
155 
156     if res.status_code == 401:
157         do_set_zoom_token(user, None)
158         raise InvalidZoomTokenError
159     elif not res.ok:
160         raise JsonableError(_("Failed to create Zoom call"))
161 
162     return json_success(request, data={"url": res.json()["join_url"]})
163 
164 
165 @csrf_exempt
166 @require_POST
167 @has_request_variables
168 def deauthorize_zoom_user(request: HttpRequest) -> HttpResponse:
169     return json_success(request)
170 
171 
172 @has_request_variables
173 def get_bigbluebutton_url(
174     request: HttpRequest, user_profile: UserProfile, meeting_name: str = REQ()
175 ) -> HttpResponse:
176     # https://docs.bigbluebutton.org/dev/api.html#create for reference on the API calls
177     # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum
178     id = "zulip-" + str(random.randint(100000000000, 999999999999))
179     password = b32encode(secrets.token_bytes(20)).decode()  # 20 bytes means 32 characters
180 
181     # We sign our data here to ensure a Zulip user can not tamper with
182     # the join link to gain access to other meetings that are on the
183     # same bigbluebutton server.
184     signed = Signer().sign_object(
185         {
186             "meeting_id": id,
187             "name": meeting_name,
188             "password": password,
189         }
190     )
191     url = append_url_query_string("/calls/bigbluebutton/join", "bigbluebutton=" + signed)
192     return json_success(request, {"url": url})
193 
194 
195 # We use zulip_login_required here mainly to get access to the user's
196 # full name from Zulip to prepopulate the user's name in the
197 # BigBlueButton meeting.  Since the meeting's details are encoded in
198 # the link the user is clicking, there is no validation tying this
199 # meeting to the Zulip organization it was created in.
200 @zulip_login_required
201 @never_cache
202 @has_request_variables
203 def join_bigbluebutton(request: HttpRequest, bigbluebutton: str = REQ()) -> HttpResponse:
204     assert request.user.is_authenticated
205 
206     if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None:
207         raise JsonableError(_("BigBlueButton is not configured."))
208 
209     try:
210         bigbluebutton_data = Signer().unsign_object(bigbluebutton)
211     except Exception:
212         raise JsonableError(_("Invalid signature."))
213 
214     create_params = urlencode(
215         {
216             "meetingID": bigbluebutton_data["meeting_id"],
217             "name": bigbluebutton_data["name"],
218             "moderatorPW": bigbluebutton_data["password"],
219             # We generate the attendee password from moderatorPW,
220             # because the BigBlueButton API requires a separate
221             # password. This integration is designed to have all users
222             # join as moderators, so we generate attendeePW by
223             # truncating the moderatorPW while keeping it long enough
224             # to not be vulnerable to brute force attacks.
225             "attendeePW": bigbluebutton_data["password"][:16],
226         },
227         quote_via=quote,
228     )
229 
230     checksum = hashlib.sha256(
231         ("create" + create_params + settings.BIG_BLUE_BUTTON_SECRET).encode()
232     ).hexdigest()
233 
234     try:
235         response = VideoCallSession().get(
236             append_url_query_string(settings.BIG_BLUE_BUTTON_URL + "api/create", create_params)
237             + "&checksum="
238             + checksum
239         )
240         response.raise_for_status()
241     except requests.RequestException:
242         raise JsonableError(_("Error connecting to the BigBlueButton server."))
243 
244     payload = ElementTree.fromstring(response.text)
245     if payload.find("messageKey").text == "checksumError":
246         raise JsonableError(_("Error authenticating to the BigBlueButton server."))
247 
248     if payload.find("returncode").text != "SUCCESS":
249         raise JsonableError(_("BigBlueButton server returned an unexpected error."))
250 
251     join_params = urlencode(
252         {
253             "meetingID": bigbluebutton_data["meeting_id"],
254             # We use the moderator password here to grant ever user
255             # full moderator permissions to the bigbluebutton session.
256             "password": bigbluebutton_data["password"],
257             "fullName": request.user.full_name,
258             # https://docs.bigbluebutton.org/dev/api.html#create
259             # The createTime option is used to have the user redirected to a link
260             # that is only valid for this meeting.
261             #
262             # Even if the same link in Zulip is used again, a new
263             # createTime parameter will be created, as the meeting on
264             # the BigBlueButton server has to be recreated. (after a
265             # few minutes)
266             "createTime": payload.find("createTime").text,
267         },
268         quote_via=quote,
269     )
270 
271     checksum = hashlib.sha256(
272         ("join" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()
273     ).hexdigest()
274     redirect_url_base = append_url_query_string(
275         settings.BIG_BLUE_BUTTON_URL + "api/join", join_params
276     )
277     return redirect(append_url_query_string(redirect_url_base, "checksum=" + checksum))
278
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/version.py b/version.py
--- a/version.py
+++ b/version.py
@@ -33,7 +33,7 @@
 # Changes should be accompanied by documentation explaining what the
 # new level means in api_docs/changelog.md, as well as "**Changes**"
 # entries in the endpoint's documentation in `zulip.yaml`.
-API_FEATURE_LEVEL = 205
+API_FEATURE_LEVEL = 206
 
 # Bump the minor PROVISION_VERSION to indicate that folks should provision
 # only when going from an old version of the code to a newer version. Bump
diff --git a/zerver/views/video_calls.py b/zerver/views/video_calls.py
--- a/zerver/views/video_calls.py
+++ b/zerver/views/video_calls.py
@@ -31,7 +31,7 @@
 from zerver.lib.response import json_success
 from zerver.lib.subdomains import get_subdomain
 from zerver.lib.url_encoding import append_url_query_string
-from zerver.lib.validator import check_dict, check_string
+from zerver.lib.validator import check_bool, check_dict, check_string
 from zerver.models import UserProfile, get_realm
 
 
@@ -142,13 +142,30 @@
     return render(request, "zerver/close_window.html")
 
 
-def make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpResponse:
+@has_request_variables
+def make_zoom_video_call(
+    request: HttpRequest,
+    user: UserProfile,
+    is_video_call: bool = REQ(json_validator=check_bool, default=True),
+) -> HttpResponse:
     oauth = get_zoom_session(user)
     if not oauth.authorized:
         raise InvalidZoomTokenError
 
+    # The meeting host has the ability to configure both their own and
+    # participants' default video on/off state for the meeting. That's
+    # why when creating a meeting, configure the video on/off default
+    # according to the desired call type. Each Zoom user can still have
+    # their own personal setting to not start video by default.
+    payload = {
+        "settings": {
+            "host_video": is_video_call,
+            "participant_video": is_video_call,
+        }
+    }
+
     try:
-        res = oauth.post("https://api.zoom.us/v2/users/me/meetings", json={})
+        res = oauth.post("https://api.zoom.us/v2/users/me/meetings", json=payload)
     except OAuth2Error:
         do_set_zoom_token(user, None)
         raise InvalidZoomTokenError

{"golden_diff": "diff --git a/version.py b/version.py\n--- a/version.py\n+++ b/version.py\n@@ -33,7 +33,7 @@\n # Changes should be accompanied by documentation explaining what the\n # new level means in api_docs/changelog.md, as well as \"**Changes**\"\n # entries in the endpoint's documentation in `zulip.yaml`.\n-API_FEATURE_LEVEL = 205\n+API_FEATURE_LEVEL = 206\n \n # Bump the minor PROVISION_VERSION to indicate that folks should provision\n # only when going from an old version of the code to a newer version. Bump\ndiff --git a/zerver/views/video_calls.py b/zerver/views/video_calls.py\n--- a/zerver/views/video_calls.py\n+++ b/zerver/views/video_calls.py\n@@ -31,7 +31,7 @@\n from zerver.lib.response import json_success\n from zerver.lib.subdomains import get_subdomain\n from zerver.lib.url_encoding import append_url_query_string\n-from zerver.lib.validator import check_dict, check_string\n+from zerver.lib.validator import check_bool, check_dict, check_string\n from zerver.models import UserProfile, get_realm\n \n \n@@ -142,13 +142,30 @@\n     return render(request, \"zerver/close_window.html\")\n \n \n-def make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpResponse:\n+@has_request_variables\n+def make_zoom_video_call(\n+    request: HttpRequest,\n+    user: UserProfile,\n+    is_video_call: bool = REQ(json_validator=check_bool, default=True),\n+) -> HttpResponse:\n     oauth = get_zoom_session(user)\n     if not oauth.authorized:\n         raise InvalidZoomTokenError\n \n+    # The meeting host has the ability to configure both their own and\n+    # participants' default video on/off state for the meeting. That's\n+    # why when creating a meeting, configure the video on/off default\n+    # according to the desired call type. Each Zoom user can still have\n+    # their own personal setting to not start video by default.\n+    payload = {\n+        \"settings\": {\n+            \"host_video\": is_video_call,\n+            \"participant_video\": is_video_call,\n+        }\n+    }\n+\n     try:\n-        res = oauth.post(\"https://api.zoom.us/v2/users/me/meetings\", json={})\n+        res = oauth.post(\"https://api.zoom.us/v2/users/me/meetings\", json=payload)\n     except OAuth2Error:\n         do_set_zoom_token(user, None)\n         raise InvalidZoomTokenError\n", "issue": "Add Zoom support for audio-only calls \nFollowing #25922, we now have support for audio calls in Zulip, but the Zoom integration doesn't yet support it.\r\n\r\nWe should figure out what parameters we need to pass when creating a Zoom call to support an \"Audio call\" button for Zoom users as well.\r\n\r\nTagged as an 8.0 release goal, since we'll want to document the audio calls feature as available for all call providers.\n", "before_files": [{"content": "import os\n\nZULIP_VERSION = \"8.0-dev+git\"\n\n# Add information on number of commits and commit hash to version, if available\nzulip_git_version_file = os.path.join(\n    os.path.dirname(os.path.abspath(__file__)), \"zulip-git-version\"\n)\nlines = [ZULIP_VERSION, \"\"]\nif os.path.exists(zulip_git_version_file):\n    with open(zulip_git_version_file) as f:\n        lines = [*f, \"\", \"\"]\nZULIP_VERSION = lines.pop(0).strip()\nZULIP_MERGE_BASE = lines.pop(0).strip()\n\nLATEST_MAJOR_VERSION = \"7.0\"\nLATEST_RELEASE_VERSION = \"7.3\"\nLATEST_RELEASE_ANNOUNCEMENT = \"https://blog.zulip.com/2023/05/31/zulip-7-0-released/\"\n\n# Versions of the desktop app below DESKTOP_MINIMUM_VERSION will be\n# prevented from connecting to the Zulip server.  Versions above\n# DESKTOP_MINIMUM_VERSION but below DESKTOP_WARNING_VERSION will have\n# a banner at the top of the page asking the user to upgrade.\nDESKTOP_MINIMUM_VERSION = \"5.4.3\"\nDESKTOP_WARNING_VERSION = \"5.9.3\"\n\n# Bump the API_FEATURE_LEVEL whenever an API change is made\n# that clients might want to condition on.  If we forget at\n# the time we make the change, then bump it later as soon\n# as we notice; clients using API_FEATURE_LEVEL will just not\n# use the new feature/API until the bump.\n#\n# Changes should be accompanied by documentation explaining what the\n# new level means in api_docs/changelog.md, as well as \"**Changes**\"\n# entries in the endpoint's documentation in `zulip.yaml`.\nAPI_FEATURE_LEVEL = 205\n\n# Bump the minor PROVISION_VERSION to indicate that folks should provision\n# only when going from an old version of the code to a newer version. Bump\n# the major version to indicate that folks should provision in both\n# directions.\n\n# Typically,\n# * adding a dependency only requires a minor version bump;\n# * removing a dependency requires a major version bump;\n# * upgrading a dependency requires a major version bump, unless the\n#   upgraded dependency is backwards compatible with all of our\n#   historical commits sharing the same major version, in which case a\n#   minor version bump suffices.\n\nPROVISION_VERSION = (247, 6)\n", "path": "version.py"}, {"content": "import hashlib\nimport json\nimport random\nimport secrets\nfrom base64 import b32encode\nfrom functools import partial\nfrom typing import Dict\nfrom urllib.parse import quote, urlencode, urljoin\n\nimport requests\nfrom defusedxml import ElementTree\nfrom django.conf import settings\nfrom django.core.signing import Signer\nfrom django.http import HttpRequest, HttpResponse\nfrom django.middleware import csrf\nfrom django.shortcuts import redirect, render\nfrom django.utils.crypto import constant_time_compare, salted_hmac\nfrom django.utils.translation import gettext as _\nfrom django.views.decorators.cache import never_cache\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_POST\nfrom oauthlib.oauth2 import OAuth2Error\nfrom requests_oauthlib import OAuth2Session\n\nfrom zerver.actions.video_calls import do_set_zoom_token\nfrom zerver.decorator import zulip_login_required\nfrom zerver.lib.exceptions import ErrorCode, JsonableError\nfrom zerver.lib.outgoing_http import OutgoingSession\nfrom zerver.lib.pysa import mark_sanitized\nfrom zerver.lib.request import REQ, has_request_variables\nfrom zerver.lib.response import json_success\nfrom zerver.lib.subdomains import get_subdomain\nfrom zerver.lib.url_encoding import append_url_query_string\nfrom zerver.lib.validator import check_dict, check_string\nfrom zerver.models import UserProfile, get_realm\n\n\nclass VideoCallSession(OutgoingSession):\n    def __init__(self) -> None:\n        super().__init__(role=\"video_calls\", timeout=5)\n\n\nclass InvalidZoomTokenError(JsonableError):\n    code = ErrorCode.INVALID_ZOOM_TOKEN\n\n    def __init__(self) -> None:\n        super().__init__(_(\"Invalid Zoom access token\"))\n\n\ndef get_zoom_session(user: UserProfile) -> OAuth2Session:\n    if settings.VIDEO_ZOOM_CLIENT_ID is None:\n        raise JsonableError(_(\"Zoom credentials have not been configured\"))\n\n    client_id = settings.VIDEO_ZOOM_CLIENT_ID\n    client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET\n\n    return OAuth2Session(\n        client_id,\n        redirect_uri=urljoin(settings.ROOT_DOMAIN_URI, \"/calls/zoom/complete\"),\n        auto_refresh_url=\"https://zoom.us/oauth/token\",\n        auto_refresh_kwargs={\n            \"client_id\": client_id,\n            \"client_secret\": client_secret,\n        },\n        token=user.zoom_token,\n        token_updater=partial(do_set_zoom_token, user),\n    )\n\n\ndef get_zoom_sid(request: HttpRequest) -> str:\n    # This is used to prevent CSRF attacks on the Zoom OAuth\n    # authentication flow.  We want this value to be unpredictable and\n    # tied to the session, but we don\u2019t want to expose the main CSRF\n    # token directly to the Zoom server.\n\n    csrf.get_token(request)\n    # Use 'mark_sanitized' to cause Pysa to ignore the flow of user controlled\n    # data out of this function. 'request.META' is indeed user controlled, but\n    # post-HMAC output is no longer meaningfully controllable.\n    return mark_sanitized(\n        \"\"\n        if getattr(request, \"_dont_enforce_csrf_checks\", False)\n        else salted_hmac(\"Zulip Zoom sid\", request.META[\"CSRF_COOKIE\"]).hexdigest()\n    )\n\n\n@zulip_login_required\n@never_cache\ndef register_zoom_user(request: HttpRequest) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    oauth = get_zoom_session(request.user)\n    authorization_url, state = oauth.authorization_url(\n        \"https://zoom.us/oauth/authorize\",\n        state=json.dumps(\n            {\"realm\": get_subdomain(request), \"sid\": get_zoom_sid(request)},\n        ),\n    )\n    return redirect(authorization_url)\n\n\n@never_cache\n@has_request_variables\ndef complete_zoom_user(\n    request: HttpRequest,\n    state: Dict[str, str] = REQ(\n        json_validator=check_dict([(\"realm\", check_string)], value_validator=check_string)\n    ),\n) -> HttpResponse:\n    if get_subdomain(request) != state[\"realm\"]:\n        return redirect(urljoin(get_realm(state[\"realm\"]).uri, request.get_full_path()))\n    return complete_zoom_user_in_realm(request)\n\n\n@zulip_login_required\n@has_request_variables\ndef complete_zoom_user_in_realm(\n    request: HttpRequest,\n    code: str = REQ(),\n    state: Dict[str, str] = REQ(\n        json_validator=check_dict([(\"sid\", check_string)], value_validator=check_string)\n    ),\n) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    if not constant_time_compare(state[\"sid\"], get_zoom_sid(request)):\n        raise JsonableError(_(\"Invalid Zoom session identifier\"))\n\n    client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET\n\n    oauth = get_zoom_session(request.user)\n    try:\n        token = oauth.fetch_token(\n            \"https://zoom.us/oauth/token\",\n            code=code,\n            client_secret=client_secret,\n        )\n    except OAuth2Error:\n        raise JsonableError(_(\"Invalid Zoom credentials\"))\n\n    do_set_zoom_token(request.user, token)\n    return render(request, \"zerver/close_window.html\")\n\n\ndef make_zoom_video_call(request: HttpRequest, user: UserProfile) -> HttpResponse:\n    oauth = get_zoom_session(user)\n    if not oauth.authorized:\n        raise InvalidZoomTokenError\n\n    try:\n        res = oauth.post(\"https://api.zoom.us/v2/users/me/meetings\", json={})\n    except OAuth2Error:\n        do_set_zoom_token(user, None)\n        raise InvalidZoomTokenError\n\n    if res.status_code == 401:\n        do_set_zoom_token(user, None)\n        raise InvalidZoomTokenError\n    elif not res.ok:\n        raise JsonableError(_(\"Failed to create Zoom call\"))\n\n    return json_success(request, data={\"url\": res.json()[\"join_url\"]})\n\n\n@csrf_exempt\n@require_POST\n@has_request_variables\ndef deauthorize_zoom_user(request: HttpRequest) -> HttpResponse:\n    return json_success(request)\n\n\n@has_request_variables\ndef get_bigbluebutton_url(\n    request: HttpRequest, user_profile: UserProfile, meeting_name: str = REQ()\n) -> HttpResponse:\n    # https://docs.bigbluebutton.org/dev/api.html#create for reference on the API calls\n    # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum\n    id = \"zulip-\" + str(random.randint(100000000000, 999999999999))\n    password = b32encode(secrets.token_bytes(20)).decode()  # 20 bytes means 32 characters\n\n    # We sign our data here to ensure a Zulip user can not tamper with\n    # the join link to gain access to other meetings that are on the\n    # same bigbluebutton server.\n    signed = Signer().sign_object(\n        {\n            \"meeting_id\": id,\n            \"name\": meeting_name,\n            \"password\": password,\n        }\n    )\n    url = append_url_query_string(\"/calls/bigbluebutton/join\", \"bigbluebutton=\" + signed)\n    return json_success(request, {\"url\": url})\n\n\n# We use zulip_login_required here mainly to get access to the user's\n# full name from Zulip to prepopulate the user's name in the\n# BigBlueButton meeting.  Since the meeting's details are encoded in\n# the link the user is clicking, there is no validation tying this\n# meeting to the Zulip organization it was created in.\n@zulip_login_required\n@never_cache\n@has_request_variables\ndef join_bigbluebutton(request: HttpRequest, bigbluebutton: str = REQ()) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None:\n        raise JsonableError(_(\"BigBlueButton is not configured.\"))\n\n    try:\n        bigbluebutton_data = Signer().unsign_object(bigbluebutton)\n    except Exception:\n        raise JsonableError(_(\"Invalid signature.\"))\n\n    create_params = urlencode(\n        {\n            \"meetingID\": bigbluebutton_data[\"meeting_id\"],\n            \"name\": bigbluebutton_data[\"name\"],\n            \"moderatorPW\": bigbluebutton_data[\"password\"],\n            # We generate the attendee password from moderatorPW,\n            # because the BigBlueButton API requires a separate\n            # password. This integration is designed to have all users\n            # join as moderators, so we generate attendeePW by\n            # truncating the moderatorPW while keeping it long enough\n            # to not be vulnerable to brute force attacks.\n            \"attendeePW\": bigbluebutton_data[\"password\"][:16],\n        },\n        quote_via=quote,\n    )\n\n    checksum = hashlib.sha256(\n        (\"create\" + create_params + settings.BIG_BLUE_BUTTON_SECRET).encode()\n    ).hexdigest()\n\n    try:\n        response = VideoCallSession().get(\n            append_url_query_string(settings.BIG_BLUE_BUTTON_URL + \"api/create\", create_params)\n            + \"&checksum=\"\n            + checksum\n        )\n        response.raise_for_status()\n    except requests.RequestException:\n        raise JsonableError(_(\"Error connecting to the BigBlueButton server.\"))\n\n    payload = ElementTree.fromstring(response.text)\n    if payload.find(\"messageKey\").text == \"checksumError\":\n        raise JsonableError(_(\"Error authenticating to the BigBlueButton server.\"))\n\n    if payload.find(\"returncode\").text != \"SUCCESS\":\n        raise JsonableError(_(\"BigBlueButton server returned an unexpected error.\"))\n\n    join_params = urlencode(\n        {\n            \"meetingID\": bigbluebutton_data[\"meeting_id\"],\n            # We use the moderator password here to grant ever user\n            # full moderator permissions to the bigbluebutton session.\n            \"password\": bigbluebutton_data[\"password\"],\n            \"fullName\": request.user.full_name,\n            # https://docs.bigbluebutton.org/dev/api.html#create\n            # The createTime option is used to have the user redirected to a link\n            # that is only valid for this meeting.\n            #\n            # Even if the same link in Zulip is used again, a new\n            # createTime parameter will be created, as the meeting on\n            # the BigBlueButton server has to be recreated. (after a\n            # few minutes)\n            \"createTime\": payload.find(\"createTime\").text,\n        },\n        quote_via=quote,\n    )\n\n    checksum = hashlib.sha256(\n        (\"join\" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()\n    ).hexdigest()\n    redirect_url_base = append_url_query_string(\n        settings.BIG_BLUE_BUTTON_URL + \"api/join\", join_params\n    )\n    return redirect(append_url_query_string(redirect_url_base, \"checksum=\" + checksum))\n", "path": "zerver/views/video_calls.py"}], "after_files": [{"content": "import os\n\nZULIP_VERSION = \"8.0-dev+git\"\n\n# Add information on number of commits and commit hash to version, if available\nzulip_git_version_file = os.path.join(\n    os.path.dirname(os.path.abspath(__file__)), \"zulip-git-version\"\n)\nlines = [ZULIP_VERSION, \"\"]\nif os.path.exists(zulip_git_version_file):\n    with open(zulip_git_version_file) as f:\n        lines = [*f, \"\", \"\"]\nZULIP_VERSION = lines.pop(0).strip()\nZULIP_MERGE_BASE = lines.pop(0).strip()\n\nLATEST_MAJOR_VERSION = \"7.0\"\nLATEST_RELEASE_VERSION = \"7.3\"\nLATEST_RELEASE_ANNOUNCEMENT = \"https://blog.zulip.com/2023/05/31/zulip-7-0-released/\"\n\n# Versions of the desktop app below DESKTOP_MINIMUM_VERSION will be\n# prevented from connecting to the Zulip server.  Versions above\n# DESKTOP_MINIMUM_VERSION but below DESKTOP_WARNING_VERSION will have\n# a banner at the top of the page asking the user to upgrade.\nDESKTOP_MINIMUM_VERSION = \"5.4.3\"\nDESKTOP_WARNING_VERSION = \"5.9.3\"\n\n# Bump the API_FEATURE_LEVEL whenever an API change is made\n# that clients might want to condition on.  If we forget at\n# the time we make the change, then bump it later as soon\n# as we notice; clients using API_FEATURE_LEVEL will just not\n# use the new feature/API until the bump.\n#\n# Changes should be accompanied by documentation explaining what the\n# new level means in api_docs/changelog.md, as well as \"**Changes**\"\n# entries in the endpoint's documentation in `zulip.yaml`.\nAPI_FEATURE_LEVEL = 206\n\n# Bump the minor PROVISION_VERSION to indicate that folks should provision\n# only when going from an old version of the code to a newer version. Bump\n# the major version to indicate that folks should provision in both\n# directions.\n\n# Typically,\n# * adding a dependency only requires a minor version bump;\n# * removing a dependency requires a major version bump;\n# * upgrading a dependency requires a major version bump, unless the\n#   upgraded dependency is backwards compatible with all of our\n#   historical commits sharing the same major version, in which case a\n#   minor version bump suffices.\n\nPROVISION_VERSION = (247, 6)\n", "path": "version.py"}, {"content": "import hashlib\nimport json\nimport random\nimport secrets\nfrom base64 import b32encode\nfrom functools import partial\nfrom typing import Dict\nfrom urllib.parse import quote, urlencode, urljoin\n\nimport requests\nfrom defusedxml import ElementTree\nfrom django.conf import settings\nfrom django.core.signing import Signer\nfrom django.http import HttpRequest, HttpResponse\nfrom django.middleware import csrf\nfrom django.shortcuts import redirect, render\nfrom django.utils.crypto import constant_time_compare, salted_hmac\nfrom django.utils.translation import gettext as _\nfrom django.views.decorators.cache import never_cache\nfrom django.views.decorators.csrf import csrf_exempt\nfrom django.views.decorators.http import require_POST\nfrom oauthlib.oauth2 import OAuth2Error\nfrom requests_oauthlib import OAuth2Session\n\nfrom zerver.actions.video_calls import do_set_zoom_token\nfrom zerver.decorator import zulip_login_required\nfrom zerver.lib.exceptions import ErrorCode, JsonableError\nfrom zerver.lib.outgoing_http import OutgoingSession\nfrom zerver.lib.pysa import mark_sanitized\nfrom zerver.lib.request import REQ, has_request_variables\nfrom zerver.lib.response import json_success\nfrom zerver.lib.subdomains import get_subdomain\nfrom zerver.lib.url_encoding import append_url_query_string\nfrom zerver.lib.validator import check_bool, check_dict, check_string\nfrom zerver.models import UserProfile, get_realm\n\n\nclass VideoCallSession(OutgoingSession):\n    def __init__(self) -> None:\n        super().__init__(role=\"video_calls\", timeout=5)\n\n\nclass InvalidZoomTokenError(JsonableError):\n    code = ErrorCode.INVALID_ZOOM_TOKEN\n\n    def __init__(self) -> None:\n        super().__init__(_(\"Invalid Zoom access token\"))\n\n\ndef get_zoom_session(user: UserProfile) -> OAuth2Session:\n    if settings.VIDEO_ZOOM_CLIENT_ID is None:\n        raise JsonableError(_(\"Zoom credentials have not been configured\"))\n\n    client_id = settings.VIDEO_ZOOM_CLIENT_ID\n    client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET\n\n    return OAuth2Session(\n        client_id,\n        redirect_uri=urljoin(settings.ROOT_DOMAIN_URI, \"/calls/zoom/complete\"),\n        auto_refresh_url=\"https://zoom.us/oauth/token\",\n        auto_refresh_kwargs={\n            \"client_id\": client_id,\n            \"client_secret\": client_secret,\n        },\n        token=user.zoom_token,\n        token_updater=partial(do_set_zoom_token, user),\n    )\n\n\ndef get_zoom_sid(request: HttpRequest) -> str:\n    # This is used to prevent CSRF attacks on the Zoom OAuth\n    # authentication flow.  We want this value to be unpredictable and\n    # tied to the session, but we don\u2019t want to expose the main CSRF\n    # token directly to the Zoom server.\n\n    csrf.get_token(request)\n    # Use 'mark_sanitized' to cause Pysa to ignore the flow of user controlled\n    # data out of this function. 'request.META' is indeed user controlled, but\n    # post-HMAC output is no longer meaningfully controllable.\n    return mark_sanitized(\n        \"\"\n        if getattr(request, \"_dont_enforce_csrf_checks\", False)\n        else salted_hmac(\"Zulip Zoom sid\", request.META[\"CSRF_COOKIE\"]).hexdigest()\n    )\n\n\n@zulip_login_required\n@never_cache\ndef register_zoom_user(request: HttpRequest) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    oauth = get_zoom_session(request.user)\n    authorization_url, state = oauth.authorization_url(\n        \"https://zoom.us/oauth/authorize\",\n        state=json.dumps(\n            {\"realm\": get_subdomain(request), \"sid\": get_zoom_sid(request)},\n        ),\n    )\n    return redirect(authorization_url)\n\n\n@never_cache\n@has_request_variables\ndef complete_zoom_user(\n    request: HttpRequest,\n    state: Dict[str, str] = REQ(\n        json_validator=check_dict([(\"realm\", check_string)], value_validator=check_string)\n    ),\n) -> HttpResponse:\n    if get_subdomain(request) != state[\"realm\"]:\n        return redirect(urljoin(get_realm(state[\"realm\"]).uri, request.get_full_path()))\n    return complete_zoom_user_in_realm(request)\n\n\n@zulip_login_required\n@has_request_variables\ndef complete_zoom_user_in_realm(\n    request: HttpRequest,\n    code: str = REQ(),\n    state: Dict[str, str] = REQ(\n        json_validator=check_dict([(\"sid\", check_string)], value_validator=check_string)\n    ),\n) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    if not constant_time_compare(state[\"sid\"], get_zoom_sid(request)):\n        raise JsonableError(_(\"Invalid Zoom session identifier\"))\n\n    client_secret = settings.VIDEO_ZOOM_CLIENT_SECRET\n\n    oauth = get_zoom_session(request.user)\n    try:\n        token = oauth.fetch_token(\n            \"https://zoom.us/oauth/token\",\n            code=code,\n            client_secret=client_secret,\n        )\n    except OAuth2Error:\n        raise JsonableError(_(\"Invalid Zoom credentials\"))\n\n    do_set_zoom_token(request.user, token)\n    return render(request, \"zerver/close_window.html\")\n\n\n@has_request_variables\ndef make_zoom_video_call(\n    request: HttpRequest,\n    user: UserProfile,\n    is_video_call: bool = REQ(json_validator=check_bool, default=True),\n) -> HttpResponse:\n    oauth = get_zoom_session(user)\n    if not oauth.authorized:\n        raise InvalidZoomTokenError\n\n    # The meeting host has the ability to configure both their own and\n    # participants' default video on/off state for the meeting. That's\n    # why when creating a meeting, configure the video on/off default\n    # according to the desired call type. Each Zoom user can still have\n    # their own personal setting to not start video by default.\n    payload = {\n        \"settings\": {\n            \"host_video\": is_video_call,\n            \"participant_video\": is_video_call,\n        }\n    }\n\n    try:\n        res = oauth.post(\"https://api.zoom.us/v2/users/me/meetings\", json=payload)\n    except OAuth2Error:\n        do_set_zoom_token(user, None)\n        raise InvalidZoomTokenError\n\n    if res.status_code == 401:\n        do_set_zoom_token(user, None)\n        raise InvalidZoomTokenError\n    elif not res.ok:\n        raise JsonableError(_(\"Failed to create Zoom call\"))\n\n    return json_success(request, data={\"url\": res.json()[\"join_url\"]})\n\n\n@csrf_exempt\n@require_POST\n@has_request_variables\ndef deauthorize_zoom_user(request: HttpRequest) -> HttpResponse:\n    return json_success(request)\n\n\n@has_request_variables\ndef get_bigbluebutton_url(\n    request: HttpRequest, user_profile: UserProfile, meeting_name: str = REQ()\n) -> HttpResponse:\n    # https://docs.bigbluebutton.org/dev/api.html#create for reference on the API calls\n    # https://docs.bigbluebutton.org/dev/api.html#usage for reference for checksum\n    id = \"zulip-\" + str(random.randint(100000000000, 999999999999))\n    password = b32encode(secrets.token_bytes(20)).decode()  # 20 bytes means 32 characters\n\n    # We sign our data here to ensure a Zulip user can not tamper with\n    # the join link to gain access to other meetings that are on the\n    # same bigbluebutton server.\n    signed = Signer().sign_object(\n        {\n            \"meeting_id\": id,\n            \"name\": meeting_name,\n            \"password\": password,\n        }\n    )\n    url = append_url_query_string(\"/calls/bigbluebutton/join\", \"bigbluebutton=\" + signed)\n    return json_success(request, {\"url\": url})\n\n\n# We use zulip_login_required here mainly to get access to the user's\n# full name from Zulip to prepopulate the user's name in the\n# BigBlueButton meeting.  Since the meeting's details are encoded in\n# the link the user is clicking, there is no validation tying this\n# meeting to the Zulip organization it was created in.\n@zulip_login_required\n@never_cache\n@has_request_variables\ndef join_bigbluebutton(request: HttpRequest, bigbluebutton: str = REQ()) -> HttpResponse:\n    assert request.user.is_authenticated\n\n    if settings.BIG_BLUE_BUTTON_URL is None or settings.BIG_BLUE_BUTTON_SECRET is None:\n        raise JsonableError(_(\"BigBlueButton is not configured.\"))\n\n    try:\n        bigbluebutton_data = Signer().unsign_object(bigbluebutton)\n    except Exception:\n        raise JsonableError(_(\"Invalid signature.\"))\n\n    create_params = urlencode(\n        {\n            \"meetingID\": bigbluebutton_data[\"meeting_id\"],\n            \"name\": bigbluebutton_data[\"name\"],\n            \"moderatorPW\": bigbluebutton_data[\"password\"],\n            # We generate the attendee password from moderatorPW,\n            # because the BigBlueButton API requires a separate\n            # password. This integration is designed to have all users\n            # join as moderators, so we generate attendeePW by\n            # truncating the moderatorPW while keeping it long enough\n            # to not be vulnerable to brute force attacks.\n            \"attendeePW\": bigbluebutton_data[\"password\"][:16],\n        },\n        quote_via=quote,\n    )\n\n    checksum = hashlib.sha256(\n        (\"create\" + create_params + settings.BIG_BLUE_BUTTON_SECRET).encode()\n    ).hexdigest()\n\n    try:\n        response = VideoCallSession().get(\n            append_url_query_string(settings.BIG_BLUE_BUTTON_URL + \"api/create\", create_params)\n            + \"&checksum=\"\n            + checksum\n        )\n        response.raise_for_status()\n    except requests.RequestException:\n        raise JsonableError(_(\"Error connecting to the BigBlueButton server.\"))\n\n    payload = ElementTree.fromstring(response.text)\n    if payload.find(\"messageKey\").text == \"checksumError\":\n        raise JsonableError(_(\"Error authenticating to the BigBlueButton server.\"))\n\n    if payload.find(\"returncode\").text != \"SUCCESS\":\n        raise JsonableError(_(\"BigBlueButton server returned an unexpected error.\"))\n\n    join_params = urlencode(\n        {\n            \"meetingID\": bigbluebutton_data[\"meeting_id\"],\n            # We use the moderator password here to grant ever user\n            # full moderator permissions to the bigbluebutton session.\n            \"password\": bigbluebutton_data[\"password\"],\n            \"fullName\": request.user.full_name,\n            # https://docs.bigbluebutton.org/dev/api.html#create\n            # The createTime option is used to have the user redirected to a link\n            # that is only valid for this meeting.\n            #\n            # Even if the same link in Zulip is used again, a new\n            # createTime parameter will be created, as the meeting on\n            # the BigBlueButton server has to be recreated. (after a\n            # few minutes)\n            \"createTime\": payload.find(\"createTime\").text,\n        },\n        quote_via=quote,\n    )\n\n    checksum = hashlib.sha256(\n        (\"join\" + join_params + settings.BIG_BLUE_BUTTON_SECRET).encode()\n    ).hexdigest()\n    redirect_url_base = append_url_query_string(\n        settings.BIG_BLUE_BUTTON_URL + \"api/join\", join_params\n    )\n    return redirect(append_url_query_string(redirect_url_base, \"checksum=\" + checksum))\n", "path": "zerver/views/video_calls.py"}]}