Skip to content

Instantly share code, notes, and snippets.

@blakek
Created November 25, 2024 16:47
Show Gist options
  • Save blakek/4a622eff7b65b9323b1b57a58523e9fb to your computer and use it in GitHub Desktop.
Save blakek/4a622eff7b65b9323b1b57a58523e9fb to your computer and use it in GitHub Desktop.
POC for getting burn bans in Mississippi
import { inspect } from "bun";
import { SimpleCache } from "./simple-cache";
interface BurnBanData {
counties: string;
issued: string;
expires: string;
exemptions: string;
}
async function getNonce(): Promise<string> {
const nonceUrl = "https://www.mfc.ms.gov/burning-info/burn-bans/";
const nonceMatch = /ninja_table_public_nonce=(?<nonce>[a-z0-9]+)/;
const responseBody = await fetch(nonceUrl).then((r) => r.text());
if (!responseBody) {
throw new Error("Failed to fetch nonce");
}
const nonce = responseBody.match(nonceMatch);
if (!nonce || !nonce.groups) {
throw new Error("Failed to parse nonce");
}
return nonce.groups.nonce;
}
async function getBurnBanData(nonce: string) {
const url = new URL("https://www.mfc.ms.gov/wp-admin/admin-ajax.php");
url.searchParams.append("action", "wp_ajax_ninja_tables_public_action");
url.searchParams.append("table_id", "1775");
url.searchParams.append("target_action", "get-all-data");
url.searchParams.append("default_sorting", "new_first");
url.searchParams.append("skip_rows", "0");
url.searchParams.append("limit_rows", "0");
url.searchParams.append("ninja_table_public_nonce", nonce);
const response = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
});
if (!response.ok) {
throw new Error("Failed to fetch burn ban data");
}
const ninjaTableData: { value: BurnBanData }[] = await response.json();
const burnBanData: BurnBanData[] = ninjaTableData.map(
({ value: row }) => row
);
return burnBanData;
}
async function main() {
const cache = new SimpleCache<BurnBanData[]>(
1000 * 60 * 60 // Expires in 1 hour
);
let data: BurnBanData[] | null = await cache.get();
if (!data) {
console.log("Fetching new data");
const nonce = await getNonce();
const burnBanData = await getBurnBanData(nonce);
cache.set(burnBanData);
data = burnBanData;
}
console.log(inspect(data));
}
main();
{"value":[{"counties":"Covington","issued":"10/25/2024","expires":"11/25/2024","exemptions":"1, 2 & 4","___id___":127},{"counties":"Marion","issued":"10/24/2024","expires":"11/30/2024","exemptions":"1 & 2","___id___":125}],"expires":1732556580355}
import * as Bun from "bun";
export interface Expirable<T> {
value: T;
expires: number;
}
/**
* A very simple cache that stores a value and its expiration time in a file.
*/
export class SimpleCache<T> {
private cacheFilePath = new URL("simple-cache.json", import.meta.url);
private cacheFile = Bun.file(this.cacheFilePath);
private timeToLive: number;
constructor(timeToLive: number) {
this.timeToLive = timeToLive;
}
async get(): Promise<T | null> {
if (!(await this.cacheFile.exists())) {
await this.set(undefined as T);
}
let cache: Expirable<T> | null = null;
try {
cache = await this.cacheFile.json();
} catch {
console.warn("Failed to read cache file");
}
if (!cache) {
return null;
}
if ("expires" in cache === false || "value" in cache === false) {
console.warn("Invalid cache format");
return null;
}
if (cache.expires < Date.now()) {
return null;
}
return cache.value;
}
async set(value: T): Promise<void> {
const writer = this.cacheFile.writer();
const cache: Expirable<T> = {
value,
expires: Date.now() + this.timeToLive,
};
const serializedCache = JSON.stringify(cache);
writer.write(serializedCache);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment