Skip to content

Commit

Permalink
Merge pull request #25592 from ankush/extensible_socketio
Browse files Browse the repository at this point in the history
feat: Extensible SocketIO
  • Loading branch information
ankush committed Apr 6, 2024
2 parents e055d27 + 266b279 commit 41ca767
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 244 deletions.
19 changes: 4 additions & 15 deletions frappe/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,21 +114,9 @@ def emit_via_redis(event, message, room):


@frappe.whitelist(allow_guest=True)
def can_subscribe_doc(doctype: str, docname: str) -> bool:
from frappe.exceptions import PermissionError

if not frappe.has_permission(doctype=doctype, doc=docname, ptype="read"):
raise PermissionError()

return True


@frappe.whitelist(allow_guest=True)
def can_subscribe_doctype(doctype: str) -> bool:
from frappe.exceptions import PermissionError

if not frappe.has_permission(doctype=doctype, ptype="read"):
raise PermissionError()
def has_permission(doctype: str, name: str) -> bool:
if not frappe.has_permission(doctype=doctype, doc=name, ptype="read"):
raise frappe.PermissionError

return True

Expand All @@ -138,6 +126,7 @@ def get_user_info():
return {
"user": frappe.session.user,
"user_type": frappe.session.data.user_type,
"installed_apps": frappe.get_installed_apps(),
}


Expand Down
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,6 @@
"socket.io": "^4.7.1",
"socket.io-client": "^4.7.1",
"sortablejs": "^1.15.0",
"superagent": "^8.0.0",
"touch": "^3.1.0",
"vue": "^3.3.0",
"vue-router": "^4.1.5",
Expand Down
112 changes: 31 additions & 81 deletions realtime/handlers/frappe_handlers.js → realtime/handlers.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
const { frappe_request } = require("../utils");
const log = console.log;

const WEBSITE_ROOM = "website";
const SITE_ROOM = "all";

function frappe_handlers(realtime, socket) {
function frappe_handlers(socket) {
socket.join(user_room(socket.user));
socket.join(WEBSITE_ROOM);

if (socket.user_type == "System User") {
socket.join(SITE_ROOM);
}

socket.has_permission = (doctype, name) => {
return new Promise((resolve) => {
socket
.frappe_request("/api/method/frappe.realtime.has_permission", { doctype, name })
.then((res) => res.json())
.then(({ message }) => {
if (message) {
resolve();
}
})
.catch((err) => console.log("Can't check permissions", err));
});
};

socket.on("doctype_subscribe", function (doctype) {
can_subscribe_doctype({
socket,
doctype,
callback: () => {
socket.join(doctype_room(doctype));
},
socket.has_permission(doctype).then(() => {
socket.join(doctype_room(doctype));
});
});

Expand All @@ -42,14 +49,8 @@ function frappe_handlers(realtime, socket) {
});

socket.on("doc_subscribe", function (doctype, docname) {
can_subscribe_doc({
socket,
doctype,
docname,
callback: () => {
let room = doc_room(doctype, docname);
socket.join(room);
},
socket.has_permission(doctype, docname).then(() => {
socket.join(doc_room(doctype, docname));
});
});

Expand All @@ -59,23 +60,18 @@ function frappe_handlers(realtime, socket) {
});

socket.on("doc_open", function (doctype, docname) {
can_subscribe_doc({
socket,
doctype,
docname,
callback: () => {
let room = open_doc_room(doctype, docname);
socket.join(room);
if (!socket.subscribed_documents) socket.subscribed_documents = [];
socket.subscribed_documents.push([doctype, docname]);

// show who is currently viewing the form
notify_subscribed_doc_users({
socket: socket,
doctype: doctype,
docname: docname,
});
},
socket.has_permission(doctype, docname).then(() => {
let room = open_doc_room(doctype, docname);
socket.join(room);
if (!socket.subscribed_documents) socket.subscribed_documents = [];
socket.subscribed_documents.push([doctype, docname]);

// show who is currently viewing the form
notify_subscribed_doc_users({
socket: socket,
doctype: doctype,
docname: docname,
});
});
});

Expand Down Expand Up @@ -110,28 +106,6 @@ function notify_disconnected_documents(socket) {
}
}

function can_subscribe_doctype(args) {
if (!args) return;
if (!args.doctype) return;
frappe_request("/api/method/frappe.realtime.can_subscribe_doctype", args.socket)
.type("form")
.query({
doctype: args.doctype,
})
.end(function (err, res) {
if (!res || res.status == 403 || err) {
if (err) {
log(err);
}
return false;
} else if (res.status == 200) {
args.callback && args.callback(err, res);
return true;
}
log("ERROR (can_subscribe_doctype): ", err, res);
});
}

function notify_subscribed_doc_users(args) {
if (!(args && args.doctype && args.docname)) {
return;
Expand Down Expand Up @@ -160,30 +134,6 @@ function notify_subscribed_doc_users(args) {
});
}

function can_subscribe_doc(args) {
if (!args) return;
if (!args.doctype || !args.docname) return;
frappe_request("/api/method/frappe.realtime.can_subscribe_doc", args.socket)
.type("form")
.query({
doctype: args.doctype,
docname: args.docname,
})
.end(function (err, res) {
if (!res) {
log("No response for doc_subscribe");
} else if (res.status == 403) {
return;
} else if (err) {
log(err);
} else if (res.status == 200) {
args.callback(err, res);
} else {
log("Something went wrong", err, res);
}
});
}

const doc_room = (doctype, docname) => "doc:" + doctype + "/" + docname;
const open_doc_room = (doctype, docname) => "open_doc:" + doctype + "/" + docname;
const user_room = (user) => "user:" + user;
Expand Down
35 changes: 32 additions & 3 deletions realtime/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const { Server } = require("socket.io");
const http = require("node:http");

const fs = require("fs");
const path = require("path");
const { get_conf, get_redis_subscriber } = require("../node_utils");
const conf = get_conf();

Expand All @@ -25,10 +27,16 @@ const authenticate = require("./middlewares/authenticate");
realtime.use(authenticate);
// =======================

// load and register handlers
const frappe_handlers = require("./handlers/frappe_handlers");
function on_connection(socket) {
frappe_handlers(realtime, socket);
socket.installed_apps.forEach((app) => {
let app_handler = get_app_handlers(app);
try {
app_handler && app_handler(socket);
} catch (err) {
console.warn(`failed to setup event handlers from ${app}`);
console.warn(err);
}
});

// ESBUild "open in editor" on error
socket.on("open_in_editor", async (data) => {
Expand All @@ -37,6 +45,27 @@ function on_connection(socket) {
});
}

const _app_handlers = {};
function get_app_handlers(app) {
if (app in _app_handlers) {
return _app_handlers[app];
}

let file = `../../${app}/realtime/handlers.js`;
let abs_path = path.resolve(__dirname, file);
let handler = null;
if (fs.existsSync(abs_path)) {
try {
handler = require(file);
} catch (err) {
console.warn(`failed to load event handlers from ${abs_path}`);
console.warn(err);
}
}
_app_handlers[app] = handler;
return handler;
}

realtime.on("connection", on_connection);
// =======================

Expand Down
44 changes: 28 additions & 16 deletions realtime/middlewares/authenticate.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
const cookie = require("cookie");
const request = require("superagent");
const { get_url } = require("../utils");

const { get_conf } = require("../../node_utils");
const { get_url } = require("../utils");
const conf = get_conf();

function authenticate_with_frappe(socket, next) {
Expand Down Expand Up @@ -30,21 +28,35 @@ function authenticate_with_frappe(socket, next) {
next(new Error("No authentication method used. Use cookie or authorization header."));
return;
}
socket.sid = cookies.sid;
socket.authorization_header = authorization_header;

let auth_req = request.get(get_url(socket, "/api/method/frappe.realtime.get_user_info"));
if (cookies.sid) {
auth_req = auth_req.query({ sid: cookies.sid });
} else {
auth_req = auth_req.set("Authorization", authorization_header);
}
socket.frappe_request = (path, args = {}, opts = {}) => {
let query_args = new URLSearchParams(args);
if (query_args.toString()) {
path = path + "?" + query_args.toString();
}

let headers = {};
if (socket.sid) {
headers["Cookie"] = `sid=${socket.sid}`;
} else if (socket.authorization_header) {
headers["Authorization"] = socket.authorization_header;
}

return fetch(get_url(socket, path), {
...opts,
headers,
});
};

auth_req
.type("form")
.then((res) => {
socket.user = res.body.message.user;
socket.user_type = res.body.message.user_type;
socket.sid = cookies.sid;
socket.authorization_header = authorization_header;
socket
.frappe_request("/api/method/frappe.realtime.get_user_info")
.then((res) => res.json())
.then(({ message }) => {
socket.user = message.user;
socket.user_type = message.user_type;
socket.installed_apps = message.installed_apps;
next();
})
.catch((e) => {
Expand Down
12 changes: 0 additions & 12 deletions realtime/utils.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
const { get_conf } = require("../node_utils");
const conf = get_conf();
const request = require("superagent");

function get_url(socket, path) {
if (!path) {
Expand All @@ -17,17 +16,6 @@ function get_url(socket, path) {
return url + path;
}

// Authenticates a partial request created using superagent
function frappe_request(path, socket) {
const partial_req = request.get(get_url(socket, path));
if (socket.sid) {
return partial_req.query({ sid: socket.sid });
} else if (socket.authorization_header) {
return partial_req.set("Authorization", socket.authorization_header);
}
}

module.exports = {
get_url,
frappe_request,
};
Loading

0 comments on commit 41ca767

Please sign in to comment.