From 59db27a5c002870aa4c8ccd2316a39c3d1e06423 Mon Sep 17 00:00:00 2001 From: Everard van Nieuwenburg Date: Wed, 30 Sep 2020 20:24:24 +0200 Subject: [PATCH 01/26] New workflow triggered using repository_dispatch --- .github/workflows/schedule_zoom_talks.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/workflows/schedule_zoom_talks.yml diff --git a/.github/workflows/schedule_zoom_talks.yml b/.github/workflows/schedule_zoom_talks.yml new file mode 100644 index 0000000..494bf95 --- /dev/null +++ b/.github/workflows/schedule_zoom_talks.yml @@ -0,0 +1,13 @@ +name: Schedule Zoom Talks +on: + repository_dispatch: + types: [schedule-zoom-talk] + +jobs: + scheduleTalks: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.client_payload.ref }} + - run: echo ${{ github.event.client_payload.sha }} From c11f047f2bda0885d246feaabde235119ab1e526 Mon Sep 17 00:00:00 2001 From: Everard van Nieuwenburg Date: Wed, 30 Sep 2020 21:56:33 +0200 Subject: [PATCH 02/26] Update schedule_zoom_talks.yml --- .github/workflows/schedule_zoom_talks.yml | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/.github/workflows/schedule_zoom_talks.yml b/.github/workflows/schedule_zoom_talks.yml index 494bf95..68ea21a 100644 --- a/.github/workflows/schedule_zoom_talks.yml +++ b/.github/workflows/schedule_zoom_talks.yml @@ -7,7 +7,21 @@ jobs: scheduleTalks: runs-on: ubuntu-latest steps: + - name: Checkout code - uses: actions/checkout@v2 + + - name: Set up Python 3.8 + uses: actions/setup-python@v2 with: - ref: ${{ github.event.client_payload.ref }} - - run: echo ${{ github.event.client_payload.sha }} + python-version: 3.8 + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + if [ -f bots/zoom_scheduling_bot_requirements.txt ]; then pip install -r bots/zoom_scheduling_bot_requirements.txt; fi + + - name: Schedule talks + working-directory: ./bots + run: python3 schedulezoomtalks.py + env: + VSF_BOT_TOKEN: ${{ secrets.VSF_BOT_TOKEN }} From d260eca79b136d7269761ee641bbfc07e7c54f01 Mon Sep 17 00:00:00 2001 From: Everard van Nieuwenburg Date: Wed, 30 Sep 2020 21:57:37 +0200 Subject: [PATCH 03/26] Create zoom_scheduling_bot_requirements.txt --- .github/workflows/zoom_scheduling_bot_requirements.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/workflows/zoom_scheduling_bot_requirements.txt diff --git a/.github/workflows/zoom_scheduling_bot_requirements.txt b/.github/workflows/zoom_scheduling_bot_requirements.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.github/workflows/zoom_scheduling_bot_requirements.txt @@ -0,0 +1 @@ + From 22eef8a8a6ebb40756b053ffd78220b25e16731e Mon Sep 17 00:00:00 2001 From: Everard van Nieuwenburg Date: Wed, 30 Sep 2020 21:57:57 +0200 Subject: [PATCH 04/26] Create schedulezoomtalks.py --- bots/schedulezoomtalks.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 bots/schedulezoomtalks.py diff --git a/bots/schedulezoomtalks.py b/bots/schedulezoomtalks.py new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/bots/schedulezoomtalks.py @@ -0,0 +1 @@ + From ae82381b6c885f81a5e21aa7384476fa61096e8d Mon Sep 17 00:00:00 2001 From: Everard van Nieuwenburg Date: Wed, 30 Sep 2020 22:44:33 +0200 Subject: [PATCH 05/26] WIP --- bots/schedulezoomtalks.py | 110 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/bots/schedulezoomtalks.py b/bots/schedulezoomtalks.py index 8b13789..f7d6846 100644 --- a/bots/schedulezoomtalks.py +++ b/bots/schedulezoomtalks.py @@ -1 +1,111 @@ +import os +import re +from copy import deepcopy +from pathlib import Path +from io import StringIO +import github +from ruamel.yaml import YAML + +import base64 +from subprocess import check_output + +import jwt +import requests +from requests import get, post, put + +yaml = YAML() +TALKS_FILE = "speakers_corner_talks.yml" + +def zoom_headers(): + token = jwt.encode( + # Create a payload of the token containing API Key & expiration time + {"iss": os.getenv("ZOOM_API_KEY"), "exp": time() + 5000}, + os.getenv("ZOOM_API_SECRET"), + algorithm='HS256' + ).decode('utf-8') + return {'authorization': f'Bearer {token}', 'content-type': 'application/json'} + +def schedule_zoom_talk(talk, headers): + time = talk.get("time") + + request_body = + { + "topic": "Speakers\' corner talk by %s"%(talk.get("name")), + "type": 2, # Scheduled meeting + "start_time": talk.get("time"), #2020-03-31T17:00:00 + "timezone": "UTC", + "duration": 90, # 90 minutes + "schedule_for": "string", # Zoom user ID or Zoom email address + "agenda":talk.get("title"), + "password": "", + "settings": { + "host_video": True, + "participant_video": True, + "cn_meeting": False, # Host the meeting in China? + "in_meeting": False, # Host the meeting in India? + "join_before_host": False, + "mute_upon_entry": True, + "watermark": False, # Add a watermark when screensharing? + "use_pmi": False, # Don't use the Personal Meeting ID, but generate one + "approval_type": 0, # Automatically approve + "audio": "both", + "auto_recording": "cloud", + "enforce_login": False, + "alternative_hosts": "", + "registrants_email_notification": True, + "contact_email": "...", + } + } + + response = requests.post( + f"https://api.zoom.us/v2/users/{0}/meetings", + body=request_body, + headers=headers, + ) + + # Extract JSON + response = response.json() + + return response.id, "" + +def parse_talks(gh): + repo = gh.get_repo("virtualscienceforum/virtualscienceforum") + headers = zoom_headers() + + try: + talks_data = repo.get_contents(TALKS_FILE, ref="master") + talks = yaml.load(StringIO(talks_data.decoded_content.decode())) + except github.UnknownObjectException: + talks_data = None + talks = [] + + num_updated = 0 + for talk in talks: + if( talk.get('zoom_link', "") == "" && talk.get('event_type') == "speakers_corner" ): + # Schedule the talk + meeting_id, hostkey = schedule_zoom_talk(talk, headers) + # Update the talk + talk.get("zoom_meeting_id") = meeting_id + + # Do something with hostkey + + num_updated += 1 + + serialized = StringIO() + yaml.dump(talks, serialized) + + # If we added Zoom links, we should update the file in the repo + if num_updated: + repo.update_file( + TALKS_FILE, f"Added Zoom link{1} for {0} scheduled speakers\'"\ + "corner talk{1}".format(num_updated,'' if num_updated == 1 else 's'), + serialized.getvalue(), + sha=talks_data.sha, + branch='master' + ) + + +if __name__ == "__main__": + gh = github.Github(os.getenv("VSF_BOT_TOKEN")) + #host_key = parse_talks(gh) From 2dea154289fbf736846b5f7b0040ad70bb3ecfae Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Thu, 8 Oct 2020 11:16:24 +0200 Subject: [PATCH 06/26] WIP --- researchseminarsdotorg.py | 103 ++++++++++++++++++ ...hedulezoomtalks.py => schedulezoomtalks.py | 102 ++++++++--------- 2 files changed, 148 insertions(+), 57 deletions(-) create mode 100644 researchseminarsdotorg.py rename bots/schedulezoomtalks.py => schedulezoomtalks.py (61%) diff --git a/researchseminarsdotorg.py b/researchseminarsdotorg.py new file mode 100644 index 0000000..023ba23 --- /dev/null +++ b/researchseminarsdotorg.py @@ -0,0 +1,103 @@ +import os +from requests import get, post + +SPEAKERS_CORNER_SEMINAR_SERIES = {"series_id": "speakerscorner", + "name": "Speakers\' Corner", + "is_conference": False, + "topics": [""], # TODO: Get a list of topics + "language": "en", + "institutions": ["Global"], + "timezone": "America/New_York", + "homepage": "https://virtualscienceforum.org/speakerscorner.md", + "visibility": 1, # 0=private, 1=unlisted, 2=public + "access_control": 0, # 0=open, see schema for more + "slots": [""], + "organizers": [{"name": "Virtual Science Forum", + "email": "vsf@virtualscienceforum.org", + "homepage": "https://virtualscienceforum.org", + "organizer": True, + "order": 0, + "display": True}]} + +def find_seminar_series(series_id): + url = "https://researchseminars.org/api/0/search/series?series_id=%s"%series_id + r = get(url) + if r.status_code == 200: + J = r.json() + results = J["properties"]["results"] + return (len(results) != 0) + +def create_seminar_series(payload, authorization): + url = "https://researchseminars.org/api/0/save/series/" + r = post(url, json=payload, headers={"authorization":authorization}) + J = r.json() + code = J.get("code") + + if r.status_code == 200: + if code == "warning": + return True, J["warnings"] + else: + return True, "" + else: + return False, "" + +def edit_seminar_series(name, payload, authorization): + url = "https://researchseminars.org/api/0/save/series/" + r = post(url, json=payload, headers={"authorization":authorization}) + J = r.json() + code = J.get("code") + + if r.status_code == 200: + if code == "warning": + return True, J["warnings"] + else: + return True, "" + else: + return False, "" + +def add_talk_to_series(series_id, payload, authorization): + url = "https://researchseminars.org/api/0/save/talk/" + r = post(url, json=payload, headers={"authorization":authorization}) + J = r.json() + code = J.get("code") + if r.status_code == 200: + if code == "warning": + return J["series_ctr"], J["warnings"] + else: + return J["series_ctr"] + else: + return "", r.status_code + +def add_talk_to_speakerscorner(talk): + # talk should be provided in yaml format + api_token = os.getenv("RESEARCHSEMINARS_API_TOKEN") + authorization = "vsf@virtualscienceforum.org %s" % api_token + + # Find speakers' corner series, and create it if it doesn't exist + if( !find_seminar_series("speakerscorner") ): + create_seminar_series(SPEAKERS_CORNER_SEMINAR_SERIES) + + # TODO: Figure out if we need to edit the series; + # Would be annoying since edits have to be approved + + # Set up payload for talk creation + talk_payload = {"title":talk.get('title'), + "speaker":talk.get('author'), # TODO: will be 'speakerS' + "live_link":"zoom.us/%s"%talk.get('zoom_meeting_id'), + "online":1, + "start_time":talk.get("time"), + "timezone":"UTC" # TODO + } + + # Make request to remote API + series_ctr, warnings = add_talk_to_series("speakerscorner", talk_payload, authorization) + + if series_ctr != "": + print("Talk with id {0} successfully added".format(series_ctr)) + if warnings != "": + print("Warnings: {0}".format(warnings)) + return True + else: + print("-- ERROR -- ") + print("Could not add talk to series, status code {0}".format(warnings)) + return False diff --git a/bots/schedulezoomtalks.py b/schedulezoomtalks.py similarity index 61% rename from bots/schedulezoomtalks.py rename to schedulezoomtalks.py index f7d6846..b313d04 100644 --- a/bots/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -1,43 +1,29 @@ import os -import re -from copy import deepcopy -from pathlib import Path -from io import StringIO import github -from ruamel.yaml import YAML - -import base64 -from subprocess import check_output - -import jwt import requests +from researchseminarsdotorg import * from requests import get, post, put +from io import StringIO +from ruamel.yaml import YAML +from common import * -yaml = YAML() TALKS_FILE = "speakers_corner_talks.yml" -def zoom_headers(): - token = jwt.encode( - # Create a payload of the token containing API Key & expiration time - {"iss": os.getenv("ZOOM_API_KEY"), "exp": time() + 5000}, - os.getenv("ZOOM_API_SECRET"), - algorithm='HS256' - ).decode('utf-8') - return {'authorization': f'Bearer {token}', 'content-type': 'application/json'} - -def schedule_zoom_talk(talk, headers): - time = talk.get("time") +def schedule_zoom_talk(talk) -> string: + # Form the talk registration body request_body = { "topic": "Speakers\' corner talk by %s"%(talk.get("name")), "type": 2, # Scheduled meeting "start_time": talk.get("time"), #2020-03-31T17:00:00 "timezone": "UTC", - "duration": 90, # 90 minutes - "schedule_for": "string", # Zoom user ID or Zoom email address + "duration": 60, # 90 minutes + "schedule_for": "vsf@virtualscienceforum.org", # Zoom user ID or Zoom email address "agenda":talk.get("title"), "password": "", + + # Meeting settings "settings": { "host_video": True, "participant_video": True, @@ -53,25 +39,40 @@ def schedule_zoom_talk(talk, headers): "enforce_login": False, "alternative_hosts": "", "registrants_email_notification": True, - "contact_email": "...", + "contact_email": "vsf@virtualscienceforum.org", } } - response = requests.post( - f"https://api.zoom.us/v2/users/{0}/meetings", - body=request_body, - headers=headers, + response = zoom_request( + requests.post, + f"{ZOOM_API}users/{user_id}/meetings", + params={"body":request_body} ) - # Extract JSON - response = response.json() + return response.id + +def parse_talks(talks) -> int: + num_updated = 0 + for talk in talks: + if( talk.get('zoom_link', "") == "" && talk.get('event_type') == "speakers_corner" ): + # Schedule the talk + meeting_id = schedule_zoom_talk(talk) + # Update the talk + talk.get("zoom_meeting_id") = meeting_id + num_updated += 1 + + # Add this talk to researchseminars.org + add_talk_to_speakerscorner(talk) - return response.id, "" + return num_updated -def parse_talks(gh): +if __name__ == "__main__": + # Get a handle on the repository + gh = github.Github(os.getenv("VSF_BOT_TOKEN")) repo = gh.get_repo("virtualscienceforum/virtualscienceforum") - headers = zoom_headers() + # Read the talks file + yaml = YAML() try: talks_data = repo.get_contents(TALKS_FILE, ref="master") talks = yaml.load(StringIO(talks_data.decoded_content.decode())) @@ -79,33 +80,20 @@ def parse_talks(gh): talks_data = None talks = [] - num_updated = 0 - for talk in talks: - if( talk.get('zoom_link', "") == "" && talk.get('event_type') == "speakers_corner" ): - # Schedule the talk - meeting_id, hostkey = schedule_zoom_talk(talk, headers) - # Update the talk - talk.get("zoom_meeting_id") = meeting_id - - # Do something with hostkey - - num_updated += 1 + # If there are talks to be parsed... + if len(talks) != 0: + # ... parse them and keep track of how many we updated + num_updated = parse_talks(talks) - serialized = StringIO() - yaml.dump(talks, serialized) + # If we added Zoom links, we should update the file in the repo + if num_updated != 0: + serialized = StringIO() + yaml.dump(talks, serialized) - # If we added Zoom links, we should update the file in the repo - if num_updated: - repo.update_file( + repo.update_file( TALKS_FILE, f"Added Zoom link{1} for {0} scheduled speakers\'"\ "corner talk{1}".format(num_updated,'' if num_updated == 1 else 's'), serialized.getvalue(), sha=talks_data.sha, branch='master' - ) - - -if __name__ == "__main__": - gh = github.Github(os.getenv("VSF_BOT_TOKEN")) - #host_key = parse_talks(gh) - + ) From 22625c6cbbe15f6ead6353fbec2b7b0af943751b Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Thu, 8 Oct 2020 12:41:47 +0200 Subject: [PATCH 07/26] Implement round of comments; password action missing --- ..._bot_requirements.txt => requirements.txt} | 0 .github/workflows/schedule_zoom_talks.yml | 8 ++- common.py | 7 ++- schedulezoomtalks.py | 58 ++++++++++++------- 4 files changed, 46 insertions(+), 27 deletions(-) rename .github/workflows/{zoom_scheduling_bot_requirements.txt => requirements.txt} (100%) diff --git a/.github/workflows/zoom_scheduling_bot_requirements.txt b/.github/workflows/requirements.txt similarity index 100% rename from .github/workflows/zoom_scheduling_bot_requirements.txt rename to .github/workflows/requirements.txt diff --git a/.github/workflows/schedule_zoom_talks.yml b/.github/workflows/schedule_zoom_talks.yml index 68ea21a..53e0ee8 100644 --- a/.github/workflows/schedule_zoom_talks.yml +++ b/.github/workflows/schedule_zoom_talks.yml @@ -2,7 +2,7 @@ name: Schedule Zoom Talks on: repository_dispatch: types: [schedule-zoom-talk] - + jobs: scheduleTalks: runs-on: ubuntu-latest @@ -14,14 +14,16 @@ jobs: uses: actions/setup-python@v2 with: python-version: 3.8 - + - name: Install dependencies run: | python -m pip install --upgrade pip - if [ -f bots/zoom_scheduling_bot_requirements.txt ]; then pip install -r bots/zoom_scheduling_bot_requirements.txt; fi + pip install -r requirements.txt - name: Schedule talks working-directory: ./bots run: python3 schedulezoomtalks.py env: VSF_BOT_TOKEN: ${{ secrets.VSF_BOT_TOKEN }} + ZOOM_API_KEY: ${{ secrets.ZOOM_API_KEY }} + ZOOM_API_SECRET: ${{ secrets.ZOOM_API_SECRET }} diff --git a/common.py b/common.py index e1f5f5b..296213a 100644 --- a/common.py +++ b/common.py @@ -7,6 +7,7 @@ ZOOM_API = "https://api.zoom.us/v2/" SPEAKERS_CORNER_USER_ID = "D0n5UNEHQiajWtgdWLlNSA" +TALKS_FILE = "speakers_corner_talks.yml" @lru_cache() def zoom_headers(duration: int=100) -> dict: @@ -43,7 +44,7 @@ def speakers_corner_user_id() -> str: def all_meetings(user_id) -> list: """Return all meetings by a user. - + Handles pagination, and adds ``live: True`` to a meeting that is running (if any). """ meetings = [] @@ -58,7 +59,7 @@ def all_meetings(user_id) -> list: next_page_token = meetings_page["next_page_token"] if not next_page_token: break - + live_meetings = zoom_request( requests.get, f"{ZOOM_API}users/{user_id}/meetings", @@ -69,5 +70,5 @@ def all_meetings(user_id) -> list: for meeting in meetings: if meeting["id"] == live_meetings[0]["id"]: meeting["live"] = True - + return meetings diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index b313d04..e678e3d 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -1,39 +1,47 @@ import os import github import requests +import secrets from researchseminarsdotorg import * from requests import get, post, put from io import StringIO from ruamel.yaml import YAML from common import * -TALKS_FILE = "speakers_corner_talks.yml" +def schedule_zoom_talk(talk) -> Tuple[string, string]: -def schedule_zoom_talk(talk) -> string: + # Generate a password for the meeting. This is required since + # otherwise the meeting room will be forced. Zoom limits the + # password length to max 10 characters. + zoom_password = secrets.token_urlsafe(10) # Form the talk registration body request_body = { "topic": "Speakers\' corner talk by %s"%(talk.get("name")), "type": 2, # Scheduled meeting - "start_time": talk.get("time"), #2020-03-31T17:00:00 + "start_time": talk["time"], "timezone": "UTC", - "duration": 60, # 90 minutes - "schedule_for": "vsf@virtualscienceforum.org", # Zoom user ID or Zoom email address - "agenda":talk.get("title"), - "password": "", + "duration": 60, + "schedule_for": SPEAKERS_CORNER_USER_ID, + "password": zoom_password, # Meeting settings "settings": { "host_video": True, - "participant_video": True, + "participant_video": False, "cn_meeting": False, # Host the meeting in China? "in_meeting": False, # Host the meeting in India? - "join_before_host": False, + "join_before_host": False, # This will be switched to True shortly + # before the meeting starts by the VSF bot. + # It will also be switched back to False + # after the meetings ends. "mute_upon_entry": True, - "watermark": False, # Add a watermark when screensharing? - "use_pmi": False, # Don't use the Personal Meeting ID, but generate one + "watermark": False, # Don't add a watermark when screensharing + "use_pmi": False, # Don't use Personal Meeting ID, but generate one "approval_type": 0, # Automatically approve + "close_registration" : True, # Close registration after event date + "waiting_room" : False, # No waiting room "audio": "both", "auto_recording": "cloud", "enforce_login": False, @@ -49,20 +57,28 @@ def schedule_zoom_talk(talk) -> string: params={"body":request_body} ) - return response.id + return response.id, zoom_password def parse_talks(talks) -> int: num_updated = 0 for talk in talks: - if( talk.get('zoom_link', "") == "" && talk.get('event_type') == "speakers_corner" ): - # Schedule the talk - meeting_id = schedule_zoom_talk(talk) - # Update the talk - talk.get("zoom_meeting_id") = meeting_id - num_updated += 1 - - # Add this talk to researchseminars.org - add_talk_to_speakerscorner(talk) + # If we are processing a speakers corner talk, and its meeting id has + # already been set, there's nothing left to do + if not (talk.get('zoom_meeting_id', "") == "") and + (talk.get('event_type') == "speakers_corner"): + continue + + # Schedule the talk + meeting_id, meeting_password = schedule_zoom_talk(talk) + # Update the talk + talk.get("zoom_meeting_id") = meeting_id + # Add this talk to researchseminars.org + add_talk_to_speakerscorner(talk) + + # TODO: Do something with the password + + + num_updated += 1 return num_updated From b82633679b14ffd2ec95b56df7a29ed489910c45 Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Thu, 8 Oct 2020 14:43:49 +0200 Subject: [PATCH 08/26] Update req's, rename function for clarity, attempt to add registration questions --- .github/workflows/requirements.txt | 4 ++- researchseminarsdotorg.py | 14 +++++----- schedulezoomtalks.py | 45 +++++++++++++++++++++++++----- 3 files changed, 48 insertions(+), 15 deletions(-) diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt index 8b13789..496d20d 100644 --- a/.github/workflows/requirements.txt +++ b/.github/workflows/requirements.txt @@ -1 +1,3 @@ - +PyGithub==1.53 +PyYAML==5.3 +requests==2.22.0 diff --git a/researchseminarsdotorg.py b/researchseminarsdotorg.py index 023ba23..1cefaf7 100644 --- a/researchseminarsdotorg.py +++ b/researchseminarsdotorg.py @@ -6,8 +6,8 @@ "is_conference": False, "topics": [""], # TODO: Get a list of topics "language": "en", - "institutions": ["Global"], - "timezone": "America/New_York", + "institutions": [""], + "timezone": "UTC", "homepage": "https://virtualscienceforum.org/speakerscorner.md", "visibility": 1, # 0=private, 1=unlisted, 2=public "access_control": 0, # 0=open, see schema for more @@ -68,7 +68,7 @@ def add_talk_to_series(series_id, payload, authorization): else: return "", r.status_code -def add_talk_to_speakerscorner(talk): +def publish_to_researchseminars(talk): # talk should be provided in yaml format api_token = os.getenv("RESEARCHSEMINARS_API_TOKEN") authorization = "vsf@virtualscienceforum.org %s" % api_token @@ -83,10 +83,10 @@ def add_talk_to_speakerscorner(talk): # Set up payload for talk creation talk_payload = {"title":talk.get('title'), "speaker":talk.get('author'), # TODO: will be 'speakerS' - "live_link":"zoom.us/%s"%talk.get('zoom_meeting_id'), - "online":1, - "start_time":talk.get("time"), - "timezone":"UTC" # TODO + "live_link":"zoom.us/%s"%talk["zoom_meeting_id"], + "online": True, + "start_time":talk["time"], + "timezone":"UTC" } # Make request to remote API diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index e678e3d..11df861 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -48,7 +48,39 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: "alternative_hosts": "", "registrants_email_notification": True, "contact_email": "vsf@virtualscienceforum.org", - } + }, + + "questions": [ + {"field_name": "First Name", "required": True}, + {"field_name": "Last Name", "required": True}, + {"field_name": "Email Address", "required": True}, + {"field_name": "Confirm Email Address", "required": True}, + {"field_name": "Organization", "required": True}, + ], + + "custom_questions": [ + { + "title": "May we contact you about future Virtual Science Forum events?", + "type": "single", # short or single + "answers": ["Yes", "No"], # only single + "required": true + }, + { + "title": "How did you hear about the Virtual Science Forum?", + "type": "single", # short or single + "answers": ["Email list", + "One of the organizers", + "A colleague (not an organizer)", + "Other"], + "required": true + }, + { + "title": "Please confirm you have read the participant instructions: \ + http://virtualscienceforum.org/#/attendeeguide*", + "type": "short", # short or single + "required": true + }, + ] } response = zoom_request( @@ -62,10 +94,9 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: def parse_talks(talks) -> int: num_updated = 0 for talk in talks: - # If we are processing a speakers corner talk, and its meeting id has - # already been set, there's nothing left to do - if not (talk.get('zoom_meeting_id', "") == "") and - (talk.get('event_type') == "speakers_corner"): + # If we are not processing a speakers corner talk, or if the + # zoom meeting id has already been set, there's nothing left to do + if "zoom_meeting_id" in talk or talk["event_type"] != "speakers_corner": continue # Schedule the talk @@ -73,11 +104,11 @@ def parse_talks(talks) -> int: # Update the talk talk.get("zoom_meeting_id") = meeting_id # Add this talk to researchseminars.org - add_talk_to_speakerscorner(talk) + publish_to_researchseminars(talk) # TODO: Do something with the password + # do_something_with_password() - num_updated += 1 return num_updated From 86ff429c87d8455634706c1c604b0c29a28880d6 Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Thu, 8 Oct 2020 14:45:20 +0200 Subject: [PATCH 09/26] Remove incorrect '-' --- .github/workflows/schedule_zoom_talks.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/schedule_zoom_talks.yml b/.github/workflows/schedule_zoom_talks.yml index 53e0ee8..628562c 100644 --- a/.github/workflows/schedule_zoom_talks.yml +++ b/.github/workflows/schedule_zoom_talks.yml @@ -8,7 +8,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - - uses: actions/checkout@v2 + uses: actions/checkout@v2 - name: Set up Python 3.8 uses: actions/setup-python@v2 From b5b7504bc9406918a567c969bf4be3c617b036bc Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Thu, 8 Oct 2020 19:43:22 +0200 Subject: [PATCH 10/26] More suggestions implemented --- researchseminarsdotorg.py | 4 +++- schedulezoomtalks.py | 50 +++++++++++++++++++-------------------- 2 files changed, 27 insertions(+), 27 deletions(-) diff --git a/researchseminarsdotorg.py b/researchseminarsdotorg.py index 1cefaf7..edfbb0a 100644 --- a/researchseminarsdotorg.py +++ b/researchseminarsdotorg.py @@ -74,7 +74,9 @@ def publish_to_researchseminars(talk): authorization = "vsf@virtualscienceforum.org %s" % api_token # Find speakers' corner series, and create it if it doesn't exist - if( !find_seminar_series("speakerscorner") ): + if not find_seminar_series("speakerscorner"): + print("[ResearchSeminars.org]: The speakerscorner seminar series "\ + "does not yet exist. Creating it now") create_seminar_series(SPEAKERS_CORNER_SEMINAR_SERIES) # TODO: Figure out if we need to edit the series; diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 11df861..97ceb81 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -2,19 +2,13 @@ import github import requests import secrets -from researchseminarsdotorg import * -from requests import get, post, put +import common +from researchseminarsdotorg import publish_to_researchseminars from io import StringIO from ruamel.yaml import YAML -from common import * def schedule_zoom_talk(talk) -> Tuple[string, string]: - # Generate a password for the meeting. This is required since - # otherwise the meeting room will be forced. Zoom limits the - # password length to max 10 characters. - zoom_password = secrets.token_urlsafe(10) - # Form the talk registration body request_body = { @@ -23,8 +17,12 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: "start_time": talk["time"], "timezone": "UTC", "duration": 60, - "schedule_for": SPEAKERS_CORNER_USER_ID, - "password": zoom_password, + "schedule_for": common.SPEAKERS_CORNER_USER_ID, + + # Generate a password for the meeting. This is required since + # otherwise the meeting room will be forced. Zoom limits the + # password length to max 10 characters. + "password": secrets.token_urlsafe(10), # Meeting settings "settings": { @@ -32,10 +30,10 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: "participant_video": False, "cn_meeting": False, # Host the meeting in China? "in_meeting": False, # Host the meeting in India? - "join_before_host": False, # This will be switched to True shortly - # before the meeting starts by the VSF bot. - # It will also be switched back to False - # after the meetings ends. + + # This will be switched to True shortly before the meeting starts + # by the VSF bot. It will also be switched back to False afterwards + "join_before_host": False, "mute_upon_entry": True, "watermark": False, # Don't add a watermark when screensharing "use_pmi": False, # Don't use Personal Meeting ID, but generate one @@ -85,13 +83,13 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: response = zoom_request( requests.post, - f"{ZOOM_API}users/{user_id}/meetings", + f"{common.ZOOM_API}users/{user_id}/meetings", params={"body":request_body} ) - return response.id, zoom_password + return response.id -def parse_talks(talks) -> int: +def schedule_talks(talks) -> int: num_updated = 0 for talk in talks: # If we are not processing a speakers corner talk, or if the @@ -103,6 +101,7 @@ def parse_talks(talks) -> int: meeting_id, meeting_password = schedule_zoom_talk(talk) # Update the talk talk.get("zoom_meeting_id") = meeting_id + # Add this talk to researchseminars.org publish_to_researchseminars(talk) @@ -121,22 +120,21 @@ def parse_talks(talks) -> int: # Read the talks file yaml = YAML() try: - talks_data = repo.get_contents(TALKS_FILE, ref="master") + talks_data = repo.get_contents(common.TALKS_FILE, ref="master") talks = yaml.load(StringIO(talks_data.decoded_content.decode())) except github.UnknownObjectException: talks_data = None talks = [] - # If there are talks to be parsed... - if len(talks) != 0: - # ... parse them and keep track of how many we updated - num_updated = parse_talks(talks) + # If we added Zoom links, we should update the file in the repo + if (num_updated := schedule_talks(talks) ): + serialized = StringIO() + yaml.dump(talks, serialized) - # If we added Zoom links, we should update the file in the repo - if num_updated != 0: - serialized = StringIO() - yaml.dump(talks, serialized) + print("I updated %d talks, here is the new yaml file"%num_updated) + print(yaml) + if False: repo.update_file( TALKS_FILE, f"Added Zoom link{1} for {0} scheduled speakers\'"\ "corner talk{1}".format(num_updated,'' if num_updated == 1 else 's'), From cfd0443fdb2a1be151d66f970632b5fe71cdc78a Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Fri, 9 Oct 2020 13:04:24 +0200 Subject: [PATCH 11/26] Fix link --- researchseminarsdotorg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/researchseminarsdotorg.py b/researchseminarsdotorg.py index edfbb0a..cbb6131 100644 --- a/researchseminarsdotorg.py +++ b/researchseminarsdotorg.py @@ -85,7 +85,7 @@ def publish_to_researchseminars(talk): # Set up payload for talk creation talk_payload = {"title":talk.get('title'), "speaker":talk.get('author'), # TODO: will be 'speakerS' - "live_link":"zoom.us/%s"%talk["zoom_meeting_id"], + "live_link":"https://virtualscienceforum-org.zoom.us/j/%s"%talk["zoom_meeting_id"], "online": True, "start_time":talk["time"], "timezone":"UTC" From 2434663b3de7ca3c85444450e5d67f2c60172590 Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Fri, 9 Oct 2020 13:04:35 +0200 Subject: [PATCH 12/26] Use PATCH for questions --- schedulezoomtalks.py | 108 ++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 47 deletions(-) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 97ceb81..3739c99 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -33,7 +33,7 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: # This will be switched to True shortly before the meeting starts # by the VSF bot. It will also be switched back to False afterwards - "join_before_host": False, + "join_before_host": False, "mute_upon_entry": True, "watermark": False, # Don't add a watermark when screensharing "use_pmi": False, # Don't use Personal Meeting ID, but generate one @@ -48,46 +48,67 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: "contact_email": "vsf@virtualscienceforum.org", }, - "questions": [ - {"field_name": "First Name", "required": True}, - {"field_name": "Last Name", "required": True}, - {"field_name": "Email Address", "required": True}, - {"field_name": "Confirm Email Address", "required": True}, - {"field_name": "Organization", "required": True}, - ], - - "custom_questions": [ - { - "title": "May we contact you about future Virtual Science Forum events?", - "type": "single", # short or single - "answers": ["Yes", "No"], # only single - "required": true - }, - { - "title": "How did you hear about the Virtual Science Forum?", - "type": "single", # short or single - "answers": ["Email list", - "One of the organizers", - "A colleague (not an organizer)", - "Other"], - "required": true - }, - { - "title": "Please confirm you have read the participant instructions: \ - http://virtualscienceforum.org/#/attendeeguide*", - "type": "short", # short or single - "required": true - }, - ] + + } + + # Update the registraion questions + try: + response = zoom_request( + requests.post, + f"{common.ZOOM_API}users/{user_id}/meetings", + params={"body":request_body} + ) + + patch_registration_questions(response.id) + + return response.id + except Exception as e: + print("Could not create meeting, error: ", e) + return None + +def patch_registration_questions(meeting_id) -> int: + + request_body = { + "questions": [ + {"field_name": "First Name", "required": True}, + {"field_name": "Last Name", "required": True}, + {"field_name": "Email Address", "required": True}, + {"field_name": "Confirm Email Address", "required": True}, + {"field_name": "Organization", "required": True}, + ], + + "custom_questions": [ + { + "title": "May we contact you about future Virtual Science Forum events?", + "type": "single", # short or single + "answers": ["Yes", "No"], # only single + "required": true + }, + { + "title": "How did you hear about the Virtual Science Forum?", + "type": "single", # short or single + "answers": ["Email list", + "One of the organizers", + "A colleague (not an organizer)", + "Other"], + "required": true + }, + { + "title": "Please confirm you have read the participant instructions: \ + http://virtualscienceforum.org/#/attendeeguide*", + "type": "short", # short or single + "required": true + }, + ] } response = zoom_request( - requests.post, - f"{common.ZOOM_API}users/{user_id}/meetings", + requests.patch, + f"{common.ZOOM_API}users/{user_id}/meetings/{meeting_id}/registrants/questions", params={"body":request_body} ) - return response.id + return response.status def schedule_talks(talks) -> int: num_updated = 0 @@ -97,18 +118,11 @@ def schedule_talks(talks) -> int: if "zoom_meeting_id" in talk or talk["event_type"] != "speakers_corner": continue - # Schedule the talk - meeting_id, meeting_password = schedule_zoom_talk(talk) - # Update the talk - talk.get("zoom_meeting_id") = meeting_id - - # Add this talk to researchseminars.org - publish_to_researchseminars(talk) - - # TODO: Do something with the password - # do_something_with_password() - - num_updated += 1 + if( meetind_id := schedule_zoom_talk(talk) ): + talk.get("zoom_meeting_id") = meeting_id + # Add this talk to researchseminars.org + publish_to_researchseminars(talk) + num_updated += 1 return num_updated From 1a2683ea2e26ca9bd136c6a554af0ad4459b7d69 Mon Sep 17 00:00:00 2001 From: Everard van Nieuwenburg Date: Fri, 9 Oct 2020 13:41:36 +0200 Subject: [PATCH 13/26] Update requirements.txt --- requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 8d5a09d..91160df 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ requests pytz python-dateutil -PyJWT \ No newline at end of file +PyJWT +PyGithub +PyYAML From 827364ae3ca4603a0b2077a63003a74eed7f4c0e Mon Sep 17 00:00:00 2001 From: Everard van Nieuwenburg Date: Fri, 9 Oct 2020 13:41:57 +0200 Subject: [PATCH 14/26] Delete redundant requirements.txt --- .github/workflows/requirements.txt | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .github/workflows/requirements.txt diff --git a/.github/workflows/requirements.txt b/.github/workflows/requirements.txt deleted file mode 100644 index 496d20d..0000000 --- a/.github/workflows/requirements.txt +++ /dev/null @@ -1,3 +0,0 @@ -PyGithub==1.53 -PyYAML==5.3 -requests==2.22.0 From b4255a711841bd8945f46fa235a27fd1c68e8859 Mon Sep 17 00:00:00 2001 From: Everard van Nieuwenburg Date: Fri, 9 Oct 2020 14:03:58 +0200 Subject: [PATCH 15/26] Switch to test branch --- schedulezoomtalks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 3739c99..73c7394 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -134,7 +134,7 @@ def schedule_talks(talks) -> int: # Read the talks file yaml = YAML() try: - talks_data = repo.get_contents(common.TALKS_FILE, ref="master") + talks_data = repo.get_contents(common.TALKS_FILE, ref="test_zoom_meeting_registering_workflow") talks = yaml.load(StringIO(talks_data.decoded_content.decode())) except github.UnknownObjectException: talks_data = None @@ -154,5 +154,5 @@ def schedule_talks(talks) -> int: "corner talk{1}".format(num_updated,'' if num_updated == 1 else 's'), serialized.getvalue(), sha=talks_data.sha, - branch='master' + branch='test_zoom_meeting_registering_workflow' ) From f84c12015da225b20844e0f38d2983d6bfee6bb2 Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Fri, 9 Oct 2020 14:23:32 +0200 Subject: [PATCH 16/26] Speaker registration --- schedulezoomtalks.py | 85 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 71 insertions(+), 14 deletions(-) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 73c7394..981a558 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -7,7 +7,7 @@ from io import StringIO from ruamel.yaml import YAML -def schedule_zoom_talk(talk) -> Tuple[string, string]: +def schedule_zoom_talk(talk) -> string: # Form the talk registration body request_body = @@ -44,28 +44,67 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: "auto_recording": "cloud", "enforce_login": False, "alternative_hosts": "", - "registrants_email_notification": True, + + # Email notifications are turned off when created, so that we can + # register the speaker without them receiving an invitation to + # their own talk. They will receive a separate email with info. + # This will be turned on with a PATCH once the speaker is registered. + "registrants_email_notification": False, "contact_email": "vsf@virtualscienceforum.org", }, } - # Update the registraion questions - try: - response = zoom_request( - requests.post, - f"{common.ZOOM_API}users/{user_id}/meetings", - params={"body":request_body} - ) - - patch_registration_questions(response.id) + # Create the meeting + response = zoom_request( + requests.post, + f"{common.ZOOM_API}users/{user_id}/meetings", + params={"body":request_body} + ) - return response.id - except Exception as e: - print("Could not create meeting, error: ", e) + if( response.status != 201 ): return None + # Extract meeting id + meeting_id = response.id + # Register the speaker + register_speaker(meeting_id) + # Update the meeting registration questions + patch_registration_questions(meeting_id, talk) + # Update the registrants email notification + patch_registration_notification(meeting_id) + + return meeting_id + +def register_speaker(meeting_id, talk) -> int: + + request_payload = { + "email": talk["email"], + "first_name": "talk["speaker_name"], + } + + # Send request + response = zoom_request( + requests.post, + f"{common.ZOOM_API}users/{user_id}/meetings/{meeting_id}/registrants", + params={"body":request_body} + ) + + # 201: Registration created + # 300: Meeting {meetingId} is not found or has expired. + # 400: + # Error Code: 1010 + # User does not belong to this account: {accountId}. + # Error Code: 3003 + # You are not the meeting host. + # Error Code: 3000 + # Cannot access meeting info. + # 404: Meeting not found. + # Error Code: 1001 + # Meeting host does not exist: {userId}. + return response.status + def patch_registration_questions(meeting_id) -> int: request_body = { @@ -110,6 +149,24 @@ def patch_registration_questions(meeting_id) -> int: return response.status +def patch_registration_notification(meeting_id) -> int: + + # Form the talk registration body + request_body = { + "settings": { + "registrants_email_notification": True, + }, + } + + # Create the meeting + response = zoom_request( + requests.patch, + f"{common.ZOOM_API}users/{user_id}/meetings/{meeting_id}", + params={"body":request_body} + ) + + return response.status + def schedule_talks(talks) -> int: num_updated = 0 for talk in talks: From 313dfb60c36beddc604725b1a0a29e4392b0be5a Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Fri, 9 Oct 2020 14:25:03 +0200 Subject: [PATCH 17/26] Remove live link --- researchseminarsdotorg.py | 1 - 1 file changed, 1 deletion(-) diff --git a/researchseminarsdotorg.py b/researchseminarsdotorg.py index cbb6131..1e43546 100644 --- a/researchseminarsdotorg.py +++ b/researchseminarsdotorg.py @@ -85,7 +85,6 @@ def publish_to_researchseminars(talk): # Set up payload for talk creation talk_payload = {"title":talk.get('title'), "speaker":talk.get('author'), # TODO: will be 'speakerS' - "live_link":"https://virtualscienceforum-org.zoom.us/j/%s"%talk["zoom_meeting_id"], "online": True, "start_time":talk["time"], "timezone":"UTC" From 0723abd825bcdb5461a42293d67712f287063fab Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Sat, 10 Oct 2020 17:51:40 +0200 Subject: [PATCH 18/26] Issue response --- schedulezoomtalks.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 981a558..1c89523 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -7,6 +7,11 @@ from io import StringIO from ruamel.yaml import YAML +ISSUE_RESPONSE_TEMPLATE = jinja2.Template( +"""Hi again! I've now created a Zoom meeting for your talk, with meeting ID + {{ meeting_id }}. You'll receive a separate email with a host key. +""") + def schedule_zoom_talk(talk) -> string: # Form the talk registration body @@ -167,7 +172,13 @@ def patch_registration_notification(meeting_id) -> int: return response.status -def schedule_talks(talks) -> int: +def respond_to_issue_about_zoom_id(repo, talk): + issue_comment = ISSUE_RESPONSE_TEMPLATE.render(meeting_id=meeting_id) + issue = repo.get_issue(number=talk["workflow_issue"]) + issue.create_comment(issue_comment) + return issue_comment + +def schedule_talks(repo, talks) -> int: num_updated = 0 for talk in talks: # If we are not processing a speakers corner talk, or if the @@ -179,6 +190,9 @@ def schedule_talks(talks) -> int: talk.get("zoom_meeting_id") = meeting_id # Add this talk to researchseminars.org publish_to_researchseminars(talk) + # Create comment in issue + respond_to_issue_about_zoom_id(repo, talk) + num_updated += 1 return num_updated @@ -198,7 +212,7 @@ def schedule_talks(talks) -> int: talks = [] # If we added Zoom links, we should update the file in the repo - if (num_updated := schedule_talks(talks) ): + if (num_updated := schedule_talks(repo, talks) ): serialized = StringIO() yaml.dump(talks, serialized) From e39fc28d807cd9f409c4cf37f4c750452a2c4b12 Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Sat, 10 Oct 2020 18:03:08 +0200 Subject: [PATCH 19/26] Tweaks --- schedulezoomtalks.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 1c89523..f698616 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -172,11 +172,15 @@ def patch_registration_notification(meeting_id) -> int: return response.status -def respond_to_issue_about_zoom_id(repo, talk): +def notify_issue_about_zoom_meeting(repo, talk): issue_comment = ISSUE_RESPONSE_TEMPLATE.render(meeting_id=meeting_id) - issue = repo.get_issue(number=talk["workflow_issue"]) - issue.create_comment(issue_comment) - return issue_comment + + try: + issue = repo.get_issue(number=talk["workflow_issue"]) + issue.create_comment(issue_comment) + except: + print("Couldn't create issue comment. The content would have been: ") + print(issue_comment) def schedule_talks(repo, talks) -> int: num_updated = 0 @@ -191,7 +195,7 @@ def schedule_talks(repo, talks) -> int: # Add this talk to researchseminars.org publish_to_researchseminars(talk) # Create comment in issue - respond_to_issue_about_zoom_id(repo, talk) + notify_issue_about_zoom_meeting(repo, talk) num_updated += 1 @@ -216,14 +220,10 @@ def schedule_talks(repo, talks) -> int: serialized = StringIO() yaml.dump(talks, serialized) - print("I updated %d talks, here is the new yaml file"%num_updated) - print(yaml) - - if False: - repo.update_file( - TALKS_FILE, f"Added Zoom link{1} for {0} scheduled speakers\'"\ - "corner talk{1}".format(num_updated,'' if num_updated == 1 else 's'), - serialized.getvalue(), - sha=talks_data.sha, - branch='test_zoom_meeting_registering_workflow' - ) + repo.update_file( + TALKS_FILE, f"Added Zoom link{1} for {0} scheduled speakers\'"\ + "corner talk{1}".format(num_updated,'' if num_updated == 1 else 's'), + serialized.getvalue(), + sha=talks_data.sha, + branch='test_zoom_meeting_registering_workflow' + ) From ee6c61d0520cc8a27bcc2a236ed2981e696e69fa Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Sat, 10 Oct 2020 18:09:42 +0200 Subject: [PATCH 20/26] Email WIP --- schedulezoomtalks.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index f698616..790c8c7 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -4,6 +4,7 @@ import secrets import common from researchseminarsdotorg import publish_to_researchseminars +from host_key_rotation import host_key from io import StringIO from ruamel.yaml import YAML @@ -182,6 +183,12 @@ def notify_issue_about_zoom_meeting(repo, talk): print("Couldn't create issue comment. The content would have been: ") print(issue_comment) +def notify_author(talk): + # Get the host key + meeting_host_key = host_key(talk["time"] + + # TODO: Email body, send email + def schedule_talks(repo, talks) -> int: num_updated = 0 for talk in talks: @@ -196,6 +203,9 @@ def schedule_talks(repo, talks) -> int: publish_to_researchseminars(talk) # Create comment in issue notify_issue_about_zoom_meeting(repo, talk) + # Email the author + notify_author(talk) + num_updated += 1 From ee9e924d52291e63e003141a60a56413298f37dc Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Sat, 10 Oct 2020 18:28:12 +0200 Subject: [PATCH 21/26] Email v1 --- common.py | 17 ++++++++++++++++- schedulezoomtalks.py | 44 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/common.py b/common.py index 296213a..c0a4546 100644 --- a/common.py +++ b/common.py @@ -9,6 +9,9 @@ SPEAKERS_CORNER_USER_ID = "D0n5UNEHQiajWtgdWLlNSA" TALKS_FILE = "speakers_corner_talks.yml" +MAILGUN_BASE_URL = "https://api.eu.mailgun.net/v3/" +MAILGUN_DOMAIN = "mail.virtualscienceforum.org/" + @lru_cache() def zoom_headers(duration: int=100) -> dict: zoom_api_key = os.getenv("ZOOM_API_KEY") @@ -32,7 +35,6 @@ def zoom_request(method: callable, *args, **kwargs): if response.content: return response.json() - def speakers_corner_user_id() -> str: users = zoom_request(requests.get, ZOOM_API + "users")["users"] sc_user_id = next( @@ -72,3 +74,16 @@ def all_meetings(user_id) -> list: meeting["live"] = True return meetings + + +def decode(response): + if response.status_code != 200: + raise RuntimeError(response.content.decode()) + return json.loads(response.content.decode()) + +def api_query(method, endpoint, **params): + return decode(method( + MAILGUN_BASE_URL + endpoint, + auth=("api", os.getenv("MAILGUN_API_KEY")), + **params + )) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 790c8c7..36f2ce0 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -13,6 +13,25 @@ {{ meeting_id }}. You'll receive a separate email with a host key. """) +EMAIL_TEMPLATE = jinja2.Template( +"""Hi {{ author }}, + + A Zoom meeting has now been scheduled for your Speakers' Corner talk. + A few minutes before your timeslot starts, you and your audience will be + able to join the meeting. You will then be able to claim the host role by + using the host key below. After an hour the meeting will automatically + be terminated, and you will recieve more information about the recording + afterwards. + + Your meeting information: + Zoom link: {{ meeting_zoom_link }} + Host key: {{ meeting_host_key }} + Talk title: {{ meeting_talk_title }} + + Thank you in advance for contributing to the Speakers' Corner! + - The VSF team + """) + def schedule_zoom_talk(talk) -> string: # Form the talk registration body @@ -183,11 +202,29 @@ def notify_issue_about_zoom_meeting(repo, talk): print("Couldn't create issue comment. The content would have been: ") print(issue_comment) -def notify_author(talk): +def notify_author(talk) -> string: # Get the host key - meeting_host_key = host_key(talk["time"] + meeting_host_key = host_key(talk["time"]) + + # Format the email body + email_text = EMAIL_TEMPLATE.render(author=talk["speaker_name"], + meeting_zoom_link="", + meeting_host_key=meeting_host_key, + meeting_talk_title=talk["title"]) + + data = { + "from": "Speakers' Corner ", + "to": "{0} <{1}>".format(talk["speaker_name"], talk["email"]), + "subject": "Speakers' Corner talk", + "text": email_text, + "html": email_text, + } - # TODO: Email body, send email + return api_query( + post, + f"{common.MAILGUN_DOMAIN}messages", + data=data + ) def schedule_talks(repo, talks) -> int: num_updated = 0 @@ -206,7 +243,6 @@ def schedule_talks(repo, talks) -> int: # Email the author notify_author(talk) - num_updated += 1 return num_updated From 1ae7003bd8b147581134ab9346589bf0939ec4c8 Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Sat, 10 Oct 2020 23:20:17 +0200 Subject: [PATCH 22/26] Add date and time to email --- schedulezoomtalks.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 36f2ce0..19426ea 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -17,16 +17,19 @@ """Hi {{ author }}, A Zoom meeting has now been scheduled for your Speakers' Corner talk. - A few minutes before your timeslot starts, you and your audience will be + Five minutes before your timeslot starts, you and your audience will be able to join the meeting. You will then be able to claim the host role by using the host key below. After an hour the meeting will automatically - be terminated, and you will recieve more information about the recording - afterwards. + be terminated. Once the recording finishes processing, you will get the + opportunity to cut out parts of it. Your meeting information: + Talk title: {{ meeting_talk_title }} + Date: {{ meeting_date }} + Time slot: {{ meeting_start }} - {{ meeting_end }} + Zoom link: {{ meeting_zoom_link }} Host key: {{ meeting_host_key }} - Talk title: {{ meeting_talk_title }} Thank you in advance for contributing to the Speakers' Corner! - The VSF team @@ -77,8 +80,6 @@ def schedule_zoom_talk(talk) -> string: "registrants_email_notification": False, "contact_email": "vsf@virtualscienceforum.org", }, - - } # Create the meeting @@ -116,18 +117,6 @@ def register_speaker(meeting_id, talk) -> int: params={"body":request_body} ) - # 201: Registration created - # 300: Meeting {meetingId} is not found or has expired. - # 400: - # Error Code: 1010 - # User does not belong to this account: {accountId}. - # Error Code: 3003 - # You are not the meeting host. - # Error Code: 3000 - # Cannot access meeting info. - # 404: Meeting not found. - # Error Code: 1001 - # Meeting host does not exist: {userId}. return response.status def patch_registration_questions(meeting_id) -> int: @@ -207,10 +196,15 @@ def notify_author(talk) -> string: meeting_host_key = host_key(talk["time"]) # Format the email body + meeting_start = datetime.datetime(talk["time"], tz=pytz.UTC) + meeting_end = meeting_start + datetime.timedelta(hours=1) email_text = EMAIL_TEMPLATE.render(author=talk["speaker_name"], meeting_zoom_link="", meeting_host_key=meeting_host_key, - meeting_talk_title=talk["title"]) + meeting_talk_title=talk["title"], + meeting_date=meeting_start.date, + meeting_start=meeting_start.time, + meeting_end=meeting_end.time) data = { "from": "Speakers' Corner ", From de39fb80ebffb998c82d8444e448037cc2100702 Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Sun, 11 Oct 2020 11:51:03 +0200 Subject: [PATCH 23/26] Add zoom link --- schedulezoomtalks.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 19426ea..3833474 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -35,7 +35,7 @@ - The VSF team """) -def schedule_zoom_talk(talk) -> string: +def schedule_zoom_talk(talk) -> Tuple[string, string]: # Form the talk registration body request_body = @@ -101,7 +101,7 @@ def schedule_zoom_talk(talk) -> string: # Update the registrants email notification patch_registration_notification(meeting_id) - return meeting_id + return meeting_id, response.join_url def register_speaker(meeting_id, talk) -> int: @@ -191,7 +191,7 @@ def notify_issue_about_zoom_meeting(repo, talk): print("Couldn't create issue comment. The content would have been: ") print(issue_comment) -def notify_author(talk) -> string: +def notify_author(talk, join_url) -> string: # Get the host key meeting_host_key = host_key(talk["time"]) @@ -199,7 +199,7 @@ def notify_author(talk) -> string: meeting_start = datetime.datetime(talk["time"], tz=pytz.UTC) meeting_end = meeting_start + datetime.timedelta(hours=1) email_text = EMAIL_TEMPLATE.render(author=talk["speaker_name"], - meeting_zoom_link="", + meeting_zoom_link=join_url, meeting_host_key=meeting_host_key, meeting_talk_title=talk["title"], meeting_date=meeting_start.date, @@ -228,14 +228,15 @@ def schedule_talks(repo, talks) -> int: if "zoom_meeting_id" in talk or talk["event_type"] != "speakers_corner": continue - if( meetind_id := schedule_zoom_talk(talk) ): + meeting_id, join_url = schedule_zoom_talk(talk) + if( meetind_id ): talk.get("zoom_meeting_id") = meeting_id # Add this talk to researchseminars.org publish_to_researchseminars(talk) # Create comment in issue notify_issue_about_zoom_meeting(repo, talk) # Email the author - notify_author(talk) + notify_author(talk, join_url) num_updated += 1 From 49129d69888d5f1479e434d66ad31a270af8e5ff Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Sun, 11 Oct 2020 11:53:49 +0200 Subject: [PATCH 24/26] Add mailgun key --- .github/workflows/schedule_zoom_talks.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/schedule_zoom_talks.yml b/.github/workflows/schedule_zoom_talks.yml index 628562c..22ec87c 100644 --- a/.github/workflows/schedule_zoom_talks.yml +++ b/.github/workflows/schedule_zoom_talks.yml @@ -27,3 +27,4 @@ jobs: VSF_BOT_TOKEN: ${{ secrets.VSF_BOT_TOKEN }} ZOOM_API_KEY: ${{ secrets.ZOOM_API_KEY }} ZOOM_API_SECRET: ${{ secrets.ZOOM_API_SECRET }} + MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }} From 0d7ac9ffa0496062d26f09d9a9d051cf64cf1203 Mon Sep 17 00:00:00 2001 From: Anton Akhmerov Date: Sun, 11 Oct 2020 13:57:14 +0200 Subject: [PATCH 25/26] bugfixes --- common.py | 4 +- researchseminarsdotorg.py | 7 ++- schedulezoomtalks.py | 117 +++++++++++++++++++------------------- 3 files changed, 67 insertions(+), 61 deletions(-) diff --git a/common.py b/common.py index c0a4546..9743451 100644 --- a/common.py +++ b/common.py @@ -1,6 +1,7 @@ from functools import lru_cache import os from time import time +import json import jwt import requests @@ -77,10 +78,11 @@ def all_meetings(user_id) -> list: def decode(response): - if response.status_code != 200: + if response.status_code > 299: # Not OK raise RuntimeError(response.content.decode()) return json.loads(response.content.decode()) + def api_query(method, endpoint, **params): return decode(method( MAILGUN_BASE_URL + endpoint, diff --git a/researchseminarsdotorg.py b/researchseminarsdotorg.py index 1e43546..b19fa84 100644 --- a/researchseminarsdotorg.py +++ b/researchseminarsdotorg.py @@ -19,14 +19,16 @@ "order": 0, "display": True}]} + def find_seminar_series(series_id): - url = "https://researchseminars.org/api/0/search/series?series_id=%s"%series_id + url = f"https://researchseminars.org/api/0/search/series?series_id={series_id}" r = get(url) if r.status_code == 200: J = r.json() results = J["properties"]["results"] return (len(results) != 0) + def create_seminar_series(payload, authorization): url = "https://researchseminars.org/api/0/save/series/" r = post(url, json=payload, headers={"authorization":authorization}) @@ -41,6 +43,7 @@ def create_seminar_series(payload, authorization): else: return False, "" + def edit_seminar_series(name, payload, authorization): url = "https://researchseminars.org/api/0/save/series/" r = post(url, json=payload, headers={"authorization":authorization}) @@ -55,6 +58,7 @@ def edit_seminar_series(name, payload, authorization): else: return False, "" + def add_talk_to_series(series_id, payload, authorization): url = "https://researchseminars.org/api/0/save/talk/" r = post(url, json=payload, headers={"authorization":authorization}) @@ -68,6 +72,7 @@ def add_talk_to_series(series_id, payload, authorization): else: return "", r.status_code + def publish_to_researchseminars(talk): # talk should be provided in yaml format api_token = os.getenv("RESEARCHSEMINARS_API_TOKEN") diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index 3833474..cc4d057 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -1,45 +1,52 @@ import os +import secrets +from io import StringIO +from typing import Tuple +import datetime + import github import requests -import secrets +from ruamel.yaml import YAML +import jinja2 +import pytz + import common from researchseminarsdotorg import publish_to_researchseminars from host_key_rotation import host_key -from io import StringIO -from ruamel.yaml import YAML + ISSUE_RESPONSE_TEMPLATE = jinja2.Template( -"""Hi again! I've now created a Zoom meeting for your talk, with meeting ID +"""I've now created a Zoom meeting for your talk, with meeting ID {{ meeting_id }}. You'll receive a separate email with a host key. """) EMAIL_TEMPLATE = jinja2.Template( """Hi {{ author }}, - A Zoom meeting has now been scheduled for your Speakers' Corner talk. - Five minutes before your timeslot starts, you and your audience will be - able to join the meeting. You will then be able to claim the host role by - using the host key below. After an hour the meeting will automatically - be terminated. Once the recording finishes processing, you will get the - opportunity to cut out parts of it. +A Zoom meeting has now been scheduled for your Speakers' Corner talk. +Five minutes before your timeslot starts, you and your audience will be +able to join the meeting. You will then be able to claim the host role by +using the host key below. After an hour the meeting will automatically +be terminated. Once the recording finishes processing, you will get the +opportunity to cut out parts of it. - Your meeting information: - Talk title: {{ meeting_talk_title }} - Date: {{ meeting_date }} - Time slot: {{ meeting_start }} - {{ meeting_end }} +Your meeting information: +Talk title: {{ meeting_talk_title }} +Date: {{ meeting_date }} +Time slot: {{ meeting_start }} - {{ meeting_end }} - Zoom link: {{ meeting_zoom_link }} - Host key: {{ meeting_host_key }} +Zoom link: {{ meeting_zoom_link }} +Host key: {{ meeting_host_key }} - Thank you in advance for contributing to the Speakers' Corner! - - The VSF team - """) +Thank you in advance for contributing to the Speakers' Corner! +- The VSF team +""") -def schedule_zoom_talk(talk) -> Tuple[string, string]: + +def schedule_zoom_talk(talk) -> Tuple[str, str]: # Form the talk registration body - request_body = - { + request_body = { "topic": "Speakers\' corner talk by %s"%(talk.get("name")), "type": 2, # Scheduled meeting "start_time": talk["time"], @@ -83,42 +90,37 @@ def schedule_zoom_talk(talk) -> Tuple[string, string]: } # Create the meeting - response = zoom_request( + response = common.zoom_request( requests.post, - f"{common.ZOOM_API}users/{user_id}/meetings", + f"{common.ZOOM_API}users/{common.SPEAKERS_CORNER_USER_ID}/meetings", params={"body":request_body} ) - if( response.status != 201 ): - return None + meeting_id = response["id"] - # Extract meeting id - meeting_id = response.id - # Register the speaker - register_speaker(meeting_id) - # Update the meeting registration questions - patch_registration_questions(meeting_id, talk) - # Update the registrants email notification + register_speaker(meeting_id, talk) + patch_registration_questions(meeting_id) patch_registration_notification(meeting_id) - return meeting_id, response.join_url + return meeting_id, response["registration_url"] -def register_speaker(meeting_id, talk) -> int: +def register_speaker(meeting_id, talk) -> int: request_payload = { "email": talk["email"], - "first_name": "talk["speaker_name"], + "first_name": talk["speaker_name"], } # Send request - response = zoom_request( + response = common.zoom_request( requests.post, - f"{common.ZOOM_API}users/{user_id}/meetings/{meeting_id}/registrants", - params={"body":request_body} + f"{common.ZOOM_API}users/{common.SPEAKERS_CORNER_USER_ID}/meetings/{meeting_id}/registrants", + params={"body": request_payload} ) return response.status + def patch_registration_questions(meeting_id) -> int: request_body = { @@ -135,7 +137,7 @@ def patch_registration_questions(meeting_id) -> int: "title": "May we contact you about future Virtual Science Forum events?", "type": "single", # short or single "answers": ["Yes", "No"], # only single - "required": true + "required": True }, { "title": "How did you hear about the Virtual Science Forum?", @@ -144,20 +146,20 @@ def patch_registration_questions(meeting_id) -> int: "One of the organizers", "A colleague (not an organizer)", "Other"], - "required": true + "required": True }, { "title": "Please confirm you have read the participant instructions: \ http://virtualscienceforum.org/#/attendeeguide*", "type": "short", # short or single - "required": true + "required": True }, ] } - response = zoom_request( + response = common.zoom_request( requests.patch, - f"{common.ZOOM_API}users/{user_id}/meetings/{meeting_id}/registrants/questions", + f"{common.ZOOM_API}users/{common.SPEAKERS_CORNER_USER_ID}/meetings/{meeting_id}/registrants/questions", params={"body":request_body} ) @@ -173,25 +175,22 @@ def patch_registration_notification(meeting_id) -> int: } # Create the meeting - response = zoom_request( + response = common.zoom_request( requests.patch, - f"{common.ZOOM_API}users/{user_id}/meetings/{meeting_id}", + f"{common.ZOOM_API}users/{common.SPEAKERS_CORNER_USER_ID}/meetings/{meeting_id}", params={"body":request_body} ) return response.status def notify_issue_about_zoom_meeting(repo, talk): - issue_comment = ISSUE_RESPONSE_TEMPLATE.render(meeting_id=meeting_id) + issue_comment = ISSUE_RESPONSE_TEMPLATE.render(meeting_id=talk["meeting_id"]) - try: - issue = repo.get_issue(number=talk["workflow_issue"]) - issue.create_comment(issue_comment) - except: - print("Couldn't create issue comment. The content would have been: ") - print(issue_comment) + issue = repo.get_issue(number=talk["workflow_issue"]) + issue.create_comment(issue_comment) -def notify_author(talk, join_url) -> string: + +def notify_author(talk, join_url) -> str: # Get the host key meeting_host_key = host_key(talk["time"]) @@ -214,8 +213,8 @@ def notify_author(talk, join_url) -> string: "html": email_text, } - return api_query( - post, + return common.api_query( + requests.post, f"{common.MAILGUN_DOMAIN}messages", data=data ) @@ -229,8 +228,8 @@ def schedule_talks(repo, talks) -> int: continue meeting_id, join_url = schedule_zoom_talk(talk) - if( meetind_id ): - talk.get("zoom_meeting_id") = meeting_id + if meeting_id: + talk["zoom_meeting_id"] = meeting_id # Add this talk to researchseminars.org publish_to_researchseminars(talk) # Create comment in issue @@ -262,7 +261,7 @@ def schedule_talks(repo, talks) -> int: yaml.dump(talks, serialized) repo.update_file( - TALKS_FILE, f"Added Zoom link{1} for {0} scheduled speakers\'"\ + common.TALKS_FILE, f"Added Zoom link{1} for {0} scheduled speakers\'"\ "corner talk{1}".format(num_updated,'' if num_updated == 1 else 's'), serialized.getvalue(), sha=talks_data.sha, From fec322659f907aa173e53b63a9332b41a3941982 Mon Sep 17 00:00:00 2001 From: Evert van Nieuwenburg Date: Sun, 11 Oct 2020 17:09:09 +0200 Subject: [PATCH 26/26] Turn off submission to researchseminars.org --- schedulezoomtalks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/schedulezoomtalks.py b/schedulezoomtalks.py index cc4d057..2cfcb84 100644 --- a/schedulezoomtalks.py +++ b/schedulezoomtalks.py @@ -231,7 +231,7 @@ def schedule_talks(repo, talks) -> int: if meeting_id: talk["zoom_meeting_id"] = meeting_id # Add this talk to researchseminars.org - publish_to_researchseminars(talk) + # publish_to_researchseminars(talk) # Create comment in issue notify_issue_about_zoom_meeting(repo, talk) # Email the author