Skip to content

Commit 81323e9

Browse files
Merge branch 'master' into slack-alert-on-failure
2 parents 6ca25ad + 67f0d9e commit 81323e9

11 files changed

Lines changed: 514 additions & 11 deletions

File tree

.github/workflows/dev.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: RescueGroups API Post
1+
name: Debug Post
22

33
on:
44
push:
@@ -17,6 +17,7 @@ jobs:
1717
uses: actions/setup-python@v5
1818
with:
1919
python-version: "3.12"
20+
cache: 'pip'
2021

2122
- name: Install dependencies
2223
run: pip install --break-system-packages -r requirements.txt
@@ -26,8 +27,8 @@ jobs:
2627
CUTEPETSBOSTON_RESCUEGROUPS_API_KEY: ${{ secrets.CUTEPETSBOSTON_RESCUEGROUPS_API_KEY }}
2728
INSTAGRAM_BUSINESS_ACCOUNT_ID: ${{ secrets.INSTAGRAM_BUSINESS_ACCOUNT_ID }}
2829
INSTAGRAM_PAGE_ACCESS_TOKEN: ${{ secrets.INSTAGRAM_PAGE_ACCESS_TOKEN }}
29-
BLUESKY_HANDLE: ${{ secrets.BLUESKY_HANDLE }}
30-
BLUESKY_PASSWORD: ${{ secrets.BLUESKY_PASSWORD }}
30+
BLUESKY_HANDLE: ${{ secrets.BLUESKY_TEST_HANDLE }}
31+
BLUESKY_PASSWORD: ${{ secrets.BLUESKY_TEST_PASSWORD }}
3132
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
3233
APP_ENV: dev
3334
run: |

.github/workflows/prod.yml

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
name: RescueGroups API Post
1+
name: Prod Account Post
22

33
on:
4-
push:
5-
# We are doing this for testing purposes, but when we start posting, we should remove this line
6-
branches:
7-
[master]
84
workflow_dispatch:
95
schedule:
106
# Every 4 hours
@@ -20,6 +16,7 @@ jobs:
2016
uses: actions/setup-python@v5
2117
with:
2218
python-version: "3.12"
19+
cache: 'pip'
2320

2421
- name: Install dependencies
2522
run: pip install --break-system-packages -r requirements.txt

.github/workflows/tests.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Tests
2+
3+
on:
4+
pull_request:
5+
push:
6+
branches: [master]
7+
workflow_dispatch:
8+
9+
jobs:
10+
pytest:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout repo
14+
uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.12"
20+
cache: pip
21+
22+
- name: Install dependencies
23+
run: pip install --break-system-packages -r requirements.txt
24+
25+
- name: Run pytest
26+
run: pytest

CNAME

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
cutepetsboston.com

adoption_sources/rescue_groups.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import os
1010
import re
1111
from typing import Iterator
12-
import json
1312

1413
import requests
1514

@@ -18,6 +17,10 @@
1817

1918
logger = logging.getLogger(__name__)
2019

20+
# Some rescues publish entries like "More Dogs Soon!" to point users at their
21+
# website; those should never be posted. Add new names here as we encounter them.
22+
PLACEHOLDER_NAMES: tuple[str, ...] = ("more dogs soon!",)
23+
2124

2225
class SourceRescueGroups(PetSource):
2326
"""
@@ -104,8 +107,12 @@ def fetch_pets(self) -> Iterator[AdoptablePet]:
104107

105108
for animal in data:
106109
pet = self._parse_animal(animal, orgs_by_id)
107-
if pet:
108-
yield pet
110+
if not pet:
111+
continue
112+
if self._is_placeholder_name(pet.name):
113+
logger.info(f"Skipping placeholder record: {pet.name!r}")
114+
continue
115+
yield pet
109116

110117
def _parse_animal(self, animal: dict, orgs_by_id: dict) -> AdoptablePet | None:
111118
"""Parse a single animal record from the API response."""
@@ -163,6 +170,9 @@ def _parse_animal(self, animal: dict, orgs_by_id: dict) -> AdoptablePet | None:
163170
logger.warning(f"Failed to parse animal {animal.get('id', 'unknown')}: {e}")
164171
return None
165172

173+
def _is_placeholder_name(self, name: str) -> bool:
174+
return name.lower() in PLACEHOLDER_NAMES
175+
166176
def _clean_name(self, name: str) -> str:
167177
"""
168178
Clean up pet name by removing promotional text.

docs/index.html

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>CutePetsBoston</title>
7+
<meta name="description" content="A Code for Boston project that posts adoptable pets from Boston-area shelters to social media." />
8+
<link rel="stylesheet" href="styles.css" />
9+
</head>
10+
<body>
11+
<svg width="0" height="0" style="position:absolute" aria-hidden="true" focusable="false">
12+
<defs>
13+
<symbol id="icon-mastodon" viewBox="0 0 24 24">
14+
<path d="M21.58 13.913c-.29 1.469-2.592 3.121-5.238 3.396-1.379.184-2.737.368-4.185.276-2.368-.092-4.236-.55-4.236-.55 0 .184.014.36.043.534.314 2.297 2.353 2.434 4.284 2.5 1.948.066 3.682-.477 3.682-.477l.08 1.687s-1.36.732-3.789.84c-1.337.072-2.999-.034-4.934-.534C2.99 20.5 2.27 16.125 2.159 11.688c-.033-1.319-.013-2.563-.013-3.604C2.146 3.55 5.224 2.22 5.224 2.22 6.871.479 9.514.367 12.05.348h.063c2.537.02 5.18.13 6.738.832 0 0 3.078 1.33 3.078 5.864 0 0 .04 3.345-.349 5.869M18.319 7.16c0-1.118-.296-2.005-.886-2.66-.61-.654-1.408-.99-2.4-.99-1.148 0-2.018.43-2.605 1.291l-.567.93-.567-.93c-.587-.861-1.457-1.291-2.605-1.291-.992 0-1.79.336-2.4.99-.59.655-.886 1.542-.886 2.66v5.466h2.182V7.323c0-1.118.476-1.682 1.43-1.682 1.052 0 1.578.661 1.578 1.972v2.856h2.169V7.613c0-1.31.526-1.972 1.578-1.972.954 0 1.43.564 1.43 1.682v4.303h2.182Z"/>
15+
</symbol>
16+
<symbol id="icon-bluesky" viewBox="0 0 24 24">
17+
<path d="M12 10.8c-1.087-2.114-4.046-6.053-6.798-7.995C2.566.944 1.561 1.266.902 1.565.139 1.908 0 3.08 0 3.768c0 .69.378 5.65.624 6.479.815 2.736 3.713 3.66 6.383 3.364.136-.02.275-.039.415-.056-.138.022-.276.04-.415.056-3.912.58-7.387 2.005-2.83 7.078 5.013 5.19 6.87-1.113 7.823-4.308.953 3.195 2.05 9.271 7.733 4.308 4.267-4.308 1.172-6.498-2.74-7.078a8.741 8.741 0 0 1-.415-.056c.14.017.279.036.415.056 2.67.297 5.568-.628 6.383-3.364.246-.828.624-5.79.624-6.478 0-.69-.139-1.861-.902-2.206-.659-.298-1.664-.62-4.3 1.24C16.046 4.748 13.087 8.687 12 10.8Z"/>
18+
</symbol>
19+
<symbol id="icon-instagram" viewBox="0 0 24 24">
20+
<path d="M12 0C8.74 0 8.333.015 7.053.072 5.775.132 4.905.333 4.14.63c-.789.306-1.459.717-2.126 1.384S.935 3.35.63 4.14C.333 4.905.131 5.775.072 7.053.012 8.333 0 8.74 0 12s.015 3.667.072 4.947c.06 1.277.261 2.148.558 2.913.306.788.717 1.459 1.384 2.126.667.666 1.336 1.079 2.126 1.384.766.296 1.636.499 2.913.558C8.333 23.988 8.74 24 12 24s3.667-.015 4.947-.072c1.277-.06 2.148-.262 2.913-.558.788-.306 1.459-.718 2.126-1.384.666-.667 1.079-1.335 1.384-2.126.296-.765.499-1.636.558-2.913.06-1.28.072-1.687.072-4.947s-.015-3.667-.072-4.947c-.06-1.277-.262-2.149-.558-2.913-.306-.789-.718-1.459-1.384-2.126C21.319 1.347 20.651.935 19.86.63c-.765-.297-1.636-.499-2.913-.558C15.667.012 15.26 0 12 0zm0 2.16c3.203 0 3.585.016 4.85.071 1.17.055 1.805.249 2.227.415.562.217.96.477 1.382.896.419.42.679.819.896 1.381.164.422.36 1.057.413 2.227.057 1.266.07 1.646.07 4.85s-.015 3.585-.074 4.85c-.061 1.17-.256 1.805-.421 2.227-.224.562-.479.96-.897 1.382-.419.419-.824.679-1.38.896-.42.164-1.065.36-2.235.413-1.274.057-1.649.07-4.859.07-3.211 0-3.586-.015-4.859-.074-1.171-.061-1.816-.256-2.236-.421-.569-.224-.96-.479-1.379-.897-.421-.419-.69-.824-.9-1.38-.165-.42-.359-1.065-.42-2.235-.045-1.26-.061-1.649-.061-4.844 0-3.196.016-3.586.061-4.861.061-1.17.255-1.814.42-2.234.21-.57.479-.96.9-1.381.419-.419.81-.689 1.379-.898.42-.166 1.051-.361 2.221-.421 1.275-.045 1.65-.06 4.859-.06zm0 3.678c-3.405 0-6.162 2.76-6.162 6.162 0 3.405 2.76 6.162 6.162 6.162 3.405 0 6.162-2.76 6.162-6.162 0-3.405-2.76-6.162-6.162-6.162zM12 16c-2.21 0-4-1.79-4-4s1.79-4 4-4 4 1.79 4 4-1.79 4-4 4zm7.846-10.405c0 .795-.646 1.44-1.44 1.44-.795 0-1.44-.646-1.44-1.44 0-.794.646-1.439 1.44-1.439.793-.001 1.44.645 1.44 1.439z"/>
21+
</symbol>
22+
</defs>
23+
</svg>
24+
25+
<header class="hero">
26+
<div class="container">
27+
<h1>CutePetsBoston</h1>
28+
<p class="headline">Finding forever homes, one cute pet at a time.</p>
29+
<p class="subheader">CutePetsBoston is a volunteer-run social media bot that shares<br />adoptable pets daily from Boston-area shelters.</p>
30+
<div class="follow">
31+
<span class="follow-label">Follow us:</span>
32+
<nav class="social-icons" aria-label="Follow CutePetsBoston">
33+
<a class="social-icon" href="https://bsky.app/profile/cutepetsboston.bsky.social" target="_blank" rel="noopener" aria-label="Bluesky">
34+
<svg aria-hidden="true" focusable="false"><use href="#icon-bluesky"/></svg>
35+
</a>
36+
<a class="social-icon" href="https://mastodon.social/@cutepetsboston" target="_blank" rel="me noopener" aria-label="Mastodon">
37+
<svg aria-hidden="true" focusable="false"><use href="#icon-mastodon"/></svg>
38+
</a>
39+
<a class="social-icon" href="https://www.instagram.com/cute.pets.boston/" target="_blank" rel="noopener" aria-label="Instagram">
40+
<svg aria-hidden="true" focusable="false"><use href="#icon-instagram"/></svg>
41+
</a>
42+
</nav>
43+
</div>
44+
</div>
45+
</header>
46+
47+
<main class="container">
48+
<section id="shelters">
49+
<h2>Featured shelters</h2>
50+
<p>
51+
Shelters we've confirmed featuring on the feed. Our broader pet pool comes
52+
from <a href="https://rescuegroups.org/" target="_blank" rel="noopener">RescueGroups.org</a>
53+
within 50 miles of Boston, so plenty of other rescues appear too.
54+
</p>
55+
<div id="shelter-list" class="card-grid">
56+
<p class="dashboard-note">Loading shelters&hellip;</p>
57+
</div>
58+
</section>
59+
60+
<section id="recent">
61+
<h2>Recent posts</h2>
62+
<p>Latest pets from our <a href="https://mastodon.social/@cutepetsboston" target="_blank" rel="noopener">Mastodon</a> feed:</p>
63+
<div id="post-grid" class="post-grid">
64+
<p class="dashboard-note">Loading recent posts&hellip;</p>
65+
</div>
66+
</section>
67+
68+
<section id="contribute">
69+
<h2>How to contribute</h2>
70+
<p>
71+
CutePetsBoston is a <a href="https://www.codeforboston.org/" target="_blank" rel="noopener">Code for Boston</a>
72+
project. Come hack on it with us.
73+
</p>
74+
<div class="action-links">
75+
<a class="btn btn-primary" href="https://github.com/codeforboston/CutePetsBoston/fork" target="_blank" rel="noopener">Fork on GitHub</a>
76+
<a class="btn" href="https://github.com/codeforboston/CutePetsBoston" target="_blank" rel="noopener">View repo</a>
77+
<a class="btn" href="https://www.meetup.com/code-for-boston/" target="_blank" rel="noopener">Code for Boston Meetup</a>
78+
<a class="btn" href="https://cfb-public.slack.com/archives/C0A742DF1SP" target="_blank" rel="noopener">#cute-pets-boston on Slack</a>
79+
</div>
80+
</section>
81+
</main>
82+
83+
<footer>
84+
Made with <span aria-label="love">&hearts;</span> by
85+
<a href="https://www.codeforboston.org/" target="_blank" rel="noopener">Code for Boston</a>
86+
volunteers.
87+
</footer>
88+
89+
<script>
90+
// Escape HTML special chars before interpolating values from JSON or the
91+
// Mastodon feed into the template literals below. JS has no built-in for
92+
// this, so we map each unsafe char to its HTML entity.
93+
const escapeHtml = (s) =>
94+
String(s).replace(/[&<>"']/g, (c) => ({
95+
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;",
96+
}[c]));
97+
98+
fetch("shelters.json")
99+
.then((r) => r.json())
100+
.then(({ shelters }) => {
101+
const list = document.getElementById("shelter-list");
102+
list.innerHTML = shelters
103+
.map(
104+
(s) => `
105+
<a class="card" href="${escapeHtml(s.url)}" target="_blank" rel="noopener">
106+
<h3>${escapeHtml(s.name)}</h3>
107+
<div class="meta">${escapeHtml(s.location)}</div>
108+
<span>Visit adoptions &rarr;</span>
109+
</a>`
110+
)
111+
.join("");
112+
})
113+
.catch(() => {
114+
document.getElementById("shelter-list").innerHTML =
115+
'<p class="dashboard-note">Shelter list unavailable.</p>';
116+
});
117+
118+
const MEDIA_NS = "http://search.yahoo.com/mrss/";
119+
const POST_LIMIT = 6;
120+
121+
fetch("https://mastodon.social/@cutepetsboston.rss")
122+
.then((r) => {
123+
if (!r.ok) throw new Error("RSS request failed: " + r.status);
124+
return r.text();
125+
})
126+
.then((xml) => {
127+
const doc = new DOMParser().parseFromString(xml, "application/xml");
128+
if (doc.querySelector("parsererror")) {
129+
throw new Error("RSS parse error");
130+
}
131+
const items = Array.from(doc.querySelectorAll("item")).slice(0, POST_LIMIT);
132+
const grid = document.getElementById("post-grid");
133+
if (items.length === 0) {
134+
grid.innerHTML = '<p class="dashboard-note">No recent posts found.</p>';
135+
return;
136+
}
137+
grid.innerHTML = items
138+
.map((item) => {
139+
const link = item.querySelector("link")?.textContent || "#";
140+
const pubDate = item.querySelector("pubDate")?.textContent || "";
141+
const dateStr = pubDate
142+
? new Date(pubDate).toLocaleDateString(undefined, {
143+
month: "short", day: "numeric",
144+
})
145+
: "";
146+
const descHtml = item.querySelector("description")?.textContent || "";
147+
// Strip HTML to get plain-text caption
148+
const tmp = document.createElement("div");
149+
tmp.innerHTML = descHtml;
150+
const text = (tmp.textContent || "").trim();
151+
const caption = text.length > 140 ? text.slice(0, 137) + "…" : text;
152+
const mediaEls = item.getElementsByTagNameNS(MEDIA_NS, "content");
153+
let imgUrl = "";
154+
for (const el of mediaEls) {
155+
const medium = el.getAttribute("medium");
156+
if (!medium || medium === "image") {
157+
imgUrl = el.getAttribute("url") || "";
158+
if (imgUrl) break;
159+
}
160+
}
161+
const img = imgUrl
162+
? `<img class="post-image" src="${escapeHtml(imgUrl)}" alt="" loading="lazy" />`
163+
: '<div class="post-image post-image-placeholder"></div>';
164+
return `
165+
<a class="post-card" href="${escapeHtml(link)}" target="_blank" rel="noopener">
166+
${img}
167+
<div class="post-body">
168+
<div class="post-caption">${escapeHtml(caption) || "View post"}</div>
169+
<div class="meta">${escapeHtml(dateStr)}</div>
170+
</div>
171+
</a>`;
172+
})
173+
.join("");
174+
})
175+
.catch((err) => {
176+
document.getElementById("post-grid").innerHTML = `
177+
<p class="dashboard-note">
178+
Couldn't load recent posts.
179+
<a href="https://mastodon.social/@cutepetsboston" target="_blank" rel="noopener">View on Mastodon &rarr;</a>
180+
</p>`;
181+
console.error("Failed to load Mastodon feed:", err);
182+
});
183+
</script>
184+
</body>
185+
</html>

docs/shelters.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"shelters": [
3+
{
4+
"name": "MSPCA-Angell",
5+
"location": "Boston, MA",
6+
"url": "https://www.mspca.org/adoption/"
7+
},
8+
{
9+
"name": "Sterling Animal Shelter",
10+
"location": "Sterling, MA",
11+
"url": "https://www.sterlingshelter.org/"
12+
},
13+
{
14+
"name": "Small Dog Rescue of New England",
15+
"location": "Rhode Island",
16+
"url": "https://www.smalldogrescuene.org/"
17+
}
18+
]
19+
}

0 commit comments

Comments
 (0)