mirror of
https://github.com/edufeed-org/nips.git
synced 2025-12-09 16:24:32 +00:00
fix language specs, remove bloating pseudocode
This commit is contained in:
parent
30aea8c65f
commit
c55b4e503b
1 changed files with 17 additions and 344 deletions
361
edufeed.md
361
edufeed.md
|
|
@ -59,9 +59,8 @@ This is how we convert each property of the AMB:
|
|||
- `description` → `["description", <value>]`
|
||||
- `about` (array of concept objects) → Repeat for each:
|
||||
- `["about:id", <uri>]`
|
||||
- `["about:prefLabel", <label>]`
|
||||
- `["about:prefLabel:lang", <label>]`
|
||||
- `["about:type", "Concept"]`
|
||||
- `["about:inLanguage", <language>]` (if applicable)
|
||||
- `keywords` → `["t", <keyword>]` (repeat for each keyword, using Nostr `t` tag)
|
||||
- `inLanguage` → `["inLanguage", <languageCode>]` (repeat for each language)
|
||||
- `image` → `["image", <uri>]`
|
||||
|
|
@ -106,41 +105,36 @@ This is how we convert each property of the AMB:
|
|||
- `["license:id", <license_uri>]`
|
||||
- `conditionsOfAccess` (Concept object) →
|
||||
- `["conditionsOfAccess:id", <uri>]`
|
||||
- `["conditionsOfAccess:prefLabel", <label>]` (optional)
|
||||
- `["conditionsOfAccess:prefLabel:lang", <label>]` (optional)
|
||||
- `["conditionsOfAccess:type", "Concept"]` (optional)
|
||||
- `["conditionsOfAccess:inLanguage", <language>]` (optional)
|
||||
|
||||
**Educational:**
|
||||
|
||||
- `learningResourceType` (array of Concept objects) → Repeat for each:
|
||||
- `["learningResourceType:id", <uri>]`
|
||||
- `["learningResourceType:prefLabel", <label>]` (optional)
|
||||
- `["learningResourceType:prefLabel:lang", <label>]` (optional)
|
||||
- `["learningResourceType:type", "Concept"]` (optional)
|
||||
- `["learningResourceType:inLanguage", <language>]` (optional)
|
||||
- `audience` (array of Concept objects) → Repeat for each:
|
||||
- `["audience:id", <uri>]`
|
||||
- `["audience:prefLabel", <label>]` (optional)
|
||||
- `["audience:prefLabel:lang", <label>]` (optional)
|
||||
- `["audience:type", "Concept"]` (optional)
|
||||
- `["audience:inLanguage", <language>]` (optional)
|
||||
- `teaches` (array of Concept objects) → Repeat for each:
|
||||
- `["teaches:id", <uri>]`
|
||||
- `["teaches:prefLabel", <label>]` (optional)
|
||||
- `["teaches:prefLabel:lang", <label>]` (optional)
|
||||
- `assesses` (array of Concept objects) → Repeat for each:
|
||||
- `["assesses:id", <uri>]`
|
||||
- `["assesses:prefLabel", <label>]` (optional)
|
||||
- `["assesses:prefLabel:lang", <label>]` (optional)
|
||||
- `competencyRequired` (array of Concept objects) → Repeat for each:
|
||||
- `["competencyRequired:id", <uri>]`
|
||||
- `["competencyRequired:prefLabel", <label>]` (optional)
|
||||
- `["competencyRequired:prefLabel:lang", <label>]` (optional)
|
||||
- `educationalLevel` (array of Concept objects) → Repeat for each:
|
||||
- `["educationalLevel:id", <uri>]`
|
||||
- `["educationalLevel:prefLabel", <label>]` (optional)
|
||||
- `["educationalLevel:prefLabel:lang", <label>]` (optional)
|
||||
- `["educationalLevel:type", "Concept"]` (optional)
|
||||
- `["educationalLevel:inLanguage", <language>]` (optional)
|
||||
- `interactivityType` (Concept object) →
|
||||
- `["interactivityType:id", <uri>]`
|
||||
- `["interactivityType:prefLabel", <label>]` (optional)
|
||||
- `["interactivityType:prefLabel:lang", <label>]` (optional)
|
||||
- `["interactivityType:type", "Concept"]` (optional)
|
||||
- `["interactivityType:inLanguage", <language>]` (optional)
|
||||
|
||||
**Relations:**
|
||||
|
||||
|
|
@ -190,218 +184,6 @@ This is how we convert each property of the AMB:
|
|||
- `["caption:encodingFormat", <format>]` (optional, IANA media type)
|
||||
- `["caption:inLanguage", <languageCode>]` (optional)
|
||||
|
||||
## Conversion Guidelines
|
||||
|
||||
### AMB → Nostr Event (JavaScript)
|
||||
|
||||
```javascript
|
||||
function flattenAMBToNostrTags(ambData) {
|
||||
const tags = [];
|
||||
|
||||
function addTag(key, value) {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
tags.push([key, String(value)]);
|
||||
}
|
||||
}
|
||||
|
||||
function flattenObject(obj, prefix = '') {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const fullKey = prefix ? `${prefix}:${key}` : key;
|
||||
|
||||
// Special case: id maps to 'd' tag
|
||||
if (key === 'id' && !prefix) {
|
||||
addTag('d', value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Special case: keywords map to 't' tags (Nostr-native)
|
||||
if (key === 'keywords' && !prefix && Array.isArray(value)) {
|
||||
value.forEach(keyword => {
|
||||
addTag('t', keyword);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
// Arrays: repeat the same key for each element
|
||||
value.forEach(item => {
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
flattenObject(item, fullKey);
|
||||
} else {
|
||||
addTag(fullKey, item);
|
||||
}
|
||||
});
|
||||
} else if (typeof value === 'object' && value !== null) {
|
||||
// Nested objects: flatten recursively
|
||||
flattenObject(value, fullKey);
|
||||
} else {
|
||||
// Simple values
|
||||
addTag(fullKey, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flattenObject(ambData);
|
||||
return tags;
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
const ambMetadata = {
|
||||
id: "https://example.org/resource/123",
|
||||
name: "Introduction to Physics",
|
||||
creator: [
|
||||
{
|
||||
name: "Dr. Jane Smith",
|
||||
type: "Person",
|
||||
affiliation: {
|
||||
name: "MIT",
|
||||
type: "Organization"
|
||||
}
|
||||
}
|
||||
],
|
||||
keywords: ["Physics", "Education"]
|
||||
};
|
||||
|
||||
const tags = flattenAMBToNostrTags(ambMetadata);
|
||||
// Result (using Nostr-native conventions):
|
||||
// [
|
||||
// ["d", "https://example.org/resource/123"],
|
||||
// ["name", "Introduction to Physics"],
|
||||
// ["creator:name", "Dr. Jane Smith"],
|
||||
// ["creator:type", "Person"],
|
||||
// ["creator:affiliation:name", "MIT"],
|
||||
// ["creator:affiliation:type", "Organization"],
|
||||
// ["t", "Physics"],
|
||||
// ["t", "Education"]
|
||||
// ]
|
||||
//
|
||||
// Note: If creator had Nostr identity, would also include:
|
||||
// ["p", "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "wss://relay.example.com", "creator"]
|
||||
```
|
||||
|
||||
### Nostr Event → AMB Metadata (JavaScript)
|
||||
|
||||
```javascript
|
||||
function unflattenNostrTagsToAMB(tags) {
|
||||
const result = {};
|
||||
|
||||
// Group tags by their key
|
||||
const tagGroups = {};
|
||||
const pTags = []; // Collect p tags separately
|
||||
|
||||
tags.forEach(([key, ...values]) => {
|
||||
// Special case: 'd' tag maps to 'id'
|
||||
if (key === 'd') {
|
||||
result.id = values[0];
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: 'p' tags with 'creator' marker map to creator.id
|
||||
if (key === 'p') {
|
||||
const [pubkey, relay, marker] = values;
|
||||
if (marker === 'creator' || marker === 'contributor') {
|
||||
pTags.push({ pubkey, relay, marker });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: 't' tags map to 'keywords'
|
||||
if (key === 't') {
|
||||
if (!result.keywords) {
|
||||
result.keywords = [];
|
||||
}
|
||||
result.keywords.push(values[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tagGroups[key]) {
|
||||
tagGroups[key] = [];
|
||||
}
|
||||
tagGroups[key].push(values);
|
||||
});
|
||||
|
||||
// Process p tags (creator/contributor)
|
||||
if (pTags.length > 0) {
|
||||
result.creator = pTags.map(({ pubkey }) => ({
|
||||
id: pubkey
|
||||
}));
|
||||
}
|
||||
|
||||
// Process each tag group
|
||||
for (const [key, valuesList] of Object.entries(tagGroups)) {
|
||||
const parts = key.split(':');
|
||||
|
||||
// Multiple occurrences of same key = array
|
||||
if (valuesList.length > 1) {
|
||||
// Check if this is an object array (has nested properties)
|
||||
const hasNestedProps = Object.keys(tagGroups).some(k =>
|
||||
k.startsWith(key + ':')
|
||||
);
|
||||
|
||||
if (hasNestedProps) {
|
||||
// Array of objects - reconstruct each object
|
||||
const arrayResult = [];
|
||||
valuesList.forEach(values => {
|
||||
const obj = {};
|
||||
setNestedValue(obj, parts, values[0]);
|
||||
arrayResult.push(obj);
|
||||
});
|
||||
setNestedValue(result, parts, arrayResult);
|
||||
} else {
|
||||
// Simple array
|
||||
setNestedValue(result, parts, valuesList.map(v => v[0]));
|
||||
}
|
||||
} else {
|
||||
// Single occurrence
|
||||
setNestedValue(result, parts, valuesList[0][0]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function setNestedValue(obj, parts, value) {
|
||||
const lastPart = parts[parts.length - 1];
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < parts.length - 1; i++) {
|
||||
if (!current[parts[i]]) {
|
||||
current[parts[i]] = {};
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
|
||||
current[lastPart] = value;
|
||||
}
|
||||
|
||||
// Example usage:
|
||||
const nostrTags = [
|
||||
["d", "https://example.org/resource/123"],
|
||||
["name", "Introduction to Physics"],
|
||||
["p", "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "wss://relay.example.com", "creator"],
|
||||
["creator:affiliation:name", "MIT"],
|
||||
["creator:affiliation:type", "Organization"],
|
||||
["t", "Physics"],
|
||||
["t", "Education"]
|
||||
];
|
||||
|
||||
const ambMetadata = unflattenNostrTagsToAMB(nostrTags);
|
||||
// Result:
|
||||
// {
|
||||
// id: "https://example.org/resource/123",
|
||||
// name: "Introduction to Physics",
|
||||
// creator: {
|
||||
// // Note: p tag is decoded to get the pubkey
|
||||
// id: "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
|
||||
// affiliation: {
|
||||
// name: "MIT",
|
||||
// type: "Organization"
|
||||
// }
|
||||
// },
|
||||
// keywords: ["Physics", "Education"]
|
||||
// }
|
||||
```
|
||||
|
||||
## How to convert an AMB nostr-event to AMB metadata
|
||||
|
||||
To convert a Nostr event back to AMB metadata:
|
||||
|
|
@ -416,93 +198,6 @@ To convert a Nostr event back to AMB metadata:
|
|||
- Convert string booleans to actual booleans
|
||||
- Parse ISO8601 dates if needed for validation
|
||||
|
||||
### Reconstruction Algorithm
|
||||
|
||||
```javascript
|
||||
function reconstructAMB(nostrEvent) {
|
||||
const amb = {
|
||||
"@context": [
|
||||
"https://w3id.org/kim/amb/context.jsonld",
|
||||
{ "@language": "de" }
|
||||
],
|
||||
type: ["LearningResource"]
|
||||
};
|
||||
|
||||
// Group tags by their base key (before first ':')
|
||||
const tagsByBase = {};
|
||||
nostrEvent.tags.forEach(tag => {
|
||||
const [key, ...values] = tag;
|
||||
const baseKey = key.split(':')[0];
|
||||
|
||||
if (!tagsByBase[baseKey]) {
|
||||
tagsByBase[baseKey] = [];
|
||||
}
|
||||
tagsByBase[baseKey].push({ fullKey: key, values });
|
||||
});
|
||||
|
||||
// Reconstruct each property
|
||||
for (const [baseKey, tags] of Object.entries(tagsByBase)) {
|
||||
if (baseKey === 'd') {
|
||||
amb.id = tags[0].values[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if this is a simple property or nested
|
||||
const hasNested = tags.some(t => t.fullKey.includes(':'));
|
||||
|
||||
if (!hasNested) {
|
||||
// Simple property
|
||||
if (tags.length === 1) {
|
||||
amb[baseKey] = tags[0].values[0];
|
||||
} else {
|
||||
// Multiple values = array
|
||||
amb[baseKey] = tags.map(t => t.values[0]);
|
||||
}
|
||||
} else {
|
||||
// Nested property - group by instance
|
||||
amb[baseKey] = reconstructNestedProperty(tags);
|
||||
}
|
||||
}
|
||||
|
||||
return amb;
|
||||
}
|
||||
|
||||
function reconstructNestedProperty(tags) {
|
||||
// Group tags that belong to the same array instance
|
||||
const instances = [];
|
||||
let currentInstance = {};
|
||||
let lastDepth = 0;
|
||||
|
||||
tags.forEach(tag => {
|
||||
const parts = tag.fullKey.split(':');
|
||||
const depth = parts.length;
|
||||
|
||||
// If depth decreased, we're starting a new instance
|
||||
if (depth <= lastDepth && Object.keys(currentInstance).length > 0) {
|
||||
instances.push(currentInstance);
|
||||
currentInstance = {};
|
||||
}
|
||||
|
||||
// Build nested structure
|
||||
let current = currentInstance;
|
||||
for (let i = 1; i < parts.length - 1; i++) {
|
||||
if (!current[parts[i]]) {
|
||||
current[parts[i]] = {};
|
||||
}
|
||||
current = current[parts[i]];
|
||||
}
|
||||
current[parts[parts.length - 1]] = tag.values[0];
|
||||
|
||||
lastDepth = depth;
|
||||
});
|
||||
|
||||
if (Object.keys(currentInstance).length > 0) {
|
||||
instances.push(currentInstance);
|
||||
}
|
||||
|
||||
return instances.length === 1 ? instances[0] : instances;
|
||||
}
|
||||
```
|
||||
|
||||
## How to query for AMB nostr-events in supporting relays
|
||||
|
||||
|
|
@ -532,17 +227,14 @@ Query capabilities should include:
|
|||
["name", "Pythagorean Theorem Video"],
|
||||
["description", "An introductory video explaining the Pythagorean theorem"],
|
||||
["about:id", "http://w3id.org/kim/schulfaecher/s1017"],
|
||||
["about:prefLabel", "Mathematik"],
|
||||
["about:prefLabel:de", "Mathematik"],
|
||||
["about:type", "Concept"],
|
||||
["about:inLanguage", "de"],
|
||||
["about:id", "http://w3id.org/kim/schulfaecher/s1005"],
|
||||
["about:prefLabel", "Deutsch"],
|
||||
["about:prefLabel:de", "Deutsch"],
|
||||
["about:type", "Concept"],
|
||||
["about:inLanguage", "de"],
|
||||
["learningResourceType:id", "http://w3id.org/openeduhub/vocabs/new_lrt/7a6e9608-2554-4981-95dc-47ab9ba924de"],
|
||||
["learningResourceType:prefLabel", "Video"],
|
||||
["learningResourceType:prefLabel:de", "Video"],
|
||||
["learningResourceType:type", "Concept"],
|
||||
["learningResourceType:inLanguage", "de"],
|
||||
["t", "Pythagoras"],
|
||||
["t", "Geometrie"],
|
||||
["t", "Mathematik"],
|
||||
|
|
@ -587,15 +279,15 @@ Query capabilities should include:
|
|||
["dateCreated", "2024-01-15"],
|
||||
["datePublished", "2024-02-01"],
|
||||
["about:id", "https://w3id.org/kim/hochschulfaechersystematik/n079"],
|
||||
["about:prefLabel", "Informatik"],
|
||||
["about:prefLabel:de", "Informatik"],
|
||||
["about:type", "Concept"],
|
||||
["learningResourceType:id", "https://w3id.org/kim/hcrt/course"],
|
||||
["learningResourceType:prefLabel", "Course"],
|
||||
["learningResourceType:prefLabel:de", "Kurs"],
|
||||
["audience:id", "http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/student"],
|
||||
["audience:prefLabel", "student"],
|
||||
["audience:prefLabel:de", "Student"],
|
||||
["audience:type", "Concept"],
|
||||
["educationalLevel:id", "https://w3id.org/kim/educationalLevel/level_06"],
|
||||
["educationalLevel:prefLabel", "Bachelor or equivalent"],
|
||||
["educationalLevel:prefLabel:en", "Bachelor or equivalent"],
|
||||
["inLanguage", "en"],
|
||||
["license:id", "https://creativecommons.org/licenses/by-sa/4.0/"],
|
||||
["isAccessibleForFree", "true"]
|
||||
|
|
@ -607,17 +299,6 @@ Query capabilities should include:
|
|||
|
||||
## Tools
|
||||
|
||||
### Decoding Nostr Identifiers with njump
|
||||
|
||||
[`njump`](https://njump.me/) is a helpful tool for decoding and navigating Nostr identifiers, particularly useful when working with `p` tags (pubkeys) and `a` tags (addressable events):
|
||||
|
||||
- **For `p` tag pubkeys**: Paste the hex pubkey or npub identifier at `https://njump.me/<pubkey-or-npub>` to view the profile and see all resources published by that user
|
||||
- Example: `https://njump.me/79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798`
|
||||
- Or: `https://njump.me/npub1...`
|
||||
|
||||
- **For `a` tag addressable events**: Use `https://njump.me/naddr1...` to view the specific event
|
||||
- Example: `https://njump.me/naddr1qqqgcjsyup9qqqg6cjv` to navigate to an addressable resource
|
||||
|
||||
### Using `nak` to create AMB events
|
||||
|
||||
You can use [`nak`](https://github.com/fiatjaf/nak) to create AMB events with the flattened tag structure:
|
||||
|
|
@ -631,9 +312,8 @@ nak event \
|
|||
--tag name="Pythagorean Theorem Video" \
|
||||
--tag description="An introductory video" \
|
||||
--tag about:id="http://w3id.org/kim/schulfaecher/s1017" \
|
||||
--tag about:prefLabel="Mathematik" \
|
||||
--tag about:prefLabel:de="Mathematik" \
|
||||
--tag about:type="Concept" \
|
||||
--tag about:inLanguage="de" \
|
||||
--tag t="Pythagoras" \
|
||||
--tag t="Geometrie" \
|
||||
--tag inLanguage="de" \
|
||||
|
|
@ -648,13 +328,6 @@ nak event \
|
|||
--tag creator:affiliation:name="MIT" \
|
||||
--tag creator:affiliation:type="Organization"
|
||||
|
||||
# Example 3: Resource with relation to another AMB event (using a tag)
|
||||
nak event \
|
||||
--kind 30142 \
|
||||
--tag d="https://example.org/resource/789" \
|
||||
--tag name="Physics Module Part 1" \
|
||||
--tag a="30142:79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798:physics-course;wss://relay.example.com;isPartOf"
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue