nip-02 along with a hacky implementation.

This commit is contained in:
fiatjaf 2020-12-07 23:56:21 -03:00
parent 4d4c7d1c48
commit f3872fb6f7
10 changed files with 249 additions and 52 deletions

51
nips/02.md Normal file
View File

@ -0,0 +1,51 @@
NIP-02
======
Petname sharing through a special event `3`: "contact list"
-------------------------------------------------------
`draft` `optional`
A special event with kind `3`, meaning "contact list" is defined as having a `content` equal to a JSON string (yes, encoded JSON inside a JSON string) that decodes to the a list of tuples of `<pubkey>` (32 bytes hex) and `<name>` (arbitrary UTF-8 name, maximum 32 characters), as the following example:
```json
[
["91cf9c4c5d9b675c00afd68db164e5ca", "alice"],
["14aeb4daf2045e469f2dff496f48dad4", "bob"],
["612ae030a3856e463a363390250e610f", "carol"]
]
```
Every new contact list that gets published overwrites the past ones, so it should contain all entries. Relays and clients SHOULD delete past contact lists as soon as they receive a new one.
This can be published by anyone and used by clients to construct local ["petname"](http://www.skyhunter.com/marcs/petnames/IntroPetNames.html) tables derived from other people's contact lists.
## Example
A user has an internal contact list that says
```json
[
["21df6d143fb96c2ec9d63726bf9edc71", "erin"]
]
```
And receives two contact lists, one from `21df6d143fb96c2ec9d63726bf9edc71` that says
```json
[
["a8bb3d884d5d90b413d9891fe4c4e46d", "david"]
]
```
and another from `a8bb3d884d5d90b413d9891fe4c4e46d` that says
```json
[
["f57f54057d2a7af0efecc8b0b66f5708", "frank"]
]
```
When the user sees `21df6d143fb96c2ec9d63726bf9edc71` the client can show _erin_ instead;
When the user sees `a8bb3d884d5d90b413d9891fe4c4e46d` the client can show _david.erin_ instead;
When the user sees `f57f54057d2a7af0efecc8b0b66f5708` the client can show _frank.david.erin_ instead.

View File

@ -14,6 +14,7 @@ const (
KindSetMetadata uint8 = 0
KindTextNote uint8 = 1
KindRecommendServer uint8 = 2
KindContactList uint8 = 3
)
type Event struct {

View File

@ -53,15 +53,18 @@ func saveEvent(w http.ResponseWriter, r *http.Request) {
// react to different kinds of events
switch evt.Kind {
case 0:
case KindSetMetadata:
// delete past set_metadata events from this user
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 1`, evt.PubKey)
case 1:
case KindTextNote:
// do nothing
case 2:
// delete past recommend_server events that match this one
case KindRecommendServer:
// delete past recommend_server events equal to this one
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 2 AND content = $2`,
evt.PubKey, evt.Content)
case KindContactList:
// delete past contact lists from this same pubkey
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 3`, evt.PubKey)
}
// insert

View File

@ -12,7 +12,7 @@
/>
</div>
<div>
<form v-if="editingName !== null" @submit="savePetName">
<form v-if="editingName !== null" @submit="setContactName">
<label>
Petname:
<input v-model="editingName" />
@ -110,9 +110,9 @@
this.$store.dispatch('publishMetadata', this.editingMetadata)
this.editingMetadata = null
},
savePetName(e) {
setContactName(e) {
e.preventDefault()
this.$store.commit('savePetName', {
this.$store.dispatch('setContactName', {
pubkey: this.$route.params.key,
name: this.editingName
})

View File

@ -1,11 +1,19 @@
// vuex store actions
import {verifySignature, publishEvent, broadcastEvent} from './helpers'
import {
verifySignature,
publishEvent,
broadcastEvent,
overwriteEvent
} from './helpers'
import {
CONTEXT_NOW,
CONTEXT_REQUESTED,
CONTEXT_PAST,
KIND_METADATA,
KIND_TEXTNOTE,
KIND_RECOMMENDSERVER
KIND_RECOMMENDSERVER,
KIND_CONTACTLIST
} from './constants'
import {db} from './globals'
@ -34,7 +42,25 @@ export default {
switch (event.kind) {
case KIND_METADATA:
store.commit('receivedSetMetadata', {event, context})
if (context === CONTEXT_REQUESTED) {
// just someone we're viewing
store.commit('receivedSetMetadata', {event, context})
} else if (context === CONTEXT_NOW) {
// an update from someone we follow that happened just now
store.commit('receivedSetMetadata', {event, context})
await db.events
.where({kind: KIND_METADATA, pubkey: event.pubkey})
.delete()
await db.events.put(event)
} else if (context === CONTEXT_PAST) {
// someone we follow, but an old update -- check first
// check first if we don't have a newer one
let foundNewer = await overwriteEvent(
{kind: KIND_METADATA, pubkey: event.pubkey},
event
)
if (!foundNewer) store.commit('receivedSetMetadata', {event, context})
}
break
case KIND_TEXTNOTE:
store.commit('receivedTextNote', {event, context})
@ -73,6 +99,62 @@ export default {
store.commit('loadedRelays', await db.relays.toArray())
break
case KIND_CONTACTLIST:
// if (!(event.pubkey in store.state.petnames)) {
// // we don't know this person, so we won't use their contact list
// return
// }
// check if we have a newest version already
let foundNewer = await overwriteEvent(
{pubkey: event.pubkey, kind: KIND_CONTACTLIST},
event
)
// process
if (!foundNewer) store.dispatch('processContactList', event)
break
}
},
async setContactName(store, {pubkey, name}) {
db.contactlist.put({pubkey, name})
store.commit('savePetName', {pubkey, name: [name]})
// publish our new contact list
publishEvent(
{
pubkey: store.getters.pubKeyHex,
created_at: Math.round(new Date().getTime() / 1000),
kind: KIND_CONTACTLIST,
content: JSON.stringify(
(await db.contactlist.toArray()).map(({pubkey, name}) => [
pubkey,
name
])
)
},
store.state.key,
store.getters.writeServers
)
},
processContactList(store, event) {
// parse event content
var entries = []
try {
entries = JSON.parse(event.content)
} catch (err) {
return
}
for (let i = 0; i < entries.length; i++) {
let [pubkey, name] = entries[i]
if (pubkey in store.state.petnames) {
// we have our own petname for this key already
continue
}
store.commit('savePetName', {pubkey, name: [name, event.pubkey]})
}
},
async addRelay(store, relay) {

View File

@ -1,6 +1,7 @@
export const KIND_METADATA = 0
export const KIND_TEXTNOTE = 1
export const KIND_RECOMMENDSERVER = 2
export const KIND_CONTACTLIST = 3
export const CONTEXT_REQUESTED = 'r'
export const CONTEXT_PAST = 'p'

View File

@ -3,17 +3,24 @@ import Dexie from 'dexie'
export const db = new Dexie('db')
db.version(1).stores({
// local personal settings and store
relays: 'host',
following: 'pubkey',
mynotes: 'id, kind, created_at',
cachedmetadata: 'pubkey',
publishlog: '[id+host]',
// any special events from other people we may want to use later
// like metadata or contactlists from others, for example
// not simple notes or transient things
events: 'id, pubkey, kind',
// the set of local pet names and maybe other things we assign to others
contactlist: 'pubkey'
})
if (localStorage.getItem('deleted') < '3') {
if (localStorage.getItem('deleted') < '4') {
db.delete().then(() => {
localStorage.setItem('deleted', '3')
localStorage.setItem('deleted', '4')
location.reload()
})
}

View File

@ -89,3 +89,24 @@ export function serializeEvent(evt) {
return Buffer.concat([version, pubkey, time, kind, reference, content])
}
export async function overwriteEvent(conditions, event) {
let events = await db.events.where(conditions).toArray()
for (let i = 0; i < events.length; i++) {
// only save if it's newer than what we have
let evt = events[i]
if (evt.created_at > event.created_at) {
// we found a newer one
return true
}
// this is older, delete it
db.events.delete(evt.id)
}
// we didn't find a newer one
await db.events.put(event)
return false
}

View File

@ -34,37 +34,56 @@ export default {
state.following.splice(state.following.indexOf(key), 1)
db.following.delete(key)
},
savePetName(state, {pubkey, name}) {
state.petnames[pubkey] = name
db.contactlist.put({pubkey, name})
},
receivedSetMetadata(state, {event, context}) {
let meta = JSON.parse(event.content)
let storeable = {
pubkey: event.pubkey,
time: event.created_at,
meta
savePetName(state, payload) {
state.petnames[payload.pubkey] = state.petnames[payload.pubkey] || []
// maybe stop here if we already have enough names for <payload.pubkey>?
if (state.petnames[payload.pubkey].length > 4) {
return
}
if (context === CONTEXT_REQUESTED) {
// just someone we're viewing
if (!state.metadata.has(event.pubkey)) {
state.metadata.set(event.pubkey, meta)
if (payload.name.length === 2) {
// this is a hierarchy of type [<name>, <author-pubkey>]
if (payload.name[1] in state.petnames) {
// if we have any names for <author-pubkey>, replace them here
state.petnames[payload.name[1]].forEach(supername => {
state.petnames[payload.pubkey].push([payload.name[0], ...supername])
})
// and keep all references to [<name>, <replaced-hierarchy>]?
// maybe keeping just some is better?
state.petnames[payload.pubkey].sort((a, b) => a.length - b.length)
state.petnames = state.petnames.slice(0, 5)
} else {
// otherwise save this in raw form
state.petnames[payload.pubkey].push(payload.name)
}
} else if (context === CONTEXT_NOW) {
// an update from someone we follow that happened just now
state.metadata.set(event.pubkey, meta)
db.cachedmetadata.put(storeable)
} else if (context === CONTEXT_PAST) {
// someone we follow, but an old update
db.cachedmetadata.get(event.pubkey).then(data => {
// only save if it's newer than what we have
if (!data || data.time < storeable.time) {
state.metadata.set(event.pubkey, meta)
db.cachedmetadata.put(storeable)
}
})
} else if (payload.name.length === 1) {
// also save this in raw form if it's a single thing
state.petnames[payload.pubkey].push(payload.name)
}
// now search all our other names for any that uses <payload.pubkey>
for (let pubkey in state.petnames) {
if (pubkey === payload.pubkey) continue
let names = state.petnames[pubkey]
for (let i = 0; i < names.length; i++) {
let name = names[i]
if (name[name.length - 1] === payload.pubkey) {
// found one, replace it with the name we just got
names[i] = name.slice(0, -1).concat(payload.name)
}
}
}
// print this mess
console.log(state.petnames)
},
receivedSetMetadata(state, {event, context}) {
try {
let meta = JSON.parse(event.content)
state.metadata.set(event.pubkey, meta)
} catch (err) {}
},
receivedTextNote(state, {event: evt, context}) {
// all notes go to browsing

View File

@ -6,6 +6,7 @@ import {pubkeyFromPrivate, makeRandom32} from './helpers'
import {db} from './globals'
import actions from './actions'
import mutations from './mutations'
import {KIND_METADATA, KIND_CONTACTLIST} from './constants'
export default createStore({
plugins: (process.env.NODE_ENV !== 'production'
@ -57,10 +58,11 @@ export default createStore({
.filter(({policy}) => policy.indexOf('r') !== -1)
.map(({host}) => host),
keyName: state => pubkey =>
state.petnames[pubkey] ||
(state.metadata.get(pubkey) || {}).name ||
(pubkey && pubkey.slice(0, 4) + '…' + pubkey.slice(-4)) ||
''
state.petnames[pubkey]
? state.petnames[pubkey].map(name => name.join('.')).join(', ')
: (state.metadata.get(pubkey) || {}).name ||
(pubkey && pubkey.slice(0, 4) + '…' + pubkey.slice(-4)) ||
''
},
mutations,
actions
@ -87,17 +89,21 @@ async function init(store) {
(a, b) => b.split(':')[1] - a.split(':')[1]
)
}),
db.cachedmetadata.toArray().then(metas => {
var metadata = {}
metas.forEach(({meta, pubkey}) => {
metadata[pubkey] = meta
})
return metadata
}),
db.events
.where({kind: KIND_METADATA})
.toArray()
.then(events => {
var metadata = {}
events.forEach(({content, pubkey}) => {
let meta = JSON.parse(content)
metadata[pubkey] = meta
})
return metadata
}),
db.contactlist.toArray().then(contacts => {
var petnames = {}
contacts.forEach(({pubkey, name}) => {
petnames[pubkey] = name
petnames[pubkey] = [[name]]
})
return petnames
})
@ -110,6 +116,12 @@ async function init(store) {
metadata: data[3],
petnames: data[4]
})
// process contact lists (nip-02)
let events = await db.events.where({kind: KIND_CONTACTLIST}).toArray()
for (let i = 0; i < events.length; i++) {
store.dispatch('processContactList', events[i])
}
}
function publishStatusLoader(store) {