diff --git a/Block Records.go b/Block Records.go index e524c41..77e650b 100644 --- a/Block Records.go +++ b/Block Records.go @@ -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 } diff --git a/Blockchain.go b/Blockchain.go index bb41729..e6c173f 100644 --- a/Blockchain.go +++ b/Blockchain.go @@ -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 diff --git a/Profile Data.go b/Profile Data.go new file mode 100644 index 0000000..25bf199 --- /dev/null +++ b/Profile Data.go @@ -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)} +} diff --git a/Test_test.go b/Test_test.go index 6fc58e6..2e166a1 100644 --- a/Test_test.go +++ b/Test_test.go @@ -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)) } } diff --git a/webapi/Blockchain.go b/webapi/Blockchain.go index d1317d4..1d5479a 100644 --- a/webapi/Blockchain.go +++ b/webapi/Blockchain.go @@ -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. diff --git a/webapi/Profile.go b/webapi/Profile.go index 33936a3..6208b01 100644 --- a/webapi/Profile.go +++ b/webapi/Profile.go @@ -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 diff --git a/webapi/readme.md b/webapi/readme.md index 41b313e..e2b2de6 100644 --- a/webapi/readme.md +++ b/webapi/readme.md @@ -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.