Démarrage rapide Cette page est traduite de l'anglais. En cas de différence, la page anglaise fait foi. Lire la page en anglais
Cette page mène une application de rien jusqu’à une carte sur votre propre compte. Les exemples ci-dessous suivent les mêmes étapes dans chaque langage.
Créer un client OAuth
Une application est un client OAuth doté d’un manifeste : commencez donc par un client. Créez-en un sur la page des clients OAuth de la console et donnez-lui les autorisations API dont votre application aura besoin, comme la lecture des plans de cours. Son identifiant client et son secret permettent à votre serveur d’appeler l’API Lingara en tant que votre application. Conservez-les dans l’environnement de votre serveur.
Construire et importer le manifeste
Le manifeste décrit votre application à Lingara :
son nom et sa description, dans une langue principale, avec des traductions facultatives ;
render_url, l’adresse HTTPS à laquelle Lingara envoie les requêtes ;
les emplacements où sa carte peut apparaître ;
le contexte qu’elle demande à voir, que l’apprenant accepte à l’installation (voir Contexte et consentement ) ;
les autorisations API dont elle a besoin, que son client OAuth doit déjà posséder ;
si elle peut proposer une note au tuteur (voir La note au tuteur ).
Le kit construit le manifeste en JSON et le vérifie selon les règles qu’applique une importation, de sorte qu’une erreur apparaisse d’abord sur votre propre machine. Sur la page Applications de la console, choisissez votre client, faites-en une application et importez le fichier.
Créer un secret de signature
Sur la même page, créez un secret de signature. Il n’est affiché qu’une seule fois. Transmettez-le à votre serveur via son environnement, et ne le commitez jamais dans le contrôle de source. Lingara signe chaque requête vers votre application avec ce secret, et le kit refuse une requête dont la signature ne correspond pas. Une application peut avoir deux secrets à la fois, ce qui vous permet d’en remplacer un sans interruption de service : tant que deux sont actifs, chaque requête est signée avec les deux.
Écrire une fonction de rendu
Votre fonction de rendu reçoit une requête et renvoie une carte. La requête indique l’emplacement, la langue de l’apprenant, le contexte qu’il a accepté de partager, et subject, une valeur stable qui identifie l’apprenant auprès de votre application. Rangez tout ce que vous stockez par apprenant sous subject. Un bouton de votre carte envoie une action, et votre serveur y répond avec une nouvelle carte. La page Cartes liste tout ce qu’une carte peut contenir.
Monter l’adaptateur
L’adaptateur du kit lit la requête brute, vérifie sa signature, appelle votre fonction et écrit la réponse. Servez-le à l’adresse indiquée par le render_url de votre manifeste. Lingara n’appelle que des adresses HTTPS sur l’internet public.
L’installer sur votre propre compte
Sur la page Applications de la console, choisissez d’installer l’application sur votre compte, choisissez ce qu’elle peut voir et confirmez. Ouvrez l’écran des plans de cours sur un grand écran, ou la page d’accueil sur le web, et votre carte apparaît.
Le client OAuth de votre application reçoit aussi des événements, comme un plan de cours qui devient prêt. Le guide des webhooks et des événements, dans la référence de l’API, les décrit.
Langage de l'exemple de code TypeScript Rust Go Java Kotlin Ruby PHP 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 Langage de l'exemple de code TypeScript Rust Go Java Kotlin Ruby PHP 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, 0o 644 ) 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. Langage de l'exemple de code TypeScript Rust Go Java Kotlin Ruby PHP 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 ()]);
},
],
); Langage de l'exemple de code TypeScript Rust Go Java Kotlin Ruby PHP 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 ();