diff --git a/src/api/acts.ts b/src/api/acts.ts index a865ad3..c16d86b 100644 --- a/src/api/acts.ts +++ b/src/api/acts.ts @@ -3,4 +3,5 @@ export * from "./acts/client.js" export * from "./acts/admin.js" export * from "./acts/rooms.js" export * from "./acts/server.js" -export * from "./acts/roomContent.js" \ No newline at end of file +export * from "./acts/roomContent.js" +export * from "./acts/subscribe.js" \ No newline at end of file diff --git a/src/api/acts/roomContent.ts b/src/api/acts/roomContent.ts index 036bcee..3cbd518 100644 --- a/src/api/acts/roomContent.ts +++ b/src/api/acts/roomContent.ts @@ -6,6 +6,8 @@ import { isCategoryInRoom, isItemInRoom, isProductInRoom, isRoomDataFull } from import { ROOM_RIGHTS } from "../../server/permissions.js"; import { act_error } from "../../server/errors.js"; import { uts } from "../../sys/tools.js"; +import { sendRoomListeners } from "../listener.js"; +import { LISTENER_ACTION, LISTENER_TYPE } from "../../server/listener.js"; export const getCategories: Act = { state: STATE.client | STATE.remote, @@ -128,10 +130,12 @@ export const addCategory: Act = { .where(eq(listCategories.roomID, roomID)) .limit(1) ).query(db); - if (req.affectedRows > 0) aws("ok", { - catID: Number(req.insertId) - }); - else aws("error", act_error.ADD_CAT); + if (req.affectedRows > 0) { + aws("ok", { + catID: Number(req.insertId) + }); + sendRoomListeners(roomID, LISTENER_TYPE.CATEGORIES, LISTENER_ACTION.ADD, [req.insertId]); + } else aws("error", act_error.ADD_CAT); } }; @@ -165,8 +169,10 @@ export const changeCategory: Act = { eq(listCategories.listCatID, data.listCatID) )) .query(db); - if (req.affectedRows > 0) aws("ok", ""); - else aws("error", act_error.CAT_NOT_EXISTS); + if (req.affectedRows > 0) { + aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.CATEGORIES, LISTENER_ACTION.CHANGE, [data.listCatID]); + } else aws("error", act_error.CAT_NOT_EXISTS); } }; @@ -202,8 +208,10 @@ export const changeCategoriesOrder: Act = { .query(db); affacted += req.affectedRows; } - if (affacted > 0) aws("ok", ""); - else aws("error", act_error.CAT_NOT_EXISTS); + if (affacted > 0) { + aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.CATEGORIES, LISTENER_ACTION.CHANGE, data.listCatIDs); + } else aws("error", act_error.CAT_NOT_EXISTS); } }; @@ -233,8 +241,10 @@ export const deleteCategory: Act = { eq(listCategories.listCatID, data.listCatID) )) .query(db); - if (req.affectedRows > 0) aws("ok", ""); - else aws("error", act_error.CAT_NOT_EXISTS); + if (req.affectedRows > 0) { + aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.CATEGORIES, LISTENER_ACTION.DELETE, [data.listCatID]); + } else aws("error", act_error.CAT_NOT_EXISTS); } }; @@ -383,10 +393,12 @@ export const addProduct: Act = { data.ean, data.parent != null ? data.parent : null, ).query(db); - if (req.affectedRows > 0) aws("ok", { - listProdID: Number(req.insertId) - }); - else aws("error", act_error.ADD_PROD); + if (req.affectedRows > 0) { + aws("ok", { + listProdID: Number(req.insertId) + }); + sendRoomListeners(roomID, LISTENER_TYPE.PRODUCTS, LISTENER_ACTION.ADD, [req.insertId]); + } else aws("error", act_error.ADD_PROD); } }; @@ -434,8 +446,10 @@ export const changeProduct: Act = { eq(listProducts.roomID, roomID), )) .query(db); - if (req.affectedRows > 0) aws("ok", ""); - else aws("error", act_error.PROD_NOT_EXISTS); + if (req.affectedRows > 0) { + aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.PRODUCTS, LISTENER_ACTION.CHANGE, [data.listProdID]); + } else aws("error", act_error.PROD_NOT_EXISTS); } }; @@ -465,8 +479,10 @@ export const deleteProduct: Act = { eq(listProducts.listProdID, data.listProdID), eq(listProducts.roomID, roomID), )).query(db); - if (req.affectedRows > 0) aws("ok", ""); - else aws("error", act_error.PROD_NOT_EXISTS); + if (req.affectedRows > 0) { + aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.PRODUCTS, LISTENER_ACTION.DELETE, [data.listProdID]); + } else aws("error", act_error.PROD_NOT_EXISTS); } }; @@ -636,10 +652,12 @@ export const addItem: Act = { data.value, data.listProdID != null ? data.listProdID : null, ).query(db); - if (req.affectedRows > 0) aws("ok", { - listItemID: Number(req.insertId) - }); - else aws("error", act_error.ADD_ITEM); + if (req.affectedRows > 0) { + aws("ok", { + listItemID: Number(req.insertId) + }); + sendRoomListeners(roomID, LISTENER_TYPE.ITEMS, LISTENER_ACTION.ADD, [req.insertId]); + } else aws("error", act_error.ADD_ITEM); } }; @@ -687,8 +705,10 @@ export const changeItem: Act = { eq(listItems.roomID, roomID) )) .query(db); - if (req.affectedRows > 0) aws("ok", ""); - else aws("error", act_error.ITEM_NOT_EXISTS); + if (req.affectedRows > 0) { + aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.ITEMS, LISTENER_ACTION.CHANGE, [data.listItemID]); + } else aws("error", act_error.ITEM_NOT_EXISTS); } }; @@ -723,8 +743,10 @@ export const changeItemState: Act = { eq(listItems.roomID, roomID) )) .query(db); - if (req.affectedRows > 0) aws("ok", ""); - else aws("error", act_error.ITEM_NOT_EXISTS); + if (req.affectedRows > 0) { + aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.ITEMS, LISTENER_ACTION.CHANGE, [data.listItemID]); + } else aws("error", act_error.ITEM_NOT_EXISTS); } }; @@ -753,7 +775,12 @@ export const changeItemStates: Act = { if ( data.listItemIDs.length != data.changedTimes.length || data.changedTimes.length != data.states.length - ) return void aws("error", "data"); + ) return void aws("error", act_error.DATA); + for (let i = 0; i < data.listItemIDs.length; i++) { + let id = data.listItemIDs[i]; + if (typeof id != "number" || id < 0) + return void aws("error", act_error.DATA); + } for (let i = 0; i < data.listItemIDs.length; i++) { let id = data.listItemIDs[i]; let time = data.changedTimes[i]; @@ -769,6 +796,7 @@ export const changeItemStates: Act = { .query(db); } aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.ITEMS, LISTENER_ACTION.CHANGE, data.listItemIDs); } }; @@ -797,8 +825,10 @@ export const deleteItem: Act = { eq(listItems.listItemID, data.listItemID), eq(listItems.roomID, roomID) )).query(db); - if (req.affectedRows > 0) aws("ok", ""); - else aws("error", act_error.ITEM_NOT_EXISTS); + if (req.affectedRows > 0) { + aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.ITEMS, LISTENER_ACTION.DELETE, [data.listItemID]); + } else aws("error", act_error.ITEM_NOT_EXISTS); } }; export const deleteItemByState: Act = { @@ -827,5 +857,6 @@ export const deleteItemByState: Act = { eq(listItems.roomID, roomID) )).query(db); aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.ITEMS, LISTENER_ACTION.DELETE, []); } } \ No newline at end of file diff --git a/src/api/acts/rooms.ts b/src/api/acts/rooms.ts index 91a2186..4ec51d2 100644 --- a/src/api/acts/rooms.ts +++ b/src/api/acts/rooms.ts @@ -8,6 +8,8 @@ import { isRoomFull } from "../helper.js"; import { fetchRemoteAsServer } from "../server.js"; import { Act, Client, STATE } from "../user.js"; import { act_error } from "../../server/errors.js"; +import { sendRoomListeners } from "../listener.js"; +import { LISTENER_ACTION, LISTENER_TYPE } from "../../server/listener.js"; export const listRooms: Act = { state: STATE.client | STATE.remote, @@ -46,7 +48,7 @@ export const listRooms: Act = { if (name != null && owner != null && rights != null && visibility != null && title != null && description != null && icon != null && confirmed != null) { return { name, server, owner, rights, visibility, title, description, icon, debug: global.debug, confirmed }; } - console.log(name, server, owner, rights, visibility, title, description, icon, global.debug, confirmed) + //console.log(name, server, owner, rights, visibility, title, description, icon, global.debug, confirmed) return null; }); if (client.state == STATE.client) { @@ -608,6 +610,7 @@ export const setAdminStatus: Act = { } else { aws("error", act_error.MEMBER_NOT_EXISTS); } + sendRoomListeners(roomID, LISTENER_TYPE.ROOMINFO, LISTENER_ACTION.CHANGE, []); } }; @@ -617,6 +620,7 @@ export const leaveRoom: Act = { data: { room: "string", server: "string", + // TODO: add force option for remote rooms }, func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { if (!checkSelfTag(data.server)) { @@ -706,6 +710,7 @@ export const setRoomRight: Act = { .where(eq(rooms.roomID, roomID)) .query(db); aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.ROOMINFO, LISTENER_ACTION.CHANGE, []); } }; @@ -735,5 +740,6 @@ export const changeRoomMeta: Act = { .where(eq(rooms.roomID, roomID)) .query(db); aws("ok", ""); + sendRoomListeners(roomID, LISTENER_TYPE.ROOMINFO, LISTENER_ACTION.CHANGE, []); } }; \ No newline at end of file diff --git a/src/api/acts/subscribe.ts b/src/api/acts/subscribe.ts new file mode 100644 index 0000000..df7f82c --- /dev/null +++ b/src/api/acts/subscribe.ts @@ -0,0 +1,68 @@ +import { act_error } from "../../server/errors.js"; +import { checkSelfTag } from "../../server/outbagURL.js"; +import { Act, Client, STATE } from "../user.js"; +import { wsClient } from "../ws.js"; + +export const subscribeRoom: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "name", + server: "string", + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + if (!(client.connectionClient instanceof wsClient)) { + return void aws("error", act_error.CONNECTION); + } + if (!checkSelfTag(data.server)) { + throw new Error("Remote not yet implemented."); + return; + } + const roomID = await client.isInRoom(data.room); + if (roomID == -1) { + aws("error", act_error.NOT_IN_ROOM); + return; + } + client.connectionClient.listenRoom(roomID, data.room, data.server); + aws("ok", ""); + } +}; + +export const unsubscribeRoom: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: { + room: "name", + server: "string", + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + if (!(client.connectionClient instanceof wsClient)) { + return void aws("error", act_error.CONNECTION); + } + if (!checkSelfTag(data.server)) { + throw new Error("Remote not yet implemented."); + return; + } + const roomID = await client.isInRoom(data.room); + if (roomID == -1) { + aws("error", act_error.NOT_IN_ROOM); + return; + } + client.connectionClient.unlistenRoom(roomID); + aws("ok", ""); + } +}; + +export const unsubscribeAllRooms: Act = { + state: STATE.client | STATE.remote, + right: 0, + data: {}, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + if (!(client.connectionClient instanceof wsClient)) { + return void aws("error", act_error.CONNECTION); + } + // TODO: When implemented close remote listeners + client.connectionClient.unlistenAllRooms(); + aws("ok", ""); + } +}; \ No newline at end of file diff --git a/src/api/listener.ts b/src/api/listener.ts new file mode 100644 index 0000000..ebff3cf --- /dev/null +++ b/src/api/listener.ts @@ -0,0 +1,38 @@ +import { wsClient } from "./ws.js" + + + +interface RoomListener { + client: wsClient; + room: string; + server: string; +} + +let roomListeners: { [key: number]: RoomListener[] } = {}; + +export const sendRoomListeners = async (roomID: number, type: string | number, action: number, ids: number[]) => { + for (const listener of roomListeners[roomID] ?? []) { + listener.client.sendFromServer({ + room: listener.room, + server: listener.server, + type, + action, + ids, + }); + } +}; + +export const addRoomListener = (client: wsClient, roomID: number, room: string, server: string) => { + if (!Array.isArray(roomListeners[roomID])) roomListeners[roomID] = []; + const existingListener = roomListeners[roomID].find(listener => listener.client === client); + if (!existingListener) roomListeners[roomID].push({ client, room, server }); + +}; + +export const removeRoomListener = (client: wsClient, roomID: number | null = null) => { + let keys = roomID == null ? Object.keys(roomListeners) as any : [roomID]; + for (const key of keys) { + roomListeners[key] = roomListeners[key].filter(listener => listener.client !== client); + if (roomListeners[key].length === 0) delete roomListeners[key]; + } +}; diff --git a/src/api/post.ts b/src/api/post.ts index 731c6fd..bf8c3d2 100644 --- a/src/api/post.ts +++ b/src/api/post.ts @@ -116,7 +116,8 @@ export const addPostMethods = (server: express.Express) => { export class postClient { lastReq = uts(); client: Client; - constructor(ip: string, client = new Client(ip)) { + constructor(ip: string, client: Client | null = null) { + if (client === null) client = new Client(ip, this); this.client = client; } async runAct(act: Act, json: any, aws: (state: string, data: any) => void) { diff --git a/src/api/user.ts b/src/api/user.ts index b502bfd..92864e0 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -5,6 +5,8 @@ import { outbagServer, outbagURLfromTag } from "../server/outbagURL.js"; import { fetchRemoteAs } from "./server.js"; import { debug } from "../sys/log.js"; import { act_error } from "../server/errors.js"; +import { wsClient } from "./ws.js"; +import { postClient } from "./post.js"; export const STATE = { no: 0b00001, @@ -39,8 +41,11 @@ export class Client { accID: number = -1; challenge: string = ""; remoteKey: string = ""; + + connectionClient: wsClient | postClient; - constructor(ip: string) { + constructor(ip: string, client: wsClient | postClient) { + this.connectionClient = client; this.server = new outbagServer("", "", "", ""); this.ip = ip; } diff --git a/src/api/ws.ts b/src/api/ws.ts index 1d212a1..1517c64 100644 --- a/src/api/ws.ts +++ b/src/api/ws.ts @@ -5,6 +5,7 @@ import { Act, checktype, Client } from "./user.js"; import { debug, error } from "../sys/log.js"; import * as importActs from "./acts.js" import { act_error } from "../server/errors.js"; +import { addRoomListener, removeRoomListener } from "./listener.js"; let acts = importActs as { [key: string]: Act }; @@ -30,7 +31,7 @@ export class wsClient { activeRequests = 0; client: Client; constructor(socket: ws.WebSocket, req: http.IncomingMessage) { - this.client = new Client(req.socket.remoteAddress ?? ""); + this.client = new Client(req.socket.remoteAddress ?? "", this); this.socket = socket; clients.push(this); @@ -143,6 +144,7 @@ export class wsClient { }); socket.on('close', () => { + this.unlistenAllRooms(); this.open = false; var i = clients.indexOf(this); delete clients[i]; @@ -154,6 +156,7 @@ export class wsClient { close(ms: number) { return new Promise(res => { + this.unlistenAllRooms(); if (this.activeRequests == 0) { this.socket.close(); return void res(); @@ -164,6 +167,34 @@ export class wsClient { }, Math.max(100, ms)); }); } + + sendFromServer(data: any): Promise { + if (this.socket.readyState != WebSocket.OPEN) { + return Promise.resolve(false); + } + return new Promise((res, rej) => { + this.socket.send(JSON.stringify({ + id: -1, + state: "server", + data: data + }), (err) => { + if(err) return void rej(false); + return void res(true); + }); + }); + + + } + + listenRoom(roomID: number, room: string, server: string) { + addRoomListener(this, roomID, room, server); + } + unlistenRoom(roomID: number) { + removeRoomListener(this, roomID); + } + unlistenAllRooms() { + removeRoomListener(this); + } } export const closeWebSocket = async () => { diff --git a/src/server/errors.ts b/src/server/errors.ts index e1e0975..9bff81f 100644 --- a/src/server/errors.ts +++ b/src/server/errors.ts @@ -10,6 +10,7 @@ export const act_error = { SERVER: "server", // uncaught error in server RECURSION: "recursion", // not allowed due to suspected remote recursion, will only appear with miss configureation REMOTE: "remote", // error while remote request (like could not contact the remote server) + CONNECTION: "connection", // the current connection type is not correct CLIENT_NOT_EXISTS: "clientnotexists", // seems like your own account does not exists (client.ts acts) ACCOUNT_NOT_EXISTS: "accountnotexists", // referred account does not exists (admin / client trennen? ) diff --git a/src/server/listener.ts b/src/server/listener.ts new file mode 100644 index 0000000..d663454 --- /dev/null +++ b/src/server/listener.ts @@ -0,0 +1,14 @@ + + +export const LISTENER_TYPE = { + ROOMINFO: 0, + ITEMS: 1, + CATEGORIES: 2, + PRODUCTS: 3, +} + +export const LISTENER_ACTION = { + ADD: 0, + CHANGE: 1, + DELETE: 2, +} \ No newline at end of file