Add classes for activity vocabulary. Migrate actor to use them.

This commit is contained in:
Feufochmar 2020-04-04 18:34:45 +02:00
parent 0c42dbb2f8
commit 6c1ac941eb
5 changed files with 964 additions and 117 deletions

View File

@ -60,16 +60,15 @@ const Render = {
audienceActor: function(actor) {
var display = '<section style="display:inline-block;">'
if (actor.valid) {
const icon = actor.info.icon ? actor.info.icon : Icons.fallback['user']
display = display + '<img src="' + icon + '" width="32" height="32" /> '
display = display + '<img src="' + actor.iconUrl(Icons.fallback['user']) + '" width="32" height="32" /> '
+ '<p style="display:inline-block;"><strong>' + actor.displayName() + '</strong> <br/>'
+ '<a href="' + actor.urls.profile + '">'
+ '<a href="' + actor.data.id + '">'
+ actor.address()
+ '</a></p>'
} else {
display = display + '<img src="' + Icons.fallback['user'] + '" width="32" height="32" /> '
+ '<p style="display:inline-block;">'
+ '<a href="' + actor.urls.profile + '">'
+ '<a href="' + actor.data.id + '">'
+ 'Other actor'
+ '</a></p>'
}
@ -138,7 +137,7 @@ const UI = {
'my-inbox': function() {
UI.updateNav('inbox-selector')
if (UI.is_connected) {
UI.showTimeline(ConnectedUser.actor.urls.inbox, ConnectedUser.tokens.user.access_token)
UI.showTimeline(ConnectedUser.actor.data.inbox, ConnectedUser.tokens.user.access_token)
UI.showPage('show-profile', ConnectedUser.actor)
} else {
UI.showTimeline(undefined, undefined)
@ -148,7 +147,7 @@ const UI = {
'my-outbox': function() {
UI.updateNav('outbox-selector')
if (UI.is_connected) {
UI.showTimeline(ConnectedUser.actor.urls.outbox, ConnectedUser.tokens.user.access_token)
UI.showTimeline(ConnectedUser.actor.data.outbox, ConnectedUser.tokens.user.access_token)
UI.showPage('show-profile', ConnectedUser.actor)
} else {
UI.showTimeline(undefined, undefined)
@ -167,8 +166,8 @@ const UI = {
'other-profile': function() {
UI.updateNav(undefined)
UI.showPage('show-profile', UI.other_actor)
if (UI.other_actor.urls.outbox) {
UI.showTimeline(UI.other_actor.urls.outbox, undefined)
if (UI.other_actor.data.outbox) {
UI.showTimeline(UI.other_actor.data.outbox, undefined)
} else {
UI.displayError('Actor does not have a public outbox.')
UI.showTimeline(undefined, undefined)
@ -182,24 +181,22 @@ const UI = {
},
'ask-password': function(_) {
Elem('connect-password').value = ''
const icon = ConnectedUser.actor.info.icon ? ConnectedUser.actor.info.icon : Icons.fallback['user']
Elem('ask-password-user-icon').innerHTML = '<img src="' + icon + '" width="32" height="32" />'
Elem('ask-password-user-icon').innerHTML = '<img src="' + ConnectedUser.actor.iconUrl(Icons.fallback['user']) + '" width="32" height="32" />'
Elem('ask-password-user-display-name').innerText = ConnectedUser.actor.displayName()
Elem('ask-password-user-address').href = ConnectedUser.actor.urls.profile
Elem('ask-password-user-address').href = ConnectedUser.actor.data.id
Elem('ask-password-user-address').innerText = ConnectedUser.actor.address()
},
'show-profile': function(actor) {
// data contains the actor to display
const icon = actor.info.icon ? actor.info.icon : Icons.fallback['user']
Elem('profile-icon').innerHTML = '<img src="' + icon + '" width="96" height="96" />'
Elem('profile-icon').innerHTML = '<img src="' + actor.iconUrl(Icons.fallback['user']) + '" width="96" height="96" />'
Elem('profile-display-name').innerText = actor.displayName()
Elem('profile-address').href = actor.urls.profile
Elem('profile-address').href = actor.data.id
Elem('profile-address').innerText = actor.address()
Elem('profile-type').innerText = actor.info.type
Elem('profile-summary').innerHTML = actor.info.summary
Elem('profile-code-source').innerText = JSON.stringify(actor.raw, null, 1)
Elem('profile-type').innerText = actor.data.type
Elem('profile-summary').innerHTML = actor.data.summary
Elem('profile-code-source').innerText = JSON.stringify(actor.data._raw, null, 1)
// Controls only shown if the actor is the connected user
if (actor.urls.profile === ConnectedUser.actor.urls.profile) {
if (actor.data.id === ConnectedUser.actor.data.id) {
Elem('profile-controls-connected').style.display = 'block';
} else {
Elem('profile-controls-connected').style.display = 'none';
@ -209,11 +206,10 @@ const UI = {
// data contains the activity to display
Elem('activity-type').innerText = activity.type
Elem('activity-published').innerText = activity.published ? activity.published.toLocaleString() : ''
const icon = activity.actor.info.icon ? activity.actor.info.icon : Icons.fallback['user']
Elem('activity-actor-icon').innerHTML = '<img src="' + icon + '" width="48" height="48" />'
Elem('activity-actor-icon').innerHTML = '<img src="' + activity.actor.iconUrl(Icons.fallback['user']) + '" width="48" height="48" />'
Elem('activity-actor-display-name').innerText = activity.actor.displayName()
Elem('activity-actor-address').innerText = activity.actor.address()
Elem('activity-actor-address').href = activity.actor.urls.profile
Elem('activity-actor-address').href = activity.actor.data.id
Elem('activity-to').innerHTML = activity.to.map(
function(element) {
return '<li class="actor-display">' + Render.audienceActor(element) + '</li>'
@ -228,11 +224,10 @@ const UI = {
Elem('activity-object').style.display = 'block'
Elem('activity-object-type').innerText = activity.object.type
Elem('activity-object-published').innerText = activity.object.published ? activity.object.published.toLocaleString() : ''
const icon = activity.object.actor.info.icon ? activity.object.actor.info.icon : Icons.fallback['user']
Elem('activity-object-actor-icon').innerHTML = '<img src="' + icon + '" width="48" height="48" />'
Elem('activity-object-actor-icon').innerHTML = '<img src="' + activity.object.actor.iconUrl(Icons.fallback['user']) + '" width="48" height="48" />'
Elem('activity-object-actor-display-name').innerText = activity.object.actor.displayName()
Elem('activity-object-actor-address').innerText = activity.object.actor.address()
Elem('activity-object-actor-address').href = activity.object.actor.urls.profile
Elem('activity-object-actor-address').href = activity.object.actor.data.id
Elem('activity-object-to').innerHTML = activity.object.to.map(
function(element) {
return '<li class="actor-display">' + Render.audienceActor(element) + '</li>'
@ -268,14 +263,14 @@ const UI = {
function(element) {
return '<li class="actor-display">'
+ Render.audienceActor(element)
+ ' <button style="vertical-align:top;" onclick="UI.removeToRecipient(\'' + element.urls.profile + '\')">×</button>'
+ ' <button style="vertical-align:top;" onclick="UI.removeToRecipient(\'' + element.data.id + '\')">×</button>'
+ '</li>'
}).join('')
Elem('send-message-cc').innerHTML = UI.composed_message.cc.map(
function(element) {
return '<li class="actor-display">'
+ Render.audienceActor(element)
+ ' <button style="vertical-align:top;" onclick="UI.removeCcRecipient(\'' + element.urls.profile + '\')">×</button>'
+ ' <button style="vertical-align:top;" onclick="UI.removeCcRecipient(\'' + element.data.id + '\')">×</button>'
+ '</li>'
}).join('')
}
@ -452,7 +447,7 @@ const UI = {
removeToRecipient: function(url_profile) {
// Don't fetch the actor, only set the profile url used in removal
const actor = new Actor()
actor.urls.profile = url_profile
actor.data.id = url_profile
UI.composed_message.removeToRecipient(actor)
UI.showPage('send-message', undefined)
},
@ -460,7 +455,7 @@ const UI = {
removeCcRecipient: function(url_profile) {
// Don't fetch the actor, only set the profile url used in removal
const actor = new Actor()
actor.urls.profile = url_profile
actor.data.id = url_profile
UI.composed_message.removeCcRecipient(actor)
UI.showPage('send-message', undefined)
},

887
src/activity-vocabulary.js Normal file
View File

@ -0,0 +1,887 @@
// Implementation of Activity Vocabulary, with extension for ActivityPub
// https://www.w3.org/TR/activitystreams-vocabulary
// https://www.w3.org/TR/activitypub
// Note: their prototypes are defined after to allow the definition of ObjectFetcher
// Base for other elements - to put things in common
const ASBase = function() {}
// Core types
const ASObject = function() {}
const ASLink = function() {}
const ASActivity = function() {}
const ASIntransitiveActivity = function() {}
const ASCollection = function() {}
const ASOrderedCollection = function() {}
const ASCollectionPage = function() {}
const ASOrderedCollectionPage = function() {}
// Extension for modelisation of ActivityPub actors
const ASActor = function() {}
// Extended Types
// Activity Types
const ASAccept = function() {}
const ASAdd = function() {}
const ASAnnounce = function() {}
const ASArrive = function() {}
const ASBlock = function() {}
const ASCreate = function() {}
const ASDelete = function() {}
const ASDislike = function() {}
const ASFlag = function() {}
const ASFollow = function() {}
const ASIgnore = function() {}
const ASInvite = function() {}
const ASJoin = function() {}
const ASLeave = function() {}
const ASLike = function() {}
const ASListen = function() {}
const ASMove = function() {}
const ASOffer = function() {}
const ASQuestion = function() {}
const ASReject = function() {}
const ASRead = function() {}
const ASRemove = function() {}
const ASTentativeReject = function() {}
const ASTentativeAccept = function() {}
const ASTravel = function() {}
const ASUndo = function() {}
const ASUpdate = function() {}
const ASView = function() {}
// Actor Types
const ASApplication = function() {}
const ASGroup = function() {}
const ASOrganization = function() {}
const ASPerson = function() {}
const ASService = function() {}
// Object Types
const ASArticle = function() {}
const ASAudio = function() {}
const ASDocument = function() {}
const ASEvent = function() {}
const ASImage = function() {}
const ASNote = function() {}
const ASPage = function() {}
const ASPlace = function() {}
const ASProfile = function() {}
const ASRelationship = function() {}
const ASTombstone = function() {}
const ASVideo = function() {}
// Link Types
const ASMention = function() {}
// Object fetcher
// Convert resources from the resource fetcher into Activity objects
Fetcher = {
// Cache of already accessed objects
knownObjects: {},
// Get an object
// Callback signature is function(load_ok, fetched_object, failure_message)
get: function(id, token, callback) {
if (Fetcher.knownObjects[id]) {
callback(true, Fetcher.knownObjects[id], '')
} else {
Fetcher.refresh(id, token, callback)
}
},
// Refresh a resource
// Callback signature is function(load_ok, fetched_object, failure_message)
refresh: function(id, token, callback) {
const request = new XMLHttpRequest()
request.onreadystatechange = function() {
if (request.readyState == 4 && request.status == 200) {
const answer = JSON.parse(request.responseText)
if (answer) {
// Build the object
const obj = Fetcher.fromJson(answer)
Fetcher.knownObjects[id] = obj
callback(true, obj, undefined)
} else {
callback(false, 'Unexpected answer from server when fetching object.')
console.log(answer)
}
} else if (request.readyState == 4) {
callback(false, 'Server error when fetching object (' + request.status + ').')
}
}
request.open('GET', id, true)
if (token) {
request.setRequestHeader('Authorization', 'Bearer ' + token)
}
request.setRequestHeader('Content-Type', 'application/activity+json')
request.setRequestHeader('Accept', 'application/activity+json')
request.send()
},
// Build from type name
_type_from_name: {
// Core types
'Object': ASObject,
'Link': ASLink,
'Activity': ASActivity,
'IntransitiveActivity': ASIntransitiveActivity,
'Collection': ASCollection,
'OrderedCollection': ASOrderedCollection,
'CollectionPage': ASCollectionPage,
'OrderedCollectionPage': ASOrderedCollectionPage,
'Actor': ASActor,
// Activity Types
'Accept': ASAccept,
'Add': ASAdd,
'Announce': ASAnnounce,
'Arrive': ASArrive,
'Block': ASBlock,
'Create': ASCreate,
'Delete': ASDelete,
'Dislike': ASDislike,
'Flag': ASFlag,
'Follow': ASFollow,
'Ignore': ASIgnore,
'Invite': ASInvite,
'Join': ASJoin,
'Leave': ASLeave,
'Like': ASLike,
'Listen': ASListen,
'Move': ASMove,
'Offer': ASOffer,
'Question': ASQuestion,
'Reject': ASReject,
'Read': ASRead,
'Remove': ASRemove,
'TentativeReject': ASTentativeReject,
'TentativeAccept': ASTentativeAccept,
'Travel': ASTravel,
'Undo': ASUndo,
'Update': ASUpdate,
'View': ASView,
// Actor Types
'Application': ASApplication,
'Group': ASGroup,
'Organization': ASOrganization,
'Person': ASPerson,
'Service': ASService,
// Object Types
'Article': ASArticle,
'Audio': ASAudio,
'Document': ASDocument,
'Event': ASEvent,
'Image': ASImage,
'Note': ASNote,
'Page': ASPage,
'Place': ASPlace,
'Profile': ASProfile,
'Relationship': ASRelationship,
'Tombstone': ASTombstone,
'Video': ASVideo,
// Link Types
'Mention': ASMention,
},
// Add additionnal types
addType: function(name, type) {
Fetcher._type_from_name[name] = type
},
// Build an object of the right type from its json representation
fromJson: function(raw) {
const result = Fetcher._type_from_name[raw.type] ? new (Fetcher._type_from_name[raw.type])() : new ASBase()
result.fromJson(raw)
return result
}
}
// ASBase prototype
ASBase.prototype = {
// Raw representation of the element
_raw: undefined,
// Description of the properties
// Array of properties that do not need to be fetched
// Those are directly copied from/to raw into the object
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ['id', 'type'],
// Array of properties that may need to be fetched
// Those need an additional fetch from raw, and special handling when converting back to raw
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: [ /*empty*/ ],
// Properties
id: undefined, // Id of element
type: undefined, // Type of element
// Methods
// Load from a Json representation
fromJson: function(raw) {
this._raw = raw
// copy everything expected from raw
this._alwaysAvailable.map(function(elem) {
this[elem] = raw[elem]
}.bind(this))
// Copy everything expected from raw, fetchAttribute should be used to retrieve the values when needed
this._mayNeedFetch.map(function(elem) {
this[elem] = raw[elem]
}.bind(this))
},
// Convert to Json representation
updateRaw: function() {
this._raw = {}
// Copy everything of _alwaysAvailable into raw
this._alwaysAvailable.map(function(elem) {
this._raw[elem] = this[elem]
}.bind(this))
// Use getAttributeRawValue for everything of _mayNeedFetch
this._mayNeedFetch.map(function(elem) {
this._raw[elem] = this.getAttributeRawValue(this[elem])
}.bind(this))
},
// Utilitary function to use for updating raw data
// Returns either:
// - val.id if it exists
// - val._raw if val.updateRaw exists, after a call to val.updateRaw()
// - val in other cases
getAttributeRawValue: function(val) {
if (val && val.id) {
return val.id
} else if (val && val.updateRaw) {
val.updateRaw()
return val._raw
} else {
return val
}
},
// For fetching properties
// Fetch a property
// After fromJson has been called, the attributes may not be yet in a usable format, notably those which can be objects
// This method fetch and convert an attribute in a usable format
// Callback signature is function(load_ok, failure_message)
// Note: done this way to avoid fetching all resources in fromJson, but only when they are needed
fetchAttribute: function(attribute_name, token, callback) {
const attribute_value = this[attribute_name]
if (this._alwaysAvailable.includes(attribute_name)) {
// Those attributes are always available in a usable format, no need to fetch
callback(true, undefined)
} else if (!this._mayNeedFetch.includes(attribute_name)) {
// Invalid call: the asked attribute cannot be fetched
callback(false, 'Invalid argument to fetchAttribute: ' + attribute_name + ' cannot be fetched.')
} else if ((typeof attribute_value === undefined) || (typeof attribute_value === null)) {
// Nothing to do
callback(true, undefined)
} else if (Array.isArray(attribute_value)) {
// Attribute is an array of elements. Each element should be converted.
this._fetchAndConvertAllAttributeValue(attribute_value.values(), token, function(load_ok, fetched_value, failure_message) {
// Update value of attribute
this[attribute_name] = fetched_value
callback(load_ok, failure_message)
}.bind(this))
} else if ((typeof attribute_value === 'object') && (attribute_value._raw !== undefined)) {
// Attribute has already been fetched
callback(true, undefined)
} else {
// Convertion needed
this._fetchAndConvertAttributeValue(attribute_value, token, function(load_ok, fetched_value, failure_message) {
// Update value of attribute
this[attribute_name] = fetched_value
callback(load_ok, failure_message)
}.bind(this))
}
},
// Fetch all elements of an array iterator to populate a result array
// Callback signature is function(load_ok, fetched_value, failure_message)
_fetchAndConvertAllAttributeValue: function(iter, token, callback, ret_value, previous_errors) {
const next = iter.next()
if (next.done) {
callback(true, ret_value, (previous_errors === '') ? undefined : previous_errors)
} else {
this._fetchAndConvertAttributeValue(next.value, token, function(load_ok, fetched_value, failure_message) {
var values = ret_value ? ret_value : []
const msg_errors = (previous_errors ? previous_errors : '') + (load_ok ? '' : '<br/>' + failure_message)
if (load_ok) {
values.push(fetched_value)
} else {
// Don't stop at first error, but cumulate error messages
console.log(failure_message)
}
// Next
this._fetchAndConvertAllAttributeValue(iter, token, callback, values, msg_errors)
}.bind(this))
}
},
// Fetch an attribute value
// Callback signature is function(load_ok, fetched_value, failure_message)
_fetchAndConvertAttributeValue: function(attribute_value, token, callback) {
if ((attribute_value === undefined) || (attribute_value === null)) {
// attribute is not present, return it as is
callback(true, attribute_value, undefined)
} else if (typeof attribute_value === 'object') {
// In object format already, as a Json value,
// use Fetcher.fromJson
callback(true, Fetcher.fromJson(attribute_value), undefined)
} else if (typeof attribute_value === 'string') {
// Link => fetch value
Fetcher.get(attribute_value, token, function(load_ok, obj, failure_message) {
if (load_ok) {
callback(true, obj, undefined)
} else {
callback(false, undefined, failure_message)
}
})
} else {
// Should not happen
console.log(attribute_value)
callback(false, undefined, 'Unexpected type of attribute.')
}
},
// Fetch several attributes at the same time
// Callback signature is function(load_ok, failure_message)
fetchAttributeList: function (attribute_lst, token, callback) {
this._fetchAttributeListIter(attribute_lst.values(), token, callback, undefined)
},
//
_fetchAttributeListIter: function (attribute_lst, token, callback, previous_errors) {
const next = attribute_lst.next()
if (next.done) {
callback(true, (previous_errors === '') ? undefined : previous_errors)
} else {
this.fetchAttribute(next.value, token, function(load_ok, failure_message) {
const msg_errors = (previous_errors ? previous_errors : '') + (load_ok ? '' : '<br/>' + failure_message)
if (!load_ok) {
// Don't stop at first error, but cumulate error messages
console.log(failure_message)
}
// Next
this._fetchAttributeListIter(attribute_lst, token, callback, msg_errors)
}.bind(this))
}
}
}
// ASObject prototype
ASObject.prototype = Object.create(ASBase.prototype)
Object.assign(ASObject.prototype,
{
// Attributes
attachment: undefined,
attributedTo: undefined,
audience: undefined,
content: undefined,
context: undefined,
name: undefined,
endTime: undefined,
generator: undefined,
icon: undefined,
image: undefined,
inReplyTo: undefined,
location: undefined,
preview: undefined,
published: undefined,
replies: undefined,
startTime: undefined,
summary: undefined,
tag: undefined,
updated: undefined,
url: undefined,
to: undefined,
bto: undefined,
cc: undefined,
bcc: undefined,
mediaType: undefined,
duration: undefined,
// defined in ActivityPub
source: undefined,
likes: undefined,
shares: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASBase.prototype._alwaysAvailable.concat([
'content', 'name', 'endTime', 'published', 'startTime', 'summary', 'updated', 'url',
'mediaType', 'duration',
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASBase.prototype._mayNeedFetch.concat([
'attachment', 'attributedTo', 'audience', 'context', 'generator', 'icon', 'image', 'inReplyTo',
'location', 'preview', 'replies', 'tag', 'to', 'bto', 'cc', 'bcc', 'source', 'likes', 'shares'
]),
})
ASObject.prototype.constructor = ASObject
// ASLink prototype
ASLink.prototype = Object.create(ASBase.prototype)
Object.assign(ASLink.prototype,
{
// Attributes
href: undefined,
rel: undefined,
mediaType: undefined,
name: undefined,
hreflang: undefined,
height: undefined,
width: undefined,
preview: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASBase.prototype._alwaysAvailable.concat([
'href', 'rel', 'mediaType', 'name', 'hreflang', 'height', 'width',
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASBase.prototype._mayNeedFetch.concat([
'preview',
]),
})
ASLink.prototype.constructor = ASLink
// ASActivity prototype
ASActivity.prototype = Object.create(ASObject.prototype)
Object.assign(ASActivity.prototype,
{
// Attributes
actor: undefined,
object: undefined,
target: undefined,
result: undefined,
origin: undefined,
instrument: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASObject.prototype._alwaysAvailable.concat([
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASObject.prototype._mayNeedFetch.concat([
'actor', 'object', 'target', 'result', 'origin', 'instrument',
]),
})
ASActivity.prototype.constructor = ASActivity
// ASIntransitiveActivity prototype
// Note: the object field is not removed from ASActivity
ASIntransitiveActivity.prototype = Object.create(ASActivity.prototype)
ASIntransitiveActivity.prototype.constructor = ASIntransitiveActivity
// ASCollection prototype
ASCollection.prototype = Object.create(ASObject.prototype)
Object.assign(ASCollection.prototype,
{
// Attributes
totalItems: undefined,
current: undefined,
first: undefined,
last: undefined,
items: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASObject.prototype._alwaysAvailable.concat([
'totalItems',
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASObject.prototype._mayNeedFetch.concat([
'current', 'first', 'last', 'items',
]),
})
ASCollection.prototype.constructor = ASCollection
// ASOrderedCollection prototype
ASOrderedCollection.prototype = Object.create(ASCollection.prototype)
ASOrderedCollection.prototype.constructor = ASOrderedCollection
// ASCollectionPage prototype
ASCollectionPage.prototype = Object.create(ASCollection.prototype)
Object.assign(ASCollectionPage.prototype,
{
// Attributes
partOf: undefined,
next: undefined,
prev: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASCollection.prototype._alwaysAvailable.concat([
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASCollection.prototype._mayNeedFetch.concat([
'partOf', 'next', 'prev',
]),
})
ASCollectionPage.prototype.constructor = ASCollectionPage
// ASOrderedCollectionPage prototype
// Specification says that it inherits from both ASCollectionPage and ASOrderedCollection,
// but ASOrderedCollection does not defines more fields than ASCollection,
// so we only inherit from ASCollectionPage, which already inherits from ASCollection
ASOrderedCollectionPage.prototype = Object.create(ASCollectionPage.prototype)
Object.assign(ASOrderedCollectionPage.prototype,
{
// Attributes
startIndex: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASCollectionPage.prototype._alwaysAvailable.concat([
'startIndex',
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASCollectionPage.prototype._mayNeedFetch.concat([
]),
})
ASOrderedCollectionPage.prototype.constructor = ASOrderedCollectionPage
// ASActor
// Defined from the ActivityPub spec, which adds fields to actor types
ASActor.prototype = Object.create(ASObject.prototype)
Object.assign(ASActor.prototype,
{
// Attributes
inbox: undefined,
outbox: undefined,
following: undefined,
followers: undefined,
liked: undefined,
streams: undefined,
preferredUsername: undefined,
endpoints: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASObject.prototype._alwaysAvailable.concat([
'preferredUsername', 'endpoints',
// Note: endpoints should be in the _mayNeedFetch, but it is usually not represented with a vocabulary object, so no type associated
// However, in the observed implementations, this structure is always provided in the actor
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASObject.prototype._mayNeedFetch.concat([
'inbox', 'outbox', 'following', 'followers', 'liked', 'streams',
]),
})
ASActor.prototype.constructor = ASActor
// Extended Types
// Activity Types
// ASAccept prototype
ASAccept.prototype = Object.create(ASActivity.prototype)
ASAccept.prototype.constructor = ASAccept
// ASTentativeAccept prototype
ASTentativeAccept.prototype = Object.create(ASAccept.prototype)
ASAccept.prototype.constructor = ASAccept
// ASAdd prototype
ASAdd.prototype = Object.create(ASActivity.prototype)
ASAdd.prototype.constructor = ASAdd
// ASArrive prototype
ASArrive.prototype = Object.create(ASIntransitiveActivity.prototype)
ASArrive.prototype.constructor = ASArrive
// ASCreate prototype
ASCreate.prototype = Object.create(ASActivity.prototype)
ASCreate.prototype.constructor = ASCreate
// ASDelete prototype
ASDelete.prototype = Object.create(ASActivity.prototype)
ASDelete.prototype.constructor = ASDelete
// ASFollow prototype
ASFollow.prototype = Object.create(ASActivity.prototype)
ASFollow.prototype.constructor = ASFollow
// ASIgnore prototype
ASIgnore.prototype = Object.create(ASActivity.prototype)
ASIgnore.prototype.constructor = ASIgnore
// ASJoin prototype
ASJoin.prototype = Object.create(ASActivity.prototype)
ASJoin.prototype.constructor = ASJoin
// ASLeave prototype
ASLeave.prototype = Object.create(ASActivity.prototype)
ASLeave.prototype.constructor = ASLeave
// ASLike prototype
ASLike.prototype = Object.create(ASActivity.prototype)
ASLike.prototype.constructor = ASLike
// ASOffer prototype
ASOffer.prototype = Object.create(ASActivity.prototype)
ASOffer.prototype.constructor = ASOffer
// ASInvite prototype
ASInvite.prototype = Object.create(ASOffer.prototype)
ASInvite.prototype.constructor = ASInvite
// ASReject prototype
ASReject.prototype = Object.create(ASActivity.prototype)
ASReject.prototype.constructor = ASReject
// ASTentativeReject prototype
ASTentativeReject.prototype = Object.create(ASReject.prototype)
ASTentativeReject.prototype.constructor = ASTentativeReject
// ASRemove prototype
ASRemove.prototype = Object.create(ASActivity.prototype)
ASRemove.prototype.constructor = ASRemove
// ASUndo prototype
ASUndo.prototype = Object.create(ASActivity.prototype)
ASUndo.prototype.constructor = ASUndo
// ASUpdate prototype
ASUpdate.prototype = Object.create(ASActivity.prototype)
ASUpdate.prototype.constructor = ASUpdate
// ASView prototype
ASView.prototype = Object.create(ASActivity.prototype)
ASView.prototype.constructor = ASView
// ASListen prototype
ASListen.prototype = Object.create(ASActivity.prototype)
ASListen.prototype.constructor = ASListen
// ASRead prototype
ASRead.prototype = Object.create(ASActivity.prototype)
ASRead.prototype.constructor = ASRead
// ASMove prototype
ASMove.prototype = Object.create(ASActivity.prototype)
ASMove.prototype.constructor = ASMove
// ASTravel prototype
ASTravel.prototype = Object.create(ASIntransitiveActivity.prototype)
ASTravel.prototype.constructor = ASTravel
// ASAnnounce prototype
ASAnnounce.prototype = Object.create(ASActivity.prototype)
ASAnnounce.prototype.constructor = ASAnnounce
// ASBlock prototype
ASBlock.prototype = Object.create(ASIgnore.prototype)
ASBlock.prototype.constructor = ASBlock
// ASFlag prototype
ASFlag.prototype = Object.create(ASActivity.prototype)
ASFlag.prototype.constructor = ASFlag
// ASDislike prototype
ASDislike.prototype = Object.create(ASActivity.prototype)
ASDislike.prototype.constructor = ASDislike
// ASQuestion prototype
ASQuestion.prototype = Object.create(ASIntransitiveActivity.prototype)
Object.assign(ASQuestion.prototype,
{
// Attributes
oneOf: undefined,
anyOf: undefined,
closed: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASIntransitiveActivity.prototype._alwaysAvailable.concat([
'closed',
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASIntransitiveActivity.prototype._mayNeedFetch.concat([
'oneOf', 'anyOf',
]),
})
ASQuestion.prototype.constructor = ASQuestion
// Actor Types
// ASApplication prototype
ASApplication.prototype = Object.create(ASActor.prototype)
ASApplication.prototype.constructor = ASApplication
// ASGroup prototype
ASGroup.prototype = Object.create(ASActor.prototype)
ASGroup.prototype.constructor = ASGroup
// ASOrganization prototype
ASOrganization.prototype = Object.create(ASActor.prototype)
ASOrganization.prototype.constructor = ASOrganization
// ASPerson prototype
ASPerson.prototype = Object.create(ASActor.prototype)
ASPerson.prototype.constructor = ASPerson
// ASService prototype
ASService.prototype = Object.create(ASActor.prototype)
ASService.prototype.constructor = ASService
// ASRelationship prototype
ASRelationship.prototype = Object.create(ASObject.prototype)
Object.assign(ASRelationship.prototype,
{
// Attributes
subject: undefined,
object: undefined,
relationship: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASObject.prototype._alwaysAvailable.concat([
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASObject.prototype._mayNeedFetch.concat([
'subject', 'object', 'relationship',
]),
})
ASRelationship.prototype.constructor = ASRelationship
// ASArticle prototype
ASArticle.prototype = Object.create(ASObject.prototype)
ASArticle.prototype.constructor = ASArticle
// ASDocument prototype
ASDocument.prototype = Object.create(ASObject.prototype)
ASDocument.prototype.constructor = ASDocument
// ASAudio prototype
ASAudio.prototype = Object.create(ASDocument.prototype)
ASAudio.prototype.constructor = ASAudio
// ASImage prototype
ASImage.prototype = Object.create(ASDocument.prototype)
ASImage.prototype.constructor = ASImage
// ASVideo prototype
ASVideo.prototype = Object.create(ASDocument.prototype)
ASVideo.prototype.constructor = ASVideo
// ASNote prototype
ASNote.prototype = Object.create(ASObject.prototype)
ASNote.prototype.constructor = ASNote
// ASPage prototype
ASPage.prototype = Object.create(ASDocument.prototype)
ASPage.prototype.constructor = ASPage
// ASEvent prototype
ASEvent.prototype = Object.create(ASObject.prototype)
ASEvent.prototype.constructor = ASEvent
// ASPlace prototype
ASPlace.prototype = Object.create(ASObject.prototype)
Object.assign(ASPlace.prototype,
{
// Attributes
accuracy: undefined,
altitude: undefined,
latitude: undefined,
longitude: undefined,
radius: undefined,
units: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASObject.prototype._alwaysAvailable.concat([
'accuracy', 'altitude', 'latitude', 'longitude',
'radius', 'units',
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASObject.prototype._mayNeedFetch.concat([
]),
})
ASPlace.prototype.constructor = ASPlace
// ASProfile prototype
ASProfile.prototype = Object.create(ASObject.prototype)
Object.assign(ASProfile.prototype,
{
// Attributes
describes: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASObject.prototype._alwaysAvailable.concat([
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASObject.prototype._mayNeedFetch.concat([
'describes'
]),
})
ASProfile.prototype.constructor = ASProfile
// ASTombstone prototype
ASTombstone.prototype = Object.create(ASObject.prototype)
Object.assign(ASTombstone.prototype,
{
// Attributes
formerType: undefined,
deleted: undefined,
// Array of properties that do not need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_alwaysAvailable: ASObject.prototype._alwaysAvailable.concat([
'formerType', 'deleted',
]),
// Array of properties that may need to be fetched
// This array should be overwritten when inheriting prototypes (by completing it)
_mayNeedFetch: ASObject.prototype._mayNeedFetch.concat([
]),
})
ASTombstone.prototype.constructor = ASTombstone
// Link Types
// ASMention prototype
ASMention.prototype = Object.create(ASLink.prototype)
ASMention.prototype.constructor = ASMention
////
// Exported structures
// Fetcher
exports.Fetcher = Fetcher
// Core types
exports.ASObject = ASObject
exports.ASLink = ASLink
exports.ASActivity = ASActivity
exports.ASIntransitiveActivity = ASIntransitiveActivity
exports.ASCollection = ASCollection
exports.ASOrderedCollection = ASOrderedCollection
exports.ASCollectionPage = ASCollectionPage
exports.ASOrderedCollectionPage = ASOrderedCollectionPage
exports.ASActor = ASActor
// Activity types
exports.ASAccept = ASAccept
exports.ASAdd = ASAdd
exports.ASAnnounce = ASAnnounce
exports.ASArrive = ASArrive
exports.ASBlock = ASBlock
exports.ASCreate = ASCreate
exports.ASDelete = ASDelete
exports.ASDislike = ASDislike
exports.ASFlag = ASFlag
exports.ASFollow = ASFollow
exports.ASIgnore = ASIgnore
exports.ASInvite = ASInvite
exports.ASJoin = ASJoin
exports.ASLeave = ASLeave
exports.ASLike = ASLike
exports.ASListen = ASListen
exports.ASMove = ASMove
exports.ASOffer = ASOffer
exports.ASQuestion = ASQuestion
exports.ASReject = ASReject
exports.ASRead = ASRead
exports.ASRemove = ASRemove
exports.ASTentativeReject = ASTentativeReject
exports.ASTentativeAccept = ASTentativeAccept
exports.ASTravel = ASTravel
exports.ASUpdate = ASUpdate
exports.ASView = ASView
// Actor types
exports.ASApplication = ASApplication
exports.ASGroup = ASGroup
exports.ASOrganization = ASOrganization
exports.ASPerson = ASPerson
exports.ASService = ASService
// Object types
exports.ASArticle = ASArticle
exports.ASAudio = ASAudio
exports.ASDocument = ASDocument
exports.ASImage = ASImage
exports.ASNote = ASNote
exports.ASPage = ASPage
exports.ASPlace = ASPlace
exports.ASProfile = ASProfile
exports.ASRelationship = ASRelationship
exports.ASTombstone = ASTombstone
exports.ASVideo = ASVideo
// Link types
exports.ASMention = ASMention

View File

@ -1,42 +1,22 @@
const {Fetcher, ASActor} = require('./activity-vocabulary.js')
// Actor class
// ActorInfo structure
var ActorInfo = function() {}
ActorInfo.prototype = {
display_name: undefined,
summary: undefined,
icon: undefined,
type: undefined
}
// ActorUrls structure
var ActorUrls = function() {}
ActorUrls.prototype = {
profile: undefined,
outbox: undefined,
inbox: undefined,
followers: undefined,
following: undefined,
oauth_token: undefined,
oauth_registration: undefined,
oauth_authorization: undefined
}
// Actor structure
var Actor = function() {
this.info = new ActorInfo()
this.urls = new ActorUrls()
}
// Add additional services around an ASActor
const Actor = function() {}
// Prototype
Actor.prototype = {
// Attributes
name: undefined,
server: undefined,
info: undefined,
urls: undefined,
raw: undefined,
data: undefined, // ASActor
name: undefined, // Name of actor, should correspond to data.preferredUsername if data is present
server: undefined, // Server of the actor, for use of name@server style
valid: false,
// Methods
// Get the name used for display
displayName: function() {
if (this.info.display_name) {
return this.info.display_name
if (this.data && this.data.name) {
return this.data.name
} else if (this.data && this.data.preferredUsername) {
return this.data.preferredUsername
} else {
return this.name
}
@ -45,6 +25,14 @@ Actor.prototype = {
address: function() {
return this.name + '@' + this.server
},
// Get the icon, or a default icon
iconUrl: function(default_icon) {
if (this.data && this.data.icon && this.data.icon.url) {
return this.data.icon.url
} else {
return default_icon
}
},
// Load from a "name@server" address.
// Callback is a function accepting two arguments:
// - a boolean indicating if the loading is complete or in failure,
@ -88,61 +76,37 @@ Actor.prototype = {
request.open('GET', 'https://' + this.server + '/.well-known/webfinger' + '?resource=acct:' + address, true)
request.send()
},
// Indicate if the type is compatible with an actor
isExpectedActorType: function(actorType) {
return (actorType === 'Application') || (actorType === 'Group') || (actorType === 'Organization') || (actorType === 'Person') || (actorType === 'Service')
},
// Load from the link identifying the account
// Callback is a function accepting two arguments:
// - a boolean indicating if the loading is complete or in failure,
// - a string indicating the failure
loadFromProfileUrl: function(profile_url, callback) {
this.urls.profile = profile_url
// Use ActivityPub protocol to get the user infos
const request = new XMLHttpRequest()
request.onreadystatechange = function() {
if (request.readyState == 4 && request.status == 200) {
const answer = JSON.parse(request.responseText)
if (answer && this.isExpectedActorType(answer.type)) {
this.raw = answer
// name and server are previously filled if called from loadFromNameAndServer
if (!this.name) {
this.name = answer.preferredUsername
}
if (!this.server) {
this.server = profile_url.replace('http://', '').replace('https://', '').split(/[/?#]/)[0]
}
if (answer.name) {
this.info.display_name = answer.name
} else {
this.info.display_name = this.name
}
this.info.summary = answer.summary ? answer.summary : ""
this.info.icon = answer.icon ? answer.icon.url : undefined
this.info.type = answer.type
this.urls.outbox = answer.outbox
this.urls.inbox = answer.inbox
this.urls.followers = answer.followers
this.urls.following = answer.following
if (answer.endpoints) {
this.urls.oauth_token = answer.endpoints.oauthTokenEndpoint
this.urls.oauth_registration = answer.endpoints.oauthRegistrationEndpoint
this.urls.oauth_authorization = answer.endpoints.oauthAuthorizationEndpoint
}
this.valid = true
callback(true, undefined)
} else {
callback(false, 'user profile: incorrect response from server')
console.log(answer)
}
} else if (request.readyState == 4) {
callback(false, 'user profile: server error')
// Fetch the activity actor
Fetcher.get(profile_url, undefined, function(load_ok, fetched_actor, failure_message) {
if (load_ok) {
this.data = fetched_actor
// Fetch a few properties
this.data.fetchAttributeList(
['preferredUsername', 'name', 'summary', 'icon', 'endpoints'],
undefined,
function (ok, error) {
if (ok) {
if (!this.name) {
this.name = this.data.preferredUsername
}
if (!this.server) {
this.server = profile_url.replace('http://', '').replace('https://', '').split(/[/?#]/)[0]
}
this.valid = true
callback(true, undefined)
} else {
callback(false, error)
}
}.bind(this))
} else {
callback(false, failure_message)
}
}.bind(this)
request.open('GET', profile_url, true)
request.setRequestHeader('Content-Type', 'application/activity+json')
request.setRequestHeader('Accept', 'application/activity+json')
request.send()
}.bind(this))
},
// Fill the values of actor from fixed data
// Usefull for displaying non-actors appearing in audience fields
@ -150,8 +114,9 @@ Actor.prototype = {
this.valid = true
this.name = name
this.server = server
this.info.display_name = display_name
this.urls.profile = url
this.data = new ASActor()
this.data.id = url
this.data.name = display_name
}
}

View File

@ -101,7 +101,7 @@ const ConnectedUser = {
callback(false, 'OAuth: server error when registering client.')
}
}
request.open('POST', ConnectedUser.actor.urls.oauth_registration, true)
request.open('POST', ConnectedUser.actor.data.endpoints.oauthRegistrationEndpoint, true)
var data = new URLSearchParams()
data.append('client_name', 'AP.Mail client')
data.append('redirect_uris', 'urn:ietf:wg:oauth:2.0:oob')
@ -129,7 +129,7 @@ const ConnectedUser = {
callback(false, 'OAuth: cannot get user tokens')
}
}
request.open('POST', ConnectedUser.actor.urls.oauth_token, true)
request.open('POST', ConnectedUser.actor.data.endpoints.oauthTokenEndpoint, true)
var data = new URLSearchParams()
data.append('client_id', ConnectedUser.tokens.server.client_id)
data.append('client_secret', ConnectedUser.tokens.server.client_secret)

View File

@ -11,10 +11,10 @@ const KnownActors = {
set: function(profile, actor) {
KnownActors.actors[profile] = actor
// Make a false actor for followers
if (actor.urls.followers) {
if (actor.data.followers) {
const followers = new Actor()
followers.fromDummyData(actor.address(), 'followers', 'Followers of ' + actor.displayName(), actor.urls.followers)
KnownActors.actors[actor.urls.followers] = followers;
followers.fromDummyData(actor.address(), 'followers', 'Followers of ' + actor.displayName(), actor.data.followers)
KnownActors.actors[actor.data.followers] = followers;
}
},
// Retrieve an actor
@ -37,7 +37,7 @@ const KnownActors = {
// A representation of "everyone" in Actor class
const publicActor = new Actor()
publicActor.fromDummyData('', 'public', 'Anyone', 'https://www.w3.org/ns/activitystreams#Public')
KnownActors.set(publicActor.urls.profile, publicActor)
KnownActors.set(publicActor.data.id, publicActor)
// Exported structures
exports.KnownActors = KnownActors