Because the real meetup.com didn’t like my idea…

EventFetch is a demo, Android “no-root” application (jq script) for fetching structured data and displaying it to the user.

It is just an example file. The real meat is in the system of standards.

Structured data, to me, is the “Network’s HTML”. It is often human readable, we (programmers) send it back and forth over the network, whenever we need some event or something to be logged somewhere.

All it is, is a let of standards. Here is an abbreviated JSON blob for one of my websites:

<!-- Structured Data -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "ProfessionalService",
  "@id": "https://meadowlarklowvoltage.com/#business",
  "name": "Meadowlark Low Voltage",
  "url": "https://meadowlarklowvoltage.com/",
  "telephone": "+1-610-707-9247",
}

Not much to it, is there…

Its literally just headings and fields sort of situation. Something =s something. Fantastic, Object-Oriented Programing I can understand at 2AM! (JSON is not OO LOL it was a joke).

EventFetch Structured Data Example

[ { "id": "it worked!", "url": "https://site.somwthing/demo-event", "author":
    "host@event.com", "published": "2026-09-14T12:00:00Z", "tags": [
      "event", "US", "PA", "12345", "20260920", "1900" ], "content": "EVENTFETCH demo\nevent in somewhere fun Planet 3"
  }
]

Lets “Pretty Print” it:

[
  {
    "id": "it worked!",
    "url": "https://site.somwthing/demo-event",
    "author": "host@event.com",
    "published": "2026-09-14T12:00:00Z",
    "tags": [
      "event",
      "US",
      "PA",
      "12345",
      "20260920",
      "1900"
    ],
    "content": "EVENTFETCH demo\nevent in somewhere fun Planet 3"
  }
]

I love structured data!

Really, I am a “web dev”. When a customer wants SEO, I make them structured data first, then we move into the fancy methods… Usually by the end of the “meta writing” as I call structured data creation. Usually, we have spent a lot of time and have positioned them to be vastly more competitive in the market than they were before. From an SEO perspective.

Simply, the world runs on structured data! We have all these applications all written by different people, often the only way they share anything is threw this sort of “third type of language”. Which is not a language at all, but AN AGREED-ON SET OF RULES AND EXPECATIONS. A protocol of sorts.

Federated Event – Structured Data Type

I herby propose, by the power vested in me by my laptop, that there be a standard “Event Structured Data Type”. So that across instances, everyone can pull events and compile lists.

I have created EventFetch as a demonstration of how small an application can be.

I have also specifically built in the door from plugins, because this is the real world, and it needs to be flexible. for “plugins”. So, if an instance or relay does not support the “baseline data structure that I cobbled together on my laptop”. You can simply redefine the fetcher with a plugin to properly phrase whatever source you would like. There is a Mastodon example.

THE CODE!!!

Was written by a robot. But is in shell! Read it as you would text!

It was written by a robot because that way a robot will be able to better fix it. This is just a demo; the expectation would be someone could use it to create their own. This one is very basic, all it does is run through the sources blobs and chunk them into files, all locally. Somone could write their own with pencil and paper or however you fancy. I used to try to write scripts before LLMs, it took HOURS!

First is the breakdown, then full thing.

Chunk by Chunk:

Code Explanation
#!/data/data/com.termux/files/usr/bin/sh

Selects the Termux shell used to execute the script.

SOURCE="$1"
PLUGIN="${2:-}"
COUNTRY="$3"
REGION="$4"
POSTAL="$5"
EVENT_DATE="$6"
EVENT_TIME="${7:-}"

Reads the command-line arguments.

SOURCE is the URL or input source.

PLUGIN is optional. If supplied, EventFetch passes the source through that normalization plugin.

COUNTRY, REGION, POSTAL, EVENT_DATE, and optional EVENT_TIME define the tags used to select matching events.

OUT="${EVENTFETCH_OUT:-$HOME/eventfetch/events}"
MAX_BYTES="${EVENTFETCH_CHUNK_SIZE:-1048576}"

Defines output settings.

EVENTFETCH_OUT can override the default output directory.

EVENTFETCH_CHUNK_SIZE can override the maximum chunk size. The default is 1,048,576 bytes, or 1 MiB.

TMP="$HOME/.eventfetch"

NORMALIZED="$TMP/normalized.jsonl"
MATCHES="$TMP/matches.jsonl"
RECORD="$TMP/record.txt"
CHUNK="$TMP/chunk.log"

Defines temporary working files.

normalized.jsonl contains normalized input records.

matches.jsonl contains only records matching the requested tags.

record.txt temporarily holds one formatted record.

chunk.log is the chunk currently being assembled.

mkdir -p "$OUT"
mkdir -p "$TMP"

Creates the output and temporary directories if they do not already exist.

BASE="events-${COUNTRY}-${REGION}-${POSTAL}-${EVENT_DATE}"

if [ -n "$EVENT_TIME" ]; then
    BASE="${BASE}-${EVENT_TIME}"
fi

Builds the base output filename from the requested location and date.

If a time was supplied, the time is appended to the filename as well.

chunk_no=1
chunk_records=0
total_records=0

: > "$CHUNK"

Initializes the chunk counters.

chunk_no identifies the current output file.

chunk_records counts records in the current chunk.

total_records counts all records saved during the fetch.

The final command creates or empties the temporary chunk file.

if [ -n "$PLUGIN" ]; then
    "$HOME/eventfetch/plugins/$PLUGIN" "$SOURCE" > "$NORMALIZED"
else
    curl -s "$SOURCE" | jq -c '.[]' > "$NORMALIZED"
fi

Normalizes the source.

If a plugin name is supplied, EventFetch executes that plugin and gives it the source. The plugin must output normalized JSONL.

If the plugin argument is empty, EventFetch uses its built-in/default path: curl downloads the source and jq converts the JSON array into one compact JSON object per line.

jq -c \
    --arg country "$COUNTRY" \
    --arg region "$REGION" \
    --arg postal "$POSTAL" \
    --arg date "$EVENT_DATE" \
    --arg time "$EVENT_TIME" '

    ([.tags[]? | ascii_downcase]) as $t |

    select($t | index("event")) |
    select($t | index($country | ascii_downcase)) |
    select($t | index($region | ascii_downcase)) |
    select($t | index($postal | ascii_downcase)) |
    select($t | index($date | ascii_downcase)) |

    select(
        ($time == "")
        or
        ($t | index($time | ascii_downcase))
    )

' "$NORMALIZED" > "$MATCHES"

Filters the normalized records by tags.

Each record’s tags are converted to lowercase for case-insensitive matching.

A record must contain:

  • event
  • requested country
  • requested region
  • requested postal code
  • requested date

If EVENT_TIME was supplied, that tag must also be present.

Matching JSON records are written to matches.jsonl.

seal_chunk() {

    if [ "$chunk_records" -eq 0 ]; then
        return
    fi

    FILE=$(printf "%s/%s-%04d.log" "$OUT" "$BASE" "$chunk_no")

    mv "$CHUNK" "$FILE"

    BYTES=$(wc -c < "$FILE")

    echo "$(basename "$FILE")    ${BYTES} bytes    ${chunk_records} records"

    chunk_no=$((chunk_no + 1))
    chunk_records=0

    : > "$CHUNK"
}

Finalizes the current chunk.

Empty chunks are ignored.

The chunk is renamed using the EventFetch base filename followed by a four-digit sequence number such as 0001.

Its filename, byte size, and record count are printed.

The chunk number is then advanced, the per-chunk record counter is reset, and a new empty working chunk is created.

stop_fetch() {
    echo
    echo "Stopping..."
    seal_chunk
    echo
    echo "$total_records records saved."
    exit
}

trap stop_fetch INT

Handles Ctrl+C / interrupt signals.

Instead of immediately abandoning the current data, EventFetch seals the partially completed chunk first and reports how many records were saved.

The trap attaches this function to the INT signal.

while IFS= read -r POST
do

Begins processing the matching records one at a time. Each JSONL line is loaded into POST without shell whitespace or backslash interpretation.

printf '%s\n' "$POST" |
jq -r '
    "========================================================================",
    "SOURCE: " + (.url // ""),
    "AUTHOR: " + (.author // ""),
    "PUBLISHED: " + (.published // ""),
    "TAGS: " + ([.tags[]?] | join(" ")),
    "",
    (.content // ""),
    "========================================================================",
    ""
' > "$RECORD"

Converts one matching JSON record into EventFetch’s human-readable log format.

It prints the source URL, author, publication date, tags, and content between separator lines.

Missing fields fall back to empty strings.

The formatted result is temporarily stored in record.txt.

RECORD_BYTES=$(wc -c < "$RECORD")
CHUNK_BYTES=$(wc -c < "$CHUNK")

Measures the size of the new record and the size of the chunk currently being assembled.

if [ "$chunk_records" -gt 0 ] &&
   [ $((CHUNK_BYTES + RECORD_BYTES)) -gt "$MAX_BYTES" ]
then
    seal_chunk
fi

Checks whether adding the next record would push the current chunk past the configured size limit.

If it would, the existing chunk is sealed first.

This keeps records intact instead of splitting an individual event across two files.

cat "$RECORD" >> "$CHUNK"

chunk_records=$((chunk_records + 1))
total_records=$((total_records + 1))

Appends the complete formatted event to the active chunk and increments both the current-chunk and total record counters.

CHUNK_BYTES=$(wc -c < "$CHUNK")

if [ "$CHUNK_BYTES" -ge "$MAX_BYTES" ]; then
    seal_chunk
fi

Measures the chunk again after adding the record.

If it has reached or exceeded the configured maximum size, the chunk is immediately sealed.

A single record larger than the chunk limit is therefore preserved whole and becomes its own oversized chunk.

done < "$MATCHES"

Feeds matches.jsonl into the processing loop until every matching event has been handled.

seal_chunk

echo
echo "Fetch complete."
echo "$total_records records saved."

After all records have been processed, seals any remaining partial chunk and prints the final completion message and total number of records saved.

All together:

#!/data/data/com.termux/files/usr/bin/sh

SOURCE="$1"
PLUGIN="${2:-}"
COUNTRY="$3"
REGION="$4"
POSTAL="$5"
EVENT_DATE="$6"
EVENT_TIME="${7:-}"

OUT="${EVENTFETCH_OUT:-$HOME/eventfetch/events}"
MAX_BYTES="${EVENTFETCH_CHUNK_SIZE:-1048576}"

TMP="$HOME/.eventfetch"

NORMALIZED="$TMP/normalized.jsonl"
MATCHES="$TMP/matches.jsonl"
RECORD="$TMP/record.txt"
CHUNK="$TMP/chunk.log"

mkdir -p "$OUT"
mkdir -p "$TMP"

BASE="events-${COUNTRY}-${REGION}-${POSTAL}-${EVENT_DATE}"

if [ -n "$EVENT_TIME" ]; then
    BASE="${BASE}-${EVENT_TIME}"
fi

chunk_no=1
chunk_records=0
total_records=0

: > "$CHUNK"

if [ -n "$PLUGIN" ]; then
    "$HOME/eventfetch/plugins/$PLUGIN" "$SOURCE" > "$NORMALIZED"
else
    curl -s "$SOURCE" | jq -c '.[]' > "$NORMALIZED"
fi

jq -c \
    --arg country "$COUNTRY" \
    --arg region "$REGION" \
    --arg postal "$POSTAL" \
    --arg date "$EVENT_DATE" \
    --arg time "$EVENT_TIME" '

    ([.tags[]? | ascii_downcase]) as $t |

    select($t | index("event")) |
    select($t | index($country | ascii_downcase)) |
    select($t | index($region | ascii_downcase)) |
    select($t | index($postal | ascii_downcase)) |
    select($t | index($date | ascii_downcase)) |

    select(
        ($time == "")
        or
        ($t | index($time | ascii_downcase))
    )

' "$NORMALIZED" > "$MATCHES"

seal_chunk() {

    if [ "$chunk_records" -eq 0 ]; then
        return
    fi

    FILE=$(printf "%s/%s-%04d.log" "$OUT" "$BASE" "$chunk_no")

    mv "$CHUNK" "$FILE"

    BYTES=$(wc -c < "$FILE")

    echo "$(basename "$FILE")    ${BYTES} bytes    ${chunk_records} records"

    chunk_no=$((chunk_no + 1))
    chunk_records=0

    : > "$CHUNK"
}

stop_fetch() {
    echo
    echo "Stopping..."
    seal_chunk
    echo
    echo "$total_records records saved."
    exit
}

trap stop_fetch INT

while IFS= read -r POST
do
    printf '%s\n' "$POST" |
    jq -r '
        "========================================================================",
        "SOURCE: " + (.url // ""),
        "AUTHOR: " + (.author // ""),
        "PUBLISHED: " + (.published // ""),
        "TAGS: " + ([.tags[]?] | join(" ")),
        "",
        (.content // ""),
        "========================================================================",
        ""
    ' > "$RECORD"

    RECORD_BYTES=$(wc -c < "$RECORD")
    CHUNK_BYTES=$(wc -c < "$CHUNK")

    if [ "$chunk_records" -gt 0 ] &&
       [ $((CHUNK_BYTES + RECORD_BYTES)) -gt "$MAX_BYTES" ]
    then
        seal_chunk
    fi

    cat "$RECORD" >> "$CHUNK"

    chunk_records=$((chunk_records + 1))
    total_records=$((total_records + 1))

    CHUNK_BYTES=$(wc -c < "$CHUNK")

    if [ "$CHUNK_BYTES" -ge "$MAX_BYTES" ]; then
        seal_chunk
    fi

done < "$MATCHES"

seal_chunk

echo
echo "Fetch complete."
echo "$total_records records saved."

An Archive to play with…

If you are running android, get Termux from the play store, you can copy that file to a tree you create (if you think my download has viruses haha). Or you can download it from the button.

You can change the top line and run it anywhere on Linux.

eventfetch/
├── fetch.sh
├── README.md
├── events/
└── plugins/
    └── mastodon

THERE IS A DEMO EVENT LIVE AT THIS DOMAIN at /eventfetch-demo.json (https://proe.whimm.ing/eventfetch-demo.json)

SHA256: 107e2b9480d849692a36538c03d7c89036cb8ff6ab5604cd7e6240fdccfce247

What to expect in Termux:

Termux — EventFetch Demo

I don’t even want you to download it, or any of this to do anything…

I want someone else too. I want someone who is smarter and better and has more time than me. To spawn something like this. The efficiency we could gain as people if we learned tools like this.

To call that file an “application” is a stretch… But to me, it does an application like thing! It goes and gets data, brings it back to me. All from my phone. Without any bloat or ads or anything.

All it needs is other people to share the same freaking “UHHH OK SO ADDRESS IS SPELLED A-D-D-R-E-S-S AND YOU WRITE IT LIKE THIS”. That’s it, if we could just pull that off, every individual human would be able to use the internet to directly communicate with their peers. Without any sort of “platforms” or ads”

Again, screaming into a void, just an idea I had.

BRING ON THE AGE OF MICRO LINUX APPS FOR ALL!


Peter Roe