diff --git a/.woodpecker/.release_code.yml b/.woodpecker/.release_code.yml index ea77639..d45959e 100644 --- a/.woodpecker/.release_code.yml +++ b/.woodpecker/.release_code.yml @@ -1,5 +1,5 @@ # .woodpecker.yml -platform: linux/arm64 +#platform: linux/arm64 pipeline: build: diff --git a/.woodpecker/.test.yml b/.woodpecker/.test.yml index 4d94068..1b43bdb 100644 --- a/.woodpecker/.test.yml +++ b/.woodpecker/.test.yml @@ -1,5 +1,5 @@ # .woodpecker.yml -platform: linux/arm64 +#platform: linux/arm64 pipeline: build: diff --git a/package-lock.json b/package-lock.json index bd161d0..35e8772 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "auth-header": "^1.0.0", "commander": "^10.0.0", "cors": "^2.8.5", - "dblang": "https://jusax.de/git/attachments/377d0a32-3eca-4a8f-9c3a-f8a045a9c5b1", + "dblang": "https://jusax.de/git/attachments/84353ff6-f81e-450b-93e1-d0a4d6d4556f", "express": "^4.18.2", "juml": "https://jusax.de/git/attachments/208913c5-2851-4b86-a53d-ca99fed168cc", "nman": "https://jusax.de/git/attachments/5333948b-fe6b-45d2-9230-ca388f6a89bc", @@ -1107,9 +1107,9 @@ } }, "node_modules/dblang": { - "version": "0.9.3", - "resolved": "https://jusax.de/git/attachments/377d0a32-3eca-4a8f-9c3a-f8a045a9c5b1", - "integrity": "sha512-Co3RZ2Dfk2Atm2Oyr7rtHJDeiMZ8NwfrvTBwfhP9wVkXQuN1WrMMQ5W+/Ho2g6c6BWbUnsqADKTaCcJZrYBbjQ==", + "version": "0.9.5", + "resolved": "https://jusax.de/git/attachments/84353ff6-f81e-450b-93e1-d0a4d6d4556f", + "integrity": "sha512-g7hBlnib2Tg7DoeGaygvnzBCPn1m47yrS9rMHkThqRfQp43wT/gUWqmnvxunNMVo0ka7PEwdwW4FpKz7VnpGqA==", "license": "UNLICENSED", "dependencies": { "gitea-release": "git+https://jusax.de/git/jusax23/gitea-release.git", diff --git a/package.json b/package.json index a8388f9..59c2d2c 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "auth-header": "^1.0.0", "commander": "^10.0.0", "cors": "^2.8.5", - "dblang": "https://jusax.de/git/attachments/377d0a32-3eca-4a8f-9c3a-f8a045a9c5b1", + "dblang": "https://jusax.de/git/attachments/84353ff6-f81e-450b-93e1-d0a4d6d4556f", "express": "^4.18.2", "juml": "https://jusax.de/git/attachments/208913c5-2851-4b86-a53d-ca99fed168cc", "nman": "https://jusax.de/git/attachments/5333948b-fe6b-45d2-9230-ca388f6a89bc", diff --git a/src/api/acts.ts b/src/api/acts.ts index 2a8139b..4fddb34 100644 --- a/src/api/acts.ts +++ b/src/api/acts.ts @@ -1,4 +1,5 @@ export * from "./acts/login.js"; export * from "./acts/client.js" export * from "./acts/admin.js" -export * from "./acts/rooms.js" \ No newline at end of file +export * from "./acts/rooms.js" +export * from "./acts/server.js" \ No newline at end of file diff --git a/src/api/acts/admin.ts b/src/api/acts/admin.ts index 993f48a..89011e1 100644 --- a/src/api/acts/admin.ts +++ b/src/api/acts/admin.ts @@ -25,8 +25,8 @@ export const getAccounts: Act = { let accID = d[accounts.accID]; let rights = d[accounts.rights]; let name = d[accounts.name]; - let viewable = d[accounts.viewable]; - let deleted = d[accounts.deleted]; + let viewable = d[accounts.viewable] ? true : false; + let deleted = d[accounts.deleted] ? true : false; let maxRooms = d[accounts.maxRooms]; let maxRoomSize = d[accounts.maxRoomSize]; let maxUsersPerRoom = d[accounts.maxUsersPerRoom]; diff --git a/src/api/acts/client.ts b/src/api/acts/client.ts index 42ae3f5..3aced2f 100644 --- a/src/api/acts/client.ts +++ b/src/api/acts/client.ts @@ -1,7 +1,7 @@ import { and, eq, ge, insert, leq, remove, select, update } from "dblang"; import { PERMISSIONS } from "../../server/permissions.js"; import { sha256, sign } from "../../sys/crypto.js"; -import { accounts, db, invitations, roomMembers, rooms } from "../../sys/db.js"; +import { accounts, db, roomMembers, rooms } from "../../sys/db.js"; import { selfTag } from "../../sys/selfTag.js"; import { getSettings, SETTINGS } from "../../sys/settings.js"; import { get64, uts } from "../../sys/tools.js"; @@ -55,7 +55,7 @@ export const getMyAccount: Act = { if (query.length > 0) { let rights = query[0][accounts.rights]; let name = query[0][accounts.name]; - let viewable = query[0][accounts.viewable]; + let viewable = query[0][accounts.viewable] ? true : false; let maxRooms = query[0][accounts.maxRooms]; let maxRoomSize = query[0][accounts.maxRoomSize]; let maxUsersPerRoom = query[0][accounts.maxUsersPerRoom]; @@ -141,8 +141,8 @@ export const createRoom: Act = { data.icon ).query(db); if (req.affectedRows > 0) { - await insert(roomMembers.roomID, roomMembers.name, roomMembers.server, roomMembers.admin) - .add(req.insertId, client.name, "local", true) + await insert(roomMembers.roomID, roomMembers.name, roomMembers.server, roomMembers.admin, roomMembers.confirmed) + .add(req.insertId, client.name, "local", true, true) .query(db); aws("ok", ""); } else { @@ -203,56 +203,3 @@ export const listPublicRooms: Act = { aws("ok", out.filter(d => d != null)); } }; - -export const getInvitations: Act = { - state: STATE.client, - right: PERMISSIONS.CAN_USE_API, - data: {}, - func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { - await remove(invitations) - .where(leq(invitations.expires, uts())) - .query(db); - let req = await select([ - invitations.invitationID, - invitations.room, - invitations.server, - invitations.ota, - invitations.expires, - invitations.fromName, - invitations.fromServer, - ], invitations) - .where(eq(invitations.accID, client.accID)) - .query(db); - let out = req.map(d => { - let invitationID = d[invitations.invitationID]; - let room = d[invitations.room]; - let server = d[invitations.server]; - let ota = d[invitations.ota]; - let expires = d[invitations.expires]; - let fromName = d[invitations.fromName]; - let fromServer = d[invitations.fromServer]; - if (invitationID != null && room != null && server != null && ota != null && expires != null && fromName != null && fromServer != null) { - return { invitationID, room, server, ota, expires, fromName, fromServer }; - } - return null; - }); - aws("ok", out.filter(d => d != null)); - } -}; - -export const deleteInvitation: Act = { - state: STATE.client, - right: PERMISSIONS.CAN_USE_API, - data: { - invitationID: "number", - }, - func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { - let req = await remove(invitations) - .where(and( - eq(invitations.accID, client.accID), - eq(invitations.invitationID, data.invitationID), - )).query(db); - if (req.affectedRows > 0) aws("ok", ""); - else aws("error", "existence"); - } -}; \ No newline at end of file diff --git a/src/api/acts/rooms.ts b/src/api/acts/rooms.ts index 6911dc2..70f9e84 100644 --- a/src/api/acts/rooms.ts +++ b/src/api/acts/rooms.ts @@ -1,10 +1,11 @@ import { alias, and, eq, exists, geq, innerJoinOn, innerJoinUsing, insert, le, minus, naturalJoin, not, or, remove, select, update } from "dblang"; import { checkSelfTag, outbagURLfromTag } from "../../server/outbagURL.js"; import { ROOM_RIGHTS } from "../../server/permissions.js"; -import { accounts, db, invitations, remoteRooms, roomMembers, roomOTAs, rooms } from "../../sys/db.js"; +import { accounts, db, remoteRooms, roomMembers, roomOTAs, rooms } from "../../sys/db.js"; import { selfTag } from "../../sys/selfTag.js"; import { uts } from "../../sys/tools.js"; import { isRoomFull } from "../helper.js"; +import { fetchRemoteAsServer } from "../server.js"; import { Act, Client, STATE } from "../user.js"; export const listRooms: Act = { @@ -23,7 +24,8 @@ export const listRooms: Act = { rooms.visibility, rooms.title, rooms.description, - rooms.icon + rooms.icon, + roomMembers.confirmed ], innerJoinUsing(rooms, roomMembers, rooms.roomID, roomMembers.roomID)) .where(and( eq(roomMembers.name, client.name), @@ -38,29 +40,73 @@ export const listRooms: Act = { let title = d[rooms.title]; let description = d[rooms.description]; let icon = d[rooms.icon]; + let confirmed = d[roomMembers.confirmed] ? true : false; let server = selfTag.tag; - if (name != null && owner != null && rights != null && visibility != null && title != null && description != null && icon != null) { - return { name, server, owner, rights, visibility, title, description, icon, debug: global.debug }; + 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) return null; - }) + }); if (client.state == STATE.client) { - let query = await select([remoteRooms.server], remoteRooms) + let query = await select([ + remoteRooms.server, + remoteRooms.room, + remoteRooms.confirmed + ], remoteRooms) .where(eq(remoteRooms.accID, client.accID)) .query(db); - for (let i = 0; i < query.length; i++) { - const server = query[i]; - let resp = await client.pass(server[remoteRooms.server], "listRooms", {}); + let serverList = [...new Set(query.map(t => t[remoteRooms.server]))]; + + let toAddRooms: ([number, string, string])[] = []; + + for (let i = 0; i < serverList.length; i++) { + const server = serverList[i]; + + let serverRooms = Object.fromEntries(query + .filter(d => d[remoteRooms.server] == server) + .map(d => [d[remoteRooms.room] + "@" + d[remoteRooms.server], { + server: d[remoteRooms.server], + room: d[remoteRooms.room], + confirmed: d[remoteRooms.confirmed] ? true : false + }]) + ); + + let resp = await client.pass(server, "listRooms", {}); if (resp.state == "ok" && Array.isArray(resp.data)) { - out.push(...resp.data.map(d => { - let { name, owner, rights, visibility, title, description, icon, server, debug } = d; - if (name != null && owner != null && rights != null && visibility != null && title != null && description != null && icon != null && debug != null) { - return { name, server, owner, rights, visibility, title, description, icon, debug }; - } - return null; - })) + for (let j = 0; j < resp.data.length; j++) { + const rRooms = resp.data[j]; + try { + let { name, owner, rights, visibility, title, description, icon, debug } = rRooms; + if (name != null && owner != null && rights != null && visibility != null && title != null && description != null && icon != null && debug != null) { + let sRoom = serverRooms[rRooms.room + "@" + rRooms.server]; + if (sRoom == null) toAddRooms.push([client.accID, name, server]); + + out.push({ + name, owner, rights, visibility, title, description, icon, server, debug, confirmed: sRoom?.confirmed ?? false + }); + delete serverRooms[rRooms.room + "@" + rRooms.server]; + } + } catch (error) { } + } + for (const k in serverRooms) { + const unfoundRoom = serverRooms[k]; + await remove(remoteRooms) + .where(and( + eq(remoteRooms.accID, client.accID), + eq(remoteRooms.server, unfoundRoom.server), + eq(remoteRooms.room, unfoundRoom.room) + )).query(db); + } + } else { + //may add unfound rooms } } + try { + await insert(remoteRooms.accID, remoteRooms.room, remoteRooms.server) + .addValues(...toAddRooms) + .query(db); + } catch (error) { } } aws("ok", out.filter(d => d != null)); } @@ -141,17 +187,19 @@ export const getRoomMembers: Act = { let req = await select([ roomMembers.name, roomMembers.server, - roomMembers.admin + roomMembers.admin, + roomMembers.confirmed, ], roomMembers) .where(eq(roomMembers.roomID, roomID)) .query(db); let out = req.map(d => { let name = d[roomMembers.name]; let server = d[roomMembers.server]; - let admin = d[roomMembers.admin]; + let admin = d[roomMembers.admin] ? true : false; + let confirmed = d[roomMembers.confirmed] ? true : false; server = server == "local" ? selfTag.tag : server; - if (name != null && server != null && admin != null) { - return { name, server, admin }; + if (name != null && server != null && admin != null && confirmed != null) { + return { name, server, admin, confirmed }; } return null; }); @@ -173,8 +221,8 @@ export const joinRoom: Act = { let resp = await client.pass(data.server, "joinRoom", data); if (resp.state == "ok") { try { - await insert(remoteRooms.accID, remoteRooms.server) - .add(client.accID, data.server) + await insert(remoteRooms.accID, remoteRooms.server, remoteRooms.room, remoteRooms.confirmed) + .add(client.accID, data.server, data.room, true) .query(db); } catch (error) { } } else if (resp.data == "ota") { @@ -248,8 +296,8 @@ export const joinPublicRoom: Act = { let resp = await client.pass(data.server, "joinPublicRoom", data); if (resp.state == "ok") { try { - await insert(remoteRooms.accID, remoteRooms.server) - .add(client.accID, data.server) + await insert(remoteRooms.accID, remoteRooms.server, remoteRooms.room, remoteRooms.confirmed) + .add(client.accID, data.server, data.room, true) .query(db); } catch (error) { } } @@ -303,7 +351,7 @@ export const getRoomOTAs: Act = { } let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.OTA); if (roomID == -1) return void aws("error", "roomAdmin"); - let req = await select([roomOTAs.token, roomOTAs.name, roomOTAs.expires, roomOTAs.usesLeft, roomOTAs.isInvitation], roomOTAs) + let req = await select([roomOTAs.token, roomOTAs.name, roomOTAs.expires, roomOTAs.usesLeft], roomOTAs) .where(eq(roomOTAs.roomID, roomID)) .query(db); aws("ok", req.map(d => ({ @@ -311,7 +359,6 @@ export const getRoomOTAs: Act = { name: d[roomOTAs.name], expires: d[roomOTAs.expires], usesLeft: d[roomOTAs.usesLeft], - isInvitation: d[roomOTAs.isInvitation], }))); } }; @@ -326,7 +373,6 @@ export const addRoomOTA: Act = { // or change it, primary key is room and token name: "string-256", expires: "number", usesLeft: "number", - isInvitation: "boolean" }, func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { if (!checkSelfTag(data.server)) { @@ -338,15 +384,14 @@ export const addRoomOTA: Act = { // or change it, primary key is room and token let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.OTA); if (roomID == -1) return void aws("error", "roomAdmin"); try { - await insert(roomOTAs.roomID, roomOTAs.token, roomOTAs.name, roomOTAs.expires, roomOTAs.usesLeft, roomOTAs.isInvitation) - .add(roomID, data.token, data.name, data.expires, data.usesLeft, data.isInvitation) + await insert(roomOTAs.roomID, roomOTAs.token, roomOTAs.name, roomOTAs.expires, roomOTAs.usesLeft) + .add(roomID, data.token, data.name, data.expires, data.usesLeft) .query(db); } catch (error) { await update(roomOTAs) .set(roomOTAs.expires, data.expires) .set(roomOTAs.usesLeft, data.usesLeft) .set(roomOTAs.name, data.name) - .set(roomOTAs.isInvitation, data.isInvitation) .where(and( eq(roomOTAs.token, data.token), eq(roomOTAs.roomID, roomID) @@ -382,57 +427,46 @@ export const deleteRoomOTA: Act = { } }; -export const invite: Act = { - state: STATE.client, +export const inviteUser: Act = { + state: STATE.client | STATE.remote, right: 0, data: { room: "name-100", roomServer: "string", name: "string", server: "string", - token: "string-256", // ota - expires: "number", // max 10 Days }, func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { - if (!checkSelfTag(data.server)) { + if (!checkSelfTag(data.roomServer)) { if (client.state != STATE.client) return void aws("error", "right"); - let resp = await client.pass(data.server, "invite", data); + let resp = await client.pass(data.roomServer, "deleteRoomOTA", data); aws(resp.state, resp.data); return; } - try { - let roomServer = await outbagURLfromTag(data.roomServer); - let resp = await select([accounts.accID], accounts) - .where(eq(accounts.name, data.name)) - .query(db); - if (resp.length == 0) { + let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.OTA); + if (roomID == -1) return void aws("error", "roomAdmin"); + + let userServer = data.server; + if (!checkSelfTag(userServer)) { + let resp = await fetchRemoteAsServer(userServer, "invite", data); + if (resp.state == "error") { + client.suspect(); client.suspect(); return void aws("error", "existence"); } - if (data.expires <= uts() + 1) return void aws("ok", ""); - await insert( - invitations.accID, - invitations.room, - invitations.server, - invitations.ota, - invitations.expires, - invitations.fromName, - invitations.fromServer - ).add( - resp[0].accID, - data.room, - roomServer.tag, - data.token, - Math.min(data.expires, uts() + 60 * 60 * 24 * 10), - client.name, - client.server.tag, - ).query(db); - - } catch (error) { - return void aws("error", "existence"); + } else { + userServer = "local"; } + // on roomServer + let req = await insert(roomMembers.roomID, roomMembers.server, roomMembers.name, roomMembers.admin) + .add(roomID, userServer, data.name, false) + .query(db); + if (req.affectedRows > 0) aws("ok", ""); + else aws("error", "existence"); } -} +}; + + export const kickMember: Act = { state: STATE.client | STATE.remote, @@ -525,7 +559,8 @@ export const leaveRoom: Act = { await remove(remoteRooms) .where(and( eq(client.accID, remoteRooms.accID), - eq(data.server, remoteRooms.server) + eq(data.server, remoteRooms.server), + eq(data.room, remoteRooms.room), )).query(db); } aws(resp.state, resp.data); @@ -572,6 +607,7 @@ export const setVisibility: Act = { aws(resp.state, resp.data); return; } + if (!([0, 1, 2]).includes(data.visibility)) return void aws("error", "data"); let roomID = await client.isRoomAdmin(data.room, ROOM_RIGHTS.OTA); if (roomID == -1) return void aws("error", "roomAdmin"); let req = await update(rooms) diff --git a/src/api/acts/server.ts b/src/api/acts/server.ts new file mode 100644 index 0000000..c74a197 --- /dev/null +++ b/src/api/acts/server.ts @@ -0,0 +1,31 @@ +import { eq, insert, select } from "dblang"; +import { checkSelfTag } from "../../server/outbagURL.js"; +import { accounts, db, remoteRooms } from "../../sys/db.js"; +import { Act, Client, STATE } from "../user.js"; + +export const invite: Act = { + state: STATE.server, + right: 0, + data: { + room: "name-100", + //roomServer: "string", + name: "string", + server: "string", + }, + func: async (client: Client, data: any, aws: (code: string, data: any) => void) => { + if (!checkSelfTag(data.server)) return void aws("error", "existence"); + let req = await select([accounts.accID], accounts) + .where(eq(accounts.name, data.name)) + .query(db); + if (req.length == 0) { + client.suspect(); + aws("error", "existence"); + return; + } + let query = await insert(remoteRooms.accID, remoteRooms.server, remoteRooms.rooms, remoteRooms.confirmed) + .add(req[0][accounts.accID], client.server.tag, data.room, false) + .query(db); + if (query.affectedRows > 0) aws("ok", ""); + else aws("error", "existence"); + } +}; \ No newline at end of file diff --git a/src/api/server.ts b/src/api/server.ts index 741e7ea..50a98c0 100644 --- a/src/api/server.ts +++ b/src/api/server.ts @@ -80,7 +80,39 @@ export const fetchRemoteAs = async (server: outbagServer, name: string, act: str data: "remote" } } -} +}; + +export const fetchRemoteAsServer = async (server: outbagServer, act: string, data: any) => { + try { + let token = await getServerToken(server); + if (token === false) throw new Error("remote"); + let resp = await sendPost( + server, + { "authorization": `Bearer ${token}` }, + act, + data + ); + if (resp.state != "error" || resp.data != "token") return resp; + token = await getServerToken(server, true); + if (token === false) throw new Error("remote"); + resp = await sendPost( + server, + { "authorization": `Bearer ${token}` }, + act, + data + ); + if (resp.state == "error" && resp.data == "token") return { + state: "error", + data: "remote" + } + return resp; + } catch (error) { + return { + state: "error", + data: "remote" + } + } +}; let cancleClear = setInterval(() => { let keys = Object.keys(remoteTempTokens); diff --git a/src/sys/db.ts b/src/sys/db.ts index d31ed3f..b5b9607 100644 --- a/src/sys/db.ts +++ b/src/sys/db.ts @@ -78,26 +78,15 @@ remoteRooms.addAttributes({ server: { type: VARCHAR(256), primaryKey: true, - } -}); - -export const invitations = db.newTable("invitations"); -invitations.addAttributes({ - invitationID: { type: INT, primaryKey: true, autoIncrement: true }, - accID: { - type: INT, - foreignKey: { - link: accounts.accID, - onDelete: onAction.cascade, - onUpdate: onAction.cascade - } }, - room: { type: VARCHAR(256) }, - server: { type: VARCHAR(256) }, - ota: { type: VARCHAR(128) }, - expires: { type: BIGINT }, - fromName: { type: VARCHAR(256) }, - fromServer: { type: VARCHAR(256) }, + room: { + type: VARCHAR(256), + primaryKey: true, + }, + confirmed: { + type: BOOL, + default: false + } }); export const settings = db.newTable("settings"); @@ -132,7 +121,7 @@ rooms.addAttributes({ } }, rights: { type: INT, default: 0b11111 }, - visibility: { type: BOOL, default: 0 }, + visibility: { type: SMALLINT, default: 0 }, title: { type: TEXT, default: "" }, description: { type: TEXT, default: "" }, icon: { type: TEXT, default: "" } @@ -151,7 +140,8 @@ roomMembers.addAttributes({ }, name: { type: VARCHAR(256) }, server: { type: VARCHAR(256) }, - admin: { type: BOOL, default: false } + admin: { type: BOOL, default: false }, + confirmed: { type: BOOL, default: false } }); roomMembers.addConstraint(uniqueKey([ roomMembers.roomID, @@ -177,7 +167,6 @@ roomOTAs.addAttributes({ }, expires: { type: BIGINT }, usesLeft: { type: INT }, - isInvitation: { type: BOOL }, }); export const listCategories = db.newTable("listCategories"); diff --git a/tests/tests/post.js b/tests/tests/post.js index d3482a9..f6e7518 100644 --- a/tests/tests/post.js +++ b/tests/tests/post.js @@ -193,7 +193,8 @@ const list = [ description: "some desc", visibility: 0, icon: "shopping", - debug: true + debug: true, + confirmed: true }, { name: room2, server: "localhost:7224", @@ -203,7 +204,8 @@ const list = [ description: "some desc 2", visibility: 1, icon: "", - debug: true + debug: true, + confirmed: true } ]); await req({ @@ -239,7 +241,8 @@ const list = [ description: "some desc", visibility: 0, icon: "shopping", - debug: true + debug: true, + confirmed: true } ]); }] diff --git a/tests/tests/ws.js b/tests/tests/ws.js index e790331..d07a813 100644 --- a/tests/tests/ws.js +++ b/tests/tests/ws.js @@ -187,7 +187,8 @@ const list = [ description: "some desc", visibility: 0, icon: "shopping", - debug: true + debug: true, + confirmed: true }, { name: room2, server: "localhost:7224", @@ -197,7 +198,8 @@ const list = [ description: "some desc 2", visibility: 1, icon: "", - debug: true + debug: true, + confirmed: true } ]); await req(handler, "deleteRoom", { @@ -222,7 +224,8 @@ const list = [ description: "some desc", visibility: 0, icon: "shopping", - debug: true + debug: true, + confirmed: true } ]);