nip-02 along with a hacky implementation.
This commit is contained in:
parent
4d4c7d1c48
commit
f3872fb6f7
|
@ -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.
|
|
@ -14,6 +14,7 @@ const (
|
||||||
KindSetMetadata uint8 = 0
|
KindSetMetadata uint8 = 0
|
||||||
KindTextNote uint8 = 1
|
KindTextNote uint8 = 1
|
||||||
KindRecommendServer uint8 = 2
|
KindRecommendServer uint8 = 2
|
||||||
|
KindContactList uint8 = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
type Event struct {
|
type Event struct {
|
||||||
|
|
|
@ -53,15 +53,18 @@ func saveEvent(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
// react to different kinds of events
|
// react to different kinds of events
|
||||||
switch evt.Kind {
|
switch evt.Kind {
|
||||||
case 0:
|
case KindSetMetadata:
|
||||||
// delete past set_metadata events from this user
|
// delete past set_metadata events from this user
|
||||||
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 1`, evt.PubKey)
|
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 1`, evt.PubKey)
|
||||||
case 1:
|
case KindTextNote:
|
||||||
// do nothing
|
// do nothing
|
||||||
case 2:
|
case KindRecommendServer:
|
||||||
// delete past recommend_server events that match this one
|
// delete past recommend_server events equal to this one
|
||||||
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 2 AND content = $2`,
|
db.Exec(`DELETE FROM event WHERE pubkey = $1 AND kind = 2 AND content = $2`,
|
||||||
evt.PubKey, evt.Content)
|
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
|
// insert
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<form v-if="editingName !== null" @submit="savePetName">
|
<form v-if="editingName !== null" @submit="setContactName">
|
||||||
<label>
|
<label>
|
||||||
Petname:
|
Petname:
|
||||||
<input v-model="editingName" />
|
<input v-model="editingName" />
|
||||||
|
@ -110,9 +110,9 @@
|
||||||
this.$store.dispatch('publishMetadata', this.editingMetadata)
|
this.$store.dispatch('publishMetadata', this.editingMetadata)
|
||||||
this.editingMetadata = null
|
this.editingMetadata = null
|
||||||
},
|
},
|
||||||
savePetName(e) {
|
setContactName(e) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
this.$store.commit('savePetName', {
|
this.$store.dispatch('setContactName', {
|
||||||
pubkey: this.$route.params.key,
|
pubkey: this.$route.params.key,
|
||||||
name: this.editingName
|
name: this.editingName
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,11 +1,19 @@
|
||||||
// vuex store actions
|
// vuex store actions
|
||||||
|
|
||||||
import {verifySignature, publishEvent, broadcastEvent} from './helpers'
|
import {
|
||||||
|
verifySignature,
|
||||||
|
publishEvent,
|
||||||
|
broadcastEvent,
|
||||||
|
overwriteEvent
|
||||||
|
} from './helpers'
|
||||||
import {
|
import {
|
||||||
CONTEXT_NOW,
|
CONTEXT_NOW,
|
||||||
|
CONTEXT_REQUESTED,
|
||||||
|
CONTEXT_PAST,
|
||||||
KIND_METADATA,
|
KIND_METADATA,
|
||||||
KIND_TEXTNOTE,
|
KIND_TEXTNOTE,
|
||||||
KIND_RECOMMENDSERVER
|
KIND_RECOMMENDSERVER,
|
||||||
|
KIND_CONTACTLIST
|
||||||
} from './constants'
|
} from './constants'
|
||||||
import {db} from './globals'
|
import {db} from './globals'
|
||||||
|
|
||||||
|
@ -34,7 +42,25 @@ export default {
|
||||||
|
|
||||||
switch (event.kind) {
|
switch (event.kind) {
|
||||||
case KIND_METADATA:
|
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
|
break
|
||||||
case KIND_TEXTNOTE:
|
case KIND_TEXTNOTE:
|
||||||
store.commit('receivedTextNote', {event, context})
|
store.commit('receivedTextNote', {event, context})
|
||||||
|
@ -73,6 +99,62 @@ export default {
|
||||||
|
|
||||||
store.commit('loadedRelays', await db.relays.toArray())
|
store.commit('loadedRelays', await db.relays.toArray())
|
||||||
break
|
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) {
|
async addRelay(store, relay) {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
export const KIND_METADATA = 0
|
export const KIND_METADATA = 0
|
||||||
export const KIND_TEXTNOTE = 1
|
export const KIND_TEXTNOTE = 1
|
||||||
export const KIND_RECOMMENDSERVER = 2
|
export const KIND_RECOMMENDSERVER = 2
|
||||||
|
export const KIND_CONTACTLIST = 3
|
||||||
|
|
||||||
export const CONTEXT_REQUESTED = 'r'
|
export const CONTEXT_REQUESTED = 'r'
|
||||||
export const CONTEXT_PAST = 'p'
|
export const CONTEXT_PAST = 'p'
|
||||||
|
|
|
@ -3,17 +3,24 @@ import Dexie from 'dexie'
|
||||||
export const db = new Dexie('db')
|
export const db = new Dexie('db')
|
||||||
|
|
||||||
db.version(1).stores({
|
db.version(1).stores({
|
||||||
|
// local personal settings and store
|
||||||
relays: 'host',
|
relays: 'host',
|
||||||
following: 'pubkey',
|
following: 'pubkey',
|
||||||
mynotes: 'id, kind, created_at',
|
mynotes: 'id, kind, created_at',
|
||||||
cachedmetadata: 'pubkey',
|
|
||||||
publishlog: '[id+host]',
|
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'
|
contactlist: 'pubkey'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (localStorage.getItem('deleted') < '3') {
|
if (localStorage.getItem('deleted') < '4') {
|
||||||
db.delete().then(() => {
|
db.delete().then(() => {
|
||||||
localStorage.setItem('deleted', '3')
|
localStorage.setItem('deleted', '4')
|
||||||
location.reload()
|
location.reload()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -89,3 +89,24 @@ export function serializeEvent(evt) {
|
||||||
|
|
||||||
return Buffer.concat([version, pubkey, time, kind, reference, content])
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -34,37 +34,56 @@ export default {
|
||||||
state.following.splice(state.following.indexOf(key), 1)
|
state.following.splice(state.following.indexOf(key), 1)
|
||||||
db.following.delete(key)
|
db.following.delete(key)
|
||||||
},
|
},
|
||||||
savePetName(state, {pubkey, name}) {
|
savePetName(state, payload) {
|
||||||
state.petnames[pubkey] = name
|
state.petnames[payload.pubkey] = state.petnames[payload.pubkey] || []
|
||||||
db.contactlist.put({pubkey, name})
|
// maybe stop here if we already have enough names for <payload.pubkey>?
|
||||||
},
|
if (state.petnames[payload.pubkey].length > 4) {
|
||||||
receivedSetMetadata(state, {event, context}) {
|
return
|
||||||
let meta = JSON.parse(event.content)
|
|
||||||
let storeable = {
|
|
||||||
pubkey: event.pubkey,
|
|
||||||
time: event.created_at,
|
|
||||||
meta
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (context === CONTEXT_REQUESTED) {
|
if (payload.name.length === 2) {
|
||||||
// just someone we're viewing
|
// this is a hierarchy of type [<name>, <author-pubkey>]
|
||||||
if (!state.metadata.has(event.pubkey)) {
|
if (payload.name[1] in state.petnames) {
|
||||||
state.metadata.set(event.pubkey, meta)
|
// 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) {
|
} else if (payload.name.length === 1) {
|
||||||
// an update from someone we follow that happened just now
|
// also save this in raw form if it's a single thing
|
||||||
state.metadata.set(event.pubkey, meta)
|
state.petnames[payload.pubkey].push(payload.name)
|
||||||
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)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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}) {
|
receivedTextNote(state, {event: evt, context}) {
|
||||||
// all notes go to browsing
|
// all notes go to browsing
|
||||||
|
|
|
@ -6,6 +6,7 @@ import {pubkeyFromPrivate, makeRandom32} from './helpers'
|
||||||
import {db} from './globals'
|
import {db} from './globals'
|
||||||
import actions from './actions'
|
import actions from './actions'
|
||||||
import mutations from './mutations'
|
import mutations from './mutations'
|
||||||
|
import {KIND_METADATA, KIND_CONTACTLIST} from './constants'
|
||||||
|
|
||||||
export default createStore({
|
export default createStore({
|
||||||
plugins: (process.env.NODE_ENV !== 'production'
|
plugins: (process.env.NODE_ENV !== 'production'
|
||||||
|
@ -57,10 +58,11 @@ export default createStore({
|
||||||
.filter(({policy}) => policy.indexOf('r') !== -1)
|
.filter(({policy}) => policy.indexOf('r') !== -1)
|
||||||
.map(({host}) => host),
|
.map(({host}) => host),
|
||||||
keyName: state => pubkey =>
|
keyName: state => pubkey =>
|
||||||
state.petnames[pubkey] ||
|
state.petnames[pubkey]
|
||||||
(state.metadata.get(pubkey) || {}).name ||
|
? state.petnames[pubkey].map(name => name.join('.')).join(', ')
|
||||||
(pubkey && pubkey.slice(0, 4) + '…' + pubkey.slice(-4)) ||
|
: (state.metadata.get(pubkey) || {}).name ||
|
||||||
''
|
(pubkey && pubkey.slice(0, 4) + '…' + pubkey.slice(-4)) ||
|
||||||
|
''
|
||||||
},
|
},
|
||||||
mutations,
|
mutations,
|
||||||
actions
|
actions
|
||||||
|
@ -87,17 +89,21 @@ async function init(store) {
|
||||||
(a, b) => b.split(':')[1] - a.split(':')[1]
|
(a, b) => b.split(':')[1] - a.split(':')[1]
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
db.cachedmetadata.toArray().then(metas => {
|
db.events
|
||||||
var metadata = {}
|
.where({kind: KIND_METADATA})
|
||||||
metas.forEach(({meta, pubkey}) => {
|
.toArray()
|
||||||
metadata[pubkey] = meta
|
.then(events => {
|
||||||
})
|
var metadata = {}
|
||||||
return metadata
|
events.forEach(({content, pubkey}) => {
|
||||||
}),
|
let meta = JSON.parse(content)
|
||||||
|
metadata[pubkey] = meta
|
||||||
|
})
|
||||||
|
return metadata
|
||||||
|
}),
|
||||||
db.contactlist.toArray().then(contacts => {
|
db.contactlist.toArray().then(contacts => {
|
||||||
var petnames = {}
|
var petnames = {}
|
||||||
contacts.forEach(({pubkey, name}) => {
|
contacts.forEach(({pubkey, name}) => {
|
||||||
petnames[pubkey] = name
|
petnames[pubkey] = [[name]]
|
||||||
})
|
})
|
||||||
return petnames
|
return petnames
|
||||||
})
|
})
|
||||||
|
@ -110,6 +116,12 @@ async function init(store) {
|
||||||
metadata: data[3],
|
metadata: data[3],
|
||||||
petnames: data[4]
|
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) {
|
function publishStatusLoader(store) {
|
||||||
|
|
Loading…
Reference in New Issue