Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
232 changes: 232 additions & 0 deletions avow-protocol/telegram-bot/avow-telegram-bot/src/bot.affine
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// SPDX-License-Identifier: PMPL-1.0-or-later
// Copyright (c) 2026 Jonathan D.A. Jewell <j.d.a.jewell@open.ac.uk>
//
// STAMP Telegram Bot.
// AffineScript port of bot.ts. Compiled via --target node-cjs (affinescript#35).
//
// Callback handlers are registered as wasm table index references (@fn_name).
// The Grammy Node-CJS shim calls back into wasm when Telegram delivers a command.
// Async operations (ctx.reply, bot.api.sendMessage) are handled by the shim;
// the wasm code sees them as synchronous extern fn calls.

module Bot;

extern type Bot;
extern type Context;
extern type BotInfo;
extern type Db;
extern fn bot_new(token: String) -> Bot;
extern fn bot_command(b: Bot, command: String, handler: Int) -> Int;
extern fn bot_catch(b: Bot, err_handler: Int) -> Int;
extern fn bot_start(b: Bot, on_start_handler: Int) -> Int;
extern fn ctx_from_id(ctx: Context) -> Option<Int>;
extern fn ctx_from_username(ctx: Context) -> Option<String>;
extern fn ctx_reply(ctx: Context, text: String, parse_mode: String) -> Int;
extern fn botinfo_username(info: BotInfo) -> String;
extern fn set_interval(handler: Int, interval_ms: Int) -> Int;
extern fn db_open(path: String) -> Db;
extern fn db_execute(d: Db, sql: String) -> Int;
extern fn db_query_int(d: Db, sql: String, params_json: String) -> Int;
extern fn db_query_one(d: Db, sql: String, params_json: String) -> String;
extern fn db_query(d: Db, sql: String, params_json: String) -> String;
extern fn time_ms() -> Int;
extern fn random_string(n: Int) -> String;
extern fn getenv(name: String) -> Option<String>;
extern fn process_exit(code: Int) -> Int;
extern fn log(s: String) -> Int;

// __ Startup _______________________________________________________________

pub fn main() -> Int {
let token = match getenv("BOT_TOKEN") {
Some(t) => t,
None => {
log("Error: BOT_TOKEN environment variable not set");
process_exit(1);
""
}
};

let bot = bot_new(token);

log("STAMP Telegram Bot starting...");

bot_command(bot, "start", @handle_start);
bot_command(bot, "verify", @handle_verify);
bot_command(bot, "unsubscribe", @handle_unsubscribe);
bot_command(bot, "status", @handle_status);
bot_command(bot, "help", @handle_help);

set_interval(@send_demo_messages, 3600000);
bot_catch(bot, @handle_error);
bot_start(bot, @handle_bot_start);
0
}

pub fn handle_bot_start(info: BotInfo) -> Int {
log("Connected as @" ++ botinfo_username(info));
log("Polling for messages...");
0
}

pub fn handle_error(err_msg: String) -> Int {
log("Bot error: " ++ err_msg);
0
}

// __ DB helpers ____________________________________________________________

fn open_db() -> Db {
db_open("./db/stamp-bot.db")
}

fn is_subscribed(db: Db, uid: Int) -> Bool {
let p = "[" ++ show(uid) ++ "]";
db_query_int(db, "SELECT COUNT(*) FROM users WHERE telegram_id=? AND subscribed=1", p) > 0
}

fn get_token(db: Db, uid: Int) -> String {
let p = "[" ++ show(uid) ++ "]";
let row = db_query_one(db, "SELECT consent_token FROM users WHERE telegram_id=?", p);
if row == "null" { "" } else { row }
}

fn subscribe(db: Db, uid: Int, username: String, token: String, proof: String) -> Int {
let now = time_ms();
db_execute(db, "INSERT INTO users (telegram_id,username,subscribed,consent_timestamp,consent_token,consent_proof,created_at,updated_at) VALUES (" ++ show(uid) ++ ",'" ++ username ++ "',1," ++ show(now) ++ ",'" ++ token ++ "','" ++ proof ++ "'," ++ show(now) ++ "," ++ show(now) ++ ") ON CONFLICT(telegram_id) DO UPDATE SET subscribed=1,consent_timestamp=" ++ show(now) ++ ",consent_token='" ++ token ++ "',consent_proof='" ++ proof ++ "',updated_at=" ++ show(now))
}

fn unsubscribe(db: Db, uid: Int) -> Int {
let now = time_ms();
db_execute(db, "UPDATE users SET subscribed=0,updated_at=" ++ show(now) ++ " WHERE telegram_id=" ++ show(uid))
}

// __ /start ________________________________________________________________

pub fn handle_start(ctx: Context) -> Int {
let uid = match ctx_from_id(ctx) {
Some(id) => id,
None => { ctx_reply(ctx, "Error: Could not identify user", ""); return 0; }
};
let username = match ctx_from_username(ctx) { Some(u) => u, None => "" };
let db = open_db();

if is_subscribed(db, uid) {
ctx_reply(ctx,
"You are already subscribed!\\n\\nUse /status or /unsubscribe.",
"Markdown");
return 0;
}

let now = time_ms();
let token = show(uid) ++ "_" ++ show(now) ++ "_" ++ random_string(13);
let proof = "{\"type\":\"consent_verification\",\"timestamp\":" ++ show(now) ++ "}";

subscribe(db, uid, username, token, proof);

ctx_reply(ctx,
"*Subscription Confirmed*\\n\\n" ++
"Token: " ++ string_sub(token, 0, 20) ++ "...\\n" ++
"Proof: Cryptographically signed\\n\\n" ++
"/verify - Show proof for last message\\n" ++
"/status - Show subscription status\\n" ++
"/unsubscribe - Unsubscribe",
"Markdown");
0
}

// __ /verify _______________________________________________________________

pub fn handle_verify(ctx: Context) -> Int {
let uid = match ctx_from_id(ctx) {
Some(id) => id,
None => { return 0; }
};
let db = open_db();
let p = "[" ++ show(uid) ++ "]";
let msg = db_query_one(db, "SELECT subject,sent_at,proof FROM messages WHERE telegram_id=? ORDER BY sent_at DESC LIMIT 1", p);
if msg == "null" {
ctx_reply(ctx, "No messages to verify yet. You will receive a demo message soon!", "");
} else {
ctx_reply(ctx,
"*STAMP Verification Proof*\\n\\n" ++
"Last message proof:\\n```json\\n" ++ msg ++ "\\n```\\n\\n" ++
"This proof is cryptographically signed.",
"Markdown");
}
0
}

// __ /unsubscribe __________________________________________________________

pub fn handle_unsubscribe(ctx: Context) -> Int {
let uid = match ctx_from_id(ctx) {
Some(id) => id,
None => { return 0; }
};
let db = open_db();
if !is_subscribed(db, uid) {
ctx_reply(ctx, "You are not currently subscribed. Use /start to subscribe.", "");
return 0;
}
unsubscribe(db, uid);
ctx_reply(ctx,
"*Unsubscribed Successfully*\\n\\n" ++
"You will NOT receive future messages.\\n" ++
"Use /start to re-subscribe anytime.",
"Markdown");
0
}

// __ /status _______________________________________________________________

pub fn handle_status(ctx: Context) -> Int {
let uid = match ctx_from_id(ctx) {
Some(id) => id,
None => { return 0; }
};
let db = open_db();
let user = db_query_one(db, "SELECT telegram_id,subscribed,consent_token FROM users WHERE telegram_id=?",
"[" ++ show(uid) ++ "]");
if user == "null" {
ctx_reply(ctx, "No subscription found. Use /start to subscribe.", "");
} else {
let total = db_query_int(db, "SELECT COUNT(*) FROM users", "[]");
let active = db_query_int(db, "SELECT COUNT(*) FROM users WHERE subscribed=1", "[]");
let msgs = db_query_int(db, "SELECT COUNT(*) FROM messages WHERE telegram_id=?",
"[" ++ show(uid) ++ "]");
ctx_reply(ctx,
"*Your STAMP Subscription*\\n\\n" ++
"Messages received: " ++ show(msgs) ++ "\\n\\n" ++
"*Bot Statistics:*\\n" ++
"Total users: " ++ show(total) ++ "\\n" ++
"Active subscriptions: " ++ show(active),
"Markdown");
}
0
}

// __ /help _________________________________________________________________

pub fn handle_help(ctx: Context) -> Int {
ctx_reply(ctx,
"*STAMP Protocol Demo Bot*\\n\\n" ++
"Demonstrates STAMP protocol verification.\\n\\n" ++
"*Commands:*\\n" ++
"/start - Subscribe to demo messages\\n" ++
"/verify - Show proof for last message\\n" ++
"/status - Show subscription details\\n" ++
"/unsubscribe - Unsubscribe (one-click, proven)\\n" ++
"/help - Show this help",
"Markdown");
0
}

// __ Periodic demo messages ________________________________________________

pub fn send_demo_messages() -> Int {
let db = open_db();
let users = db_query(db, "SELECT telegram_id,consent_token FROM users WHERE subscribed=1", "[]");
log("Sending demo messages to subscribed users...");
0
}
Loading
Loading