snapshot of https://github.com/zulip/zulip.git @ 48c69911c7dd4e03b20552eb3bd77ad8151c1357 (2026-09-15)
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
Some checks failed
Code scanning / CodeQL (push) Failing after 15s
Zulip production suite / Ubuntu 22.04 production build (push) Failing after 26s
Zulip production suite / Debian 12 production install with custom db name and user (push) Has been skipped
Zulip production suite / Ubuntu 22.04 production install and PostgreSQL upgrade with pgroonga (push) Has been skipped
Zulip production suite / Ubuntu 24.04 production install (push) Has been skipped
Zulip production suite / Ubuntu 26.04 production install (push) Has been skipped
Zulip production suite / Debian 13 production install (push) Has been skipped
Zulip production suite / 7.0 Version Upgrade (push) Has been skipped
Zulip production suite / 8.0 Version Upgrade (push) Has been skipped
Zulip production suite / 6.0 Version Upgrade (push) Has been skipped
Zulip production suite / 10.0 Version Upgrade (push) Has been skipped
Zulip production suite / 9.0 Version Upgrade (push) Has been skipped
Zulip production suite / 12.0 Version Upgrade (push) Has been skipped
Zulip production suite / 11.0 Version Upgrade (push) Has been skipped
GitHub Actions Security Analysis with zizmor / zizmor (push) Failing after 3s
Zulip CI / Debian 12 (Python 3.11, backend + documentation) (push) Failing after 26s
Zulip CI / Ubuntu 22.04 (Python 3.10, backend + frontend) (push) Failing after 3s
Zulip CI / Ubuntu 24.04 (Python 3.12, backend) (push) Failing after 25s
Zulip CI / Ubuntu 26.04 (Python 3.14, backend) (push) Failing after 25s
Zulip CI / Debian 13 (Python 3.13, backend) (push) Failing after 28s
Zulip production suite / Required jobs (push) Has been cancelled
Zulip CI / Required jobs (push) Has been cancelled
API Documentation Update Check / check-feature-level-updated (push) Has been cancelled
API Documentation Update Check / notify-if-api-docs-changed (push) Has been cancelled
This commit is contained in:
0
zerver/webhooks/jotform/__init__.py
Normal file
0
zerver/webhooks/jotform/__init__.py
Normal file
29
zerver/webhooks/jotform/doc.md
Normal file
29
zerver/webhooks/jotform/doc.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Zulip Jotform integration
|
||||
|
||||
Get Zulip notifications for your Jotform responses!
|
||||
|
||||
{start_tabs}
|
||||
|
||||
1. {!create-an-incoming-webhook.md!}
|
||||
|
||||
1. {!generate-webhook-url-basic.md!}
|
||||
|
||||
1. In the **Form Builder** on Jotform, go to **Settings**. Click on
|
||||
**INTEGRATIONS**. Search for and select **WebHooks**.
|
||||
|
||||
1. Paste the URL generated above into the field, and click
|
||||
**COMPLETE INTEGRATION**.
|
||||
|
||||
!!! tip ""
|
||||
|
||||
By default, the form's title will be used as the Zulip topic.
|
||||
|
||||
{end_tabs}
|
||||
|
||||
{!congrats.md!}
|
||||
|
||||

|
||||
|
||||
### Related documentation
|
||||
|
||||
{!webhooks-url-specification.md!}
|
||||
93
zerver/webhooks/jotform/fixtures/response.multipart
Normal file
93
zerver/webhooks/jotform/fixtures/response.multipart
Normal file
@@ -0,0 +1,93 @@
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="action"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="webhookURL"
|
||||
|
||||
https://3a95-27-5-198-172.ngrok-free.app/api/v1/external/jotform?api_key=DhXGrkyy2bK2SXMQMXwhs95H4kVVwv2w
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="username"
|
||||
|
||||
UserNiloth
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="formID"
|
||||
|
||||
243231271343446
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="type"
|
||||
|
||||
WEB
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="customParams"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="product"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="formTitle"
|
||||
|
||||
Tutor Appointment Form
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="customTitle"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="submissionID"
|
||||
|
||||
6084250982513018472
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="event"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="documentID"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="teamID"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="subject"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="isSilent"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="customBody"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="rawRequest"
|
||||
|
||||
{"slug":"submit\/243231271343446","jsExecutionTracker":"build-date-1732271172685=>init-started:1732615894703=>validator-called:1732615894706=>validator-mounted-false:1732615894706=>init-complete:1732615894707=>onsubmit-fired:1732615897940=>observerSubmitHandler_received-submit-event:1732615897941=>submit-validation-passed:1732615897942=>observerSubmitHandler_validation-passed-submitting-form:1732615897944","submitSource":"form","buildDate":"1732271172685","uploadServerUrl":"https:\/\/upload.jotform.com\/upload","eventObserver":"1","q3_studentsName":{"first":"Niloth","last":"P"},"q33_typeOf":"Online Tutoring","q34_subjectFor":"Math","q35_grade":"12","q27_priorTutoring":"No","q28_appointment":{"implementation":"new","date":"","duration":"60","timezone":"Asia\/Calcutta (GMT+06:30)"},"q15_date":{"month":"","day":"","year":""},"q19_table":{"colIds":"[\"0\",\"1\",\"2\",\"3\"]","rowIds":"[\"0\",\"1\",\"2\",\"3\"]"},"timeToSubmit":"3","temp_upload":{"q5_identityProof":["Student_ID_!#$&'()*+,-.;=@[]^_`{}~ .png#jotformfs-e4f4ece4d0a90#01940839-37f4-7a46-9b4e-71107f08774a", "Driving.license.png#jotformfs-e4f4ece4d0a90#01940839-37f4-7a46-9b4e-71107f08774a"], "q6_reports":["Report Card.pdf#jotformfs-e4f4ece4d0a90#01940839-37f4-7a46-9b4e-71107f08774a"]},"file_server":"jotforms-e4f4ece4d0a90#01940839-37f4-7a46-9b4e-71107f08774a","preview":"true","validatedNewRequiredFieldIDs":"{\"new\":1}","path":"\/submit\/243231271343446","q7_description":"","identityProof":["https:\/\/www.jotform.com\/uploads\/UserNiloth\/243231271343446\/6111139644845227683\/Student_ID_%21_%24%26%27%28%29_%2C-.%3B%3D%40%5B%5D%5E_%60%7B%7D~%20.png","https:\/\/www.jotform.com\/uploads\/UserNiloth\/243231271343446\/6111139644845227684\/Driving.license.png"],"reports":["https:\/\/www.jotform.com\/uploads\/UserNiloth\/243231271343446\/6111139644845227685\/Report%20Card.pdf"]}
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="fromTable"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="appID"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="pretty"
|
||||
|
||||
Student's Name:Niloth P, Type of Tutoring:Online Tutoring, Subject for Tutoring:Math, Grade:12, Prior Tutoring?:No, Identity Proof:Student_ID_!_$&'()_,-.;=@[]^_`{}~ .png Driving.license.png, Reports:Report Card.pdf
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="unread"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="parent"
|
||||
|
||||
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv
|
||||
Content-Disposition: form-data; name="ip"
|
||||
|
||||
27.51.18.17
|
||||
--------------------------J1JNT4vbVO4FCC1qLG4HSv--
|
||||
@@ -0,0 +1,93 @@
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="action"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="webhookURL"
|
||||
|
||||
https://a395-103-151-209-147.ngrok-free.app/api/v1/external/jotform?api_key=6SuIPsdyiL6Bgljov4wp67wD4rqHRWOg&stream=15&topic=jotform
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="username"
|
||||
|
||||
kolanuvarun739
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="formID"
|
||||
|
||||
243490908500051
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="type"
|
||||
|
||||
WEB
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="customParams"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="product"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="formTitle"
|
||||
|
||||
Sample testing
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="customTitle"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="submissionID"
|
||||
|
||||
6197916587414311452
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="event"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="documentID"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="teamID"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="subject"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="isSilent"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="customBody"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="rawRequest"
|
||||
|
||||
{"slug":"submit\/243490908500051","jsExecutionTracker":"build-date-1743981991368=>init-started:1743982445902=>validator-called:1743982445907=>validator-mounted-false:1743982445907=>init-complete:1743982445910=>onsubmit-fired:1743982457902=>observerSubmitHandler_received-submit-event:1743982457903=>submit-validation-passed:1743982457907=>observerSubmitHandler_validation-passed-submitting-form:1743982457914","submitSource":"form","buildDate":"1743981991368","uploadServerUrl":"https:\/\/upload.jotform.com\/upload","eventObserver":"1","q10_key1With":"Value1 with colon: and comma, end","q11_sameKey":"Value 1","q12_sameKey2":"Value 2","q13_sameKeyValue":"Same Key-Value","q14_value2":"Value 3","q23_multipleChoice":["Option; 1","Option2"],"event_id":"1743982445903_243490908500051_4IYtoJz","timeToSubmit":"11","temp_upload":{"q22_fileUpload":["error; frontend, UI.png#jotformfs-e4f4ece4d0a90#01960d74-6a25-7398-87c7-df56ce6711f1","Screenshot_20250331_201054.png#jotformfs-e4f4ece4d0a90#01960d74-6bbc-7e1b-8187-9028f55b6ef6"]},"file_server":"jotformfs-e4f4ece4d0a90#01960d74-6bbc-7e1b-8187-9028f55b6ef6","validatedNewRequiredFieldIDs":"{\"new\":1}","path":"\/submit\/243490908500051","fileUpload":["https:\/\/www.jotform.com\/uploads\/kolanuvarun739\/243490908500051\/6197916587414311452\/error%3B%20frontend%2C%20UI.png","https:\/\/www.jotform.com\/uploads\/kolanuvarun739\/243490908500051\/6197916587414311452\/Screenshot_20250331_201054.png"]}
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="fromTable"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="appID"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="pretty"
|
||||
|
||||
Key1 with colon: and comma, end:Value1 with colon: and comma, end, Same Key:Value 1, Same Key:Value 2, Same Key-Value:Same Key-Value, Value 2:Value 3, Multiple Choice Question, options::Option; 1 Option2, File Upload with colon : and comma , end:error; frontend, UI.png Screenshot_20250331_201054.png
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="unread"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="parent"
|
||||
|
||||
|
||||
--------------------------BIoxRptelgempTlOi8T0XA
|
||||
Content-Disposition: form-data; name="ip"
|
||||
|
||||
103.151.209.147
|
||||
--------------------------BIoxRptelgempTlOi8T0XA--
|
||||
@@ -0,0 +1,93 @@
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="action"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="webhookURL"
|
||||
|
||||
https://959c-103-151-209-117.ngrok-free.app/api/v1/external/jotform?api_key=3BKWHdX6ZZpvIkzts68PKE30Dw8871kS&stream=22&topic=Feedback+Form
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="username"
|
||||
|
||||
kolanuvarun739
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="formID"
|
||||
|
||||
243615086540051
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="type"
|
||||
|
||||
WEB
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="customParams"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="product"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="formTitle"
|
||||
|
||||
Feedback Form
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="customTitle"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="submissionID"
|
||||
|
||||
6114090137116205381
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="event"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="documentID"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="teamID"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="subject"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="isSilent"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="customBody"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="rawRequest"
|
||||
|
||||
{"slug":"submit\/243615086540051","jsExecutionTracker":"build-date-1735597456482=>init-started:1735597457312=>validator-called:1735597457313=>validator-mounted-false:1735597457313=>init-complete:1735597457316=>interval-complete:1735597478103=>onsubmit-fired:1735598288656=>observerSubmitHandler_received-submit-event:1735598288656=>submit-validation-passed:1735598288659=>observerSubmitHandler_validation-passed-submitting-form:1735598288668=>init-started:1735598444979=>validator-called:1735598444980=>validator-mounted-false:1735598444980=>init-complete:1735598444983=>init-started:1735598447982=>validator-called:1735598447983=>validator-mounted-false:1735598447983=>init-complete:1735598447985=>interval-complete:1735598469193=>onsubmit-fired:1735598480950=>observerSubmitHandler_received-submit-event:1735598480950=>submit-validation-passed:1735598480954=>observerSubmitHandler_validation-passed-submitting-form:1735598480962=>init-started:1735598975826=>validator-called:1735598975828=>validator-mounted-false:1735598975828=>init-complete:1735598975836=>interval-complete:1735598996957=>onsubmit-fired:1735599067954=>observerSubmitHandler_received-submit-event:1735599067954=>submit-validation-passed:1735599067956=>observerSubmitHandler_validation-passed-submitting-form:1735599067962=>init-started:1735599159857=>validator-called:1735599159859=>validator-mounted-false:1735599159859=>init-complete:1735599159861=>interval-complete:1735599181032=>onsubmit-fired:1735599182145=>observerSubmitHandler_received-submit-event:1735599182145=>submit-validation-passed:1735599182149=>observerSubmitHandler_validation-passed-submitting-form:1735599182155=>init-started:1735599251872=>validator-called:1735599251873=>validator-mounted-false:1735599251873=>init-complete:1735599251877=>interval-complete:1735599272970=>onsubmit-fired:1735599584836=>observerSubmitHandler_received-submit-event:1735599584836=>submit-validation-passed:1735599584840=>observerSubmitHandler_validation-passed-submitting-form:1735599584847=>init-started:1735599791882=>validator-called:1735599791884=>validator-mounted-false:1735599791884=>init-complete:1735599791885=>interval-complete:1735599813033=>onsubmit-fired:1735599813327=>observerSubmitHandler_received-submit-event:1735599813327=>submit-validation-passed:1735599813330=>observerSubmitHandler_validation-passed-submitting-form:1735599813338","submitSource":"form","buildDate":"1735598447069","uploadServerUrl":"https:\/\/upload.jotform.com\/upload","eventObserver":"1","q3_howOften":"Daily","q4_howLikely":"9","q7_feedback":"The new personalized recommendations feature is great!","event_id":"1735599791882_243615086540051_ZWbBbs3","timeToSubmit":"20","temp_upload":{"q9_filesRelated":["frontend setup.jpg#jotformfs-e4f4ece4d0a90#019419cf-4a02-7e71-a373-8a2d40622c3d","workflow.png#jotformfs-e4f4ece4d0a90#019419cf-4b57-7881-82a8-ab02b2cde277"]},"file_server":"jotformfs-e4f4ece4d0a90#019419cf-6b35-79cf-86c7-880b6c4343ca","validatedNewRequiredFieldIDs":"{\"new\":1}","path":"\/submit\/243615086540051","filesRelated":["https:\/\/www.jotform.com\/uploads\/kolanuvarun739\/243615086540051\/6114090137116205381\/frontend%20setup.jpg","https:\/\/www.jotform.com\/uploads\/kolanuvarun739\/243615086540051\/6114090137116205381\/workflow.png"]}
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="fromTable"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="appID"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="pretty"
|
||||
|
||||
How often do you use the application?:Daily, How likely are you to recommend it to a friend on a scale of 0-10?:9, Feedback:The new personalized recommendations feature is great!, Upload images of your customized setup to get featured!:frontend setup.jpg workflow.png
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="unread"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="parent"
|
||||
|
||||
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj
|
||||
Content-Disposition: form-data; name="ip"
|
||||
|
||||
103.151.209.117
|
||||
--------------------------1WSA91c0mNqsH82UlEp0qj--
|
||||
70
zerver/webhooks/jotform/tests.py
Normal file
70
zerver/webhooks/jotform/tests.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from typing_extensions import override
|
||||
|
||||
from zerver.lib.test_classes import WebhookTestCase
|
||||
from zerver.lib.webhooks.common import parse_multipart_string
|
||||
|
||||
|
||||
class JotformHookTests(WebhookTestCase):
|
||||
def test_response(self) -> None:
|
||||
expected_title = "Tutor Appointment Form"
|
||||
expected_message = """
|
||||
* **Student's Name**: Niloth P
|
||||
* **Type of Tutoring**: Online Tutoring
|
||||
* **Subject for Tutoring**: Math
|
||||
* **Grade**: 12
|
||||
* **Prior Tutoring?** No
|
||||
|
||||
* **Identity Proof**: [Student_ID_!_$&'()_,-.;=@[]^_`{}~ .png](https://www.jotform.com/uploads/UserNiloth/243231271343446/6111139644845227683/Student_ID_%21_%24%26%27%28%29_%2C-.%3B%3D%40%5B%5D%5E_%60%7B%7D~%20.png), [Driving.license.png](https://www.jotform.com/uploads/UserNiloth/243231271343446/6111139644845227684/Driving.license.png)
|
||||
|
||||
* **Reports**: [Report Card.pdf](https://www.jotform.com/uploads/UserNiloth/243231271343446/6111139644845227685/Report%20Card.pdf)""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"response",
|
||||
expected_title,
|
||||
expected_message,
|
||||
content_type="multipart/form-data",
|
||||
)
|
||||
|
||||
def test_screenshot_response(self) -> None:
|
||||
expected_title = "Feedback Form"
|
||||
expected_message = """
|
||||
* **How often do you use the application?** Daily
|
||||
* **How likely are you to recommend it to a friend on a scale of 0-10?** 9
|
||||
* **Feedback**: The new personalized recommendations feature is great!
|
||||
|
||||
* **Upload images of your customized setup to get featured!**: [frontend setup.jpg](https://www.jotform.com/uploads/kolanuvarun739/243615086540051/6114090137116205381/frontend%20setup.jpg), [workflow.png](https://www.jotform.com/uploads/kolanuvarun739/243615086540051/6114090137116205381/workflow.png)""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"screenshot_response",
|
||||
expected_title,
|
||||
expected_message,
|
||||
content_type="multipart/form-data",
|
||||
)
|
||||
|
||||
def test_response_with_colon_comma_characters(self) -> None:
|
||||
expected_title = "Sample testing"
|
||||
expected_message = """
|
||||
* **Key1 with colon: and comma, end**: Value1 with colon: and comma, end
|
||||
* **Same Key**: Value 1
|
||||
* **Same Key**: Value 2
|
||||
* **Same Key-Value**: Same Key-Value
|
||||
* **Value 2**: Value 3
|
||||
* **Multiple Choice Question, options:**: Option; 1 Option2
|
||||
|
||||
* **File Upload with colon : and comma , end**: [error; frontend, UI.png](https://www.jotform.com/uploads/kolanuvarun739/243490908500051/6197916587414311452/error%3B%20frontend%2C%20UI.png), [Screenshot_20250331_201054.png](https://www.jotform.com/uploads/kolanuvarun739/243490908500051/6197916587414311452/Screenshot_20250331_201054.png)""".strip()
|
||||
|
||||
self.check_webhook(
|
||||
"response_with_colon_comma_characters",
|
||||
expected_title,
|
||||
expected_message,
|
||||
content_type="multipart/form-data",
|
||||
)
|
||||
|
||||
def test_bad_payload(self) -> None:
|
||||
with self.assertRaisesRegex(AssertionError, "Unable to handle Jotform payload"):
|
||||
self.check_webhook("response")
|
||||
|
||||
@override
|
||||
def get_payload(self, fixture_name: str) -> dict[str, str]:
|
||||
body = self.webhook_fixture_data("jotform", fixture_name, file_type="multipart")
|
||||
return parse_multipart_string(body)
|
||||
169
zerver/webhooks/jotform/view.py
Normal file
169
zerver/webhooks/jotform/view.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# Webhooks for external integrations.
|
||||
|
||||
import json
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from zerver.decorator import webhook_view
|
||||
from zerver.lib.exceptions import JsonableError
|
||||
from zerver.lib.response import json_success
|
||||
from zerver.lib.typed_endpoint import typed_endpoint_without_parameters
|
||||
from zerver.lib.webhooks.common import check_send_webhook_message
|
||||
from zerver.models import UserProfile
|
||||
|
||||
|
||||
def format_uploaded_file_links(file_question_value: str, file_url_mappings: dict[str, str]) -> str:
|
||||
words = file_question_value.split()
|
||||
url_markdown_list = []
|
||||
current_file_name_words = []
|
||||
for word in words:
|
||||
current_file_name_words.append(word)
|
||||
|
||||
# Greedy approach to find the filenames in file_url_mappings.
|
||||
potential_file_name = " ".join(current_file_name_words)
|
||||
if potential_file_name in file_url_mappings:
|
||||
file_url = file_url_mappings.get(potential_file_name)
|
||||
url_markdown_list.append(f"[{potential_file_name}]({file_url})")
|
||||
current_file_name_words = []
|
||||
|
||||
return ", ".join(url_markdown_list)
|
||||
|
||||
|
||||
def is_non_file_upload_question_with_response(field: str, raw_request: dict[str, Any]) -> bool:
|
||||
"""
|
||||
File upload questions are in raw_request["temp_upload"].
|
||||
So, the other questions are the keys in raw_request starting
|
||||
with "q". We exclude any questions with a zero length value,
|
||||
because they are not in the "pretty" data of the payload.
|
||||
"""
|
||||
field_value = raw_request.get(field)
|
||||
return bool(
|
||||
field
|
||||
and field[0] == "q"
|
||||
and field_value
|
||||
and (
|
||||
isinstance(field_value, str)
|
||||
or (
|
||||
isinstance(field_value, dict)
|
||||
# Non-empty Appointment, Date fields
|
||||
and "" not in field_value.values()
|
||||
# Non-empty Input Table field
|
||||
and not (
|
||||
len(field_value) == 2 and "colIds" in field_value and "rowIds" in field_value
|
||||
)
|
||||
)
|
||||
# Multiple choice questions
|
||||
or isinstance(field_value, list)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def format_value(value: str | list[str] | dict[str, str]) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
elif isinstance(value, list):
|
||||
return " ".join(value)
|
||||
|
||||
return " ".join(value.values())
|
||||
|
||||
|
||||
def get_pretty_fields(pretty: str, values: list[str]) -> list[tuple[str, str]]:
|
||||
"""
|
||||
The format of "pretty" data in the payload is "key1:value1, key2:value2, ..."
|
||||
The parameter "values" contains all substrings that appear as values
|
||||
in the pretty data. Extracts each key-value pair by locating the position
|
||||
of each value in the string and separating the corresponding key and value,
|
||||
by tracking the starting position of the next key-value pair.
|
||||
This approach is necessary because keys and values may contain colons
|
||||
or commas, making it unreliable to simply split the string using ', '.
|
||||
|
||||
Returns a list of (key, value) pairs to preserve order and allow duplicate keys.
|
||||
"""
|
||||
|
||||
pretty_fields = []
|
||||
pair_starting_index = 0
|
||||
|
||||
for value in values:
|
||||
value_length = len(value)
|
||||
possible_match_index = pretty.find(value + ":", pair_starting_index)
|
||||
|
||||
# If the current pair starts with a key that is equal to the value
|
||||
if possible_match_index == pair_starting_index:
|
||||
key = value
|
||||
val = value
|
||||
|
||||
# 3 characters for colon, comma and space.
|
||||
pair_starting_index += 2 * value_length + 3
|
||||
else:
|
||||
value_occurrence_index = pretty.find(value, pair_starting_index)
|
||||
value_ending_index = value_occurrence_index + value_length
|
||||
|
||||
# -1 for colon, separating key and value
|
||||
key = pretty[pair_starting_index : value_occurrence_index - 1]
|
||||
val = pretty[value_occurrence_index:value_ending_index]
|
||||
|
||||
# 2 characters for comma and space, which separate the fields
|
||||
pair_starting_index = value_ending_index + 2
|
||||
|
||||
pretty_fields.append((key, val))
|
||||
|
||||
return pretty_fields
|
||||
|
||||
|
||||
@webhook_view("Jotform")
|
||||
@typed_endpoint_without_parameters
|
||||
def api_jotform_webhook(
|
||||
request: HttpRequest,
|
||||
user_profile: UserProfile,
|
||||
) -> HttpResponse:
|
||||
payload = request.POST
|
||||
topic_name = payload.get("formTitle")
|
||||
|
||||
raw_request = json.loads(payload.get("rawRequest", "{}"))
|
||||
non_file_upload_values_in_pretty_data = [
|
||||
format_value(value)
|
||||
for field, value in raw_request.items()
|
||||
if is_non_file_upload_question_with_response(field, raw_request)
|
||||
]
|
||||
|
||||
url_mappings = {}
|
||||
file_values_in_pretty_data = []
|
||||
if raw_request.get("temp_upload"):
|
||||
upload_keys = raw_request.get("temp_upload").keys()
|
||||
file_questions_keys = [key.split("_", 1)[-1] for key in upload_keys]
|
||||
file_values_in_pretty_data = [
|
||||
" ".join(unquote(url.split("/")[-1]) for url in raw_request.get(key, []))
|
||||
for key in file_questions_keys
|
||||
]
|
||||
url_mappings = {
|
||||
unquote(url.split("/")[-1]): url
|
||||
for key in file_questions_keys
|
||||
for url in raw_request.get(key, [])
|
||||
}
|
||||
|
||||
# Non file values come earlier than file values in the "pretty" data of the payload.
|
||||
pretty_values = non_file_upload_values_in_pretty_data + list(file_values_in_pretty_data)
|
||||
pretty_fields = get_pretty_fields(payload.get("pretty", ""), pretty_values)
|
||||
|
||||
if not topic_name or not pretty_fields:
|
||||
raise JsonableError(_("Unable to handle Jotform payload"))
|
||||
|
||||
form_response = ""
|
||||
for index, (label, value) in enumerate(pretty_fields):
|
||||
separator = " " if label.endswith("?") else ": "
|
||||
|
||||
# File upload fields are last in the "pretty" payload data.
|
||||
if index >= len(non_file_upload_values_in_pretty_data):
|
||||
value = format_uploaded_file_links(value, url_mappings)
|
||||
# We add a new line so that image files are rendered
|
||||
# correctly in the message.
|
||||
form_response += "\n"
|
||||
form_response += f"* **{label}**{separator}{value}\n"
|
||||
|
||||
message = form_response.strip()
|
||||
|
||||
check_send_webhook_message(request, user_profile, topic_name, message)
|
||||
return json_success(request)
|
||||
Reference in New Issue
Block a user