From 9798ecc75709d62a82efe9097bafdff945d2078d Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Fri, 6 Sep 2024 23:10:40 +0900 Subject: [PATCH 1/7] Add: add ban features into event code --- src/lottery/middlewares/eventValidator.js | 32 ++++++++++++++++ src/lottery/modules/contracts.js | 45 ++++++++++++++--------- src/lottery/modules/quests.js | 10 ++++- src/lottery/routes/invites.js | 3 +- src/lottery/routes/items.js | 3 +- src/lottery/routes/quests.js | 3 +- src/lottery/routes/transactions.js | 3 +- src/lottery/services/globalState.js | 18 ++++++--- src/lottery/services/invites.js | 7 +++- src/lottery/services/items.js | 1 + src/middlewares/ban.js | 10 +++-- src/modules/stores/mongo.js | 2 +- src/services/notifications.js | 1 + src/services/rooms.js | 8 +++- src/services/users.js | 2 + 15 files changed, 113 insertions(+), 35 deletions(-) create mode 100644 src/lottery/middlewares/eventValidator.js diff --git a/src/lottery/middlewares/eventValidator.js b/src/lottery/middlewares/eventValidator.js new file mode 100644 index 00000000..0d5975ce --- /dev/null +++ b/src/lottery/middlewares/eventValidator.js @@ -0,0 +1,32 @@ +const { eventStatusModel } = require("../modules/stores/mongo"); +const logger = require("../../modules/logger"); + +/** + * 사용자가 차단 되었는지 여부를 판단합니다. + * 차단된 사용자는 이벤트에 한하여 서비스 이용에 제재를 받습니다. + * @param {*} req eventStatus가 성공적일 경우 req.eventStatus = eventStatus로 들어갑니다. + * @param {*} res + * @param {*} next + * @returns + */ +const eventValidator = async (req, res, next) => { + try { + const eventStatus = await eventStatusModel + .findOne({ userId: req.userOid }) + .lean(); + if (!eventStatus) { + return res + .status(400) + .json({ error: "eventValidator: nonexistent eventStatus" }); + } + req.eventStatus = eventStatus; + } catch (err) { + logger.error(err); + res.error(500).json({ + error: "eventValidator: internal server error", + }); + } + next(); +}; + +module.exports = eventValidator; diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index a415b0af..ece0b274 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -109,8 +109,8 @@ const quests = buildQuests({ * @returns {Promise} * @usage lottery/globalState - createUserGlobalStateHandler */ -const completeFirstLoginQuest = async (userId, timestamp) => { - return await completeQuest(userId, timestamp, quests.firstLogin); +const completeFirstLoginQuest = async (req, userId, timestamp) => { + return await completeQuest(req, userId, timestamp, quests.firstLogin); }; /** @@ -121,8 +121,8 @@ const completeFirstLoginQuest = async (userId, timestamp) => { * @description 방을 만들 때마다 호출해 주세요. * @usage rooms - createHandler */ -const completeFirstRoomCreationQuest = async (userId, timestamp) => { - return await completeQuest(userId, timestamp, quests.firstRoomCreation); +const completeFirstRoomCreationQuest = async (req, userId, timestamp) => { + return await completeQuest(req, userId, timestamp, quests.firstRoomCreation); }; /** @@ -137,7 +137,12 @@ const completeFirstRoomCreationQuest = async (userId, timestamp) => { * @description 정산 요청이 이루어질 때마다 호출해 주세요. * @usage rooms - commitSettlementHandler */ -const completeFareSettlementQuest = async (userId, timestamp, roomObject) => { +const completeFareSettlementQuest = async ( + req, + userId, + timestamp, + roomObject +) => { logger.info( `User ${userId} requested to complete fareSettlementQuest in Room ${roomObject._id}` ); @@ -150,7 +155,7 @@ const completeFareSettlementQuest = async (userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(userId, timestamp, quests.fareSettlement); + return await completeQuest(req, userId, timestamp, quests.fareSettlement); }; /** @@ -165,7 +170,7 @@ const completeFareSettlementQuest = async (userId, timestamp, roomObject) => { * @description 송금이 이루어질 때마다 호출해 주세요. * @usage rooms - commitPaymentHandler */ -const completeFarePaymentQuest = async (userId, timestamp, roomObject) => { +const completeFarePaymentQuest = async (req, userId, timestamp, roomObject) => { logger.info( `User ${userId} requested to complete farePaymentQuest in Room ${roomObject._id}` ); @@ -178,7 +183,7 @@ const completeFarePaymentQuest = async (userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(userId, timestamp, quests.farePayment); + return await completeQuest(req, userId, timestamp, quests.farePayment); }; /** @@ -189,8 +194,8 @@ const completeFarePaymentQuest = async (userId, timestamp, roomObject) => { * @description 닉네임을 변경할 때마다 호출해 주세요. * @usage users - editNicknameHandler */ -const completeNicknameChangingQuest = async (userId, timestamp) => { - return await completeQuest(userId, timestamp, quests.nicknameChanging); +const completeNicknameChangingQuest = async (req, userId, timestamp) => { + return await completeQuest(req, userId, timestamp, quests.nicknameChanging); }; /** @@ -202,10 +207,15 @@ const completeNicknameChangingQuest = async (userId, timestamp) => { * @description 계좌를 변경할 때마다 호출해 주세요. * @usage users - editAccountHandler */ -const completeAccountChangingQuest = async (userId, timestamp, newAccount) => { +const completeAccountChangingQuest = async ( + req, + userId, + timestamp, + newAccount +) => { if (newAccount === "") return null; - return await completeQuest(userId, timestamp, quests.accountChanging); + return await completeQuest(req, userId, timestamp, quests.accountChanging); }; /** @@ -218,13 +228,14 @@ const completeAccountChangingQuest = async (userId, timestamp, newAccount) => { * @usage notifications - editOptionsHandler */ const completeAdPushAgreementQuest = async ( + req, userId, timestamp, advertisement ) => { if (!advertisement) return null; - return await completeQuest(userId, timestamp, quests.adPushAgreement); + return await completeQuest(req, userId, timestamp, quests.adPushAgreement); }; /** @@ -234,8 +245,8 @@ const completeAdPushAgreementQuest = async ( * @returns {Promise} * @usage lottery/globalState - createUserGlobalStateHandler */ -const completeEventSharingQuest = async (userId, timestamp) => { - return await completeQuest(userId, timestamp, quests.eventSharing); +const completeEventSharingQuest = async (req, userId, timestamp) => { + return await completeQuest(req, userId, timestamp, quests.eventSharing); }; /** @@ -245,8 +256,8 @@ const completeEventSharingQuest = async (userId, timestamp) => { * @returns {Promise} * @description 상품을 구입할 때마다 호출해 주세요. */ -const completeItemPurchaseQuest = async (userId, timestamp) => { - return await completeQuest(userId, timestamp, quests.itemPurchase); +const completeItemPurchaseQuest = async (req, userId, timestamp) => { + return await completeQuest(req, userId, timestamp, quests.itemPurchase); }; module.exports = { diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 0d79ac81..2813ba45 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -8,6 +8,7 @@ const logger = require("../../modules/logger"); const mongoose = require("mongoose"); const { eventConfig } = require("../../../loadenv"); +const { validateServiceBanRecord } = require("../../modules/ban"); const eventPeriod = eventConfig && { startAt: new Date(eventConfig.period.startAt), endAt: new Date(eventConfig.period.endAt), @@ -64,11 +65,16 @@ const buildQuests = (quests) => { * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다. * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화된 경우에도 실패로 처리됩니다. */ -const completeQuest = async (userId, timestamp, quest) => { +const completeQuest = async (req, userId, timestamp, quest) => { try { // 1단계: 유저의 EventStatus를 가져옵니다. 블록드리스트인지도 확인합니다. const eventStatus = await eventStatusModel.findOne({ userId }).lean(); - if (!eventStatus || eventStatus.isBanned) return null; + const banErrorMessage = await validateServiceBanRecord( + req, + eventConfig.mode + ); + + if (!eventStatus || !!banErrorMessage) return null; // 2단계: 이벤트 기간인지 확인합니다. if ( diff --git a/src/lottery/routes/invites.js b/src/lottery/routes/invites.js index 65e4271e..ec576160 100644 --- a/src/lottery/routes/invites.js +++ b/src/lottery/routes/invites.js @@ -13,7 +13,8 @@ router.get( // 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 router.use(require("../../middlewares/auth")); -router.use(require("../middlewares/checkBanned")); +router.use(require("../../middlewares/ban")); +router.use(require("../middlewares/eventValidator")); router.use(require("../middlewares/timestampValidator")); router.post("/create", invitesHandlers.createInviteUrlHandler); diff --git a/src/lottery/routes/items.js b/src/lottery/routes/items.js index 0de8be18..970fc066 100644 --- a/src/lottery/routes/items.js +++ b/src/lottery/routes/items.js @@ -19,7 +19,8 @@ router.get( // 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 router.use(require("../../middlewares/auth")); -router.use(require("../middlewares/checkBanned")); +router.use(require("../../middlewares/ban")); +router.use(require("../middlewares/eventValidator")); router.use(require("../middlewares/timestampValidator")); router.post( diff --git a/src/lottery/routes/quests.js b/src/lottery/routes/quests.js index e9845434..8ce3b07b 100644 --- a/src/lottery/routes/quests.js +++ b/src/lottery/routes/quests.js @@ -7,7 +7,8 @@ const questsHandlers = require("../services/quests"); // 아래의 Endpoint 접근 시 로그인, 차단 여부 및 시각 체크 필요 router.use(require("../../middlewares/auth")); -router.use(require("../middlewares/checkBanned")); +router.use(require("../../middlewares/ban")); +router.use(require("../middlewares/eventValidator")); router.use(require("../middlewares/timestampValidator")); router.post( diff --git a/src/lottery/routes/transactions.js b/src/lottery/routes/transactions.js index f9e375ca..34360ed8 100644 --- a/src/lottery/routes/transactions.js +++ b/src/lottery/routes/transactions.js @@ -3,8 +3,9 @@ const router = express.Router(); const transactionsHandlers = require("../services/transactions"); -// 아래의 Endpoint 접근 시 로그인 필요 +// 아래의 Endpoint 접근 시 로그인 체크 필요 router.use(require("../../middlewares/auth")); +router.use(require("../middlewares/eventValidator")); router.get("/", transactionsHandlers.getUserTransactionsHandler); diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index ee01f47e..095fdbc5 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -6,6 +6,7 @@ const { nodeEnv } = require("../../../loadenv"); const { eventConfig } = require("../../../loadenv"); const contracts = require("../modules/contracts"); +const { validateServiceBanRecord } = require("../../modules/ban"); const quests = Object.values(contracts.quests); // 아래의 함수는 2024 추석 이벤트에서 사용되지 않습니다. @@ -85,11 +86,13 @@ const createUserGlobalStateHandler = async (req, res) => { const inviterStatus = req.body.inviter && (await eventStatusModel.findById(req.body.inviter).lean()); + const banErrorMessage = await validateServiceBanRecord( + req, + eventConfig.mode + ); if ( req.body.inviter && - (!inviterStatus || - inviterStatus.isBanned || - !inviterStatus.isInviteUrlEnabled) + (!inviterStatus || !!banErrorMessage || !inviterStatus.isInviteUrlEnabled) ) return res.status(400).json({ error: "GlobalState/create : invalid inviter", @@ -128,11 +131,16 @@ const createUserGlobalStateHandler = async (req, res) => { await eventStatus.save(); // 퀘스트를 완료 처리합니다. - await contracts.completeFirstLoginQuest(req.userOid, req.timestamp); + await contracts.completeFirstLoginQuest(req, req.userOid, req.timestamp); if (inviterStatus) { - await contracts.completeEventSharingQuest(req.userOid, req.timestamp); await contracts.completeEventSharingQuest( + req, + req.userOid, + req.timestamp + ); + await contracts.completeEventSharingQuest( + req, inviterStatus.userId, req.timestamp ); diff --git a/src/lottery/services/invites.js b/src/lottery/services/invites.js index 6479bce8..c0fc3012 100644 --- a/src/lottery/services/invites.js +++ b/src/lottery/services/invites.js @@ -3,6 +3,7 @@ const { userModel } = require("../../modules/stores/mongo"); const logger = require("../../modules/logger"); const { eventConfig } = require("../../../loadenv"); +const { validateServiceBanRecord } = require("../../modules/ban"); const searchInviterHandler = async (req, res) => { try { @@ -13,9 +14,13 @@ const searchInviterHandler = async (req, res) => { const inviterStatus = await eventStatusModel .findById(req.params.inviter) .lean(); + const banErrorMessage = await validateServiceBanRecord( + req, + eventConfig.mode + ); if ( !inviterStatus || - inviterStatus.isBanned || + !!banErrorMessage || !inviterStatus.isInviteUrlEnabled ) return res diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index eb00535d..fcad766f 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -309,6 +309,7 @@ const purchaseItemHandler = async (req, res) => { // 4단계: 퀘스트를 완료 처리합니다. await contracts.completeItemPurchaseQuest( + req, req.userOid, transaction.createdAt ); diff --git a/src/middlewares/ban.js b/src/middlewares/ban.js index f70b9550..19e19cdd 100644 --- a/src/middlewares/ban.js +++ b/src/middlewares/ban.js @@ -1,3 +1,4 @@ +const { eventConfig } = require("../../loadenv"); const { validateServiceBanRecord } = require("../modules/ban"); const serviceMapper = new Map([ @@ -6,10 +7,11 @@ const serviceMapper = new Map([ ]); const banMiddleware = async (req, res, next) => { - const banErrorMessage = await validateServiceBanRecord( - req, - serviceMapper.get(req.originalUrl) - ); + let service = serviceMapper.get(req.originalUrl); + if (!service && !!eventConfig && req.originalUrl.includes(eventConfig.mode)) { + service = eventConfig.mode; + } + const banErrorMessage = await validateServiceBanRecord(req, service); if (banErrorMessage !== undefined) { return res.status(400).json({ error: banErrorMessage }); } diff --git a/src/modules/stores/mongo.js b/src/modules/stores/mongo.js index f236b829..fd33ba16 100755 --- a/src/modules/stores/mongo.js +++ b/src/modules/stores/mongo.js @@ -40,7 +40,7 @@ const banSchema = Schema({ // 필요시 이곳에 정지를 시킬 서비스를 추가함. enum: [ "service", // service: 방 생성/참여 제한 - "2023-fall-event", // xxxx-xxxx-event: 특정 이벤트 참여 제한 + "2024fall", // 2024fall: 가을학기 추석 이벤트 참여 제한 ], }, }); diff --git a/src/services/notifications.js b/src/services/notifications.js index 633f6739..d95fe162 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -109,6 +109,7 @@ const editOptionsHandler = async (req, res) => { // 이벤트 코드입니다. await contracts?.completeAdPushAgreementQuest( + req, req.userOid, req.timestamp, options.advertisement diff --git a/src/services/rooms.js b/src/services/rooms.js index 0ef798fe..da6f61a0 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -110,7 +110,11 @@ const createHandler = async (req, res) => { const roomObjectFormated = formatSettlement(roomObject); // 이벤트 코드입니다. - await contracts?.completeFirstRoomCreationQuest(req.userOid, req.timestamp); + await contracts?.completeFirstRoomCreationQuest( + req, + req.userOid, + req.timestamp + ); return res.send(roomObjectFormated); } catch (err) { @@ -587,6 +591,7 @@ const commitSettlementHandler = async (req, res) => { // 이벤트 코드입니다. await contracts?.completeFareSettlementQuest( + req, req.userOid, req.timestamp, roomObject @@ -660,6 +665,7 @@ const commitPaymentHandler = async (req, res) => { // 이벤트 코드입니다. await contracts?.completeFarePaymentQuest( + req, req.userOid, req.timestamp, roomObject diff --git a/src/services/users.js b/src/services/users.js index 8c51c736..68d1c505 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -53,6 +53,7 @@ const editNicknameHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. await contracts?.completeNicknameChangingQuest( + req, req.userOid, req.timestamp ); @@ -80,6 +81,7 @@ const editAccountHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. await contracts?.completeAccountChangingQuest( + req, req.userOid, req.timestamp, newAccount From b9efeeb4f409c52ca041ddeb6de48543d65dac24 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 25 Sep 2024 00:13:17 +0900 Subject: [PATCH 2/7] Add: resolve comments --- src/lottery/middlewares/eventValidator.js | 2 +- src/lottery/modules/contracts.js | 90 +++++++++++++++++------ src/lottery/modules/quests.js | 6 +- src/lottery/services/globalState.js | 25 +++++-- src/lottery/services/invites.js | 4 +- src/lottery/services/items.js | 4 +- src/middlewares/ban.js | 7 +- src/modules/ban.js | 15 ++-- src/services/notifications.js | 5 +- src/services/rooms.js | 17 +++-- src/services/users.js | 12 +-- 11 files changed, 132 insertions(+), 55 deletions(-) diff --git a/src/lottery/middlewares/eventValidator.js b/src/lottery/middlewares/eventValidator.js index 0d5975ce..bbff1c99 100644 --- a/src/lottery/middlewares/eventValidator.js +++ b/src/lottery/middlewares/eventValidator.js @@ -20,13 +20,13 @@ const eventValidator = async (req, res, next) => { .json({ error: "eventValidator: nonexistent eventStatus" }); } req.eventStatus = eventStatus; + next(); } catch (err) { logger.error(err); res.error(500).json({ error: "eventValidator: internal server error", }); } - next(); }; module.exports = eventValidator; diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index ece0b274..596e1414 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -104,29 +104,38 @@ const quests = buildQuests({ /** * firstLogin 퀘스트의 완료를 요청합니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @usage lottery/globalState - createUserGlobalStateHandler */ -const completeFirstLoginQuest = async (req, userId, timestamp) => { - return await completeQuest(req, userId, timestamp, quests.firstLogin); +const completeFirstLoginQuest = async (sid, timestamp, url, userId) => { + return await completeQuest(sid, timestamp, url, userId, quests.firstLogin); }; /** * firstRoomCreation 퀘스트의 완료를 요청합니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @description 방을 만들 때마다 호출해 주세요. * @usage rooms - createHandler */ -const completeFirstRoomCreationQuest = async (req, userId, timestamp) => { - return await completeQuest(req, userId, timestamp, quests.firstRoomCreation); +const completeFirstRoomCreationQuest = async (sid, timestamp, url, userId) => { + return await completeQuest( + sid, + timestamp, + url, + userId, + quests.firstRoomCreation + ); }; /** * fareSettlement 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. @@ -138,9 +147,10 @@ const completeFirstRoomCreationQuest = async (req, userId, timestamp) => { * @usage rooms - commitSettlementHandler */ const completeFareSettlementQuest = async ( - req, - userId, + sid, timestamp, + url, + userId, roomObject ) => { logger.info( @@ -155,11 +165,18 @@ const completeFareSettlementQuest = async ( ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(req, userId, timestamp, quests.fareSettlement); + return await completeQuest( + sid, + timestamp, + url, + userId, + quests.fareSettlement + ); }; /** * farePayment 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {Object} roomObject - 방의 정보입니다. @@ -170,7 +187,13 @@ const completeFareSettlementQuest = async ( * @description 송금이 이루어질 때마다 호출해 주세요. * @usage rooms - commitPaymentHandler */ -const completeFarePaymentQuest = async (req, userId, timestamp, roomObject) => { +const completeFarePaymentQuest = async ( + sid, + timestamp, + url, + userId, + roomObject +) => { logger.info( `User ${userId} requested to complete farePaymentQuest in Room ${roomObject._id}` ); @@ -183,23 +206,31 @@ const completeFarePaymentQuest = async (req, userId, timestamp, roomObject) => { ) return null; // 택시 출발 시각이 이벤트 기간 내에 포함되지 않는 경우 퀘스트 완료 요청을 하지 않습니다. - return await completeQuest(req, userId, timestamp, quests.farePayment); + return await completeQuest(sid, timestamp, url, userId, quests.farePayment); }; /** * nicknameChanging 퀘스트의 완료를 요청합니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @description 닉네임을 변경할 때마다 호출해 주세요. * @usage users - editNicknameHandler */ -const completeNicknameChangingQuest = async (req, userId, timestamp) => { - return await completeQuest(req, userId, timestamp, quests.nicknameChanging); +const completeNicknameChangingQuest = async (sid, timestamp, url, userId) => { + return await completeQuest( + sid, + timestamp, + url, + userId, + quests.nicknameChanging + ); }; /** * accountChanging 퀘스트의 완료를 요청합니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {string} newAccount - 변경된 계좌입니다. @@ -208,18 +239,26 @@ const completeNicknameChangingQuest = async (req, userId, timestamp) => { * @usage users - editAccountHandler */ const completeAccountChangingQuest = async ( - req, - userId, + sid, timestamp, + url, + userId, newAccount ) => { if (newAccount === "") return null; - return await completeQuest(req, userId, timestamp, quests.accountChanging); + return await completeQuest( + sid, + timestamp, + url, + userId, + quests.accountChanging + ); }; /** * adPushAgreement 퀘스트의 완료를 요청합니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다. @@ -228,36 +267,45 @@ const completeAccountChangingQuest = async ( * @usage notifications - editOptionsHandler */ const completeAdPushAgreementQuest = async ( - req, - userId, + sid, timestamp, + url, + userId, advertisement ) => { if (!advertisement) return null; - return await completeQuest(req, userId, timestamp, quests.adPushAgreement); + return await completeQuest( + sid, + timestamp, + url, + userId, + quests.adPushAgreement + ); }; /** * eventSharing 퀘스트의 완료를 요청합니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @usage lottery/globalState - createUserGlobalStateHandler */ -const completeEventSharingQuest = async (req, userId, timestamp) => { - return await completeQuest(req, userId, timestamp, quests.eventSharing); +const completeEventSharingQuest = async (sid, timestamp, url, userId) => { + return await completeQuest(sid, timestamp, url, userId, quests.eventSharing); }; /** * itemPurchase 퀘스트의 완료를 요청합니다. + * @param {Object} req - request 객체입니다. * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. * @returns {Promise} * @description 상품을 구입할 때마다 호출해 주세요. */ -const completeItemPurchaseQuest = async (req, userId, timestamp) => { - return await completeQuest(req, userId, timestamp, quests.itemPurchase); +const completeItemPurchaseQuest = async (sid, timestamp, url, userId) => { + return await completeQuest(sid, timestamp, url, userId, quests.itemPurchase); }; module.exports = { diff --git a/src/lottery/modules/quests.js b/src/lottery/modules/quests.js index 2813ba45..a11c2623 100644 --- a/src/lottery/modules/quests.js +++ b/src/lottery/modules/quests.js @@ -65,12 +65,14 @@ const buildQuests = (quests) => { * @param {number} quest.maxCount - 퀘스트의 최대 완료 가능 횟수입니다. * @returns {Object|null} 성공한 경우 Object를, 실패한 경우 null을 반환합니다. 이미 최대 완료 횟수에 도달했거나, 퀘스트가 원격으로 비활성화된 경우에도 실패로 처리됩니다. */ -const completeQuest = async (req, userId, timestamp, quest) => { +const completeQuest = async (sid, timestamp, url, userId, quest) => { try { // 1단계: 유저의 EventStatus를 가져옵니다. 블록드리스트인지도 확인합니다. const eventStatus = await eventStatusModel.findOne({ userId }).lean(); const banErrorMessage = await validateServiceBanRecord( - req, + sid, + timestamp, + url, eventConfig.mode ); diff --git a/src/lottery/services/globalState.js b/src/lottery/services/globalState.js index 095fdbc5..94859b00 100644 --- a/src/lottery/services/globalState.js +++ b/src/lottery/services/globalState.js @@ -87,7 +87,9 @@ const createUserGlobalStateHandler = async (req, res) => { req.body.inviter && (await eventStatusModel.findById(req.body.inviter).lean()); const banErrorMessage = await validateServiceBanRecord( - req, + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, eventConfig.mode ); if ( @@ -131,18 +133,25 @@ const createUserGlobalStateHandler = async (req, res) => { await eventStatus.save(); // 퀘스트를 완료 처리합니다. - await contracts.completeFirstLoginQuest(req, req.userOid, req.timestamp); + await contracts.completeFirstLoginQuest( + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, + req.userOid + ); if (inviterStatus) { await contracts.completeEventSharingQuest( - req, - req.userOid, - req.timestamp + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, + req.userOid ); await contracts.completeEventSharingQuest( - req, - inviterStatus.userId, - req.timestamp + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, + inviterStatus.userId ); } diff --git a/src/lottery/services/invites.js b/src/lottery/services/invites.js index c0fc3012..27fe2479 100644 --- a/src/lottery/services/invites.js +++ b/src/lottery/services/invites.js @@ -15,7 +15,9 @@ const searchInviterHandler = async (req, res) => { .findById(req.params.inviter) .lean(); const banErrorMessage = await validateServiceBanRecord( - req, + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, eventConfig.mode ); if ( diff --git a/src/lottery/services/items.js b/src/lottery/services/items.js index fcad766f..0bc89c93 100644 --- a/src/lottery/services/items.js +++ b/src/lottery/services/items.js @@ -309,7 +309,9 @@ const purchaseItemHandler = async (req, res) => { // 4단계: 퀘스트를 완료 처리합니다. await contracts.completeItemPurchaseQuest( - req, + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, req.userOid, transaction.createdAt ); diff --git a/src/middlewares/ban.js b/src/middlewares/ban.js index 19e19cdd..3a71a91c 100644 --- a/src/middlewares/ban.js +++ b/src/middlewares/ban.js @@ -11,7 +11,12 @@ const banMiddleware = async (req, res, next) => { if (!service && !!eventConfig && req.originalUrl.includes(eventConfig.mode)) { service = eventConfig.mode; } - const banErrorMessage = await validateServiceBanRecord(req, service); + const banErrorMessage = await validateServiceBanRecord( + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, + service + ); if (banErrorMessage !== undefined) { return res.status(400).json({ error: banErrorMessage }); } diff --git a/src/modules/ban.js b/src/modules/ban.js index 4068db78..c8db3d6b 100644 --- a/src/modules/ban.js +++ b/src/modules/ban.js @@ -2,19 +2,22 @@ const logger = require("./logger"); const { banModel } = require("./stores/mongo"); /** - * @param {*} req - * @param {String} service + * @param {string} sid + * @param {number} timestamp + * @param {string} url + * @param {string} service + * @returns {string} */ -const validateServiceBanRecord = async (req, service) => { +const validateServiceBanRecord = async (sid, timestamp, url, service) => { let banRecord = undefined; try { // 현재 시각이 expireAt 보다 작고, 본인인 경우(ban의 userId가 userId랑 같은 경우) 중 serviceName이 "service"인 record를 모두 가져옴 const bans = await banModel .find({ - userSid: req.session.loginInfo.sid, + userSid: sid, expireAt: { - $gte: req.timestamp, + $gte: timestamp, }, serviceName: service, }) @@ -34,7 +37,7 @@ const validateServiceBanRecord = async (req, service) => { .toISOString() .replace("T", " ") .split(".")[0]; - const banErrorMessage = `${req.originalUrl} : user ${req.userId} (${req.session.loginInfo.sid}) is temporarily restricted from service until ${formattedExpireAt}.`; + const banErrorMessage = `${url} : ${sid} is temporarily restricted from service until ${formattedExpireAt}.`; return banErrorMessage; } return; diff --git a/src/services/notifications.js b/src/services/notifications.js index d95fe162..9a57c831 100644 --- a/src/services/notifications.js +++ b/src/services/notifications.js @@ -109,9 +109,10 @@ const editOptionsHandler = async (req, res) => { // 이벤트 코드입니다. await contracts?.completeAdPushAgreementQuest( - req, - req.userOid, + req.session.loginInfo.sid, req.timestamp, + req.originalUrl, + req.userOid, options.advertisement ); diff --git a/src/services/rooms.js b/src/services/rooms.js index da6f61a0..fa05546f 100644 --- a/src/services/rooms.js +++ b/src/services/rooms.js @@ -111,9 +111,10 @@ const createHandler = async (req, res) => { // 이벤트 코드입니다. await contracts?.completeFirstRoomCreationQuest( - req, - req.userOid, - req.timestamp + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, + req.userOid ); return res.send(roomObjectFormated); @@ -591,9 +592,10 @@ const commitSettlementHandler = async (req, res) => { // 이벤트 코드입니다. await contracts?.completeFareSettlementQuest( - req, - req.userOid, + req.session.loginInfo.sid, req.timestamp, + req.originalUrl, + req.userOid, roomObject ); @@ -665,9 +667,10 @@ const commitPaymentHandler = async (req, res) => { // 이벤트 코드입니다. await contracts?.completeFarePaymentQuest( - req, - req.userOid, + req.session.loginInfo.sid, req.timestamp, + req.originalUrl, + req.userOid, roomObject ); diff --git a/src/services/users.js b/src/services/users.js index 68d1c505..a252dd86 100644 --- a/src/services/users.js +++ b/src/services/users.js @@ -53,9 +53,10 @@ const editNicknameHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. await contracts?.completeNicknameChangingQuest( - req, - req.userOid, - req.timestamp + req.session.loginInfo.sid, + req.timestamp, + req.originalUrl, + req.userOid ); res @@ -81,9 +82,10 @@ const editAccountHandler = async (req, res) => { if (result) { // 이벤트 코드입니다. await contracts?.completeAccountChangingQuest( - req, - req.userOid, + req.session.loginInfo.sid, req.timestamp, + req.originalUrl, + req.userOid, newAccount ); From 793157c7759153550ec2370e5aca3c1f4ba8bccc Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 25 Sep 2024 00:19:17 +0900 Subject: [PATCH 3/7] Add: resolve comments --- src/lottery/modules/contracts.js | 45 +++++++++++++++++++------------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/lottery/modules/contracts.js b/src/lottery/modules/contracts.js index 596e1414..bfdd1e54 100644 --- a/src/lottery/modules/contracts.js +++ b/src/lottery/modules/contracts.js @@ -104,9 +104,10 @@ const quests = buildQuests({ /** * firstLogin 퀘스트의 완료를 요청합니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @returns {Promise} * @usage lottery/globalState - createUserGlobalStateHandler */ @@ -116,9 +117,10 @@ const completeFirstLoginQuest = async (sid, timestamp, url, userId) => { /** * firstRoomCreation 퀘스트의 완료를 요청합니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @returns {Promise} * @description 방을 만들 때마다 호출해 주세요. * @usage rooms - createHandler @@ -135,9 +137,10 @@ const completeFirstRoomCreationQuest = async (sid, timestamp, url, userId) => { /** * fareSettlement 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {Object} roomObject - 방의 정보입니다. * @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. @@ -176,9 +179,10 @@ const completeFareSettlementQuest = async ( /** * farePayment 퀘스트의 완료를 요청합니다. 방의 참가자 수가 2명 미만이면 요청하지 않습니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {Object} roomObject - 방의 정보입니다. * @param {mongoose.Types.ObjectId} roomObject._id - 방의 ObjectId입니다. * @param {Array<{ user: mongoose.Types.ObjectId }>} roomObject.part - 참여자 목록입니다. @@ -211,9 +215,10 @@ const completeFarePaymentQuest = async ( /** * nicknameChanging 퀘스트의 완료를 요청합니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @returns {Promise} * @description 닉네임을 변경할 때마다 호출해 주세요. * @usage users - editNicknameHandler @@ -230,9 +235,10 @@ const completeNicknameChangingQuest = async (sid, timestamp, url, userId) => { /** * accountChanging 퀘스트의 완료를 요청합니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {string} newAccount - 변경된 계좌입니다. * @returns {Promise} * @description 계좌를 변경할 때마다 호출해 주세요. @@ -258,9 +264,10 @@ const completeAccountChangingQuest = async ( /** * adPushAgreement 퀘스트의 완료를 요청합니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @param {boolean} advertisement - 변경된 광고성 알림 수신 동의 여부입니다. * @returns {Promise} * @description 알림 옵션을 변경할 때마다 호출해 주세요. @@ -286,9 +293,10 @@ const completeAdPushAgreementQuest = async ( /** * eventSharing 퀘스트의 완료를 요청합니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @returns {Promise} * @usage lottery/globalState - createUserGlobalStateHandler */ @@ -298,9 +306,10 @@ const completeEventSharingQuest = async (sid, timestamp, url, userId) => { /** * itemPurchase 퀘스트의 완료를 요청합니다. - * @param {Object} req - request 객체입니다. - * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. + * @param {string} sid - user의 sid입니다. * @param {number|Date} timestamp - 퀘스트 완료를 요청한 시각입니다. + * @param {string} url - 요청한 url입니다. + * @param {string|mongoose.Types.ObjectId} userId - 퀘스트를 완료한 사용자의 ObjectId입니다. * @returns {Promise} * @description 상품을 구입할 때마다 호출해 주세요. */ From 07d6c3b74b72e8f6410fb4d9b51444052a96051c Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 8 Oct 2024 22:24:22 +0900 Subject: [PATCH 4/7] Add: req.session.loginInfo.sid added to test --- test/services/rooms.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/test/services/rooms.js b/test/services/rooms.js index 4e948ad1..36d53b02 100644 --- a/test/services/rooms.js +++ b/test/services/rooms.js @@ -33,6 +33,11 @@ describe("[rooms] 1.createHandler", () => { }, userId: testUser1.id, app, + session: { + loginInfo: { + sid: testUser1.id, + }, + }, }); let res = httpMocks.createResponse(); await roomsHandlers.createHandler(req, res); @@ -89,6 +94,11 @@ describe("[rooms] 4.joinHandler", () => { }, userId: testUser2.id, app, + session: { + loginInfo: { + sid: testUser1.id, + }, + }, }); let res = httpMocks.createResponse(); await roomsHandlers.joinHandler(req, res); From 6f64cac68069e5339f034c0299fc679d11a3d742 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 8 Oct 2024 22:31:27 +0900 Subject: [PATCH 5/7] Add: req.session.loginInfo.sid added to test --- test/services/rooms.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/test/services/rooms.js b/test/services/rooms.js index 36d53b02..d1f24fe1 100644 --- a/test/services/rooms.js +++ b/test/services/rooms.js @@ -96,7 +96,7 @@ describe("[rooms] 4.joinHandler", () => { app, session: { loginInfo: { - sid: testUser1.id, + sid: testUser2.id, }, }, }); @@ -160,6 +160,11 @@ describe("[rooms] 7.commitSettlementHandler", () => { userId: testUser1.id, timestamp: Date.now() + 60 * 1000, app, + session: { + loginInfo: { + sid: testUser1.id, + }, + }, }); let res = httpMocks.createResponse(); await roomsHandlers.commitSettlementHandler(req, res); @@ -180,6 +185,11 @@ describe("[rooms] 8.commitPaymentHandler", () => { body: { roomId: testRoom._id }, userId: testUser2.id, app, + session: { + loginInfo: { + sid: testUser2.id, + }, + }, }); let res = httpMocks.createResponse(); await roomsHandlers.commitPaymentHandler(req, res); @@ -201,6 +211,11 @@ describe("[rooms] 9.abortHandler", () => { userId: testUser2.id, session: {}, app, + session: { + loginInfo: { + sid: testUser2.id, + }, + }, }); let res = httpMocks.createResponse(); await roomsHandlers.abortHandler(req, res); From a4ef34a06ba024beacf0253791ae1d5037973495 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Tue, 8 Oct 2024 22:56:51 +0900 Subject: [PATCH 6/7] Add: resolve error in test; invalid arguments error --- test/services/rooms.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/test/services/rooms.js b/test/services/rooms.js index d1f24fe1..ff351016 100644 --- a/test/services/rooms.js +++ b/test/services/rooms.js @@ -38,6 +38,9 @@ describe("[rooms] 1.createHandler", () => { sid: testUser1.id, }, }, + timestamp: Date.now(), + originalUrl: "test-url/rooms/create", + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await roomsHandlers.createHandler(req, res); @@ -99,6 +102,9 @@ describe("[rooms] 4.joinHandler", () => { sid: testUser2.id, }, }, + timestamp: Date.now(), + originalUrl: "test-url/rooms/join", + userOid: testUser2._id, }); let res = httpMocks.createResponse(); await roomsHandlers.joinHandler(req, res); @@ -165,6 +171,9 @@ describe("[rooms] 7.commitSettlementHandler", () => { sid: testUser1.id, }, }, + timestamp: Date.now(), + originalUrl: "test-url/rooms/commitSettlement", + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await roomsHandlers.commitSettlementHandler(req, res); @@ -190,6 +199,9 @@ describe("[rooms] 8.commitPaymentHandler", () => { sid: testUser2.id, }, }, + timestamp: Date.now(), + originalUrl: "test-url/rooms/commitPayment", + userOid: testUser2._id, }); let res = httpMocks.createResponse(); await roomsHandlers.commitPaymentHandler(req, res); @@ -216,6 +228,9 @@ describe("[rooms] 9.abortHandler", () => { sid: testUser2.id, }, }, + timestamp: Date.now(), + originalUrl: "test-url/rooms/abort", + userOid: testUser2._id, }); let res = httpMocks.createResponse(); await roomsHandlers.abortHandler(req, res); From fd4437af60312f52b5129e3db82ea19155cecd21 Mon Sep 17 00:00:00 2001 From: TaehyeonPark Date: Wed, 6 Nov 2024 00:25:09 +0900 Subject: [PATCH 7/7] Fix: mocha error fix --- test/services/rooms.js | 3 +-- test/services/users.js | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/test/services/rooms.js b/test/services/rooms.js index ff351016..b8e625aa 100644 --- a/test/services/rooms.js +++ b/test/services/rooms.js @@ -164,14 +164,13 @@ describe("[rooms] 7.commitSettlementHandler", () => { let req = httpMocks.createRequest({ body: { roomId: testRoom._id }, userId: testUser1.id, - timestamp: Date.now() + 60 * 1000, app, session: { loginInfo: { sid: testUser1.id, }, }, - timestamp: Date.now(), + timestamp: Date.now() + 60 * 1000, originalUrl: "test-url/rooms/commitSettlement", userOid: testUser1._id, }); diff --git a/test/services/users.js b/test/services/users.js index 4f2195da..a131bd46 100644 --- a/test/services/users.js +++ b/test/services/users.js @@ -56,6 +56,14 @@ describe("[users] 3.editNicknameHandler", () => { body: { nickname: testNickname, }, + session: { + loginInfo: { + sid: testUser1.id, + }, + }, + timestamp: Date.now(), + originalUrl: "test-url/users/editNickname", + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await usersHandlers.editNicknameHandler(req, res); @@ -83,6 +91,14 @@ describe("[users] 4.editAccountHandler", () => { body: { account: testAccount, }, + session: { + loginInfo: { + sid: testUser1.id, + }, + }, + timestamp: Date.now(), + originalUrl: "test-url/users/editAccount", + userOid: testUser1._id, }); let res = httpMocks.createResponse(); await usersHandlers.editAccountHandler(req, res);