Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 14 additions & 22 deletions manifest.konnector
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
{
"version": "1.0.0",
"name": "Connector template",
"name": "Nextcloud",
"type": "konnector",
"language": "node",
"icon": "icon.svg",
"slug": "template",
"source": "git@github.com:konnectors/template.git",
"slug": "nextcloud",
"source": "git@github.com:konnectors/nextcloud.git",
"editor": "Cozy",
"vendor_link": "Link to the target website",
"vendor_link": "https://nextcloud.cozycloud.cc",
"categories": ["others"],
"fields": {
"login": {
"type": "text"
},
"password": {
"type": "password"
},
"url": {
"type": "text",
"label": "Your nextcloud full url (ex. \"https://nextcloud.cozycloud.cc\")"
}
},
"folders": [{"defaultDir": "$administrative/$konnector/$account"}],
"data_types": [
"bill"
"contact"
],
"screenshots": [],
"permissions": {
Expand All @@ -43,15 +47,9 @@
"langs": ["fr", "en"],
"locales": {
"fr": {
"short_description": "Récupère la liste de livres",
"long_description": "Récupère la liste de livre sur le site exemple",
"short_description": "Récupère la liste de contacts nextcloud",
"long_description": "Récupère la liste contact nextcloud sur le site.",
"permissions": {
"bank operations": {
"description": "Utilisé pour relier les factures à des operations bancaires"
},
"bills": {
"description": "Utilisé pour sauver les données des factures"
},
"files": {
"description": "Utilisé pour sauvegarder les factures"
},
Expand All @@ -61,15 +59,9 @@
}
},
"en": {
"short_description": "Fetch a list of books",
"long_description": "Fetch a list of books from the example website",
"short_description": "Fetch a list of nextcloud contacts",
"long_description": "Fetch a list of nextcloud contacts on the website",
"permissions": {
"bank operations": {
"description": "Required to link bank operations to bills"
},
"bills": {
"description": "Required to save the bills data"
},
"files": {
"description": "Required to save the bills"
},
Expand All @@ -79,6 +71,6 @@
}
}
},
"banksTransactionRegExp": "\\bbooks\\b",
"banksTransactionRegExp": "\\bnextcloud\\b",
"manifest_version": "2"
}
9 changes: 6 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
{
"name": "cozy-konnector-template",
"name": "nextcloud",
"version": "1.0.0",
"description": "",
"repository": {
"type": "git",
"url": "git@github.com:konnectors/cozy-konnector-template.git"
"url": "git@github.com:konnectors/nextcloud.git"
},
"keywords": [],
"author": "Cozy Cloud",
Expand Down Expand Up @@ -36,7 +36,10 @@
"travisDeployKey": "./bin/generate_travis_deploy_key"
},
"dependencies": {
"cozy-konnector-libs": "4.56.4"
"cozy-konnector-libs": "4.56.4",
"cozy-vcard": "^0.2.18",
"webdav": "^4",
"xml2js": "^0.5.0"
},
"devDependencies": {
"cozy-jobs-cli": "1.20.2",
Expand Down
161 changes: 55 additions & 106 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -1,124 +1,73 @@
const {
BaseKonnector,
requestFactory,
scrape,
log,
utils
} = require('cozy-konnector-libs')
/* eslint-disable no-unused-vars */
const { BaseKonnector, requestFactory, log } = require('cozy-konnector-libs')
const { NextcloudClient } = require('./nextcloudClient')
const VCardParser = require('cozy-vcard')

const request = requestFactory({
// The debug mode shows all the details about HTTP requests and responses. Very useful for
// debugging but very verbose. This is why it is commented out by default
// debug: true,
// Activates [cheerio](https://cheerio.js.org/) parsing on each page
cheerio: true,
// If cheerio is activated do not forget to deactivate json parsing (which is activated by
// default in cozy-konnector-libs
json: false,
// This allows request-promise to keep cookies between requests
cheerio: false,
json: true,
jar: true
})

const VENDOR = 'template'
const baseUrl = 'http://books.toscrape.com'
const cozyVCardParser = {
parse: data => {
const parser = new VCardParser(data)
return parser.contacts
}
}

module.exports = new BaseKonnector(start)

// The start function is run by the BaseKonnector instance only when it got all the account
// information (fields). When you run this connector yourself in "standalone" mode or "dev" mode,
// the account information come from ./konnector-dev-config.json file
// cozyParameters are static parameters, independents from the account. Most often, it can be a
// secret api key.
async function start(fields, cozyParameters) {
log('info', 'Authenticating ...')
if (cozyParameters) log('debug', 'Found COZY_PARAMETERS')
await authenticate.bind(this)(fields.login, fields.password)
const userCookies = await authenticate.bind(this)(fields)
log('info', 'Successfully logged in')
// The BaseKonnector instance expects a Promise as return of the function
log('info', 'Fetching the list of documents')
const $ = await request(`${baseUrl}/index.html`)
// cheerio (https://cheerio.js.org/) uses the same api as jQuery (http://jquery.com/)
log('info', 'Parsing list of documents')
const documents = await parseDocuments($)

// Here we use the saveBills function even if what we fetch are not bills,
// but this is the most common case in connectors
log('info', 'Saving data to Cozy')
await this.saveBills(documents, fields, {
// This is a bank identifier which will be used to link bills to bank operations. These
// identifiers should be at least a word found in the title of a bank operation related to this
// bill. It is not case sensitive.
identifiers: ['books']
})
const userContacts = await getUserContacts.bind(this)(fields, userCookies)
}

// This shows authentication using the [signin function](https://github.com/konnectors/libs/blob/master/packages/cozy-konnector-libs/docs/api.md#module_signin)
// even if this in another domain here, but it works as an example
function authenticate(username, password) {
return this.signin({
url: `http://quotes.toscrape.com/login`,
formSelector: 'form',
formData: { username, password },
// The validate function will check if the login request was a success. Every website has a
// different way to respond: HTTP status code, error message in HTML ($), HTTP redirection
// (fullResponse.request.uri.href)...
validate: (statusCode, $, fullResponse) => {
log(
'debug',
fullResponse.request.uri.href,
'not used here but should be useful for other connectors'
)
// The login in toscrape.com always works except when no password is set
if ($(`a[href='/logout']`).length === 1) {
return true
} else {
// cozy-konnector-libs has its own logging function which format these logs with colors in
// standalone and dev mode and as JSON in production mode
log('error', $('.error').text())
return false
}
}
})
}

// The goal of this function is to parse a HTML page wrapped by a cheerio instance
// and return an array of JS objects which will be saved to the cozy by saveBills
// (https://github.com/konnectors/libs/blob/master/packages/cozy-konnector-libs/docs/api.md#savebills)
function parseDocuments($) {
// You can find documentation about the scrape function here:
// https://github.com/konnectors/libs/blob/master/packages/cozy-konnector-libs/docs/api.md#scrape
const docs = scrape(
$,
{
title: {
sel: 'h3 a',
attr: 'title'
},
amount: {
sel: '.price_color',
parse: normalizePrice
},
fileurl: {
sel: 'img',
attr: 'src',
parse: src => `${baseUrl}/${src}`
}
async function authenticate(fields) {
const loginPage = await request(`${fields.url}/login`)
const requestToken = loginPage.match(/data-requesttoken="(.*)">/)[1]
const postLoginPage = await request(`${fields.url}/login`, {
method: 'POST',
form: {
user: fields.login,
password: fields.password,
timezone: 'Europe/Paris',
timezone_offset: 2,
requesttoken: requestToken
},
'article'
)
return docs.map(doc => ({
...doc,
// The saveBills function needs a date field
// even if it is a little artificial here (these are not real bills)
date: new Date(),
currency: 'EUR',
filename: `${utils.formatDate(new Date())}_${VENDOR}_${doc.amount.toFixed(
2
)}EUR${doc.vendorRef ? '_' + doc.vendorRef : ''}.jpg`,
vendor: VENDOR
}))
followRedirect: true,
resolveWithFullResponse: true
})
if (postLoginPage.request.uri.href !== `${fields.url}/apps/dashboard/`) {
log('warn', 'something went wrong with login')
throw new Error('LOGIN_FAILED')
}
const cookies = postLoginPage.request.headers.cookie.split('; ')
let userNumber
for (const cookie of cookies) {
if (cookie.startsWith('nc_username')) {
userNumber = cookie.split('=')[1]
}
}
return userNumber
}

// Convert a price string to a float
function normalizePrice(price) {
return parseFloat(price.replace('£', '').trim())
async function getUserContacts(fields, userNumber) {
log('info', 'getUserContacts starts')
const ncClient = new NextcloudClient({ fields, userNumber })
const client = ncClient.createClient()
const contactsVCards = await ncClient.getUserContacts(client)
let parsedContacts = []
for (const contactVCard of contactsVCards) {
const contact = cozyVCardParser.parse(contactVCard)
if (contact[0].n.match(';;;')) {
contact[0].n = contact[0].n.replace(/;;;/g, '').replace(';', ' ').trim()
}
parsedContacts.push(contact[0])
}
return parsedContacts
}
61 changes: 61 additions & 0 deletions src/nextcloudClient.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
const { createClient } = require('webdav')
const { log } = require('cozy-konnector-libs')
const xml2js = require('xml2js')

class NextcloudClient {
constructor({ fields, userNumber }) {
this.fields = fields
this.userNumber = userNumber
}
createClient() {
const client = createClient(
`${this.fields.url}/remote.php/dav/addressbooks/users/${this.userNumber}/default_shared_by_admin/`,
{ username: this.fields.login, password: this.fields.password }
)
return client
}

async getUserContacts(client) {
log('info', 'getUserContacts starts')
const resultContacts = await this.makeCustomRequest(client, '/')
const contactsHref = []
xml2js.parseString(resultContacts.data, function (err, result) {
if (err) {
log('warn', err)
return
}
const nodes = result['d:multistatus']['d:response']
for (const node of nodes) {
contactsHref.push(node['d:href'][0])
}
})
// Here we shifting the first element because it's just de base URL
contactsHref.shift()
const fullContactsVCARD = []
for (const contactHref of contactsHref) {
const contactCode = contactHref.split('admin')[1]
const fullContact = await this.makeCustomRequest(client, contactCode)
fullContactsVCARD.push(fullContact)
}
return fullContactsVCARD
}

async makeCustomRequest(client, path) {
log('info', 'makeCustomRequest starts')
if (path === '/') {
const result = await client.customRequest('/', {
method: 'PROPFIND',
headers: {
'Content-Type': 'text/xml'
}
})
return result
}
const result = await client.getFileContents(path, {
format: 'text'
})
return result
}
}

module.exports = { NextcloudClient }
Loading