diff --git a/Quests/IsekaiUserPointsQuests.alias.php b/Quests/IsekaiUserPointsQuests.alias.php new file mode 100644 index 0000000..a6bcc64 --- /dev/null +++ b/Quests/IsekaiUserPointsQuests.alias.php @@ -0,0 +1,2 @@ += 1.39.0" + }, + "MessagesDirs": { + "IsekaiUserPointsQuests": [ + "i18n" + ] + }, + "ExtensionMessagesFiles": { + "IsekaiUserPointsQuestsAlias": "IsekaiUserPointsQuests.alias.php" + }, + "AutoloadNamespaces": { + "Isekai\\UserPoints\\Quests\\": "includes" + }, + "Hooks": { + "LoadExtensionSchemaUpdates": "Isekai\\UserPoints\\Quests\\Hooks::onLoadExtensionSchemaUpdates", + "PageSaveComplete": "Isekai\\UserPoints\\Quests\\Hooks::onPageSaveComplete", + "BeforePageDisplay": "Isekai\\UserPoints\\Quests\\Hooks::onBeforePageDisplay" + }, + "ResourceModules": { + "ext.isekai.userpoints.quests.notification": { + "scripts": ["ext.isekai.userpoints.quests.notification.js"], + "messages": [ + "comma-separator", + "isekai-userpoints-point-name-num", + "isekai-quests-complete-notification-title", + "isekai-quests-complete-notification-message", + "isekai-quests-daily-count", + "isekai-quests-weekly-count", + "isekai-quests-monthly-count", + "isekai-quests-type-edit-notification", + "isekai-quests-type-create-notification" + ] + } + }, + "ResourceFileModulePaths": { + "localBasePath": "modules", + "remoteExtPath": "IsekaiUserPoints/Quests/modules" + }, + "APIModules": { + "userquestsgetnotification": "Isekai\\UserPoints\\Quests\\Api\\ApiUserQuestsGetNotification" + }, + "config": { + "IsekaiQuestsConfig": { + "value": { + "new": { + "periodQuestNumber": 1, + "periodType": "daily", + "points": { + "exp": { + "points": [60] + } + } + }, + "edit": { + "periodQuestNumber": 2, + "periodType": "daily", + "points": { + "exp": { + "points": [40, 40] + } + } + } + } + }, + "IsekaiQuestsTaskConfig": { + "value": { + "edit": { + "unitPoints": { + "exp": 10 + }, + "maxPoints": { + "exp": 400 + }, + "unitWords": 100 + } + } + } + }, + "manifest_version": 2 +} \ No newline at end of file diff --git a/Quests/i18n/zh-hans.json b/Quests/i18n/zh-hans.json new file mode 100644 index 0000000..e9ad4af --- /dev/null +++ b/Quests/i18n/zh-hans.json @@ -0,0 +1,16 @@ +{ + "isekai-userpoints-quests": "异世界百科 用户积分 任务系统", + "isekai-userpoints-quests-desc": "用户完成对应任务后自动获取积分。", + + "apihelp-userquestsgetnotification-summary": "获取用户任务完成的通知消息", + + "isekai-quests-complete-notification-title": "任务完成", + "isekai-quests-complete-notification-message": "$1,获得了$2$3", + + "isekai-quests-daily-count": "(今日 $1/$2)", + "isekai-quests-weekly-count": "(本周 $1/$2)", + "isekai-quests-monthly-count": "(本月 $1/$2)", + + "isekai-quests-type-edit-notification": "编辑了页面", + "isekai-quests-type-create-notification": "创建了页面" +} \ No newline at end of file diff --git a/Quests/includes/Api/ApiUserQuestsGetNotification.php b/Quests/includes/Api/ApiUserQuestsGetNotification.php new file mode 100644 index 0000000..953c72e --- /dev/null +++ b/Quests/includes/Api/ApiUserQuestsGetNotification.php @@ -0,0 +1,32 @@ +getMain(), $method ); + } + + public function execute() { + $user = $this->getUser(); + + if (!$user->isRegistered()) { + $this->dieWithError('apierror-mustbeloggedin-generic', 'login-required'); + } + + $sesssionManager = SessionManager::getGlobalSession(); + if ($sesssionManager->exists(QuestsUtils::SESSION_KEY_QUEST_COMPLETE_NOTIFICATION)) { + $notificationData = $sesssionManager->get(QuestsUtils::SESSION_KEY_QUEST_COMPLETE_NOTIFICATION); + $this->getResult()->addValue(['userquestsgetnotification'], 'notification', $notificationData); + } else { + $this->getResult()->addValue(['userquestsgetnotification'], 'notification', null); + } + } + + public function isInternal() { + return true; + } +} \ No newline at end of file diff --git a/Quests/includes/Hooks.php b/Quests/includes/Hooks.php new file mode 100644 index 0000000..1e3c249 --- /dev/null +++ b/Quests/includes/Hooks.php @@ -0,0 +1,79 @@ +getDB()->getType(); + $updater->addExtensionTable('isekai_user_quests_record', $dir . $type . '/isekai_user_quests_record.sql'); + } + + /** + * @param \WikiPage $wikiPage + * @param \MediaWiki\User\UserIdentity $user + * @param string $summary + * @param int $flags + * @param \MediaWiki\Revision\RevisionRecord $revisionRecord + * @param \MediaWiki\Storage\EditResult $editResult + */ + public static function onPageSaveComplete($wikiPage, $user, $summary, $flags, $revisionRecord, $editResult) { + wfDebugLog('IsekaiQuests', 'onPageSaveComplete'); + $services = MediaWikiServices::getInstance(); + $config = $services->getMainConfig(); + + $questsConfig = $config->get('IsekaiQuestsConfig'); + + $allowedNamespaces = [NS_MAIN, NS_CATEGORY, NS_TEMPLATE, NS_HELP]; + + if (in_array($wikiPage->getNamespace(), $allowedNamespaces)) { + if ($flags & EDIT_NEW && isset($questsConfig['new'])) { + // 新建页面 + $questConfig = $questsConfig['new']; + + $questRes = QuestsUtils::onQuestComplete($user, 'new', $questConfig); + wfDebugLog('IsekaiQuests', 'quest finished: ', var_export($questRes, true)); + if ($questRes) { + QuestsUtils::setQuestCompleteNotification('new', $questRes['deltaPoints'], $questRes['completeNum'], $questConfig); + return true; + } + } + + // 如果新建页面的任务次数已满,则作为编辑页面处理 + if ($flags & EDIT_UPDATE && isset($questsConfig['edit'])) { + // 编辑页面 + $questConfig = $questsConfig['edit']; + + $questRes = QuestsUtils::onQuestComplete($user, 'edit', $questConfig); + wfDebugLog('IsekaiQuests', 'quest finished: ', var_export($questRes, true)); + if ($questRes) { + QuestsUtils::setQuestCompleteNotification('edit', $questRes['deltaPoints'], $questRes['completeNum'], $questConfig); + return true; + } + } + } + + return true; + } + + public static function onBeforePageDisplay(OutputPage $out, $skin) { + $out->addModules(['ext.isekai.userpoints.quests.notification']); + + $sessionManager = SessionManager::getGlobalSession(); + if ($sessionManager->exists(QuestsUtils::SESSION_KEY_QUEST_COMPLETE_NOTIFICATION)) { + $out->addJsConfigVars([ + 'wgIsekaiQuestsCompleteNotification' => $sessionManager->get(QuestsUtils::SESSION_KEY_QUEST_COMPLETE_NOTIFICATION), + ]); + $sessionManager->remove(QuestsUtils::SESSION_KEY_QUEST_COMPLETE_NOTIFICATION); + } + } +} diff --git a/Quests/includes/QuestsUtils.php b/Quests/includes/QuestsUtils.php new file mode 100644 index 0000000..693ec41 --- /dev/null +++ b/Quests/includes/QuestsUtils.php @@ -0,0 +1,152 @@ +getDBLoadBalancer()->getConnection(DB_REPLICA); + + $res = $dbr->newSelectQueryBuilder() + ->from('isekai_user_quests_record') + ->where([ + 'user_id' => $user->getId(), + 'quest_type' => $questType, + 'period_time' => $periodTime, + ])->field('quest_complete_num') + ->fetchField(); + + if ($res === false) { + return 0; + } + + return intval($res); + } + + public static function setUserQuestCompleteNum(User $user, string $questType, string $periodTime, int $num) { + $dbr = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection(DB_REPLICA); + $dbw = MediaWikiServices::getInstance()->getDBLoadBalancer()->getConnection(DB_PRIMARY); + + $res = $dbr->selectField('isekai_user_quests_record', 'id', [ + 'user_id' => $user->getId(), + 'quest_type' => $questType, + 'period_time' => $periodTime, + ], __METHOD__); + + if ($res === false) { + $dbw->insert('isekai_user_quests_record', [ + 'user_id' => $user->getId(), + 'quest_type' => $questType, + 'period_time' => $periodTime, + 'quest_complete_num' => $num, + ], __METHOD__); + } else { + $dbw->update('isekai_user_quests_record', [ + 'quest_complete_num' => $num, + ], [ + 'id' => $res, + ], __METHOD__); + } + } + + public static function onQuestComplete(User $user, string $questType, array $questConfig) { + $services = MediaWikiServices::getInstance(); + + $periodQuestNumber = $questConfig['periodQuestNumber']; + $periodType = $questConfig['periodType']; + $pointsConfig = $questConfig['points']; + + $periodTime = QuestsUtils::getPeriodTime($periodType); + + $completeNum = QuestsUtils::getUserQuestCompleteNum($user, $questType, $periodTime); + + if ($completeNum > $periodQuestNumber) { + return false; + } + + $deltaPoints = []; + + /** @var IsekaiUserPointsFactory */ + $userPointsFactory = $services->getService('IsekaiUserPoints'); + foreach ($pointsConfig as $pointType => $pointConfig) { + $userPoints = $userPointsFactory->newFromUser($user, $pointType); + + if ($userPoints) { + $expireTime = $pointConfig['expireTime'] ?? -1; + $pointsAddConfig = $pointConfig['points']; + + if (count($pointsAddConfig) > 0) { + // 获取当前任务完成次数对应的积分数量 + $periodIndex = $completeNum % count($pointsAddConfig); + $pointsAdd = $pointsAddConfig[$periodIndex]; + + $userPoints->addPoints($pointsAdd, $expireTime, 'quests', $questType); + + $deltaPoints[$pointType] = $pointsAdd; + } + } + } + + $completeNum ++; + QuestsUtils::setUserQuestCompleteNum($user, $questType, $periodTime, $completeNum); + + return [ + 'completeNum' => $completeNum, + 'deltaPoints' => $deltaPoints, + ]; + } + + /** + * Get period time string from period type + * @param string $periodType + * @return string + */ + public static function getPeriodTime($periodType = 'daily') { + switch ($periodType) { + case 'daily': + return date('Y-m-d'); + case 'weekly': + return date('Y-W') . 'week'; + case 'monthly': + return date('Y-m'); + } + } + + public static function setQuestCompleteNotification(string $questType, array $deltaPoints, $completeNum = null, ?array $questConfig = null) { + $sesssionManager = SessionManager::getGlobalSession(); + + $resultPointDelta = []; + foreach ($deltaPoints as $pointType => $delta) { + $pointName = Utils::getPointName($pointType); + $pointIcon = Utils::getPointIcon($pointType); + + $resultPointDelta[] = [ + 'point_type' => $pointType, + 'name' => $pointName, + 'icon' => $pointIcon, + 'points' => $delta, + ]; + } + + $notificationData = [ + 'questType' => $questType, + 'deltaPoints' => $resultPointDelta, + ]; + + if ($questConfig) { + $notificationData['completeNum'] = $completeNum; + $notificationData['periodQuestNumber'] = $questConfig['periodQuestNumber']; + $notificationData['periodType'] = $questConfig['periodType']; + } + + $sesssionManager->set(self::SESSION_KEY_QUEST_COMPLETE_NOTIFICATION, $notificationData); + } +} diff --git a/Quests/modules/ext.isekai.userpoints.quests.notification.js b/Quests/modules/ext.isekai.userpoints.quests.notification.js new file mode 100644 index 0000000..2d459b1 --- /dev/null +++ b/Quests/modules/ext.isekai.userpoints.quests.notification.js @@ -0,0 +1,69 @@ +function onQuestsCompleteNotification(notificationData) { + var questType = notificationData.questType; + var deltaPoints = notificationData.deltaPoints; + var completeNum = notificationData.completeNum; + var periodQuestNumber = notificationData.periodQuestNumber; + var periodType = notificationData.periodType; + + var pointMsgList = []; + deltaPoints.forEach(function (pointDeltaInfo) { + var msg = mw.msg('isekai-userpoints-point-name-num', pointDeltaInfo.name, pointDeltaInfo.icon, pointDeltaInfo.points); + pointMsgList.push(msg); + + var separator = mw.msg('comma-separator'); + var pointMsg = pointMsgList.join(separator); + + var actionMsg = mw.msg('isekai-quests-type-' + questType + '-notification'); + + var completeNumMsg = ''; + switch (periodType) { + case 'daily': + completeNumMsg = mw.msg('isekai-quests-daily-count', completeNum, periodQuestNumber); + break; + case 'weekly': + completeNumMsg = mw.msg('isekai-quests-weekly-count', completeNum, periodQuestNumber); + break; + case 'monthly': + completeNumMsg = mw.msg('isekai-quests-monthly-count', completeNum, periodQuestNumber); + break; + } + + var notificationMsg = mw.msg('isekai-quests-complete-notification-message', actionMsg, pointMsg, completeNumMsg); + + mw.notify('', { + title: mw.msg('isekai-quests-complete-notification-title'), + tag: 'isekai-userpoints-quest', + id: 'isekai-userpoints-quest-notify', + }); + + function changeNotifyContent() { + var notifyDom = document.querySelector('#isekai-userpoints-quest-notify'); + if (notifyDom) { + notifyDom.querySelector('.mw-notification-content').innerHTML = notificationMsg; + } else { + requestAnimationFrame(changeNotifyContent); + } + } + + changeNotifyContent(); + }); +} + +mw.hook('postEdit').add(function (data) { + console.log('onPostEdit'); + + var notificationData = mw.config.get('wgIsekaiQuestsCompleteNotification'); + if (notificationData) { + onQuestsCompleteNotification(notificationData); + } else { + // VE,通过API获取消息 + var mwApi = new mw.Api(); + mwApi.get({ + action: 'userquestsgetnotification', + }).done(function (data) { + if (data.userquestsgetnotification && data.userquestsgetnotification.notification) { + onQuestsCompleteNotification(data.userquestsgetnotification.notification); + } + }); + } +}); \ No newline at end of file diff --git a/Quests/sql/mysql/isekai_user_quests_record.sql b/Quests/sql/mysql/isekai_user_quests_record.sql new file mode 100644 index 0000000..204377a --- /dev/null +++ b/Quests/sql/mysql/isekai_user_quests_record.sql @@ -0,0 +1,9 @@ +CREATE TABLE /*_*/isekai_user_quests_record ( + `id` INT UNSIGNED NOT NULL PRIMARY KEY AUTO_INCREMENT, + `user_id` INT UNSIGNED NOT NULL, + `quest_type` VARCHAR(60) NOT NULL, + `period_time` VARCHAR(50) NOT NULL, + `quest_complete_num` INT UNSIGNED NOT NULL DEFAULT '0' +) /*$wgDBTableOptions*/; +ALTER TABLE /*_*/isekai_user_quests_record ADD INDEX (`user_id`, `quest_type`); +ALTER TABLE /*_*/isekai_user_quests_record ADD INDEX (`user_id`); \ No newline at end of file diff --git a/Quests/sql/postgres/isekai_user_quests_record.sql b/Quests/sql/postgres/isekai_user_quests_record.sql new file mode 100644 index 0000000..c21f13a --- /dev/null +++ b/Quests/sql/postgres/isekai_user_quests_record.sql @@ -0,0 +1,9 @@ +CREATE TABLE isekai_user_quests_record ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL, + quest_type VARCHAR(60) NOT NULL, + period_time VARCHAR(50) NOT NULL, + quest_complete_num INT NOT NULL DEFAULT 0 +); +ALTER TABLE isekai_user_points_log ADD INDEX (user_id, quest_type); +ALTER TABLE isekai_user_points_log ADD INDEX (user_id); \ No newline at end of file diff --git a/extension.json b/extension.json index ddf7ab1..e9b4ac2 100644 --- a/extension.json +++ b/extension.json @@ -1,14 +1,14 @@ { - "name": "Isekai User Points", + "name": "IsekaiUserPoints", "namemsg": "isekai-userpoints", "author": "Hyperzlib", "version": "1.0.0", "url": "https://git.isekai.cn/Isekai-Project/mediawiki-extension-IsekaiUserPoints", "descriptionmsg": "isekai-userpoints-desc", - "license-name": "GPL-2.0-or-later", + "license-name": "MIT", "type": "api", "requires": { - "MediaWiki": ">= 1.35.0" + "MediaWiki": ">= 1.39.0" }, "MessagesDirs": { "IsekaiUserPoints": [ @@ -24,7 +24,8 @@ "Hooks": { "LoadExtensionSchemaUpdates": "Isekai\\UserPoints\\Hooks::onLoadExtensionSchemaUpdates", "BeforePageDisplay": "Isekai\\UserPoints\\Hooks::onBeforePageDisplay", - "GetPreferences": "Isekai\\UserPoints\\Hooks::onGetPreferences" + "GetPreferences": "Isekai\\UserPoints\\Hooks::onGetPreferences", + "ResourceLoaderGetConfigVars": "Isekai\\UserPoints\\Hooks::onResourceLoaderGetConfigVars" }, "APIModules": { "userdailysign": "Isekai\\UserPoints\\Api\\ApiUserDailySign" diff --git a/i18n/zh-hans.json b/i18n/zh-hans.json index 1c09171..08eafe0 100644 --- a/i18n/zh-hans.json +++ b/i18n/zh-hans.json @@ -2,6 +2,12 @@ "isekai-userpoints": "异世界百科 用户积分", "isekai-userpoints-desc": "存储用户积分,为其他应用提供积分功能", + "action-queryuserpoints": "查询其他用户的积分", + "right-queryuserpoints": "查询其他用户的积分", + + "action-edituserpoints": "编辑用户积分", + "right-edituserpoints": "编辑用户积分", + "isekai-userpoints-point-name-num": "$2 $3 $1", "isekai-userpoints-service-system": "积分系统", diff --git a/includes/Hooks.php b/includes/Hooks.php index 5c2362b..c0ebfb3 100644 --- a/includes/Hooks.php +++ b/includes/Hooks.php @@ -3,6 +3,8 @@ namespace Isekai\UserPoints; use DatabaseUpdater; +use DateTime; +use DateTimeZone; use MediaWiki\MediaWikiServices; use OutputPage; use Skin; @@ -62,4 +64,10 @@ class Hooks { } } } + + public static function onResourceLoaderGetConfigVars(array &$vars) { + $defaultTimezone = date_default_timezone_get(); + $tzInfo = new DateTimeZone($defaultTimezone); + $vars['wgTimezoneOffsetMinutes'] = $tzInfo->getOffset(new DateTime()) / 60; + } } diff --git a/modules/ext.isekai.userpoints.dailysign.js b/modules/ext.isekai.userpoints.dailysign.js index 0bfef71..0b5121a 100644 --- a/modules/ext.isekai.userpoints.dailysign.js +++ b/modules/ext.isekai.userpoints.dailysign.js @@ -1,25 +1,44 @@ $(function() { if (!mw.user.isAnon()) { - const storeKey = 'isekai-userpoints-dailysign-lastSignDate'; - const lastSignDate = localStorage.getItem(storeKey); - const today = new Date().toLocaleDateString(); + var storeKey = 'isekai-userpoints-dailysign-lastSignDate'; + var lastSignDate = localStorage.getItem(storeKey); + + function getDateWithTimezone(deltaMinutes) { + var date = new Date(); + var offset = date.getTimezoneOffset(); + var offsetMilliSeconds = offset * 60 * 1000; + var deltaMilliSeconds = deltaMinutes * 60 * 1000; + var newDate = new Date(date.getTime() + offsetMilliSeconds + deltaMilliSeconds); + return newDate; + } + + function getDateString(dateObj) { + var year = dateObj.getFullYear(); + var month = dateObj.getMonth() + 1; + var date = dateObj.getDate(); + return year + '-' + month + '-' + date; + } + + var timezoneOffset = mw.config.get('wgTimezoneOffsetMinutes'); + var today = getDateString(getDateWithTimezone(timezoneOffset)); + if (lastSignDate !== today) { - let mwApi = new mw.Api(); + var mwApi = new mw.Api(); mwApi.postWithToken('csrf', { action: 'userdailysign', }).done(function(data) { if (data.userdailysign && data.userdailysign.success) { if (Array.isArray(data.userdailysign.point_delta)) { - const pointDelta = data.userdailysign.point_delta; - let pointMsgList = []; + var pointDelta = data.userdailysign.point_delta; + var pointMsgList = []; pointDelta.forEach(function (pointDeltaInfo) { - let msg = mw.msg('isekai-userpoints-point-name-num', pointDeltaInfo.name, pointDeltaInfo.icon, pointDeltaInfo.points); + var msg = mw.msg('isekai-userpoints-point-name-num', pointDeltaInfo.name, pointDeltaInfo.icon, pointDeltaInfo.points); pointMsgList.push(msg); }); - let separator = mw.msg('comma-separator'); - let pointMsg = pointMsgList.join(separator); + var separator = mw.msg('comma-separator'); + var pointMsg = pointMsgList.join(separator); - let notificationMsg = mw.msg('isekai-userpoints-dailysign-notify-success', pointMsg); + var notificationMsg = mw.msg('isekai-userpoints-dailysign-notify-success', pointMsg); mw.notify('', { title: mw.msg('isekai-userpoints-dailysign-notify-title'), tag: 'isekai-userpoints-dailysign', @@ -27,7 +46,7 @@ $(function() { }); function changeNotifyContent() { - let notifyDom = document.querySelector('#isekai-userpoints-dailysign-notify'); + var notifyDom = document.querySelector('#isekai-userpoints-dailysign-notify'); if (notifyDom) { notifyDom.querySelector('.mw-notification-content').innerHTML = notificationMsg; } else {