Skip to content

Quick start — Wasm

A Wasm plugin is a wasm32 module with a few exports and one import. Rust and C have ready SDKs in the repository; any other language does the same following the Wasm ABI.

{
"schema": 1,
"id": "com.example.counter",
"name": "Counter",
"version": "1.0.0",
"description": "Counts the messages it sees.",
"author": { "name": "Example Dev" },
"license": "MIT",
"compat": { "freshgram": ">=7.0.0 <8.0.0", "api": 7 },
"runtime": { "kind": "wasm", "module": "counter.wasm", "language": "rust" },
"permissions": {
"required": [
{ "id": "messages.read", "reason": "Counts the messages it sees." },
{ "id": "storage.private", "reason": "Remembers the count." }
]
},
"contributes": { "events": ["message.new"] }
}

The plugin’s Cargo.toml is a cdylib library that depends on the freshgram crate (from Telegram/plugins_sdk/rust):

[lib]
crate-type = ["cdylib"]
[dependencies]
freshgram = { path = "../.." }
[profile.release]
opt-level = "s"
lto = true
panic = "abort"
strip = true

src/lib.rs:

use freshgram::{log, storage_get, storage_set, Value};
fn init(_info: &Value) -> i32 {
let _ = log("counter ready");
0
}
fn event(event: &Value) -> i32 {
let chat = event.get("chat").and_then(Value::as_i64).unwrap_or(0);
let count = match storage_get("count") {
Ok(value) => value.and_then(|value| value.as_i64()).unwrap_or(0) + 1,
Err(error) => {
let _ = log("cannot read the counter");
return 100 + error.kind as i32 + 1;
}
};
if let Err(error) = storage_set("count", count) {
let _ = log("cannot save the counter");
return 200 + error.kind as i32 + 1;
}
let _ = storage_set("last_chat", chat);
count as i32
}
freshgram::plugin!(init = init, event = event);

Build:

Terminal window
cargo build --release --target wasm32-unknown-unknown

Put the resulting target/wasm32-unknown-unknown/release/counter.wasm next to manifest.json.

#include "freshgram.h"
int fg_plugin_init(fg_value info) {
(void)info;
fg_log("counter ready");
return 0;
}
int fg_plugin_event(fg_value event) {
fg_i64 chat = 0;
fg_as_int(fg_find(event, "chat"), &chat);
fg_i64 count = 0;
int read = fg_storage_get_int("count", &count);
if (read != FG_OK) {
fg_log("cannot read the counter");
return 100 + read;
}
count += 1;
int written = fg_storage_set_int("count", count);
if (written != FG_OK) {
fg_log("cannot save the counter");
return 200 + written;
}
fg_storage_set_int("last_chat", chat);
return (int)count;
}

Build (the header is self-contained, no libc; the host tests build the example the same way):

Terminal window
clang --target=wasm32 -O2 -nostdlib -fno-builtin -Wl,--no-entry -o counter.wasm counter.c

The freshgram.call import is declared in the header with import_module/import_name attributes, so no extra linker flags are needed.

Both init and the event handler return an int. 0 means success; a non-zero value is recorded by the host as the status of the handling. The example above returns the counter, which is allowed, but real plugins should return 0 on success and a negative number on error.

The numbers in the examples are only illustrative. In Rust, error.kind as i32 is the position of the variant in the ErrorKind enum (Denied is 0 … Cancelled is 7, Transport is 8), so the Rust example returns 109 for Transport and 108 for Cancelled; these are not the FG_* codes of the C header. The host does not interpret the number: for ordinary events the status is ignored, for init a non-zero status only writes a line to the plugin log, and only for message hooks does a non-zero status discard the plugin’s edit. See Return values.