Simplified profile code.

This commit is contained in:
Kleissner
2021-09-28 05:39:38 +02:00
parent 9ef6a88512
commit 608dbf6f47
7 changed files with 203 additions and 375 deletions

View File

@@ -24,6 +24,11 @@ Offset Size Info
Tag data record contains only raw data and may be referenced by Tags in File records.
This is a basic embedded way of compression when tags are repetitive in multiple files within the same block.
Profile records:
Offset Size Info
0 2 Type
2 ? Data according to the type
*/
package core
@@ -40,10 +45,10 @@ import (
// RecordTypeX defines the type of the record
const (
RecordTypeProfile = 0 // Profile data about the end user
RecordTypeProfile = 0 // Profile data about the end user.
RecordTypeTagData = 1 // Tag data record to be referenced by one or multiple tags. Only valid in the context of the current block.
RecordTypeFile = 2 // File
RecordTypeDelete = 3 // Delete previous record by ID.
RecordTypeInvalid1 = 3 // Do not use.
RecordTypeCertificate = 4 // Certificate to certify provided information in the blockchain issued by a trusted 3rd party.
RecordTypeContentRating = 5 // Content rating (positive).
RecordTypeContentReport = 6 // Content report (negative).
@@ -247,10 +252,10 @@ func decodeBlockRecords(block *Block) (decoded *BlockDecoded, err error) {
decoded.RecordsDecoded = append(decoded.RecordsDecoded, file)
}
if profile, err := decodeBlockRecordProfile(block.RecordsRaw); err != nil {
if profileFields, err := decodeBlockRecordProfile(block.RecordsRaw); err != nil {
return nil, err
} else if profile != nil {
decoded.RecordsDecoded = append(decoded.RecordsDecoded, *profile)
} else if len(profileFields) > 0 {
decoded.RecordsDecoded = append(decoded.RecordsDecoded, profileFields)
}
return decoded, nil
@@ -260,152 +265,51 @@ func decodeBlockRecords(block *Block) (decoded *BlockDecoded, err error) {
// BlockRecordProfile provides information about the end user.
type BlockRecordProfile struct {
Fields []BlockRecordProfileField // All fields
Blobs []BlockRecordProfileBlob // All blobs
Type uint16 // See ProfileX constants.
Data []byte // Data
}
// BlockRecordProfileField contains a single information about the end user. The data is always UTF8 text encoded.
// Note that all profile data is arbitrary and shall be considered untrusted and unverified.
// To establish trust, the user must load Certificates into the blockchain that validate certain data.
type BlockRecordProfileField struct {
Type uint16 // See ProfileFieldX constants.
Text string // The data
}
// ProfileFieldX constants define well known profile information
const (
ProfileFieldName = 0 // Arbitrary username
ProfileFieldEmail = 1 // Email address
ProfileFieldWebsite = 2 // Website address
ProfileFieldTwitter = 3 // Twitter account without the @
ProfileFieldYouTube = 4 // YouTube channel URL
ProfileFieldAddress = 5 // Physical address
)
// BlockRecordProfileBlob is similar to BlockRecordProfileField but contains binary objects instead of text.
// It can be used for example to store a profile picture on the blockchain.
type BlockRecordProfileBlob struct {
Type uint16 // See ProfileBlobX constants.
Data []byte // The data
}
// ProfileBlobX constants define well known blobs
// Pictures should be in JPEG or PNG format.
const (
ProfileBlobPicture = 0 // Profile picture, unspecified size
)
// decodeBlockRecordProfile decodes only profile records. Other records are ignored.
func decodeBlockRecordProfile(recordsRaw []BlockRecordRaw) (profile *BlockRecordProfile, err error) {
fields := make(map[uint16]string)
blobs := make(map[uint16][]byte)
func decodeBlockRecordProfile(recordsRaw []BlockRecordRaw) (fields []BlockRecordProfile, err error) {
fieldMap := make(map[uint16][]byte)
for _, record := range recordsRaw {
if record.Type != RecordTypeProfile {
continue
}
// header: 4 bytes
if len(record.Data) < 4 {
if len(record.Data) < 2 {
return nil, errors.New("profile record invalid size")
}
countFields := binary.LittleEndian.Uint16(record.Data[0:2])
countBlobs := binary.LittleEndian.Uint16(record.Data[2:4])
index := 4
for n := 0; n < int(countFields); n++ {
if index+4 > len(record.Data) {
return nil, errors.New("profile record field invalid size")
}
fieldType := binary.LittleEndian.Uint16(record.Data[index : index+2])
fieldSize := binary.LittleEndian.Uint32(record.Data[index+2 : index+2+4])
if index+6+int(fieldSize) > len(record.Data) {
return nil, errors.New("profile record field data invalid size")
}
fields[fieldType] = string(record.Data[index+6 : index+6+int(fieldSize)])
index += 6 + int(fieldSize)
}
for n := 0; n < int(countBlobs); n++ {
if index+4 > len(record.Data) {
return nil, errors.New("profile record field invalid size")
}
blobType := binary.LittleEndian.Uint16(record.Data[index : index+2])
blobSize := binary.LittleEndian.Uint32(record.Data[index+2 : index+2+4])
if index+6+int(blobSize) > len(record.Data) {
return nil, errors.New("profile record field data invalid size")
}
blobs[blobType] = record.Data[index+6 : index+6+int(blobSize)]
index += 6 + int(blobSize)
}
fieldType := binary.LittleEndian.Uint16(record.Data[0:2])
fieldMap[fieldType] = record.Data[2:]
}
if len(fields) == 0 && len(blobs) == 0 {
return nil, nil
for fieldType, fieldData := range fieldMap {
fields = append(fields, BlockRecordProfile{Type: fieldType, Data: fieldData})
}
profile = &BlockRecordProfile{}
for fieldType, fieldText := range fields {
profile.Fields = append(profile.Fields, BlockRecordProfileField{Type: fieldType, Text: fieldText})
}
for blobType, blobData := range blobs {
profile.Blobs = append(profile.Blobs, BlockRecordProfileBlob{Type: blobType, Data: blobData})
}
return profile, nil
return fields, nil
}
// encodeBlockRecordProfile encodes the profile record.
func encodeBlockRecordProfile(profile BlockRecordProfile) (recordsRaw []BlockRecordRaw, err error) {
if len(profile.Fields) > math.MaxUint16 || len(profile.Blobs) > math.MaxUint16 {
func encodeBlockRecordProfile(fields []BlockRecordProfile) (recordsRaw []BlockRecordRaw, err error) {
if len(fields) > math.MaxUint16 {
return nil, errors.New("exceeding max count of fields")
}
data := make([]byte, 4)
binary.LittleEndian.PutUint16(data[0:2], uint16(len(profile.Fields)))
binary.LittleEndian.PutUint16(data[2:4], uint16(len(profile.Blobs)))
for n := range profile.Fields {
storeB := []byte(profile.Fields[n].Text)
if len(storeB) > math.MaxUint32 {
for n := range fields {
if len(fields[n].Data) > math.MaxUint32 {
return nil, errors.New("exceeding max field size")
}
var tempData [6]byte
binary.LittleEndian.PutUint16(tempData[0:2], profile.Fields[n].Type)
binary.LittleEndian.PutUint32(tempData[2:6], uint32(len(storeB)))
data := make([]byte, 2)
binary.LittleEndian.PutUint16(data[0:2], fields[n].Type)
data = append(data, fields[n].Data...)
data = append(data, tempData[:]...)
data = append(data, storeB...)
recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeProfile, Data: data})
}
for n := range profile.Blobs {
if len(profile.Blobs[n].Data) > math.MaxUint32 {
return nil, errors.New("exceeding max blob size")
}
var tempData [6]byte
binary.LittleEndian.PutUint16(tempData[0:2], profile.Blobs[n].Type)
binary.LittleEndian.PutUint32(tempData[2:6], uint32(len(profile.Blobs[n].Data)))
data = append(data, tempData[:]...)
data = append(data, profile.Blobs[n].Data...)
}
recordsRaw = append(recordsRaw, BlockRecordRaw{Type: RecordTypeProfile, Data: data})
return recordsRaw, nil
}

View File

@@ -360,54 +360,22 @@ func UserBlockchainListFiles() (files []BlockRecordFile, status int) {
return files, status
}
// UserProfileReadField reads the specified profile field. See core.ProfileFieldX for the full list. The returned text is UTF-8 text encoded. Status is BlockchainStatusX.
func UserProfileReadField(index uint16) (text string, status int) {
// UserProfileReadField reads the specified profile field. See ProfileX for the list of recognized fields. The encoding depends on the field type. Status is BlockchainStatusX.
func UserProfileReadField(index uint16) (data []byte, status int) {
found := false
status = blockchainIterate(func(block *Block) (statusI int) {
profile, err := decodeBlockRecordProfile(block.RecordsRaw)
fields, err := decodeBlockRecordProfile(block.RecordsRaw)
if err != nil {
return BlockchainStatusCorruptBlockRecord
} else if profile == nil {
} else if len(fields) == 0 {
return BlockchainStatusOK
}
// Check if the field is available in the profile record. If there are multiple records, only return the latest one.
for n := range profile.Fields {
if profile.Fields[n].Type == index {
text = profile.Fields[n].Text
found = true
}
}
return BlockchainStatusOK
})
if status != BlockchainStatusOK {
return "", status
} else if !found {
return "", BlockchainStatusDataNotFound
}
return text, BlockchainStatusOK
}
// UserProfileReadBlob reads a specific profile blob. A blob is binary data. See core.ProfileBlobX. Status is BlockchainStatusX.
func UserProfileReadBlob(index uint16) (blob []byte, status int) {
found := false
status = blockchainIterate(func(block *Block) (statusI int) {
profile, err := decodeBlockRecordProfile(block.RecordsRaw)
if err != nil {
return BlockchainStatusCorruptBlockRecord
} else if profile == nil {
return BlockchainStatusOK
}
// Check if the blob is available in the profile record. If there are multiple records, only return the latest one.
for n := range profile.Blobs {
if profile.Blobs[n].Type == index {
blob = profile.Blobs[n].Data
for n := range fields {
if fields[n].Type == index {
data = fields[n].Data
found = true
}
}
@@ -421,47 +389,36 @@ func UserProfileReadBlob(index uint16) (blob []byte, status int) {
return nil, BlockchainStatusDataNotFound
}
return blob, BlockchainStatusOK
return data, BlockchainStatusOK
}
// UserProfileList lists all profile fields and blobs. Status is BlockchainStatusX.
func UserProfileList() (fields []BlockRecordProfileField, blobs []BlockRecordProfileBlob, status int) {
uniqueFields := make(map[uint16]string)
uniqueBlobs := make(map[uint16][]byte)
// UserProfileList lists all profile fields. Status is BlockchainStatusX.
func UserProfileList() (fields []BlockRecordProfile, status int) {
uniqueFields := make(map[uint16][]byte)
status = blockchainIterate(func(block *Block) (statusI int) {
profile, err := decodeBlockRecordProfile(block.RecordsRaw)
fields, err := decodeBlockRecordProfile(block.RecordsRaw)
if err != nil {
return BlockchainStatusCorruptBlockRecord
} else if profile == nil {
return BlockchainStatusOK
}
for n := range profile.Fields {
uniqueFields[profile.Fields[n].Type] = profile.Fields[n].Text
}
for n := range profile.Blobs {
uniqueBlobs[profile.Blobs[n].Type] = profile.Blobs[n].Data
for n := range fields {
uniqueFields[fields[n].Type] = fields[n].Data
}
return BlockchainStatusOK
})
for key, value := range uniqueFields {
fields = append(fields, BlockRecordProfileField{Type: key, Text: value})
fields = append(fields, BlockRecordProfile{Type: key, Data: value})
}
for key, value := range uniqueBlobs {
blobs = append(blobs, BlockRecordProfileBlob{Type: key, Data: value})
}
return fields, blobs, status
return fields, status
}
// UserProfileWrite writes profile fields and blobs to the blockchain. Status is BlockchainStatusX.
func UserProfileWrite(profile BlockRecordProfile) (newHeight, newVersion uint64, status int) {
encoded, err := encodeBlockRecordProfile(profile)
func UserProfileWrite(fields []BlockRecordProfile) (newHeight, newVersion uint64, status int) {
encoded, err := encodeBlockRecordProfile(fields)
if err != nil {
return 0, 0, BlockchainStatusCorruptBlockRecord
}
@@ -470,56 +427,21 @@ func UserProfileWrite(profile BlockRecordProfile) (newHeight, newVersion uint64,
}
// UserProfileDelete deletes fields and blobs from the blockchain. Status is BlockchainStatusX.
func UserProfileDelete(fields, blobs []uint16) (newHeight, newVersion uint64, status int) {
func UserProfileDelete(fields []uint16) (newHeight, newVersion uint64, status int) {
return blockchainIterateDeleteRecord(func(record *BlockRecordRaw) (deleteAction int) {
if record.Type != RecordTypeProfile {
return 0 // no action
}
profile, err := decodeBlockRecordProfile([]BlockRecordRaw{*record})
if err != nil || profile == nil {
existingFields, err := decodeBlockRecordProfile([]BlockRecordRaw{*record})
if err != nil || len(existingFields) != 1 {
return 3 // error blockchain corrupt
}
// process all blobs and fields
var newFields []BlockRecordProfileField
var newBlobs []BlockRecordProfileBlob
refactorRecord := false
parseFields:
for n := range profile.Fields {
for _, i := range fields {
if profile.Fields[n].Type == i {
refactorRecord = true
continue parseFields
}
for _, i := range fields {
if i == existingFields[0].Type { // found a file ID to delete?
return 1 // delete record
}
newFields = append(newFields, profile.Fields[n])
}
parseBlobs:
for n := range profile.Blobs {
for _, i := range blobs {
if profile.Blobs[n].Type == i {
refactorRecord = true
continue parseBlobs
}
}
newBlobs = append(newBlobs, profile.Blobs[n])
}
if len(newFields) == 0 && len(newBlobs) == 0 { // delete the entire record in case no fields and blobs are left
return 1 // delete record
} else if refactorRecord {
// refactor
encoded, err := encodeBlockRecordProfile(BlockRecordProfile{Fields: newFields, Blobs: newBlobs})
if err != nil || len(encoded) != 1 {
return 3
}
record.Data = encoded[0].Data
return 2 // replace record
}
return 0 // no action on record

32
Profile Data.go Normal file
View File

@@ -0,0 +1,32 @@
/*
File Name: Profile Data.go
Copyright: 2021 Peernet s.r.o.
Author: Peter Kleissner
*/
package core
// List of recognized profile fields.
const (
ProfileName = 0 // Arbitrary username
ProfileEmail = 1 // Email address
ProfileWebsite = 2 // Website address
ProfileTwitter = 3 // Twitter account without the @
ProfileYouTube = 4 // YouTube channel URL
ProfileAddress = 5 // Physical address
ProfilePicture = 6 // Profile picture, blob
)
// The encoding of profile fields depends on the field. Text data is always UTF-8 text encoded.
// Note that all profile data is arbitrary and shall be considered untrusted and unverified.
// To establish trust, the user must load Certificates into the blockchain that validate certain data.
// Text returns the profile field as text encoded
func (info *BlockRecordProfile) Text() string {
return string(info.Data)
}
// ProfileFieldFromText returns a profile field from text
func ProfileFieldFromText(Type uint16, Text string) BlockRecordProfile {
return BlockRecordProfile{Type: Type, Data: []byte(Text)}
}

View File

@@ -91,7 +91,7 @@ func TestBlockEncoding(t *testing.T) {
return
}
encoded1, _ := encodeBlockRecordProfile(BlockRecordProfile{Fields: []BlockRecordProfileField{{Type: ProfileFieldName, Text: "Test User 1"}}})
encoded1, _ := encodeBlockRecordProfile([]BlockRecordProfile{ProfileFieldFromText(ProfileName, "Test User 1")})
file1 := BlockRecordFile{Hash: hashData([]byte("Test data")), Type: TypeText, Format: FormatText, Size: 9, ID: uuid.New()}
file1.Tags = append(file1.Tags, TagFromText(TagName, "Filename 1.txt"))
@@ -134,11 +134,7 @@ func TestBlockEncoding(t *testing.T) {
printFile(record)
case BlockRecordProfile:
profile := record
fmt.Printf("* Profile\n")
for _, field := range profile.Fields {
fmt.Printf(" %d = %s\n", field.Type, field.Text)
}
printProfileField(record)
}
@@ -228,6 +224,8 @@ func printFile(file BlockRecordFile) {
fmt.Printf(" Name %s\n", tag.Text())
case TagFolder:
fmt.Printf(" Folder %s\n", tag.Text())
case TagDescription:
fmt.Printf(" Description %s\n", tag.Text())
}
}
}
@@ -270,8 +268,10 @@ func TestBlockchainProfile(t *testing.T) {
initUserBlockchain()
// write some test profile data
newHeight, newVersion, status := UserProfileWrite(BlockRecordProfile{Fields: []BlockRecordProfileField{{Type: ProfileFieldName, Text: "Test User 1"}, {Type: ProfileFieldEmail, Text: "test@test.com"}},
Blobs: []BlockRecordProfileBlob{{Type: 100, Data: []byte{0, 1, 2, 3}}}})
newHeight, newVersion, status := UserProfileWrite([]BlockRecordProfile{
ProfileFieldFromText(ProfileName, "Test User 1"),
ProfileFieldFromText(ProfileEmail, "test@test.com"),
{Type: 100, Data: []byte{0, 1, 2, 3}}})
fmt.Printf("Write profile data: Status %d height %d version %d\n", status, newHeight, newVersion)
@@ -281,29 +281,35 @@ func TestBlockchainProfile(t *testing.T) {
fmt.Printf("----------------\n")
// delete profile info
newHeight, newVersion, status = UserProfileDelete([]uint16{ProfileFieldEmail}, nil)
newHeight, newVersion, status = UserProfileDelete([]uint16{ProfileEmail})
fmt.Printf("Deleted profile email: Status %d height %d version %d\n", status, newHeight, newVersion)
printProfileData()
}
func printProfileData() {
fields, blobs, status := UserProfileList()
fields, status := UserProfileList()
if status != BlockchainStatusOK {
fmt.Printf("Reading profile data error status: %d\n", status)
return
}
if len(fields) == 0 && len(blobs) == 0 {
if len(fields) == 0 {
fmt.Printf("No profile data to visualize.\n")
return
}
for _, field := range fields {
fmt.Printf("* Field %d = %s\n", field.Type, field.Text)
}
for _, blob := range blobs {
fmt.Printf("* Blob %d = %s\n", blob.Type, hex.EncodeToString(blob.Data))
printProfileField(field)
}
}
func printProfileField(field BlockRecordProfile) {
switch field.Type {
case ProfileName, ProfileEmail, ProfileWebsite, ProfileTwitter, ProfileYouTube, ProfileAddress:
fmt.Printf("* Field %d = %s\n", field.Type, string(field.Data))
default:
fmt.Printf("* Field %d = %s\n", field.Type, hex.EncodeToString(field.Data))
}
}

View File

@@ -86,27 +86,6 @@ type apiBlockchainBlock struct {
RecordsDecoded []interface{} `json:"recordsdecoded"` // Records decoded. The encoding for each record depends on its type.
}
// apiBlockRecordProfile contains end-user information. Any data is treated as untrusted and unverified by default.
type apiBlockRecordProfile struct {
Fields []apiBlockRecordProfileField `json:"fields"` // All fields
Blobs []apiBlockRecordProfileBlob `json:"blobs"` // Blobs
}
// apiBlockRecordProfileField contains a single information about the end user. The data is always UTF8 text encoded.
// Note that all profile data is arbitrary and shall be considered untrusted and unverified.
// To establish trust, the user must load Certificates into the blockchain that validate certain data.
type apiBlockRecordProfileField struct {
Type uint16 `json:"type"` // See ProfileFieldX constants.
Text string `json:"text"` // The data
}
// apiBlockRecordProfileBlob is similar to apiBlockRecordProfileField but contains binary objects instead of text.
// It can be used for example to store a profile picture on the blockchain.
type apiBlockRecordProfileBlob struct {
Type uint16 `json:"type"` // See ProfileBlobX constants.
Data []byte `json:"data"` // The data
}
// apiFileMetadata contains metadata information.
type apiFileMetadata struct {
Type uint16 `json:"type"` // See core.TagX constants.

View File

@@ -15,68 +15,63 @@ import (
// apiProfileData contains profile metadata stored on the blockchain. Any data is treated as untrusted and unverified by default.
type apiProfileData struct {
Fields []apiBlockRecordProfileField `json:"fields"` // All fields
Blobs []apiBlockRecordProfileBlob `json:"blobs"` // All blobs
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See core.BlockchainStatusX.
Fields []apiBlockRecordProfile `json:"fields"` // All fields
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See core.BlockchainStatusX.
}
// apiBlockRecordProfile provides information about the end user. Note that all profile data is arbitrary and shall be considered untrusted and unverified.
// To establish trust, the user must load Certificates into the blockchain that validate certain data.
type apiBlockRecordProfile struct {
Type uint16 `json:"type"` // See ProfileX constants.
// Depending on the exact type, one of the below fields is used for proper encoding:
Text string `json:"text"` // Text value. UTF-8 encoding.
Blob []byte `json:"blob"` // Binary data
}
/*
apiProfileList lists all users profile fields and blobs.
apiProfileList lists all users profile fields.
Request: GET /profile/list
Response: 200 with JSON structure apiProfileData
*/
func apiProfileList(w http.ResponseWriter, r *http.Request) {
fields, blobs, status := core.UserProfileList()
fields, status := core.UserProfileList()
result := apiProfileData{Status: status}
for n := range fields {
result.Fields = append(result.Fields, apiBlockRecordProfileField{Type: fields[n].Type, Text: fields[n].Text})
}
for n := range blobs {
result.Blobs = append(result.Blobs, apiBlockRecordProfileBlob{Type: blobs[n].Type, Data: blobs[n].Data})
result.Fields = append(result.Fields, blockRecordProfileToAPI(fields[n]))
}
EncodeJSON(w, r, result)
}
/*
apiProfileRead reads a specific users profile field or blob.
For the index see core.ProfileFieldX and core.ProfileBlobX constants.
apiProfileRead reads a specific users profile field. See core.ProfileX for recognized fields.
Request: GET /profile/read?field=[index] or &blob=[index]
Request: GET /profile/read?field=[index]
Response: 200 with JSON structure apiProfileData
*/
func apiProfileRead(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
fieldN, err1 := strconv.Atoi(r.Form.Get("field"))
blobN, err2 := strconv.Atoi(r.Form.Get("blob"))
if (err1 != nil && err2 != nil) || (err1 == nil && fieldN < 0) || (err2 == nil && blobN < 0) {
if err1 != nil || fieldN < 0 {
http.Error(w, "", http.StatusBadRequest)
return
}
var result apiProfileData
if err1 == nil {
var text string
if text, result.Status = core.UserProfileReadField(uint16(fieldN)); result.Status == core.BlockchainStatusOK {
result.Fields = append(result.Fields, apiBlockRecordProfileField{Type: uint16(fieldN), Text: text})
}
} else {
var data []byte
if data, result.Status = core.UserProfileReadBlob(uint16(blobN)); result.Status == core.BlockchainStatusOK {
result.Blobs = append(result.Blobs, apiBlockRecordProfileBlob{Type: uint16(blobN), Data: data})
}
var data []byte
if data, result.Status = core.UserProfileReadField(uint16(fieldN)); result.Status == core.BlockchainStatusOK {
result.Fields = append(result.Fields, blockRecordProfileToAPI(core.BlockRecordProfile{Type: uint16(fieldN), Data: data}))
}
EncodeJSON(w, r, result)
}
/*
apiProfileWrite writes profile fields or blobs.
For the index see core.ProfileFieldX and core.ProfileBlobX constants.
apiProfileWrite writes profile fields. See core.ProfileX for recognized fields.
Request: POST /profile/write with JSON structure apiProfileData
Response: 200 with JSON structure apiBlockchainBlockStatus
@@ -87,23 +82,19 @@ func apiProfileWrite(w http.ResponseWriter, r *http.Request) {
return
}
var profile core.BlockRecordProfile
var fields []core.BlockRecordProfile
for n := range input.Fields {
profile.Fields = append(profile.Fields, core.BlockRecordProfileField{Type: input.Fields[n].Type, Text: input.Fields[n].Text})
}
for n := range input.Blobs {
profile.Blobs = append(profile.Blobs, core.BlockRecordProfileBlob{Type: input.Blobs[n].Type, Data: input.Blobs[n].Data})
fields = append(fields, blockRecordProfileFromAPI(input.Fields[n]))
}
newHeight, newVersion, status := core.UserProfileWrite(profile)
newHeight, newVersion, status := core.UserProfileWrite(fields)
EncodeJSON(w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
}
/*
apiProfileDelete deletes profile fields or blobs identified by the types.
For the index see core.ProfileFieldX and core.ProfileBlobX constants.
apiProfileDelete deletes profile fields identified by the types. See core.ProfileX for recognized fields.
Request: POST /profile/delete with JSON structure apiProfileData
Response: 200 with JSON structure apiBlockchainBlockStatus
@@ -114,16 +105,13 @@ func apiProfileDelete(w http.ResponseWriter, r *http.Request) {
return
}
var fields, blobs []uint16
var fields []uint16
for n := range input.Fields {
fields = append(fields, input.Fields[n].Type)
}
for n := range input.Blobs {
blobs = append(blobs, input.Blobs[n].Type)
}
newHeight, newVersion, status := core.UserProfileDelete(fields, blobs)
newHeight, newVersion, status := core.UserProfileDelete(fields)
EncodeJSON(w, r, apiBlockchainBlockStatus{Status: status, Height: newHeight, Version: newVersion})
}
@@ -131,22 +119,34 @@ func apiProfileDelete(w http.ResponseWriter, r *http.Request) {
// --- conversion from core to API data ---
func blockRecordProfileToAPI(input core.BlockRecordProfile) (output apiBlockRecordProfile) {
for n := range input.Fields {
output.Fields = append(output.Fields, apiBlockRecordProfileField{Type: input.Fields[n].Type, Text: input.Fields[n].Text})
}
for n := range input.Blobs {
output.Blobs = append(output.Blobs, apiBlockRecordProfileBlob{Type: input.Blobs[n].Type, Data: input.Blobs[n].Data})
output.Type = input.Type
switch input.Type {
case core.ProfileName, core.ProfileEmail, core.ProfileWebsite, core.ProfileTwitter, core.ProfileYouTube, core.ProfileAddress:
output.Text = input.Text()
case core.ProfilePicture:
output.Blob = input.Data
default:
output.Blob = input.Data
}
return output
}
func blockRecordProfileFromAPI(input apiBlockRecordProfile) (output core.BlockRecordProfile) {
for n := range input.Fields {
output.Fields = append(output.Fields, core.BlockRecordProfileField{Type: input.Fields[n].Type, Text: input.Fields[n].Text})
}
for n := range input.Blobs {
output.Blobs = append(output.Blobs, core.BlockRecordProfileBlob{Type: input.Blobs[n].Type, Data: input.Blobs[n].Data})
output.Type = input.Type
switch input.Type {
case core.ProfileName, core.ProfileEmail, core.ProfileWebsite, core.ProfileTwitter, core.ProfileYouTube, core.ProfileAddress:
output.Data = []byte(input.Text)
case core.ProfilePicture:
output.Data = input.Blob
default:
output.Data = input.Blob
}
return output

View File

@@ -37,10 +37,10 @@ These are the functions provided by the API:
/blockchain/self/list/file List all files stored on the blockchain
/blockchain/self/delete/file Delete files from the blockchain
/profile/list List all users profile fields and blobs
/profile/read Read a profile field or blob
/profile/write Write profile fields or blobs
/profile/delete Delete profile fields or blobs
/profile/list List all profile fields
/profile/read Read a profile field
/profile/write Write profile fields
/profile/delete Delete profile fields
/search Submit a search request
/search/result Return search results
@@ -98,6 +98,18 @@ type apiResponsePeerSelf struct {
## Blockchain Functions
Common status codes returned by various endpoints:
```go
const (
BlockchainStatusOK = 0 // No problems in the blockchain detected.
BlockchainStatusBlockNotFound = 1 // Missing block in the blockchain.
BlockchainStatusCorruptBlock = 2 // Error block encoding
BlockchainStatusCorruptBlockRecord = 3 // Error block record encoding
BlockchainStatusDataNotFound = 4 // Requested data not available in the blockchain
)
```
### Blockchain Self Header
This function returns information about the current peer. It is not required that a peer has a blockchain. If no data is shared, there are no blocks. The blockchain does not formally have a header as each block has the same structure.
@@ -167,23 +179,6 @@ The array `RecordsDecoded` will contain any present record of the following:
* Profile records, see `apiBlockRecordProfile`
* File records, see `apiBlockRecordFile`
```go
type apiBlockRecordProfile struct {
Fields []apiBlockRecordProfileField `json:"fields"` // All fields
Blobs []apiBlockRecordProfileBlob `json:"blobs"` // Blobs
}
type apiBlockRecordProfileField struct {
Type uint16 `json:"type"` // See ProfileFieldX constants.
Text string `json:"text"` // The data
}
type apiBlockRecordProfileBlob struct {
Type uint16 `json:"type"` // See ProfileBlobX constants.
Data []byte `json:"data"` // The data
}
```
## File Functions
These functions allow adding, deleting, and listing files stored on the users blockchain. Only metadata is actually stored on the blockchain.
@@ -355,25 +350,26 @@ Example response:
## Profile Functions
User profile data such as the username, email address, and picture are stored on the blockchain. The Profile API distinguishes between text data (= fields) and binary data (= blobs). Text is UTF-8 encoded.
User profile data such as the username, email address, and picture are stored on the blockchain. Profile fields are text (UTF-8) or binary encoded, depending on the type.
Below is the current list of well known profile information. Clients may define additional fields. The purpose of this defined list is to provide a common mapping across different client software. In the Go code they are defined as constants starting with `ProfileField` and `ProfileBlob`.
Note that all profile data is arbitrary and shall be considered untrusted and unverified. To establish trust, the user must load Certificates into the blockchain that validate certain data.
| Field Identifier | Purpose |
|------------------|----------------------------|
| 0 | Username, arbitrary |
| 1 | Email address |
| 2 | Website address |
| 3 | Twitter account with the @ |
| 4 | YouTube channel URL |
| 5 | Physical address |
Below is the list of well known profile information. Clients may define additional fields. The purpose of this defined list is to provide a common mapping across different client software. Undefined types are always mapped into the `blob` field.
| Blob Identifier | Purpose |
|------------------|----------------------------|
| 0 | Profile picture, unspecified size |
| Type | Constant | Encoding | Info |
|------|----------------|----------|-------------------------------|
| 0 | ProfileName | Text | Arbitrary username |
| 1 | ProfileEmail | Text | Email address |
| 2 | ProfileWebsite | Text | Website address |
| 3 | ProfileTwitter | Text | Twitter account without the @ |
| 4 | ProfileYouTube | Text | YouTube channel URL |
| 5 | ProfileAddress | Text | Physical address |
| 6 | ProfilePicture | Blob | Profile picture |
### Profile List
This lists all profile fields.
```
Request: GET /profile/list
Response: 200 with JSON structure apiProfileData
@@ -381,18 +377,16 @@ Response: 200 with JSON structure apiProfileData
```go
type apiProfileData struct {
Fields []apiBlockRecordProfileField `json:"fields"` // All fields
Blobs []apiBlockRecordProfileBlob `json:"blobs"` // All blobs
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See core.BlockchainStatusX.
Fields []apiBlockRecordProfile `json:"fields"` // All fields
Status int `json:"status"` // Status of the operation, only used when this structure is returned from the API. See core.BlockchainStatusX.
}
const (
BlockchainStatusOK = 0 // No problems in the blockchain detected.
BlockchainStatusBlockNotFound = 1 // Missing block in the blockchain.
BlockchainStatusCorruptBlock = 2 // Error block encoding
BlockchainStatusCorruptBlockRecord = 3 // Error block record encoding
BlockchainStatusDataNotFound = 4 // Requested data not available in the blockchain
)
type apiBlockRecordProfile struct {
Type uint16 `json:"type"` // See ProfileX constants.
// Depending on the exact type, one of the below fields is used for proper encoding:
Text string `json:"text"` // Text value. UTF-8 encoding.
Blob []byte `json:"blob"` // Binary data
}
```
Example request: `http://127.0.0.1:112/profile/list`
@@ -403,22 +397,23 @@ Example response:
{
"fields": [{
"type": 0,
"text": "Test Username 2022"
"text": "Test Username 2021",
"blob": null
}, {
"type": 1,
"text": "test@example.com"
"text": "test@example.com",
"blob": null
}],
"blobs": null,
"status": 0
}
```
### Profile Read
This reads a specific users' profile field or blob. For the index see the `ProfileFieldX` and `ProfileBlobX` constants.
This reads a specific profile field. See ProfileX for recognized fields.
```
Request: GET /profile/read?field=[index] or &blob=[index]
Request: GET /profile/read?field=[index]
Response: 200 with JSON structure apiProfileData
```
@@ -430,16 +425,16 @@ Example response:
{
"fields": [{
"type": 0,
"text": "Test Username 2022"
"text": "Test Username 2021",
"blob": null
}],
"blobs": null,
"status": 0
}
```
### Profile Write
This writes profile fields and blobs. It can write multiple fields and blobs at once.
This writes profile fields. It can write multiple fields at once. See ProfileX for recognized fields.
```
Request: POST /profile/write with JSON structure apiProfileData
@@ -469,7 +464,7 @@ Example response:
### Profile Delete
This function allows to delete profile data (both fields and blobs). Only the type number in the fields and blobs are required. Multiple fields and blobs can be deleted at the same time.
This function allows to delete profile fields. Only the type number is required. Multiple fields can be deleted at the same time.
```
Request: POST /profile/delete with JSON structure apiProfileData
@@ -486,16 +481,6 @@ Example POST request to `http://127.0.0.1:112/profile/delete` (deleting the prof
}
```
One practical use case is deleting the profile picture (by specifying blob 0) with the following payload:
```json
{
"blobs": [{
"type": 0
}]
}
```
## Search API
The search API provides a high-level function to search for files in Peernet. Searching is always asynchronous. `/search` returns an UUID which is used to loop over `/search/result` until the search is terminated.