SMeet Mini Apps
Examples
Each example is a complete Mini App with its page, its server and its bot. Download one, give it the key of your Mini App and the token of your bot, and run it behind your HTTPS origin.
The three examples#
| Example | What it shows | Runs on | Archive |
|---|---|---|---|
| Form | sign-in without cookies, a draft on the device, submitToBot() with SMeet's confirmation, the bot's answer in the chat | Node.js 22.13 or newer, no packages | smeet-mini-apps-forms.zip |
| Service desk | a session check before every operation, files given back safely, an outbox of chat notices, operators | Node.js 22.13 or newer and PostgreSQL 11 or newer | smeet-mini-apps-desk.zip |
| Minimal Python | the shortest server: sign-in, a session check, an answer of the bot | Python 3.9 or newer, the standard library only | smeet-mini-apps-python-minimal.zip |
Every example has a README in Russian and English with every setting, an .env.example and its tests.
Form#
A form that a person fills in inside the SMeet window and sends to the bot.
- The page loads the SDK in
<head>and callsconnect(),ready()and the sign-in inwindow.onload. - Sign-in: a one-time attempt from the server,
authorize(), the exchange on the server withIdempotency-Key: login:<attempt id>, the server's own session in the page's memory. - The draft lives in
localStorageunder a key with the environment and the person's id, and carries its ownrequestId: sending it again never creates a second form. - The production process is also the bot's only receiver of updates. It files each
mini_app_submissionby itsenvironment, confirms the position only after the write and answers in the chat with the revision of the update.
Its server consumes the login attempt and exchanges the code:
export async function signIn({ body, attempts, sessions, authorApi, environment }) {
const attemptId = field(body, 'attempt_id', { pattern: ATTEMPT_RE, max: 64 });
const attemptSecret = field(body, 'attempt_secret', { max: 128 });
const code = field(body, 'code', { max: 512 });
const codeVerifier = field(body, 'code_verifier', { pattern: VERIFIER_RE, max: 128 });
if (!attempts.consume(attemptId, attemptSecret)) {
throw new HttpError(400, 'LOGIN_RETRY', 'The sign-in attempt is unknown, used or expired', { reason: 'attempt' });
}
let exchanged;
try {
exchanged = await authorApi.exchange({ code, codeVerifier, idempotencyKey: `login:${attemptId}` });
} catch (error) {
throw pageError(error);
}
if (exchanged.environment !== environment) {
// SMeet already refuses a code of the other environment with ENVIRONMENT_MISMATCH (6.3); never trust it twice.
throw new HttpError(500, 'SERVER_MISCONFIGURED', 'The session belongs to another environment');
}
const own = sessions.create(exchanged);
return {
session_token: own.token,
expires_at: new Date(own.expiresAt).toISOString(),
environment: exchanged.environment,
user: {
id: exchanged.user?.id,
display_name: exchanged.user?.display_name ?? null,
language_code: exchanged.user?.language_code ?? 'en',
},
start_param: exchanged.start_param ?? null,
};
}Service desk#
An organisation's service desk: a person sees their tickets and creates a new one with a file; operators see every ticket of the space and change its status.
checkSessionwithout a cache before reading tickets, saving the draft, uploading and downloading a file, creating a ticket and changing a status.- The draft and its file are kept on the server per app, environment, space and person; reopening the window finds the file already there.
- A ticket is created under an
Idempotency-Keyof its draft, and the same transaction writes an outbox job with theexpected_access_revisionof that request's check (The outbox). - The worker sends the notice as text, or as a document with the ticket's file, with the key
ntf:<notification id>. - An operator's change of status sends nothing to the chat; the person sees it in the app.
- A file goes back only as an attachment of type
application/octet-streamwithnosniffandsandbox; in the web window the person gets a copy in the chat (Files).
Minimal Python#
The shortest Mini App with a server of its own, in one file, server.py: the page, the SDK as /sdk.js, the domain file /.well-known/smeet-mini-app.txt, the login attempt, the exchange, checkSession before every operation and the bot's sendMessage with the revision of that check. The page, page.js, calls ready() and authorize() in window.onload.
def checked(self, token: Optional[str]) -> Tuple[Dict[str, Any], Dict[str, Any]]:
"""checkSession without a cache before every protected operation (6.5)."""
with self.lock:
session = self.sessions.get(token or "")
if not session or session["expires"] <= self.clock():
raise PageError(401, "SESSION_EXPIRED")
try:
check = self.author("checkSession", {"session_token": session["smeet"]})
except SMeetError as error:
raise page_error(error)
if not check.get("active") or (check.get("user") or {}).get("id") != session["user"].get("id"):
with self.lock:
self.sessions.pop(token, None)
raise PageError(401, "SESSION_INACTIVE", reason=check.get("reason"))
return session, checkWhat an archive holds#
An archive keeps the layout of the documentation's sources, so the example finds the SDK where it expects it:
smeet-mini-apps-forms/
examples/.gitignore data, builds and packages stay out of git
examples/forms/ the example: README.md, .env.example, package.json, server, lib, web, test
sdk/smeet-mini-apps-1.0.0.js the SDK the example builds into its page, with .esm.js and the .d.ts declarations- An archive is made for your own origin: the base path is
/, and the server sendsframe-ancestors https://messenger.scrile.comitself, taken fromSMEET_URL. - It has no
.env, no data and no installed packages. The service desk installs its one package, the PostgreSQL client, withnpm cifrom itspackage-lock.json. - The same code runs SMeet's own demos under
https://messenger.scrile.com/mini-apps/; only the environment differs. That placement is not part of the archive.
Running against SMeet#
- One process per environment, each with the key of its environment in
MINI_APP_KEYand its own origin. The test version and the production version never share a process. - A bot has exactly one receiver of updates: the production process (
BOT_MODE=pollingorwebhook), which files each submission by itsenvironment. The test process runs withBOT_MODE=off; while there is no production process, the test process may receive them instead. SMEET_URLishttps://messenger.scrile.com. The minimal Python example also needsMINI_APP_VERIFICATION_TOKEN, the token of/.well-known/smeet-mini-app.txt.- Run the tests of an example with
npm testorpython3 -m unittest test_server.py; they need no SMeet.