Lingara Lingara بەڵگەنامەکان ڕێنماییەکان API کتێبخانەکان ئەپەکان دروستکردن ئەپی وێب
زمان: سۆرانی

دەستپێکی خێرا

ئەم پەڕەیە لە ئینگلیزییەوە وەرگێڕدراوە. ئەگەر هەردووکیان جیاواز بوون، پەڕە ئینگلیزییەکە ڕاستە. پەڕە ئینگلیزییەکە بخوێنەوە

ئەم پەڕەیە ئەپێک لە هیچەوە دەگەیەنێتە کارتێک لەسەر هەژماری خۆت. نموونەکانی خوارەوە لە هەموو زمانێکدا هەمان هەنگاوەکان دەگرنەبەر.

کلاینتێکی OAuth دروست بکە

ئەپ کلاینتێکی OAuth ـە کە مانیفێستێکی هەیە، بۆیە بە کلاینتێک دەست پێبکە. یەکێک لە پەڕەی کلاینتەکانی OAuth ی کۆنسۆڵ دروست بکە و ئەو توانا API یانەی پێ بدە کە ئەپەکەت پێویستی پێیانە، وەک خوێندنەوەی پلانی وانەکان. ناسنامە و نهێنیی کلاینتەکە ڕێگە بە ڕاژەکارەکەت دەدەن کە Lingara API وەک ئەپەکەت بانگ بکات. لە ژینگەی ڕاژەکارەکەتدا هەڵیانبگرە.

مانیفێستەکە دروست بکە و باری بکە

مانیفێستەکە ئەپەکەت بە Lingara دەناسێنێت:

  • ناو و وەسفەکەی، بە زمانێکی سەرەکی، لەگەڵ وەرگێڕانی ئارەزوومەندانە؛
  • render_url، ناونیشانی HTTPS کە Lingara داواکارییەکانی بۆ دەنێرێت؛
  • ئەو شوێنانەی کە کارتەکەی دەتوانێت تێیاندا دەربکەوێت؛
  • ئەو دۆخەی کە داوا دەکات ببینێت، کە فێرخوازەکە لە کاتی دامەزراندندا ڕەزامەندی لەسەر دەدات (بڕوانە دۆخ و ڕەزامەندی)؛
  • ئەو توانا API یانەی پێویستی پێیانە، کە دەبێت کلاینتی OAuth ەکەی پێشتر هەیبێت؛
  • ئایا دەتوانێت تێبینییەک بۆ مامۆستا پێشکەش بکات (بڕوانە تێبینیی مامۆستا).

کیتەکە مانیفێستەکە وەک JSON دروست دەکات و بە پێی ئەو یاسایانەی کە بارکردن جێبەجێیان دەکات دەیپشکنێت، بۆیە هەر هەڵەیەک یەکەمجار لەسەر ئامێری خۆت دەردەکەوێت. لە پەڕەی ئەپەکانی کۆنسۆڵ، کلاینتەکەت هەڵبژێرە، بیکە بە ئەپ، و فایلەکە بار بکە.

نهێنییەکی واژوو دروست بکە

لە هەمان پەڕەدا، نهێنییەکی واژوو دروست بکە. تەنها یەک جار نیشان دەدرێت. لە ڕێگەی ژینگەکەیەوە بیدە بە ڕاژەکارەکەت، و هەرگیز بۆ کۆنترۆڵی سەرچاوە کۆمیتی مەکە. Lingara هەر داواکارییەک بۆ ئەپەکەت پێی واژوو دەکات، و کیتەکە ئەو داواکارییە ڕەت دەکاتەوە کە واژووەکەی ناگونجێت. ئەپێک دەتوانێت لە یەک کاتدا دوو نهێنیی هەبێت، بۆیە دەتوانیت یەکێکیان بەبێ وەستانی خزمەتگوزاری بگۆڕیت: تا کاتێک دوو نهێنی چالاکن، هەر داواکارییەک بە هەردووکیان واژوو دەکرێت.

فەنکشنی ڕێندەر بنووسە

فەنکشنی ڕێندەرەکەت داواکارییەک وەردەگرێت و کارتێک دەگەڕێنێتەوە. داواکارییەکە شوێنەکە، زمانی فێرخوازەکە، ئەو دۆخەی کە ڕەزامەندی داوە هاوبەشی بکات، و subject ناودەبات، کە بەهایەکی جێگیرە و فێرخوازەکە بە ئەپەکەت دەناسێنێت. هەرچییەک کە بۆ هەر فێرخوازێک هەڵیدەگریت لەژێر subject دا بیهێڵەرەوە. دوگمەیەک لەسەر کارتەکەت کردارێک دەنێرێت، و ڕاژەکارەکەت بە کارتێکی نوێ وەڵامی دەداتەوە. پەڕەی کارتەکان هەموو ئەوەی کارتێک دەتوانێت هەڵیبگرێت ڕیز دەکات.

ئەداپتەرەکە دابنێ

ئەداپتەری کیتەکە داواکارییە خاوەکە دەخوێنێتەوە، واژووەکەی دەپشکنێت، فەنکشنەکەت بانگ دەکات، و وەڵامەکە دەنووسێت. لەسەر ئەو ناونیشانەی کە render_url ی مانیفێستەکەت ناوی دەبات پێشکەشی بکە. Lingara تەنها ناونیشانە HTTPS ـەکانی سەر ئینتەرنێتی گشتی بانگ دەکات.

لەسەر هەژماری خۆت دایبمەزرێنە

لە پەڕەی ئەپەکانی کۆنسۆڵ، دامەزراندنی ئەپەکە لەسەر هەژمارەکەت هەڵبژێرە، دیاری بکە چی دەتوانێت ببینێت، و پشتڕاستی بکەرەوە. شاشەی پلانی وانەکان لەسەر شاشەیەکی فراوان، یان پەڕەی ماڵەوە لەسەر وێب بکەرەوە، و کارتەکەت دەردەکەوێت.

کلاینتی OAuth ی ئەپەکەت ڕووداویش وەردەگرێت، وەک ئامادەبوونی پلانێکی وانە. ڕێبەری وێبهووک و ڕووداوەکان لە سەرچاوەی API دا وەسفیان دەکات.

زمانی نموونەی کۆد

TypeScript

npm install @lingara/apps@0.1.0-alpha.6

Rust

cargo add lingara-apps@0.1.0-alpha.6 --features axum
cargo add axum@0.8
cargo add tokio --features macros,net,rt-multi-thread

Go

go get github.com/Spinning-Cat-Studios/lingara_app_framework_sdk/go@v0.1.0-alpha.6

Java

<dependency>
  <groupId>com.getlingara</groupId>
  <artifactId>lingara-apps-java</artifactId>
  <version>0.1.0-alpha.6</version>
</dependency>

Kotlin

implementation("com.getlingara:lingara-apps-kotlin:0.1.0-alpha.6")

Ruby

gem install lingara-apps -v 0.1.0-alpha.6

PHP

composer require spinningcatstudios/lingara-apps:0.1.0-alpha.6 spinningcatstudios/lingara:@alpha
زمانی نموونەی کۆد

TypeScript

import { manifest } from "@lingara/apps";

const json = manifest()
  .defaultLocale("en")
  .name({ en: "Daily five", "zh-Hans": "每日五词" })
  .description("Five words to review, picked from your plan.")
  .renderUrl("https://apps.example.com/lingara/render")
  .slots("home.side", "plans.empty_detail")
  .context("languages", "plan_summary")
  // `.scopes(…)` lists the API scopes your client uses, for the learner's consent page. This app uses none.
  .tutorNote()
  .toJson(); // Throws ManifestError naming the rule an upload would refuse.

writeFileSync("manifest.json", json); // Upload it in the console.

Rust

let manifest = manifest()
    .default_locale("en")
    .name("Daily five")
    .description("Five words to review, picked from your plan.")
    .render_url("https://apps.example.com/lingara/render")
    .slots([AppSlotName::HomeSide, AppSlotName::PlansEmptyDetail])
    .context([ContextSliceKind::Languages, ContextSliceKind::PlanSummary])
    // `.scopes(…)` lists the API scopes your client uses, for the learner's consent page. This app uses none.
    .tutor_note(true)
    .build()?;
std::fs::write("manifest.json", manifest.to_json())?;

Go

manifest, err := lingaraapps.NewManifest().
	DefaultLocale("en").
	Names(map[string]string{"en": "Daily five", "zh-Hans": "每日五词"}).
	Description("Five words to review, picked from your plan.").
	RenderURL("https://apps.example.com/lingara").
	Slots(lingaraapps.AppSlotNameHomeSide, lingaraapps.AppSlotNamePlansEmptyDetail).
	Context(lingaraapps.ContextSliceKindLanguages, lingaraapps.ContextSliceKindPlanSummary).
	// `.Scopes(…)` lists the API scopes your client uses, for the learner's consent page. This app uses none.
	TutorNote(true).
	Build()
if err != nil {
	return err // a *lingaraapps.ManifestError names the broken rule
}
data, err := manifest.ToJSON()
if err != nil {
	return err
}
// Upload manifest.json in the console.
err = os.WriteFile("manifest.json", data, 0o644)

Java

import com.getlingara.apps.Manifest;
import com.getlingara.apps.model.AppSlotName;
import com.getlingara.apps.model.ContextSliceKind;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;

String json =
    Manifest.manifest()
        .defaultLocale("en")
        .name("Daily five")
        .description("Five words to review, picked from your plan.")
        .renderUrl("https://apps.example.com/lingara/render")
        .slots(AppSlotName.HOME_SIDE)
        .context(ContextSliceKind.LANGUAGES, ContextSliceKind.PLAN_SUMMARY)
        // `.scopes(…)` lists the API scopes your client uses, for the learner's consent
        // page. This app uses none.
        .tutorNote(true)
        .build() // refuses what the upload would, naming the rule
        .toJson();
// Upload manifest.json in the console, beside the app's icon.
Files.writeString(Path.of("manifest.json"), json);

Kotlin

import com.getlingara.apps.kotlin.manifest
import com.getlingara.apps.kotlin.model.AppSlotName
import com.getlingara.apps.kotlin.model.ContextSliceKind
import java.io.File

val json =
    manifest {
        defaultLocale = "en"
        name("Daily five")
        description("Five words to review, picked from your plan.")
        renderUrl = "https://apps.example.com/lingara/render"
        slots(AppSlotName.HOME_PERIOD_SIDE)
        context(ContextSliceKind.LANGUAGES, ContextSliceKind.PLAN_SUMMARY)
        // `scopes(…)` lists the API scopes your client uses, for the learner's consent page.
        // This app uses none.
        tutorNote = true
    }.toJson() // refuses what the upload would, naming the rule
// Upload manifest.json in the console, beside the app's icon.
File("manifest.json").writeText(json)

Ruby

manifest = Lingara::Apps.manifest
  .default_locale("en")
  .name("Daily five") # A bare String is the default locale's; or {"en" => …, "zh-Hans" => …}.
  .description("Five words to review, picked from your plan.")
  .render_url("https://apps.example.com/lingara")
  .slots("home.side", "plans.empty_detail")
  .context("languages", "plan_summary")
  # `.scopes(…)` lists the API scopes your client uses, for the learner's consent page. This app uses none.
  .tutor_note
# Raises Lingara::Apps::ManifestError naming the rule the upload would refuse.
File.write("manifest.json", manifest.to_json)

PHP

use Lingara\Apps\Generated\AppSlotName;
use Lingara\Apps\Manifest;

$json = Manifest::create()
    ->defaultLocale('en')
    ->name(['en' => 'Daily five', 'zh-Hans' => '每日五词'])
    ->description('Five words to review, picked from your plan.')
    ->renderUrl('https://apps.example.com/lingara/render')
    ->slots(AppSlotName::HOME_SIDE, AppSlotName::PLANS_EMPTY_DETAIL)
    ->context('languages', 'plan_summary')
    // `->scopes(…)` lists the API scopes your client uses, for the learner's consent page. This app uses none.
    ->tutorNote()
    ->toJson(); // Throws ManifestException naming the rule an upload would refuse.

file_put_contents('manifest.json', $json); // Upload it in the console.
زمانی نموونەی کۆد

TypeScript

const app = lingaraApp({
  // lgr_whsec_…, from the app's settings; pass two during a rotation.
  secret: process.env.LINGARA_APP_SECRET!,
  // Hold per-learner state on `subject`, the same value your events carry.
  render: (request) => (request.slot === "home.side" ? withNote(request) : todayCard(streaks.get(request.subject) ?? 0)),
  actions: {
    // The button's `action`; may arrive twice, so make it safe to repeat.
    done: (request) => {
      streaks.set(request.subject, (streaks.get(request.subject) ?? 0) + 1);
      return todayCard(streaks.get(request.subject)!);
    },
  },
});

Rust

// The app's signing secret (lgr_whsec_…), from the environment.
let seen = Seen::default();
let (render_seen, action_seen) = (Arc::clone(&seen), Arc::clone(&seen));
let app = App::new([std::env::var("LINGARA_APP_SECRET")?], move |request: AppRenderRequest| {
    render(Arc::clone(&render_seen), request)
})?
// The button's `action` comes back as `action_id`. An action can arrive
// twice, so keep it safe to repeat.
.action("next", move |request: AppActionRequest| {
    let seen = Arc::clone(&action_seen);
    async move {
        *seen.lock().map_err(|_| "poisoned")?.entry(request.subject).or_default() += 1;
        today_card("Next word", "zh")
    }
});

Go

// Per-learner state is keyed on Subject, the lgr_sub_… value the app's
// events carry too.
var mu sync.Mutex
streaks := map[string]int{}

render := func(ctx context.Context, req lingaraapps.AppRenderRequest) (lingaraapps.Replier, error) {
	mu.Lock()
	days := streaks[req.Subject]
	mu.Unlock()
	return lingaraapps.NewCard().
		Heading("Daily five", 1).
		Progress(float64(days%5)/5, "This week").
		Button("Done today", "done").
		Build()
}
// An action may arrive twice: make it safe to run twice.
done := func(ctx context.Context, req lingaraapps.AppActionRequest) (lingaraapps.Replier, error) {
	mu.Lock()
	streaks[req.Subject]++
	mu.Unlock()
	return lingaraapps.NewCard().Heading("Nice work", 1).Text("See you tomorrow.").Build()
}
app := lingaraapps.App{
	Render:  render,
	Actions: map[string]lingaraapps.ActionFunc{"done": done},
}

Java

import com.getlingara.apps.Card;
import com.getlingara.apps.LingaraApp;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

// Per-learner state is keyed on subject, the value the app's events carry too.
Map<String, Integer> seen = new ConcurrentHashMap<>();
LingaraApp app =
    LingaraApp.builder()
        // The app's signing secret (lgr_whsec_…); pass two during a rotation.
        .secrets(System.getenv("LINGARA_APP_SECRET"))
        .render(
            request -> {
              int count = seen.getOrDefault(request.getSubject(), 0);
              return Card.card()
                  .heading("Today's five", 1)
                  .text("Reviewed " + count + " so far.")
                  .button("Next", "next")
                  .build();
            })
        // The button's action comes back as the request's action_id. It may arrive
        // twice, so make it safe to repeat.
        .action(
            "next",
            request -> {
              int count = seen.merge(request.getSubject(), 1, Integer::sum);
              return Card.card().progress(Math.min(1.0, count / 5.0), count + " of 5").build();
            })
        .build();

Kotlin

import com.getlingara.apps.kotlin.LingaraApp
import com.getlingara.apps.kotlin.card
import com.getlingara.apps.kotlin.lingaraApp
import java.util.concurrent.ConcurrentHashMap

// Per-learner state is keyed on subject, the value the app's events carry too.
val seen = ConcurrentHashMap<String, Int>()
val app =
    lingaraApp {
        // The app's signing secret (lgr_whsec_…); pass two during a rotation.
        secrets(System.getenv("LINGARA_APP_SECRET"))
        render { request ->
            val count = seen.getOrDefault(request.subject, 0)
            card {
                heading("Today's five", 1)
                text("Reviewed $count so far.")
                button("Next", "next")
            }
        }
        // The button's action comes back as the request's action_id. It may arrive twice,
        // so make it safe to repeat.
        action("next") { request ->
            val count = seen.merge(request.subject, 1, Int::plus) ?: 1
            card { progress(minOf(1.0, count / 5.0), "$count of 5") }
        }
    }

Ruby

# lgr_whsec_…, from the app's settings; pass an Array of two during a rotation.
app = Lingara::Apps::App.new(secret: ENV.fetch("LINGARA_APP_SECRET"))
# Hold per-learner state on `subject`, the same value your events carry.
app.render do |request|
  (request.slot == "home.side") ? with_note(request) : today_card(STREAKS[request.subject])
end
# The button's `action`; it may arrive twice, so make it safe to repeat.
app.action("done") do |request|
  STREAKS[request.subject] += 1
  today_card(STREAKS[request.subject])
end

PHP

use Lingara\Apps\App;
use Lingara\Apps\Generated\AppActionRequest;
use Lingara\Apps\Generated\AppRenderRequest;

$app = new App(
    // lgr_whsec_…, from the app's settings; pass a list of two during a rotation.
    secret: (string) getenv('LINGARA_APP_SECRET'),
    // Hold per-learner state on `subject`, the same value your events carry.
    render: static fn(AppRenderRequest $request) => $request->getSlot() === AppSlotName::HOME_SIDE
        ? withNote($request)
        : todayCard($streaks[$request->getSubject()] ?? 0),
    actions: [
        // The button's `action`; may arrive twice, so make it safe to repeat.
        'done' => static function (AppActionRequest $request) use (&$streaks): CardModel {
            $streaks[$request->getSubject()] = ($streaks[$request->getSubject()] ?? 0) + 1;
            return todayCard($streaks[$request->getSubject()]);
        },
    ],
);
زمانی نموونەی کۆد

TypeScript

// A node:http request listener: it reads the raw body, verifies, dispatches and replies.
createServer(nodeHandler(app)).listen(8787);

Rust

// Nest the adapter at the path your manifest's render_url names.
let routes = axum::Router::new().nest("/lingara/render", lingara_apps::axum::router(app));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
axum::serve(listener, routes).await?;

Go

// The app's signing secret, lgr_whsec_…, or both during a rotation.
handler, err := lingaraapps.NewHandler(app, os.Getenv("LINGARA_APP_SECRET"))
if err != nil {
	return err // a malformed secret fails here, at startup
}
mux := http.NewServeMux()
mux.Handle("/lingara", handler)
err = http.ListenAndServe(":8080", mux)

Java

import com.getlingara.apps.JdkHttpHandler;
import com.getlingara.apps.LingaraApp;
import com.sun.net.httpserver.HttpServer;
import java.io.IOException;
import java.net.InetSocketAddress;

// The JDK's own server: the handler reads the raw body, verifies it, then dispatches.
HttpServer server = HttpServer.create(new InetSocketAddress(8080), 0);
server.createContext("/lingara/render", JdkHttpHandler.of(app));
server.start();

Kotlin

import com.getlingara.apps.kotlin.LingaraApp
import com.getlingara.apps.kotlin.ktor.lingaraApp
import io.ktor.server.cio.CIO
import io.ktor.server.engine.embeddedServer
import io.ktor.server.routing.route
import io.ktor.server.routing.routing

// Route.lingaraApp reads the raw body itself, so content negotiation never parses it.
embeddedServer(CIO, port = 8080) {
    routing {
        route("/lingara/render") { lingaraApp(app) }
    }
}.start(wait = true)

Ruby

# config.ru: any Rack server (Puma, Falcon, rackup) runs it. It reads the
# raw body itself, so mount it where nothing has parsed the JSON first.
run Lingara::Apps::RackApp.new(app)

PHP

use Lingara\Apps\Psr15Handler;
use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator;

// A PSR-15 handler: mount it on your framework's route, or answer from a
// front controller as here. It reads the raw body, verifies, dispatches and replies.
$factory = new Psr17Factory();
$handler = new Psr15Handler($app, $factory, $factory);
$response = $handler->handle((new ServerRequestCreator($factory, $factory, $factory, $factory))->fromGlobals());
http_response_code($response->getStatusCode());
foreach ($response->getHeaders() as $name => $values) {
    header("{$name}: " . implode(', ', $values));
}
echo $response->getBody();

هەروەها ببینە