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
|
||||
KindTextNote uint8 = 1
|
||||
KindRecommendServer uint8 = 2
|
||||
KindContactList uint8 = 3
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
})
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in New Issue