9
10const stripe = new Stripe(Deno.env.get("stripe_sk_customer_readonly") as string, {
11 apiVersion: "2020-08-27",
12});
13
14and the `excludes` for what you don't want to get notified for.
15
16You can use [Twitter's search operators](https://developer.twitter.com/en/docs/twitter-api/v1/rules-and-filtering/search-operators) to customize your query, for some collection of keywords, filtering out others, and much more!
17
18## 2. Notification
22## Twitter Data & Limitations
23
24The Twitter API has become unusable. This val gets Twitter data via [SocialData](https://socialdata.tools),
25an affordable Twitter scraping API. In order to make this val easy for
26you to fork & use without signing up for another API, I am proxying
27SocialData via @stevekrouse/socialDataProxy. Val Town Pro users can call this proxy
28100 times per day, so be sure not to set this cron to run more than once every 15 min.
29
30If you want to run it more, get your own [SocialData](https://socialdata.tools)
31API token and pay for it directly.
1import { discordWebhook } from "https://esm.town/v/stevekrouse/discordWebhook";
2import { AtpAgent } from "npm:@atproto/api@0.13.15";
3import pMap from "npm:p-map";
4
6
7const agent = new AtpAgent({
8 service: "https://public.api.bsky.app/",
9 // fetch, ideally we'd use our @std/fetch proxy here but that doesn't work and I don't know why
10});
76}
77
78const endpointURL = "https://mjweaver01-sermongptapi.web.val.run";
79
80function App() {
642 <title>Sermon Generator</title>
643 <style>
644 @import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500&display=swap');
645 body {
646 font-family: 'Roboto', sans-serif;
63 const fetchStories = async () => {
64 try {
65 const response = await fetch("/api/stories");
66 if (!response.ok) throw new Error("Failed to fetch dates");
67 const data = await response.json();
77 try {
78 dispatch({ type: "loading", value: true });
79 const response = await fetch(`/api/comments?query=${encodeURIComponent(query)}&story=${story}&page=${page}`);
80 if (!response.ok) throw new Error("Failed to fetch comments");
81 const data = await response.json();
360export default async function(req: Request): Promise<Response> {
361 const url = new URL(req.url);
362 if (url.pathname === "/api/stories") {
363 const storySearch = await hnSearch({
364 search_by_date: true,
381 }
382
383 if (url.pathname === "/api/comments") {
384 const params = url.searchParams;
385 const query = params.get("query") || "";
265
266 if (!jsonContent) {
267 throw new Error("Failed to extract JSON from the API response");
268 }
269
405
406const css = `
407 @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap');
408
409 :root {
737async function server(request: Request): Promise<Response> {
738 if (request.method === "POST" && new URL(request.url).pathname === "/generate-training") {
739 const YOUTUBE_API_KEY = Deno.env.get("YOUTUBE_API_KEY2");
740 const useApiKey = YOUTUBE_API_KEY !== undefined && YOUTUBE_API_KEY !== "";
741 if (!YOUTUBE_API_KEY) {
742 console.warn("YouTube API key (YOUTUBE_API_KEY2) is not set. Falling back to search URL method.");
743 }
744
805 for (const placeholder of videoPlaceholders) {
806 const searchQuery = placeholder.replace("[VIDEO: ", "").replace("]", "");
807 const videoId = await getYouTubeVideoId(searchQuery, sport, YOUTUBE_API_KEY);
808 if (videoId) {
809 const embedHtml = `
891}
892
893async function getYouTubeVideoId(query, sport, apiKey, useApiKey = true) {
894 if (useApiKey) {
895 try {
896 const searchUrl = `https://www.googleapis.com/youtube/v3/search?part=snippet&q=${
897 encodeURIComponent(sport + " " + query)
898 }&key=${apiKey}&type=video&maxResults=1`;
899 const response = await fetch(searchUrl);
900 const data = await response.json();
901 if (data.error) {
902 console.warn("YouTube API error:", data.error.message);
903 return getFallbackYouTubeLink(query, sport);
904 }
907 }
908 } catch (error) {
909 console.error("Error fetching from YouTube API:", error);
910 }
911 }
77
78<p>
79 Spotify <a href='https://www.theverge.com/2024/12/5/24311523/spotify-locked-down-apis-developers'>does</a>
80 a <a href='https://www.theverge.com/2024/11/14/24294995/spotify-ai-fake-albums-scam-distributors-metadata'>lot</a>
81 of <a href='https://www.vice.com/en/article/spotify-rogan-rfk-vaccine-misinformation-policy/'>bad</a>
92 needlessly complex because, like way too many companies,
93 Bandcamp has <a href='https://www.reddit.com/r/BandCamp/comments/k8ojd2/question_for_any_devs_with_bandcamp_experience/'>locked down</a>
94 and <a href='https://github.com/michaelherger/Bandcamp-API'>hobbled</a> their public
95 API, probably because bad actors would abuse it, which is reasonable, but
96 on the other hand, made it impossible to do creative things like this that benefit
97 Bandcamp without annoying workarounds. I don't know, people would probably
98 <a href='https://www.404media.co/bluesky-posts-machine-learning-ai-datasets-hugging-face/'>
99 train some large language model on Bandcamp data</a> if there was a public API,
100 because that's what people do now.
101</p>
5wacky enough to buy their music instead of streaming it from some service.
6
7Because Bandcamp doesn't have an API, this hinges on you going to your purchases page, copying the purchases, and pasting it in. Thanks to
8the ability of [the system clipboard to contain HTML](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardItem), the same technology that makes
9copy-and-pasted text have unpredictable and annoying font and boldness choices also lets this parse and reformat that purchases page into something
10shareable.
1/** @jsxImportSource https://esm.sh/react */
2import { BskyAgent } from "https://esm.sh/@atproto/api";
3import React, { useState } from "https://esm.sh/react";
4import { createRoot } from "https://esm.sh/react-dom/client";