From 234f7f7cd789d1a238d51fc5e49d69efeda6d649 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:15:50 +0000 Subject: [PATCH 1/5] Initial plan From 456a58dcca4f38fea5aa74773b5475b9f1d7d5f4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:21:54 +0000 Subject: [PATCH 2/5] Implement Bluesky posting workflow with Google Calendar integration Co-authored-by: gvegayon <893619+gvegayon@users.noreply.github.com> --- .github/scripts/README.md | 68 ++++++ .../post_to_bluesky.cpython-312.pyc | Bin 0 -> 9496 bytes .github/scripts/post_to_bluesky.py | 210 ++++++++++++++++++ .github/workflows/bluesky-post.yml | 36 +++ 4 files changed, 314 insertions(+) create mode 100644 .github/scripts/README.md create mode 100644 .github/scripts/__pycache__/post_to_bluesky.cpython-312.pyc create mode 100755 .github/scripts/post_to_bluesky.py create mode 100644 .github/workflows/bluesky-post.yml diff --git a/.github/scripts/README.md b/.github/scripts/README.md new file mode 100644 index 0000000..9347a95 --- /dev/null +++ b/.github/scripts/README.md @@ -0,0 +1,68 @@ +# Bluesky Posting Workflow + +This directory contains the GitHub Actions workflow and script for automatically posting Utah Data Science Center events to Bluesky. + +## How it works + +1. **Schedule**: The workflow runs twice a week: + - Mondays at 9:00 AM MST + - Wednesdays at 8:00 AM MST + +2. **Data Source**: The script fetches the next upcoming event from the Utah Data Science Center Google Calendar (calendar ID: `ekol7ulqm14nv155angut2rlfo@group.calendar.google.com`) + +3. **Posting**: It formats the event information into a social media-friendly message and posts it to Bluesky using the AT Protocol + +## Setup Instructions + +### Required GitHub Secrets + +To enable Bluesky posting, add the following secrets to your GitHub repository: + +1. Go to your repository Settings → Secrets and variables → Actions +2. Add these repository secrets: + - `BLUESKY_USERNAME`: Your Bluesky handle (e.g., `username.bsky.social`) + - `BLUESKY_PASSWORD`: Your Bluesky app password (not your regular password!) + +### Creating a Bluesky App Password + +1. Log in to Bluesky +2. Go to Settings → App Passwords +3. Create a new app password for "GitHub Actions Bot" +4. Use this app password (not your regular password) as `BLUESKY_PASSWORD` + +### Optional Secrets + +- `GOOGLE_CALENDAR_API_KEY`: If you want to use a different Google Calendar API key (the script has a fallback to the existing key) + +## Manual Testing + +You can manually trigger the workflow by: +1. Going to the Actions tab in your repository +2. Selecting "Post to Bluesky" +3. Clicking "Run workflow" + +## Files + +- `bluesky-post.yml`: GitHub Actions workflow definition +- `post_to_bluesky.py`: Python script that handles calendar fetching and Bluesky posting +- `README.md`: This documentation file + +## Message Format + +The bot posts messages in this format: + +``` +šŸ”¬ Join us for our next Utah Data Science Center event! +šŸ“… [Event Title] +ā° [Day, Month Date at Time] +šŸ“ [Location] + +[Brief description...] + +šŸ”— More info: https://datascience.utah.edu/seminar +šŸ“… Calendar: [Google Calendar Link] + +#DataScience #Utah #AI #MachineLearning +``` + +If no upcoming events are found, it posts a generic message directing people to the website. \ No newline at end of file diff --git a/.github/scripts/__pycache__/post_to_bluesky.cpython-312.pyc b/.github/scripts/__pycache__/post_to_bluesky.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..384a45b1dcc994cf8428ca8d49643410e7fb0762 GIT binary patch literal 9496 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 100: + clean_desc = clean_desc[:100] + "..." + message_parts.append(f"\n\n{clean_desc}") + + message_parts.append(f"\n\nšŸ”— More info: https://datascience.utah.edu/seminar") + + if html_link: + message_parts.append(f"\nšŸ“… Calendar: {html_link}") + + message_parts.append("\n\n#DataScience #Utah #AI #MachineLearning") + + return "".join(message_parts) + +def main(): + """Main function to orchestrate the calendar fetch and Bluesky post""" + # Get environment variables + bluesky_username = os.getenv("BLUESKY_USERNAME") + bluesky_password = os.getenv("BLUESKY_PASSWORD") + google_api_key = os.getenv("GOOGLE_CALENDAR_API_KEY") + + # Check if credentials are provided + if not bluesky_username or not bluesky_password: + logger.warning("Bluesky credentials not provided. Skipping post.") + logger.info("This is expected until the Bluesky credentials are added to GitHub secrets.") + logger.info("To add credentials, set BLUESKY_USERNAME and BLUESKY_PASSWORD in GitHub repository secrets.") + return + + # Use the hardcoded API key if not provided as secret (fallback to existing key) + if not google_api_key: + google_api_key = "AIzaSyBq6TTVUkWGCs0vmGh1XlIuGn0w5dCtbsA" + logger.info("Using fallback Google Calendar API key") + + # Fetch the next event + calendar_fetcher = GoogleCalendarFetcher(google_api_key) + next_event = calendar_fetcher.get_next_event() + + # Format the message + message = format_event_message(next_event) + logger.info(f"Formatted message: {message}") + + # Post to Bluesky + poster = BlueSkyPoster(bluesky_username, bluesky_password) + + if poster.authenticate(): + success = poster.post_message(message) + if success: + logger.info("Successfully completed Bluesky post workflow") + else: + logger.error("Failed to post message to Bluesky") + sys.exit(1) + else: + logger.error("Failed to authenticate with Bluesky") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/bluesky-post.yml b/.github/workflows/bluesky-post.yml new file mode 100644 index 0000000..1f4b78a --- /dev/null +++ b/.github/workflows/bluesky-post.yml @@ -0,0 +1,36 @@ +name: Post to Bluesky + +on: + schedule: + # Monday at 9 AM MST (16:00 UTC) - MST is UTC-7 + - cron: '0 16 * * 1' + # Wednesday at 8 AM MST (15:00 UTC) - MST is UTC-7 + - cron: '0 15 * * 3' + workflow_dispatch: # Allow manual triggering for testing + +jobs: + post-to-bluesky: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install requests python-dateutil + + - name: Post to Bluesky + env: + BLUESKY_USERNAME: ${{ secrets.BLUESKY_USERNAME }} + BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} + GOOGLE_CALENDAR_API_KEY: ${{ secrets.GOOGLE_CALENDAR_API_KEY }} + run: | + python .github/scripts/post_to_bluesky.py + continue-on-error: true # Don't fail the workflow if Bluesky credentials aren't set up yet \ No newline at end of file From 13c3e1a56984a2dc42222d1d6c96a79e32d0d407 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Sep 2025 19:22:56 +0000 Subject: [PATCH 3/5] Update .gitignore to exclude Python cache files Co-authored-by: gvegayon <893619+gvegayon@users.noreply.github.com> --- .../__pycache__/post_to_bluesky.cpython-312.pyc | Bin 9496 -> 0 bytes .gitignore | 5 +++++ 2 files changed, 5 insertions(+) delete mode 100644 .github/scripts/__pycache__/post_to_bluesky.cpython-312.pyc diff --git a/.github/scripts/__pycache__/post_to_bluesky.cpython-312.pyc b/.github/scripts/__pycache__/post_to_bluesky.cpython-312.pyc deleted file mode 100644 index 384a45b1dcc994cf8428ca8d49643410e7fb0762..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9496 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 Date: Tue, 2 Sep 2025 19:47:09 +0000 Subject: [PATCH 4/5] Add dry-run mode for PRs/commits and on-demand workflow triggering Co-authored-by: gvegayon <893619+gvegayon@users.noreply.github.com> --- .github/scripts/post_to_bluesky.py | 22 ++++++++++++++++++++-- .github/workflows/bluesky-post.yml | 11 ++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.github/scripts/post_to_bluesky.py b/.github/scripts/post_to_bluesky.py index 71fdf84..e9469d9 100755 --- a/.github/scripts/post_to_bluesky.py +++ b/.github/scripts/post_to_bluesky.py @@ -8,6 +8,7 @@ import sys import json import requests +import argparse from datetime import datetime, timezone from dateutil import parser import logging @@ -167,13 +168,19 @@ def format_event_message(event): def main(): """Main function to orchestrate the calendar fetch and Bluesky post""" + # Parse command line arguments + parser = argparse.ArgumentParser(description="Post Utah Data Science Center events to Bluesky") + parser.add_argument("--dry-run", action="store_true", + help="Generate and display the message without posting to Bluesky") + args = parser.parse_args() + # Get environment variables bluesky_username = os.getenv("BLUESKY_USERNAME") bluesky_password = os.getenv("BLUESKY_PASSWORD") google_api_key = os.getenv("GOOGLE_CALENDAR_API_KEY") - # Check if credentials are provided - if not bluesky_username or not bluesky_password: + # In dry-run mode, we don't need Bluesky credentials + if not args.dry_run and (not bluesky_username or not bluesky_password): logger.warning("Bluesky credentials not provided. Skipping post.") logger.info("This is expected until the Bluesky credentials are added to GitHub secrets.") logger.info("To add credentials, set BLUESKY_USERNAME and BLUESKY_PASSWORD in GitHub repository secrets.") @@ -190,6 +197,17 @@ def main(): # Format the message message = format_event_message(next_event) + + if args.dry_run: + logger.info("DRY RUN MODE - Message would be posted to Bluesky:") + print("\n" + "="*60) + print("BLUESKY POST PREVIEW") + print("="*60) + print(message) + print("="*60) + logger.info("Dry run completed successfully") + return + logger.info(f"Formatted message: {message}") # Post to Bluesky diff --git a/.github/workflows/bluesky-post.yml b/.github/workflows/bluesky-post.yml index 1f4b78a..bb5e1c5 100644 --- a/.github/workflows/bluesky-post.yml +++ b/.github/workflows/bluesky-post.yml @@ -7,6 +7,8 @@ on: # Wednesday at 8 AM MST (15:00 UTC) - MST is UTC-7 - cron: '0 15 * * 3' workflow_dispatch: # Allow manual triggering for testing + pull_request: # Run in dry-mode for PRs + push: # Run in dry-mode for commits jobs: post-to-bluesky: @@ -32,5 +34,12 @@ jobs: BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }} GOOGLE_CALENDAR_API_KEY: ${{ secrets.GOOGLE_CALENDAR_API_KEY }} run: | - python .github/scripts/post_to_bluesky.py + # Determine if this is a dry run (for PRs and pushes) + if [[ "${{ github.event_name }}" == "pull_request" || "${{ github.event_name }}" == "push" ]]; then + echo "Running in dry-mode (PR/push event) - will only generate and display the message" + python .github/scripts/post_to_bluesky.py --dry-run + else + echo "Running in posting mode (scheduled/manual trigger) - will post to Bluesky" + python .github/scripts/post_to_bluesky.py + fi continue-on-error: true # Don't fail the workflow if Bluesky credentials aren't set up yet \ No newline at end of file From 8401a3f00ca4935a8f2e73b6acd7dcad6a8bb301 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 3 Sep 2025 00:40:55 +0000 Subject: [PATCH 5/5] Improve Bluesky message format and error handling Co-authored-by: gvegayon <893619+gvegayon@users.noreply.github.com> --- .github/scripts/post_to_bluesky.py | 151 ++++++++++++++++++++++++----- 1 file changed, 126 insertions(+), 25 deletions(-) diff --git a/.github/scripts/post_to_bluesky.py b/.github/scripts/post_to_bluesky.py index e9469d9..39f556f 100755 --- a/.github/scripts/post_to_bluesky.py +++ b/.github/scripts/post_to_bluesky.py @@ -44,8 +44,8 @@ def authenticate(self): logger.error(f"Failed to authenticate with Bluesky: {e}") return False - def post_message(self, message): - """Post a message to Bluesky""" + def post_message(self, message, facets=None): + """Post a message to Bluesky with optional richtext facets""" if not self.session: logger.error("Not authenticated. Call authenticate() first.") return False @@ -59,14 +59,20 @@ def post_message(self, message): # Create the post record now = datetime.now(timezone.utc).isoformat() + record = { + "$type": "app.bsky.feed.post", + "text": message, + "createdAt": now + } + + # Add facets if provided (for rich text links) + if facets: + record["facets"] = facets + post_data = { "repo": self.session["did"], "collection": "app.bsky.feed.post", - "record": { - "$type": "app.bsky.feed.post", - "text": message, - "createdAt": now - } + "record": record } response = requests.post(post_url, headers=headers, json=post_data) @@ -117,10 +123,32 @@ def get_next_event(self): logger.error(f"Failed to fetch calendar events: {e}") return None -def format_event_message(event): +def create_richtext_facets(text, link_text, url): + """Create Bluesky richtext facets for linking text to a URL""" + # Find the position of the link text in the message + start_pos = text.find(link_text) + if start_pos == -1: + return None + + end_pos = start_pos + len(link_text) + + facets = [{ + "index": { + "byteStart": len(text[:start_pos].encode('utf-8')), + "byteEnd": len(text[:end_pos].encode('utf-8')) + }, + "features": [{ + "$type": "app.bsky.richtext.facet#link", + "uri": url + }] + }] + + return facets + +def format_event_message(event, posting_day="unknown"): """Format the event data into a Bluesky post message""" if not event: - return "Join us for our next Utah Data Science Center event! Check our website for details: https://datascience.utah.edu/seminar" + return "" # Return empty string when no event found # Extract event details title = event.get("summary", "Utah Data Science Event") @@ -128,6 +156,24 @@ def format_event_message(event): location = event.get("location", "") html_link = event.get("htmlLink", "") + # Extract speaker from description or title + speaker = "" + if description: + # Try to extract speaker from common patterns + import re + speaker_patterns = [ + r"Speaker:?\s*([^\n\r]+)", + r"Presenter:?\s*([^\n\r]+)", + r"by\s+([^\n\r]+)", + ] + for pattern in speaker_patterns: + match = re.search(pattern, description, re.IGNORECASE) + if match: + speaker = match.group(1).strip() + # Clean up any trailing periods or extra whitespace + speaker = speaker.rstrip('.') + break + # Parse start time start_time = None if "start" in event: @@ -136,10 +182,26 @@ def format_event_message(event): elif "date" in event["start"]: start_time = parser.parse(event["start"]["date"]) + # Determine timing context based on posting day and event day + now = datetime.now() + timing_prefix = "šŸ”¬ Join us for our next Utah Data Science Center event!" + + if start_time: + event_day = start_time.strftime("%A") + if posting_day == "monday" and event_day == "Wednesday": + timing_prefix = f"šŸ”¬ Join us {event_day}, {start_time.strftime('%b %d')} for our next Utah Data Science Center event!" + elif posting_day == "wednesday" and start_time.date() == now.date(): + timing_prefix = "šŸ”¬ Join us today for our Utah Data Science Center event!" + elif start_time: + timing_prefix = f"šŸ”¬ Join us {event_day}, {start_time.strftime('%b %d')} for our next Utah Data Science Center event!" + # Format the message - message_parts = ["šŸ”¬ Join us for our next Utah Data Science Center event!"] + message_parts = [timing_prefix] + + message_parts.append(f"\n\nšŸ“… {title}") - message_parts.append(f"\nšŸ“… {title}") + if speaker: + message_parts.append(f"\nšŸ‘¤ Speaker: {speaker}") if start_time: formatted_time = start_time.strftime("%A, %B %d at %I:%M %p") @@ -152,17 +214,22 @@ def format_event_message(event): if description and description.strip(): # Remove markdown and HTML tags for a cleaner description clean_desc = description.replace("*", "").replace("**", "").replace("#", "") - # Take first sentence or first 100 characters - if len(clean_desc) > 100: - clean_desc = clean_desc[:100] + "..." - message_parts.append(f"\n\n{clean_desc}") - - message_parts.append(f"\n\nšŸ”— More info: https://datascience.utah.edu/seminar") + # Take first sentence or first 100 characters, avoiding speaker info + lines = clean_desc.split('\n') + desc_text = "" + for line in lines: + if not any(keyword in line.lower() for keyword in ['speaker:', 'presenter:', 'by ']): + desc_text = line.strip() + break + + if desc_text and len(desc_text) > 100: + desc_text = desc_text[:100] + "..." + if desc_text: + message_parts.append(f"\n\n{desc_text}") - if html_link: - message_parts.append(f"\nšŸ“… Calendar: {html_link}") + message_parts.append(f"\n\nMore details here") # This will be linked using richtext facets - message_parts.append("\n\n#DataScience #Utah #AI #MachineLearning") + message_parts.append("\n\n#datascience #ai #Utah #MachineLearning") return "".join(message_parts) @@ -191,12 +258,38 @@ def main(): google_api_key = "AIzaSyBq6TTVUkWGCs0vmGh1XlIuGn0w5dCtbsA" logger.info("Using fallback Google Calendar API key") - # Fetch the next event - calendar_fetcher = GoogleCalendarFetcher(google_api_key) - next_event = calendar_fetcher.get_next_event() + # Determine posting day context + current_day = datetime.now().strftime("%A").lower() + posting_day = "unknown" + if current_day == "monday": + posting_day = "monday" + elif current_day == "wednesday": + posting_day = "wednesday" + + # Fetch the next event with graceful error handling + try: + calendar_fetcher = GoogleCalendarFetcher(google_api_key) + next_event = calendar_fetcher.get_next_event() + except Exception as e: + logger.error(f"Failed to fetch calendar data (this may be due to network restrictions): {e}") + if args.dry_run: + logger.info("DRY RUN MODE - Would attempt to fetch calendar data") + logger.info("No event data available for preview due to network restrictions") + return + else: + logger.warning("Skipping post due to calendar fetch failure") + return # Format the message - message = format_event_message(next_event) + message = format_event_message(next_event, posting_day) + + # If no event found and message is empty, skip posting + if not message: + logger.info("No upcoming events found. Skipping post.") + return + + # Create richtext facets for the "here" link + facets = create_richtext_facets(message, "here", "https://datascience.utah.edu/seminar") if args.dry_run: logger.info("DRY RUN MODE - Message would be posted to Bluesky:") @@ -204,6 +297,14 @@ def main(): print("BLUESKY POST PREVIEW") print("="*60) print(message) + if facets: + print("\nRichtext Links:") + for facet in facets: + start = facet["index"]["byteStart"] + end = facet["index"]["byteEnd"] + link_text = message.encode('utf-8')[start:end].decode('utf-8') + url = facet["features"][0]["uri"] + print(f" '{link_text}' -> {url}") print("="*60) logger.info("Dry run completed successfully") return @@ -214,7 +315,7 @@ def main(): poster = BlueSkyPoster(bluesky_username, bluesky_password) if poster.authenticate(): - success = poster.post_message(message) + success = poster.post_message(message, facets) if success: logger.info("Successfully completed Bluesky post workflow") else: