Web & Network

Three recipes for network-connected KonsolScript apps - from calling a REST API to scraping web pages to building a LAN chat tool.

Back to Kookbook


HTTP API Client

Modules: curl plugin · JSON · Konsol

Demonstrates the full request/response cycle against the public JSONPlaceholder demo API - no key required. Covers setting headers, GETting a single resource, iterating a JSON array response, and POSTing a new resource.

Usage

minks http_api_client.ks

Sample output

=== HTTP API Client ===
Fetching post #1 from JSONPlaceholder...
HTTP status: 200
--- Post #1 ---
User ID : 1
Title   : sunt aut facere repellat provident occaecati excepturi optio reprehenderit
Body    : quia et suscipit...
...
Fetching first 3 comments for post #1...
  [1] id labore ex et quam laborum <Eliseo@gardner.biz>
       laudantium enim quasi est...
...
Creating a new post via POST...
POST status: 201
Server assigned ID: 101
Done.

Script

// http_api_client.ks - call REST APIs and process JSON responses
// Modules: curl plugin, JSON, Konsol
// Usage:  minks http_api_client.ks
//
// Demonstrates the full request/response cycle against the public
// JSONPlaceholder API: set headers, GET a single resource, iterate a list,
// and POST a new resource - no API key required.

#include "curl"

Konsol:Print("=== HTTP API Client ===");

// ── Step 1: Set common request headers ───────────────────────────────────────
// Headers persist across calls in this session until ClearHeaders() is called.
Curl:SetHeader("Accept", "application/json");
Curl:SetHeader("User-Agent", "KonsolScript/1.0");
Curl:SetTimeout(10);

// ── Step 2: GET - fetch a single post ────────────────────────────────────────
// Curl:Get writes the response body into the pre-declared variable (ByRef).
Konsol:Print("Fetching post #1 from JSONPlaceholder...");

Var:String body;
try {
    Curl:Get("https://jsonplaceholder.typicode.com/posts/1", body);
} catch (CurlException e) {
    Konsol:Print("Request failed: ${e.message}");
    Konsol:Exit(1);
}

// Always check the HTTP status before parsing the body.
Var:Number status;
Curl:Status(status);
Konsol:Print("HTTP status: ${status}");

if (status != 200) {
    Konsol:Print("Request failed - aborting.");
    Konsol:Exit(1);
}

// ── Step 3: Parse the JSON response ──────────────────────────────────────────
// JSON:Parse reads the raw string and builds a document (bare identifier).
// JSON:Get uses dot-notation paths to extract individual fields.
Var:Number postId;
Var:Number userId;
Var:String title;
Var:String postBody;

JSON:Parse(body, postDoc);
JSON:Get("id", postDoc, postId);
JSON:Get("userId", postDoc, userId);
JSON:Get("title", postDoc, title);
JSON:Get("body", postDoc, postBody);

Konsol:Print("--- Post #${postId} ---");
Konsol:Print("User ID : ${userId}");
Konsol:Print("Title   : ${title}");
Konsol:Print("Body    : ${postBody}");

// ── Step 4: GET - fetch a list and iterate ───────────────────────────────────
// Root JSON arrays are accessed by zero-based numeric index paths.
// e.g. "0.name", "1.email" for the first and second elements.
Konsol:Print("");
Konsol:Print("Fetching first 3 comments for post #1...");

Var:String listBody;
try {
    Curl:Get("https://jsonplaceholder.typicode.com/posts/1/comments", listBody);
} catch (CurlException e) {
    Konsol:Print("Comments request failed: ${e.message}");
    Konsol:Exit(1);
}

Curl:Status(status);
if (status != 200) {
    Konsol:Print("Comments request failed.");
    Konsol:Exit(1);
}

JSON:Parse(listBody, comments);

// JSONPlaceholder always returns 5 comments per post - iterate the first 3.
Var:String cName;
Var:String cEmail;
Var:String cBody;

for (Number i = 0; i < 3; i++) {
    Var:Number num = i + 1;
    JSON:Get("${i}.name", comments, cName);
    JSON:Get("${i}.email", comments, cEmail);
    JSON:Get("${i}.body", comments, cBody);

    Konsol:Print("  [${num}] ${cName} <${cEmail}>");
    Konsol:Print("       ${cBody}");
    Konsol:Print("");
}

// ── Step 5: POST - create a new resource ─────────────────────────────────────
// Switch to a JSON Content-Type header for the POST body.
Konsol:Print("Creating a new post via POST...");

Curl:ClearHeaders();
Curl:SetHeader("Content-Type", "application/json");
Curl:SetHeader("Accept", "application/json");

Var:String payload = """{
  "title": "KonsolScript demo",
  "body": "Hello from minks!",
  "userId": 1
}""";
Var:String postResponse;
try {
    Curl:Post("https://jsonplaceholder.typicode.com/posts", payload, postResponse);
} catch (CurlException e) {
    Konsol:Print("POST failed: ${e.message}");
    Konsol:Exit(1);
}

Curl:Status(status);
Konsol:Print("POST status: ${status}");    // expects 201 Created

if (status == 201) {
    JSON:Parse(postResponse, newDoc);
    Var:Number newId;
    JSON:Get("id", newDoc, newId);
    Konsol:Print("Server assigned ID: ${newId}");
}

Konsol:Print("Done.");


Web Scraper

Modules: curl plugin · Regex · String · File · List · Konsol

Fetches any public URL, extracts the page <title>, and collects all absolute http:// / https:// links using per-line regex matching. Optionally saves the link list to a text file.

Key patterns:

Usage

minks web_scraper.ks <url> [output.txt]

# examples
minks web_scraper.ks https://example.com
minks web_scraper.ks https://example.com links.txt

Sample output

=== Web Scraper ===
Target: https://example.com
HTTP status: 200
Page size: 47 lines
Page title: Example Domain
External links found: 1
  [1] https://www.iana.org/domains/reserved
Done.

Script

// web_scraper.ks - fetch a web page and extract data with patterns
// Modules: curl plugin, Regex, String, File, List, Konsol
// Usage:  minks web_scraper.ks <url> [output.txt]
//
// Fetches any public URL, extracts the page <title> and all absolute
// http/https links, prints them to the console, and optionally saves
// the link list to a text file.

#include "curl"

Konsol:Print("=== Web Scraper ===");

// ── Step 1: Read arguments ────────────────────────────────────────────────────
Var:List args;
OS:Args(args);

Var:Number argc;
List:Size(args, argc);

if (argc < 1) {
    Konsol:Print("Usage: minks web_scraper.ks <url> [output.txt]");
    Konsol:Exit(1);
}

Var:String url;
List:Get(0, args, url);

Var:String outFile = "";
if (argc >= 2) {
    List:Get(1, args, outFile);
}

Konsol:Print("Target: ${url}");

// ── Step 2: Fetch the page ───────────────────────────────────────────────────
Curl:SetHeader("User-Agent", "KonsolScript/1.0");
Curl:SetTimeout(15);

Var:String html;
try {
    Curl:Get(url, html);
} catch (CurlException e) {
    Konsol:Print("Fetch failed: ${e.message}");
    Konsol:Exit(1);
}

Var:Number status;
Curl:Status(status);
Konsol:Print("HTTP status: ${status}");

if (status != 200) {
    Konsol:Print("Fetch failed - aborting.");
    Konsol:Exit(1);
}

// ── Step 3: Split the HTML into lines for per-line regex matching ─────────────
// String:Split breaks the full HTML string on newlines, giving us a list of
// lines we can process one at a time - the same pattern used in log_analyzer.ks.
Var:List htmlLines;
String:Split(html, "\n", htmlLines);

Var:Number lineCount;
List:Size(htmlLines, lineCount);

Konsol:Print("Page size: ${lineCount} lines");

// ── Step 4: Extract the <title> ──────────────────────────────────────────────
// Scan each line; stop as soon as we find the title tag.
Var:List titleGroups;
Var:Number titleGc;
Var:String pageTitle = "(none)";
Var:Boolean titleFound = false;

for (Number li = 0; li < lineCount; li++) {
    Var:String line;
    List:Get(li, htmlLines, line);

    if (!titleFound) {
        List:Clear(titleGroups);
        Regex:Groups("<title>([^<]+)</title>", line, titleGroups);
        List:Size(titleGroups, titleGc);

        if (titleGc >= 2) {
            List:Get(1, titleGroups, pageTitle);
            String:Trim(pageTitle, pageTitle);
            titleFound = true;
        }
    }
}

Konsol:Print("Page title: ${pageTitle}");

// ── Step 5: Extract absolute href links ──────────────────────────────────────
// Regex:Groups finds the first match on each line.
// The pattern captures only http/https URLs to skip relative paths and anchors.
Var:List results;
Var:Number linkCount = 0;

Var:List hrefGroups;
Var:Number hrefGc;

for (Number li = 0; li < lineCount; li++) {
    Var:String line;
    List:Get(li, htmlLines, line);

    List:Clear(hrefGroups);
    Regex:Groups("href=\"(https?://[^\"]+)\"", line, hrefGroups);
    List:Size(hrefGroups, hrefGc);

    if (hrefGc >= 2) {
        Var:String href;
        List:Get(1, hrefGroups, href);
        List:Push(href, results);
        linkCount = linkCount + 1;
    }
}

// Note: one href is captured per line.  Minified HTML with many links on
// one line may be under-counted.  Unminify first or extend the loop with
// a second Regex:Groups pass if needed.

Konsol:Print("External links found: ${linkCount}");
Konsol:Print("");

// ── Step 6: Print the results ────────────────────────────────────────────────
for (Number ri = 0; ri < linkCount; ri++) {
    Var:String link;
    List:Get(ri, results, link);
    Var:Number num = ri + 1;
    Konsol:Print("  [${num}] ${link}");
}

// ── Step 7: Optionally save to a text file ───────────────────────────────────
if (outFile != "") {
    Var:Number wh;
    File:Open(outFile, "w", wh);
    File:Write("Scraped from: ${url}\n", wh);
    File:Write("Title: ${pageTitle}\n", wh);
    File:Write("Links:\n", wh);

    for (Number si = 0; si < linkCount; si++) {
        Var:String slink;
        List:Get(si, results, slink);
        File:Write("${slink}\n", wh);
    }

    File:Close(wh);
    Konsol:Print("Saved to ${outFile}");
}

Konsol:Print("Done.");


LAN Chat

Modules: net plugin · Konsol

Simple peer-to-peer text chat over a local network. One player hosts (h); others join (j) by entering the host's IP address. All messages are broadcast to every connected player.

Key patterns:

Usage

# on the host machine
minks lan_chat.ks
# > Host or join? (h/j): h

# on each client machine (same LAN)
minks lan_chat.ks
# > Host or join? (h/j): j
# > Host IP address: 192.168.1.10

Sample session

=== LAN Chat ===
Host or join? (h/j): h
Your name: Alice
Starting session 'chatroom' on port 2310...
Waiting for players...  (type a message to start chatting)
Type 'quit' to disconnect.

[Bob] hey Alice!
> Hello Bob!
[Bob] this is cool
> quit
Disconnected.  Goodbye, Alice!

Script

// lan_chat.ks - peer-to-peer LAN messaging
// Modules: net plugin, Konsol
// Usage:  minks lan_chat.ks
//
// Run once on the host machine (choose "h"), then run again on each
// client machine on the same LAN (choose "j").  All players share the
// session name "chatroom"; the host listens on port 2310.
//
// Limitation: Konsol:Input is blocking, so incoming messages only appear
// after you press Enter.  For a fully asynchronous UI you would split
// sending and receiving into a producer/consumer pattern using two
// separate processes.

#include "net"

Konsol:Print("=== LAN Chat ===");

// ── Step 1: Choose role ───────────────────────────────────────────────────────
Var:String role;
Konsol:Input("Host or join? (h/j): ", role);

Var:String playerName;
Konsol:Input("Your name: ", playerName);

Var:Number handle;

if (role == "h") {
    // Net:Host starts a session and waits for connections on port 2310.
    // maxPlayers = 8 allows up to 8 participants (including the host).
    Konsol:Print("Starting session 'chatroom' on port 2310...");
    Net:Host(playerName, 8, "chatroom", handle);
    Konsol:Print("Waiting for players...  (type a message to start chatting)");
} else {
    // Net:Join connects to a host by IP address.
    Var:String hostIP;
    Konsol:Input("Host IP address: ", hostIP);
    Konsol:Print("Connecting to ${hostIP}...");
    Net:Join(playerName, "chatroom", hostIP, handle);
    Konsol:Print("Connected!  (type a message to start chatting)");
}

Konsol:Print("Type 'quit' to disconnect.");
Konsol:Print("");

// ── Step 2: Chat loop ─────────────────────────────────────────────────────────
Var:Boolean running = true;
Var:String sender;
Var:String inMsg;
Var:Boolean got;
Var:String outMsg;
Var:Boolean sendOk;

while (running) {
    // Net:Check must be called every iteration to pump the network stack
    // and process connection/disconnection events.
    Net:Check(handle);

    // Drain all pending incoming messages before prompting for input.
    // Net:GetMessage returns one message at a time; loop until got = false.
    Net:GetMessage(handle, sender, inMsg, got);
    while (got) {
        Konsol:Print("[${sender}] ${inMsg}");
        Net:GetMessage(handle, sender, inMsg, got);
    }

    // Block for user input, then send to all connected players.
    Konsol:Input("> ", outMsg);

    if (outMsg == "quit") {
        running = false;
    } else {
        // Net:Send broadcasts to all connected players.
        // Use Net:SendTo(handle, targetName, msg, ok) for direct messages.
        Net:Send(handle, outMsg, sendOk);
        if (!sendOk) {
            Konsol:Print("Send failed - connection may be lost.");
            running = false;
        }
    }
}

// ── Step 3: Clean up ─────────────────────────────────────────────────────────
Net:Quit(handle);
Konsol:Print("Disconnected.  Goodbye, ${playerName}!");


Back to Kookbook