Skip to content

Commit

Permalink
Update to latest version
Browse files Browse the repository at this point in the history
  • Loading branch information
dunglas committed May 26, 2020
1 parent 27a90a1 commit 4ad93ba
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 135 deletions.
17 changes: 13 additions & 4 deletions docs/ecosystem/awesome.md
Expand Up @@ -2,10 +2,19 @@

## Examples

* [Python/Flask: chat application](https://github.com/dunglas/mercure/tree/master/examples/chat-python-flask) ([live demo](https://python-chat.mercure.rocks/))
* [Node.js: publishing](https://github.com/dunglas/mercure/tree/master/examples/publisher-node.js)
* [PHP: publishing](https://github.com/dunglas/mercure/tree/master/examples/publisher-php.php)
* [Ruby: publishing](https://github.com/dunglas/mercure/tree/master/examples/publisher-ruby.rb)
* [A chat with the list of connected users (using subscriptions, written in JavaScript and Python/Flask)](https://github.com/dunglas/mercure/tree/master/examples/chat-python-flask)

### Publish

* [Node.js](https://github.com/dunglas/mercure/tree/master/examples/publish/node.js)
* [PHP](https://github.com/dunglas/mercure/tree/master/examples/publish/php.php)
* [Ruby](https://github.com/dunglas/mercure/tree/master/examples/publish/ruby.rb)
* [JavaScript (the official debug interface)](https://github.com/dunglas/mercure/tree/master/public/app.js)

### Subscribe

* [Python](https://github.com/dunglas/mercure/tree/master/examples/subscribe/python.py)
* [JavaScript (the official debug interface)](https://github.com/dunglas/mercure/tree/master/public/app.js)

## Documentation and Code Generation

Expand Down
86 changes: 13 additions & 73 deletions examples/chat-python-flask/chat.py
Expand Up @@ -26,19 +26,18 @@
from sseclient import SSEClient
import jwt
import os
import threading
import json
import urllib.parse
from uritemplate import expand

HUB_URL = os.environ.get(
'HUB_URL', 'http://localhost:3000/.well-known/mercure')
JWT_KEY = os.environ.get('JWT_KEY', '!ChangeMe!')
TOPIC = 'https://chat.example.com/messages/{id}'
SUBSCRIPTION_TOPIC = 'https://mercure.rocks/subscriptions/https%3A%2F%2Fchat.example.com%2Fmessages%2F%7Bid%7D/{subscriptionID}'
MESSAGE_TEMPLATE = os.environ.get(
'MESSAGE_TEMPLATE', 'https://chat.example.com/messages/{id}')

lock = threading.Lock()
last_event_id = None
connected_users = {}
SUBSCRIPTIONS_TEMPLATE = '/.well-known/mercure/subscriptions/{topic}{/subscriber}'
SUBSCRIPTIONS_TOPIC = expand(SUBSCRIPTIONS_TEMPLATE, topic=MESSAGE_TEMPLATE)

app = Flask(__name__)

Expand All @@ -54,80 +53,21 @@ def chat():
if not username:
abort(400)

user_iri = 'https://chat.example.com/users/'+username
targets = ['https://chat.example.com/user', user_iri,
'https://mercure.rocks/targets/subscriptions/'+urllib.parse.quote(TOPIC, safe='')]
token = jwt.encode(
{'mercure': {'subscribe': targets, 'publish': targets}},
{'mercure':
{
'subscribe': [MESSAGE_TEMPLATE, SUBSCRIPTIONS_TEMPLATE],
'publish': [MESSAGE_TEMPLATE],
'payload': {'username': username}
}
},
JWT_KEY,
algorithm='HS256',
)

lock.acquire()
local_last_event_id = last_event_id
cu = list(connected_users.keys())
lock.release()
cu.sort()

resp = make_response(render_template('chat.html', config={
'hubURL': HUB_URL, 'userIRI': user_iri, 'connectedUsers': cu, 'lastEventID': local_last_event_id}))
'hubURL': HUB_URL, 'messageTemplate': MESSAGE_TEMPLATE, 'subscriptionsTopic': SUBSCRIPTIONS_TOPIC, 'username': username}))
resp.set_cookie('mercureAuthorization', token, httponly=True, path='/.well-known/mercure',
samesite="strict", domain=os.environ.get('COOKIE_DOMAIN', None), secure=request.is_secure) # Force secure to True for real apps

return resp


@app.before_first_request
def start_sse_listener():
t = threading.Thread(target=sse_listener)
t.start()


def sse_listener():
global connected_users
global last_event_id

token = jwt.encode(
{'mercure': {'subscribe': ['https://chat.example.com/user',
'https://mercure.rocks/targets/subscriptions/'+urllib.parse.quote(TOPIC, safe='')]}},
JWT_KEY,
algorithm='HS256',
)

updates = SSEClient(
HUB_URL,
params={'topic': [TOPIC, SUBSCRIPTION_TOPIC]},
headers={'Authorization': b'Bearer '+token},
)
for update in updates:
app.logger.debug("Update received: %s", update)
data = json.loads(update.data)

if data['@type'] == 'https://chat.example.com/Message':
# Store the chat history somewhere if you want to
lock.acquire()
last_event_id = update.id
lock.release()
break

if data['@type'] == 'https://mercure.rocks/Subscription':
# Instead of maintaining a local user list, you may want to use Redis or similar service

user = next((x for x in data['subscribe'] if x.startswith(
'https://chat.example.com/users/')), None)

if user is None:
break

lock.acquire()
last_event_id = update.id
if data['active']:
connected_users[user] = True
elif user in connected_users:
del connected_users[user]
lock.release()

cu = list(connected_users.keys())
cu.sort()

app.logger.info("Connected users: %s", cu)
2 changes: 1 addition & 1 deletion examples/chat-python-flask/requirements.txt
@@ -1,4 +1,4 @@
Flask>=1.1.2
PyJWT>=1.7.1
sseclient>=0.0.26
uritemplate>=3.0.1
gunicorn>=20.0.4
115 changes: 58 additions & 57 deletions examples/chat-python-flask/static/chat.js
@@ -1,26 +1,59 @@
const type = "https://chat.example.com/Message";
const topic = "https://chat.example.com/messages/{id}";
const { hubURL, userIRI, connectedUsers } = JSON.parse(
const { hubURL, messageTemplate, subscriptionsTopic, username } = JSON.parse(
document.getElementById("config").textContent
);

const iriToUserName = (iri) =>
iri.replace(/^https:\/\/chat.example.com\/users\//, "");
document.getElementById("username").textContent = iriToUserName(userIRI);
document.getElementById("username").textContent = username;

const $messages = document.getElementById("messages");
const $userList = document.getElementById("userList");
let $userListUL = null;
let $messagesUL = null;

let userList = new Map(
connectedUsers
.reduce((acc, val) => {
if (val !== userIRI) acc.push([val, true]);
return acc;
}, [])
.sort()
);
let userList, es;
(async () => {
const resp = await fetch(new URL(subscriptionsTopic, hubURL), {
credentials: "include",
});
const subscriptionCollection = await resp.json();
userList = new Map(
subscriptionCollection.subscriptions
.reduce((acc, { payload }) => {
if (payload.username != username) acc.push([payload.username, true]);
return acc;
}, [])
.sort()
);
updateUserListView();

const subscribeURL = new URL(hubURL);
subscribeURL.searchParams.append(
"Last-Event-ID",
subscriptionCollection.lastEventID
);
subscribeURL.searchParams.append("topic", messageTemplate);
subscribeURL.searchParams.append(
"topic",
`${subscriptionsTopic}{/subscriber}`
);

const es = new EventSource(subscribeURL, { withCredentials: true });
es.onmessage = ({ data }) => {
const update = JSON.parse(data);

if (update["@type"] === type) {
displayMessage(update);
return;
}

if (update["type"] === "Subscription") {
updateUserList(update);
return;
}

console.warn("Received an unsupported update type", update);
};
})();

const updateUserListView = () => {
if (userList.size === 0) {
Expand All @@ -36,43 +69,15 @@ const updateUserListView = () => {
$userListUL.textContent = "";
}

userList.forEach((v, userIRI) => {
userList.forEach((_, username) => {
const li = document.createElement("li");
li.append(document.createTextNode(iriToUserName(userIRI)));
li.append(document.createTextNode(username));
$userListUL.append(li);
});
$userList.append($userListUL);
};

updateUserListView();

const subscribeURL = new URL(hubURL);
subscribeURL.searchParams.append("Last-Event-ID", config.lastEventID);
subscribeURL.searchParams.append("topic", topic);
subscribeURL.searchParams.append(
"topic",
`https://mercure.rocks/subscriptions/${encodeURIComponent(
topic
)}/{subscriptionID}`
);

const es = new EventSource(subscribeURL, { withCredentials: true });
es.onmessage = ({ data }) => {
const update = JSON.parse(data);

switch (update["@type"]) {
case type:
displayMessage(update);
return;
case "https://mercure.rocks/Subscription":
updateUserList(update);
return;
default:
console.error("Unknown update type");
}
};

const displayMessage = ({ user, message }) => {
const displayMessage = ({ username, message }) => {
if (!$messagesUL) {
$messagesUL = document.createElement("ul");

Expand All @@ -81,18 +86,14 @@ const displayMessage = ({ user, message }) => {
}

const li = document.createElement("li");
li.append(document.createTextNode(`<${iriToUserName(user)}> ${message}`));
li.append(document.createTextNode(`<${username}> ${message}`));
$messagesUL.append(li);
};

const updateUserList = ({ active, subscribe }) => {
const user = subscribe.find((u) =>
u.startsWith("https://chat.example.com/users/")
);
if (user === userIRI) return;

active ? userList.set(user, true) : userList.delete(user);
const updateUserList = ({ active, payload }) => {
if (username === payload.username) return;

active ? userList.set(payload.username, true) : userList.delete(payload.username);
userList = new Map([...userList.entries()].sort());

updateUserListView();
Expand All @@ -102,17 +103,17 @@ document.querySelector("form").onsubmit = function (e) {
e.preventDefault();

const uid = window.crypto.getRandomValues(new Uint8Array(10)).join("");
const iri = topic.replace("{id}", uid);
const messageTopic = messageTemplate.replace("{id}", uid);

const body = new URLSearchParams({
data: JSON.stringify({
"@type": type,
"@id": iri,
user: userIRI,
"@id": messageTopic,
username: username,
message: this.elements.message.value,
}),
topic: iri,
target: "https://chat.example.com/user",
topic: messageTopic,
private: true,
});
fetch(hubURL, { method: "POST", body, credentials: "include" });
this.elements.message.value = "";
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
17 changes: 17 additions & 0 deletions examples/subscribe/python.py
@@ -0,0 +1,17 @@
from sseclient import SSEClient
import jwt
import os

token = jwt.encode(
{'mercure': {'subscribe': ['*']}},
os.environ.get('JWT_KEY', '!ChangeMe!'),
algorithm='HS256',
)

updates = SSEClient(
os.environ.get('HUB_URL', 'http://localhost:3000/.well-known/mercure'),
params={'topic': ['*']},
headers={'Authorization': b'Bearer '+token},
)
for update in updates:
print("Update received: ", update)
2 changes: 2 additions & 0 deletions examples/subscribe/requirements.txt
@@ -0,0 +1,2 @@
PyJWT>=1.7.1
sseclient>=0.0.26

0 comments on commit 4ad93ba

Please sign in to comment.