Skip to content

Commit

Permalink
Images will be re-uploaded on ad top-up. This ensures that the images…
Browse files Browse the repository at this point in the history
… 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).
  • Loading branch information
Robert Honz committed Oct 28, 2021
1 parent c37a0a6 commit 6049cef
Show file tree
Hide file tree
Showing 6 changed files with 182 additions and 32 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
33 changes: 26 additions & 7 deletions src-electron/main-process/kleinanzeigen-workflow.js
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -92,7 +94,7 @@ export const adTopUp = async (id, price) => {
let adXmlPost = `<?xml version='1.0' encoding='UTF-8' standalone='yes' ?><ad:ad xmlns:types="http://www.ebayclassifiedsgroup.com/schema/types/v1" xmlns:cat="http://www.ebayclassifiedsgroup.com/schema/category/v1" xmlns:ad="http://www.ebayclassifiedsgroup.com/schema/ad/v1" xmlns:loc="http://www.ebayclassifiedsgroup.com/schema/location/v1" xmlns:attr="http://www.ebayclassifiedsgroup.com/schema/attribute/v1" xmlns:pic="http://www.ebayclassifiedsgroup.com/schema/picture/v1" xmlns:user="http://www.ebayclassifiedsgroup.com/schema/user/v1" xmlns:rate="http://www.ebayclassifiedsgroup.com/schema/rate/v1" xmlns:reply="http://www.ebayclassifiedsgroup.com/schema/reply/v1" xmlns:feed="http://www.ebayclassifiedsgroup.com/schema/feed/v1" locale="en_US" id="0"><ad:email>${settings.getSync('credentials.email')}</ad:email>`;
const k = new Kleinanzeigen();
const regexAmount = /amount>(.*)<\/types:amount>/;
const substAmounnt = `amount>${price}</types:amount>`;
const substAmount = `amount>${price}</types:amount>`;
const regexTitle = /<ad:title>.*<\/ad:title>/;
const regexDesc = /<ad:description>.*<\/ad:description>/;
const regexCat = /<cat:category id="\d+"/;
Expand All @@ -103,11 +105,23 @@ export const adTopUp = async (id, price) => {
const regexPosterType = /<ad:poster-type>.*<\/ad:poster-type>/;
const regexContactName = /<ad:contact-name>.*<\/ad:contact-name>/;
const regexAttr = /<attr:attributes>.*<\/attr:attributes>/;
const regexPics = /<pic:pictures>.*<\/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)} />`;
Expand All @@ -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(/&amp;/g, '&');
adXmlPost = he.decode(adXmlPost);
adXmlPost = adXmlPost.replace(/&/g, '&amp;');
adXmlPost += '</ad:ad>';

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;
Expand All @@ -145,3 +163,4 @@ export const getProfile = async () => {
throw e;
}
};

34 changes: 18 additions & 16 deletions src-electron/main-process/kleinanzeigen.js
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
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';
USER_AGENT = 'Dalvik/2.2.0';
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 {
this._email = settings.getSync('credentials.email');
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');
Expand All @@ -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);
}
Expand All @@ -45,7 +47,7 @@ export class Kleinanzeigen {
'X-EBAYK-APP': this.EBAYK_APP,
'Content-Type': 'application/xml',
'User-Agent': this.USER_AGENT
}
};
};

_generateHeaderPassword() {
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -348,7 +350,7 @@ export class Kleinanzeigen {
try {
const result = await this._changeAdStatus(id, 'active');

return result
return result;
} catch (e) {
throw e;
}
Expand All @@ -358,7 +360,7 @@ export class Kleinanzeigen {
try {
const result = await this._changeAdStatus(id, 'paused');

return result
return result;
} catch (e) {
throw e;
}
Expand Down Expand Up @@ -392,7 +394,7 @@ export class Kleinanzeigen {
try {
const result = await this._httpPostJsonFile('/pictures.json', path);

return result
return result;
} catch (e) {
throw e;
}
Expand Down
118 changes: 112 additions & 6 deletions src-electron/main-process/utilities.js
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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) => {
Expand All @@ -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});
};
4 changes: 3 additions & 1 deletion src/pages/Index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export default {
}
},
mounted() {
this.token = null;
this.$q.electron.ipcRenderer.on('m-login', (event, arg) => {
this.isLogin = arg;
Expand Down Expand Up @@ -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...'
});
}
}
Expand Down
Loading

0 comments on commit 6049cef

Please sign in to comment.