Browse Source

Added remaining scripts

Arnaud Vergnet 4 years ago
parent
commit
e91e6ba41e

+ 14
- 0
custom_css/RU/customGeneral.css View File

@@ -0,0 +1,14 @@
1
+body {
2
+	font-size: 1.5rem;
3
+	text-shadow: none;
4
+	font-family: 'Roboto';
5
+}
6
+
7
+
8
+#ru_page {
9
+	padding: 5px !important;
10
+}
11
+
12
+.ui-bar, header {
13
+	display: none;
14
+}

+ 4
- 0
custom_css/RU/customLight.css View File

@@ -0,0 +1,4 @@
1
+#ru_page {
2
+	background-color: #fff;
3
+	color: #000;
4
+}

+ 3
- 0
custom_css/bluemind/customMobile.css View File

@@ -0,0 +1,3 @@
1
+#mailview-bottom {
2
+	min-height: 1000px;
3
+}

+ 15
- 0
custom_css/ent/customMobile.css View File

@@ -0,0 +1,15 @@
1
+#columns-table > tbody > tr > td {
2
+
3
+    display: block;
4
+
5
+    width: 100%;
6
+
7
+}
8
+
9
+
10
+
11
+.portlet {
12
+
13
+    padding : 5px;
14
+
15
+}

+ 23
- 0
custom_css/planex/customDark.css View File

@@ -0,0 +1,23 @@
1
+body {
2
+    background-color: #222;
3
+}
4
+
5
+.fc-unthemed .fc-content, .fc-unthemed .fc-divider, .fc-unthemed .fc-list-heading td, .fc-unthemed .fc-list-view, .fc-unthemed .fc-popover, .fc-unthemed .fc-row, .fc-unthemed tbody, .fc-unthemed td, .fc-unthemed th, .fc-unthemed thead {
6
+    border-color: #505050;
7
+}
8
+
9
+h2, table, .fc-toolbar .fc-center > * {
10
+	color: #ebebeb;
11
+}
12
+
13
+.fc-event-container {
14
+    color: #000;
15
+}
16
+
17
+.fc-event-container .fc-bg {
18
+    opacity: 0;
19
+}
20
+
21
+.fc-unthemed td.fc-today {
22
+	background-color: #444;
23
+}

+ 23
- 0
custom_css/planex/customDark2.css View File

@@ -0,0 +1,23 @@
1
+body {
2
+    background-color: #222;
3
+}
4
+
5
+.fc-unthemed .fc-content, .fc-unthemed .fc-divider, .fc-unthemed .fc-list-heading td, .fc-unthemed .fc-list-view, .fc-unthemed .fc-popover, .fc-unthemed .fc-row, .fc-unthemed tbody, .fc-unthemed td, .fc-unthemed th, .fc-unthemed thead {
6
+    border-color: #505050;
7
+}
8
+
9
+h2, table, .fc-toolbar .fc-center > * {
10
+	color: #ebebeb;
11
+}
12
+
13
+.fc-event-container {
14
+    color: #000;
15
+}
16
+
17
+.fc-event-container .fc-bg {
18
+    opacity: 0;
19
+}
20
+
21
+.fc-unthemed td.fc-today {
22
+	background-color: #444;
23
+}

+ 98
- 0
custom_css/planex/customMobile.css View File

@@ -0,0 +1,98 @@
1
+body > .container {
2
+    padding-top: 30px;
3
+}
4
+
5
+header {
6
+    display: none;
7
+}
8
+
9
+.fc-toolbar .fc-center {
10
+    width: 100%;
11
+}
12
+.fc-toolbar .fc-center > * {
13
+    float: none;
14
+    width: 100%;
15
+    margin: 0;
16
+}
17
+
18
+#rotateToLandscape {
19
+display: none;
20
+}
21
+
22
+#rotateToPortrait{
23
+display: block;
24
+}
25
+
26
+
27
+@media only screen and (max-width: 575px) {
28
+    body > .container {
29
+        padding-top: 10px;
30
+    }
31
+    
32
+    
33
+    #rotateToLandscape {
34
+    display: block;
35
+    }
36
+    
37
+    #rotateToPortrait{
38
+    display: none;
39
+    }
40
+    
41
+    #calendar .fc-view-container {
42
+    overflow: auto;
43
+    }
44
+    
45
+    #calendar .fc-agendaWeek-view {
46
+    width: 225%;
47
+    }
48
+    
49
+    #calendar .fc-month-view {
50
+    width: 250%;
51
+    }
52
+    
53
+    #entite {
54
+    margin-bottom: 5px !important;
55
+    }
56
+    
57
+    #entite,
58
+    #groupe,
59
+    #calendar .fc-left,
60
+    #calendar .fc-right {
61
+    width: calc(100% - 20px);
62
+    margin: 0 10px;
63
+    }
64
+    
65
+    #calendar .fc-right .fc-button-group {
66
+    margin: 0;
67
+    }
68
+    
69
+    #calendar .fc-button {
70
+    margin: 2px 0;
71
+    }
72
+    
73
+    #calendar .fc-left .fc-button-group {
74
+    float: right;
75
+    }
76
+    
77
+    #calendar .fc-left .fc-button-group .fc-button {
78
+    font-size: 1.2rem;
79
+    margin: 0 0 0 5px;
80
+    }
81
+    
82
+    #groupe_visibility {
83
+    width: 100%;
84
+    }
85
+    
86
+    #calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title {
87
+    font-size: 0.7rem
88
+    }
89
+    
90
+    .tooltiptopicevent h1 {
91
+        font-size: 1rem;
92
+    }
93
+    
94
+    .tooltiptopicevent {
95
+        border-radius: 0.2rem;
96
+        border: solid 1px #717171;
97
+    }
98
+}

+ 98
- 0
custom_css/planex/customMobile2.css View File

@@ -0,0 +1,98 @@
1
+body > .container {
2
+    padding-top: 30px;
3
+}
4
+
5
+header {
6
+    display: none;
7
+}
8
+
9
+.fc-toolbar .fc-center {
10
+    width: 100%;
11
+}
12
+.fc-toolbar .fc-center > * {
13
+    float: none;
14
+    width: 100%;
15
+    margin: 0;
16
+}
17
+
18
+#rotateToLandscape {
19
+display: none;
20
+}
21
+
22
+#rotateToPortrait{
23
+display: block;
24
+}
25
+
26
+
27
+@media only screen and (max-width: 575px) {
28
+    body > .container {
29
+        padding-top: 10px;
30
+    }
31
+    
32
+    
33
+    #rotateToLandscape {
34
+    display: block;
35
+    }
36
+    
37
+    #rotateToPortrait{
38
+    display: none;
39
+    }
40
+    
41
+    #calendar .fc-view-container {
42
+    overflow: auto;
43
+    }
44
+    
45
+    #calendar .fc-agendaWeek-view {
46
+    width: 225%;
47
+    }
48
+    
49
+    #calendar .fc-month-view {
50
+    width: 250%;
51
+    }
52
+    
53
+    #entite {
54
+    margin-bottom: 5px !important;
55
+    }
56
+    
57
+    #entite,
58
+    #groupe,
59
+    #calendar .fc-left,
60
+    #calendar .fc-right {
61
+    width: calc(100% - 20px);
62
+    margin: 0 10px;
63
+    }
64
+    
65
+    #calendar .fc-right .fc-button-group {
66
+    margin: 0;
67
+    }
68
+    
69
+    #calendar .fc-button {
70
+    margin: 2px 0;
71
+    }
72
+    
73
+    #calendar .fc-left .fc-button-group {
74
+    float: right;
75
+    }
76
+    
77
+    #calendar .fc-left .fc-button-group .fc-button {
78
+    font-size: 1.2rem;
79
+    margin: 0 0 0 5px;
80
+    }
81
+    
82
+    #groupe_visibility {
83
+    width: 100%;
84
+    }
85
+    
86
+    #calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title {
87
+    font-size: 0.7rem
88
+    }
89
+    
90
+    .tooltiptopicevent h1 {
91
+        font-size: 1rem;
92
+    }
93
+    
94
+    .tooltiptopicevent {
95
+        border-radius: 0.2rem;
96
+        border: solid 1px #717171;
97
+    }
98
+}

+ 181
- 0
custom_css/planex/customMobile3.css View File

@@ -0,0 +1,181 @@
1
+body > .container {
2
+
3
+    padding-top: 10px;
4
+
5
+}
6
+
7
+
8
+
9
+header {
10
+
11
+    display: none;
12
+
13
+}
14
+
15
+
16
+
17
+.fc-toolbar .fc-center {
18
+
19
+    width: 100%;
20
+
21
+}
22
+
23
+.fc-toolbar .fc-center > * {
24
+
25
+    float: none;
26
+
27
+    width: 100%;
28
+
29
+    margin: 0;
30
+
31
+}
32
+
33
+
34
+
35
+#entite {
36
+
37
+    margin-bottom: 5px !important;
38
+
39
+}
40
+
41
+
42
+
43
+#entite,
44
+
45
+#groupe,
46
+
47
+#calendar .fc-left,
48
+
49
+#calendar .fc-right {
50
+
51
+    width: calc(100% - 20px);
52
+
53
+    margin: 0 10px;
54
+
55
+}
56
+
57
+
58
+
59
+#calendar .fc-right {
60
+
61
+    display: flex;
62
+
63
+    margin-top: 5px;
64
+
65
+}
66
+
67
+
68
+
69
+#calendar .fc-right .fc-button-group {
70
+
71
+    margin: 0;
72
+
73
+    margin-left: auto;
74
+
75
+}
76
+
77
+
78
+
79
+#calendar .fc-button {
80
+
81
+    margin: 2px 0;
82
+
83
+}
84
+
85
+
86
+
87
+#calendar .fc-left .fc-button-group {
88
+
89
+    float: right;
90
+
91
+}
92
+
93
+
94
+
95
+#calendar .fc-left .fc-button-group .fc-button {
96
+
97
+    font-size: 1.2rem;
98
+
99
+    margin: 0 0 0 5px;
100
+
101
+}
102
+
103
+
104
+
105
+#groupe_visibility {
106
+
107
+    width: 100%;
108
+
109
+}
110
+
111
+
112
+
113
+#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-title {
114
+
115
+    font-size: 0.6rem
116
+
117
+}
118
+
119
+
120
+
121
+#calendar .fc-agendaWeek-view .fc-content-skeleton .fc-time {
122
+
123
+    font-size: 0.5rem
124
+
125
+}
126
+
127
+
128
+
129
+#calendar .fc-month-view .fc-content-skeleton .fc-title {
130
+
131
+    font-size: 0.6rem
132
+
133
+}
134
+
135
+
136
+
137
+#calendar .fc-month-view .fc-content-skeleton .fc-time {
138
+
139
+    font-size: 0.7rem
140
+
141
+}
142
+
143
+
144
+
145
+.fc-axis {
146
+
147
+    font-size: 0.8rem;
148
+
149
+    width: 15px !important;
150
+
151
+}
152
+
153
+
154
+
155
+.fc-day-header {
156
+
157
+    font-size: 0.8rem;
158
+
159
+}
160
+
161
+
162
+
163
+.tooltiptopicevent h1 {
164
+
165
+    font-size: 0.8rem;
166
+
167
+}
168
+
169
+
170
+
171
+.tooltiptopicevent {
172
+
173
+    border-radius: 0.2rem;
174
+
175
+    border: solid 1px #717171;
176
+
177
+}
178
+
179
+
180
+
181
+

+ 53
- 0
custom_css/rooms/customBibMobile.css View File

@@ -0,0 +1,53 @@
1
+.navbar,.hero-unit, footer {
2
+    display:none;
3
+}
4
+
5
+.hero-unit3, .hero-unit2, .hero-unit-form {
6
+    background-color: #fff;
7
+    box-shadow: none;
8
+    padding: 0;
9
+    margin: 0;
10
+}
11
+
12
+.hero-unit-form h4 {
13
+    font-size: 2rem;
14
+    line-height: 2rem;
15
+}
16
+
17
+.btn {
18
+    font-size: 1.5rem;
19
+    line-height: 1.5rem;
20
+    padding: 20px;
21
+}
22
+
23
+.btn-danger {
24
+    background-image: none;
25
+    background-color: #be1522;
26
+}
27
+
28
+.table {
29
+    font-size: 0.8rem;
30
+}
31
+
32
+.table td {
33
+    padding: 0;
34
+    height: 18.2333px;
35
+    border: none;
36
+    border-bottom: 1px solid #c1c1c1;
37
+}
38
+
39
+.table td[style="max-width:55px;"] {
40
+    max-width: 110px !important;
41
+}
42
+
43
+.table-bordered {
44
+    min-width: 50px;
45
+}
46
+
47
+th {
48
+    height: 50px;
49
+}
50
+
51
+.table-bordered {
52
+    border-collapse: collapse;
53
+}

+ 52
- 0
custom_css/rooms/customMobile.css View File

@@ -0,0 +1,52 @@
1
+body, body > .container2 {
2
+    padding-top: 0;
3
+    width: 100%;
4
+    overflow-y: hidden;
5
+}
6
+
7
+tbody tr, header, b, br, body > .container2 > h3, body > .container2 > h1 {
8
+    display: none;
9
+}
10
+
11
+.table-bordered td, .table-bordered th {
12
+    border: none;
13
+    border-right: 1px solid #dee2e6;
14
+    border-bottom: 1px solid #dee2e6;
15
+}
16
+
17
+.table {
18
+    padding: 0;
19
+    margin: 0;
20
+    width: 200%;
21
+    max-width: 200%;
22
+    display: block;
23
+}
24
+
25
+tbody {
26
+    display: block;
27
+    overflow: auto;
28
+    width: 100%;
29
+    height: calc(100vh - 50px);
30
+}
31
+
32
+thead {
33
+    display: block;
34
+    width: 100%;
35
+}
36
+
37
+tbody tr[bgcolor], thead tr {
38
+    width: 100%;
39
+    display: inline-flex;
40
+}
41
+
42
+.table tbody td, .table thead td[colspan] {
43
+    padding: 0;
44
+    flex: 1;
45
+    height: 50px;
46
+    margin: 0;
47
+}
48
+
49
+.table tbody td[bgcolor="white"], .table thead td {
50
+    flex: 0 0 150px;
51
+    height: 50px;
52
+}

+ 1
- 0
dashboard/dashboard_data.json
File diff suppressed because it is too large
View File


+ 0
- 0
dashboard/err View File


+ 142
- 0
dashboard/handler.py View File

@@ -0,0 +1,142 @@
1
+import json
2
+from datetime import date
3
+import urllib.request
4
+import os.path
5
+
6
+WASHINSA_FILE = '../washinsa/washinsa.json'
7
+MENU_FILE = '../menu/menu_data.json'
8
+FACEBOOK_FILE = '../facebook/facebook_data.json'
9
+FACEBOOK_LOCK = '../facebook/lock'
10
+PROXIMO_URL = 'https://etud.insa-toulouse.fr/~proximo/data/stock-v2.json'
11
+TUTORINSA_URL = 'https://etud.insa-toulouse.fr/~tutorinsa/api/get_data.php'
12
+PLANNING_URL = 'https://amicale-insat.fr/event/json/list'
13
+
14
+DASHBOARD_FILE = 'dashboard_data.json'
15
+
16
+
17
+def get_available_machines():
18
+    """
19
+    Get the number of available washing/drying machines
20
+
21
+    :return: a tuple containing the number of available dryers and washers
22
+    """
23
+    with open(WASHINSA_FILE) as f:
24
+        data = json.load(f)
25
+        available_dryers = 0
26
+        available_washers = 0
27
+        for machine in data['dryers']:
28
+            if machine['state'] == 'DISPONIBLE':
29
+                available_dryers += 1
30
+        for machine in data['washers']:
31
+            if machine['state'] == 'DISPONIBLE':
32
+                available_washers += 1
33
+    return available_dryers, available_washers
34
+
35
+
36
+def get_today_menu():
37
+    """
38
+    Check if the menu for the current day is available
39
+
40
+    :return: a list containing today's menu
41
+    """
42
+    with open(MENU_FILE) as f:
43
+        data = json.load(f)
44
+        menu = []
45
+        for i in range(0, len(data)):
46
+            if data[i]['date'] == date.today().strftime('%Y-%m-%d'):
47
+                menu = data[i]['meal'][0]['foodcategory']
48
+                break
49
+        return menu
50
+
51
+
52
+def get_proximo_article_number():
53
+    """
54
+    Get the number of articles on sale at proximo
55
+
56
+    :return: an integer representing the number of articles
57
+    """
58
+    with urllib.request.urlopen(PROXIMO_URL) as response:
59
+        data = json.loads(response.read().decode())
60
+        count = 0
61
+        for article in data['articles']:
62
+            if int(article['quantity']) > 0:
63
+                count += 1
64
+        return count
65
+
66
+
67
+def get_tutorinsa_tutoring_number():
68
+    """
69
+    Get the number of tutoring classes available
70
+
71
+    :return: an integer representing the number of tutoring classes
72
+    """
73
+    try:
74
+        with urllib.request.urlopen(TUTORINSA_URL) as response:
75
+            data = json.loads(response.read().decode())
76
+            return int(data['tutorials_number'])
77
+    except:
78
+        return 0
79
+
80
+
81
+def get_today_events():
82
+    """
83
+    Get today's events
84
+
85
+    :return: an array containing today's events
86
+    """
87
+    with urllib.request.urlopen(PLANNING_URL) as response:
88
+        data = json.loads(response.read().decode())
89
+        today_events = []
90
+        for event in data:
91
+            if event['date_begin'].split(' ')[0] == date.today().strftime('%Y-%m-%d'):
92
+                today_events.append(event)
93
+        return today_events
94
+
95
+
96
+def get_news_feed():
97
+    """
98
+    Get facebook news and truncate the data to 15 entries for faster loading
99
+
100
+    :return: an object containing the facebook feed data
101
+    """
102
+    # Prevent concurrent access to file
103
+    while os.path.isfile(FACEBOOK_LOCK):
104
+        print("Waiting for lock")
105
+    with open(FACEBOOK_FILE) as f:
106
+        data = json.load(f)
107
+        if 'data' in data and len(data['data']) > 15:
108
+            del data['data'][14:]
109
+        else:
110
+            data = {'data': []}
111
+        return data
112
+
113
+
114
+def generate_dashboard_json():
115
+    """
116
+    Generate the actual dashboard
117
+
118
+    :return: an object containing the dashboard's data
119
+    """
120
+    available_machines = get_available_machines()
121
+    available_tutorials = get_tutorinsa_tutoring_number()
122
+    return {
123
+        'dashboard': {
124
+            'today_events': get_today_events(),
125
+            'available_machines': {
126
+                'dryers': available_machines[0],
127
+                'washers': available_machines[1]
128
+            },
129
+            'available_tutorials': available_tutorials,
130
+            'today_menu': get_today_menu(),
131
+            'proximo_articles': get_proximo_article_number()
132
+        },
133
+        'news_feed': get_news_feed()
134
+    }
135
+
136
+
137
+def main():
138
+    with open(DASHBOARD_FILE, 'w') as f:
139
+        json.dump(generate_dashboard_json(), f)
140
+
141
+
142
+main()

+ 162931
- 0
dashboard/log
File diff suppressed because it is too large
View File


+ 120
- 0
expo_notifications/dao.php View File

@@ -0,0 +1,120 @@
1
+<?php
2
+
3
+class Dao
4
+{
5
+    private $conn;
6
+    private $debug = false;
7
+
8
+    private function get_debug_mode()
9
+    {
10
+        $this->debug = file_exists(__DIR__ . DIRECTORY_SEPARATOR . "DEBUG");
11
+    }
12
+
13
+
14
+    public function __construct()
15
+    {
16
+        $this->get_debug_mode();
17
+        if ($this->debug) {
18
+            $username = 'test';
19
+            $password = $this->read_password();;
20
+            $dsn = 'mysql:dbname=test;host=127.0.0.1';
21
+        } else {
22
+            $username = 'amicale_app';
23
+            $password = $this->read_password();
24
+            $dsn = 'mysql:dbname=amicale_app;host=127.0.0.1';
25
+        }
26
+        try {
27
+            $this->conn = new PDO($dsn, $username, $password, [PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8']);
28
+        } catch (PDOException $e) {
29
+            echo $e;
30
+        }
31
+    }
32
+
33
+    private function read_password()
34
+    {
35
+        if ($this->debug)
36
+            $real_path = __DIR__ . DIRECTORY_SEPARATOR . ".htpassdb_debug";
37
+        else
38
+            $real_path = __DIR__ . DIRECTORY_SEPARATOR . ".htpassdb";
39
+        $file = fopen($real_path, "r") or die("Unable to open DB password file!");;
40
+        $password = fgets($file);
41
+        fclose($file);
42
+        return trim($password);
43
+    }
44
+
45
+    /**
46
+     * Return the list of machines watched by the user associated by the given token
47
+     *
48
+     * @param $token
49
+     * @return array
50
+     */
51
+    public function get_machine_watchlist($token) {
52
+        $this->register_user($token);
53
+        $sql = "SELECT machine_id FROM machine_watchlist WHERE user_token=:token";
54
+        $cursor = $this->conn->prepare($sql); // Protect against SQL injections
55
+        $cursor->bindParam(':token', $token);
56
+        $cursor->execute();
57
+        $result = $cursor->fetchAll();
58
+        $finalArray = [];
59
+        foreach ($result  as $row) {
60
+            array_push($finalArray, $row["machine_id"]);
61
+        }
62
+        return $finalArray;
63
+    }
64
+
65
+
66
+    public function set_machine_reminder($token, $time) {
67
+        $this->register_user($token);
68
+        $sql = "UPDATE users SET machine_reminder_time=:time WHERE token=:token";
69
+        $cursor = $this->conn->prepare($sql); // Protect against SQL injections
70
+        $cursor->bindParam(':token', $token);
71
+        $cursor->bindParam(':time', $time);
72
+        var_dump($cursor->execute());
73
+    }
74
+
75
+
76
+    /**
77
+     * Add/Remove a machine from the database for the specified token.
78
+     *
79
+     * @param $token
80
+     * @param $machine_id
81
+     * @param $should_add
82
+     */
83
+    public function update_machine_end_token($token, $machine_id, $should_add, $locale)
84
+    {
85
+        $this->register_user($token);
86
+        $this->update_user_locale($token, $locale);
87
+        if ($should_add)
88
+            $sql = "INSERT INTO machine_watchlist (machine_id, user_token) VALUES (:id, :token)";
89
+        else
90
+            $sql = "DELETE FROM machine_watchlist WHERE machine_id=:id AND user_token=:token";
91
+        $cursor = $this->conn->prepare($sql); // Protect against SQL injections
92
+        $cursor->bindParam(':id', $machine_id);
93
+        $cursor->bindParam(':token', $token);
94
+        $cursor->execute();
95
+    }
96
+
97
+
98
+    /**
99
+     * Register user in the database if not already in it
100
+     * @param $userToken
101
+     * @param $locale
102
+     */
103
+    private function register_user($userToken) {
104
+        $sql = "INSERT INTO users (token) VALUES (:token)";
105
+        $cursor = $this->conn->prepare($sql); // Protect against SQL injections
106
+        $cursor->bindParam(':token', $userToken);
107
+        $cursor->execute();
108
+    }
109
+
110
+    private function update_user_locale($token, $locale) {
111
+        $sql = "UPDATE users SET locale=:locale WHERE token=:token";
112
+        $cursor = $this->conn->prepare($sql); // Protect against SQL injections
113
+        $cursor->bindParam(':token', $token);
114
+        $cursor->bindParam(':locale', $locale);
115
+        $cursor->execute();
116
+    }
117
+
118
+}
119
+
120
+

+ 10
- 0
expo_notifications/en.json View File

@@ -0,0 +1,10 @@
1
+{
2
+  "endNotification": {
3
+    "title": "Machine n°%s finished",
4
+    "body": "Come get your laundry in machine n°%s"
5
+  },
6
+  "reminderNotification": {
7
+    "title": "Machine n°%s running...",
8
+    "body": "Don't forget your laundry in machine n°%s"
9
+  }
10
+}

+ 319
- 0
expo_notifications/exponent_server_sdk/__init__.py View File

@@ -0,0 +1,319 @@
1
+from collections import namedtuple
2
+import json
3
+
4
+
5
+class PushResponseError(Exception):
6
+    """Base class for all push reponse errors"""
7
+
8
+    def __init__(self, push_response):
9
+        if push_response.message:
10
+            self.message = push_response.message
11
+        else:
12
+            self.message = 'Unknown push response error'
13
+        super(PushResponseError, self).__init__(self.message)
14
+
15
+        self.push_response = push_response
16
+
17
+
18
+class DeviceNotRegisteredError(PushResponseError):
19
+    """Raised when the push token is invalid
20
+
21
+    To handle this error, you should stop sending messages to this token.
22
+    """
23
+    pass
24
+
25
+
26
+class MessageTooBigError(PushResponseError):
27
+    """Raised when the notification was too large.
28
+
29
+    On Android and iOS, the total payload must be at most 4096 bytes.
30
+    """
31
+    pass
32
+
33
+
34
+class MessageRateExceededError(PushResponseError):
35
+    """Raised when you are sending messages too frequently to a device
36
+
37
+    You should implement exponential backoff and slowly retry sending messages.
38
+    """
39
+    pass
40
+
41
+
42
+class PushServerError(Exception):
43
+    """Raised when the push token server is not behaving as expected
44
+
45
+    For example, invalid push notification arguments result in a different
46
+    style of error. Instead of a "data" array containing errors per
47
+    notification, an "error" array is returned.
48
+
49
+    {"errors": [
50
+      {"code": "API_ERROR",
51
+       "message": "child \"to\" fails because [\"to\" must be a string]. \"value\" must be an array."
52
+      }
53
+    ]}
54
+    """
55
+
56
+    def __init__(self, message, response, response_data=None, errors=None):
57
+        self.message = message
58
+        self.response = response
59
+        self.response_data = response_data
60
+        self.errors = errors
61
+        super(PushServerError, self).__init__(self.message)
62
+
63
+
64
+class PushMessage(namedtuple('PushMessage', [
65
+        'to', 'data', 'title', 'body', 'sound', 'ttl', 'expiration',
66
+        'priority', 'badge', 'channel_id'])):
67
+    """An object that describes a push notification request.
68
+
69
+    You can override this class to provide your own custom validation before
70
+    sending these to the Exponent push servers. You can also override the
71
+    get_payload function itself to take advantage of any hidden or new
72
+    arguments before this library updates upstream.
73
+
74
+        Args:
75
+            to: A token of the form ExponentPushToken[xxxxxxx]
76
+            data: A dict of extra data to pass inside of the push notification.
77
+                The total notification payload must be at most 4096 bytes.
78
+            title: The title to display in the notification. On iOS, this is
79
+                displayed only on Apple Watch.
80
+            body: The message to display in the notification.
81
+            sound: A sound to play when the recipient receives this
82
+                notification. Specify "default" to play the device's default
83
+                notification sound, or omit this field to play no sound.
84
+            ttl: The number of seconds for which the message may be kept around
85
+                for redelivery if it hasn't been delivered yet. Defaults to 0.
86
+            expiration: UNIX timestamp for when this message expires. It has
87
+                the same effect as ttl, and is just an absolute timestamp
88
+                instead of a relative one.
89
+            priority: Delivery priority of the message. 'default', 'normal',
90
+                and 'high' are the only valid values.
91
+            badge: An integer representing the unread notification count. This
92
+                currently only affects iOS. Specify 0 to clear the badge count.
93
+            channel_id: ID of the Notification Channel through which to display
94
+                this notification on Android devices.
95
+
96
+    """
97
+    def get_payload(self):
98
+        # Sanity check for invalid push token format.
99
+        if not PushClient.is_exponent_push_token(self.to):
100
+            raise ValueError('Invalid push token')
101
+
102
+        # There is only one required field.
103
+        payload = {
104
+            'to': self.to,
105
+        }
106
+
107
+        # All of these fields are optional.
108
+        if self.data is not None:
109
+            payload['data'] = self.data
110
+        if self.title is not None:
111
+            payload['title'] = self.title
112
+        if self.body is not None:
113
+            payload['body'] = self.body
114
+        if self.sound is not None:
115
+            payload['sound'] = self.sound
116
+        if self.ttl is not None:
117
+            payload['ttl'] = self.ttl
118
+        if self.expiration is not None:
119
+            payload['expiration'] = self.expiration
120
+        if self.priority is not None:
121
+            payload['priority'] = self.priority
122
+        if self.badge is not None:
123
+            payload['badge'] = self.badge
124
+        if self.channel_id is not None:
125
+            payload['channelId'] = self.channel_id
126
+        return payload
127
+
128
+
129
+# Allow optional arguments for PushMessages since everything but the `to` field
130
+# is optional. Unfortunately namedtuples don't allow for an easy way to create
131
+# a required argument at the contructor level right now.
132
+PushMessage.__new__.__defaults__ = (None,) * len(PushMessage._fields)
133
+
134
+
135
+class PushResponse(namedtuple('PushResponse', [
136
+        'push_message', 'status', 'message', 'details'])):
137
+    """Wrapper class for a push notification response.
138
+
139
+    A successful single push notification:
140
+        {'status': 'ok'}
141
+
142
+    An invalid push token
143
+        {'status': 'error',
144
+         'message': '"adsf" is not a registered push notification recipient'}
145
+    """
146
+    # Known status codes
147
+    ERROR_STATUS = 'error'
148
+    SUCCESS_STATUS = 'ok'
149
+
150
+    # Known error strings
151
+    ERROR_DEVICE_NOT_REGISTERED = 'DeviceNotRegistered'
152
+    ERROR_MESSAGE_TOO_BIG = 'MessageTooBig'
153
+    ERROR_MESSAGE_RATE_EXCEEDED = 'MessageRateExceeded'
154
+
155
+    def is_success(self):
156
+        """Returns True if this push notification successfully sent."""
157
+        return self.status == PushResponse.SUCCESS_STATUS
158
+
159
+    def validate_response(self):
160
+        """Raises an exception if there was an error. Otherwise, do nothing.
161
+
162
+        Clients should handle these errors, since these require custom handling
163
+        to properly resolve.
164
+        """
165
+        if self.is_success():
166
+            return
167
+
168
+        # Handle the error if we have any information
169
+        if self.details:
170
+            error = self.details.get('error', None)
171
+
172
+            if error == PushResponse.ERROR_DEVICE_NOT_REGISTERED:
173
+                raise DeviceNotRegisteredError(self)
174
+            elif error == PushResponse.ERROR_MESSAGE_TOO_BIG:
175
+                raise MessageTooBigError(self)
176
+            elif error == PushResponse.ERROR_MESSAGE_RATE_EXCEEDED:
177
+                raise MessageRateExceededError(self)
178
+
179
+        # No known error information, so let's raise a generic error.
180
+        raise PushResponseError(self)
181
+
182
+
183
+class PushClient(object):
184
+    """Exponent push client
185
+
186
+    See full API docs at https://docs.expo.io/versions/latest/guides/push-notifications.html#http2-api
187
+    """
188
+    DEFAULT_HOST = "https://exp.host"
189
+    DEFAULT_BASE_API_URL = "/--/api/v2"
190
+
191
+    def __init__(self, host=None, api_url=None):
192
+        """Construct a new PushClient object.
193
+
194
+        Args:
195
+            host: The server protocol, hostname, and port.
196
+            api_url: The api url at the host.
197
+        """
198
+        self.host = host
199
+        if not self.host:
200
+            self.host = PushClient.DEFAULT_HOST
201
+
202
+        self.api_url = api_url
203
+        if not self.api_url:
204
+            self.api_url = PushClient.DEFAULT_BASE_API_URL
205
+
206
+    @classmethod
207
+    def is_exponent_push_token(cls, token):
208
+        """Returns `True` if the token is an Exponent push token"""
209
+        import six
210
+
211
+        return (
212
+            isinstance(token, six.string_types) and
213
+            token.startswith('ExponentPushToken'))
214
+
215
+    def _publish_internal(self, push_messages):
216
+        """Send push notifications
217
+
218
+        The server will validate any type of syntax errors and the client will
219
+        raise the proper exceptions for the user to handle.
220
+
221
+        Each notification is of the form:
222
+        {
223
+          'to': 'ExponentPushToken[xxx]',
224
+          'body': 'This text gets display in the notification',
225
+          'badge': 1,
226
+          'data': {'any': 'json object'},
227
+        }
228
+
229
+        Args:
230
+            push_messages: An array of PushMessage objects.
231
+        """
232
+        # Delayed import because this file is immediately read on install, and
233
+        # the requests library may not be installed yet.
234
+        import requests
235
+
236
+        response = requests.post(
237
+            self.host + self.api_url + '/push/send',
238
+            data=json.dumps([pm.get_payload() for pm in push_messages]),
239
+            headers={
240
+                'accept': 'application/json',
241
+                'accept-encoding': 'gzip, deflate',
242
+                'content-type': 'application/json',
243
+            }
244
+        )
245
+
246
+        # Let's validate the response format first.
247
+        try:
248
+            response_data = response.json()
249
+        except ValueError:
250
+            # The response isn't json. First, let's attempt to raise a normal
251
+            # http error. If it's a 200, then we'll raise our own error.
252
+            response.raise_for_status()
253
+
254
+            raise PushServerError('Invalid server response', response)
255
+
256
+        # If there are errors with the entire request, raise an error now.
257
+        if 'errors' in response_data:
258
+            raise PushServerError(
259
+                'Request failed',
260
+                response,
261
+                response_data=response_data,
262
+                errors=response_data['errors'])
263
+
264
+        # We expect the response to have a 'data' field with the responses.
265
+        if 'data' not in response_data:
266
+            raise PushServerError(
267
+                'Invalid server response',
268
+                response,
269
+                response_data=response_data)
270
+
271
+        # Use the requests library's built-in exceptions for any remaining 4xx
272
+        # and 5xx errors.
273
+        response.raise_for_status()
274
+
275
+        # Sanity check the response
276
+        if len(push_messages) != len(response_data['data']):
277
+            raise PushServerError(
278
+                ('Mismatched response length. Expected %d %s but only '
279
+                 'received %d' % (
280
+                     len(push_messages),
281
+                     'receipt' if len(push_messages) == 1 else 'receipts',
282
+                     len(response_data['data']))),
283
+                response,
284
+                response_data=response_data)
285
+
286
+        # At this point, we know it's a 200 and the response format is correct.
287
+        # Now let's parse the responses per push notification.
288
+        receipts = []
289
+        for i, receipt in enumerate(response_data['data']):
290
+            receipts.append(PushResponse(
291
+                push_message=push_messages[i],
292
+                # If there is no status, assume error.
293
+                status=receipt.get('status', PushResponse.ERROR_STATUS),
294
+                message=receipt.get('message', ''),
295
+                details=receipt.get('details', None)))
296
+
297
+        return receipts
298
+
299
+    def publish(self, push_message):
300
+        """Sends a single push notification
301
+
302
+        Args:
303
+            push_message: A single PushMessage object.
304
+
305
+        Returns:
306
+           A PushResponse object which contains the results.
307
+        """
308
+        return self.publish_multiple([push_message])[0]
309
+
310
+    def publish_multiple(self, push_messages):
311
+        """Sends multiple push notifications at once
312
+
313
+        Args:
314
+            push_messages: An array of PushMessage objects.
315
+
316
+        Returns:
317
+           An array of PushResponse objects which contains the results.
318
+        """
319
+        return self._publish_internal(push_messages)

+ 10
- 0
expo_notifications/fr.json View File

@@ -0,0 +1,10 @@
1
+{
2
+  "endNotification": {
3
+    "title": "Machine n°%s terminée",
4
+    "body": "Vous pouvez venir chercher votre linge dans la machine n°%s"
5
+  },
6
+  "reminderNotification": {
7
+    "title": "Machine n°%s en cours...",
8
+    "body": "N'oubliez pas votre linge dans la machine n°%s"
9
+  }
10
+}

+ 163
- 0
expo_notifications/handler.py View File

@@ -0,0 +1,163 @@
1
+from exponent_server_sdk import PushClient
2
+from exponent_server_sdk import PushMessage
3
+import mysql.connector  # using lib from https://github.com/expo/expo-server-sdk-python
4
+import json
5
+from enum import Enum
6
+
7
+isDebug = False
8
+
9
+
10
+class Priority(Enum):
11
+    DEFAULT = 'default'
12
+    NORMAL = 'normal'
13
+    HIGH = 'high'
14
+
15
+
16
+class ChannelIDs(Enum):
17
+    REMINDERS = 'reminders'
18
+
19
+
20
+class MachineStates(Enum):
21
+    FINISHED = 'TERMINE'
22
+    READY = 'DISPONIBLE'
23
+    RUNNING = 'EN COURS'
24
+    BROKEN = 'HS'
25
+    ERROR = 'ERROR'
26
+
27
+
28
+if isDebug:
29
+    washinsaFile = 'data.json'
30
+    db = mysql.connector.connect(
31
+        host="127.0.0.1",
32
+        user="test",
33
+        passwd="coucou",
34
+        database="test"
35
+    )
36
+else:
37
+    washinsaFile = '../washinsa/washinsa.json'
38
+    db = mysql.connector.connect(
39
+        host="127.0.0.1",
40
+        user="amicale_app",
41
+        passwd="EYiDCalfNj",
42
+        database="amicale_app"
43
+    )
44
+
45
+
46
+def send_push_message(token, title, body, channel_id, extra=None):
47
+    prio = Priority.HIGH.value if channel_id == ChannelIDs.REMINDERS.value else Priority.NORMAL.value
48
+    print(prio)
49
+    response = PushClient().publish(
50
+        PushMessage(to=token,
51
+                    title=title,
52
+                    body=body,
53
+                    data=extra,
54
+                    sound='default',
55
+                    priority=prio))
56
+
57
+
58
+def get_machines_of_state(state):
59
+    machines = []
60
+    with open(washinsaFile) as f:
61
+        data = json.load(f)
62
+        for d in data['dryers']:
63
+            if d['state'] == state:
64
+                machines.append(d['number'])
65
+        for d in data['washers']:
66
+            if d['state'] == state:
67
+                machines.append(d['number'])
68
+    return machines
69
+
70
+
71
+def get_machine_remaining_time(machine_id):
72
+    with open(washinsaFile) as f:
73
+        data = json.load(f)
74
+        for d in data['dryers']:
75
+            if d['number'] == machine_id:
76
+                return int(d['remainingTime'])
77
+        for d in data['washers']:
78
+            if d['number'] == machine_id:
79
+                return int(d['remainingTime'])
80
+    return 0
81
+
82
+
83
+def send_end_notifications():
84
+    cursor = db.cursor()
85
+    machines = get_machines_of_state(MachineStates.FINISHED.value)
86
+    for machine_id in machines:
87
+        cursor.execute('SELECT * FROM machine_watchlist WHERE machine_id=%s', (machine_id,))
88
+        result = cursor.fetchall()
89
+        for r in result:
90
+            token = r[2]
91
+            translation = get_notification_translation(token, False)
92
+            body = translation["body"].replace("%s", machine_id, 1)
93
+            title = translation["title"].replace("%s", machine_id, 1)
94
+            # Remove from db
95
+            cursor.execute('DELETE FROM machine_watchlist WHERE machine_id=%s AND user_token=%s', (machine_id, token,))
96
+            db.commit()
97
+            send_push_message(token, title, body, ChannelIDs.REMINDERS.value)
98
+
99
+
100
+def send_reminder_notifications():
101
+    cursor = db.cursor()
102
+    machines = get_machines_of_state(MachineStates.RUNNING.value)
103
+    print(machines)
104
+    for machine_id in machines:
105
+        remaining_time = get_machine_remaining_time(machine_id)
106
+        print(remaining_time)
107
+        cursor.execute('SELECT * FROM machine_watchlist WHERE machine_id=%s', (machine_id,))
108
+        result = cursor.fetchall()
109
+        print(result)
110
+        for r in result:
111
+            if r[3] == 0:  # We did not send a reminder notification yet
112
+                token = r[2]
113
+                user_reminder_time = get_user_reminder_time(token)
114
+                if user_reminder_time >= remaining_time:
115
+                    translation = get_notification_translation(token, True)
116
+                    body = translation["body"].replace("%s", machine_id, 1)
117
+                    title = translation["title"].replace("%s", machine_id, 1)
118
+                    cursor.execute(
119
+                        'UPDATE machine_watchlist SET reminder_sent=%s WHERE machine_id=%s AND user_token=%s',
120
+                        (1, machine_id, token,))
121
+                    db.commit()
122
+                    send_push_message(token, title, body, ChannelIDs.REMINDERS.value)
123
+
124
+
125
+def get_user_reminder_time(token):
126
+    cursor = db.cursor()
127
+    cursor.execute('SELECT machine_reminder_time FROM users WHERE token=%s', (token,))
128
+    result = cursor.fetchall()
129
+    print(result[0][0])
130
+    return result[0][0]
131
+
132
+
133
+def get_user_locale(token):
134
+    cursor = db.cursor()
135
+    cursor.execute('SELECT locale FROM users WHERE token=%s', (token,))
136
+    result = cursor.fetchall()
137
+    print(result[0][0])
138
+    locale = 'en'
139
+    if "fr" in result[0][0]:
140
+        locale = 'fr'
141
+    return locale
142
+
143
+
144
+def get_notification_translation(token, is_reminder):
145
+    locale = get_user_locale(token)
146
+    file_name = locale + '.json'
147
+    print(file_name)
148
+    with open(file_name) as f:
149
+        data = json.load(f)
150
+        if is_reminder:
151
+            translation = data["reminderNotification"]
152
+        else:
153
+            translation = data["endNotification"]
154
+    print(translation)
155
+    return translation
156
+
157
+
158
+def main():
159
+    send_reminder_notifications()
160
+    send_end_notifications()
161
+
162
+
163
+main()

+ 52
- 0
expo_notifications/save_token.php View File

@@ -0,0 +1,52 @@
1
+<?php
2
+require_once 'dao.php';
3
+
4
+$password = "df1g3f1ghdf54qds3f879";
5
+
6
+$rest_json = file_get_contents("php://input");
7
+$_POST = json_decode($rest_json, true);
8
+
9
+if (!isset($_POST['password']) || isset($_POST['password']) != $password)
10
+    die("Access denied");
11
+
12
+
13
+if (isset($_POST['function'])) {
14
+    if ($_POST['function'] == "setup_machine_notification")
15
+        setup_machine_notification();
16
+    elseif ($_POST['function'] == "get_machine_watchlist")
17
+        get_machine_watchlist();
18
+    elseif ($_POST['function'] == "set_machine_reminder")
19
+        set_machine_reminder();
20
+} else
21
+    show_error();
22
+
23
+function setup_machine_notification() {
24
+    $token = $_POST['token'];
25
+    $enabled = boolval($_POST['enabled']);
26
+    $machineId = intval($_POST['machine_id']);
27
+    $locale = $_POST['locale'];
28
+
29
+    $dao = new Dao();
30
+    $dao->update_machine_end_token($token, $machineId, $enabled, $locale);
31
+}
32
+
33
+function get_machine_watchlist() {
34
+    $token = $_POST['token'];
35
+
36
+    $dao = new Dao();
37
+    echo json_encode($dao->get_machine_watchlist($token));
38
+}
39
+
40
+function set_machine_reminder() {
41
+    $token = $_POST['token'];
42
+    $time = intval($_POST['time']);
43
+
44
+    $dao = new Dao();
45
+    $dao->set_machine_reminder($token, $time);
46
+}
47
+
48
+
49
+function show_error() {
50
+    echo "Échec :\n";
51
+    var_dump($_POST);
52
+}

+ 1
- 0
facebook/facebook_data.json
File diff suppressed because it is too large
View File


+ 6
- 0
facebook/facebook_update.sh View File

@@ -0,0 +1,6 @@
1
+#/bin/bash!
2
+
3
+touch lock
4
+#curl "https://graph.facebook.com/v3.3/amicale.deseleves/posts?fields=message%2Cfull_picture%2Ccreated_time%2Cpermalink_url&&date_format=U&access_token=EAAGliUs4Ei8BAGwHmg7SNnosoEDMuDhP3i5lYOGrIGzZBNeMeGzGhpUigJt167cKXEIM0GiurSgaC0PS4Xg2GBzOVNiZCfr8u48VVB15a9YbOsuhjBqhHAMb2sz6ibwOuDhHSvwRZCUpBZCjmAW12e7RjWJp0jvyNoYYvIQbfaLWi3Nk2mBc" > facebook_data.json
5
+curl "https://graph.facebook.com/v6.0/amicale.deseleves/published_posts?fields=full_picture,message,permalink_url,created_time&date_format=U&access_token=EAAGliUs4Ei8BAGwHmg7SNnosoEDMuDhP3i5lYOGrIGzZBNeMeGzGhpUigJt167cKXEIM0GiurSgaC0PS4Xg2GBzOVNiZCfr8u48VVB15a9YbOsuhjBqhHAMb2sz6ibwOuDhHSvwRZCUpBZCjmAW12e7RjWJp0jvyNoYYvIQbfaLWi3Nk2mBc" > facebook_data.json
6
+rm lock

+ 13
- 0
update_washinsa.sh View File

@@ -0,0 +1,13 @@
1
+#/bin/bash!
2
+
3
+cd $HOME/public_html/washinsa
4
+# update washing machine list
5
+php index.php
6
+
7
+cd ../expo_notifications
8
+# watch for new notifications with the new list
9
+python3 handler.py
10
+
11
+cd ../dashboard
12
+# Update the dashboard
13
+python3 handler.py > log 2> err 

Loading…
Cancel
Save