contacts-pane
Version:
Contacts Pane: Contacts manager for Address Book, Groups, and Individuals.
773 lines (720 loc) • 30.9 kB
JavaScript
// The tools pane is for managing and debugging and maintaining solid contacts databases
//
/* global confirm, $rdf */
import * as UI from 'solid-ui'
import { store } from 'solid-logic'
import { saveNewGroup, addPersonToGroup, groupMembers } from './contactLogic'
export function toolsPane (
selectAllGroups,
selectedGroups,
groupsMainTable,
book,
dataBrowserContext,
me
) {
const dom = dataBrowserContext.dom
const kb = store
const ns = UI.ns
const VCARD = ns.vcard
const buttonStyle = 'font-size: 100%; margin: 0.8em; padding:0.5em;'
const pane = dom.createElement('div')
const table = pane.appendChild(dom.createElement('table'))
table.setAttribute(
'style',
'font-size:120%; margin: 1em; border: 0.1em #ccc ;'
)
const headerRow = table.appendChild(dom.createElement('tr'))
headerRow.textContent = UI.utils.label(book) + ' - tools'
headerRow.setAttribute(
'style',
'min-width: 20em; padding: 1em; font-size: 150%; border-bottom: 0.1em solid red; margin-bottom: 2em;'
)
const statusRow = table.appendChild(dom.createElement('tr'))
const statusBlock = statusRow.appendChild(dom.createElement('div'))
statusBlock.setAttribute('style', 'padding: 2em;')
const MainRow = table.appendChild(dom.createElement('tr'))
const box = MainRow.appendChild(dom.createElement('table'))
table.appendChild(dom.createElement('tr')) // bottomRow
const context = {
target: book,
me: me,
noun: 'address book',
div: pane,
dom: dom,
statusRegion: statusBlock
}
function complain (message) {
console.log(message)
statusBlock.appendChild(UI.widgets.errorMessageBlock(dom, message, 'pink'))
}
// Body of main pane function
async function main () {
box.appendChild(
UI.aclControl.ACLControlBox5(
book.dir(),
dataBrowserContext,
'book',
kb,
function (ok, body) {
if (!ok) box.innerHTML = 'ACL control box Failed: ' + body
}
)
)
//
try {
await UI.login.registrationControl(context, book, ns.vcard('AddressBook'))
} catch (e) {
UI.widgets.complain(context, 'registrationControl: ' + e)
}
console.log('Registration control finished.')
// Output stats in line mode form
const logSpace = MainRow.appendChild(dom.createElement('pre'))
function log (message) {
console.log(message)
logSpace.textContent += message + '\n'
}
function stats () {
const totalCards = kb.each(undefined, VCARD('inAddressBook'), book).length
log('' + totalCards + ' cards loaded. ')
let groups = kb.each(book, VCARD('includesGroup'))
const strings = new Set(groups.map(group => group.uri)) // remove dups
groups = [...strings].map(uri => kb.sym(uri))
log('' + groups.length + ' total groups. ')
const gg = []
for (const g in selectedGroups) {
gg.push(g)
}
log('' + gg.length + ' selected groups. ')
}
async function loadIndexHandler (_event) {
loadIndexButton.setAttribute('style', 'background-color: #ffc;')
const nameEmailIndex = kb.any(book, ns.vcard('nameEmailIndex'))
try {
await kb.fetcher.load(nameEmailIndex)
} catch (e) {
loadIndexButton.setAttribute('style', 'background-color: #fcc;')
log('Error: People index has NOT been loaded' + e + '\n')
}
loadIndexButton.setAttribute('style', 'background-color: #cfc;')
log(' People index has been loaded\n')
} // loadIndexHandler
const loadIndexButton = pane.appendChild(dom.createElement('button'))
loadIndexButton.textContent = 'Load main index'
loadIndexButton.style.cssText = buttonStyle
loadIndexButton.addEventListener('click', loadIndexHandler)
const statButton = pane.appendChild(dom.createElement('button'))
statButton.textContent = 'Statistics'
statButton.style.cssText = buttonStyle
statButton.addEventListener('click', stats)
const checkAccessButton = pane.appendChild(dom.createElement('button'))
checkAccessButton.textContent =
'Check individual card access of selected groups'
checkAccessButton.style.cssText = buttonStyle
async function checkAcces (_event) {
function doCard (card) {
UI.widgets.fixIndividualCardACL(card, log, function (ok, message) {
if (ok) {
log('Success for ' + UI.utils.label(card))
} else {
log('Failure for ' + UI.utils.label(card) + ': ' + message)
}
})
}
const gg = []
for (const g in selectedGroups) {
gg.push(g)
}
for (let i = 0; i < gg.length; i++) {
const g = kb.sym(gg[i])
const a = groupMembers(kb, g)
log(UI.utils.label(g) + ': ' + a.length + ' members')
for (let j = 0; j < a.length; j++) {
const card = a[j]
log(UI.utils.label(card))
doCard(card)
}
}
}
checkAccessButton.addEventListener('click', checkAcces)
// ///////////////////////////////////////////////////////////////////////////
//
// DUPLICATES CHECK
const checkDuplicates = pane.appendChild(dom.createElement('button'))
checkDuplicates.textContent = 'Find duplicate cards'
checkDuplicates.style.cssText = buttonStyle
checkDuplicates.addEventListener('click', function (_event) {
const stats = {} // global god context
stats.book = book
stats.nameEmailIndex = kb.any(book, ns.vcard('nameEmailIndex'))
log('Loading name index...')
store.fetcher.nowOrWhenFetched(
stats.nameEmailIndex,
undefined,
function (_ok, _message) {
log('Loaded name index.')
stats.cards = []
stats.duplicates = []
stats.definitive = []
stats.nameless = []
stats.exactDuplicates = []
stats.nameOnlyDuplicates = []
stats.uniquesSet = []
stats.groupProblems = []
// Erase one card and all its files -> (err)
//
/*
function eraseOne (card) {
return new Promise(function (resolve, reject) {
function removeFromMainIndex () {
var indexBit = kb.connectedStatements(card, stats.nameEmailIndex)
log('Bits of the name index file:' + indexBit)
log('Patching main index file...')
kb.updater.update(indexBit, [], function (uri, ok, body) {
if (ok) {
log('Success')
resolve(null)
} else {
log('Error patching index file! ' + body)
reject('Error patching index file! ' + body)
}
})
}
var filesToDelete = [ card.doc() ]
var photos = kb.each(card, ns.vcard('hasPhoto')) // could be > 1
if (photos.length) {
filesToDelete = filesToDelete.concat(photos)
}
filesToDelete.push(card.dir()) // the folder last
log('Files to delete: ' + filesToDelete)
if (!confirm('DELETE card ' + card.dir() + ' for "' + kb.any(card, VCARD('fn')) + '", with ' + kb.each(card).length + 'statements?')) {
return resolve('Cancelled by user')
}
function deleteNextFile () {
var resource = filesToDelete.shift()
if (!resource) {
log('All deleted')
removeFromMainIndex()
resolve()
}
log('Deleting ... ' + resource)
kb.fetcher.delete(resource)
.then(function () {
log('Deleted ok: ' + resource)
deleteNextFile()
})
.catch(function (e) {
var err = '*** ERROR deleting ' + resource + ': ' + e
log(err)
if (confirm('Patch out index file for card ' + card.dir() + ' EVEN THOUGH card DELETE errors?')) {
removeFromMainIndex()
} else {
reject(err)
}
})
}
deleteNextFile()
}) // Promise
} // erase one
*/
// Check actual records to see which are exact matches - slow
stats.nameDupLog = kb.sym(book.dir().uri + 'dedup-nameDupLog.ttl')
stats.exactDupLog = kb.sym(book.dir().uri + 'dedup-exactDupLog.ttl')
/*
function checkOne (card) {
return new Promise(function (resolve, reject) {
var name = kb.anyValue(card, ns.vcard('fn'))
var other = stats.definitive[name]
kb.fetcher.load([card, other]).then(function (xhrs) {
var exclude = {}
exclude[ns.vcard('hasUID').uri] = true
exclude[ns.dc('created').uri] = true
exclude[ns.dc('modified').uri] = true
function filtered (x) {
return kb.statementsMatching(null, null, null, x.doc()).filter(function (st) {
return !exclude[st.predicate.uri]
})
}
var desc = filtered(card)
var desc2 = filtered(other)
// var desc = connectedStatements(card, card.doc(), exclude)
// var desc2 = connectedStatements(other, other.doc(), exclude)
if (desc.length !== desc2.length) {
log('CARDS to NOT match lengths ')
stats.nameOnlyDuplicates.push(card)
return resolve(false)
}
if (!desc.length) {
log('@@@@@@ Zero length ')
stats.nameOnlyDuplicates.push(card)
return resolve(false)
}
// //////// Compare the two
// Cheat: serialize and compare
// var cardText = $rdf.serialize(card.doc(), kb, card.doc().uri, 'text/turtle')
// var otherText = $rdf.serialize(other.doc(), kb, other.doc().uri, 'text/turtle')
var cardText = (new $rdf.Serializer(kb)).setBase(card.doc().uri).statementsToN3(desc)
var otherText = (new $rdf.Serializer(kb)).setBase(other.doc().uri).statementsToN3(desc2)
//
// log('Name: ' + name + ', statements: ' + desc.length)
// log('___________________________________________')
// log('KEEPING: ' + other.doc() + '\n' + cardText)
// log('___________________________________________')
// log('DELETING: '+ card.doc() + '\n' + otherText)
// log('___________________________________________')
//
if (cardText !== otherText) {
log('Texts differ')
stats.nameOnlyDuplicates.push(card)
return resolve(false)
}
var cardGroups = kb.each(null, ns.vcard('hasMember'), card)
var otherGroups = kb.each(null, ns.vcard('hasMember'), other)
for (var j = 0; j < cardGroups.length; j++) {
var found = false
for (var k = 0; k < otherGroups.length; k++) {
if (otherGroups[k].sameTerm(cardGroups[j])) { found = true }
}
if (!found) {
log('This one groups: ' + cardGroups)
log('Other one groups: ' + otherGroups)
log('Cant delete this one because it has a group, ' + cardGroups[j] + ', which the other does not.')
stats.nameOnlyDuplicates.push(card)
return resolve(false)
}
}
console.log('Group check done -- exact duplicate: ' + card)
stats.exactDuplicates.push(card)
resolve(true)
}).catch(function (e) {
log('Cant load a card! ' + [card, other] + ': ' + e)
stats.nameOnlyDuplicates.push(card)
resolve(false)
// if (confirm('Patch out index file for card ' + card.dir() + ' EVEN THOUGH card READ errors?')){
// removeFromMainIndex()
// }
})
})
} // checkOne
*/
stats.nameOnlyErrors = []
stats.nameLessZeroData = []
stats.nameLessIndex = []
stats.namelessUniques = []
stats.nameOnlyDuplicatesGroupDiff = []
function checkOneNameless (card) {
return new Promise(function (resolve) {
kb.fetcher
.load(card)
.then(function (_xhr) {
log(' Nameless check ' + card)
const exclude = {}
exclude[ns.vcard('hasUID').uri] = true
exclude[ns.dc('created').uri] = true
exclude[ns.dc('modified').uri] = true
function filtered (x) {
return kb
.statementsMatching(null, null, null, x.doc())
.filter(function (st) {
return !exclude[st.predicate.uri]
})
}
const desc = filtered(card)
// var desc = connectedStatements(card, card.doc(), exclude)
// var desc2 = connectedStatements(other, other.doc(), exclude)
if (!desc.length) {
log(' Zero length ' + card)
stats.nameLessZeroData.push(card)
return resolve(false)
}
// Compare the two
// Cheat: serialize and compare
// var cardText = $rdf.serialize(card.doc(), kb, card.doc().uri, 'text/turtle')
// var otherText = $rdf.serialize(other.doc(), kb, other.doc().uri, 'text/turtle')
const cardText = new $rdf.Serializer(kb)
.setBase(card.doc().uri)
.statementsToN3(desc)
const other = stats.nameLessIndex[cardText]
if (other) {
log(' Matches with ' + other)
// alain not sure it works we may need to concat with 'sameAs' group.doc (.map(st => st.why))
const cardGroups = kb.each(null, ns.vcard('hasMember'), card)
const otherGroups = kb.each(null, ns.vcard('hasMember'), other)
for (let j = 0; j < cardGroups.length; j++) {
let found = false
for (let k = 0; k < otherGroups.length; k++) {
if (otherGroups[k].sameTerm(cardGroups[j])) found = true
}
if (!found) {
log('This one groups: ' + cardGroups)
log('Other one groups: ' + otherGroups)
log(
'Cant skip this one because it has a group, ' +
cardGroups[j] +
', which the other does not.'
)
stats.nameOnlyDuplicatesGroupDiff.push(card)
return resolve(false)
}
}
console.log('Group check done -- exact duplicate: ' + card)
} else {
log('First nameless like: ' + card.doc())
log('___________________________________________')
log(cardText)
log('___________________________________________')
stats.nameLessIndex[cardText] = card
stats.namelessUniques.push(card)
}
resolve(true)
})
.catch(function (e) {
log('Cant load a nameless card!: ' + e)
stats.nameOnlyErrors.push(card)
resolve(false)
})
})
} // checkOneNameless
function checkAllNameless () {
stats.namelessToCheck =
stats.namelessToCheck || stats.nameless.slice()
log('Nameless check left: ' + stats.namelessToCheck.length)
return new Promise(function (resolve) {
const x = stats.namelessToCheck.shift()
if (!x) {
log('namelessUniques: ' + stats.namelessUniques.length)
log('namelessUniques: ' + stats.namelessUniques)
if (stats.namelessUniques.length > 0 &&
confirm(
'Add all ' +
stats.namelessUniques.length +
' nameless cards to the rescued set?'
)
) {
stats.uniques = stats.uniques.concat(stats.namelessUniques)
for (let k = 0; k < stats.namelessUniques.length; k++) {
stats.uniqueSet[stats.namelessUniques[k].uri] = true
}
}
return resolve(true)
}
checkOneNameless(x).then(function (exact) {
log(' Nameless check returns ' + exact)
checkAllNameless() // loop
})
})
}
function checkGroupMembers () {
return new Promise(function (resolve) {
// var inUniques = 0
log('Groups loaded')
for (let i = 0; i < stats.uniques.length; i++) {
stats.uniquesSet[stats.uniques[i].uri] = true
}
stats.groupMembers = []
kb.each(null, ns.vcard('hasMember'))
.map(group => { stats.groupMembers = stats.groupMembers.concat(groupMembers(kb, group)) })
log(' Naive group members ' + stats.groupMembers.length)
stats.groupMemberSet = []
for (let j = 0; j < stats.groupMembers.length; j++) {
stats.groupMemberSet[stats.groupMembers[j].uri] =
stats.groupMembers[j]
}
stats.groupMembers2 = []
for (const g in stats.groupMemberSet) {
stats.groupMembers2.push(stats.groupMemberSet[g])
}
log(' Compact group members ' + stats.groupMembers2.length)
if (
$rdf.keepThisCodeForLaterButDisableFerossConstantConditionPolice
) {
// Don't inspect as seems groups membership is complete
for (let i = 0; i < stats.groupMembers.length; i++) {
const card = stats.groupMembers[i]
if (stats.uniquesSet[card.uri]) {
// inUniques += 1
} else {
log(' Not in uniques: ' + card)
stats.groupProblems.push(card)
if (stats.duplicateSet[card.uri]) {
log(' ** IN duplicates alas:' + card)
} else {
log(' **** WTF?')
}
}
}
log('Problem cards: ' + stats.groupProblems.length)
} // if
resolve(true)
})
} // checkGroupMembers
function scanForDuplicates () {
return new Promise(function (resolve) {
stats.cards = kb.each(undefined, VCARD('inAddressBook'), stats.book)
log('' + stats.cards.length + ' total cards')
let c, card, name
for (c = 0; c < stats.cards.length; c++) {
card = stats.cards[c]
name = kb.anyValue(card, ns.vcard('fn'))
if (!name) {
stats.nameless.push(card)
continue
}
if (stats.definitive[name] === card) {
// pass
} else if (stats.definitive[name]) {
const n = stats.duplicates.length
if (n < 100 || (n < 1000 && n % 10 === 0) || n % 100 === 0) {
// log('' + n + ') Possible duplicate ' + card + ' of: ' + definitive[name])
}
stats.duplicates.push(card)
} else {
stats.definitive[name] = card
}
}
stats.duplicateSet = []
for (let i = 0; i < stats.duplicates.length; i++) {
stats.duplicateSet[stats.duplicates[i].uri] = stats.duplicates[i]
}
stats.namelessSet = []
for (let i = 0; i < stats.nameless.length; i++) {
stats.namelessSet[stats.nameless[i].uri] = stats.nameless[i]
}
stats.uniques = []
stats.uniqueSet = []
for (let i = 0; i < stats.cards.length; i++) {
const uri = stats.cards[i].uri
if (!stats.duplicateSet[uri] && !stats.namelessSet[uri]) {
stats.uniques.push(stats.cards[i])
stats.uniqueSet[uri] = stats.cards[i]
}
}
log('Uniques: ' + stats.uniques.length)
log('' + stats.nameless.length + ' nameless cards.')
log(
'' +
stats.duplicates.length +
' name-duplicate cards, leaving ' +
(stats.cards.length - stats.duplicates.length)
)
resolve(true)
})
}
// Save a new clean version
function saveCleanPeople () {
let cleanPeople
return Promise.resolve()
.then(() => {
cleanPeople = kb.sym(stats.book.dir().uri + 'clean-people.ttl')
let sts = []
for (let i = 0; i < stats.uniques.length; i++) {
sts = sts.concat(
kb.connectedStatements(stats.uniques[i], stats.nameEmailIndex)
)
}
const sz = new $rdf.Serializer(kb).setBase(stats.nameEmailIndex.uri)
log('Serializing index of uniques...')
const data = sz.statementsToN3(sts)
return kb.fetcher.webOperation('PUT', cleanPeople, {
data: data,
contentType: 'text/turtle'
})
})
.then(function () {
log('Done uniques log ' + cleanPeople)
return true
})
.catch(function (e) {
log('Error saving uniques: ' + e)
})
}
function saveCleanGroup (g) {
let cleanGroup
return Promise.resolve()
.then(() => {
const s = g.uri.replace('/Group/', '/NewGroup/')
cleanGroup = kb.sym(s)
let sts = []
for (let i = 0; i < stats.uniques.length; i++) {
sts = sts.concat(
kb.connectedStatements(stats.uniques[i], g.doc())
)
}
const sz = new $rdf.Serializer(kb).setBase(g.uri)
log(' Regenerating group of uniques...' + cleanGroup)
const data = sz.statementsToN3(sts)
return kb.fetcher.webOperation('PUT', cleanGroup, {
data: data,
contentType: 'text/turtle'
})
})
.then(() => {
log(' Done uniques group ' + cleanGroup)
return true
})
.catch(e => {
log('Error saving : ' + e)
})
}
function saveAllGroups () {
log('Saving ALL GROUPS')
return Promise.all(stats.groupObjects.map(saveCleanGroup))
}
const getAndSortGroups = function () {
let groups = []
if (stats.book) {
const books = [stats.book]
books.forEach(function (book) {
const gs = book ? kb.each(book, ns.vcard('includesGroup')) : []
const gs2 = gs.map(function (g) {
return [book, kb.any(g, ns.vcard('fn')), g]
})
groups = groups.concat(gs2)
})
groups.sort()
}
return groups
}
const groups = getAndSortGroups() // Needed?
stats.groupObjects = groups.map(gstr => gstr[2])
log('Loading ' + stats.groupObjects.length + ' groups... ')
kb.fetcher
.load(stats.groupObjects)
.then(scanForDuplicates)
.then(checkGroupMembers)
.then(checkAllNameless)
.then(() => {
return new Promise(function (resolve, reject) {
if (confirm('Write new clean versions?')) {
resolve(true)
} else {
reject()
}
})
})
.then(saveCleanPeople)
.then(saveAllGroups)
.then(function () {
log('Done!')
})
}
)
})
async function fixGroupless (book) {
const groupless = await getGroupless(book)
if (groupless.length === 0) {
log('No groupless cards found.')
return
}
const groupOfUngrouped = await saveNewGroup(book, 'ZSortThese')
if (confirm(`Add the ${groupless.length} cards without groups to a ZSortThese group?`)) {
for (const person of groupless) {
log(' adding ' + person)
await addPersonToGroup(person, groupOfUngrouped)
}
}
log('People moved to group.')
}
async function getGroupless (book) {
const groupIndex = kb.any(book, ns.vcard('groupIndex'))
const nameEmailIndex = kb.any(book, ns.vcard('nameEmailIndex'))
try {
await kb.fetcher.load([nameEmailIndex, groupIndex])
const groups = kb.each(book, ns.vcard('includesGroup'))
await kb.fetcher.load(groups)
} catch (e) {
complain('Error loading stuff:' + e)
}
const reverseIndex = {}
const groupless = []
let groups = kb.each(book, VCARD('includesGroup'))
const strings = new Set(groups.map(group => group.uri)) // remove dups
groups = [...strings].map(uri => kb.sym(uri))
log('' + groups.length + ' total groups. ')
for (let i = 0; i < groups.length; i++) {
const g = groups[i]
const a = groupMembers(kb, g)
log(UI.utils.label(g) + ': ' + a.length + ' members')
for (let j = 0; j < a.length; j++) {
kb.allAliases(a[j]).forEach(function (y) {
reverseIndex[y.uri] = g
})
}
}
const cards = kb.each(undefined, VCARD('inAddressBook'), book)
log('' + cards.length + ' total cards')
for (let c = 0; c < cards.length; c++) {
if (!reverseIndex[cards[c].uri]) {
groupless.push(cards[c])
log(' groupless ' + UI.utils.label(cards[c]))
}
}
log('' + groupless.length + ' groupless cards.')
return groupless
}
const checkGroupless = pane.appendChild(dom.createElement('button'))
checkGroupless.style.cssText = buttonStyle
checkGroupless.textContent = 'Find individuals with no group'
checkGroupless.addEventListener('click', function (_event) {
log('Loading groups...')
selectAllGroups(selectedGroups, groupsMainTable, async function (ok, message) {
if (!ok) {
log('Load all groups: failed: ' + message)
return
}
const nameEmailIndex = kb.any(book, ns.vcard('nameEmailIndex'))
try {
await kb.fetcher.load(nameEmailIndex)
} catch (e) {
complain(e)
}
log('Loaded groups and name index.')
getGroupless(book)
log('Groupless list finished..')
}) // select all groups then
})
const fixGrouplessButton = pane.appendChild(dom.createElement('button'))
fixGrouplessButton.style.cssText = buttonStyle
fixGrouplessButton.textContent = 'Put all individuals with no group in a new group'
fixGrouplessButton.addEventListener('click', _event => fixGroupless(book))
async function fixToOldDataModel (book) {
async function updateToOldDataModel(groups) {
let ds = []
let ins = []
groups.forEach(group => {
let vcardOrWebids = kb.statementsMatching(null, ns.owl('sameAs'), null, group.doc()).map(st => st.subject)
const strings = new Set(vcardOrWebids.map(contact => contact.uri)) // remove dups
vcardOrWebids = [...strings].map(uri => kb.sym(uri))
vcardOrWebids.forEach(item => {
if (!kb.each(item, ns.vcard('fn'), null, group.doc()).length) {
// delete item this is a new data model, item is a webid not a card.
ds = ds.concat(kb
.statementsMatching(item, ns.owl('sameAs'), null, group.doc())
.concat(kb.statementsMatching(undefined, undefined, item, group.doc())))
// add webid card to group
const cards = kb.each(item, ns.owl('sameAs'), null, group.doc())
cards.forEach(card => {
ins = ins.concat($rdf.st(card, ns.owl('sameAs'), item, group.doc()))
.concat($rdf.st(group, ns.vcard('hasMember'), card, group.doc()))
})
}
})
})
if (ds.length && confirm('Groups can be updated to old data model ?')) {
await kb.updater.updateMany(ds, ins)
alert('Update done')
} else { if (!ds.length) alert('Nothing to update.\nAll Groups already use the old data model.')}
}
let groups = kb.each(book, VCARD('includesGroup'))
const strings = new Set(groups.map(group => group.uri)) // remove dups
groups = [...strings].map(uri => kb.sym(uri))
updateToOldDataModel(groups)
}
const fixToOldDataModelButton = pane.appendChild(dom.createElement('button'))
fixToOldDataModelButton.style.cssText = buttonStyle
fixToOldDataModelButton.textContent = 'Revert groups to old data model'
fixToOldDataModelButton.addEventListener('click', _event => fixToOldDataModel(book))
} // main
main()
return pane
} // toolsPane
// ends