├── static ├── messages.json ├── tokens.json ├── schedules.json ├── img_upload_bottom.txt ├── favicon.ico ├── img_upload_top.txt ├── login.css ├── style2.css └── style.css ├── requirements.txt ├── templates ├── force.html ├── results.html ├── index.html ├── layout.html ├── layout2.html ├── profile.html ├── conversations.html ├── autoreplier.html ├── schedule.html ├── conversation.html ├── home.html ├── ad.html ├── post.html ├── search.html └── stage2.html ├── LICENSE ├── README.md ├── kijijiapi.py └── server.py /static/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [] 3 | } -------------------------------------------------------------------------------- /static/tokens.json: -------------------------------------------------------------------------------- 1 | { 2 | "users": [] 3 | } -------------------------------------------------------------------------------- /static/schedules.json: -------------------------------------------------------------------------------- 1 | { 2 | "schedules": [] 3 | } -------------------------------------------------------------------------------- /static/img_upload_bottom.txt: -------------------------------------------------------------------------------- 1 | 2 | ------FormBoundary7MA4YWxkTrZu0gW-- 3 | -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rybodiddly/Kijiji-Reposter/HEAD/static/favicon.ico -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | apscheduler 2 | flask 3 | flask-wtf 4 | httpx 5 | pgeocode 6 | xmltodict 7 | urllib3 8 | -------------------------------------------------------------------------------- /templates/force.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout2.html' %} 2 | 3 | {% block title %}Force Post Ad{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 |

Force Post Ad

9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
Full Ad File Path:
Email:
Password:
25 | 26 |
27 | 28 |
29 | {% endblock %} -------------------------------------------------------------------------------- /templates/results.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout.html' %} 2 | 3 | {% block title %}Results{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {% if data != None %} 17 | {% for item in data %} 18 | 19 | 20 | 21 | 22 | 24 | {% endfor %} 25 | {% endif %} 26 |
ImageAd #TitlePrice
{{ item.id }}{{ item.title }}{{ item.price }} 23 |
27 |
28 | 40 | {% endblock %} 41 | -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Login 6 | 7 | 8 | 9 | 10 |
11 |

Login

12 | 15 |
16 | 17 | 20 | 21 | 22 | 25 | 26 | 27 |
{{ msg }}
28 | 29 |
30 |
31 | 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 RyboDiddly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 23 |
24 | {% block content %}{% endblock %} 25 |
26 | 27 | -------------------------------------------------------------------------------- /templates/layout2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% block title %}{% endblock %} 6 | 7 | 8 | 9 | 10 | 11 | 23 |
24 | {% block content %}{% endblock %} 25 |
26 | 27 | -------------------------------------------------------------------------------- /templates/profile.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout2.html' %} 2 | 3 | {% block title %}Profile{% endblock %} 4 | 5 | {% block content %} 6 |

Profile

7 |
8 |

Your account details are below:

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 |
Display Name{{ data['user:user-profile']['user:user-display-name'] }}
Nickname{{ data['user:user-profile']['user:user-nickname'] }}
User ID #{{ data['user:user-profile']['user:user-id'] }}
User Email{{ data['user:user-profile']['user:user-email'] }}
Registration Date{{ data['user:user-profile']['user:user-registration-date']|convert }}
Reply Rate{{ data['user:user-profile']['user:reply-rate'] }}
Review Score{{ data['user:user-profile']['user:average-review-score'] }}
39 |
40 | {% endblock %} -------------------------------------------------------------------------------- /templates/conversations.html: -------------------------------------------------------------------------------- 1 | 2 | {% set link = page|increment %} 3 | {% extends 'layout.html' %} 4 | 5 | {% block title %}Conversations{% endblock %} 6 | 7 | {% block content %} 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {% if 'user:user-conversation' in conversations['user:user-conversations'] %} 20 | {% for item in conversations['user:user-conversations']['user:user-conversation'] %} 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | {% endfor %} 29 | {% else %} 30 | 31 | 32 | 33 | {% endif %} 34 |
ImageFromSubjectReadDate
{{ item['user:ad-replier-name'] }}{{ item['user:ad-subject'] }}{{ item['user:user-message']['user:read'] }}{{ item['user:user-message']['user:post-time-stamp']|convert }}
35 |
36 | 48 | {% endblock %} -------------------------------------------------------------------------------- /static/img_upload_top.txt: -------------------------------------------------------------------------------- 1 | ------FormBoundary7MA4YWxkTrZu0gW 2 | Content-Disposition: form-data; name="XML Payload" 3 | 4 | 5 | 6 | 7 | AgAAAA**AQAAAA**aAAAAA**y9arYQ**nY+sHZ2PrBmdj6wVnY+sEZ2PrA2dj6MHloakAZGGqQSdj6x9nY+seQ**j8wBAA**AAMAAA**XCyjvCgYtZFhE1DSKsy4Fhz8o9eiKRNMtKr/P1ltxsz2B9XZgQOhlI0lG8NUmFbnltvBo5GsIYrQ56N/lZW8/yzmUczzl87c+iDF1GBH+La4WuDnGOTMlg2gLbroX7gw87xBvRaaUPRP7ZaLzhO6MRbGrTByZf/C+VphIZ5tzsUHJvxvjsgAuEaQ/vTvNfrd23FKJ8CgBt7tcK0YEJM0uBwgTzcACe2XnUKZF2f5C8AeS6z5vH5U9rf7r0CZ6BG5o2sD9umOJYCm9VimQbI/7+FCQNsqcV8erS1sng9/0fIHVk9wgfYF0yTWn0Qn8Q/Vc3jBbIAPqWKRlm2tSdjYwV8hVRNOhe7EfyHt+cpPpU6GwngeZc3+lH4eyl83443wlbRv3IWuH+eB9rdhRg1J1UIfBN5R4O1CdHPDLzRBBgduMTSB5RVHJP4cEFQL2uycW2LJq6HAFcqt3qW2mF6HdQZ8nHa3FuN7cESK4hoxJ2JABLAGHwFEDZNDlCr0gFSBZ5ajni92yxUB9h2MkBD2gMHf8qZGqHCIQVNBnEs81Dq+BK0Qc1FLaYtxQaWErTq4nOd76SkKE7uqqK4IRAhJ/hcyeqPBFSe5bNyNtiSE1g9ff0g1KaV9n5Zry6rJEr9/gOunTiTN9qLDpf2sYlD3tJL3cCSsgLCZlpAMt3+YXDmK6Fx1UA5BQAg5UIeWr1QrE4w/YEKfbD3zdf7Ww5/qIhw5trjdqvbh6Z9tHq9YGBu+wUm4AA0hHdMGa7acgU7L 8 | 9 | Kijiji CA Image 10 | Supersize 11 | 365 12 | 13 | 14 | 15 | ------FormBoundary7MA4YWxkTrZu0gW 16 | Content-Disposition: form-data; name="Kijiji CA Image"; filename="image.jpg" 17 | Content-Type: image/jpeg 18 | 19 | -------------------------------------------------------------------------------- /templates/autoreplier.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout2.html' %} 2 | 3 | {% block title %}Auto Message Replier Settings{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 |

Auto Message Replier Settings

9 |
10 | 11 | {% for rule in rules['rules'] %} 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {% endfor %} 23 |
Rule{{rule['rule']}}
Response{{rule['response']}}
24 |
25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 |
Rule
Response
Password
42 |
43 |

To create a new rule, enter the new rule and response criteria in the fields above. Rules represent the search
44 | criteria used by the autoreplier to locate text within received messages, responses are what is returned.


45 | 46 |
47 | 48 |
49 | {% endblock %} -------------------------------------------------------------------------------- /templates/schedule.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout2.html' %} 2 | 3 | {% block title %}{{adID}} Repost Schedule{% endblock %} 4 | 5 | {% block content %} 6 |

{{ adID }} Repost Schedule

7 |
8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
Time 1:{{ schedules[0] }}
Time 2:{{ schedules[1] }}
Time 3:{{ schedules[2] }}
Time 4:{{ schedules[3] }}
Time 5:{{ schedules[4] }}
Time 6:{{ schedules[5] }}
Time 7:{{ schedules[6] }}
Time 8:{{ schedules[7] }}
51 | 52 |
53 |

To change a schedule, enter a new time and click update.
54 | To remove a schedule, enter "NONE"
55 | Times must be in 24hr format. Example: 07:00, 13:00, 21:35

56 | 57 | 58 |
59 |
60 | {% endblock %} -------------------------------------------------------------------------------- /templates/conversation.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'layout.html' %} 3 | 4 | {% block title %}Kijiji Reposter{% endblock %} 5 | 6 | {% block content %} 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | {% if 'user:user-message' in conversation['user:user-conversation'] %} 17 | {% if conversation|testreplylist == true %} 18 | {% for item in conversation['user:user-conversation']['user:user-message'] %} 19 | 20 | 21 | 22 | 23 | 24 | {% endfor %} 25 | {% else %} 26 | 27 | 28 | 29 | 30 | 31 | {% endif %} 32 | {% endif %} 33 |
SenderMessageDate
{{ item['user:sender-name'] }}{{ item['user:msg-content'] }}{{ item['user:post-time-stamp']|convert }}
{{ conversation['user:user-conversation']['user:user-message']['user:sender-name'] }}{{ conversation['user:user-conversation']['user:user-message']['user:msg-content'] }}{{ conversation['user:user-conversation']['user:user-message']['user:post-time-stamp']|convert }}
34 |
35 |
36 | 37 | 38 | 39 | 40 |
{{ form.reply.label }}
{{ form.reply }}
41 |
42 |
43 | 49 | {% endblock %} -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | {% extends 'layout.html' %} 3 | 4 | {% block title %}Kijiji Reposter{% endblock %} 5 | 6 | {% block content %} 7 | 8 |
9 |

Welcome back, {{ email }}!

10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | {% if 'ad:ad' in data['ad:ads'] %} 21 | {% if data|testlist == true %} 22 | {% for item in data['ad:ads']['ad:ad'] %} 23 | 24 | 25 | 26 | 27 | 28 | 35 | 36 | 37 | 38 | 39 | 40 | {% endfor %} 41 | {% else %} 42 | 43 | 44 | 45 | 46 | 47 | 54 | 55 | 56 | 57 | 58 | 59 | {% endif %} 60 | {% endif %} 61 |
ImageAd #TitleCategoryPriceCreatedExpires
{{ item['@id'] }}{{ item['ad:title'] }}{{ item['cat:category']['@id'] }}{% if 'ad:price' in item %} 29 | {% if item['ad:price']['types:price-type']['types:value'] == 'SPECIFIED_AMOUNT'%} 30 | {{ item['ad:price']['types:amount'] }} 31 | {% else %} 32 | {{ item['ad:price']['types:price-type']['types:value'] }} 33 | {% endif %} 34 | {% endif %}{{ item['ad:start-date-time']|convert }}{{ item['ad:end-date-time']|convert }}{% if item['@id']|checkSchedule == true %}{% endif %}
{{ data['ad:ads']['ad:ad']['@id'] }}{{ data['ad:ads']['ad:ad']['ad:title'] }}{{ data['ad:ads']['ad:ad']['cat:category']['@id'] }}{% if 'ad:price' in data['ad:ads']['ad:ad'] %} 48 | {% if data['ad:ads']['ad:ad']['ad:price']['types:price-type']['types:value'] == 'SPECIFIED_AMOUNT'%} 49 | {{ data['ad:ads']['ad:ad']['ad:price']['types:amount'] }} 50 | {% else %} 51 | {{ data['ad:ads']['ad:ad']['ad:price']['types:price-type']['types:value'] }} 52 | {% endif %} 53 | {% endif %}{{ data['ad:ads']['ad:ad']['ad:start-date-time']|convert }}{{ data['ad:ads']['ad:ad']['ad:end-date-time']|convert }}{% if data['ad:ads']['ad:ad']['@id']|checkSchedule == true %}{% endif %}
62 |
63 | 75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /templates/ad.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout2.html' %} 2 | 3 | {% block title %}Ad {{adID}} {% endblock %} 4 | 5 | {% block content %} 6 |

Ad {{ adID }}

7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
Ad title{{ data['ad:ad']['ad:title'] }}
Category{{ data['ad:ad']['cat:category']['cat:id-name'] }}
Category ID{{ data['ad:ad']['cat:category']['@id'] }}
Price{% if 'ad:price' in data['ad:ad'] %}{% if data['ad:ad']['ad:price']['types:price-type']['types:value'] == 'SPECIFIED_AMOUNT' %}{{ data['ad:ad']['ad:price']['types:amount'] }}{% else %}None{% endif %}{% else %}None{% endif %}
Description{{ data['ad:ad']['ad:description'] }}
Seller ID{{ data['ad:ad']['ad:user-id'] }}
Location ID{{ data['ad:ad']['loc:locations']['loc:location']['@id'] }}
Longitude{{ data['ad:ad']['loc:locations']['loc:location']['loc:longitude'] }}
Latitude{{ data['ad:ad']['loc:locations']['loc:location']['loc:latitude'] }}
Address{{ data['ad:ad']['ad:ad-address']['types:full-address'] }}
Creation Date{{ data['ad:ad']['ad:creation-date-time']|convert }}
Start Date{{ data['ad:ad']['ad:start-date-time']|convert }}
End Date{{ data['ad:ad']['ad:end-date-time']|convert }}
Ranking{{ data['ad:ad']['ad:rank'] }}
Views{{ data['ad:ad']['ad:view-ad-count'] }}
Pics{% for item in data['ad:ad']|imglist %}{{item+"\n"}}{% endfor %}
74 | {% if data['ad:ad']['ad:user-id'] != userID %} 75 |
76 |
77 | 78 | 79 | 80 | 81 |
{{ form.reply.label }}
{{ form.reply }}
82 |
83 | {% endif %} 84 |
85 | 90 | {% endblock %} 91 | -------------------------------------------------------------------------------- /static/login.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: -apple-system, BlinkMacSystemFont, "segoe ui", roboto, oxygen, ubuntu, cantarell, "fira sans", "droid sans", "helvetica neue", Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | body { 9 | background-color: #435165; 10 | margin: 0; 11 | } 12 | .login, .register { 13 | width: 400px; 14 | background-color: #ffffff; 15 | box-shadow: 0 0 9px 0 rgba(0, 0, 0, 0.3); 16 | margin: 100px auto; 17 | } 18 | .login h1, .register h1 { 19 | text-align: center; 20 | color: #5b6574; 21 | font-size: 24px; 22 | padding: 20px 0 20px 0; 23 | border-bottom: 1px solid #dee0e4; 24 | } 25 | .login .links, .register .links { 26 | display: flex; 27 | padding: 0 15px; 28 | } 29 | .login .links a, .register .links a { 30 | color: #adb2ba; 31 | text-decoration: none; 32 | display: inline-flex; 33 | padding: 0 10px 10px 10px; 34 | font-weight: bold; 35 | } 36 | .login .links a:hover, .register .links a:hover { 37 | color: #9da3ac; 38 | } 39 | .login .links a.active, .register .links a.active { 40 | border-bottom: 3px solid #373373; 41 | color: #373373; 42 | } 43 | .login form, .register form { 44 | display: flex; 45 | flex-wrap: wrap; 46 | justify-content: center; 47 | padding-top: 20px; 48 | } 49 | .login form label, .register form label { 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | width: 50px; 54 | height: 50px; 55 | background-color: #373373; 56 | color: #ffffff; 57 | } 58 | .login form input[type="password"], .login form input[type="text"], .login form input[type="email"], .register form input[type="password"], .register form input[type="text"], .register form input[type="email"] { 59 | width: 310px; 60 | height: 50px; 61 | border: 1px solid #dee0e4; 62 | margin-bottom: 20px; 63 | padding: 0 15px; 64 | } 65 | .login form input[type="submit"], .register form input[type="submit"] { 66 | width: 100%; 67 | padding: 15px; 68 | margin-top: 20px; 69 | background-color: #373373; 70 | border: 0; 71 | cursor: pointer; 72 | font-weight: bold; 73 | color: #ffffff; 74 | transition: background-color 0.2s; 75 | } 76 | .login form input[type="submit"]:hover, .register form input[type="submit"]:hover { 77 | background-color: #57539d; 78 | transition: background-color 0.2s; 79 | } 80 | .navtop { 81 | background-color: #373373; 82 | height: 60px; 83 | width: 100%; 84 | border: 0; 85 | } 86 | .navtop div { 87 | display: flex; 88 | margin: 0 auto; 89 | width: 85%; 90 | height: 100%; 91 | } 92 | .navtop div h1, .navtop div a { 93 | display: inline-flex; 94 | align-items: center; 95 | } 96 | .navtop div h1 { 97 | flex: 1; 98 | font-size: 24px; 99 | padding: 0; 100 | margin: 0; 101 | color: #eaebed; 102 | font-weight: normal; 103 | } 104 | .navtop div a { 105 | padding: 0 20px; 106 | text-decoration: none; 107 | color: #c1c4c8; 108 | font-weight: bold; 109 | } 110 | .navtop div a i { 111 | padding: 2px 8px 0 0; 112 | } 113 | .navtop div a:hover { 114 | color: #eaebed; 115 | } 116 | body.loggedin { 117 | background-color: #f3f4f7; 118 | -------------------------------------------------------------------------------- /templates/post.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout2.html' %} 2 | 3 | {% block title %}Post Ad{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 |

Post Ad

9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 |
Category{{ form.cat1 }} {{ form.cat2 }} {{ form.cat3 }}
17 |
18 | 19 |
20 |
21 | 108 | {% endblock %} -------------------------------------------------------------------------------- /templates/search.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout2.html' %} 2 | 3 | {% block title %}Search{% endblock %} 4 | 5 | {% block content %} 6 | 7 |

Search

8 |
9 | 31 |
32 | 119 | {% endblock %} -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kijiji-Reposter 2 | Kijiji Automated Reposting and Replying Utility written in Python (version 3). The reposter is completely api driven (i.e. no webscraping). Utilizes Flask, to run a local server designed to manage the GUI interface and reposting / scheduling and auto reply functions. Viable for both server and desktop environments, but if desktop, system must run 24/7 and have sleep functions disabled. 3 | 4 | 5 | __Recent Updates:__ 6 | 7 | - fixed ad geo location 8 | - fixed eBay image upload api token 9 | - added search functionality 10 | - dynamic categories, locations & attributes 11 | - added support for optional attributes (fullfilment, cashless etc.) 12 | - added token retention system 13 | - added conversations support 14 | - fixed message auto replier bugs 15 | - absolute paths incorporated for wider compatibility 16 | - retry failed attempts 17 | 18 | __Requirements:__ 19 | ``` 20 | apscheduler (if you get pytz error, it can be ignored, but you can avoid by using tzlocal 2.1) 21 | flask 22 | flask-wtf (Install WTForms 2.3.3 first, else it will break html5 import) 23 | httpx 24 | pgeocode 25 | xmltodict 26 | urllib3 27 | ``` 28 | 29 | 30 | __Usage:__ 31 | 32 | Edit the secret key argument on line 25 in server.py. Then run: 33 | ``` 34 | python server.py 35 | ``` 36 | 37 | 38 | __Connections:__ 39 | 40 | Once server is running, connect to either of the addresses listed below in a web browser. Or if running on a network, connect to the ip and port 5000 of computer running the kijiji reposter server. 41 | ``` 42 | localhost:5000/ 43 | 127.0.0.1:5000/ 44 | ``` 45 | 46 | 47 | __Accounts:__ 48 | 49 | Login using an existing kijiji account. Or if you do not have an account, create one at kijiji.ca. 50 | 51 | 52 | __Reposting:__ 53 | 54 | To create a reposting schedule, begin creating an ad by clicking the 'Post' icon at the top of the home screen. While entering the ad details, make sure to check the repost checkbox and enter the reposting times (eg. 07:00 am, 1:30 pm). Currently, only 8 reposting slots have been implemented. But you can edit the server.py code to allow for more. 55 | 56 | 57 | __Auto Replier:__ 58 | 59 | The auto replier scans your account for new messages, and if a new message is found and contains any word or phrase (not case sensitive) defined as a 'rule', it will automatically send the associated 'response'. When setting up a new rule, enter the desired, rule and response along with your password, as it will be required by the auto replier when logging in to check recent messages. Example usage: 60 | 61 | ``` 62 | Rule: Hi, is this still available? 63 | Response: Yes, it's still available. 64 | 65 | Rule: Is the price negotiable? 66 | Response: No, it's not negotiable. 67 | ``` 68 | 69 | Currently the auto replier checks your messages every 25 minutes if rules have been created. You can adjust the timing by editing the following code on line 1690: 70 | 71 | ``` 72 | sched.add_job(messageAutoReplier,'cron',minute='*/25') 73 | ``` 74 | 75 | Change the `*/25` to any number of your choosing, example: `*/6` 76 | 77 | Please note that using the auto replier will mark messages as read, meaning that when checking your messages manually they will not appear as new / unread. If you do not desire this functionality, and would like to deactivate the autoreplier, simply comment out line 1690 as shown in the code below: 78 | 79 | ``` 80 | #sched.add_job(messageAutoReplier,'cron',minute='*/25') 81 | ``` 82 | 83 | __Token Retention System:__ 84 | 85 | It was discovered that tokens remain valid almost indefinitely. To avoid congestion and redundant login attempts, tokens are retained for one day. This setting can be altered by editing the math on line 88 in kijijiapi.py: 86 | ``` 87 | expiryTime = int(time.time()) + (24 * 60 * 60) # 24hrs, 60mins, 60secs = 1 day 88 | ``` 89 | 90 | __Force Post Ad from File:__ 91 | 92 | If you require the ability to force post an ad from file due to botched reposting, accidental deletion or other strange circumstances, you can access the force post function at `localhost:5000/force`. Note that you will first need to have an ad file saved in the users folder, both of which would have been created when initially posting an ad with a reposting schedule, and two, you will also be required to manually update the 'current_ad_id' field in the schedules.json file after the forced repost if a reposting schedule exists. 93 | 94 | 95 | __ToDo:__ 96 | 97 | - implement async 98 | - impliment notification functionality 99 | - basic bug fixes / improvements 100 | -------------------------------------------------------------------------------- /static/style2.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: -apple-system, BlinkMacSystemFont, "segoe ui", roboto, oxygen, ubuntu, cantarell, "fira sans", "droid sans", "helvetica neue", Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | body { 9 | background-color: #435165; 10 | margin: 0; 11 | } 12 | .login, .register { 13 | width: 400px; 14 | background-color: #ffffff; 15 | box-shadow: 0 0 9px 0 rgba(0, 0, 0, 0.3); 16 | margin: 100px auto; 17 | } 18 | .login h1, .register h1 { 19 | text-align: center; 20 | color: #5b6574; 21 | font-size: 24px; 22 | padding: 20px 0 20px 0; 23 | border-bottom: 1px solid #dee0e4; 24 | } 25 | .login .links, .register .links { 26 | display: flex; 27 | padding: 0 15px; 28 | } 29 | .login .links a, .register .links a { 30 | color: #adb2ba; 31 | text-decoration: none; 32 | display: inline-flex; 33 | padding: 0 10px 10px 10px; 34 | font-weight: bold; 35 | } 36 | .login .links a:hover, .register .links a:hover { 37 | color: #9da3ac; 38 | } 39 | .login .links a.active, .register .links a.active { 40 | border-bottom: 3px solid #373373; 41 | color: #373373; 42 | } 43 | .login form, .register form { 44 | display: flex; 45 | flex-wrap: wrap; 46 | justify-content: center; 47 | padding-top: 20px; 48 | } 49 | .login form label, .register form label { 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | width: 50px; 54 | height: 50px; 55 | background-color: #373373; 56 | color: #ffffff; 57 | } 58 | .login form input[type="password"], .login form input[type="text"], .login form input[type="email"], .register form input[type="password"], .register form input[type="text"], .register form input[type="email"] { 59 | width: 310px; 60 | height: 50px; 61 | border: 1px solid #dee0e4; 62 | margin-bottom: 20px; 63 | padding: 0 15px; 64 | } 65 | .login form input[type="submit"], .register form input[type="submit"] { 66 | width: 100%; 67 | padding: 15px; 68 | margin-top: 20px; 69 | background-color: #373373; 70 | border: 0; 71 | cursor: pointer; 72 | font-weight: bold; 73 | color: #ffffff; 74 | transition: background-color 0.2s; 75 | } 76 | .login form input[type="submit"]:hover, .register form input[type="submit"]:hover { 77 | background-color: #2868c7; 78 | transition: background-color 0.2s; 79 | } 80 | .navtop { 81 | background-color: #373373; 82 | height: 60px; 83 | width: 100%; 84 | border: 0; 85 | } 86 | .navtop div { 87 | display: flex; 88 | margin: 0 auto; 89 | width: 85%; 90 | height: 100%; 91 | } 92 | .navtop div h1, .navtop div a { 93 | display: inline-flex; 94 | align-items: center; 95 | } 96 | .navtop div h1 { 97 | flex: 1; 98 | font-size: 24px; 99 | padding: 0; 100 | margin: 0; 101 | color: #eaebed; 102 | font-weight: normal; 103 | } 104 | .navtop div a { 105 | padding: 0 20px; 106 | text-decoration: none; 107 | color: #c1c4c8; 108 | font-weight: bold; 109 | } 110 | .navtop div a i { 111 | padding: 2px 8px 0 0; 112 | } 113 | .navtop div a:hover { 114 | color: #eaebed; 115 | } 116 | body.loggedin { 117 | background-color: #f3f4f7; 118 | } 119 | .content { 120 | width: 90%; 121 | margin: 0 auto; 122 | } 123 | .content h2 { 124 | margin: 0; 125 | padding: 25px 0; 126 | font-size: 25px; 127 | border-bottom: 1px solid #e0e0e3; 128 | color: #4a536e; 129 | } 130 | .content > p, .content > div { 131 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); 132 | margin: 25px 0; 133 | padding: 25px; 134 | background-color: #fff; 135 | } 136 | .content > p table td, .content > div table td { 137 | padding: 5px; 138 | font-size: 16px; 139 | word-break: break-all; 140 | } 141 | .content > p table td:first-child, .content > div table td:first-child { 142 | font-weight: bold; 143 | color: #4a536e; 144 | padding-right: 15px; 145 | } 146 | .content > div p { 147 | padding: 5px; 148 | margin: 0 0 10px 0; 149 | } 150 | table { 151 | width: 100%; 152 | } 153 | thead tr th:first-child, 154 | tbody tr td:first-child { 155 | width: 18%; 156 | min-width: 18%; 157 | max-width: 18%; 158 | word-break: break-all; 159 | vertical-align: top; 160 | } 161 | thead tr th:nth-child(2), 162 | tbody tr td:nth-child(2) { 163 | white-space: pre-wrap; /* css-3 */ 164 | white-space: -moz-pre-wrap; /* Mozilla, since 1999 */ 165 | white-space: -pre-wrap; /* Opera 4-6 */ 166 | white-space: -o-pre-wrap; /* Opera 7 */ 167 | word-wrap: break-word; /* IE */ 168 | } 169 | a { 170 | color: #373373; 171 | font-size: 16px; 172 | text-decoration: none; 173 | } 174 | input[type=text] { 175 | box-sizing: border-box; 176 | } 177 | 178 | textarea { 179 | font-size: 16px; 180 | width: 700px; 181 | height: 275px; 182 | resize: none; 183 | overflow-y: scroll; 184 | } 185 | 186 | #adtitle { 187 | width: 65%; 188 | } 189 | 190 | input[type=button], input[type=submit], input[type=reset] { 191 | background-color: #373373; 192 | border-radius: 8px; 193 | border: none; 194 | color: white; 195 | padding: 8px 16px; 196 | text-decoration: none; 197 | margin: 4px 2px; 198 | cursor: pointer; 199 | 200 | } 201 | 202 | select { 203 | width: 30%; 204 | outline: none; 205 | font-size: 16px; 206 | } 207 | 208 | -------------------------------------------------------------------------------- /static/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | font-family: -apple-system, BlinkMacSystemFont, "segoe ui", roboto, oxygen, ubuntu, cantarell, "fira sans", "droid sans", "helvetica neue", Arial, sans-serif; 4 | font-size: 16px; 5 | -webkit-font-smoothing: antialiased; 6 | -moz-osx-font-smoothing: grayscale; 7 | } 8 | body { 9 | background-color: #435165; 10 | margin: 0; 11 | } 12 | .login, .register { 13 | width: 400px; 14 | background-color: #ffffff; 15 | box-shadow: 0 0 9px 0 rgba(0, 0, 0, 0.3); 16 | margin: 100px auto; 17 | } 18 | .login h1, .register h1 { 19 | text-align: center; 20 | color: #5b6574; 21 | font-size: 24px; 22 | padding: 20px 0 20px 0; 23 | border-bottom: 1px solid #dee0e4; 24 | } 25 | .login .links, .register .links { 26 | display: flex; 27 | padding: 0 15px; 28 | } 29 | .login .links a, .register .links a { 30 | color: #adb2ba; 31 | text-decoration: none; 32 | display: inline-flex; 33 | padding: 0 10px 10px 10px; 34 | font-weight: bold; 35 | } 36 | .login .links a:hover, .register .links a:hover { 37 | color: #9da3ac; 38 | } 39 | .login .links a.active, .register .links a.active { 40 | border-bottom: 3px solid #373373; 41 | color: #373373; 42 | } 43 | .login form, .register form { 44 | display: flex; 45 | flex-wrap: wrap; 46 | justify-content: center; 47 | padding-top: 20px; 48 | } 49 | .login form label, .register form label { 50 | display: flex; 51 | justify-content: center; 52 | align-items: center; 53 | width: 50px; 54 | height: 50px; 55 | background-color: #373373; 56 | color: #ffffff; 57 | } 58 | .login form input[type="password"], .login form input[type="text"], .login form input[type="email"], .register form input[type="password"], .register form input[type="text"], .register form input[type="email"] { 59 | width: 310px; 60 | height: 50px; 61 | border: 1px solid #dee0e4; 62 | margin-bottom: 20px; 63 | padding: 0 15px; 64 | } 65 | .login form input[type="submit"], .register form input[type="submit"] { 66 | width: 100%; 67 | padding: 15px; 68 | margin-top: 20px; 69 | background-color: #373373; 70 | border: 0; 71 | cursor: pointer; 72 | font-weight: bold; 73 | color: #ffffff; 74 | transition: background-color 0.2s; 75 | } 76 | .login form input[type="submit"]:hover, .register form input[type="submit"]:hover { 77 | background-color: #57539d; 78 | transition: background-color 0.2s; 79 | } 80 | .navtop { 81 | background-color: #373373; 82 | height: 60px; 83 | width: 100%; 84 | border: 0; 85 | } 86 | .navtop div { 87 | display: flex; 88 | margin: 0 auto; 89 | width: 85%; 90 | height: 100%; 91 | } 92 | .navtop div h1, .navtop div a { 93 | display: inline-flex; 94 | align-items: center; 95 | } 96 | .navtop div h1 { 97 | flex: 1; 98 | font-size: 24px; 99 | padding: 0; 100 | margin: 0; 101 | color: #eaebed; 102 | font-weight: normal; 103 | } 104 | .navtop div a { 105 | padding: 0 20px; 106 | text-decoration: none; 107 | color: #c1c4c8; 108 | font-weight: bold; 109 | } 110 | .navtop div a i { 111 | padding: 2px 8px 0 0; 112 | } 113 | .navtop div a:hover { 114 | color: #eaebed; 115 | } 116 | body.loggedin { 117 | background-color: #f3f4f7; 118 | } 119 | .content { 120 | width: 90%; 121 | margin: 0 auto; 122 | } 123 | .content h2 { 124 | margin: 0; 125 | padding: 25px 0; 126 | font-size: 25px; 127 | border-bottom: 1px solid #e0e0e3; 128 | color: #4a536e; 129 | } 130 | .content > p, .content > div { 131 | box-shadow: 0 0 5px 0 rgba(0, 0, 0, 0.1); 132 | margin: 25px 0; 133 | padding: 25px; 134 | background-color: #fff; 135 | } 136 | .content > p table td, .content > div table td { 137 | padding: 5px; 138 | font-size: 16px; 139 | word-break: break-all; 140 | } 141 | .content > div table td pre { 142 | font-size: 16px; 143 | overflow-x: auto; 144 | white-space: pre-wrap; 145 | white-space: -moz-pre-wrap; 146 | white-space: -pre-wrap; 147 | white-space: -o-pre-wrap; 148 | word-wrap: break-word; 149 | } 150 | .content > p table td:first-child, .content > div table tr:first-child { 151 | font-weight: bold; 152 | color: #4a536e; 153 | padding-right: 15px; 154 | } 155 | .content > div p { 156 | padding: 5px; 157 | margin: 0 0 10px 0; 158 | } 159 | a { 160 | color: #373373; 161 | font-size: 16px; 162 | text-decoration: none; 163 | } 164 | 165 | a:hover { 166 | color: #57539d; 167 | font-size: 16px; 168 | text-decoration: none; 169 | } 170 | table { 171 | border-collapse:collapse; 172 | width:100%; 173 | } 174 | tbody tr:nth-child(even) {background: #f3f4f7 } 175 | 176 | #adlist tr:hover { 177 | background-color: #aca9e0; 178 | } 179 | 180 | #adlist tr:first-child:hover { 181 | background-color: white; 182 | } 183 | #adlist tr:nth-child(even):hover { 184 | background-color: #aca9e0; 185 | } 186 | #conversation input[type=button], input[type=submit], input[type=reset] { 187 | background-color: #373373; 188 | border-radius: 8px; 189 | border: none; 190 | color: white; 191 | padding: 8px 16px; 192 | text-decoration: none; 193 | margin: 4px 2px; 194 | cursor: pointer; 195 | } 196 | #conversation tbody tr td:first-child { 197 | width: 11%; 198 | min-width: 11%; 199 | max-width: 11%; 200 | word-break: break-all; 201 | } 202 | #conversation tbody tr td:nth-child(3) { 203 | width: 16%; 204 | min-width: 16%; 205 | max-width: 16%; 206 | word-break: break-all; 207 | } 208 | #reply tbody tr:nth-child(even) {background: white } 209 | #reply textarea { 210 | font-size: 16px; 211 | width: 700px; 212 | height: 275px; 213 | resize: none; 214 | overflow-y: scroll; 215 | } 216 | -------------------------------------------------------------------------------- /templates/stage2.html: -------------------------------------------------------------------------------- 1 | {% extends 'layout2.html' %} 2 | 3 | {% block title %}Post Ad{% endblock %} 4 | 5 | {% block content %} 6 | 7 | 8 |

Post Ad

9 |
10 |
11 | 12 | 13 | 14 | 15 | 16 | {%for item in attribForm %} 17 | 18 | 19 | 20 | 21 | {% endfor %} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 73 | 74 | 75 | 76 | 77 | 78 |
{{ postForm.adtype.label }}{{ postForm.adtype }}
{{item.label}}{{item(style="list-style:none; padding-left: 0; margin: 0 auto;")}}
{{ postForm.adtitle.label }}{{ postForm.adtitle}}
{{ postForm.description.label }}{{ postForm.description }}
{{ postForm.price.label }}{{ postForm.pricetype }} {{ postForm.price }}
{{ postForm.loc1.label }}{{ postForm.loc1 }} {{ postForm.loc2 }} {{ postForm.loc3 }}
{{ postForm.postalcode.label }}{{ postForm.postalcode }}
{{ postForm.phone.label }}{{ postForm.phone }}
{{ postForm.file1.label }}{{ postForm.file1 }} 49 | {{ postForm.file2 }} 50 | {{ postForm.file3 }} 51 | {{ postForm.file4 }} 52 | {{ postForm.file5 }} 53 | {{ postForm.file6 }} 54 | {{ postForm.file7 }} 55 | {{ postForm.file8 }} 56 | {{ postForm.file9 }} 57 | {{ postForm.file10 }}
{{ postForm.repost.label }}{{ postForm.repost }}
{{ postForm.time1.label }}{{ postForm.time1 }} Required for Repost 66 | {{ postForm.time2 }} 67 | {{ postForm.time3 }} 68 | {{ postForm.time4 }} 69 | {{ postForm.time5 }} 70 | {{ postForm.time6 }} 71 | {{ postForm.time7 }} 72 | {{ postForm.time8 }}
{{ postForm.password.label }}{{ postForm.password }} Required for Repost
79 | 80 |
81 |
82 | 83 | 237 | {% endblock %} -------------------------------------------------------------------------------- /kijijiapi.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import re 4 | import time 5 | import urllib 6 | import xmltodict 7 | 8 | THIS_FOLDER = os.path.dirname(os.path.abspath(__file__)) 9 | 10 | def picUpload(fileData, session): 11 | # Picture Upload to eBay Server before create payload 12 | url = 'https://api.ebay.com/ws/api.dll' 13 | headers={ 14 | 'Host':'api.ebay.com', 15 | 'Content-Type':'multipart/form-data; boundary=----FormBoundary7MA4YWxkTrZu0gW', 16 | 'Connection':'keep-alive', 17 | 'X-EBAY-API-CALL-NAME':'UploadSiteHostedPictures', 18 | 'Accept':'*/*', 19 | 'Accept-Language':'en-ca', 20 | 'Accept-Encoding':'gzip, deflate, br', 21 | 'User-Agent':'Kijiji/35739.100 CFNetwork/1121.2.2 Darwin/19.3.0' 22 | } 23 | 24 | picPayloadTopFile = os.path.join(THIS_FOLDER, 'static/img_upload_top.txt') 25 | picPayloadBottomFile = os.path.join(THIS_FOLDER, 'static/img_upload_bottom.txt') 26 | 27 | with open(picPayloadTopFile, 'r') as top: 28 | picPayloadTop = top.read() 29 | 30 | with open(picPayloadBottomFile, 'r') as bottom: 31 | picPayloadBottom = bottom.read() 32 | 33 | picPayload = picPayloadTop.encode('utf-8') + fileData + picPayloadBottom.encode('utf-8') 34 | 35 | r = session.post(url, headers=headers, data = picPayload) 36 | 37 | if r.status_code == 200 and r.text != '': 38 | parsed = xmltodict.parse(r.text) 39 | picUrl = parsed['UploadSiteHostedPicturesResponse']['SiteHostedPictureDetails']['FullURL'] 40 | return picUrl 41 | else: 42 | parsed = xmltodict.parse(r.text) 43 | print(parsed) 44 | 45 | 46 | def loginFunction(session, email, password): 47 | userExists = False 48 | tokenExpired = False 49 | tokensFile = os.path.join(THIS_FOLDER, 'static/tokens.json') 50 | userID = '' 51 | userToken = '' 52 | 53 | with open(tokensFile, 'r') as jsonFile: 54 | data = json.load(jsonFile) 55 | 56 | for item in data['users']: 57 | if re.search(r"\b{}\b".format(email), item['email'], re.IGNORECASE) is not None: 58 | userExists = True 59 | now = int(time.time()) 60 | 61 | if now >= item['token_expiry']: 62 | tokenExpired = True 63 | else: 64 | userID = item['userID'] 65 | userToken = item['token'] 66 | 67 | if userExists == False or tokenExpired == True: 68 | 69 | url = 'https://mingle.kijiji.ca/api/users/login' 70 | headers = { 71 | 'content-type':'application/x-www-form-urlencoded', 72 | 'accept':'*/*', 73 | 'x-ecg-ver':'1.67', 74 | 'x-ecg-ab-test-group':'', 75 | 'accept-language':'en-CA', 76 | 'accept-encoding':'gzip', 77 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)' 78 | } 79 | 80 | payload = {'username': email, 'password':password, 'socialAutoRegistration': 'false'} 81 | 82 | r = session.post(url, headers = headers, data = payload) 83 | 84 | # if kijiji response valid attempt to parse response 85 | if r.status_code == 200 and r.text != '': 86 | parsed = xmltodict.parse(r.text) 87 | userID = parsed['user:user-logins']['user:user-login']['user:id'] 88 | userToken = parsed['user:user-logins']['user:user-login']['user:token'] 89 | expiryTime = int(time.time()) + (24 * 60 * 60) 90 | 91 | # Create user entry 92 | if userExists == False: 93 | addUser = { 94 | 'email': email, 95 | 'userID': userID, 96 | 'token': userToken, 97 | 'token_expiry': expiryTime, 98 | } 99 | 100 | with open(tokensFile, 'r') as json_file: 101 | data = json.load(json_file) 102 | update = data['users'] 103 | update.append(addUser) 104 | 105 | with open(tokensFile,'w') as json_file: 106 | json.dump(data, json_file, indent=4) 107 | 108 | 109 | if tokenExpired == True: 110 | with open(tokensFile, 'r') as json_file: 111 | data = json.load(json_file) 112 | for item in data['users']: 113 | if email == item['email']: 114 | item['token'] = userToken 115 | item['token_expiry'] = expiryTime 116 | 117 | with open(tokensFile,'w') as json_file: 118 | json.dump(data, json_file, indent=4) 119 | 120 | return userID, userToken 121 | 122 | else: 123 | parsed = xmltodict.parse(r.text) 124 | print(parsed) 125 | else: 126 | return userID, userToken 127 | 128 | def getAttributes(session, userID, token, attributeID): 129 | url = 'https://mingle.kijiji.ca/api/ads/metadata/{}'.format(attributeID) 130 | userAuth = 'id="{}", token="{}"'.format(userID, token) 131 | headers = { 132 | 'accept':'*/*', 133 | 'x-ecg-ver':'1.67', 134 | 'x-ecg-authorization-user': userAuth, 135 | 'x-ecg-ab-test-group':'', 136 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 137 | 'accept-language':'en-CA', 138 | 'accept-encoding':'gzip' 139 | } 140 | 141 | r = session.get(url, headers = headers) 142 | 143 | if r.status_code == 200 and r.text != '': 144 | parsed = xmltodict.parse(r.text) 145 | return parsed 146 | else: 147 | parsed = xmltodict.parse(r.text) 148 | print(parsed) 149 | 150 | def getCategories(session, userID, token): 151 | url = 'https://mingle.kijiji.ca/api/categories' 152 | userAuth = 'id="{}", token="{}"'.format(userID, token) 153 | headers = { 154 | 'accept':'*/*', 155 | 'x-ecg-ver':'1.67', 156 | 'x-ecg-authorization-user': userAuth, 157 | 'x-ecg-ab-test-group':'', 158 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 159 | 'accept-language':'en-CA', 160 | 'accept-encoding':'gzip' 161 | } 162 | 163 | r = session.get(url, headers = headers) 164 | 165 | if r.status_code == 200 and r.text != '': 166 | parsed = xmltodict.parse(r.text) 167 | return parsed 168 | else: 169 | parsed = xmltodict.parse(r.text) 170 | print(parsed) 171 | 172 | def getLocations(session, userID, token): 173 | url = 'https://mingle.kijiji.ca/api/locations' 174 | userAuth = 'id="{}", token="{}"'.format(userID, token) 175 | headers = { 176 | 'accept':'*/*', 177 | 'x-ecg-ver':'1.67', 178 | 'x-ecg-authorization-user': userAuth, 179 | 'x-ecg-ab-test-group':'', 180 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 181 | 'accept-language':'en-CA', 182 | 'accept-encoding':'gzip' 183 | } 184 | 185 | r = session.get(url, headers = headers) 186 | 187 | if r.status_code == 200 and r.text != '': 188 | parsed = xmltodict.parse(r.text) 189 | return parsed 190 | else: 191 | parsed = xmltodict.parse(r.text) 192 | print(parsed) 193 | 194 | def getAdList(session, userID, token): 195 | url = 'https://mingle.kijiji.ca/api/users/{}/ads?size=50&page=0&_in=id,title,price,ad-type,locations,ad-status,category,pictures,start-date-time,features-active,view-ad-count,user-id,phone,email,rank,ad-address,phone-click-count,map-view-count,ad-source-id,ad-channel-id,contact-methods,attributes,link,description,feature-group-active,end-date-time,extended-info,highest-price'.format(userID) 196 | userAuth = 'id="{}", token="{}"'.format(userID, token) 197 | headers = { 198 | 'accept':'*/*', 199 | 'x-ecg-ver':'1.67', 200 | 'x-ecg-authorization-user': userAuth, 201 | 'x-ecg-ab-test-group':'', 202 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 203 | 'accept-language':'en-CA', 204 | 'accept-encoding':'gzip' 205 | } 206 | 207 | r = session.get(url, headers = headers) 208 | 209 | if r.status_code == 200 and r.text != '': 210 | parsed = xmltodict.parse(r.text) 211 | return parsed 212 | else: 213 | parsed = xmltodict.parse(r.text) 214 | print(parsed) 215 | 216 | def getAd(session, userID, token, adID): 217 | url = 'https://mingle.kijiji.ca/api/users/{}/ads/{}'.format(userID, adID) 218 | userAuth = 'id="{}", token="{}"'.format(userID, token) 219 | headers = { 220 | 'accept':'*/*', 221 | 'x-ecg-ver':'1.67', 222 | 'x-ecg-authorization-user': userAuth, 223 | 'x-ecg-ab-test-group':'', 224 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 225 | 'accept-language':'en-CA', 226 | 'accept-encoding':'gzip' 227 | } 228 | 229 | r = session.get(url, headers = headers) 230 | 231 | if r.status_code == 200 and r.text != '': 232 | parsed = xmltodict.parse(r.text) 233 | return parsed 234 | else: 235 | parsed = xmltodict.parse(r.text) 236 | print(parsed) 237 | 238 | def getSearchedAd(session, userID, token, adID): 239 | url = 'https://mingle.kijiji.ca/api/ads/{}'.format(adID) 240 | userAuth = 'id="{}", token="{}"'.format(userID, token) 241 | headers = { 242 | 'accept':'*/*', 243 | 'x-ecg-ver':'1.67', 244 | 'x-ecg-authorization-user': userAuth, 245 | 'x-ecg-ab-test-group':'', 246 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 247 | 'accept-language':'en-CA', 248 | 'accept-encoding':'gzip' 249 | } 250 | 251 | r = session.get(url, headers = headers) 252 | 253 | if r.status_code == 200 and r.text != '': 254 | parsed = xmltodict.parse(r.text) 255 | return parsed 256 | else: 257 | parsed = xmltodict.parse(r.text) 258 | print(parsed) 259 | 260 | 261 | def adExists(session, userID, token, adID): 262 | url = 'https://mingle.kijiji.ca/api/users/{}/ads/{}'.format(userID, adID) 263 | userAuth = 'id="{}", token="{}"'.format(userID, token) 264 | headers = { 265 | 'accept':'*/*', 266 | 'x-ecg-ver':'1.67', 267 | 'x-ecg-authorization-user': userAuth, 268 | 'x-ecg-ab-test-group':'', 269 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 270 | 'accept-language':'en-CA', 271 | 'accept-encoding':'gzip' 272 | } 273 | 274 | r = session.get(url, headers = headers) 275 | 276 | if r.status_code == 200: 277 | return True 278 | else: 279 | return False 280 | 281 | def getProfile(session, userID, token): 282 | url = 'https://mingle.kijiji.ca/api/users/{}/profile'.format(userID) 283 | userAuth = 'id="{}", token="{}"'.format(userID, token) 284 | headers = { 285 | 'accept':'*/*', 286 | 'x-ecg-ver':'1.67', 287 | 'x-ecg-authorization-user': userAuth, 288 | 'x-ecg-ab-test-group':'', 289 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 290 | 'accept-language':'en-CA', 291 | 'accept-encoding':'gzip' 292 | } 293 | 294 | r = session.get(url, headers = headers) 295 | 296 | if r.status_code == 200 and r.text != '': 297 | parsed = xmltodict.parse(r.text) 298 | return parsed 299 | else: 300 | parsed = xmltodict.parse(r.text) 301 | print(parsed) 302 | 303 | def submitFunction(session, userID, token, payload): 304 | url = 'https://mingle.kijiji.ca/api/users/{}/ads'.format(userID) 305 | userAuth = 'id="{}", token="{}"'.format(userID, token) 306 | headers={ 307 | 'content-type':'application/xml', 308 | 'accept':'*/*', 309 | 'x-ecg-ver':'1.67', 310 | 'x-ecg-ab-test-group':'', 311 | 'accept-encoding': 'gzip', 312 | 'x-ecg-authorization-user': userAuth, 313 | 'accept-language':'en-CA', 314 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)' 315 | } 316 | r = session.post(url, headers=headers, data=payload) 317 | 318 | if r.status_code == 201 and r.text != '': 319 | parsed = xmltodict.parse(r.text) 320 | return parsed 321 | else: 322 | parsed = xmltodict.parse(r.text) 323 | print(parsed) 324 | 325 | def editAd(session, userID, adID, token, payload): 326 | url = 'https://mingle.kijiji.ca/api/users/{}/ads/{}'.format(userID, adID) 327 | userAuth = 'id="{}", token="{}"'.format(userID, token) 328 | headers={ 329 | 'Host': 'mingle.kijiji.ca', 330 | 'Accept': '*/*', 331 | 'timestamp': str(int(time.time())), 332 | 'X-ECG-VER': '3.6', 333 | 'X-ECG-AB-TEST-GROUP': '', 334 | 'Accept-Encoding': 'gzip', 335 | 'X-ECG-Authorization-User': userAuth, 336 | 'Accept-Language': 'en-CA', 337 | 'User-Agent': 'Kijiji 15.18.0 (iPhone; iOS 14.6; en_CA)', 338 | 'Connection': 'keep-alive', 339 | 'Content-Type': 'application/xml' 340 | } 341 | r = session.put(url, headers=headers, data=payload) 342 | 343 | if r.status_code == 200 and r.text != '': 344 | parsed = xmltodict.parse(r.text) 345 | return parsed 346 | else: 347 | parsed = xmltodict.parse(r.text) 348 | print(parsed) 349 | 350 | def deleteAd(session, userID, adID, token): 351 | url = 'https://mingle.kijiji.ca/api/users/{}/ads/{}'.format(userID, adID) 352 | userAuth = 'id="{}", token="{}"'.format(userID, token) 353 | headers = { 354 | 'content-type':'application/xml', 355 | 'x-ecg-ver':'1.67', 356 | 'x-ecg-ab-test-group':'', 357 | 'x-ecg-authorization-user': userAuth, 358 | 'accept-encoding': 'gzip', 359 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)' 360 | } 361 | 362 | r = session.delete(url, headers = headers) 363 | 364 | if r.status_code == 204: 365 | print('Ad ' + adID + ' Successfully Deleted') 366 | else: 367 | parsed = xmltodict.parse(r.text) 368 | print(parsed) 369 | 370 | def getConversations(session, userID, token, page): 371 | url = 'https://mingle.kijiji.ca/api/users/{}/conversations?size=25&page={}'.format(userID, page) 372 | userAuth = 'id="{}", token="{}"'.format(userID, token) 373 | headers = { 374 | 'accept':'*/*', 375 | 'x-ecg-ver':'1.67', 376 | 'x-ecg-authorization-user': userAuth, 377 | 'x-ecg-ab-test-group':'', 378 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 379 | 'accept-language':'en-CA', 380 | 'accept-encoding':'gzip' 381 | } 382 | if page is not None and page != 'None': 383 | r = session.get(url, headers = headers) 384 | 385 | if r.status_code == 200 and r.text != '': 386 | parsed = xmltodict.parse(r.text) 387 | return parsed 388 | else: 389 | parsed = xmltodict.parse(r.text) 390 | print(parsed) 391 | 392 | def getConversation(session, userID, token, conversationID): 393 | url = 'https://mingle.kijiji.ca/api/users/{}/conversations/{}?tail=100'.format(userID, conversationID) 394 | userAuth = 'id="{}", token="{}"'.format(userID, token) 395 | headers = { 396 | 'accept':'*/*', 397 | 'x-ecg-ver':'1.67', 398 | 'x-ecg-authorization-user': userAuth, 399 | 'x-ecg-ab-test-group':'', 400 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)', 401 | 'accept-language':'en-CA', 402 | 'accept-encoding':'gzip' 403 | } 404 | r = session.get(url, headers = headers) 405 | 406 | if r.status_code == 200 and r.text != '': 407 | parsed = xmltodict.parse(r.text) 408 | return parsed 409 | else: 410 | parsed = xmltodict.parse(r.text) 411 | print(parsed) 412 | 413 | def sendReply(session, userID, token, payload): 414 | url = 'https://mingle.kijiji.ca/api/replies/reply-to-ad-conversation' 415 | userAuth = 'id="{}", token="{}"'.format(userID, token) 416 | headers = { 417 | 'content-type':'application/xml', 418 | 'accept':'*/*', 419 | 'x-ecg-ver':'1.67', 420 | 'x-ecg-ab-test-group':'', 421 | 'accept-language':'en-CA', 422 | 'x-ecg-authorization-user': userAuth, 423 | 'accept-encoding':'gzip', 424 | 'user-agent':'Kijiji 12.15.0 (iPhone; iOS 13.5.1; en_CA)' 425 | } 426 | 427 | r = session.post(url, headers = headers, data=payload) 428 | 429 | if r.status_code == 201 and r.text != '': 430 | parsed = xmltodict.parse(r.text) 431 | return parsed 432 | else: 433 | parsed = xmltodict.parse(r.text) 434 | print(parsed) 435 | 436 | def createReplyPayload(adID, replyName, replyEmail, reply, conversationID, direction): 437 | replyPayload = { 438 | "reply:reply-to-ad-conversation": { 439 | "@xmlns:types": "http://www.ebayclassifiedsgroup.com/schema/types/v1", 440 | "@xmlns:cat": "http://www.ebayclassifiedsgroup.com/schema/category/v1", 441 | "@xmlns:loc": "http://www.ebayclassifiedsgroup.com/schema/location/v1", 442 | "@xmlns:ad": "http://www.ebayclassifiedsgroup.com/schema/ad/v1", 443 | "@xmlns:attr": "http://www.ebayclassifiedsgroup.com/schema/attribute/v1", 444 | "@xmlns:pic": "http://www.ebayclassifiedsgroup.com/schema/picture/v1", 445 | "@xmlns:user": "http://www.ebayclassifiedsgroup.com/schema/user/v1", 446 | "@xmlns:rate": "http://www.ebayclassifiedsgroup.com/schema/rate/v1", 447 | "@xmlns:reply": "http://www.ebayclassifiedsgroup.com/schema/reply/v1", 448 | "@locale": "en-CA", 449 | "reply:ad-id": adID, 450 | "reply:reply-username": replyName, 451 | "reply:reply-phone": None, 452 | "reply:reply-email": replyEmail, 453 | "reply:reply-message": reply, 454 | "reply:conversation-id": conversationID, 455 | "reply:reply-direction": { 456 | "types:value": direction}}} 457 | 458 | # Parse into XML 459 | payload = xmltodict.unparse(replyPayload, short_empty_elements=True, pretty=True) 460 | return payload 461 | 462 | def createReplyAdPayload(adID, replyName, replyEmail, reply): 463 | 464 | replyPayload = { 465 | "reply:reply-to-ad-conversation": { 466 | "@xmlns:types": "http://www.ebayclassifiedsgroup.com/schema/types/v1", 467 | "@xmlns:cat": "http://www.ebayclassifiedsgroup.com/schema/category/v1", 468 | "@xmlns:loc": "http://www.ebayclassifiedsgroup.com/schema/location/v1", 469 | "@xmlns:ad": "http://www.ebayclassifiedsgroup.com/schema/ad/v1", 470 | "@xmlns:attr": "http://www.ebayclassifiedsgroup.com/schema/attribute/v1", 471 | "@xmlns:pic": "http://www.ebayclassifiedsgroup.com/schema/picture/v1", 472 | "@xmlns:user": "http://www.ebayclassifiedsgroup.com/schema/user/v1", 473 | "@xmlns:rate": "http://www.ebayclassifiedsgroup.com/schema/rate/v1", 474 | "@xmlns:reply": "http://www.ebayclassifiedsgroup.com/schema/reply/v1", 475 | "@locale": "en-CA", 476 | "reply:ad-id": adID, 477 | "reply:reply-username": replyName, 478 | "reply:reply-phone": None, 479 | "reply:reply-email": replyEmail, 480 | "reply:reply-message": reply, 481 | "reply:structured-msg-id": "1", 482 | "reply:reply-direction": { 483 | "types:value": "TO_OWNER"}}} 484 | 485 | # Parse into XML 486 | payload = xmltodict.unparse(replyPayload, short_empty_elements=True, pretty=True) 487 | return payload 488 | 489 | def searchFunction(session, userID, token, longitude, latitude, size, postal_code, page, radius, category, criteria): 490 | userAuth = 'id="{}", token="{}"'.format(userID, token) 491 | criteria = urllib.parse.quote(criteria) 492 | topads = 'true' 493 | sort_type = 'DATE_DESCENDING' 494 | postal_code = urllib.parse.quote(postal_code) 495 | url = 'https://mingle.kijiji.ca/api/ads?ad-status=ACTIVE&includeTopAds={}&sortType={}&q={}&longitude={}&searchOptionsExactMatch=false&latitude={}&extension[origin]=SRP&size={}&address={}&page={}&distance={}&categoryId={}&_in=id,title,price,ad-type,locations,ad-status,category,pictures,start-date-time,features-active,view-ad-count,user-id,phone,email,rank,ad-address,phone-click-count,map-view-count,ad-source-id,ad-channel-id,contact-methods,attributes,link,description,feature-group-active,end-date-time,extended-info,highest-price,notice,has-virtual-tour-url'.format(topads, sort_type, criteria, longitude, latitude, size, postal_code, page, radius, category) 496 | headers = { 497 | 'Host': 'mingle.kijiji.ca', 498 | 'timestamp': str(int(time.time())), 499 | 'X-ECG-VER': '3.6', 500 | 'Accept-Language': 'en-CA', 501 | 'X-ECG-Authorization-User': userAuth, 502 | 'Accept-Encoding': 'gzip', 503 | 'Accept': '*/*', 504 | 'User-Agent': 'Kijiji 15.17.0 (iPhone; iOS 14.6; en_CA)', 505 | 'Connection': 'keep-alive' 506 | } 507 | r = session.get(url, headers=headers) 508 | if r.status_code == 200 and r.text != '': 509 | parsed = xmltodict.parse(r.text) 510 | return parsed 511 | else: 512 | parsed = xmltodict.parse(r.text) 513 | print(parsed) 514 | 515 | def checkPostalCodeLength(postal_code): 516 | if len(postal_code) == 6: 517 | section1 = postal_code[:3] 518 | section2 = postal_code[3:6] 519 | return section1 + ' ' + section2 520 | else: 521 | return postal_code -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import datetime 3 | import httpx 4 | import json 5 | import pgeocode 6 | import os 7 | import re 8 | import time 9 | import urllib3 10 | import xmltodict 11 | from apscheduler.schedulers.background import BackgroundScheduler 12 | from flask import Flask, request, jsonify, send_file, render_template, session, redirect, url_for, send_from_directory, Markup 13 | from flask_wtf import FlaskForm, Form 14 | from flask_wtf.file import FileField, FileRequired, FileAllowed 15 | from kijijiapi import * 16 | from wtforms.fields.html5 import DateField, TimeField 17 | from wtforms import SelectField, SelectMultipleField, TextField, TextAreaField, validators, StringField, SubmitField, FieldList, FormField, BooleanField, IntegerField, widgets 18 | from werkzeug.utils import secure_filename 19 | 20 | app = Flask(__name__) 21 | 22 | # Set Absolute Path 23 | THIS_FOLDER = os.path.dirname(os.path.abspath(__file__)) 24 | 25 | # Change this to your secret key (can be anything, it's for extra protection) 26 | app.secret_key = 'your secret key' 27 | 28 | # Class Declarations: 29 | # Creates Custom MultiCheckboxField used for Optional Enum Type Attributes 30 | class MultiCheckboxField(SelectMultipleField): 31 | widget = widgets.ListWidget(prefix_label=False) 32 | option_widget = widgets.CheckboxInput() 33 | 34 | # Persistent Forms used when Posting Ad 35 | class PostForm(FlaskForm): 36 | class Meta: 37 | csrf = False 38 | adtitle = TextField(id='adtitle', label= 'Ad Title', validators=[validators.DataRequired(), validators.Length(max=64)]) 39 | adtype = SelectField(id='adtype', label='Ad Type', choices=[]) 40 | cat1 = SelectField(id='cat1', label='Category') 41 | cat2 = SelectField(id='cat2') 42 | cat3 = SelectField(id='cat3') 43 | description = TextAreaField(id='description', label='Description', validators=[validators.DataRequired()]) 44 | loc1 = SelectField(id='loc1', label='Location') 45 | loc2 = SelectField(id='loc2') 46 | loc3 = SelectField(id='loc3') 47 | price = TextField(id='price', label='Price') 48 | pricetype = SelectField(id='pricetype', label='Price Type', choices = ['SPECIFIED_AMOUNT', 'PLEASE_CONTACT', 'SWAP_TRADE', 'FREE']) 49 | postalcode = TextField(id='postalcode',label='Postal Code', validators=[validators.DataRequired()]) 50 | phone = TextField(id='phone', label='Phone') 51 | file1 = FileField(id='file1', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 52 | file2 = FileField(id='file2', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 53 | file3 = FileField(id='file3', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 54 | file4 = FileField(id='file4', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 55 | file5 = FileField(id='file5', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 56 | file6 = FileField(id='file6', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 57 | file7 = FileField(id='file7', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 58 | file8 = FileField(id='file8', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 59 | file9 = FileField(id='file9', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 60 | file10 = FileField(id='file10', label='Pictures', validators=[FileAllowed(['jpg', 'jpeg', 'png'], 'Images only!')]) 61 | repost = BooleanField(id='repost', label='Repost') 62 | time1 = TimeField(id='time1', label='Time') 63 | time2 = TimeField(id='time2', label='Time') 64 | time3 = TimeField(id='time3', label='Time') 65 | time4 = TimeField(id='time4', label='Time') 66 | time5 = TimeField(id='time5', label='Time') 67 | time6 = TimeField(id='time6', label='Time') 68 | time7 = TimeField(id='time7', label='Time') 69 | time8 = TimeField(id='time8', label='Time') 70 | password = TextField(id='password', label='Password') 71 | 72 | class ConversationForm(FlaskForm): 73 | class Meta: 74 | csrf = False 75 | reply = TextAreaField(id='reply', label='Reply', validators=[validators.DataRequired()]) 76 | 77 | class SearchForm(FlaskForm): 78 | class Meta: 79 | csrf = False 80 | cat1 = SelectField(id='cat1', label='Category') 81 | cat2 = SelectField(id='cat2') 82 | cat3 = SelectField(id='cat3') 83 | postal_code = TextField(id='postal_code', label='Postal Code', validators=[validators.DataRequired(), validators.Length(max=8)]) 84 | search = TextField(id='search', label='Search', validators=[validators.DataRequired(), validators.Length(max=64)]) 85 | radius = TextField(id='radius', label='Radius', validators=[validators.DataRequired(), validators.Length(max=10)]) 86 | 87 | 88 | # Functions: 89 | def getXML(filename): 90 | #retrievs parsed xml file 91 | with open(filename, 'r') as f: 92 | content = f.read() 93 | f.close() 94 | parsed = xmltodict.parse(content) 95 | return parsed 96 | 97 | def timeValidator(time): 98 | # reformats timestamp into valid / usable format 99 | if time != None and time != '': 100 | validTime = time.strftime("%H:%M") 101 | return validTime 102 | else: 103 | return None 104 | 105 | def timeSubtractor(time): 106 | if time != None and time != '': 107 | delta = datetime.timedelta(minutes=3) 108 | converted = datetime.datetime.strptime(time,"%H:%M") 109 | minus = (converted - delta).strftime("%H:%M") 110 | return minus 111 | else: 112 | return None 113 | 114 | def chooseCategory(cat1, cat2, cat3): 115 | if cat3 != None and cat3 != '': 116 | return cat3 117 | elif (cat3 is None or cat3 == '') and (cat2 != None and cat2 != ''): 118 | return cat2 119 | else: 120 | if cat1 == 'Kijiji Village': 121 | return '36611001' 122 | elif cat1 == 'Buy & Sell': 123 | return '10' 124 | elif cat1 == 'Cars & Vehicles': 125 | return '27' 126 | elif cat1 == 'Real Estate': 127 | return '34' 128 | elif cat1 == 'Jobs': 129 | return '45' 130 | elif cat1 == 'Services': 131 | return '72' 132 | elif cat1 == 'Pets': 133 | return '112' 134 | elif cat1 == 'Community': 135 | return '1' 136 | elif cat1 == 'Vacation Rentals': 137 | return '800' 138 | elif cat1 == 'Free Stuff': 139 | return '17220001' 140 | 141 | def chooseLocation(loc1, loc2, loc3): 142 | if loc3 != None and loc3 != '': 143 | return loc3 144 | elif (loc3 is None or loc3 == '') and (loc2 != None and loc2 != ''): 145 | return loc2 146 | elif (loc3 is None or loc3 == '') and (loc2 is None or loc2 == '') and (loc1 != None and loc1 != ''): 147 | return loc1 148 | else: 149 | # default to Canada '0' 150 | return '0' 151 | 152 | def picLink(data, session): 153 | if data != None and data != '' and data != b'': 154 | file = data 155 | fileData = file.read() 156 | picLink = picUpload(fileData, session) 157 | return picLink 158 | else: 159 | return None 160 | 161 | def testListInstance(data): 162 | if isinstance(data,list): 163 | return True 164 | 165 | def testDictInstance(data): 166 | if isinstance(data,dict): 167 | return True 168 | 169 | def reposter(): 170 | #delete then repost 171 | writeActivate = False 172 | 173 | scheduleFile = os.path.join(THIS_FOLDER, 'static/schedules.json') 174 | with open(scheduleFile, 'r') as jsonFile: 175 | data = json.load(jsonFile) 176 | for item in data['schedules']: 177 | # Time Calcualtions 178 | now = datetime.datetime.now() 179 | current_time = now.strftime("%H:%M") #"%H:%M:%S" 180 | 181 | try: 182 | # Delete Ad 3 Minutes Before Repost Time 183 | if (timeSubtractor(item['time1']) == current_time) or (timeSubtractor(item['time2']) == current_time) or (timeSubtractor(item['time3']) == current_time) or (timeSubtractor(item['time4']) == current_time) or (timeSubtractor(item['time5']) == current_time) or (timeSubtractor(item['time6']) == current_time) or (timeSubtractor(item['time7']) == current_time) or (timeSubtractor(item['time8']) == current_time): 184 | 185 | email = item['useremail'] 186 | password = item['userpassword'] 187 | adID = item['current_ad_id'] 188 | 189 | # Retry 20 times if exception raised 190 | tries = 20 191 | for i in range(tries): 192 | try: 193 | # Login 194 | userID, userToken = loginFunction(kijijiSession, email, password) 195 | 196 | # Delete Old Ad 197 | deleteAd(kijijiSession, userID, adID, userToken) 198 | print('3 minute delay until repost ', now) 199 | 200 | except: 201 | print('Error: Deletion Failed at: ', now) 202 | print('Retry Attempt:', i) 203 | if i < tries - 1: # i is zero indexed 204 | time.sleep(5) 205 | continue 206 | 207 | else: 208 | # exit loop if successful 209 | break 210 | 211 | # Repost at current time 212 | # Check if Deleted 213 | if (item['time1'] == current_time) or (item['time2'] == current_time) or (item['time3'] == current_time) or (item['time4'] == current_time) or (item['time5'] == current_time) or (item['time6'] == current_time) or (item['time7'] == current_time) or (item['time8'] == current_time): 214 | #repost 215 | 216 | email = item['useremail'] 217 | password = item['userpassword'] 218 | adFile = item['ad_file'] 219 | adID = item['current_ad_id'] 220 | 221 | # Retry 20 times if exception raised 222 | tries = 20 223 | for i in range(tries): 224 | try: 225 | # Login 226 | userID, userToken = loginFunction(kijijiSession, email, password) 227 | 228 | # Check if Ad exists, if not, then deletion was successful 229 | exists = adExists(kijijiSession, userID, userToken, adID) 230 | if exists == False: 231 | 232 | # Open file / Get payload 233 | with open(adFile, 'r') as f: 234 | payload = f.read() 235 | 236 | # Post Ad 237 | parsed = submitFunction(kijijiSession, userID, userToken, payload) 238 | new_adID = parsed['ad:ad']['@id'] 239 | 240 | # Edit ad id in json file to match new ad id 241 | item['current_ad_id'] = new_adID 242 | writeActivate = True 243 | print('Reposting Completed at: ', now) 244 | 245 | except: 246 | print('Error: Reposting Failed at: ', now) 247 | print('Retry Attempt:', i) 248 | if i < tries - 1: # i is zero indexed 249 | time.sleep(5) 250 | continue 251 | 252 | else: 253 | # exit loop if successful 254 | break 255 | except: 256 | print('No Valid Schedules Found') 257 | 258 | # Write updates to json file if successful reposting has occurred 259 | if writeActivate == True: 260 | print('Updating Schedules') 261 | with open(scheduleFile, 'w') as jsonFile: 262 | json.dump(data, jsonFile, indent=4) 263 | 264 | 265 | def messageAutoReplier(): 266 | 267 | print('Message Auto Replier: Checking Messages') 268 | 269 | messageFile = os.path.join(THIS_FOLDER, 'static/messages.json') 270 | 271 | with open(messageFile, 'r') as jsonFile: 272 | 273 | data = json.load(jsonFile) 274 | if len(data['users']) != 0: 275 | 276 | for user in data['users']: 277 | 278 | if len(user['rules']) != 0: 279 | 280 | email = user['useremail'] 281 | password = user['userpassword'] 282 | page = '0' #0 = first 25 283 | 284 | # Retry 20 times if exception raised 285 | tries = 20 286 | for i in range(tries): 287 | try: 288 | # Login 289 | userID, userToken = loginFunction(kijijiSession, email, password) 290 | # Get 25 Most recent Conversations 291 | conversations = getConversations(kijijiSession, userID, userToken, page) 292 | except: 293 | now = datetime.datetime.now() 294 | print('Error Auto Replier Unable to Login or Get Conversations at:', now) 295 | print('Retry Attempt:', i) 296 | if i < tries - 1: # i is zero indexed 297 | time.sleep(5) 298 | continue 299 | else: 300 | for rule in user['rules']: 301 | 302 | if 'user:user-conversation' in conversations['user:user-conversations']: 303 | 304 | # Initialize Reply Variables 305 | direction = '' 306 | replyName = '' 307 | replyEmail = '' 308 | content = '' 309 | conversationID = '' 310 | adID = '' 311 | messageRead = '' 312 | answered = '' 313 | 314 | isList = testListInstance(conversations['user:user-conversations']['user:user-conversation']) 315 | 316 | # only 1 conversation if not list 317 | if isList == False: 318 | 319 | for key, value in conversations['user:user-conversations']['user:user-conversation'].items(): 320 | sendMessage = False 321 | unread = False 322 | 323 | if key == '@uid': 324 | conversationID = value 325 | 326 | if key == 'user:num-unread-msg': 327 | 328 | if value != '0': 329 | unread = True 330 | 331 | conversation = getConversation(kijijiSession, userID, userToken, conversationID) 332 | 333 | adID = conversation['user:user-conversation']['user:ad-id'] 334 | ownerUserID = conversation['user:user-conversation']['user:ad-owner-id'] 335 | ownerEmail = conversation['user:user-conversation']['user:ad-owner-email'] 336 | ownerName = conversation['user:user-conversation']['user:ad-owner-name'] 337 | replierUserID = conversation['user:user-conversation']['user:ad-replier-id'] 338 | replierEmail = conversation['user:user-conversation']['user:ad-replier-email'] 339 | replierName = conversation['user:user-conversation']['user:ad-replier-name'] 340 | 341 | # Calculate Message Direction 342 | if ownerUserID == userID: 343 | replyName = ownerName 344 | replyEmail = ownerEmail 345 | direction = 'TO_BUYER' 346 | 347 | elif replierUserID == userID: 348 | replyName = replierName 349 | replyEmail = replierEmail 350 | direction = 'TO_OWNER' 351 | 352 | if key == 'user:user-message': 353 | for element, attribute in value.items(): 354 | if element == 'user:msg-content': 355 | content = attribute 356 | 357 | if element == 'user:read': 358 | messageRead = attribute 359 | 360 | if element == 'user:answered': 361 | answered = attribute 362 | 363 | if element == 'user:sender-id': 364 | senderID = attribute 365 | 366 | for index, message in rule.items(): 367 | 368 | #if message in content and unread == True: 369 | if (re.search(r"\b{}\b".format(message), content, re.IGNORECASE) is not None) and unread == True and senderID != userID and answered == 'false': 370 | sendMessage = True 371 | 372 | if index == 'response' and sendMessage == True: 373 | reply = message 374 | finalPayload = createReplyPayload(adID, replyName, replyEmail, reply, conversationID, direction) 375 | now = datetime.datetime.now() 376 | sendReply(kijijiSession, userID, userToken, finalPayload) 377 | print('Auto replied', direction, 'in conversation', conversationID, 'at', now) 378 | 379 | # Reset Variables for next iteration 380 | sendMessage = False 381 | unread = False 382 | direction = '' 383 | replyName = '' 384 | replyEmail = '' 385 | content = '' 386 | conversationID = '' 387 | adID = '' 388 | messageRead = '' 389 | answered = '' 390 | 391 | # multiple conversations 392 | else: 393 | 394 | for item in conversations['user:user-conversations']['user:user-conversation']: 395 | sendMessage = False 396 | unread = False 397 | 398 | conversationID = item['@uid'] 399 | 400 | if item['user:num-unread-msg'] != '0': 401 | 402 | unread = True 403 | 404 | conversation = getConversation(kijijiSession, userID, userToken, conversationID) 405 | 406 | adID = conversation['user:user-conversation']['user:ad-id'] 407 | ownerUserID = conversation['user:user-conversation']['user:ad-owner-id'] 408 | ownerEmail = conversation['user:user-conversation']['user:ad-owner-email'] 409 | ownerName = conversation['user:user-conversation']['user:ad-owner-name'] 410 | replierUserID = conversation['user:user-conversation']['user:ad-replier-id'] 411 | replierEmail = conversation['user:user-conversation']['user:ad-replier-email'] 412 | replierName = conversation['user:user-conversation']['user:ad-replier-name'] 413 | 414 | # Calculate Message Direction 415 | if ownerUserID == userID: 416 | replyName = ownerName 417 | replyEmail = ownerEmail 418 | direction = 'TO_BUYER' 419 | 420 | elif replierUserID == userID: 421 | replyName = replierName 422 | replyEmail = replierEmail 423 | direction = 'TO_OWNER' 424 | 425 | for element, attribute in item['user:user-message'].items(): 426 | 427 | messageID = item['user:user-message']['@id'] 428 | 429 | if element == 'user:msg-content': 430 | content = attribute 431 | 432 | if element == 'user:read': 433 | messageRead = attribute 434 | 435 | if element == 'user:answered': 436 | answered = attribute 437 | 438 | if element == 'user:sender-id': 439 | senderID = attribute 440 | 441 | for index, message in rule.items(): 442 | 443 | #if message in content and unread == True: 444 | if (re.search(r"\b{}\b".format(message), content, re.IGNORECASE) is not None) and unread == True and senderID != userID and answered == 'false': 445 | sendMessage = True 446 | 447 | if index == 'response' and sendMessage == True: 448 | reply = message 449 | finalPayload = createReplyPayload(adID, replyName, replyEmail, reply, conversationID, direction) 450 | now = datetime.datetime.now() 451 | sendReply(kijijiSession, userID, userToken, finalPayload) 452 | print('Auto replied', direction, 'in conversation', conversationID, 'at', now) 453 | 454 | # Reset Variables for next iteration 455 | sendMessage = False 456 | unread = False 457 | direction = '' 458 | replyName = '' 459 | replyEmail = '' 460 | content = '' 461 | conversationID = '' 462 | adID = '' 463 | messageRead = '' 464 | answered = '' 465 | break 466 | 467 | # Create Session with Http2.0 compatability for Kijiji separate from Flask local session 468 | # SSL verification disabled to avoid ConnectionPool Max retries exception 469 | # Need to impliment this in future (httpx module still in alpha) 470 | urllib3.disable_warnings() 471 | timeout = httpx.Timeout(15.0, connect=30.0) 472 | kijijiSession = httpx.Client(verify=False, timeout=timeout) 473 | 474 | # Initialize global Variables 475 | # category and location global variables are used to store temporary dynamic data during ad posting 476 | categoriesData = '' 477 | locationsData = '' 478 | 479 | # Routes: 480 | 481 | # http://localhost:5000/ - this will be the login page, we need to use both GET and POST requests 482 | @app.route('/', methods=['GET', 'POST']) 483 | def login(): 484 | # Initialize output message if something goes wrong... 485 | msg = '' 486 | # Check if "email" and "password" POST requests exist (user submitted form) 487 | if request.method == 'POST' and 'email' in request.form and 'password' in request.form: 488 | 489 | # Create variables for easy access 490 | email = request.form['email'] 491 | password = request.form['password'] 492 | 493 | try: 494 | userID, userToken = loginFunction(kijijiSession, email, password) 495 | # Create local session data accessible to other routes 496 | session['loggedin'] = True 497 | session['user_id'] = userID 498 | #session['user_email'] = userEmail #redundant 499 | session['user_email'] = email 500 | session['user_token'] = userToken 501 | # Redirect to home page 502 | return redirect(url_for('home')) 503 | except: 504 | # Account doesnt exist or email/password incorrect 505 | msg = 'Unable to Access Kijiji Account' 506 | print('Login Error: Unable to Access Kijiji Account') 507 | return render_template('index.html', msg=msg) 508 | else: 509 | # Show the login form with message (if any) 510 | return render_template('index.html', msg=msg) 511 | 512 | # http://localhost:5000/logout - this will be the logout page 513 | @app.route('/logout') 514 | def logout(): 515 | # Remove session data, this will log the user out 516 | session.pop('loggedin', None) 517 | session.pop('user_id', None) 518 | session.pop('user_email', None) 519 | session.pop('user_token', None) 520 | # Redirect to login page 521 | return redirect(url_for('login')) 522 | 523 | 524 | # http://localhost:5000/home - this will be the home page, only accessible for loggedin users 525 | @app.route('/home') 526 | def home(): 527 | # Check if user is loggedin 528 | if 'loggedin' in session: 529 | # Retrieve Current Ad List 530 | userID = session['user_id'] 531 | token = session['user_token'] 532 | parsed = getAdList(kijijiSession, userID, token) 533 | # User is loggedin show them the home page 534 | return render_template('home.html', email = session['user_email'], data=parsed) #, profileData=profileData) 535 | else: 536 | # User is not loggedin redirect to login page 537 | return redirect(url_for('login')) 538 | 539 | 540 | # http://localhost:5000/profile - this will be the profile page, only accessible for loggedin users 541 | @app.route('/profile') 542 | def profile(): 543 | # Check if user is loggedin 544 | if 'loggedin' in session: 545 | # Retrieve Profile Data 546 | userID = session['user_id'] 547 | token = session['user_token'] 548 | parsed = getProfile(kijijiSession, userID, token) 549 | # Show the profile page with account info 550 | return render_template('profile.html', data=parsed) 551 | else: 552 | # User is not loggedin redirect to login page 553 | return redirect(url_for('login')) 554 | 555 | 556 | # http://localhost:5000/ad 557 | @app.route('/ad/') 558 | def ad(adID): 559 | # Check if user is loggedin 560 | if 'loggedin' in session: 561 | # View Ad from kijiji account 562 | userID = session['user_id'] 563 | token = session['user_token'] 564 | parsed = getAd(kijijiSession, userID, token, adID) 565 | # Show the profile page with account info 566 | return render_template('ad.html', data=parsed, adID=adID, userID=userID) 567 | else: 568 | # User is not loggedin redirect to login page 569 | return redirect(url_for('login')) 570 | 571 | # http://localhost:5000/ad 572 | @app.route('/viewad/') 573 | def viewad(adID): 574 | # Check if user is loggedin 575 | if 'loggedin' in session: 576 | # View Ad from kijiji account 577 | userID = session['user_id'] 578 | token = session['user_token'] 579 | parsed = getSearchedAd(kijijiSession, userID, token, adID) 580 | form = ConversationForm() 581 | # Show the profile page with account info 582 | return render_template('ad.html', data=parsed, adID=adID, userID=userID, form=form) 583 | else: 584 | # User is not loggedin redirect to login page 585 | return redirect(url_for('login')) 586 | 587 | # http://localhost:5000/search 588 | @app.route('/search', methods=['GET', 'POST']) 589 | def search(): 590 | # Check if user is loggedin 591 | if 'loggedin' in session: 592 | # Search for an Ad - Stage 1 - Select Category 593 | userID = session['user_id'] 594 | token = session['user_token'] 595 | global categoriesData 596 | categoriesData = getCategories(kijijiSession, userID, token) 597 | 598 | choiceList = [] 599 | 600 | for x in categoriesData['cat:categories']['cat:category']['cat:category']: 601 | choiceList.append(x['cat:id-name']) 602 | 603 | form = SearchForm() 604 | form.cat1.choices = choiceList 605 | 606 | return render_template('search.html', form=form) 607 | else: 608 | # User is not loggedin redirect to login page 609 | return redirect(url_for('login')) 610 | 611 | # http://localhost:5000/results 612 | @app.route('/results', methods=['GET', 'POST']) 613 | def results(): 614 | # Check if user is loggedin 615 | if 'loggedin' in session: 616 | userID = session['user_id'] 617 | token = session['user_token'] 618 | 619 | # reset category data 620 | global categoriesData 621 | categoriesData = '' 622 | 623 | form = SearchForm() 624 | catChoice = chooseCategory(form.cat1.data, form.cat2.data, form.cat3.data) 625 | 626 | postal_code = checkPostalCodeLength(form.postal_code.data) 627 | nomi = pgeocode.Nominatim('ca') 628 | location = nomi.query_postal_code(postal_code) 629 | 630 | # need to add category field to narrow results 631 | searched = searchFunction(kijijiSession, userID, token, location.longitude, location.latitude, '100', postal_code, '0', form.radius.data, catChoice, form.search.data) 632 | 633 | class Results: 634 | def __init__(self, id, price, title, description, address, seller_id, pics, cover_img): 635 | self.id = id 636 | self.price = price 637 | self.title = title 638 | self.description = description 639 | self.address = address 640 | self.seller_id = seller_id 641 | self.pics = pics 642 | self.cover_img = cover_img 643 | 644 | searchResults = [] 645 | 646 | try: 647 | for results in searched['ad:ads']['ad:ad']: 648 | id = '' 649 | price = '' 650 | title = '' 651 | description = '' 652 | address = '' 653 | seller_id = '' 654 | pics = [] 655 | cover_img = '' 656 | 657 | for key, value in results.items(): 658 | if key == '@id': 659 | id = value 660 | if key == 'ad:price': 661 | for element, attribute in value.items(): 662 | if element == 'types:amount': 663 | price = attribute 664 | if key == 'ad:title': 665 | title = value 666 | if key == 'ad:description': 667 | description = value 668 | if key == 'ad:ad-address': 669 | for element, attribute in value.items(): 670 | if element == 'types:full-address': 671 | address = attribute 672 | if key == 'ad:user-id': 673 | seller_id = value 674 | if key == 'pic:pictures': 675 | try: 676 | for item in value['pic:picture']: 677 | for element, attribute in item.items(): 678 | for sub_element in attribute: 679 | for sub_key, sub_value in sub_element.items(): 680 | if sub_key == '@href': 681 | pics.append(sub_value) 682 | except: 683 | pass 684 | 685 | try: 686 | for item in value['pic:picture']['pic:link']: 687 | for key, value in item.items(): 688 | if key == '@href': 689 | pics.append(value) 690 | except: 691 | pass 692 | 693 | for item in pics: 694 | if '$_14' in item: 695 | cover_img = item 696 | break 697 | 698 | #sort pic list for cover image. fix class to contain single image for imparse in jinja 699 | searchResults.append(Results(id, price, title, description, address, seller_id, pics, cover_img)) 700 | except: 701 | print('Search Failed or No Results Returned') 702 | 703 | return render_template('results.html', data=searchResults) 704 | else: 705 | # User is not loggedin redirect to login page 706 | return redirect(url_for('login')) 707 | 708 | # http://localhost:5000/post 709 | @app.route('/post', methods=['GET', 'POST']) 710 | def post(): 711 | # Check if user is loggedin 712 | if 'loggedin' in session: 713 | # Post An Ad - Stage 1 - Select Category 714 | userID = session['user_id'] 715 | token = session['user_token'] 716 | global categoriesData 717 | categoriesData = getCategories(kijijiSession, userID, token) 718 | 719 | choiceList = [] 720 | 721 | for x in categoriesData['cat:categories']['cat:category']['cat:category']: 722 | choiceList.append(x['cat:id-name']) 723 | 724 | form = PostForm() 725 | form.cat1.choices = choiceList 726 | 727 | return render_template('post.html', form=form) 728 | else: 729 | # User is not loggedin redirect to login page 730 | return redirect(url_for('login')) 731 | 732 | 733 | @app.route('/submit', methods=['GET', 'POST']) 734 | def submit(): 735 | if 'loggedin' in session: 736 | # Submit Ad for Posting - Final Stage 737 | #reset locationsData 738 | global locationsData 739 | locationsData = '' 740 | # Need to get name from profile 741 | userID = session['user_id'] 742 | token = session['user_token'] 743 | parsed = getProfile(kijijiSession, userID, token) 744 | session['user_displayname'] = parsed['user:user-profile']['user:user-display-name'] 745 | 746 | # Process Form Data 747 | form = PostForm() 748 | 749 | attributes = {} 750 | attributesPayload = {} 751 | picturesPayload = {} 752 | remainders = {} 753 | locChoice = chooseLocation(form.loc1.data, form.loc2.data, form.loc3.data) 754 | 755 | # get submitted form items 756 | f = request.form 757 | for key in f.keys(): 758 | for value in f.getlist(key): 759 | # gather attributes (AttributeForm items) 760 | # filter out persistent form items to determine dynamic attributes 761 | if key != 'adtype' and key != 'adtitle' and key != 'cat1' and key != 'cat2' and key != 'cat3' and key != 'description' and key != 'pricetype' and key != 'price' and key != 'loc1' and key != 'loc2' and key != 'loc3' and key != 'postalcode' and key != 'phone' and key != 'file1' and key != 'file2' and key != 'file3' and key != 'file4' and key != 'file5' and key != 'file6' and key != 'file7' and key != 'file8' and key != 'file9' and key != 'file10' and key != 'repost' and key != 'time1' and key != 'time2' and key != 'time3' and key != 'time4' and key != 'time5' and key != 'time6' and key != 'time7' and key != 'time8' and key != 'password': 762 | # For Multipart Attributes like MultiCheckbox Enum 763 | if key in attributes: 764 | oldKey = attributes[key] 765 | attributes[key] = oldKey + ',' + value 766 | # For Standard Enum Attributes 767 | if key not in attributes: 768 | attributes[key] = value 769 | 770 | # gathter PostForm items 771 | else: 772 | remainders[key] = value 773 | 774 | 775 | # Build Attributes Payload 776 | if len(attributes) != 0: 777 | attributesPayload = {'attr:attributes':{'attr:attribute': []}} 778 | for key, value in attributes.items(): 779 | # Correct BOOLEAN Attritubes into correct formatting for kijiji 780 | if value == True or value == 'y': 781 | attributesPayload['attr:attributes']['attr:attribute'].append({'@type': '', '@localized-label': '', '@name': key, 'attr:value': 'true'}) 782 | 783 | # If above conditions do not apply, and value is not None, append attribute 784 | if (value != True or value != 'y') and (value != False or value != 'n') and (value != None and value != ''): 785 | attributesPayload['attr:attributes']['attr:attribute'].append({'@type': '', '@localized-label': '', '@name': key, 'attr:value': value}) 786 | 787 | # set xml type variable for Date based attributes 788 | if 'date' in key: 789 | attributesPayload['attr:attributes']['attr:attribute'].append({'@type': 'DATE', '@localized-label': '', '@name': key, 'attr:value': value+'T00:00:00Z'}) 790 | 791 | # Collect items from remainders 792 | #Variable initialization 793 | adtitle = '' 794 | description = '' 795 | adtype = '' 796 | postalcode = '' 797 | fulladdress = '' 798 | pricetype = '' 799 | price = '' 800 | fileData1 = b'' 801 | fileData2 = b'' 802 | fileData3 = b'' 803 | fileData4 = b'' 804 | fileData5 = b'' 805 | fileData6 = b'' 806 | fileData7 = b'' 807 | fileData8 = b'' 808 | fileData9 = b'' 809 | fileData10 = b'' 810 | pic1Link = '' 811 | pic2Link = '' 812 | pic3Link = '' 813 | pic4Link = '' 814 | pic5Link = '' 815 | pic6Link = '' 816 | pic7Link = '' 817 | pic8Link = '' 818 | pic9Link = '' 819 | pic10Link = '' 820 | adID = '' 821 | 822 | # put remainig form data into appropriate variables 823 | for key, value in remainders.items(): 824 | if key == 'adtitle': 825 | adtitle = value 826 | elif key == 'description': 827 | description = value 828 | elif key == 'adtype': 829 | adtype = value 830 | elif key == 'postalcode': 831 | postalcode = value 832 | elif key == 'fulladdress': # still yet to be implimented 833 | fulladdress = value 834 | elif key == 'pricetype': 835 | pricetype = value 836 | elif key == 'price': 837 | price = value 838 | 839 | # Begin assembling entire Payload 840 | responsePayload = { 841 | 'ad:ad': { 842 | '@xmlns:types': 'http://www.ebayclassifiedsgroup.com/schema/types/v1', 843 | '@xmlns:cat': 'http://www.ebayclassifiedsgroup.com/schema/category/v1', 844 | '@xmlns:loc': 'http://www.ebayclassifiedsgroup.com/schema/location/v1', 845 | '@xmlns:ad': 'http://www.ebayclassifiedsgroup.com/schema/ad/v1', 846 | '@xmlns:attr': 'http://www.ebayclassifiedsgroup.com/schema/attribute/v1', 847 | '@xmlns:pic': 'http://www.ebayclassifiedsgroup.com/schema/picture/v1', 848 | '@xmlns:user': 'http://www.ebayclassifiedsgroup.com/schema/user/v1', 849 | '@xmlns:rate': 'http://www.ebayclassifiedsgroup.com/schema/rate/v1', 850 | '@xmlns:reply': 'http://www.ebayclassifiedsgroup.com/schema/reply/v1', 851 | '@locale': 'en-CA'}} 852 | 853 | if adtitle != None and adtitle != '': 854 | responsePayload['ad:ad'].update({'ad:title': adtitle}) 855 | 856 | if description != None and description != '': 857 | responsePayload['ad:ad'].update({'ad:description': description}) 858 | 859 | if locChoice != None and locChoice != '': 860 | responsePayload['ad:ad'].update({'loc:locations': {'loc:location': {'@id': locChoice}}}) 861 | 862 | if adtype != None and adtype != '': 863 | responsePayload['ad:ad'].update({'ad:ad-type': {'ad:value': adtype}}) 864 | 865 | if session['cat'] != None and session['cat'] != '': 866 | responsePayload['ad:ad'].update({'cat:category': {'@id': session['cat']}}) 867 | 868 | if session['user_email'] != None and session['user_email'] != '': 869 | responsePayload['ad:ad'].update({'ad:email': session['user_email']}) 870 | 871 | if session['user_displayname'] != None and session['user_displayname'] != '': 872 | responsePayload['ad:ad'].update({'ad:poster-contact-name': session['user_displayname']}) 873 | 874 | if session['user_id'] != None and session['user_id'] != '': 875 | responsePayload['ad:ad'].update({'ad:account-id': session['user_id']}) 876 | 877 | if (postalcode != None and postalcode != '') or (fulladdress != None and fulladdress != ''): 878 | responsePayload['ad:ad'].update({'ad:ad-address': {}}) 879 | 880 | # Generate GeoLocation 881 | postal_code = checkPostalCodeLength(postalcode) 882 | nomi = pgeocode.Nominatim('ca') 883 | location = nomi.query_postal_code(postal_code) 884 | 885 | if fulladdress != None and fulladdress != '': 886 | responsePayload['ad:ad']['ad:ad-address'].update({'types:full-address': fulladdress}) 887 | 888 | if postalcode != None and postalcode != '': 889 | responsePayload['ad:ad']['ad:ad-address'].update({'types:zip-code': postalcode}) 890 | 891 | responsePayload['ad:ad']['ad:ad-address'].update({'types:longitude': location.longitude}) 892 | responsePayload['ad:ad']['ad:ad-address'].update({'types:latitude': location.latitude}) 893 | 894 | responsePayload['ad:ad'].update({'ad:visible-on-map': 'true'}) 895 | 896 | if (pricetype != None and pricetype != '') or (price != None and price != ''): 897 | responsePayload['ad:ad'].update({'ad:price': {}}) 898 | 899 | if pricetype != None and pricetype != '': 900 | responsePayload['ad:ad']['ad:price'].update({'types:price-type':{'types:value': pricetype}}) 901 | 902 | if price != None and price != '': 903 | responsePayload['ad:ad']['ad:price'].update({'types:amount': price}) 904 | 905 | # add attributes payload if attributes payload exist 906 | if len(attributesPayload) != 0: 907 | responsePayload['ad:ad'].update(attributesPayload) 908 | 909 | # Verify and Upload Pictures 910 | # Retreive Uploaded Picure Link 911 | # picLink function calls #picUpload function 912 | if (form.file1.data != None and form.file1.data != '' and form.file1.data != b'') or (form.file2.data != None and form.file2.data != '' and form.file2.data != b'') or (form.file3.data != None and form.file3.data != '' and form.file3.data != b'') or (form.file4.data != None and form.file4.data != '' and form.file4.data != b'') or (form.file5.data != None and form.file5.data != '' and form.file5.data != b'') or (form.file6.data != None and form.file6.data != '' and form.file6.data != b'') or (form.file7.data != None and form.file7.data != '' and form.file7.data != b'') or (form.file8.data != None and form.file8.data != '' and form.file8.data != b'') or (form.file9.data != None and form.file9.data != '' and form.file9.data != b'') or (form.file10.data != None and form.file10.data != '' and form.file10.data != b''): 913 | 914 | pic1Link = picLink(form.file1.data, kijijiSession) 915 | pic2Link = picLink(form.file2.data, kijijiSession) 916 | pic3Link = picLink(form.file3.data, kijijiSession) 917 | pic4Link = picLink(form.file4.data, kijijiSession) 918 | pic5Link = picLink(form.file5.data, kijijiSession) 919 | pic6Link = picLink(form.file6.data, kijijiSession) 920 | pic7Link = picLink(form.file7.data, kijijiSession) 921 | pic8Link = picLink(form.file8.data, kijijiSession) 922 | pic9Link = picLink(form.file9.data, kijijiSession) 923 | pic10Link = picLink(form.file10.data, kijijiSession) 924 | 925 | # Create Picture Payload 926 | if (pic1Link != None and pic1Link != '') or (pic2Link != None and pic2Link != '') or (pic3Link != None and pic3Link != '') or (pic4Link != None and pic4Link != '') or (pic5Link != None and pic5Link != '') or (pic6Link != None and pic6Link != '') or (pic7Link != None and pic7Link != '') or (pic8Link != None and pic8Link != '') or (pic9Link != None and pic9Link != '') or (pic10Link != None and pic10Link != ''): 927 | picturesPayload = {'pic:pictures':{'pic:picture': []}} 928 | 929 | if pic1Link != None and pic1Link != '': 930 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic1Link}}) 931 | 932 | if pic2Link != None and pic2Link != '': 933 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic2Link}}) 934 | 935 | if pic3Link != None and pic3Link != '': 936 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic3Link}}) 937 | 938 | if pic4Link != None and pic4Link != '': 939 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic4Link}}) 940 | 941 | if pic5Link != None and pic5Link != '': 942 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic5Link}}) 943 | 944 | if pic6Link != None and pic6Link != '': 945 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic6Link}}) 946 | 947 | if pic7Link != None and pic7Link != '': 948 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic7Link}}) 949 | 950 | if pic8Link != None and pic8Link != '': 951 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic8Link}}) 952 | 953 | if pic9Link != None and pic9Link != '': 954 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic9Link}}) 955 | 956 | if pic10Link != None and pic10Link != '': 957 | picturesPayload['pic:pictures']['pic:picture'].append({'pic:link': { '@rel': 'saved', '@href': pic10Link}}) 958 | 959 | # add attributes payload if attributes payload exist 960 | if len(picturesPayload) != 0: 961 | responsePayload['ad:ad'].update(picturesPayload) 962 | 963 | # Parse final payload into XML 964 | finalPayload = xmltodict.unparse(responsePayload, short_empty_elements=True, pretty=True) 965 | 966 | # Debug Option to evaluate Submissions 967 | # if debugMode = True, will not submit final payload 968 | # and instead prints to console for evaluation 969 | debugMode = False 970 | 971 | if debugMode == True: 972 | print(finalPayload) 973 | 974 | if debugMode == False: 975 | # Submit Final Payload 976 | parsed = submitFunction(kijijiSession, userID, token, finalPayload) 977 | # Retreive Ad ID# for newly posted Ad 978 | adID = parsed['ad:ad']['@id'] 979 | 980 | # Create Repost Data 981 | if form.repost.data == True: 982 | # check if user path exists 983 | # if not create it 984 | # write ad file to user directory 985 | path = 'user/' + session['user_id'] 986 | userDir = os.path.exists(path) 987 | if userDir == True: 988 | # write ad to file 989 | with open(path + '/' + adID + '.xml', 'w') as w: 990 | w.write(finalPayload) 991 | else: 992 | #create directory 993 | os.makedirs(path) 994 | #write ad to file 995 | with open(path + '/' + adID + '.xml', 'w') as w: 996 | w.write(finalPayload) 997 | 998 | # Update schedules.json with repost information 999 | useremail = session['user_email'] 1000 | userpassword = form.password.data 1001 | ad_file = path + '/' + adID + '.xml' 1002 | ad_id = adID 1003 | 1004 | newSchedule = { 1005 | 'time1': timeValidator(form.time1.data), 1006 | 'time2': timeValidator(form.time2.data), 1007 | 'time3': timeValidator(form.time3.data), 1008 | 'time4': timeValidator(form.time4.data), 1009 | 'time5': timeValidator(form.time5.data), 1010 | 'time6': timeValidator(form.time6.data), 1011 | 'time7': timeValidator(form.time7.data), 1012 | 'time8': timeValidator(form.time8.data), 1013 | 'useremail': useremail, 1014 | 'userpassword': userpassword, 1015 | 'ad_file': ad_file, 1016 | 'current_ad_id': ad_id 1017 | } 1018 | 1019 | jsonFile = os.path.join(THIS_FOLDER, 'static/schedules.json') 1020 | 1021 | with open(jsonFile, 'r') as json_file: 1022 | data = json.load(json_file) 1023 | update = data['schedules'] 1024 | update.append(newSchedule) 1025 | 1026 | with open(jsonFile,'w') as json_file: 1027 | json.dump(data, json_file, indent=4) 1028 | 1029 | return redirect(url_for('home')) 1030 | else: 1031 | return redirect(url_for('login')) 1032 | 1033 | @app.route('/cat/') 1034 | def category_choice(choice): 1035 | choiceList = [] 1036 | for x in categoriesData['cat:categories']['cat:category']['cat:category']: 1037 | try: 1038 | if x['cat:id-name'] == choice: 1039 | for y in x['cat:category']: 1040 | choiceObj = {} 1041 | choiceObj['id'] = y['@id'] 1042 | choiceObj['name'] = y['cat:id-name'] 1043 | choiceList.append(choiceObj) 1044 | except: 1045 | choiceObj = {} 1046 | choiceObj['id'] = '' 1047 | choiceObj['name'] = '' 1048 | choiceList.append(choiceObj) 1049 | 1050 | return jsonify(choiceList) 1051 | 1052 | 1053 | @app.route('/cat2/') 1054 | def category_choice2(choice): 1055 | split = choice.split('~') 1056 | choiceList = [] 1057 | 1058 | for x in categoriesData['cat:categories']['cat:category']['cat:category']: 1059 | if x['cat:id-name'] == split[0]: 1060 | try: 1061 | for y in x['cat:category']: 1062 | try: 1063 | if y['@id'] == split[1]: 1064 | for z in y['cat:category']: 1065 | choiceObj = {} 1066 | choiceObj['id'] = z['@id'] 1067 | choiceObj['name'] = z['cat:id-name'] 1068 | choiceList.append(choiceObj) 1069 | except: 1070 | choiceObj = {} 1071 | choiceObj['id'] = '' 1072 | choiceObj['name'] = '' 1073 | choiceList.append(choiceObj) 1074 | except: 1075 | print('No Sub-Categories') 1076 | 1077 | return jsonify(choiceList) 1078 | 1079 | @app.route('/loc/') 1080 | def location_choice(choice): 1081 | choiceList = [] 1082 | for x in locationsData['loc:locations']['loc:location']['loc:location']: 1083 | try: 1084 | if x['loc:localized-name'] == choice: 1085 | for y in x['loc:location']: 1086 | choiceObj = {} 1087 | choiceObj['id'] = y['@id'] 1088 | choiceObj['name'] = y['loc:localized-name'] 1089 | choiceObj['long'] = y['loc:longitude'] 1090 | choiceObj['lat'] = y['loc:latitude'] 1091 | choiceList.append(choiceObj) 1092 | except: 1093 | choiceObj = {} 1094 | choiceObj['id'] = '' 1095 | choiceObj['name'] = '' 1096 | choiceObj['long'] = '' 1097 | choiceObj['lat'] = '' 1098 | choiceList.append(choiceObj) 1099 | 1100 | return jsonify(choiceList) 1101 | 1102 | @app.route('/loc2/') 1103 | def location_choice2(choice): 1104 | split = choice.split('~') 1105 | choiceList = [] 1106 | 1107 | for x in locationsData['loc:locations']['loc:location']['loc:location']: 1108 | try: 1109 | if x['loc:localized-name'] == split[0]: 1110 | for y in x['loc:location']: 1111 | if y['@id'] == split[1]: 1112 | for z in y['loc:location']: 1113 | choiceObj = {} 1114 | choiceObj['id'] = z['@id'] 1115 | choiceObj['name'] = z['loc:localized-name'] 1116 | choiceObj['long'] = z['loc:longitude'] 1117 | choiceObj['lat'] = z['loc:latitude'] 1118 | choiceList.append(choiceObj) 1119 | 1120 | except: 1121 | choiceObj = {} 1122 | choiceObj['id'] = '' 1123 | choiceObj['name'] = '' 1124 | choiceObj['long'] = '' 1125 | choiceObj['lat'] = '' 1126 | choiceList.append(choiceObj) 1127 | 1128 | return jsonify(choiceList) 1129 | 1130 | 1131 | @app.route('/attributes', methods=['GET', 'POST']) 1132 | def attributes(): 1133 | if 'loggedin' in session: 1134 | # reset categoriesData 1135 | global categoriesData 1136 | categoriesData = '' 1137 | 1138 | postForm = PostForm() 1139 | catChoice = chooseCategory(postForm.cat1.data, postForm.cat2.data, postForm.cat3.data) 1140 | 1141 | # create session variable to pass category choice to stage2.html 1142 | session['cat'] = catChoice 1143 | 1144 | # Location Options 1145 | userID = session['user_id'] 1146 | token = session['user_token'] 1147 | 1148 | global locationsData 1149 | locationsData = getLocations(kijijiSession, userID, token) 1150 | locationList = [] 1151 | 1152 | try: 1153 | for y in locationsData['loc:locations']['loc:location']['loc:location']: 1154 | locationList.append(y['loc:localized-name']) 1155 | except: 1156 | locationList.append(locationsData['loc:locations']['loc:location']['loc:localized-name']) 1157 | 1158 | postForm.loc1.choices = locationList 1159 | attributesFile = getAttributes(kijijiSession, userID, token, catChoice) 1160 | 1161 | # Update Ad Type Choices based on xml items 1162 | # Currently static, but allows for future flexibility 1163 | try: 1164 | items = [(x['#text'], x['@localized-label']) for x in attributesFile['ad:ad']['ad:ad-type']['ad:supported-value']] 1165 | postForm.adtype.choices = items 1166 | except: 1167 | print('No Ad Types Available') 1168 | 1169 | # Begin Parsing Attributes xml for selected category 1170 | # Initialize Attribute Containers 1171 | enumDict = {'enums':[]} 1172 | enumMultiDict = {'enums':[]} 1173 | stringDict = {'strings':[]} 1174 | integerDict = {'integers':[]} 1175 | dateDict = {'dates':[]} 1176 | boolDict = {'bools':[]} 1177 | 1178 | if 'attr:attribute' in attributesFile['ad:ad']['attr:attributes']: 1179 | 1180 | if testDictInstance(attributesFile['ad:ad']['attr:attributes']['attr:attribute']) == True: 1181 | print('Error 0000: This attribute set is currently unimplemented, please report to developer!') 1182 | 1183 | if testListInstance(attributesFile['ad:ad']['attr:attributes']['attr:attribute']) == True: 1184 | 1185 | for x in attributesFile['ad:ad']['attr:attributes']['attr:attribute']: 1186 | 1187 | if testDictInstance(x) == True: 1188 | 1189 | # Parse Standard ENUM Attributes 1190 | if x['@deprecated'] == 'false' and x['@write'] != 'unsupported' and '@sub-type' not in x and x['@type'] == 'ENUM': 1191 | 1192 | newitem = { 1193 | 'label': { 1194 | x['@name']: x['@localized-label'] 1195 | }, 1196 | 'choices': {} 1197 | } 1198 | 1199 | if 'attr:supported-value' in x and testDictInstance(x['attr:supported-value']) == True: 1200 | print('Error 0001: This attribute set is currently unimplemented, please report to developer!') 1201 | 1202 | if 'attr:supported-value' in x and testListInstance(x['attr:supported-value']) == True: 1203 | for y in x['attr:supported-value']: 1204 | 1205 | if testDictInstance(y) == True: 1206 | newitem['choices'].update({y['#text']: y['@localized-label']}) 1207 | 1208 | if testListInstance(y) == True: 1209 | print('Error 0002: This attribute set is currently unimplemented, please report to developer!') 1210 | 1211 | enumDict['enums'].append(newitem) 1212 | 1213 | # Parse Optional Enum Attributes 1214 | if '@sub-type' in x: 1215 | 1216 | newitem = { 1217 | 'label': { 1218 | x['@name']: x['@localized-label'] 1219 | }, 1220 | 'choices': {} 1221 | } 1222 | 1223 | if 'attr:supported-value' in x and testDictInstance(x['attr:supported-value']) == True: 1224 | 1225 | trigger = False 1226 | localized = '' 1227 | text = '' 1228 | 1229 | for y in x['attr:supported-value'].items(): 1230 | if y[0] == '@localized-label': 1231 | localized = y[1] 1232 | 1233 | if y[0] == '#text': 1234 | text = y[1] 1235 | 1236 | newitem['choices'].update({text: localized}) 1237 | 1238 | if 'attr:supported-value' in x and testListInstance(x['attr:supported-value']) == True: 1239 | 1240 | for y in x['attr:supported-value']: 1241 | newitem['choices'].update({y['#text']: y['@localized-label']}) 1242 | 1243 | enumMultiDict['enums'].append(newitem) 1244 | 1245 | if testListInstance(x) == True: 1246 | print('Error 0003: This attribute set is currently unimplemented, please report to developer!') 1247 | 1248 | # Parse STRING Types 1249 | if x['@deprecated'] == 'false' and x['@write'] != 'unsupported' and x['@type'] == 'STRING': 1250 | 1251 | newitem = { 1252 | 'label': { 1253 | x['@name']: x['@localized-label'] 1254 | } 1255 | } 1256 | 1257 | stringDict['strings'].append(newitem) 1258 | 1259 | # Parse INTEGER Types 1260 | if x['@deprecated'] == 'false' and x['@write'] != 'unsupported' and x['@type'] == 'INTEGER': 1261 | 1262 | newitem = { 1263 | 'label': { 1264 | x['@name']: x['@localized-label'] 1265 | } 1266 | } 1267 | 1268 | integerDict['integers'].append(newitem) 1269 | 1270 | # Parse DATE Types 1271 | if x['@deprecated'] == 'false' and x['@write'] != 'unsupported' and x['@type'] == 'DATE': 1272 | 1273 | newitem = { 1274 | 'label': { 1275 | x['@name']: x['@localized-label'] 1276 | } 1277 | } 1278 | 1279 | dateDict['dates'].append(newitem) 1280 | 1281 | # Parse BOOLEAN Types 1282 | if x['@deprecated'] == 'false' and x['@write'] != 'unsupported' and x['@type'] == 'BOOLEAN': 1283 | 1284 | newitem = { 1285 | 'label': { 1286 | x['@name']: x['@localized-label'] 1287 | } 1288 | } 1289 | 1290 | boolDict['bools'].append(newitem) 1291 | 1292 | # Dynamic Forms for Attributes (temporary class) 1293 | class AttributeForm(FlaskForm): 1294 | class Meta: 1295 | csrf = False 1296 | 1297 | # Create Dyname Form Attributes / Elements 1298 | # Create Standard ENUM Type Attributes 1299 | for item in enumDict['enums']: 1300 | 1301 | labels = [] 1302 | choices = [] 1303 | 1304 | for labelAttribute in item['label'].items(): 1305 | labels.append(labelAttribute) 1306 | fieldID = labelAttribute[0] 1307 | title = labelAttribute[1] 1308 | 1309 | for choiceAttribute in item['choices'].items(): 1310 | choices.append(choiceAttribute) 1311 | 1312 | setattr(AttributeForm, fieldID, SelectField(title, choices=choices)) 1313 | choices = [] 1314 | 1315 | # Create Optional ENUM Type Attributes 1316 | for item in enumMultiDict['enums']: 1317 | 1318 | labels = [] 1319 | choices = [] 1320 | 1321 | for labelAttribute in item['label'].items(): 1322 | labels.append(labelAttribute) 1323 | fieldID = labelAttribute[0] 1324 | title = labelAttribute[1] 1325 | 1326 | for choiceAttribute in item['choices'].items(): 1327 | choices.append(choiceAttribute) 1328 | 1329 | setattr(AttributeForm, fieldID, MultiCheckboxField(title, choices=choices)) 1330 | choices = [] 1331 | 1332 | # Create STRING Type Attributes 1333 | for item in stringDict['strings']: 1334 | for labelAttribute in item['label'].items(): 1335 | fieldID = labelAttribute[0] 1336 | title = labelAttribute[1] 1337 | setattr(AttributeForm, fieldID, TextField(title)) 1338 | 1339 | # Create INTEGER Type Attributes 1340 | for item in integerDict['integers']: 1341 | for labelAttribute in item['label'].items(): 1342 | fieldID = labelAttribute[0] 1343 | title = labelAttribute[1] 1344 | setattr(AttributeForm, fieldID, IntegerField(title)) 1345 | 1346 | # Create DATE Type Attributes 1347 | for item in dateDict['dates']: 1348 | for labelAttribute in item['label'].items(): 1349 | fieldID = labelAttribute[0] 1350 | title = labelAttribute[1] 1351 | setattr(AttributeForm, fieldID, DateField(title)) 1352 | 1353 | # Create BOOLEAN Type Attributes 1354 | for item in boolDict['bools']: 1355 | for labelAttribute in item['label'].items(): 1356 | fieldID = labelAttribute[0] 1357 | title = labelAttribute[1] 1358 | setattr(AttributeForm, fieldID, BooleanField(title)) 1359 | 1360 | # Initialize Dynamic Form at the End, after elements have been generated 1361 | attribForm = AttributeForm() 1362 | 1363 | return render_template('stage2.html', postForm=postForm, attribForm=attribForm, attrib=catChoice) 1364 | else: 1365 | return redirect(url_for('login')) 1366 | 1367 | @app.route('/make/', methods=['GET', 'POST']) 1368 | def make_choice(choice): 1369 | 1370 | split = choice.split('~') 1371 | 1372 | attributesFile = os.path.join(THIS_FOLDER, 'static/attributes/' + split[1]) 1373 | attributeData = getXML(attributesFile) 1374 | choiceList = [] 1375 | 1376 | for x in attributeData['ad:ad']['attr:dependent-attributes']['attr:dependent-attribute']['attr:dependent-supported-value']: 1377 | try: 1378 | 1379 | if x['attr:supported-value']['#text'] == split[0]: 1380 | 1381 | for y in x['attr:dependent-attribute']['attr:supported-value']: 1382 | choiceObj = {} 1383 | choiceObj['name'] = y['@localized-label'] 1384 | choiceObj['id'] = y['#text'] 1385 | choiceList.append(choiceObj) 1386 | except: 1387 | print('error') 1388 | choiceObj = {} 1389 | choiceObj['name'] = '' 1390 | choiceObj['id'] = '' 1391 | choiceList.append(choiceObj) 1392 | 1393 | return jsonify(choiceList) 1394 | 1395 | # http://localhost:5000/delete 1396 | @app.route('/delete/') 1397 | def delete(adID): 1398 | # Check if user is loggedin 1399 | if 'loggedin' in session: 1400 | # delete ad from kijiji account 1401 | userID = session['user_id'] 1402 | token = session['user_token'] 1403 | deleteAd(kijijiSession, userID, adID, token) 1404 | 1405 | # Remove Schedule associated with ad 1406 | scheduleFile = os.path.join(THIS_FOLDER, 'static/schedules.json') 1407 | with open(scheduleFile, 'r') as f: 1408 | data = json.load(f) 1409 | 1410 | for item in range(len(data['schedules'])): 1411 | if data['schedules'][item]['current_ad_id'] == adID: 1412 | del data['schedules'][item] 1413 | break 1414 | 1415 | with open(scheduleFile,'w') as f: 1416 | json.dump(data, f, indent=4) 1417 | 1418 | return redirect(url_for('home')) 1419 | else: 1420 | # User is not loggedin redirect to login page 1421 | return redirect(url_for('login')) 1422 | 1423 | @app.route('/schedule/', methods=['GET', 'POST']) 1424 | def schedule(adID): 1425 | 1426 | if 'loggedin' in session: 1427 | 1428 | scheduleFile = os.path.join(THIS_FOLDER, 'static/schedules.json') 1429 | with open(scheduleFile, 'r') as f: 1430 | data = json.load(f) 1431 | schedules = [] 1432 | for item in data['schedules']: 1433 | if item['current_ad_id'] == adID: 1434 | times = [item['time1'],item['time2'],item['time3'],item['time4'],item['time5'],item['time6'],item['time7'],item['time8']] 1435 | schedules.extend(times) 1436 | 1437 | return render_template('schedule.html', schedules=schedules, adID=adID) 1438 | else: 1439 | return redirect(url_for('login')) 1440 | 1441 | @app.route('/reschedule', methods=['GET', 'POST']) 1442 | def reschedule(): 1443 | # variable initialization 1444 | time1 = '' 1445 | time2 = '' 1446 | time3 = '' 1447 | time4 = '' 1448 | time5 = '' 1449 | time6 = '' 1450 | time7 = '' 1451 | time8 = '' 1452 | adID = '' 1453 | r = request.form 1454 | for key, value in r.items(): 1455 | if key == 'time1': 1456 | time1 = value 1457 | elif key == 'time2': 1458 | time2 = value 1459 | elif key == 'time3': 1460 | time3 = value 1461 | elif key == 'time4': 1462 | time4 = value 1463 | elif key == 'time5': 1464 | time5 = value 1465 | elif key == 'time6': 1466 | time6 = value 1467 | elif key == 'time7': 1468 | time7 = value 1469 | elif key == 'time8': 1470 | time8 = value 1471 | elif key == 'adID': 1472 | adID = value 1473 | 1474 | scheduleFile = os.path.join(THIS_FOLDER, 'static/schedules.json') 1475 | with open(scheduleFile, 'r') as f: 1476 | data = json.load(f) 1477 | for item in data['schedules']: 1478 | if item['current_ad_id'] == adID: 1479 | 1480 | if time1 == 'NONE': 1481 | item['time1'] = None 1482 | elif time1 != '' and time1 != None: 1483 | item['time1'] = time1 1484 | 1485 | if time2 == 'NONE': 1486 | item['time2'] = None 1487 | elif time2 != '' and time2 != None: 1488 | item['time2'] = time2 1489 | 1490 | if time3 == 'NONE': 1491 | item['time3'] = None 1492 | elif time3 != '' and time3 != None: 1493 | item['time3'] = time3 1494 | 1495 | if time4 == 'NONE': 1496 | item['time4'] = None 1497 | elif time4 != '' and time4 != None: 1498 | item['time4'] = time4 1499 | 1500 | if time5 == 'NONE': 1501 | item['time5'] = None 1502 | elif time5 != '' and time5 != None: 1503 | item['time5'] = time5 1504 | 1505 | if time6 == 'NONE': 1506 | item['time6'] = None 1507 | elif time6 != '' and time6 != None: 1508 | item['time6'] = time6 1509 | 1510 | if time7 == 'NONE': 1511 | item['time7'] = None 1512 | elif time7 != '' and time7 != None: 1513 | item['time7'] = time7 1514 | 1515 | if time8 == 'NONE': 1516 | item['time8'] = None 1517 | elif time8 != '' and time8 != None: 1518 | item['time8'] = time8 1519 | 1520 | with open(scheduleFile, 'w') as f: 1521 | json.dump(data, f, indent=4) 1522 | 1523 | return redirect(url_for('home')) 1524 | 1525 | @app.route('/favicon.ico') 1526 | def favicon(): 1527 | return send_from_directory(os.path.join(app.root_path, 'static'), 'favicon.ico', mimetype='image/vnd.microsoft.icon') 1528 | 1529 | @app.template_filter('convert') 1530 | def unix_timestamp_to_string_date(unix_time): 1531 | try: 1532 | cut_time = unix_time.replace('Z', '') 1533 | converted = datetime.datetime.strptime(cut_time, '%Y-%m-%dT%H:%M:%S.%f') 1534 | return converted 1535 | except: 1536 | return None 1537 | 1538 | # Grabs first thumbnail image for each ad in users ad list for home page 1539 | @app.template_filter('imgparse') 1540 | def parse(data): 1541 | try: 1542 | for p in data['pic:pictures']['pic:picture']: 1543 | try: 1544 | for x in p['pic:link']: 1545 | parsed = x['@href'] 1546 | return parsed 1547 | except: 1548 | for x in data['pic:pictures']['pic:picture']['pic:link']: 1549 | parsed = x['@href'] 1550 | return parsed 1551 | 1552 | except: 1553 | print('Error Parsing Images or No Image to Return') 1554 | return None 1555 | 1556 | # Builds a list of image urls for individual ad page 1557 | @app.template_filter('imglist') 1558 | def buildImgList(data): 1559 | image_list = [] 1560 | try: 1561 | for p in data['pic:pictures']['pic:picture']: 1562 | try: 1563 | for x in p['pic:link']: 1564 | image_list.append(x['@href']) 1565 | except: 1566 | for x in data['pic:pictures']['pic:picture']['pic:link']: 1567 | image_list.append(x['@href']) 1568 | 1569 | except: 1570 | print('Error Parsing Images or No Image to Return') 1571 | empty = '' 1572 | return empty 1573 | return image_list 1574 | 1575 | @app.template_filter('checkSchedule') 1576 | def checkSchedule(adID): 1577 | 1578 | scheduleFile = os.path.join(THIS_FOLDER, 'static/schedules.json') 1579 | with open(scheduleFile, 'r') as f: 1580 | data = json.load(f) 1581 | 1582 | for item in data['schedules']: 1583 | if item['current_ad_id'] == adID: 1584 | return True 1585 | 1586 | # if list, then more than one ad 1587 | @app.template_filter('testlist') 1588 | def testlist(data): 1589 | if isinstance(data['ad:ads']['ad:ad'],list): 1590 | return True 1591 | 1592 | # if list, then more than one ad 1593 | @app.template_filter('testreplylist') 1594 | def testreplylist(conversation): 1595 | if isinstance(conversation['user:user-conversation']['user:user-message'],list): 1596 | return True 1597 | 1598 | # Force Post 1599 | @app.route('/force', methods=['GET', 'POST']) 1600 | def force(): 1601 | 1602 | if 'loggedin' in session: 1603 | 1604 | return render_template('force.html') 1605 | else: 1606 | return redirect(url_for('login')) 1607 | 1608 | @app.route('/forcepost', methods=['GET', 'POST']) 1609 | def forcepost(): 1610 | 1611 | if 'loggedin' in session: 1612 | 1613 | now = datetime.datetime.now() 1614 | 1615 | try: 1616 | 1617 | # Variable Initialization 1618 | file = '' 1619 | email = '' 1620 | password = '' 1621 | 1622 | # Get Form Data 1623 | r = request.form 1624 | for key, value in r.items(): 1625 | if key == 'file': 1626 | file = value 1627 | elif key == 'email': 1628 | email = value 1629 | elif key == 'password': 1630 | password = value 1631 | 1632 | # Login 1633 | userID, userToken = loginFunction(kijijiSession, email, password) 1634 | 1635 | # Open file / Get payload 1636 | with open(file, 'r') as f: 1637 | payload = f.read() 1638 | 1639 | # Post Ad 1640 | parsed = submitFunction(kijijiSession, userID, userToken, payload) 1641 | 1642 | print('Forced Reposting Completed at: ', now) 1643 | 1644 | except: 1645 | print('Error: Forced Reposting Failed at: ', now) 1646 | 1647 | return redirect(url_for('force')) 1648 | else: 1649 | return redirect(url_for('login')) 1650 | 1651 | # Conversations 1652 | @app.route('/conversations/', methods=['GET', 'POST']) 1653 | def conversations(page): 1654 | 1655 | if 'loggedin' in session: 1656 | 1657 | # Get Credentials 1658 | userID = session['user_id'] 1659 | token = session['user_token'] 1660 | # Fetch Mail 1661 | conversations = getConversations(kijijiSession, userID, token, page) 1662 | 1663 | return render_template('conversations.html', conversations = conversations, page=page) 1664 | else: 1665 | return redirect(url_for('login')) 1666 | 1667 | @app.template_filter('increment') 1668 | def increment(page): 1669 | if page is not None and page != 'None': 1670 | newpage = (int(page) + 1) 1671 | link = '/conversations/' + str(newpage) 1672 | return link 1673 | 1674 | @app.template_filter('increment_search') 1675 | def increment_search(page): 1676 | if page is not None and page != 'None': 1677 | newpage = (int(page) + 1) 1678 | link = '/results/' + str(newpage) 1679 | return link 1680 | 1681 | @app.route('/conversation/', methods=['GET', 'POST']) 1682 | def conversation(conversationID): 1683 | 1684 | if 'loggedin' in session: 1685 | 1686 | # Get Credentials 1687 | userID = session['user_id'] 1688 | token = session['user_token'] 1689 | #Set form 1690 | form = ConversationForm() 1691 | # Fetch Mail 1692 | conversation = getConversation(kijijiSession, userID, token, conversationID) 1693 | 1694 | return render_template('conversation.html', conversation = conversation, form=form) 1695 | else: 1696 | return redirect(url_for('login')) 1697 | 1698 | @app.route('/reply/', methods=['GET', 'POST']) 1699 | def reply(info): 1700 | 1701 | if 'loggedin' in session: 1702 | 1703 | # Get Credentials 1704 | userID = session['user_id'] 1705 | token = session['user_token'] 1706 | #Set form 1707 | form = ConversationForm() 1708 | #Get Reply Data 1709 | reply = form.reply.data 1710 | # Split data elements from info variable 1711 | data = info.split('~') 1712 | conversationID = data[0] 1713 | adID = data[1] 1714 | ownerUserID = data[2] 1715 | ownerEmail = data[3] 1716 | ownerName = data[4] 1717 | replierUserID = data[5] 1718 | replierEmail = data[6] 1719 | replierName = data[7] 1720 | # Initialize Reply Variables 1721 | direction = '' 1722 | replyName = '' 1723 | replyEmail = '' 1724 | if ownerUserID == userID: 1725 | replyName = ownerName 1726 | replyEmail = ownerEmail 1727 | direction = 'TO_BUYER' 1728 | 1729 | elif replierUserID == userID: 1730 | replyName = replierName 1731 | replyEmail = replierEmail 1732 | direction = 'TO_OWNER' 1733 | 1734 | # Create final payload 1735 | finalPayload = createReplyPayload(adID, replyName, replyEmail, reply, conversationID, direction) 1736 | 1737 | # Send Reply 1738 | sendReply(kijijiSession, userID, token, finalPayload) 1739 | 1740 | # Refresh Conversation 1741 | time.sleep(2) 1742 | return redirect(url_for('conversation', conversationID=conversationID)) 1743 | else: 1744 | return redirect(url_for('login')) 1745 | 1746 | @app.route('/reply_ad/', methods=['GET', 'POST']) 1747 | def reply_ad(adID): 1748 | 1749 | if 'loggedin' in session: 1750 | 1751 | # Get Credentials 1752 | userID = session['user_id'] 1753 | token = session['user_token'] 1754 | #Set form 1755 | form = ConversationForm() 1756 | #Get Reply Data 1757 | reply = form.reply.data 1758 | #Get Current User Profile Data 1759 | profile = getProfile(kijijiSession, userID, token) 1760 | 1761 | # Initialize Reply Variables 1762 | replyName = profile['user:user-profile']['user:user-display-name'] 1763 | replyEmail = profile['user:user-profile']['user:user-email'] 1764 | 1765 | # Create final payload 1766 | finalPayload = createReplyAdPayload(adID, replyName, replyEmail, reply) 1767 | 1768 | # Send Reply 1769 | sendReply(kijijiSession, userID, token, finalPayload) 1770 | 1771 | # Refresh Conversation 1772 | time.sleep(2) 1773 | return redirect(url_for('viewad', adID=adID)) 1774 | else: 1775 | return redirect(url_for('login')) 1776 | 1777 | # Message Auto Replier 1778 | @app.route('/autoreplier', methods=['GET', 'POST']) 1779 | def autoreplier(): 1780 | 1781 | if 'loggedin' in session: 1782 | 1783 | messagesFile = os.path.join(THIS_FOLDER, 'static/messages.json') 1784 | 1785 | # Get Credentials 1786 | userID = session['user_id'] 1787 | userEmail = session['user_email'] 1788 | 1789 | with open(messagesFile, 'r') as f: 1790 | data = json.load(f) 1791 | rules = {} 1792 | for item in data['users']: 1793 | if item['user'] == userID: 1794 | rules = item 1795 | 1796 | return render_template('autoreplier.html', userID=userID, userEmail = userEmail, rules=rules) 1797 | else: 1798 | return redirect(url_for('login')) 1799 | 1800 | # Add New Rule to Auto Replier 1801 | @app.route('/updatereplier', methods=['GET', 'POST']) 1802 | def updatereplier(): 1803 | 1804 | if 'loggedin' in session: 1805 | 1806 | # Variable Initialization 1807 | userID = '' 1808 | userEmail = '' 1809 | password = '' 1810 | rule = '' 1811 | response = '' 1812 | 1813 | # Retrieve Form Data 1814 | r = request.form 1815 | 1816 | for key, value in r.items(): 1817 | if key == 'userID': 1818 | userID = value 1819 | elif key == 'userEmail': 1820 | userEmail = value 1821 | elif key == 'rule': 1822 | rule = value 1823 | elif key == 'response': 1824 | response = value 1825 | elif key == 'password': 1826 | password = value 1827 | 1828 | messagesFile = os.path.join(THIS_FOLDER, 'static/messages.json') 1829 | 1830 | # search to see if user exists 1831 | # if not, use complete 1832 | # if user does exist, just append basic 1833 | 1834 | newRuleComplete = { 1835 | "user": userID, 1836 | "useremail": userEmail, 1837 | "userpassword": password, 1838 | "rules": [ 1839 | { 1840 | "rule": rule, 1841 | "response": response 1842 | } 1843 | ] 1844 | } 1845 | 1846 | newRuleBasic = { 1847 | "rule": rule, 1848 | "response": response 1849 | } 1850 | 1851 | with open(messagesFile, 'r') as json_file: 1852 | data = json.load(json_file) 1853 | 1854 | if len(data['users']) != 0: 1855 | for item in data['users']: 1856 | if item['user'] == userID: 1857 | update = item['rules'] 1858 | update.append(newRuleBasic) 1859 | 1860 | for item in data['users']: 1861 | if item['user'] != userID: 1862 | update = data['users'] 1863 | update.append(newRuleComplete) 1864 | else: 1865 | update = data['users'] 1866 | update.append(newRuleComplete) 1867 | 1868 | 1869 | with open(messagesFile,'w') as json_file: 1870 | json.dump(data, json_file, indent=4) 1871 | 1872 | # Retreive Updated rules to send to autoreplier page 1873 | with open(messagesFile, 'r') as f: 1874 | data = json.load(f) 1875 | rules = {} 1876 | for item in data['users']: 1877 | if item['user'] == userID: 1878 | rules = item 1879 | 1880 | return redirect(url_for('autoreplier', userID=userID, userEmail = userEmail, rules=rules)) 1881 | else: 1882 | return redirect(url_for('login')) 1883 | 1884 | @app.route('/removerule/') 1885 | def removerule(rule): 1886 | # Check if user is loggedin 1887 | if 'loggedin' in session: 1888 | userID = session['user_id'] 1889 | writeActivate = False 1890 | messagesFile = os.path.join(THIS_FOLDER, 'static/messages.json') 1891 | with open(messagesFile, 'r') as f: 1892 | data = json.load(f) 1893 | # Search for Rule and Delete if Found 1894 | for user in data['users']: 1895 | if user['user'] == userID: 1896 | for num, item in enumerate(user['rules']): 1897 | for index, message in item.items(): 1898 | if re.search(r"\b{}\b".format(rule), message): 1899 | writeActivate = True 1900 | del user['rules'][num] 1901 | break 1902 | 1903 | # Write Changes if Activated 1904 | if writeActivate == True: 1905 | with open(messagesFile,'w') as f: 1906 | json.dump(data, f, indent=4) 1907 | 1908 | return redirect(url_for('autoreplier')) 1909 | else: 1910 | # User is not loggedin redirect to login page 1911 | return redirect(url_for('login')) 1912 | 1913 | 1914 | # Run Scheduler as Daemon in Background 1915 | sched = BackgroundScheduler(daemon=True) 1916 | sched.add_job(reposter,'cron',minute='*') # every minute 1917 | sched.add_job(messageAutoReplier,'cron',minute='*/25') # every 25 minutes 1918 | sched.start() 1919 | atexit.register(lambda: sched.shutdown()) 1920 | 1921 | if __name__ == "__main__": 1922 | app.run(debug=True, host='0.0.0.0', use_reloader=False) # disable reloader, messes with apscheduler 1923 | --------------------------------------------------------------------------------