Compare commits

...

2 commits

Author SHA1 Message Date
@s.roertgen
30aea8c65f update amb nip 2025-10-27 21:42:28 +01:00
@s.roertgen
19dddd09ce Remove language tag as it is not required in AMB 2025-03-31 17:34:14 +02:00

View file

@ -2,7 +2,7 @@
## Abstract ## Abstract
This NIP defines how to handle the metadata profile ["Allgemeines Metadatenprofil für Bildugnsressourcen" (AMB)](https://dini-ag-kim.github.io/amb/latest/) in nostr: This NIP defines how to handle the metadata profile ["Allgemeines Metadatenprofil für Bildungsressourcen" (AMB)](https://dini-ag-kim.github.io/amb/latest/) in nostr:
- How to convert AMB metadata to an AMB nostr event - How to convert AMB metadata to an AMB nostr event
- How to convert an AMB nostr-event to AMB metadata - How to convert an AMB nostr-event to AMB metadata
@ -11,87 +11,514 @@ This NIP defines how to handle the metadata profile ["Allgemeines Metadatenprofi
## Event Kind ## Event Kind
This NIP defines `kind:30142` as an AMB Metadata Event. This NIP defines `kind:30142` as an AMB Metadata Event.
This means this is a replacable event, that can be adressed using `kind:pubkey:d-tag`. This means this is a replaceable event, that can be addressed using `kind:pubkey:d-tag`.
## How to convert AMB metadata *to* an AMB nostr event ## How to convert AMB metadata *to* an AMB nostr event
The transformation is quite straightforward. The transformation uses JSON-flattening with `:` as the delimiter to convert nested AMB metadata structures into flat Nostr tags. Additionally, Nostr-native tag conventions are used where applicable for better interoperability and query efficiency.
For the attributes of the AMB we use tags.
For attributes expecting values of controlled vocabularies, we use this scheme: ### Nostr-Native Conventions
`[<attribute_name>, <id> <prefLabel>, <languageCode>, <type>]` This NIP follows Nostr conventions where they align with AMB requirements:
For properties that expect arrays, each value is represented as a separate tag with the same key. - **`t` tags**: Used for keywords/topics (instead of flattened `keywords` tags)
- **`p` tags**: Used for creator/contributor references when the creator has a Nostr identity (pubkey), with fallback to flattened structure for non-Nostr identifiers
- **`a` tags**: Used for references to other addressable events on Nostr (including other AMB events), with fallback to flattened URIs for external resources
### Flattening Rules
1. **Simple properties**: Map directly to tags
- AMB: `{"name": "Resource Title"}`
- Nostr: `["name", "Resource Title"]`
2. **Nested objects**: Flatten using `:` delimiter
- AMB: `{"creator": {"name": "John", "id": "123"}}`
- Nostr: `["creator:name", "John"]`, `["creator:id", "123"]`
3. **Arrays**: Repeat the same flattened tag key (order is preserved by tag array position)
- AMB: `{"keywords": ["Math", "Physics"]}`
- Nostr: `["t", "Math"]`, `["t", "Physics"]` (Nostr-native `t` tag)
4. **Arrays of objects**: Repeat flattened keys for each object
- AMB: `{"creator": [{"name": "John"}, {"name": "Jane"}]}`
- Nostr: `["creator:name", "John"]`, `["creator:name", "Jane"]`
5. **Deep nesting**: Continue flattening with additional `:` delimiters
- AMB: `{"creator": {"affiliation": {"name": "MIT"}}}`
- Nostr: `["creator:affiliation:name", "MIT"]`
### Property Mappings
This is how we convert each property of the AMB: This is how we convert each property of the AMB:
General: **General:**
- `id`: `["d", <id>]` (we use nostr's `d`-tag here as identifier) - `id``["d", <id>]` (special case: use Nostr's `d` tag as identifier)
- `type`: `["type", <type1>, <type2>, ...]` - `type``["type", <value>]` (repeat for multiple types)
- `name`: `["name", <name>]` - `name``["name", <value>]`
- `description`: `["description", <description>, <languageCode>]` (language optional, not specified in AMB) - `description``["description", <value>]`
- `about`: `["about", <id>, <prefLabel>, <language>,]` - `about` (array of concept objects) → Repeat for each:
- `keywords`: `["keywords", <keyword1>, <keyword2>, ...]` - `["about:id", <uri>]`
- `inLanguage`: `["inLanguage", <languageCode1>, <languageCode2>, ...]` - `["about:prefLabel", <label>]`
- `image`: `["image", <image>]` - `["about:type", "Concept"]`
- `trailer`: `["image", <contentUrl>, <type>, <encodingFormat>, <contentSize>, <sha256>, <embedUrl>, <bitRate>]` - `["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>]`
- `trailer` (MediaObject) →
- `["trailer:contentUrl", <url>]`
- `["trailer:type", <"VideoObject"|"AudioObject">]`
- `["trailer:encodingFormat", <format>]` (optional)
- `["trailer:contentSize", <bytes>]` (optional)
- `["trailer:sha256", <hash>]` (optional)
- `["trailer:embedUrl", <url>]` (optional)
- `["trailer:bitrate", <kbps>]` (optional)
Provenience: **Provenance:**
- `creator`: `["creator", <id>, <name>, <type>, <affiliationName>, <affiliationType>, <affiliationId>]` - `creator` (array of Person/Organization objects) → Repeat for each:
- `contributor`: `["contributor", <id>, <name>, <type>, <affiliationName>, <affiliationType>, <affiliationId>]` - **Nostr-native (if creator has Nostr pubkey)**: `["p", <npub-hex>, <relay>, "creator"]`
- `dateCreated`: `["dateCreated", <ISO8601Date>]` - **Fallback (for non-Nostr identifiers)**:
- `datePublished`: `["datePublished", <ISO8601Date>]` - `["creator:id", <uri>]` (optional, e.g., ORCID, GND)
- `dateModified`: `["dateModified", <ISO8601Date>]` - `["creator:name", <name>]`
- `publisher`: `["publisher", <id>, <name>, <type>]` - `["creator:type", <"Person"|"Organization">]`
- `funder`: `["funder", <id>, <name>, <type>]` - `["creator:honorificPrefix", <title>]` (optional, for persons)
- `["creator:affiliation:id", <uri>]` (optional)
- `["creator:affiliation:name", <name>]` (optional)
- `["creator:affiliation:type", "Organization"]` (optional)
- `contributor` (array of Person/Organization objects) → Same structure as `creator`
- `dateCreated``["dateCreated", <ISO8601Date>]`
- `datePublished``["datePublished", <ISO8601Date>]`
- `dateModified``["dateModified", <ISO8601Date>]`
- `publisher` (array of Organization/Person objects) → Repeat for each:
- `["publisher:id", <uri>]` (optional)
- `["publisher:name", <name>]`
- `["publisher:type", <"Organization"|"Person">]`
- `funder` (array of Person/Organization/FundingScheme objects) → Repeat for each:
- `["funder:id", <uri>]` (optional)
- `["funder:name", <name>]`
- `["funder:type", <"Person"|"Organization"|"FundingScheme">]`
Costs And Rights: **Costs and Rights:**
- `isAccessibleForFree`: `["isAccessibleForFree", <isAccessibleForFree>]` - `isAccessibleForFree``["isAccessibleForFree", <"true"|"false">]`
- `license`: `["license", <license_uri>, <license_shortName>]` (`license_shortName` optional, not specified in AMB) - `license` (object) →
- `conditionsOfAccess`: `["conditionsOfAccess", <id>, <prefLabel>,, <language> ]` - `["license:id", <license_uri>]`
- `conditionsOfAccess` (Concept object) →
- `["conditionsOfAccess:id", <uri>]`
- `["conditionsOfAccess:prefLabel", <label>]` (optional)
- `["conditionsOfAccess:type", "Concept"]` (optional)
- `["conditionsOfAccess:inLanguage", <language>]` (optional)
Educational: **Educational:**
- `learningResourceType`: `["learningResourceType", <id>, <label>, <language>]` - `learningResourceType` (array of Concept objects) → Repeat for each:
- `audience`: `["audience", <id>, <prefLabel>, <language>]` - `["learningResourceType:id", <uri>]`
- `teaches`: `["teaches", <id>, <prefLabel>, <language>]` - `["learningResourceType:prefLabel", <label>]` (optional)
- `assesses`: `["assesses", <id>, <prefLabel>, <language>]` - `["learningResourceType:type", "Concept"]` (optional)
- `competencyRequired`: `["competencyRequired", <id>, <prefLabel>, <language>]` - `["learningResourceType:inLanguage", <language>]` (optional)
- `educationalLevel`: `["educationalLevel", <id>, <prefLabel>, <language>]` - `audience` (array of Concept objects) → Repeat for each:
- `interactivityType`: `["interactivityType", <id>, <prefLabel>, <language>]` - `["audience:id", <uri>]`
- `["audience:prefLabel", <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)
- `assesses` (array of Concept objects) → Repeat for each:
- `["assesses:id", <uri>]`
- `["assesses:prefLabel", <label>]` (optional)
- `competencyRequired` (array of Concept objects) → Repeat for each:
- `["competencyRequired:id", <uri>]`
- `["competencyRequired:prefLabel", <label>]` (optional)
- `educationalLevel` (array of Concept objects) → Repeat for each:
- `["educationalLevel:id", <uri>]`
- `["educationalLevel:prefLabel", <label>]` (optional)
- `["educationalLevel:type", "Concept"]` (optional)
- `["educationalLevel:inLanguage", <language>]` (optional)
- `interactivityType` (Concept object) →
- `["interactivityType:id", <uri>]`
- `["interactivityType:prefLabel", <label>]` (optional)
- `["interactivityType:type", "Concept"]` (optional)
- `["interactivityType:inLanguage", <language>]` (optional)
Relations: **Relations:**
- `isBasedOn` `["isBasedOn", <id>, <name>]` (additional attributes unsupported right now) - `isBasedOn` (array of objects) → Repeat for each:
- `isPartOf`: `["isPartOf", <id>, <name>, <type>]` - **Nostr-native (if referenced resource is addressable AMB event)**: `["a", "30142:<pubkey>:<d-value>", <relay>, "isBasedOn"]`
- `hasPart`: `["hasPart", <id>, <name>, <type>]` - **Fallback (for external URIs)**:
- `["isBasedOn:id", <uri>]`
- `["isBasedOn:name", <name>]` (optional)
- `isPartOf` (array of objects) → Repeat for each:
- **Nostr-native (if referenced resource is addressable AMB event)**: `["a", "30142:<pubkey>:<d-value>", <relay>, "isPartOf"]`
- **Fallback (for external URIs)**:
- `["isPartOf:id", <uri>]`
- `["isPartOf:name", <name>]` (optional)
- `["isPartOf:type", <type>]` (optional)
- `hasPart` (array of objects) → Repeat for each:
- **Nostr-native (if referenced resource is addressable AMB event)**: `["a", "30142:<pubkey>:<d-value>", <relay>, "hasPart"]`
- **Fallback (for external URIs)**:
- `["hasPart:id", <uri>]`
- `["hasPart:name", <name>]` (optional)
- `["hasPart:type", <type>]` (optional)
Meta-Metadata: **Meta-Metadata:**
- unsupported right now - `mainEntityOfPage` (array of WebPage objects) → Repeat for each:
- `["mainEntityOfPage:id", <uri>]`
- `["mainEntityOfPage:type", "WebContent"]`
- `["mainEntityOfPage:provider:id", <uri>]` (optional)
- `["mainEntityOfPage:provider:name", <name>]` (optional)
- `["mainEntityOfPage:provider:type", <type>]` (optional)
- `["mainEntityOfPage:dateCreated", <ISO8601Date>]` (optional)
- `["mainEntityOfPage:dateModified", <ISO8601Date>]` (optional)
Technical: **Technical:**
- `duration`: `["duration", <duration>]` - `duration``["duration", <ISO8601Duration>]` (format: PnYnMnDTnHnMnS)
- `encoding` (unsupported right now) - `encoding` (array of MediaObject objects) → Repeat for each:
- `caption`: `["caption", <id>, <encodingFormat>, <language>, <type>]` - `["encoding:type", "MediaObject"]`
- `["encoding:contentUrl", <url>]` (or use `embedUrl`)
- `["encoding:embedUrl", <url>]` (or use `contentUrl`)
- `["encoding:encodingFormat", <format>]` (optional, IANA media type)
- `["encoding:contentSize", <bytes>]` (optional)
- `["encoding:sha256", <hash>]` (optional)
- `["encoding:bitrate", <kbps>]` (optional)
- `caption` (array of MediaObject objects) → Repeat for each:
- `["caption:id", <uri>]`
- `["caption:type", "MediaObject"]`
- `["caption:encodingFormat", <format>]` (optional, IANA media type)
- `["caption:inLanguage", <languageCode>]` (optional)
## Conversion Guidelines
## How to convert an AMB nostr-event to AMB metadata. ### AMB → Nostr Event (JavaScript)
TODO ```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;
}
## How to query for AMB nostr-events in supporting relays. // 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"]
};
TODO 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:
1. **Extract tags**: Get the `tags` array from the Nostr event
2. **Group by prefix**: Collect all tags that share the same prefix (before the first `:`)
3. **Reconstruct nesting**: Use the `:` delimiter to rebuild nested object structure
4. **Handle arrays**: Multiple tags with identical keys become array elements
5. **Preserve order**: Array order is determined by tag order in the event
6. **Special mappings**:
- `d` tag → `id` property
- 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
TODO: This section will be completed once relay support is implemented.
Query capabilities should include:
- Filter by `kind:30142` for all AMB events
- Filter by author (`pubkey`)
- Filter by specific subjects using `#about:id` tag
- Filter by learning resource type using `#learningResourceType:id` tag
- Filter by educational level using `#educationalLevel:id` tag
- Full-text search in `name` and `description` tags
## Examples ## Examples
### Example 1 ### Example 1: Simple Educational Resource
```json ```json
{ {
@ -100,67 +527,138 @@ TODO
"pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
"created_at": 1743419457, "created_at": 1743419457,
"tags": [ "tags": [
[ ["d", "oersi.org/resources/aHR0cHM6Ly9hdi50aWIuZXUvbWVkaWEvNjY5ODM=11"],
"name", ["type", "LearningResource"],
"hello world" ["name", "Pythagorean Theorem Video"],
], ["description", "An introductory video explaining the Pythagorean theorem"],
[ ["about:id", "http://w3id.org/kim/schulfaecher/s1017"],
"description", ["about:prefLabel", "Mathematik"],
"noch eine beschreibung", ["about:type", "Concept"],
"de" ["about:inLanguage", "de"],
], ["about:id", "http://w3id.org/kim/schulfaecher/s1005"],
[ ["about:prefLabel", "Deutsch"],
"d", ["about:type", "Concept"],
"oersi.org/resources/aHR0cHM6Ly9hdi50aWIuZXUvbWVkaWEvNjY5ODM\\=11" ["about:inLanguage", "de"],
], ["learningResourceType:id", "http://w3id.org/openeduhub/vocabs/new_lrt/7a6e9608-2554-4981-95dc-47ab9ba924de"],
[ ["learningResourceType:prefLabel", "Video"],
"about", ["learningResourceType:type", "Concept"],
"http://w3id.org/kim/schulfaecher/s1017", ["learningResourceType:inLanguage", "de"],
"Mathematik", ["t", "Pythagoras"],
"de" ["t", "Geometrie"],
], ["t", "Mathematik"],
[ ["inLanguage", "de"],
"about", ["license:id", "https://creativecommons.org/licenses/by/4.0/"],
"http://w3id.org/kim/schulfaecher/s1005", ["isAccessibleForFree", "true"]
"Deutsch",
"de"
],
[
"learningResourceType",
"http://w3id.org/openeduhub/vocabs/new_lrt/7a6e9608-2554-4981-95dc-47ab9ba924de",
"Video",
"de"
],
[
"keywords",
"Pythagoras",
"Geometrie",
"neu"
],
[
"inLanguage",
"de"
]
], ],
"content": "", "content": "",
"sig": "6b0b78d56dea322864d35ea3b6d7e892d0e62bed96cd11ecb27d6c1d0b6d0cd68cd9ec82419946a5fb3c8d4a21eca88c9a5dad47a3b3e466ba18787224a613ef" "sig": "6b0b78d56dea322864d35ea3b6d7e892d0e62bed96cd11ecb27d6c1d0b6d0cd68cd9ec82419946a5fb3c8d4a21eca88c9a5dad47a3b3e466ba18787224a613ef"
} }
``` ```
## Tools ### Example 2: Resource with Creator and Affiliation
To create "Example 1" you could for example use [`nak`](https://github.com/fiatjaf/nak) ```json
{
```bash "kind": 30142,
nak event \ "id": "7ca749b4897efdc98fe2803dc60f68c9e1cd29764e8a55d1e9ef47a46ba4fe75",
--k 30142 \ "pubkey": "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
--t name="hello world" \ "created_at": 1743419500,
--t description="noch eine beschreibung;de" \ "tags": [
--t d="oersi.org/resources/aHR0cHM6Ly9hdi50aWIuZXUvbWVkaWEvNjY5ODM\=11" \ ["d", "https://example.org/courses/physics-101"],
--t about="http://w3id.org/kim/schulfaecher/s1017;Mathematik;de" \ ["type", "LearningResource"],
--t about="http://w3id.org/kim/schulfaecher/s1005;Deutsch;de" \ ["type", "Course"],
--t learningResourceType="http://w3id.org/openeduhub/vocabs/new_lrt/7a6e9608-2554-4981-95dc-47ab9ba924de;Video;de" \ ["name", "Introduction to Physics"],
--t keywords="Pythagoras;Geometrie;neu" \ ["description", "A comprehensive introduction to classical mechanics"],
--tag inLanguage="de" ["p", "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", "wss://relay.example.com", "creator"],
["creator:id", "https://orcid.org/0000-0001-2345-6789"],
["creator:name", "Dr. Jane Smith"],
["creator:type", "Person"],
["creator:honorificPrefix", "Dr."],
["creator:affiliation:id", "https://ror.org/example123"],
["creator:affiliation:name", "Massachusetts Institute of Technology"],
["creator:affiliation:type", "Organization"],
["p", "2c4b52e2a4f7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c", "wss://relay.example.com", "creator"],
["creator:id", "https://orcid.org/0000-0009-8765-4321"],
["creator:name", "Prof. John Doe"],
["creator:type", "Person"],
["creator:honorificPrefix", "Prof."],
["creator:affiliation:name", "Stanford University"],
["creator:affiliation:type", "Organization"],
["dateCreated", "2024-01-15"],
["datePublished", "2024-02-01"],
["about:id", "https://w3id.org/kim/hochschulfaechersystematik/n079"],
["about:prefLabel", "Informatik"],
["about:type", "Concept"],
["learningResourceType:id", "https://w3id.org/kim/hcrt/course"],
["learningResourceType:prefLabel", "Course"],
["audience:id", "http://purl.org/dcx/lrmi-vocabs/educationalAudienceRole/student"],
["audience:prefLabel", "student"],
["audience:type", "Concept"],
["educationalLevel:id", "https://w3id.org/kim/educationalLevel/level_06"],
["educationalLevel:prefLabel", "Bachelor or equivalent"],
["inLanguage", "en"],
["license:id", "https://creativecommons.org/licenses/by-sa/4.0/"],
["isAccessibleForFree", "true"]
],
"content": "",
"sig": "8d1c89f5da33ec9a2b456def78a90b1cd23e456f78a90b12cd34e567f89a012b34c56d78e9f0a12bc3d45e6f78901a23b45c67d89e0f1a2b3c4d5e6f7890123a"
}
``` ```
## 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:
```bash
# Example 1: Simple resource with Nostr-native t tags
nak event \
--kind 30142 \
--tag d="oersi.org/resources/example123" \
--tag type="LearningResource" \
--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:type="Concept" \
--tag about:inLanguage="de" \
--tag t="Pythagoras" \
--tag t="Geometrie" \
--tag inLanguage="de" \
--tag license:id="https://creativecommons.org/licenses/by/4.0/"
# Example 2: Resource with creator (with Nostr identity p tag)
nak event \
--kind 30142 \
--tag d="https://example.org/resource/456" \
--tag name="Physics Course" \
--tag p="79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798;wss://relay.example.com;creator" \
--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
- [AMB Specification](https://dini-ag-kim.github.io/amb/latest/)
- [Nostr Protocol (NIP-01)](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [Addressable Events (NIP-33)](https://github.com/nostr-protocol/nips/blob/master/33.md)
- [JSON-Flattening Concept](https://localizely.com/json-flattener/)