Γρήγορη εκκίνηση Αυτή η σελίδα έχει μεταφραστεί από τα αγγλικά. Αν οι δύο διαφέρουν, η αγγλική σελίδα είναι η σωστή. Διαβάστε την αγγλική σελίδα
Αυτή η σελίδα οδηγεί μια εφαρμογή από το μηδέν μέχρι μια κάρτα στον δικό σας λογαριασμό. Τα παρακάτω δείγματα ακολουθούν τα ίδια βήματα σε κάθε γλώσσα.
Δημιουργία πελάτη OAuth
Μια εφαρμογή είναι ένας πελάτης OAuth με ένα μανιφέστο, οπότε ξεκινήστε με έναν πελάτη. Δημιουργήστε έναν στη σελίδα «Πελάτες OAuth» της κονσόλας και δώστε του τις δυνατότητες API που θα χρειαστεί η εφαρμογή σας, όπως την ανάγνωση σχεδίων μαθημάτων. Το αναγνωριστικό πελάτη και το μυστικό του επιτρέπουν στον διακομιστή σας να καλεί το Lingara API ως η εφαρμογή σας. Κρατήστε τα στο περιβάλλον του διακομιστή σας.
Δημιουργία και μεταφόρτωση του μανιφέστου
Το μανιφέστο περιγράφει την εφαρμογή σας στο Lingara:
το όνομα και την περιγραφή της, σε μια κύρια γλώσσα, με προαιρετικές μεταφράσεις·
render_url, τη διεύθυνση HTTPS στην οποία το Lingara στέλνει αιτήματα·
τις θέσεις στις οποίες μπορεί να εμφανιστεί η κάρτα της·
το πλαίσιο που ζητά να δει, στο οποίο συναινεί ο μαθητής κατά την εγκατάσταση (δείτε Πλαίσιο και συναίνεση )·
τις δυνατότητες API που χρειάζεται, τις οποίες πρέπει να έχει ήδη ο πελάτης OAuth της·
αν μπορεί να προσφέρει μια σημείωση στον δάσκαλο (δείτε Η σημείωση για τον δάσκαλο ).
Το κιτ δημιουργεί το μανιφέστο ως JSON και το ελέγχει με τους κανόνες που εφαρμόζει μια μεταφόρτωση, ώστε ένα λάθος να φανεί πρώτα στον δικό σας υπολογιστή. Στη σελίδα «Εφαρμογές» της κονσόλας, επιλέξτε τον πελάτη σας, κάντε τον εφαρμογή και μεταφορτώστε το αρχείο.
Δημιουργία μυστικού υπογραφής
Στην ίδια σελίδα, δημιουργήστε ένα μυστικό υπογραφής. Εμφανίζεται μία φορά. Δώστε το στον διακομιστή σας μέσω του περιβάλλοντός του και μην το καταχωρίσετε ποτέ στο σύστημα ελέγχου εκδόσεων. Το Lingara υπογράφει με αυτό κάθε αίτημα προς την εφαρμογή σας, και το κιτ απορρίπτει ένα αίτημα του οποίου η υπογραφή δεν ταιριάζει. Μια εφαρμογή μπορεί να έχει δύο μυστικά ταυτόχρονα, ώστε να μπορείτε να αντικαταστήσετε το ένα χωρίς διακοπή λειτουργίας: όσο είναι ενεργά δύο, κάθε αίτημα υπογράφεται και με τα δύο.
Συγγραφή συνάρτησης render
Η συνάρτηση render σας λαμβάνει ένα αίτημα και επιστρέφει μια κάρτα. Το αίτημα αναφέρει τη θέση, τη γλώσσα του μαθητή, το πλαίσιο που συμφώνησε να μοιραστεί, και το subject, μια σταθερή τιμή που ταυτοποιεί τον μαθητή για την εφαρμογή σας. Κρατήστε ό,τι αποθηκεύετε ανά μαθητή κάτω από το subject. Ένα κουμπί στην κάρτα σας στέλνει μια ενέργεια, και ο διακομιστής σας απαντά σε αυτή με μια νέα κάρτα. Η σελίδα κάρτες απαριθμεί όλα όσα μπορεί να περιέχει μια κάρτα.
Σύνδεση του προσαρμογέα
Ο προσαρμογέας του κιτ διαβάζει το ακατέργαστο αίτημα, ελέγχει την υπογραφή του, καλεί τη συνάρτησή σας και γράφει την απάντηση. Εξυπηρετήστε τον στη διεύθυνση που ονομάζει το render_url του μανιφέστου σας. Το Lingara καλεί μόνο διευθύνσεις HTTPS στο δημόσιο διαδίκτυο.
Εγκατάσταση στον δικό σας λογαριασμό
Στη σελίδα «Εφαρμογές» της κονσόλας, επιλέξτε να εγκαταστήσετε την εφαρμογή στον λογαριασμό σας, διαλέξτε τι μπορεί να δει και επιβεβαιώστε. Ανοίξτε την οθόνη σχεδίων μαθημάτων σε μια πλατιά οθόνη, ή την αρχική σελίδα στον ιστό, και η κάρτα σας εμφανίζεται.
Ο πελάτης OAuth της εφαρμογής σας λαμβάνει επίσης συμβάντα, όπως όταν ένα σχέδιο μαθήματος γίνεται έτοιμο. Ο οδηγός για webhooks και συμβάντα στην αναφορά API τα περιγράφει.
Γλώσσα παραδείγματος κώδικα 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 Γλώσσα παραδείγματος κώδικα 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. Γλώσσα παραδείγματος κώδικα 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 ()]);
},
],
); Γλώσσα παραδείγματος κώδικα 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 ();