Skip to content

Commit 310ad99

Browse files
authored
Merge pull request #9 from bb-Ricardo/development
v1.1.0 changes
2 parents 374fc07 + c3e6e70 commit 310ad99

15 files changed

Lines changed: 747 additions & 5 deletions

File tree

README.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,82 @@ All options are described in the example file.
139139
After starting the API the first time it will add additional fields to each event.
140140
Such as a choice for the hosting Kennel or amount of Hash Cash.
141141

142+
## Listmonk support to post event
143+
It is possible to post an event directly to via [Listmonk](https://github.com/knadh/listmonk) to a mailing list.
144+
145+
### Requirements
146+
* a running listmonk instance
147+
* WordPress Advanced Custom Fields plugin
148+
* Wordpress WPCode Plugin
149+
150+
First both WordPress plugins should be installed.
151+
152+
Using the `Advanced Custom Fields` we add a new Field Group `Run Announcement`. There we add a new field `Mailing List`.
153+
* Type: Message
154+
* Label: Mailing List
155+
* Name: mailing_list
156+
* Message:
157+
```
158+
<div>
159+
<button
160+
id="send-to-mailing-list-button"
161+
type="button"
162+
class="button-secondary send-to-mailing-list-button"
163+
id="refresh-cache">Send/Update Event to Mailing List
164+
</button>
165+
</div>
166+
```
167+
168+
In the bottom we can find `Settings`.
169+
* Rules:
170+
1. Post Typ
171+
2. is equal to
172+
3. Event
173+
* Representation:
174+
1. Style: Standard
175+
2. Position: Side
176+
3. Label Placement: Top
177+
4. Instruction placement: Below
178+
5. Order No: 2
179+
180+
* Now press `Save Changes`
181+
182+
After that we switch to the `WPCode` plugin and a Javascript function to this button we just created.
183+
Here we add a new `Send Mailinglist` code snippet of type `PHP Snipet` with this content:
184+
```php
185+
/* Inline script printed out in the header */
186+
add_action('admin_footer', 'tutsplus_add_script_wp_head');
187+
function tutsplus_add_script_wp_head() {
188+
?>
189+
<script id="updateMailingList" type="text/javascript">
190+
document.querySelector( '.send-to-mailing-list-button' ).addEventListener( 'click', function( e ) {
191+
var xhttp = new XMLHttpRequest();
192+
var params = {
193+
user: "<?php echo get_current_user_id(); ?>",
194+
token: "<?php echo wp_get_session_token(); ?>"
195+
}
196+
197+
xhttp.onreadystatechange = function() {
198+
if (this.readyState == 4 && this.status == 200) {
199+
console.log(this.responseText);
200+
}
201+
};
202+
203+
xhttp.open("POST", "/api/v1/send-newsletter/<?php echo get_the_ID(); ?>" , true);
204+
xhttp.setRequestHeader('Content-type', 'application/json')
205+
206+
xhttp.send(JSON.stringify(params));
207+
xhttp.onload = function() {
208+
// Do whatever with response
209+
alert("Mailing List request status: " + xhttp.responseText)
210+
}
211+
212+
} );
213+
</script>
214+
<?php
215+
}
216+
```
217+
With Listmonk running on the same hos, sending an event via Mailing list is just a button press away.
142218

143219
## License
144220
>You can check out the full license [here](LICENSE.txt)

api/factory/runs.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,9 @@ def get_hash_runs(params: HashParams) -> List[Hash]:
137137
log.error(f"DB query should return a list, got {type(posts)}")
138138
return return_list
139139

140+
if len(posts) == 0:
141+
return return_list
142+
140143
post_meta = conn.get_posts_meta(post_ids)
141144
event_manager_form_fields = php_deserialize(conn.get_config_item("event_manager_submit_event_form_fields"))
142145

@@ -173,6 +176,7 @@ def get_hash_runs(params: HashParams) -> List[Hash]:
173176
"geo_lat": post_attr.get("geolocation_lat"),
174177
"geo_long": post_attr.get("geolocation_long"),
175178
"geo_location_name": post_attr.get("geolocation_formatted_address"),
179+
"geo_map_url": post_attr.get("_hash_geo_map_url"),
176180
"location_name": post_attr.get("_event_location"),
177181
"location_additional_info": post_attr.get("_hash_location_specifics"),
178182
"facebook_group_id": config.app_settings.default_facebook_group_id,
@@ -243,6 +247,15 @@ def get_hash_runs(params: HashParams) -> List[Hash]:
243247
if event_attributes is not None and isinstance(event_attributes, list):
244248
hash_data["event_attributes"] = event_attributes
245249

250+
# handle geo_map_url
251+
if hash_data.get("geo_map_url") is None and \
252+
hash_data.get("geo_lat") is not None and hash_data.get("geo_long") is not None:
253+
254+
hash_data["geo_map_url"] = config.app_settings.maps_url_template.format(
255+
lat=hash_data.get("geo_lat"),
256+
long=hash_data.get("geo_long")
257+
)
258+
246259
# parse event data
247260
try:
248261
run = Hash(**hash_data)

api/models/exceptions.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,11 @@ class APITokenValidationFailed(HTTPException):
2525
"""
2626
def __init__(self):
2727
super().__init__(status_code=HTTP_403_FORBIDDEN, detail="API token validation failed")
28+
29+
30+
class CredentialsInvalid(HTTPException):
31+
"""
32+
return a forbidden due to wrong API token
33+
"""
34+
def __init__(self):
35+
super().__init__(status_code=HTTP_403_FORBIDDEN, detail="Credentials invalid")

api/models/run.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@
1212
from pytz import utc
1313
from enum import Enum
1414

15-
from pydantic import BaseModel, AnyHttpUrl, Field, validator, root_validator
15+
from pydantic import BaseModel, AnyHttpUrl, Field, validator, root_validator, ValidationError
1616
from pydantic.dataclasses import dataclass
1717
from fastapi import Query
1818
from fastapi.exceptions import RequestValidationError
1919

2020
from config.hash import hash_attributes, hash_scope
2121
from common.misc import format_slug
22+
from common.log import get_logger
2223
from api.models.exceptions import RequestValidationError
2324

25+
log = get_logger()
26+
2427

2528
# generate from config.hash lists
2629
HashAttributes = Enum('HashAttributes', {x: format_slug(x) for x in hash_attributes}, type=str)
@@ -175,4 +178,19 @@ def set_empty_strings_to_none(cls, value):
175178
return None
176179
return value
177180

181+
@validator("geo_map_url", always=True, pre=True)
182+
def loose_type_geo_map_url(cls, value):
183+
class SelfValidate(BaseModel):
184+
url: AnyHttpUrl
185+
186+
if value is None:
187+
return
188+
try:
189+
SelfValidate(url=value)
190+
except ValidationError as e:
191+
log.warning(f"Issues while validating 'geo_map_url' value '{value}': {e.errors()[0].get('msg')}")
192+
return
193+
194+
return value
195+
178196
# EOF

api/models/send_newsletter.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) 2022 Ricardo Bartels. All rights reserved.
3+
#
4+
# wordpress-hash-event-api
5+
#
6+
# This work is licensed under the terms of the MIT license.
7+
# For a copy, see file LICENSE.txt included in this
8+
# repository or visit: <https://opensource.org/licenses/MIT>.
9+
10+
from pydantic.dataclasses import dataclass
11+
from typing import Any, Dict, List
12+
from pydantic import BaseModel
13+
14+
15+
@dataclass
16+
class SendNewsletterParams:
17+
user: int
18+
token: str
19+
20+
21+
class MailingListModel(BaseModel):
22+
id: int
23+
name: str
24+
25+
26+
class ListmonkReturnData(BaseModel):
27+
id: int
28+
created_at: str
29+
updated_at: str
30+
views: int
31+
clicks: int
32+
bounces: int
33+
lists: List[MailingListModel]
34+
started_at: Any
35+
to_send: int
36+
sent: int
37+
uuid: str
38+
type: str
39+
name: str
40+
subject: str
41+
from_email: str
42+
body: str
43+
altbody: Any
44+
send_at: Any
45+
status: str
46+
content_type: str
47+
tags: List
48+
headers: List
49+
template_id: int
50+
messenger: str
51+
archive: bool
52+
archive_template_id: int
53+
archive_meta: Dict[str, Any]
54+
55+
56+
class ListmonkReturnDataList(BaseModel):
57+
data: ListmonkReturnData

api/routers/send_newsletter.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) 2022 Ricardo Bartels. All rights reserved.
3+
#
4+
# wordpress-hash-event-api
5+
#
6+
# This work is licensed under the terms of the MIT license.
7+
# For a copy, see file LICENSE.txt included in this
8+
# repository or visit: <https://opensource.org/licenses/MIT>.
9+
10+
import hashlib
11+
from fastapi import APIRouter, HTTPException
12+
from time import time
13+
14+
from api.models.run import HashParams
15+
from api.models.send_newsletter import SendNewsletterParams, ListmonkReturnDataList
16+
from api.models.exceptions import CredentialsInvalid
17+
from api.factory.runs import get_hash_runs
18+
from source.database import get_db_handler
19+
from common.misc import php_deserialize, grab
20+
from common.log import get_logger
21+
from listmonk.handler import get_listmonk_handler
22+
23+
24+
log = get_logger()
25+
26+
newsletter = APIRouter(
27+
prefix="/send-newsletter",
28+
tags=["newsletter"]
29+
)
30+
31+
32+
# noinspection PyShadowingBuiltins
33+
@newsletter.post("/{post_id}", response_model=ListmonkReturnDataList, summary="post run",
34+
description="post run via newsletter")
35+
async def get_run(post_id: int, params: SendNewsletterParams):
36+
"""
37+
To post/update a run to listmonk
38+
39+
- **id**: The integer id of the desired run
40+
"""
41+
42+
db_handler = get_db_handler()
43+
44+
# query user metadata from database
45+
user_params = db_handler.get_usermeta(params.user)
46+
session_tokens = None
47+
for item in user_params:
48+
if item.get("meta_key") == "session_tokens":
49+
session_tokens = item.get("meta_value")
50+
51+
if session_tokens is None:
52+
log.debug(f"No session_tokens metadata found in wordpress DB for user id {params.user}")
53+
raise CredentialsInvalid
54+
55+
# use hashed session token to find session in database
56+
hashed_token_sha256 = hashlib.sha256(params.token.encode()).hexdigest()
57+
hashed_token_sha1 = hashlib.sha1(params.token.encode()).hexdigest()
58+
59+
session_data = None
60+
try:
61+
session_data = php_deserialize(session_tokens).get(hashed_token_sha256)
62+
if session_data is None:
63+
session_data = php_deserialize(session_tokens).get(hashed_token_sha1)
64+
except Exception as e:
65+
log.debug(f"PHP deserialization of 'session_tokens' failed: {e}")
66+
67+
if session_data is None:
68+
log.debug(f"No session data found in wordpress DB for user id {params.user}")
69+
raise CredentialsInvalid
70+
71+
# check if session has not expired
72+
expiration_ts = session_data.get("expiration")
73+
if not isinstance(expiration_ts, int):
74+
log.debug(f"session data expiration is not an int: {session_data}")
75+
raise CredentialsInvalid
76+
77+
if expiration_ts < int(time()):
78+
log.debug(f"session already expired")
79+
raise CredentialsInvalid
80+
81+
# all checks passed and user presented a valid session
82+
83+
# fetch post
84+
# noinspection PyArgumentList
85+
result = get_hash_runs(HashParams(id=post_id))
86+
87+
if result is None or len(result) == 0:
88+
raise HTTPException(status_code=404, detail="Run not found")
89+
90+
event = result[0]
91+
92+
# fetch template from listmonk
93+
listmonk_handler = get_listmonk_handler()
94+
listmonk_template = listmonk_handler.get_template(listmonk_handler.config.body_template_id)
95+
96+
if listmonk_template is None:
97+
raise HTTPException(status_code=404,
98+
detail=f"Listmonk template {listmonk_handler.config.body_template_id} not found")
99+
100+
template_body = grab(listmonk_template, "data.body")
101+
102+
if template_body is None:
103+
template_body = event.event_description
104+
105+
# set all paragraph text to center
106+
event.event_description = event.event_description.replace('<p>', '<p style="text-align: center;">')
107+
108+
# use data from post and apply to template
109+
try:
110+
campaign_body = template_body.format(**event.__dict__)
111+
except Exception as e:
112+
log.error(f"Failed to format template: {e}")
113+
raise HTTPException(status_code=500, detail=f"Failed to format template: {e}")
114+
115+
# fetch post metadata to check if newsletter has already been sent before
116+
post_meta_data = db_handler.get_posts_meta([post_id])
117+
post_campaign_id = None
118+
subject_prefix = ""
119+
for post_meta in post_meta_data:
120+
if post_meta.get("meta_key") == "listmonk_campaign_id":
121+
post_campaign_id = post_meta.get("meta_value")
122+
123+
if post_campaign_id is not None:
124+
subject_prefix = "UPDATE: "
125+
126+
# prepare campaign data
127+
campaign_data = {
128+
"name": f"{subject_prefix}{event.event_name}",
129+
"subject": f"{subject_prefix}[{event.kennel_name}] Run #{event.run_number}, "
130+
f"{event.start_date:%A %d %B %Y, %H:%M} @ {event.location_name}",
131+
"lists": listmonk_handler.config.list_ids,
132+
"type": "regular",
133+
"content_type": "html",
134+
"body": campaign_body
135+
}
136+
137+
if listmonk_handler.config.campaign_template_id is not None:
138+
campaign_data["template_id"] = listmonk_handler.config.campaign_template_id
139+
140+
# create listmonk campaign
141+
campaign_result = listmonk_handler.add_campaign(campaign_data)
142+
143+
if campaign_result is None:
144+
raise HTTPException(status_code=503, detail=f"Upstream request failed")
145+
146+
campaign_id = grab(campaign_result, "data.id")
147+
148+
# send campaign
149+
if listmonk_handler.config.send_campaign is True:
150+
campaign_result = listmonk_handler.set_campaign_status(campaign_id, "running")
151+
152+
if campaign_result is None:
153+
raise HTTPException(status_code=503, detail=f"Upstream request failed, unable to start campaign")
154+
155+
# write campaign id to WP database
156+
if post_campaign_id is not None:
157+
db_handler.update_post_meta(post_id, "listmonk_campaign_id", campaign_id)
158+
else:
159+
db_handler.add_post_meta(post_id, "listmonk_campaign_id", campaign_id)
160+
161+
return ListmonkReturnDataList(**campaign_result)
162+
163+
# EOF

0 commit comments

Comments
 (0)