ssb-profile
v7.0.2
Published
A helper module for reading and writing to "profile threads" in scuttlebutt
Downloads
82
Readme
ssb-profile
An secret-stack
plugin for creating, reading, updating profiles in scuttlebutt
Example Usage
const Stack = require('secret-stack')
const caps = require('ssb-caps')
const Config = require('ssb-config/inject')
const config = Config({})
const ssb = Stack({ caps })
.use(require('ssb-db'))
+ .use(require('ssb-backlinks')) // required
+ .use(require('ssb-query')) // required
+ .use(require('ssb-profile'))
.use(require('ssb-tribes')) // (optonal) support for private messages
.call(null, config)
const details = {
preferredName: 'Ben',
avatarImage: {
blob: '&CLbw5B9d5+H59oxDNOy4bOkwIaOhfLfqOLm1MGKyTLI=.sha256',
mimeType: 'image/png'
}
}
ssb.profile.person.public.create(details, (err, profileId) => {
// ...
})
// later:
ssb.profile.person.public.get(profileId, (err, profile) => {
// ...
})
// or:
const update = {
preferredName: 'Ben Tairea',
}
ssb.profile.person.public.update(profileId, update, (err, updateMsg) => {
// ...
})
Requirements
secret-stack
instance running the following plugins:
ssb-db2/core
ssb-classic
ssb-db2/compat
ssb-db2/compat/publish
ssb-db2/compat/feedstate
ssb-box2
ssb-tribes
API
NOTE - all update
methods currently auto-resolve any branch conflicts for you (if they exist)
The "winner" for conflicting fields is chosen from the tip that was most recently updated.
Profiles for people:
ssb.profile.person.source.*
- has every field
- recps must be
[group]
ssb.profile.person.group.*
- excludes:
[phone, address, email]
- recps must be
[group]
- excludes:
ssb.profile.person.admin.*
- same as
source
, but admins can post updates to it too. - recps must be
[poBoxId, feedId]
(for someone sending something to admins who wants to be part of updates) OR[groupId]
(for something that is one-way to admins/ admin-only)
- same as
ssb.profile.person.public.*
- only:
[preferredName, avatarImage]
- no recps
- only:
graph TB
subgraph Personal group
source
end
public(public)
subgraph Family group
group(group)
subgraph kaitiaki group
admin(admin)
end
end
source-..->public
source-..->group
source-..->admin
This graph show how Āhau uses these profiles. Dotted lines show how updates to the source profile are propogate to the others.
Profiles for communities
ssb.profile.community.public.*
- public community profilessb.profile.community.group.*
- encrypted community profile
Profiles for pataka
- `ssb.profile.pataka.public*- public pataka profile
Person profile (PUBLIC)
Handles public facing (unencrypted) profiles of type profile/person
.
ssb.profile.person.public
.create(details, cb)
.get(profileId, cb)
.update(profileId, details, cb)
.tombstone(profileId, details, cb)
Here details
is an Object which allows:
{
authors: {
add: [Author] // required on .create
remove: [Author]
},
preferredName: String,
gender: Gender,
source: ProfileSource,
avatarImage: Image,
tombstone: Tombstone
// allowPublic: Booelan // if using ssb-recps-guard
}
NOTES:
authors
is a special field which defines permissions for updates- you must set
authors.add
when creating a record
- you must set
- This type is deliberatly quite limited, to avoid accidental sharing of private data.
- All fields (apart from
authors
) can also be set tonull
- See below for types.
Person group profile
Handles encrypted profiles of type profile/person
.
ssb.profile.person.group
.create(details, cb)
.get(profileId, cb)
.update(profileId, details, cb)
.tombstone(profileId, details, cb)
.findAdminProfileLinks(groupProfileId, opts, cb)
(see below)
Here details
is an Object:
{
recps: [Recp], // required
authors: {
add: [Author] // required on .create
remove: [Author]
},
preferredName: String,
legalName: String,
altNames: {
add: [String],
remove: [String]
},
avatarImage: Image,
headerImage: Image,
description: String,
gender: Gender,
source: ProfileSource,
aliveInterval: EdtfIntervalString,
deceased: Boolean,
placeOfBirth: String,
placeOfDeath: String,
buriedLocation: String,
birthOrder: Int,
profession: String,
education: [String], // overwrites last Array of Strings
school: [String], // overwrites last Array of Strings
address: String,
city: String,
country: String,
postCode: String,
phone: String,
email: String,
customFields: [CustomField]
tombstone: Tombstone
}
NOTES:
authors
is a special field which defines permissions for updates- you must set
authors.add
when creating a record
- you must set
recps
is required when creating, but updates copy the initialrecps
- All fields (apart from
authors
,altNames
) can also be set tonull
CustomField
{ [key]: value } - A custom field is a field on a persons profile which can have a value of multiple types. The person profile will use what is defined on the community profiles to provide its own value for that fieldkey
UnixTimevalue
String | [String] | Boolean | EdtfDate | Number | Blob | [Blob]
- See below for Types
Community profile (PUBLIC)
Handles public facing (unencrypted) profiles of type profile/community
.
ssb.profile.community.public
.create(details, cb)
.get(profileId, cb)
.update(profileId, details, cb)
.tombstone(profileId, details, cb)
Here details
is an Object which allows:
{
authors: {
add: [Author] // required on .create
remove: [Author],
},
preferredName: String,
description: String,
avatarImage: Image,
headerImage: Image,
address: String,
city: String,
country: String,
postCode: String,
phone: String,
email: String,
// these two fields are only on public community profiles
joiningQuestions: CustomForm,
customFields: CustomFields
tombstone: Tombstone,
poBoxId: POBoxId // public part of the poBoxId for a subgroup
// allowPublic: Boolean // if using ssb-recps-guard
}
NOTES:
authors
is a special field which defines permissions for updates- you must set
authors.add
when creating a record
- you must set
- All fields (apart from
authors
) can also be set tonull
POBoxId
is aString
cipherlink that can be used in recps by anyone, to send messages only those with the secret key can opencustomFields
are defined on the public community profile and then you use those definitions for what you fill in on the person profile- See below for Types
Community profile (GROUP)
Handles encrypted profiles of type profile/community
and is for use within a group.
ssb.profile.community.group
.create(details, cb)
.get(profileId, cb)
.update(profileId, details, cb)
.tombstone(profileId, details, cb)
Here details
is an Object of form:
{
recps: [Recp], // required
authors: {
add: [Author] // required on .create
remove: [Author],
},
preferredName: String,
description: String,
avatarImage: Image,
headerImage: Image,
address: String,
city: String,
country: String,
postCode: String,
phone: String,
email: String,
// private settings
// only on the group community profile
allowWhakapapaViews: Boolean,
allowPersonsList: Boolean,
allowStories: Boolean,
// public settings
// only on the public community profile
acceptsVerifiedCredentials: Boolean,
issuesVerifiedCredentials: Boolean,
tombstone: Tombstone,
poBoxId: POBoxId
}
NOTES:
recps
is required when creating, but updates copy the initialrecps
authors
is a special field which defines permissions for updates- you must set
authors.add
when creating a record
- you must set
- All fields (apart from
authors
) can also be set tonull
POBoxId
is aString
cipherlink that can be used in recps by anyone, to send messages only those with the secret key can open- See below for Types
How get
methods work
Because there might be multiple offline edits to a profile which didn't know bout one-another, it's possible for divergence to happen:
A (the root message)
|
B (an edit after A)
/ \
C D (two concurrent edits after B)
profile
is an Object which maps the key of a each latest edit to the state
it perceives the profile to be in! So for that prior example:
// profile
{
key: MessageId, // the root message of the profile tangle, aka profileId
type: ProfileType,
recps: [Recp], // recipients (will be null on public records)
originalAuthor: FeedId
...state, // the best guess of the current state of each field
states: [ // (advanced) in depth detail about the state of all tips
{ key: C, ...state }, //
{ key: D, ...state },
],
conflictFields: [String] // a list of any fields which are in conflict
}
where
recps
is the private recipients who can access the profilestates
[State] - the one / multiple states in which the profile is in:- these are sorted from most to least recent edit (by asserted publishedDate on the last update message)
key
MessageId is the key of the message which is the most recent editstate
is an object which shows what the state of the profile is (from the perspective of a person standing at that particular "head")- e.g. for some Public Person profile, it might look like:
// State { type: 'person' // added to help you out authors: { '@xIP5FV16FwPUiIZ0TmINhoCo4Hdx6c4KQcznEDeWtWg=.ed25519': [ { start: 203, end: Integer } ] }, preferredName: 'Ben Tairea', gender: 'male', source: 'ahau', tombstone: null // all profile fields are present, are "null" if unset }
Fields which get reduced:
authors
returns a collection of authors, and "intervals" for which that author was active- these are sequence numbers from the authors feed (unless the author is
"*"
in which case it's a time-stamp)
- these are sequence numbers from the authors feed (unless the author is
altNames
returns an Array of names (ordered is not guarenteed)
ssb.profile.link.create(profileId, opts, cb)
where
profileId
MessageId is the profile you're creating a link toopts
Object (optional) allows you to tune the link:opts.feedId
FeedId if provided creates alink/feed-profile
with provided feedId instead of current ssb instance's feedIdopts.groupId
GroupId creates alink/group-profile
opts.profileId
MsgId creates alink/profile-profile/admin
(setprofileId
to be the group profile,opts.profileId
to be the admin profile)opts.allowPublic
Boolean (optional) - if you havessb-recps-guard
installed and want to bypass it for a public (unencrypted) link
cb
Function - callback with signature(err, link)
wherelink
is the link message
Note:
- if you link to a private profile, the link will be encrypted to the same
recps
as that profile - if you provide
opts.feedId
andopts.groupId
you will get an error
Find methods
ssb.profile.find(opts, cb)
Arguments:
opts
Object - an options object with properties:opts.name
String - a name (or fragment of) that could be part of apreferredName
orlegalName
oraltNames
opts.type
String (optional)- if set, method will only return profiles of given type
- Valid types:
'person'
'person/admin'
'person/source'
'community'
'pataka'
null
- if set tonull
, will return all types
- default:
'person'
- opts.groupId String (optional)
- only returns results encrypted to a particular group
- if it's a GroupId, and that group has a poBoxId, profiles encrypted to both are included
- id it's a POBoxId, then just profiles encrypted to that P.O. Box will be included
opts.includeTombstoned
Boolean (optional) - whether to include profiles which habe been tombstoned (default:false
)
cb
Function - a callback with signature(err, suggestions)
wheresuggestions
is an array of Profiles
ssb.profile.findByFeedId(feedId, cb)
Takes a feedId
and calls back with all profiles which that feedId
has linked to it.
Signature of cb is cb(err, profiles)
where profiles
is of form:
{
public: [Profile],
private: [Profile]
}
NOTE:
- profiles which have been tombstoned are not included in results
- profiles are ordered from oldest to newest in terms of when they were linked to the
feedId
- advanced :
ssb.profile.findByFeedId(feedId, opts, cb)
opts.getProfile
- provide your own getter. signaturegetProfile(profileId, cb)
- callback with
cb(null, null)
if you want to exclude a result - useful if you want to add a cache to your getter, or only allow certain types of profile
- callback with
opts.groupId
GroupId - only return profiles that exist in a particular private groupopts.sortPublicPrivate
Boolean - whether to sort into{ public, private }
- default:
true
- if
false
returns an Array of profiles
- default:
opts.selfLinkOnly
Boolean - only include profiles where thelink
message was authored by thefeedId
- default:
true
- if
false
, public and private groupings are further split intoself
andother
:{ self: { public: [Profile], private: [Profile] }, other: { public: [Profile], private: [Profile] } }
- if
false
you get profiles that anyone has linked to that feedId,- WARNING links asserted by others could be malicious
- if you trust your context this can be a useful fallback
- default:
ssb.profile.findByGroupId(groupId, cb)
Takes a groupId
and calls back with all profiles which that feedId
has linked to it.
Signature of cb is cb(err, profiles)
where profiles
is of form:
{
public: [Profile],
private: [Profile]
}
NOTE:
- profiles which have been tombstoned are not included in results
- profiles are ordered from oldest to newest in terms of when they were linked to the
feedId
- advanced you can call this with
ssb.profile.findByGroupId(feedId, opts, cb)
opts.getProfile
- provide your own getter. signaturegetProfile(profileId, cb)
- callback with
cb(null, null)
if you want to exclude a result - useful if you want to add a cache to your getter, or only allow certain types of profile
- callback with
ssb.profile.findFeedsByProfileId(profileId, cb)
Takes a profileId
and calls back with all the feedIds which that profileId
has linked to it.
Signature of cb is cb(err, feeds)
where feeds
is of form:
[FeedId, FeedId, ...]
NOTE:
- advanced :
ssb.profile.findFeedsByProfile(profileId, opts, cb)
opts.selfLinkOnly
Boolean - only include profiles where thelink
message was authored by thefeedId
- default:
true
- if
false
returns results in format:{ self: [FeedId, ...], // feeds that have link themselves to the profile other: [FeedId, ...] // feeds that another person has linked to the profile }
- default:
- alias
ssb.profile.findFeedsByProfile
ssb.profile.person.group.findAdminProfileLinks(profileId, opts, cb)
Takes a profileId
(person group profileId) and calls back with the parentLinks
and childLinks
which that profileId
has linked to it.
Signature of cb is cb(err, links)
where links
is of form:
{
parentLinks: [Link],
childLinks: [Link]
}
and Link
is:
{
key: MsgId,
type: 'link/profile-profile/admin',
parent: MsgId,
child: MsgId,
states: [{ key: MsgId, tombstone: Tombstone }]
originalAuthor: FeedId,
recps: [GroupId]
}
Types
Author
String aFeedId
or"*"
(i.e. any user)- any updates that arent from a valid author are classed as invalid and will be ignored when using the get method
Recp
String a "recipient", usually aFeedId
orGroupId
- the record will be encrypted so only that recipient(s) can access the record
- requires
ssb-tribes
to be installed as a plugin
Image
Object:{ blob: Blob, // the content address for the blob (with prefex &) mimeType: String, // mimetype of the image unbox: UnboxKey, // (optional) a String for unboxing the blob if encrypted size: Number, // (optional) size of the image in bytes width: Number, // (optional) width of image in pixels height: Number // (optional) height of image in pixels }
Gender
String (male|female|other|unknown)ProfileSource
String (ahau|webForm). AProfileSource
is an enum explaining where this profile came from e.g.ahau
- it was created inahau
.webForm
- it was created using awebForm
EdtfIntervalString
- see edtf module and library of congress specTombstone
Object{ date: UnixTime, // an Integer indicating microseconds from 1st Jan 1970, can be negative! reason: String // (optional) }
UnixTime
Integer microseconds since 00:00 1st Jan 1970 (can be negative, read more)CustomForm
[FormField] - used generate custom form for people applying to join a community. e.g[ { type: 'input', label: 'Who introduced you?' }, { type: 'textarea', label: 'Please tell use about yourself' }, ]
FormField
Object of shape:{ type: FieldType, // String: input|textarea label: String }
CustomFields
{ [key]: CustomFieldDef } - defines the custom fields that person profiles within the group will usekey
UnixTime (see example above)CustomFieldDef
Object of shape{ type: String, // text|array|list|checkbox|file label: String, order: Number, required: Boolean, visibleBy: String, // members|admin // NOTE: these fields are used when type=list options: [String], multiple: Boolean // NOTE: these fields are used when type=file fileTypes: [String] // document|video|audio|photo description: String, // a helpful descriptions may be needed when uploading files multiple: Boolean }
- Valid types
text
string valuearray
multiple response valuelist
value containing one or more values from the defined optionscheckbox
boolean valuefile
blob values to store files
- Valid types
Blob
Object - the blob object for the uploaded media, see ssb-blobs and ssb-hyper-blobs
Record types
graph TB
%% ssb.profile
%% cipherlinks
feedId(feedId)
groupId(groupId)
%% public profiles
personPublic[profile/person]
communityPublic[profile/community]
%% public links
linkPersonPublic([link/feed-profile])
linkCommunityPublic([link/group-profile])
%% pataka[profile/pataka]
subgraph group
communityGroup[profile/community]
personGroup[profile/person<br/>]
%% links encrypted to the group
linkPersonGroup([link/feed-profile])
linkCommunityGroup([link/group-profile])
linkPersonPersonAdmin([link/profile-profile/admin])
subgraph admin
personAdmin[profile/person/admin]
%% links encrypted to the admins
linkPersonAdmin([link/feed-profile])
end
end
%% connecting links
feedId -..-> linkPersonPublic -..-> personPublic
feedId -.-> linkPersonGroup -.-> personGroup
feedId -.-> linkPersonAdmin -.-> personAdmin
personAdmin -.-> linkPersonPersonAdmin -.-> personGroup
groupId -..-> linkCommunityPublic -..-> communityPublic
groupId -..-> linkCommunityGroup -..-> communityGroup
%% styling
classDef default fill:#990098, stroke:purple, stroke-width:1, color:white, font-family:sans, font-size:14px;
classDef cluster fill:#1fdbde55, stroke:#1fdbde;
classDef path stroke: blue;
classDef encrypted fill:#ffffffaa, stroke:purple, stroke-width:1, color:black, font-family:sans, font-size:14px;
classDef cipherlink fill:#0000ff33, stroke:purple, stroke-width:0, color:#00f, font-family:sans, font-size: 14px;
class personGroup,personAdmin,communityGroup,linkPersonGroup,linkPersonAdmin,linkCommunityGroup,linkPersonPersonAdmin encrypted;
class feedId,groupId cipherlink
Note - you only have link/profile-profile/admin
for "unowned" profile (i.e. no link/feed-profile
is present)
FAQ
I want to delete my legalName, how do?
- first, know that if you previously published a legalName it will always be part of your record (even if it's not currently displayed)
- if you want to clear a text field, just publish an update with null value:
{ legalName: null }
How do I clear an image?
- same as with legalName - set it to null
Multiple editors for a profile?
- work in progress!
- currently supports multiple writers, but does not support merging of branched state
- by default,
.update
extends the most recent branch
- by default,
Development
Project layout (made with tree
):
.
├── index.js // ssb-server plugin (collects all methods)
├── method // user facing methods
├── spec // describes message + how to reduce them
│ ├── person
│ │ ├── source
│ │ ├── group
│ │ ├── admin
│ │ └── private
│ ├── community
│ │ ├── group
│ │ └── public
│ ├── pataka
│ │
│ ├── link
│ │ ├── feed-profile
│ │ ├── group-profile
│ │ └── profile-profile-admin
│ └── lib
│
└── test // tests!
run npm test
to run tests