SMeetMini Apps

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#

ExampleWhat it showsRuns onArchive
Formsign-in without cookies, a draft on the device, submitToBot() with SMeet's confirmation, the bot's answer in the chatNode.js 22.13 or newer, no packagessmeet-mini-apps-forms.zip
Service deska session check before every operation, files given back safely, an outbox of chat notices, operatorsNode.js 22.13 or newer and PostgreSQL 11 or newersmeet-mini-apps-desk.zip
Minimal Pythonthe shortest server: sign-in, a session check, an answer of the botPython 3.9 or newer, the standard library onlysmeet-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 calls connect(), ready() and the sign-in in window.onload.
  • Sign-in: a one-time attempt from the server, authorize(), the exchange on the server with Idempotency-Key: login:<attempt id>, the server's own session in the page's memory.
  • The draft lives in localStorage under a key with the environment and the person's id, and carries its own requestId: 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_submission by its environment, 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:

JavaScriptdocs/mini-apps/examples/forms/lib/login.mjs
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.

  • checkSession without 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-Key of its draft, and the same transaction writes an outbox job with the expected_access_revision of 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-stream with nosniff and sandbox; 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.

Pythondocs/mini-apps/examples/python-minimal/server.py
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, check

What an archive holds#

An archive keeps the layout of the documentation's sources, so the example finds the SDK where it expects it:

Text
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 sends frame-ancestors https://messenger.scrile.com itself, taken from SMEET_URL.
  • It has no .env, no data and no installed packages. The service desk installs its one package, the PostgreSQL client, with npm ci from its package-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_KEY and 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=polling or webhook), which files each submission by its environment. The test process runs with BOT_MODE=off; while there is no production process, the test process may receive them instead.
  • SMEET_URL is https://messenger.scrile.com. The minimal Python example also needs MINI_APP_VERIFICATION_TOKEN, the token of /.well-known/smeet-mini-app.txt.
  • Run the tests of an example with npm test or python3 -m unittest test_server.py; they need no SMeet.