From 6049cef68a865946562e2d7b8e2466ac40da9486 Mon Sep 17 00:00:00 2001 From: Robert Honz Date: Thu, 28 Oct 2021 17:12:10 +0200 Subject: [PATCH] Images will be re-uploaded on ad top-up. This ensures that the images will not be deleted after 30 days. User feedback and ad list refreshing implemented (resolves #11). Fixes a login bug (when the token was expired). --- package.json | 1 + .../main-process/kleinanzeigen-workflow.js | 33 +++-- src-electron/main-process/kleinanzeigen.js | 34 ++--- src-electron/main-process/utilities.js | 118 +++++++++++++++++- src/pages/Index.vue | 4 +- src/pages/Overview.vue | 24 +++- 6 files changed, 182 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index ebb8f8f..784c342 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "electron-unhandled": "^3.0.2", "electron-util": "^0.14.2", "form-data": "^4.0.0", + "he": "^1.2.0", "lodash": "^4.17.20", "quasar": "^1.16.0", "vue-i18n": "^8.0.0", diff --git a/src-electron/main-process/kleinanzeigen-workflow.js b/src-electron/main-process/kleinanzeigen-workflow.js index 475a8c3..7b87138 100644 --- a/src-electron/main-process/kleinanzeigen-workflow.js +++ b/src-electron/main-process/kleinanzeigen-workflow.js @@ -1,5 +1,7 @@ -import { Kleinanzeigen } from './kleinanzeigen'; +import {Kleinanzeigen} from './kleinanzeigen'; import settings from 'electron-settings'; +import {reUploadImages, xmlBuilderPictureLinks} from './utilities'; +import he from 'he'; export const login = async () => { @@ -92,7 +94,7 @@ export const adTopUp = async (id, price) => { let adXmlPost = `${settings.getSync('credentials.email')}`; const k = new Kleinanzeigen(); const regexAmount = /amount>(.*)<\/types:amount>/; - const substAmounnt = `amount>${price}`; + const substAmount = `amount>${price}`; const regexTitle = /.*<\/ad:title>/; const regexDesc = /.*<\/ad:description>/; const regexCat = / { const regexPosterType = /.*<\/ad:poster-type>/; const regexContactName = /.*<\/ad:contact-name>/; const regexAttr = /.*<\/attr:attributes>/; - const regexPics = /.*<\/pic:pictures>/; + let ad = null; + let xmlPictures = ''; + + try { + ad = await k.getAd(id); + } catch (e) { + throw e; + } + + if ('pictures' in ad) { + const adImageUrls = await reUploadImages(ad.pictures.picture); + xmlPictures = xmlBuilderPictureLinks(adImageUrls); + } try { const adXml = await k.getAdXml(id); - const adXmlPrice = adXml.replace(regexAmount, substAmounnt); + const adXmlPrice = adXml.replace(regexAmount, substAmount); adXmlPost += adXmlPrice.match(regexTitle); adXmlPost += adXmlPrice.match(regexDesc); adXmlPost += `${adXmlPrice.match(regexCat)} />`; @@ -117,15 +131,19 @@ export const adTopUp = async (id, price) => { adXmlPost += adXmlPrice.match(regexPosterType); adXmlPost += adXmlPrice.match(regexContactName); adXmlPost += adXmlPrice.match(regexAttr); - adXmlPost += adXmlPrice.match(regexPics); + adXmlPost += xmlPictures; + // Weird char encoding / decoding magic to comply with XML encoding. + adXmlPost = adXmlPost.replace(/&/g, '&'); + adXmlPost = he.decode(adXmlPost); + adXmlPost = adXmlPost.replace(/&/g, '&'); adXmlPost += ''; const resultCreate = await k.createAd(adXmlPost); - let resultDelete = null; + let resultDelete = false; if (resultCreate === true) { resultDelete = await k.deleteAd(id); - // TODO: Check if creation was successfull otherwise delete newly created item. + // TODO: Check if deletion was successful otherwise delete newly created item. } return resultCreate, resultDelete; @@ -145,3 +163,4 @@ export const getProfile = async () => { throw e; } }; + diff --git a/src-electron/main-process/kleinanzeigen.js b/src-electron/main-process/kleinanzeigen.js index 75db307..a252773 100644 --- a/src-electron/main-process/kleinanzeigen.js +++ b/src-electron/main-process/kleinanzeigen.js @@ -1,10 +1,10 @@ import axios from 'axios'; -import { RemoteSystemError, AuthorizationError, RemoteNotFound, AttributeError, AxiosError } from './exceptions'; +import {AttributeError, AuthorizationError, AxiosError, RemoteNotFound, RemoteSystemError} from './exceptions'; import settings from 'electron-settings'; import crypto from 'crypto'; import _ from 'lodash'; import FormData from 'form-data'; -import { readFileAsync } from './utilities'; +import {readFileAsync} from './utilities'; export class Kleinanzeigen { APK_APP_VERSION = '13.4.2'; @@ -12,7 +12,7 @@ export class Kleinanzeigen { BASE_URL = 'https://api.ebay-kleinanzeigen.de/api'; EBAYK_APP = '13a6dde3-935d-4cd8-9992-db8a8c4b6c0f1456515662229'; BASIC_AUTH_USER = 'android'; - BASIC_AUTH_PASSWORD = 'TaR60pEttY'; + BASIC_AUTH_PASSWORD = 'TaR60pEttY'; constructor() { try { @@ -20,7 +20,7 @@ export class Kleinanzeigen { this._password = settings.getSync('credentials.password'); this._token = settings.getSync('credentials.token'); } catch (e) { - throw new AttributeError('Credentials not set. Please provide e-mail address and password.') + throw new AttributeError('Credentials not set. Please provide e-mail address and password.'); } this._passwordHashed = crypto.createHash('sha1').update(this._password, 'utf8').digest('base64'); @@ -32,7 +32,9 @@ export class Kleinanzeigen { username: this.BASIC_AUTH_USER, password: this.BASIC_AUTH_PASSWORD }, - validateStatus: function (status) { return true; } + validateStatus: function (status) { + return true; + } }; this._axios = axios.create(axiosConfig); } @@ -45,7 +47,7 @@ export class Kleinanzeigen { 'X-EBAYK-APP': this.EBAYK_APP, 'Content-Type': 'application/xml', 'User-Agent': this.USER_AGENT - } + }; }; _generateHeaderPassword() { @@ -115,7 +117,7 @@ export class Kleinanzeigen { _getJsonContent(data) { let content = null; - _.forOwn(data, function(value, key) { + _.forOwn(data, function (value, key) { if (key.startsWith('{http')) { content = data[key].value; } @@ -128,7 +130,7 @@ export class Kleinanzeigen { let data = null; try { - data = await this._httpGetData(urlSuffix) + data = await this._httpGetData(urlSuffix); } catch (e) { throw e; } @@ -142,7 +144,7 @@ export class Kleinanzeigen { let data = null; try { - data = await this._httpGetHeaders(urlSuffix) + data = await this._httpGetHeaders(urlSuffix); } catch (e) { throw e; } @@ -158,7 +160,7 @@ export class Kleinanzeigen { response = await this._axios.put(urlSuffix, data); } catch (e) { console.log(e); - throw AxiosError(e) + throw AxiosError(e); } this._validateHttpResponse(response); @@ -173,7 +175,7 @@ export class Kleinanzeigen { response = await this._axios.delete(urlSuffix); } catch (e) { console.log(e); - throw AxiosError(e) + throw AxiosError(e); } this._validateHttpResponse(response); @@ -188,7 +190,7 @@ export class Kleinanzeigen { response = await this._axios.post(urlSuffix, data); } catch (e) { console.log(e); - throw AxiosError(e) + throw AxiosError(e); } this._validateHttpResponse(response); @@ -312,7 +314,7 @@ export class Kleinanzeigen { let content = null; try { - content = await this._httpGetData(urlSuffix); + content = await this._httpGetData(urlSuffix); } catch (e) { throw e; } @@ -348,7 +350,7 @@ export class Kleinanzeigen { try { const result = await this._changeAdStatus(id, 'active'); - return result + return result; } catch (e) { throw e; } @@ -358,7 +360,7 @@ export class Kleinanzeigen { try { const result = await this._changeAdStatus(id, 'paused'); - return result + return result; } catch (e) { throw e; } @@ -392,7 +394,7 @@ export class Kleinanzeigen { try { const result = await this._httpPostJsonFile('/pictures.json', path); - return result + return result; } catch (e) { throw e; } diff --git a/src-electron/main-process/utilities.js b/src-electron/main-process/utilities.js index 9ec6b56..575c2c0 100644 --- a/src-electron/main-process/utilities.js +++ b/src-electron/main-process/utilities.js @@ -1,9 +1,15 @@ -import { RemoteSystemError, AuthorizationError, RemoteNotFound, AttributeError, AxiosError } from './exceptions'; -import { readFile } from 'fs'; +import {AttributeError, AuthorizationError, AxiosError, RemoteNotFound, RemoteSystemError} from './exceptions'; +import {createWriteStream, mkdtemp, readFile, unlink} from 'fs'; +import axios from 'axios'; +import _ from 'lodash'; +import {join} from 'path'; +import {Kleinanzeigen} from './kleinanzeigen'; +import {tmpdir} from 'os'; +import builder from 'xmlbuilder'; export const generateExceptionStr = (exc) => { - return `${exc.name}: ${exc.message}` -} + return `${exc.name}: ${exc.message}`; +}; export const exceptionHandler = (exc, ipcEvent) => { if (exc.constructor === AuthorizationError) { @@ -19,7 +25,7 @@ export const exceptionHandler = (exc, ipcEvent) => { } else { ipcEvent.reply('m-error-general', exc.message); } -} +}; export const readFileAsync = async function (path) { return new Promise((resolve, reject) => { @@ -31,4 +37,104 @@ export const readFileAsync = async function (path) { return resolve(data); }); }); -} +}; +export const createTempDir = async function (prefix = 'ebk-') { + return new Promise((resolve, reject) => { + mkdtemp(join(tmpdir(), prefix), (err, folder) => { + if (err) { + return reject(err); + } + + return resolve(folder); + }); + }); +}; + +export const reUploadImages = async function (adPictures) { + const adImageUrls = []; + const adImagePath = []; + let tmpDirPath = null; + + // Create tmp dir + try { + tmpDirPath = await createTempDir(); + } catch (e) { + throw e; + } + + // Download images + for (const arrayItem of adPictures) { + const imgUrl = _.find(arrayItem.link, function (o) { + return o.rel === 'XXL'; + }).href; + const imgUrlSplit = imgUrl.split('/'); + const imgNameNew = `${imgUrlSplit[imgUrlSplit.length - 2]}_${imgUrlSplit[imgUrlSplit.length - 1]}`; + const pathDest = join(tmpDirPath, imgNameNew); + + try { + const data = await downloadImage(imgUrl, pathDest); + + adImagePath.push(pathDest); + } catch (e) { + throw e; + } + } + + // Upload images + const k = new Kleinanzeigen(); + + for (const aip of adImagePath) { + try { + const imageLinks = await k.uploadPicture(aip); + + adImageUrls.push(imageLinks); + + unlink(aip, function (err) { + if (err) return console.log(err); + }); + } catch (e) { + throw e; + } + } + + return adImageUrls; +}; + +export const downloadImage = async function (url, path) { + const writer = createWriteStream(path); + + const response = await axios({ + url, + method: 'GET', + responseType: 'stream' + }); + + response.data.pipe(writer); + + return new Promise((resolve, reject) => { + writer.on('finish', resolve); + writer.on('error', reject); + }); +}; + +export const xmlBuilderPictureLinks = function (adImageUrls) { + const picPicture = []; + + for (const aiu of adImageUrls) { + const picLink = []; + + for (const linkObj of aiu.link) { + picLink.push({ + '@href': linkObj.href, + '@rel': linkObj.rel + }); + } + + picPicture.push({'pic:link': picLink}); + } + + const picPictures = {'pic:picture': picPicture}; + const root = {'pic:pictures': picPictures}; + const xmlFeedPictures = builder.create(root, {encoding: 'utf-8', headless: true}); + return xmlFeedPictures.end({pretty: false}); +}; diff --git a/src/pages/Index.vue b/src/pages/Index.vue index 08a601b..0b4d33c 100644 --- a/src/pages/Index.vue +++ b/src/pages/Index.vue @@ -31,6 +31,8 @@ export default { } }, mounted() { + this.token = null; + this.$q.electron.ipcRenderer.on('m-login', (event, arg) => { this.isLogin = arg; @@ -61,7 +63,7 @@ export default { }, showLoading: function() { this.$q.loading.show({ - message: 'Anmeldung läuft. Informationen werden abgefragt...' + message: 'Anmeldung läuft. Informationen werden geladen...' }); } } diff --git a/src/pages/Overview.vue b/src/pages/Overview.vue index e98d27e..896b25f 100644 --- a/src/pages/Overview.vue +++ b/src/pages/Overview.vue @@ -21,7 +21,7 @@ q-item-section(avatar) q-icon(name="language") q-item-section - q-item-label {{ad.title.value}} + q-item-label {{ heDecode(ad.title.value) }} q-item q-item-section(avatar) q-icon(name="error") @@ -32,7 +32,7 @@ q-icon.on-left.on-right(name="star") | {{ ad.count_watch}} q-icon.on-left.on-right(name="event") - | {{ ad['start-date-time'].value.substring(0, 10) }} + | {{ formatStartDateTime(ad['start-date-time']) }} q-item q-item-section(avatar) q-item-section @@ -83,6 +83,7 @@ import { ek } from 'src/mixins/ek' import { user } from 'src/mixins/user' import { electronHelper } from 'src/mixins/electronHelper' +import he from 'he'; export default { name: 'PageOverview', @@ -124,6 +125,15 @@ export default { } }) + this.$q.electron.ipcRenderer.on('m-ads-topup', (event, arg) => { + if (arg === true) { + this.$toasted.success('Die Anzeige wurde erfolgreich nach oben geschoben.'); + this.getAds(); + } else { + this.$toasted.error('Oops. Da ist etwas schief gelaufen.'); + } + }) + if (this.ads != undefined) { if (this.ads.length === 0) { this.getAds() @@ -200,6 +210,16 @@ export default { const { value: amount } = ad.price.amount const currency = ad.price['currency-iso-code'].value['localized-label'] return `${amount} ${currency}` + priceSuffix + }, + heDecode (str) { + return he.decode(str); + }, + formatStartDateTime(adStartDateTime) { + if ('value' in adStartDateTime) { + return adStartDateTime.value.substring(0, 10); + } else { + return new Date().toISOString().substring(0, 10); + } } } }