commit 317ee2946183ee0f7723ebc9ec19facf157885db Author: Tim Düsterhus Date: Fri Aug 17 00:30:59 2018 +0200 Initial import diff --git a/.babelrc b/.babelrc new file mode 100644 index 0000000..768e4c4 --- /dev/null +++ b/.babelrc @@ -0,0 +1,17 @@ +{ "presets": [ [ "env" + , { "targets": { "browsers": [ "last 2 chrome versions" + , "last 2 chromeandroid versions" + , "firefox esr" + , "not firefox 52" + , "last 2 firefox versions" + , "edge >= 15" + , "safari >= 11" + , "ios >= 11" + ] + } + , "debug": true + , "include": [ ] + } + ] + ] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a2e1de --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +node_modules + +be.bastelstu.chat.tar.gz +be.bastelstu.chat.tar +files.tar +files_wcf.tar +acptemplates.tar +templates.tar + +Bastelstu.be.Chat.js +Bastelstu.be.Chat.babel.js + +files_wcf/js/Bastelstu.be.Chat.min.js diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..83dcefb --- /dev/null +++ b/LICENSE @@ -0,0 +1,104 @@ +License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved. +“Business Source License” is a trademark of MariaDB Corporation Ab. + +Parameters + +Licensor: Tim Düsterhus +Licensed Work: Tim’s Chat 4.0 + The Licensed Work is (c) 2010-2018 Tim Düsterhus +Additional Use Grant: You may use the Licensed Work when your application + uses the Licensed Work for a purpose that does neither + directly or indirectly generate revenue. + +Change Date: 2022-08-16 + +Change License: Version 2 or later of the GNU General Public License as + published by the Free Software Foundation. + +For information about alternative licensing arrangements for the Software, +please email: tim bastelstu.be + +Notice + +The Business Source License (this document, or the “License”) is not an Open +Source license. However, the Licensed Work will eventually be made available +under an Open Source License, as stated in this License. + +For more information on the use of the Business Source License for MariaDB +products, please visit the MariaDB Business Source License FAQ at +https://mariadb.com/bsl-faq-mariadb. + +For more information on the use of the Business Source License generally, +please visit the Adopting and Developing Business Source License FAQ at +https://mariadb.com/bsl-faq-adopting. + +----------------------------------------------------------------------------- + +Business Source License 1.1 + +Terms + +The Licensor hereby grants you the right to copy, modify, create derivative +works, redistribute, and make non-production use of the Licensed Work. The +Licensor may make an Additional Use Grant, above, permitting limited +production use. + +Effective on the Change Date, or the fourth anniversary of the first publicly +available distribution of a specific version of the Licensed Work under this +License, whichever comes first, the Licensor hereby grants you rights under +the terms of the Change License, and the rights granted in the paragraph +above terminate. + +If your use of the Licensed Work does not comply with the requirements +currently in effect as described in this License, you must purchase a +commercial license from the Licensor, its affiliated entities, or authorized +resellers, or you must refrain from using the Licensed Work. + +All copies of the original and modified Licensed Work, and derivative works +of the Licensed Work, are subject to this License. This License applies +separately for each version of the Licensed Work and the Change Date may vary +for each version of the Licensed Work released by Licensor. + +You must conspicuously display this License on each original or modified copy +of the Licensed Work. If you receive the Licensed Work in original or +modified form from a third party, the terms and conditions set forth in this +License apply to your use of that work. + +Any use of the Licensed Work in violation of this License will automatically +terminate your rights under this License for the current and all other +versions of the Licensed Work. + +This License does not grant you any right in any trademark or logo of +Licensor or its affiliates (provided that you may use a trademark or logo of +Licensor as expressly required by this License). + +TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON +AN “AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, +EXPRESS OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND +TITLE. + +MariaDB hereby grants you permission to use this License’s text to license +your works, and to refer to it using the trademark “Business Source License”, +as long as you comply with the Covenants of Licensor below. + +Covenants of Licensor + +In consideration of the right to use this License’s text and the “Business +Source License” name and trademark, Licensor covenants to MariaDB, and to all +other recipients of the licensed work to be provided by Licensor: + +1. To specify as the Change License the GPL Version 2.0 or any later version, + or a license that is compatible with GPL Version 2.0 or a later version, + where “compatible” means that software provided under the Change License can + be included in a program with software provided under GPL Version 2.0 or a + later version. Licensor may specify additional Change Licenses without + limitation. + +2. To either: (a) specify an additional grant of rights to use that does not + impose any additional restriction on the right granted in this License, as + the Additional Use Grant; or (b) insert the text “None”. + +3. To specify a Change Date. + +4. Not to modify this License in any other way. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..14318cd --- /dev/null +++ b/Makefile @@ -0,0 +1,43 @@ +FILES = $(shell find files -type f) +WCF_FILES = $(shell find files_wcf -type f) +JS_MODULE_FILES = $(shell find files_wcf/js/Bastelstu.be -type f) + +all: be.bastelstu.chat.tar be.bastelstu.chat.tar.gz + +be.bastelstu.chat.tar.gz: be.bastelstu.chat.tar + gzip -9 < $< > $@ + +be.bastelstu.chat.tar: files.tar files_wcf.tar acptemplates.tar templates.tar *.xml LICENSE sql/*.sql language/*.xml + tar cvf be.bastelstu.chat.tar --numeric-owner --exclude-vcs -- $^ + +files.tar: $(FILES) +files_wcf.tar: $(WCF_FILES) files_wcf/js/Bastelstu.be.Chat.min.js +acptemplates.tar: acptemplates/*.tpl +templates.tar: templates/*.tpl + +%.tar: + tar cvf $@ --numeric-owner --exclude-vcs -C $* -- $(^:$*/%=%) + +files_wcf/js/Bastelstu.be.Chat.min.js: Bastelstu.be.Chat.babel.js + yarn run terser --comments '/Copyright|stackoverflow/' -m -c pure_funcs=[console.debug] --verbose --timings -o $@ $^ + +Bastelstu.be.Chat.babel.js: Bastelstu.be.Chat.js .babelrc + yarn run babel $< --out-file $@ + +Bastelstu.be.Chat.js: $(JS_MODULE_FILES) + yarn run r.js -o require.build.js + +clean: + -rm -f files.tar + -rm -f files_wcf.tar + -rm -f templates.tar + -rm -f acptemplates.tar + -rm -f Bastelstu.be.Chat.js + -rm -f Bastelstu.be.Chat.babel.js + -rm -f files_wcf/js/Bastelstu.be.Chat.min.js + +distclean: clean + -rm -f be.bastelstu.chat.tar + -rm -f be.bastelstu.chat.tar.gz + +.PHONY: distclean clean diff --git a/aclOption.xml b/aclOption.xml new file mode 100644 index 0000000..2f8117d --- /dev/null +++ b/aclOption.xml @@ -0,0 +1,48 @@ + + + + + + be.bastelstu.chat.room + + + be.bastelstu.chat.room + + + + + + + + + + + + + + + diff --git a/acpMenu.xml b/acpMenu.xml new file mode 100644 index 0000000..b3b6e54 --- /dev/null +++ b/acpMenu.xml @@ -0,0 +1,42 @@ + + + + + wcf.acp.menu.link.application + + + + chat\acp\page\RoomListPage + chat.acp.menu.link.chat + admin.chat.canManageRoom + 1 + + + + chat\acp\form\RoomAddForm + chat.acp.menu.link.room.list + admin.chat.canManageRoom + fa-plus + + + + chat\acp\page\CommandTriggerListPage + chat.acp.menu.link.chat + admin.chat.canManageTriggers + + + + chat\acp\form\CommandTriggerAddForm + chat.acp.menu.link.command.trigger.list + admin.chat.canManageTriggers + fa-plus + + + + chat\acp\page\SuspensionListPage + chat.acp.menu.link.chat + admin.chat.canManageSuspensions + + + + diff --git a/acptemplates/__chatVersion.tpl b/acptemplates/__chatVersion.tpl new file mode 100644 index 0000000..2a0d898 --- /dev/null +++ b/acptemplates/__chatVersion.tpl @@ -0,0 +1,5 @@ +
+
{lang}chat.acp.index.system.software.chatVersion{/lang}
+
{$__chat->getPackage()->packageVersion}
+
+ diff --git a/acptemplates/commandTriggerAdd.tpl b/acptemplates/commandTriggerAdd.tpl new file mode 100644 index 0000000..9b4804d --- /dev/null +++ b/acptemplates/commandTriggerAdd.tpl @@ -0,0 +1,72 @@ +{include file='header' pageTitle='chat.acp.command.trigger.'|concat:$action} + +
+
+

{lang}chat.acp.command.trigger.{$action}{/lang}

+
+ + +
+ +{include file='formError'} + +{if $success|isset} +

{lang}wcf.global.success.{$action}{/lang}

+{/if} + +
+
+
+ +
+
+ + {if $errorField == 'commandTrigger'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}chat.acp.command.trigger.commandTrigger.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ + + +
+
+ + + {if $errorField == 'className'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}chat.acp.command.trigger.className.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ +
+
+ +
+ + {@SECURITY_TOKEN_INPUT_TAG} +
+
+ +{include file='footer'} + diff --git a/acptemplates/commandTriggerList.tpl b/acptemplates/commandTriggerList.tpl new file mode 100644 index 0000000..07fac70 --- /dev/null +++ b/acptemplates/commandTriggerList.tpl @@ -0,0 +1,86 @@ +{include file='header' pageTitle='chat.acp.command.trigger.list'} + + + + +
+
+

{lang}chat.acp.command.trigger.list{/lang}

+
+ + +
+ +{hascontent} +
+ {content}{pages print=true assign=pagesLinks controller="CommandTriggerList" application="chat" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}{/content} +
+{/hascontent} + +{hascontent} +
+ + + + + + + + {event name='columnHeads'} + + + + + {content} + {foreach from=$objects item=trigger} + + + + + + + + {event name='columns'} + + {/foreach} + {/content} + +
{lang}wcf.global.objectID{/lang}{lang}chat.acp.command.trigger{/lang}{lang}chat.acp.command.className{/lang}
+ + + + {event name='rowButtons'} + {@$trigger->triggerID}/{$trigger->commandTrigger}{$trigger->className}
+
+{hascontentelse} +

{lang}wcf.global.noItems{/lang}

+{/hascontent} + + + +{include file='footer'} + diff --git a/acptemplates/roomAdd.tpl b/acptemplates/roomAdd.tpl new file mode 100644 index 0000000..944cf68 --- /dev/null +++ b/acptemplates/roomAdd.tpl @@ -0,0 +1,113 @@ +{include file='header' pageTitle='chat.acp.room.'|concat:$action} + +{include file='aclPermissions'} + +{include file='multipleLanguageInputJavascript' elementIdentifier='title' forceSelection=false} +{include file='multipleLanguageInputJavascript' elementIdentifier='topic' forceSelection=false} + +{if $roomID|isset} + {include file='aclPermissionJavaScript' containerID='aclContainer' objectTypeID=$aclObjectTypeID objectID=$roomID} +{else} + {include file='aclPermissionJavaScript' containerID='aclContainer' objectTypeID=$aclObjectTypeID} +{/if} + +
+
+

{lang}chat.acp.room.{$action}{/lang}

+ {if $action == 'edit'}

{$room->getTitle()}

{/if} +
+ + +
+ +{include file='formError'} + +{if $success|isset} +

{lang}wcf.global.success.{$action}{/lang}

+{/if} + +
+
+
+ +
+
+ + {if $errorField == 'title'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {elseif $errorType == 'multilingual'} + {lang}wcf.global.form.error.multilingual{/lang} + {else} + {lang}chat.acp.room.title.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ + + +
+
+ + + {if $errorField == 'topic'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}chat.acp.room.topic.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ + + +
+
+ +
+ + + +
+
+ + + {if $errorField == 'userLimit'} + + {if $errorType == 'empty'} + {lang}wcf.global.form.error.empty{/lang} + {else} + {lang}chat.acp.room.userLimit.error.{@$errorType}{/lang} + {/if} + + {/if} +
+ +
+ +
+
+
{lang}wcf.acl.permissions{/lang}
+
+
+
+
+ +
+ + {@SECURITY_TOKEN_INPUT_TAG} +
+
+ +{include file='footer'} + diff --git a/acptemplates/roomList.tpl b/acptemplates/roomList.tpl new file mode 100644 index 0000000..fa9613c --- /dev/null +++ b/acptemplates/roomList.tpl @@ -0,0 +1,81 @@ +{include file='header' pageTitle='chat.acp.room.list'} + + + +
+
+

{lang}chat.acp.room.list{/lang}

+
+ + +
+ +{hascontent} +
+ {content}{pages print=true assign=pagesLinks controller="RoomList" application="chat" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder"}{/content} +
+{/hascontent} + +{hascontent} +
+
    + {content} + {foreach from=$objects item=room} +
  1. + + {$room} + + + + + + {event name='itemButtons'} + + +
  2. + {/foreach} + {/content} +
+
+ +
+ +
+{hascontentelse} +

{lang}wcf.global.noItems{/lang}

+{/hascontent} + +
+ {hascontent} +
+ {content}{@$pagesLinks}{/content} +
+ {/hascontent} + + +
+ +{include file='footer'} + diff --git a/acptemplates/suspensionList.tpl b/acptemplates/suspensionList.tpl new file mode 100644 index 0000000..37f189d --- /dev/null +++ b/acptemplates/suspensionList.tpl @@ -0,0 +1,192 @@ +{include file='header' pageTitle='chat.acp.suspension.list'} + + + +
+
+

{lang}chat.acp.suspension.list{/lang}

+
+ + {hascontent} + + {/hascontent} +
+ +
+
+

{lang}wcf.global.filter{/lang}

+ +
+
+
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+
+ +
+
+ +
+
+
+ +
+
+ + {event name='filterFields'} +
+ +
+ + {@SECURITY_TOKEN_INPUT_TAG} +
+
+
+ + +{capture assign=additionalParameters}{* + *}{if $userID !== null}&userID={$userID}{/if}{* + *}{if $judgeID !== null}&judgeID={$judgeID}{/if}{* + *}{if $roomID !== null}&roomID={$roomID}{/if}{* + *}{if $objectTypeID !== null}&objectTypeID={$objectTypeID}{/if}{* + *}{if $showExpired !== null}&showExpired={$showExpired}{/if}{* +*}{/capture} + +{hascontent} +
+ {content}{pages print=true assign=pagesLinks controller="SuspensionList" application="chat" link="pageNo=%d&sortField=$sortField&sortOrder=$sortOrder$additionalParameters"}{/content} +
+{/hascontent} + +{hascontent} +
+ + + + + + + + + + + + {event name='columnHeads'} + + + + + {content} + {foreach from=$objects item=suspension} + + + + + + + + + + + + {event name='columns'} + + {/foreach} + {/content} + +
{lang}wcf.global.objectID{/lang}{lang}chat.acp.suspension.type{/lang}{lang}chat.acp.suspension.username{/lang}{lang}chat.acp.suspension.judge{/lang}{lang}chat.acp.suspension.room{/lang}{lang}chat.acp.suspension.time{/lang}{lang}chat.acp.suspension.expires{/lang}
+ + {event name='rowButtons'} + {@$suspension->suspensionID}objectTypeID}{/link}">{lang}chat.acp.suspension.type.{$suspension->getSuspensionType()->objectType}{/lang}userID}{/link}">{$suspension->getUser()->username}judgeID}{/link}">{$suspension->judge}roomID}{/link}">{if $suspension->getRoom() !== null}{$suspension->getRoom()}{else}-{/if}{@$suspension->time|time} + {assign var='isActive' value=$suspension->isActive()} + {if $isActive}{/if} + {if $suspension->expires !== null}{@$suspension->expires|time}{else}{lang}chat.acp.suspension.expires.forever{/lang}{/if} + {if $isActive}{/if} + {if $suspension->revoked !== null} +
{lang}chat.acp.suspension.revoked{/lang} + {/if} +
+
+{hascontentelse} +

{lang}wcf.global.noItems{/lang}

+{/hascontent} + +
+ {hascontent} +
+ {content}{@$pagesLinks}{/content} +
+ {/hascontent} + + {hascontent} + + {/hascontent} +
+ +{include file='footer'} + diff --git a/box.xml b/box.xml new file mode 100644 index 0000000..35d6ebc --- /dev/null +++ b/box.xml @@ -0,0 +1,42 @@ + + + + + Chaträume (Inhaltsbereich) + Chat Rooms (Content) + system + be.bastelstu.chat.roomList + contentTop + 1 + 0 + + com.woltlab.wcf.Dashboard + + + Chaträume + + + Chat Rooms + + + + + Chaträume (Seitenleiste) + Chat Rooms (Sidebar) + system + be.bastelstu.chat.roomList + sidebarRight + 1 + 0 + + be.bastelstu.chat.Room + + + Chaträume + + + Chat Rooms + + + + diff --git a/chatCommand.xml b/chatCommand.xml new file mode 100644 index 0000000..d07ecc7 --- /dev/null +++ b/chatCommand.xml @@ -0,0 +1,103 @@ + + + + + chat\system\command\AwayCommand + + away + + + + + chat\system\command\BackCommand + + + + chat\system\command\BanCommand + + ban + + + + + chat\system\command\BroadcastCommand + + broadcast + + + + + chat\system\command\ColorCommand + + color + + + + + chat\system\command\InfoCommand + + info + + + + + chat\system\command\MeCommand + + me + + + + + chat\system\command\MuteCommand + + mute + + + + + chat\system\command\PlainCommand + + + + chat\system\command\TeamCommand + + team + + + + + chat\system\command\TemproomCommand + + temproom + + + + + chat\system\command\UnbanCommand + + unban + + + + + chat\system\command\UnmuteCommand + + unmute + + + + + chat\system\command\WhereCommand + + where + + + + + chat\system\command\WhisperCommand + + whisper + + + + diff --git a/eventListener.xml b/eventListener.xml new file mode 100644 index 0000000..5ea4b5c --- /dev/null +++ b/eventListener.xml @@ -0,0 +1,166 @@ + + + + + + wcf\system\cronjob\HourlyCleanUpCronjob + execute + chat\system\event\listener\HourlyCleanUpCronjobExecuteChatCleanUpListener + user + + + wcf\system\cronjob\HourlyCleanUpCronjob + execute + chat\system\event\listener\HourlyCleanUpCronjobExecuteChatCleanUpListener + admin + + + + + wcf\system\cronjob\HourlyCleanUpCronjob + execute + chat\system\event\listener\HourlyCleanUpCronjobExecuteTemproomListener + user + + + wcf\system\cronjob\HourlyCleanUpCronjob + execute + chat\system\event\listener\HourlyCleanUpCronjobExecuteTemproomListener + admin + + + chat\data\room\Room + canSee + chat\system\event\listener\RoomCanSeeTemproomListener + user + + + chat\acp\page\RoomListPage + calculateNumberOfPages + chat\system\event\listener\RoomListPageTemproomListener + admin + + + chat\acp\form\RoomEditForm + readParameters + chat\system\event\listener\RoomEditFormTemproomListener + admin + + + chat\acp\page\SuspensionListPage + readData + chat\system\event\listener\SuspensionListPageTemproomListener + admin + + + + + chat\data\room\Room + canJoin + chat\system\event\listener\RoomCanJoinUserLimitListener + user + + + + + chat\data\room\Room + canJoin + chat\system\event\listener\RoomCanJoinBanListener + user + + + chat\data\room\Room + canWritePublicly + chat\system\event\listener\RoomCanWritePubliclyMuteListener + user + + + chat\system\command\InfoCommand + execute + chat\system\event\listener\InfoCommandSuspensionsListener + user + + + chat\data\room\RoomAction + getUsers + chat\system\event\listener\RoomActionGetUsersModeratorListener + user + + + + + + + wcf\system\cronjob\HourlyCleanUpCronjob + execute + chat\system\event\listener\HourlyCleanUpCronjobExecuteChatCleanUpListener + user + + + wcf\system\cronjob\HourlyCleanUpCronjob + execute + chat\system\event\listener\HourlyCleanUpCronjobExecuteChatCleanUpListener + admin + + + + + wcf\system\cronjob\HourlyCleanUpCronjob + execute + chat\system\event\listener\HourlyCleanUpCronjobExecuteTemproomListener + user + + + wcf\system\cronjob\HourlyCleanUpCronjob + execute + chat\system\event\listener\HourlyCleanUpCronjobExecuteTemproomListener + admin + + + chat\data\room\Room + canSee + chat\system\event\listener\RoomCanSeeTemproomListener + user + + + chat\acp\page\RoomListPage + calculateNumberOfPages + chat\system\event\listener\RoomListPageTemproomListener + admin + + + chat\acp\form\RoomEditForm + readParameters + chat\system\event\listener\RoomEditFormTemproomListener + admin + + + + + chat\data\room\Room + canJoin + chat\system\event\listener\RoomCanJoinUserLimitListener + user + + + + + chat\data\room\Room + canJoin + chat\system\event\listener\RoomCanJoinBanListener + user + + + chat\data\room\Room + canWritePublicly + chat\system\event\listener\RoomCanWritePubliclyMuteListener + user + + + chat\system\command\InfoCommand + execute + chat\system\event\listener\InfoCommandSuspensionsListener + user + + + diff --git a/files/acp/be.bastelstu.chat_install.php b/files/acp/be.bastelstu.chat_install.php new file mode 100644 index 0000000..3105d64 --- /dev/null +++ b/files/acp/be.bastelstu.chat_install.php @@ -0,0 +1,21 @@ +createBoxCondition( 'be.bastelstu.chat.roomListDashboard' + , 'be.bastelstu.chat.box.roomList.condition' + , 'be.bastelstu.chat.roomFilled' + , [ 'chatRoomIsFilled' => 1 ] + ); diff --git a/files/acp/be.bastelstu.chat_update.php b/files/acp/be.bastelstu.chat_update.php new file mode 100644 index 0000000..3ca26ae --- /dev/null +++ b/files/acp/be.bastelstu.chat_update.php @@ -0,0 +1,35 @@ +getObjectTypeIDByName('be.bastelstu.chat.messageType', 'be.bastelstu.chat.messageType.chatUpdate'); + +if ($objectTypeID) { + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => null + , 'userID' => null + , 'username' => '' + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ ]) + ] + ] + ) + )->executeAction(); +} + +$CHATCore = file_get_contents(__DIR__.'/../lib/system/CHATCore.class.php'); +if (strpos($CHATCore, 'chat.phar.php') === false) { + @unlink(__DIR__.'/../chat.phar.php'); +} diff --git a/files/acp/global.php b/files/acp/global.php new file mode 100644 index 0000000..c3243f6 --- /dev/null +++ b/files/acp/global.php @@ -0,0 +1,18 @@ +handle('chat', true); diff --git a/files/global.php b/files/global.php new file mode 100644 index 0000000..8ee87b1 --- /dev/null +++ b/files/global.php @@ -0,0 +1,16 @@ +handle('chat'); diff --git a/files/lib/acp/form/CommandTriggerAddForm.class.php b/files/lib/acp/form/CommandTriggerAddForm.class.php new file mode 100644 index 0000000..9ff850f --- /dev/null +++ b/files/lib/acp/form/CommandTriggerAddForm.class.php @@ -0,0 +1,159 @@ +sqlOrderBy = 'command.className'; + $commandList->readObjects(); + + $this->commands = $commandList->getObjects(); + + parent::readData(); + } + + /** + * @inheritDoc + */ + public function readFormParameters() { + parent::readFormParameters(); + + if (isset($_POST['commandTrigger'])) $this->commandTrigger = \wcf\util\StringUtil::trim($_POST['commandTrigger']); + if (isset($_POST['className'])) $this->className = \wcf\util\StringUtil::trim($_POST['className']); + } + + /** + * @inheritDoc + */ + public function validate() { + parent::validate(); + + if (empty($this->commandTrigger)) { + throw new UserInputException('commandTrigger', 'empty'); + } + + // Triggers must not contain whitespace + if (preg_match('~\s~', $this->commandTrigger)) { + throw new UserInputException('commandTrigger', 'invalid'); + } + + // Check for duplicates + $trigger = CommandTrigger::getTriggerByName($this->commandTrigger); + if ((!isset($this->trigger) && $trigger->triggerID) || (isset($this->trigger) && $trigger->triggerID != $this->trigger->triggerID)) { + throw new UserInputException('commandTrigger', 'duplicate'); + } + + if (empty($this->className)) { + throw new UserInputException('className', 'empty'); + } + + // Check if the command is registered + foreach ($this->commands as $command) { + if ($command->className === $this->className) { + $this->command = $command; + break; + } + } + + if (!$this->command) { + throw new UserInputException('className', 'notFound'); + } + } + + /** + * @inheritDoc + */ + public function save() { + parent::save(); + + $fields = [ 'commandTrigger' => $this->commandTrigger + , 'commandID' => $this->command->commandID + ]; + + // create room + $this->objectAction = new \chat\data\command\CommandTriggerAction([ ], 'create', [ 'data' => array_merge($this->additionalFields, $fields) ]); + $this->objectAction->executeAction(); + + $this->saved(); + + // reset values + $this->commandTrigger = $this->className = ''; + + // show success message + WCF::getTPL()->assign('success', true); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ 'action' => 'add' + , 'commandTrigger' => $this->commandTrigger + , 'className' => $this->className + , 'availableCommands' => $this->commands + ]); + } +} diff --git a/files/lib/acp/form/CommandTriggerEditForm.class.php b/files/lib/acp/form/CommandTriggerEditForm.class.php new file mode 100644 index 0000000..6acea7f --- /dev/null +++ b/files/lib/acp/form/CommandTriggerEditForm.class.php @@ -0,0 +1,112 @@ +triggerID = intval($_REQUEST['id']); + $this->trigger = new CommandTrigger($this->triggerID); + + if (!$this->trigger) { + throw new IllegalLinkException(); + } + + parent::readParameters(); + } + + /** + * @inheritDoc + */ + public function readData() { + parent::readData(); + + if (empty($_POST)) { + $commandList = new \chat\data\command\CommandList(); + $commandList->getConditionBuilder()->add('command.commandID = ?', [ $this->trigger->commandID ]); + $commandList->readObjects(); + $commands = $commandList->getObjects(); + + if (!count($commands)) { + throw new IllegalLinkException(); + } + + $this->commandTrigger = $this->trigger->commandTrigger; + $this->className = $commands[$this->trigger->commandID]->className; + } + } + + /** + * @inheritDoc + */ + public function save() { + \wcf\form\AbstractForm::save(); + + $fields = [ 'commandTrigger' => $this->commandTrigger + , 'commandID' => $this->command->commandID + ]; + + // update trigger + $this->objectAction = new CommandTriggerAction([ $this->trigger ], 'update', [ 'data' => array_merge($this->additionalFields, $fields) ]); + $this->objectAction->executeAction(); + + $this->saved(); + + // show success message + WCF::getTPL()->assign('success', true); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ 'action' => 'edit' + , 'triggerID' => $this->trigger->triggerID + ]); + } +} diff --git a/files/lib/acp/form/RoomAddForm.class.php b/files/lib/acp/form/RoomAddForm.class.php new file mode 100644 index 0000000..1d85b19 --- /dev/null +++ b/files/lib/acp/form/RoomAddForm.class.php @@ -0,0 +1,215 @@ +register('title'); + I18nHandler::getInstance()->register('topic'); + + $this->aclObjectTypeID = ACLHandler::getInstance()->getObjectTypeID('be.bastelstu.chat.room'); + } + + /** + * @inheritDoc + */ + public function readFormParameters() { + parent::readFormParameters(); + + // read i18n values + I18nHandler::getInstance()->readValues(); + + // handle i18n plain input + if (I18nHandler::getInstance()->isPlainValue('title')) $this->title = I18nHandler::getInstance()->getValue('title'); + if (I18nHandler::getInstance()->isPlainValue('topic')) $this->topic = I18nHandler::getInstance()->getValue('topic'); + if (isset($_POST['userLimit'])) $this->userLimit = intval($_POST['userLimit']); + if (isset($_POST['topicUseHtml'])) $this->topicUseHtml = true; + } + + /** + * @inheritDoc + */ + public function validate() { + parent::validate(); + + // validate title + if (!I18nHandler::getInstance()->validateValue('title')) { + if (I18nHandler::getInstance()->isPlainValue('title')) { + throw new UserInputException('title'); + } + else { + throw new UserInputException('title', 'multilingual'); + } + } + + // validate topic + if (!I18nHandler::getInstance()->validateValue('topic', false, true)) { + throw new UserInputException('topic'); + } + + if (mb_strlen($this->topic) > 10000) { + throw new UserInputException('topic', 'tooLong'); + } + + if ($this->userLimit < 0) { + throw new UserInputException('userLimit', 'negative'); + } + } + + /** + * @inheritDoc + */ + public function save() { + parent::save(); + + $fields = [ 'title' => $this->title + , 'topic' => $this->topic + , 'topicUseHtml' => (int) $this->topicUseHtml + , 'userLimit' => $this->userLimit + , 'position' => 0 // TODO + ]; + + // create room + $this->objectAction = new \chat\data\room\RoomAction([], 'create', [ 'data' => array_merge($this->additionalFields, $fields) ]); + $returnValues = $this->objectAction->executeAction(); + + // save i18n values + $this->saveI18nValue($returnValues['returnValues'], [ 'title', 'topic' ]); + + // save ACL + ACLHandler::getInstance()->save($returnValues['returnValues']->roomID, $this->aclObjectTypeID); + + $this->saved(); + + // reset values + $this->title = $this->topic = ''; + $this->userLimit = 0; + $this->topicUseHtml = false; + + I18nHandler::getInstance()->reset(); + ACLHandler::getInstance()->disableAssignVariables(); + + // show success message + WCF::getTPL()->assign('success', true); + } + + /** + * Saves i18n values. + * + * @param Room $room + * @param string[] $columns + */ + public function saveI18nValue(Room $room, $columns) { + $data = [ ]; + + foreach ($columns as $columnName) { + $languageItem = 'chat.room.room'.$room->roomID.'.'.$columnName; + + if (I18nHandler::getInstance()->isPlainValue($columnName)) { + if ($room->$columnName === $languageItem) { + I18nHandler::getInstance()->remove($languageItem); + } + } + else { + $packageID = \wcf\data\package\PackageCache::getInstance()->getPackageID('be.bastelstu.chat'); + + I18nHandler::getInstance()->save( $columnName + , $languageItem + , 'chat.room' + , $packageID + ); + + $data[$columnName] = $languageItem; + } + } + + if (!empty($data)) { + (new RoomEditor($room))->update($data); + } + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + ACLHandler::getInstance()->assignVariables($this->aclObjectTypeID); + I18nHandler::getInstance()->assignVariables(); + + WCF::getTPL()->assign([ 'action' => 'add' + , 'aclObjectTypeID' => $this->aclObjectTypeID + , 'userLimit' => $this->userLimit + , 'topicUseHtml' => $this->topicUseHtml + ]); + } +} + diff --git a/files/lib/acp/form/RoomEditForm.class.php b/files/lib/acp/form/RoomEditForm.class.php new file mode 100644 index 0000000..9c9e75f --- /dev/null +++ b/files/lib/acp/form/RoomEditForm.class.php @@ -0,0 +1,117 @@ +roomID = intval($_REQUEST['id']); + $this->room = new Room($this->roomID); + + if (!$this->room) { + throw new IllegalLinkException(); + } + + parent::readParameters(); + } + + /** + * @inheritDoc + */ + public function readData() { + parent::readData(); + + if (empty($_POST)) { + $packageID = \wcf\data\package\PackageCache::getInstance()->getPackageID('be.bastelstu.chat'); + I18nHandler::getInstance()->setOptions('title', $packageID, $this->room->title, 'chat.room.room\d+.title'); + I18nHandler::getInstance()->setOptions('topic', $packageID, $this->room->topic, 'chat.room.room\d+.topic'); + $this->userLimit = $this->room->userLimit; + $this->topicUseHtml = $this->room->topicUseHtml; + } + } + + /** + * @inheritDoc + */ + public function save() { + \wcf\form\AbstractForm::save(); + + $fields = [ 'title' => $this->title + , 'topic' => $this->topic + , 'topicUseHtml' => (int) $this->topicUseHtml + , 'userLimit' => $this->userLimit + , 'position' => 0 // TODO + ]; + + // update room + $this->objectAction = new RoomAction([ $this->room ], 'update', [ 'data' => array_merge($this->additionalFields, $fields) ]); + $returnValues = $this->objectAction->executeAction(); + + // save i18n values + $this->saveI18nValue($this->room, [ 'title', 'topic' ]); + + // save ACL + ACLHandler::getInstance()->save($this->room->roomID, $this->aclObjectTypeID); + + $this->saved(); + + // show success message + WCF::getTPL()->assign('success', true); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + I18nHandler::getInstance()->assignVariables(!empty($_POST)); + + WCF::getTPL()->assign([ 'action' => 'edit' + , 'roomID' => $this->room->roomID + , 'room' => $this->room + ]); + } +} diff --git a/files/lib/acp/page/CommandTriggerListPage.class.php b/files/lib/acp/page/CommandTriggerListPage.class.php new file mode 100644 index 0000000..6d65515 --- /dev/null +++ b/files/lib/acp/page/CommandTriggerListPage.class.php @@ -0,0 +1,55 @@ +objectList->sqlSelects = 'command.className'; + $this->objectList->sqlJoins = 'LEFT JOIN chat'.WCF_N.'_command command ON (command.commandID = command_trigger.commandID)'; + } +} diff --git a/files/lib/acp/page/RoomListPage.class.php b/files/lib/acp/page/RoomListPage.class.php new file mode 100644 index 0000000..0b373ab --- /dev/null +++ b/files/lib/acp/page/RoomListPage.class.php @@ -0,0 +1,45 @@ +roomID = intval($_REQUEST['roomID']); + if (isset($_REQUEST['userID']) && $_REQUEST['userID'] !== '') $this->userID = intval($_REQUEST['userID']); + if (isset($_REQUEST['judgeID']) && $_REQUEST['judgeID'] !== '') $this->judgeID = intval($_REQUEST['judgeID']); + if (isset($_REQUEST['objectTypeID']) && $_REQUEST['objectTypeID'] !== '') $this->objectTypeID = intval($_REQUEST['objectTypeID']); + // Checkboxes need special handling + if (!empty($_POST) && !isset($_POST['showExpired'])) $this->showExpired = false; + + if (isset($_POST['searchUsername'])) { + $this->searchUsername = StringUtil::trim($_POST['searchUsername']); + + if (!empty($this->searchUsername)) { + $this->userID = User::getUserByUsername($this->searchUsername)->userID; + } + } + else if ($this->userID !== null) { + $this->searchUsername = (new User($this->userID))->username; + } + + if (isset($_POST['searchJudge'])) { + $this->searchJudge = StringUtil::trim($_POST['searchJudge']); + + if (!empty($this->searchJudge)) { + $this->judgeID = User::getUserByUsername($this->searchJudge)->userID; + } + } + else if ($this->judgeID !== null) { + $this->searchJudge = (new User($this->judgeID))->username; + } + } + + /** + * @inheritDoc + */ + public function readData() { + $this->availableObjectTypes = \wcf\data\object\type\ObjectTypeCache::getInstance()->getObjectTypes('be.bastelstu.chat.suspension'); + + $roomList = new \chat\data\room\RoomList(); + $roomList->sqlOrderBy = "room.position"; + $roomList->readObjects(); + $this->availableRooms = $roomList->getObjects(); + + parent::readData(); + + \wcf\system\cache\runtime\UserRuntimeCache::getInstance()->cacheObjectIDs(array_map(function (Suspension $s) { + return $s->userID; + }, $this->objectList->getObjects())); + } + + /** + * @inheritDoc + */ + protected function initObjectList() { + parent::initObjectList(); + + $this->objectList->sqlSelects .= 'COALESCE(suspension.revoked, suspension.expires, 2147483647) AS expiresSort'; + + if (!empty($this->availableRooms)) { + $this->objectList->getConditionBuilder()->add('(roomID IN (?) OR roomID IS NULL)', [ array_map(function (Room $room) { + return $room->roomID; + }, $this->availableRooms) ]); + } + else { + $this->objectList->getConditionBuilder()->add('1 = 0'); + } + + if ($this->userID !== null) { + $this->objectList->getConditionBuilder()->add('userID = ?', [ $this->userID ]); + } + + if ($this->roomID !== null) { + if ($this->roomID === 0) { + $this->objectList->getConditionBuilder()->add('roomID IS NULL'); + } + else { + $this->objectList->getConditionBuilder()->add('roomID = ?', [ $this->roomID ]); + } + } + + if ($this->objectTypeID !== null) { + $this->objectList->getConditionBuilder()->add('objectTypeID = ?', [ $this->objectTypeID ]); + } + + if ($this->judgeID !== null) { + $this->objectList->getConditionBuilder()->add('judgeID = ?', [ $this->judgeID ]); + } + + if ($this->showExpired === false) { + $this->objectList->getConditionBuilder()->add('expires >= ?', [ TIME_NOW ]); + } + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ 'userID' => $this->userID + , 'roomID' => $this->roomID + , 'objectTypeID' => $this->objectTypeID + , 'judgeID' => $this->judgeID + , 'availableRooms' => $this->availableRooms + , 'availableObjectTypes' => $this->availableObjectTypes + , 'searchUsername' => $this->searchUsername + , 'searchJudge' => $this->searchJudge + , 'showExpired' => $this->showExpired + ]); + } +} diff --git a/files/lib/data/command/Command.class.php b/files/lib/data/command/Command.class.php new file mode 100644 index 0000000..ecb70e4 --- /dev/null +++ b/files/lib/data/command/Command.class.php @@ -0,0 +1,51 @@ +getPackageID('be.bastelstu.chat'); + } + + if ($this->packageID === $chatPackageID && $this->identifier === 'plain') { + return true; + } + + $sql = "SELECT COUNT(*) + FROM chat".WCF_N."_command_trigger + WHERE commandID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ $this->commandID ]); + return $statement->fetchSingleColumn() > 0; + } +} diff --git a/files/lib/data/command/CommandCache.class.php b/files/lib/data/command/CommandCache.class.php new file mode 100644 index 0000000..59102c7 --- /dev/null +++ b/files/lib/data/command/CommandCache.class.php @@ -0,0 +1,110 @@ +getData(); + + $this->commands = $data['commands']; + $this->packages = $data['packages']; + $this->triggers = $data['triggers']; + } + + /** + * Returns a specific command. + * + * @param integer $commandID + * @return Command + */ + public function getCommand($commandID) { + if (isset($this->commands[$commandID])) { + return $this->commands[$commandID]; + } + + return null; + } + + /** + * Returns a specific command defined by a trigger. + * + * @param string $trigger + * @return Command + */ + public function getCommandByTrigger($trigger) { + if (isset($this->triggers[$trigger])) { + return $this->commands[$this->triggers[$trigger]]; + } + + return null; + } + + /** + * Returns the command defined by the given package and identifier. + * + * @param \wcf\data\package\Package $package + * @param string $identifier + * @return Command + */ + public function getCommandByPackageAndIdentifier(\wcf\data\package\Package $package, $identifier) { + if (isset($this->packages[$package->packageID][$identifier])) { + return $this->packages[$package->packageID][$identifier]; + } + + return null; + } + + /** + * Returns all commands. + * + * @return Command[] + */ + public function getCommands() { + return $this->commands; + } + + /** + * Returns all triggers. + * + * @return int[] + */ + public function getTriggers() { + return $this->triggers; + } +} diff --git a/files/lib/data/command/CommandEditor.class.php b/files/lib/data/command/CommandEditor.class.php new file mode 100644 index 0000000..be4cb42 --- /dev/null +++ b/files/lib/data/command/CommandEditor.class.php @@ -0,0 +1,25 @@ +commandTrigger; + } + + /** + * @inheritDoc + */ + public function getObjectID() { + return $this->triggerID; + } + + /** + * Returns the trigger specified by its commandTrigger value + * + * @param string $name + * @return CommandTrigger + */ + public static function getTriggerByName($name) { + $sql = "SELECT * + FROM chat".WCF_N."_command_trigger + WHERE commandTrigger = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ $name ]); + $row = $statement->fetchArray(); + if (!$row) $row = []; + + return new self(null, $row); + } +} diff --git a/files/lib/data/command/CommandTriggerAction.class.php b/files/lib/data/command/CommandTriggerAction.class.php new file mode 100644 index 0000000..0bfcd34 --- /dev/null +++ b/files/lib/data/command/CommandTriggerAction.class.php @@ -0,0 +1,30 @@ +reset(); + } +} diff --git a/files/lib/data/command/CommandTriggerList.class.php b/files/lib/data/command/CommandTriggerList.class.php new file mode 100644 index 0000000..4381635 --- /dev/null +++ b/files/lib/data/command/CommandTriggerList.class.php @@ -0,0 +1,20 @@ +data['payload'] = @unserialize($this->data['payload']); + if (!is_array($this->data['payload'])) { + $this->data['payload'] = [ ]; + } + } + + /** + * Returns whether this message already is inside the log. + * + * @return boolean + */ + public function isInLog() { + return $this->time < (TIME_NOW - CHAT_ARCHIVE_AFTER); + } + + /** + * Returns the message type object of this message. + * + * @return \wcf\data\object\type\ObjectType + */ + public function getMessageType() { + return \wcf\data\object\type\ObjectTypeCache::getInstance()->getObjectType($this->objectTypeID); + } + + /** + * Returns the chat room that contains this message. + * + * @return \chat\data\room\Room + */ + public function getRoom() { + return \chat\data\room\RoomCache::getInstance()->getRoom($this->roomID); + } +} diff --git a/files/lib/data/message/MessageAction.class.php b/files/lib/data/message/MessageAction.class.php new file mode 100644 index 0000000..18c918b --- /dev/null +++ b/files/lib/data/message/MessageAction.class.php @@ -0,0 +1,307 @@ +parameters['updateTimestamp']) && $this->parameters['updateTimestamp']) { + $sql = "UPDATE chat".WCF_N."_room_to_user SET lastPush = ? WHERE roomID = ? AND userID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ TIME_NOW, $message->roomID, $message->userID ]); + } + if (isset($this->parameters['grantPoints']) && $this->parameters['grantPoints']) { + UserActivityPointHandler::getInstance()->fireEvent('be.bastelstu.chat.activityPointEvent.message', $message->messageID, $message->userID); + } + + $pushHandler = \wcf\system\push\PushHandler::getInstance(); + if ($pushHandler->isEnabled() && in_array('target:channels', $pushHandler->getFeatureFlags())) { + $fastSelect = $message->getMessageType()->getProcessor()->supportsFastSelect(); + if ($fastSelect) { + $target = [ 'channels' => [ 'be.bastelstu.chat.room-'.$message->roomID ] ]; + } + else { + $target = [ 'channels' => [ 'be.bastelstu.chat' ] ]; + } + $pushHandler->sendMessage([ 'message' => 'be.bastelstu.chat.message' + , 'target' => $target + ]); + } + + return $message; + } + + /** + * Validates parameters and permissions. + */ + public function validateTrash() { + // read objects + if (empty($this->objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + foreach ($this->getObjects() as $message) { + if ($message->isDeleted) continue; + + $messageType = $message->getMessageType()->getProcessor(); + if (!($messageType instanceof \chat\system\message\type\IDeletableMessageType) || !$messageType->canDelete($message->getDecoratedObject())) { + throw new PermissionDeniedException(); + } + } + } + + /** + * Marks this message as deleted and creates a tombstone message. + * + * Note: Contrary to other applications there is no way to undelete a message. + */ + public function trash() { + if (empty($this->objects)) { + $this->readObjects(); + } + + $data = [ 'isDeleted' => 1 + ]; + + $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.messageType', 'be.bastelstu.chat.messageType.tombstone'); + if (!$objectTypeID) { + throw new \LogicException('Missing object type'); + } + + WCF::getDB()->beginTransaction(); + $objectAction = new static($this->getObjects(), 'update', [ 'data' => $data ]); + $objectAction->executeAction(); + foreach ($this->getObjects() as $message) { + if ($message->isDeleted) continue; + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $message->roomID + , 'userID' => null + , 'username' => '' + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'messageID' => $message->messageID ]) + ] + ] + ) + )->executeAction(); + } + WCF::getDB()->commitTransaction(); + } + + /** + * Prunes chat messages older than chat_log_archivetime days. + */ + public function prune() { + // Check whether pruning is disabled. + if (!CHAT_LOG_ARCHIVETIME) return; + + $sql = "SELECT messageID + FROM chat".WCF_N."_message + WHERE time < ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ TIME_NOW - CHAT_LOG_ARCHIVETIME * 86400 ]); + $messageIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); + + return call_user_func([$this->className, 'deleteAll'], $messageIDs); + } + + /** + * Validates parameters and permissions. + */ + public function validatePull() { + $this->readString('sessionID', true); + if ($this->parameters['sessionID']) { + $this->parameters['sessionID'] = pack('H*', str_replace('-', '', $this->parameters['sessionID'])); + } + + $this->readInteger('roomID'); + $this->readBoolean('inLog', true); + + $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); + if ($room === null) throw new UserInputException('roomID'); + if (!$room->canSee($user = null, $reason)) throw $reason; + $user = new \chat\data\user\User(WCF::getUser()); + if (!$this->parameters['inLog'] && !$user->isInRoom($room)) throw new PermissionDeniedException(); + if ($this->parameters['inLog'] && !$room->canSeeLog(null, $reason)) throw $reason; + + $this->readInteger('from', true); + $this->readInteger('to', true); + + // One may not pass both 'from' and 'to' + if ($this->parameters['from'] && $this->parameters['to']) { + throw new UserInputException(); + } + } + + /** + * Pulls messages for the given room. + */ + public function pull() { + $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); + if ($room === null) throw new UserInputException('roomID'); + + if (($sessionID = $this->parameters['sessionID'])) { + if (strlen($sessionID) !== 16) throw new UserInputException('sessionID'); + + (new \chat\data\user\UserAction([], 'clearDeadSessions'))->executeAction(); + + WCF::getDB()->beginTransaction(); + // update timestamp + $sql = "UPDATE chat".WCF_N."_room_to_user + SET lastPull = ? + WHERE roomID = ? + AND userID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ TIME_NOW + , $room->roomID + , WCF::getUser()->userID + ]); + + $sql = "UPDATE chat".WCF_N."_session + SET lastRequest = ? + WHERE roomID = ? + AND userID = ? + AND sessionID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ TIME_NOW + , $room->roomID + , WCF::getUser()->userID + , $sessionID + ]); + WCF::getDB()->commitTransaction(); + } + + // Determine message types supporting fast select + $objectTypes = \wcf\data\object\type\ObjectTypeCache::getInstance()->getObjectTypes('be.bastelstu.chat.messageType'); + $fastSelect = array_map(function ($item) { + return $item->objectTypeID; + }, array_filter($objectTypes, function ($item) { + return $item->getProcessor()->supportsFastSelect(); + })); + + // Build fast select filter + $condition = new \wcf\system\database\util\PreparedStatementConditionBuilder(); + $condition->add('((roomID = ? AND objectTypeID IN (?)) OR objectTypeID NOT IN (?))', [ $room->roomID, $fastSelect, $fastSelect ]); + + $sortOrder = 'DESC'; + // Add offset + if ($this->parameters['from']) { + $condition->add('messageID >= ?', [ $this->parameters['from'] ]); + $sortOrder = 'ASC'; + } + if ($this->parameters['to']) { + $condition->add('messageID <= ?', [ $this->parameters['to'] ]); + } + + $sql = "SELECT messageID + FROM chat".WCF_N."_message + ".$condition." + ORDER BY messageID ".$sortOrder; + $statement = WCF::getDB()->prepareStatement($sql, 20); + $statement->execute($condition->getParameters()); + $messageIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); + + $objectList = new MessageList(); + $objectList->setObjectIDs($messageIDs); + $objectList->readObjects(); + $objects = $objectList->getObjects(); + + $canSeeLog = $room->canSeeLog(); + $messages = array_map(function (Message $item) use ($room) { + return new ViewableMessage($item, $room); + }, array_filter($objects, function (Message $message) use ($canSeeLog, $room) { + if ($this->parameters['inLog'] || $message->isInLog()) { + return $canSeeLog && $message->getMessageType()->getProcessor()->canSeeInLog($message, $room); + } + else { + return $message->getMessageType()->getProcessor()->canSee($message, $room); + } + })); + + $embeddedObjectMessageIDs = array_map(function ($message) { + return $message->messageID; + }, array_filter($messages, function ($message) { + return $message->hasEmbeddedObjects; + })); + + if (!empty($embeddedObjectMessageIDs)) { + // load embedded objects + \wcf\system\message\embedded\object\MessageEmbeddedObjectManager::getInstance()->loadObjects('be.bastelstu.chat.message', $embeddedObjectMessageIDs); + } + + return [ 'messages' => $messages + , 'from' => $this->parameters['from'] ?: (!empty($objects) ? reset($objects)->messageID : $this->parameters['to'] + 1) + , 'to' => $this->parameters['to'] ?: (!empty($objects) ? end($objects)->messageID : $this->parameters['from'] - 1) + ]; + } + + /** + * Validates parameters and permissions. + */ + public function validatePush() { + $this->readInteger('roomID'); + + $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); + if ($room === null) throw new UserInputException('roomID'); + if (!$room->canSee($user = null, $reason)) throw $reason; + $user = new \chat\data\user\User(WCF::getUser()); + if (!$user->isInRoom($room)) throw new PermissionDeniedException(); + + $this->readInteger('commandID'); + $command = CommandCache::getInstance()->getCommand($this->parameters['commandID']); + if ($command === null) throw new UserInputException('commandID'); + if (!$command->hasTriggers()) { + if (!$command->getProcessor()->allowWithoutTrigger()) { + throw new UserInputException('commandID'); + } + } + + $this->readJSON('parameters', true); + } + + /** + * Pushes a new message into the given room. + */ + public function push() { + $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); + if ($room === null) throw new UserInputException('roomID'); + + $command = CommandCache::getInstance()->getCommand($this->parameters['commandID']); + if ($command === null) throw new UserInputException('commandID'); + + $processor = $command->getProcessor(); + $processor->validate($this->parameters['parameters'], $room); + $processor->execute($this->parameters['parameters'], $room); + } +} diff --git a/files/lib/data/message/MessageEditor.class.php b/files/lib/data/message/MessageEditor.class.php new file mode 100644 index 0000000..9cc429f --- /dev/null +++ b/files/lib/data/message/MessageEditor.class.php @@ -0,0 +1,25 @@ +room = $room; + } + + /** + * @inheritDoc + */ + public function jsonSerialize() { + $link = LinkHandler::getInstance()->getLink('Log', [ 'application' => 'chat' + , 'messageid' => $this->messageID + , 'object' => $this->room + ]); + + if ($this->isDeleted) { + $payload = false; + $objectType = 'be.bastelstu.chat.messageType.tombstone'; + } + else { + $payload = $this->getMessageType()->getProcessor()->getPayload($this->getDecoratedObject()); + $objectType = $this->getMessageType()->objectType; + } + + return [ 'messageID' => $this->messageID + , 'userID' => $this->userID + , 'username' => $this->username + , 'time' => $this->time + , 'payload' => $payload + , 'objectType' => $objectType + , 'link' => $link + , 'isIgnored' => WCF::getUserProfileHandler()->isIgnoredUser($this->userID) + , 'isDeleted' => (bool) $this->isDeleted + ]; + } +} diff --git a/files/lib/data/room/Room.class.php b/files/lib/data/room/Room.class.php new file mode 100644 index 0000000..f1ff514 --- /dev/null +++ b/files/lib/data/room/Room.class.php @@ -0,0 +1,262 @@ +getTitle(); + } + + /** + * Returns whether the given user can see at least + * one chat room. If no user is given the current user + * should be assumed + * + * @param \wcf\data\user\UserProfile $user + * @return boolean + */ + public static function canSeeAny(\wcf\data\user\UserProfile $user = null) { + $rooms = RoomCache::getInstance()->getRooms(); + foreach ($rooms as $room) { + if ($room->canSee($user)) return true; + } + + return false; + } + + /** + * Returns whether the given user can see this room. + * If no user is given the current user should be assumed. + * + * @param \wcf\data\user\UserProfile $user + * @return boolean + */ + public function canSee(\wcf\data\user\UserProfile $user = null, \Exception &$reason = null) { + static $cache = [ ]; + if ($user === null) $user = new \wcf\data\user\UserProfile(WCF::getUser()); + + if (!isset($cache[$this->roomID])) $cache[$this->roomID] = []; + if (array_key_exists($user->userID, $cache[$this->roomID])) { + return ($reason = $cache[$this->roomID][$user->userID]) === null; + } + + if (!$user->userID) { + $reason = new PermissionDeniedException(); + return ($cache[$this->roomID][$user->userID] = $reason) === null; + } + + $result = null; + if (!PermissionHandler::get($user)->getPermission($this, 'user.canSee')) { + $result = new PermissionDeniedException(); + } + + $parameters = [ 'user' => $user + , 'result' => $result + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + $reason = $parameters['result']; + + if (!($reason === null || $reason instanceof \Exception || $reason instanceof \Throwable)) { + throw new \DomainException('Result of canSee must be a \Throwable or null.'); + } + + return ($cache[$this->roomID][$user->userID] = $reason) === null; + } + + /** + * Returns whether the given user can see the log of this room. + * If no user is given the current user should be assumed. + * + * @param \wcf\data\user\UserProfile $user + * @return boolean + */ + public function canSeeLog(\wcf\data\user\UserProfile $user = null, \Exception &$reason = null) { + static $cache = [ ]; + if ($user === null) $user = new \wcf\data\user\UserProfile(WCF::getUser()); + + if (!isset($cache[$this->roomID])) $cache[$this->roomID] = []; + if (array_key_exists($user->userID, $cache[$this->roomID])) { + return ($reason = $cache[$this->roomID][$user->userID]) === null; + } + + $result = null; + if (!PermissionHandler::get($user)->getPermission($this, 'user.canSeeLog')) { + $result = new PermissionDeniedException(); + } + + $parameters = [ 'user' => $user + , 'result' => $result + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeLog', $parameters); + $reason = $parameters['result']; + + if (!($reason === null || $reason instanceof \Exception || $reason instanceof \Throwable)) { + throw new \DomainException('Result of canSeeLog must be a \Throwable or null.'); + } + + return ($cache[$this->roomID][$user->userID] = $reason) === null; + } + + /** + * Returns whether the given user can join this room. + * If no user is given the current user should be assumed. + * + * @param \wcf\data\user\UserProfile $user + * @return boolean + */ + public function canJoin(\wcf\data\user\UserProfile $user = null, \Exception &$reason = null) { + static $cache = [ ]; + if ($user === null) $user = new \wcf\data\user\UserProfile(WCF::getUser()); + + if (!isset($cache[$this->roomID])) $cache[$this->roomID] = []; + if (array_key_exists($user->userID, $cache[$this->roomID])) { + return ($reason = $cache[$this->roomID][$user->userID]) === null; + } + + $parameters = [ 'user' => $user + , 'result' => null + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canJoin', $parameters); + $reason = $parameters['result']; + + if (!($reason === null || $reason instanceof \Exception || $reason instanceof \Throwable)) { + throw new \DomainException('Result of canJoin must be a \Throwable or null.'); + } + + return ($cache[$this->roomID][$user->userID] = $reason) === null; + } + + /** + * Returns whether the given user can write public messages in this room. + * If no user is given the current user should be assumed. + * + * @param \wcf\data\user\UserProfile $user + * @return boolean + */ + public function canWritePublicly(\wcf\data\user\UserProfile $user = null, \Exception &$reason = null) { + static $cache = [ ]; + if ($user === null) $user = new \wcf\data\user\UserProfile(WCF::getUser()); + + if (!isset($cache[$this->roomID])) $cache[$this->roomID] = []; + if (array_key_exists($user->userID, $cache[$this->roomID])) { + return ($reason = $cache[$this->roomID][$user->userID]) === null; + } + + $result = null; + if (!PermissionHandler::get($user)->getPermission($this, 'user.canWrite')) { + $result = new PermissionDeniedException(); + } + + $parameters = [ 'user' => $user + , 'result' => $result + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canWritePublicly', $parameters); + $reason = $parameters['result']; + + if (!($reason === null || $reason instanceof \Exception || $reason instanceof \Throwable)) { + throw new \DomainException('Result of canWritePublicly must be a \Throwable or null.'); + } + + return ($cache[$this->roomID][$user->userID] = $reason) === null; + } + + /** + * @inheritDoc + */ + public function getTitle() { + return WCF::getLanguage()->get($this->title); + } + + /** + * @inheritDoc + */ + public function getTopic() { + $topic = StringUtil::trim(WCF::getLanguage()->get($this->topic)); + + if (!$this->topicUseHtml) { + $topic = StringUtil::encodeHTML($topic); + } + + return $topic; + } + + /** + * Returns an array of users in this room. + */ + public function getUsers() { + if (self::$userToRoom === null) { + $sql = "SELECT r2u.userID, r2u.roomID + FROM chat".WCF_N."_room_to_user r2u + INNER JOIN wcf".WCF_N."_user u + ON r2u.userID = u.userID + WHERE r2u.active = ? + ORDER BY u.username ASC"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ 1 ]); + self::$userToRoom = $statement->fetchMap('roomID', 'userID', false); + + if (!empty(self::$userToRoom)) { + UserRuntimeCache::getInstance()->cacheObjectIDs(array_merge(...self::$userToRoom)); + } + } + + if (!isset(self::$userToRoom[$this->roomID])) return [ ]; + + return UserRuntimeCache::getInstance()->getObjects(self::$userToRoom[$this->roomID]); + } + + /** + * @inheritDoc + */ + public function getLink() { + return LinkHandler::getInstance()->getLink('Room', [ 'application' => 'chat' + , 'object' => $this + , 'forceFrontend' => true + ] + ); + } + + /** + * @inheritDoc + */ + public function jsonSerialize() { + return [ 'title' => $this->getTitle() + , 'topic' => $this->getTopic() + , 'link' => $this->getLink() + ]; + } +} diff --git a/files/lib/data/room/RoomAction.class.php b/files/lib/data/room/RoomAction.class.php new file mode 100644 index 0000000..3d5b7c2 --- /dev/null +++ b/files/lib/data/room/RoomAction.class.php @@ -0,0 +1,376 @@ +parameters['user']); + + $this->readString('sessionID'); + $this->parameters['sessionID'] = pack('H*', str_replace('-', '', $this->parameters['sessionID'])); + + $this->readInteger('roomID'); + + $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); + if ($room === null) throw new UserInputException('roomID'); + if (!$room->canSee($user = null, $reason)) throw $reason; + if (!$room->canJoin($user = null, $reason)) throw $reason; + } + + /** + * Makes the given user join the current chat room. + */ + public function join() { + $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.messageType', 'be.bastelstu.chat.messageType.join'); + if (!$objectTypeID) throw new \LogicException('Missing object type'); + // User cannot be set during an AJAX request, but may be set by Tim’s Chat itself. + if (!isset($this->parameters['user'])) $this->parameters['user'] = WCF::getUser(); + $user = new ChatUser($this->parameters['user']); + + // Check parameters + $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); + if ($room === null) throw new UserInputException('roomID'); + $sessionID = $this->parameters['sessionID']; + if (strlen($sessionID) !== 16) throw new UserInputException('sessionID'); + + try { + // Create room_to_user mapping. + $sql = "INSERT INTO chat".WCF_N."_room_to_user (active, roomID, userID) VALUES (?, ?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ 0, $room->roomID, $user->userID ]); + } + catch (\wcf\system\database\exception\DatabaseException $e) { + // Ignore if there already is a mapping. + if ((string) $e->getCode() !== '23000') throw $e; + } + + try { + $sql = "INSERT INTO chat".WCF_N."_session (roomID, userID, sessionID, lastRequest) VALUES (?, ?, ?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ $room->roomID, $user->userID, $sessionID, TIME_NOW ]); + } + catch (\wcf\system\database\exception\DatabaseException $e) { + if ((string) $e->getCode() !== '23000') throw $e; + + throw new UserInputException('sessionID'); + } + + $markAsBack = function () use ($user, $room) { + $userProfile = new \wcf\data\user\UserProfile($user->getDecoratedObject()); + $package = \wcf\data\package\PackageCache::getInstance()->getPackageByIdentifier('be.bastelstu.chat'); + $command = \chat\data\command\CommandCache::getInstance()->getCommandByPackageAndIdentifier($package, 'back'); + $processor = $command->getProcessor(); + $processor->execute([ ], $room, $userProfile); + }; + + if ($user->chatAway !== null) { + $markAsBack(); + } + + // Attempt to mark the user as active in the room. + $sql = "UPDATE chat".WCF_N."_room_to_user SET active = ? WHERE roomID = ? AND userID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ 1, $room->roomID, $user->userID ]); + if ($statement->getAffectedRows() === 0) { + // The User already is inside the room: Nothing to do here. + return; + } + + // Update lastPull. This must not be merged into the above query, because of the 'getAffectedRows' check. + $sql = "UPDATE chat".WCF_N."_room_to_user SET lastPull = ? WHERE roomID = ? AND userID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ TIME_NOW, $room->roomID, $user->userID ]); + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ ]) + ] + ] + ) + )->executeAction(); + + UserActivityPointHandler::getInstance()->fireEvent('be.bastelstu.chat.activityPointEvent.join', 0, $user->userID); + $pushHandler = \wcf\system\push\PushHandler::getInstance(); + $pushHandler->sendMessage([ 'message' => 'be.bastelstu.chat.join' + , 'target' => 'registered' + ]); + } + + /** + * Validates parameters and permissions. + */ + public function validateLeave() { + unset($this->parameters['user']); + + $this->readString('sessionID'); + $this->parameters['sessionID'] = pack('H*', str_replace('-', '', $this->parameters['sessionID'])); + + $this->readInteger('roomID'); + $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); + if ($room === null) throw new UserInputException('roomID'); + // Do not check permissions: If the user is not inside the room nothing happens, if he is it + // may lead to a faster eviction of the user. + } + + /** + * Makes the given user leave the current chat room. + */ + public function leave() { + $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.messageType', 'be.bastelstu.chat.messageType.leave'); + if ($objectTypeID) { + // User cannot be set during an AJAX request, but may be set by Tim’s Chat itself. + if (!isset($this->parameters['user'])) $this->parameters['user'] = WCF::getUser(); + $user = new ChatUser($this->parameters['user']); + + $room = RoomCache::getInstance()->getRoom($this->parameters['roomID']); + if ($room === null) throw new UserInputException('roomID'); + + $sessionID = null; + if (isset($this->parameters['sessionID'])) { + $sessionID = $this->parameters['sessionID']; + if (strlen($sessionID) !== 16) throw new UserInputException('sessionID'); + } + + // Delete session. + $condition = new \wcf\system\database\util\PreparedStatementConditionBuilder(); + $condition->add('roomID = ?', [ $room->roomID ]); + $condition->add('userID = ?', [ $user->userID ]); + if ($sessionID !== null) { + $condition->add('sessionID = ?', [ $sessionID ]); + } + $sql = "DELETE FROM chat".WCF_N."_session + ".$condition; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($condition->getParameters()); + if ($statement->getAffectedRows() === 0) { + throw new UserInputException('sessionID'); + } + + try { + $commited = false; + WCF::getDB()->beginTransaction(); + + // Check whether we deleted the last session. + $sql = "SELECT COUNT(*) + FROM chat".WCF_N."_session + WHERE roomID = ? + AND userID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ $room->roomID, $user->userID ]); + + // We did not: Nothing to do here. + if ($statement->fetchColumn()) return; + + // Mark the user as inactive. + $sql = "UPDATE chat".WCF_N."_room_to_user SET active = ? WHERE roomID = ? AND userID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ 0, $room->roomID, $user->userID ]); + if ($statement->getAffectedRows() === 0) throw new \LogicException('Unreachable'); + + WCF::getDB()->commitTransaction(); + $commited = true; + } + finally { + if (!$commited) WCF::getDB()->rollBackTransaction(); + } + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ ]) + ] + ] + ) + )->executeAction(); + + $pushHandler = \wcf\system\push\PushHandler::getInstance(); + $pushHandler->sendMessage([ 'message' => 'be.bastelstu.chat.leave' + , 'target' => 'registered' + ]); + } + else { + throw new \LogicException('Missing object type'); + } + } + + /** + * Validates parameters and permissions. + */ + public function validateGetUsers() { + if (empty($this->getObjects())) { + $this->readObjects(); + } + if (count($this->getObjects()) !== 1) { + throw new UserInputException('objectIDs'); + } + + $room = $this->getObjects()[0]; + + $user = new ChatUser(WCF::getUser()); + if (!$user->isInRoom($room->getDecoratedObject())) throw new PermissionDeniedException(); + } + + /** + * Returns the userIDs of the users in this room. + */ + public function getUsers() { + if (empty($this->getObjects())) { + $this->readObjects(); + } + if (count($this->getObjects()) !== 1) { + throw new UserInputException('objectIDs'); + } + + $room = $this->getObjects()[0]; + + $users = (new \chat\data\user\UserAction([ ], 'getUsersByID', [ + 'userIDs' => array_keys($room->getUsers()) + ]))->executeAction()['returnValues']; + + $users = array_map(function (array $user) use ($room) { + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($user['userID']); + if (!isset($user['permissions'])) $user['permissions'] = []; + $user['permissions']['canWritePublicly'] = $room->canWritePublicly($userProfile); + + return $user; + }, $users); + + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getUsers', $users); + + return $users; + } + + /** + * @inheritDoc + */ + public function validateUpdatePosition() { + // validate permissions + if (is_array($this->permissionsUpdate) && !empty($this->permissionsUpdate)) { + WCF::getSession()->checkPermissions($this->permissionsUpdate); + } + else { + throw new PermissionDeniedException(); + } + + $this->readIntegerArray('structure', false, 'data'); + + $roomList = new RoomList(); + $roomList->readObjects(); + + foreach ($this->parameters['data']['structure'][0] as $roomID) { + $room = $roomList->search($roomID); + if ($room === null) throw new UserInputException('structure'); + } + } + + /** + * @inheritDoc + */ + public function updatePosition() { + $roomList = new RoomList(); + $roomList->readObjects(); + + $i = 0; + WCF::getDB()->beginTransaction(); + foreach ($this->parameters['data']['structure'][0] as $roomID) { + $room = $roomList->search($roomID); + if ($room === null) continue; + + $editor = new RoomEditor($room); + $editor->update([ 'position' => $i++ ]); + } + WCF::getDB()->commitTransaction(); + } + + /** + * Validates permissions. + */ + public function validateGetBoxRoomList() { + if (!\chat\data\room\Room::canSeeAny()) throw new \wcf\system\exception\PermissionDeniedException(); + + $this->readBoolean('isSidebar', true); + $this->readBoolean('skipEmptyRooms', true); + $this->readInteger('activeRoomID', true); + + unset($this->parameters['boxController']); + $this->readInteger('boxID', true); + if ($this->parameters['boxID']) { + $box = new \wcf\data\box\Box($this->parameters['boxID']); + if ($box->boxID) { + $this->parameters['boxController'] = $box->getController(); + if ($this->parameters['boxController'] instanceof \chat\system\box\RoomListBoxController) { + // all checks passed, end validation; otherwise throw the exception below + return; + } + } + + throw new UserInputException('boxID'); + } + } + + /** + * Returns dashboard roomlist. + */ + public function getBoxRoomList() { + if (isset($this->parameters['boxController'])) { + $this->parameters['boxController']->setActiveRoomID($this->parameters['activeRoomID']); + + return [ 'template' => $this->parameters['boxController']->getContent() ]; + } + + // Fetch all rooms, the templates have filtering in place + $rooms = RoomCache::getInstance()->getRooms(); + + $template = 'boxRoomList'.($this->parameters['isSidebar'] ? 'Sidebar' : ''); + + \wcf\system\WCF::getTPL()->assign([ 'boxRoomList' => $rooms + , 'skipEmptyRooms' => $this->parameters['skipEmptyRooms'] + , 'activeRoomID' => $this->parameters['activeRoomID'] + ]); + + return [ 'template' => \wcf\system\WCF::getTPL()->fetch($template, 'chat') ]; + } +} diff --git a/files/lib/data/room/RoomCache.class.php b/files/lib/data/room/RoomCache.class.php new file mode 100644 index 0000000..3ed2888 --- /dev/null +++ b/files/lib/data/room/RoomCache.class.php @@ -0,0 +1,66 @@ +rooms = \chat\system\cache\builder\RoomCacheBuilder::getInstance()->getData(); + } + + /** + * Returns a specific room. + * + * @param integer $roomID + * @return Room + */ + public function getRoom($roomID) { + if (isset($this->rooms[$roomID])) { + return $this->rooms[$roomID]; + } + + return null; + } + + /** + * Returns all rooms. + * + * @return Room[] + */ + public function getRooms() { + return $this->rooms; + } +} diff --git a/files/lib/data/room/RoomEditor.class.php b/files/lib/data/room/RoomEditor.class.php new file mode 100644 index 0000000..9701ff2 --- /dev/null +++ b/files/lib/data/room/RoomEditor.class.php @@ -0,0 +1,33 @@ +reset(); + \chat\system\permission\PermissionHandler::resetCache(); + } +} diff --git a/files/lib/data/room/RoomList.class.php b/files/lib/data/room/RoomList.class.php new file mode 100644 index 0000000..b26b9c8 --- /dev/null +++ b/files/lib/data/room/RoomList.class.php @@ -0,0 +1,25 @@ +getConditionBuilder()->add('(expires IS NULL OR expires > ?)', [ TIME_NOW ]); + $suspensionList->getConditionBuilder()->add('revoked IS NULL'); + $suspensionList->getConditionBuilder()->add('userID = ?', [ $user->userID ]); + $suspensionList->getConditionBuilder()->add('objectTypeID = ?', [ $objectTypeID ]); + $suspensionList->getConditionBuilder()->add('(roomID IS NULL OR roomID = ?)', [ $room->roomID ]); + + $suspensionList->readObjects(); + + return array_filter($suspensionList->getObjects(), function (Suspension $suspension) { + return $suspension->isActive(); + }); + } + + /** + * Returns the suspension object type of this message. + * + * @return \wcf\data\object\type\ObjectType + */ + public function getSuspensionType() { + return \wcf\data\object\type\ObjectTypeCache::getInstance()->getObjectType($this->objectTypeID); + } + + /** + * Returns whether this suspension still is in effect. + * + * @return boolean + */ + public function isActive() { + if ($this->revoked !== null) return false; + if (!$this->getSuspensionType()->getProcessor()->hasEffect($this)) return false; + + if ($this->expires === null) return true; + + return $this->expires > TIME_NOW; + } + + /** + * Returns the chat room this suspension is in effect. + * Returns null if this is a global suspension. + * + * @return \chat\data\room\Room + */ + public function getRoom() { + if ($this->roomID === null) { + return null; + } + + return \chat\data\room\RoomCache::getInstance()->getRoom($this->roomID); + } + + /** + * Returns the user that is affected by this suspension. + * + * @return \wcf\data\user\User + */ + public function getUser() { + return \wcf\system\cache\runtime\UserRuntimeCache::getInstance()->getObject($this->userID); + } + + /** + * @inheritDoc + */ + public function jsonSerialize() { + return [ 'userID' => $this->userID + , 'username' => $this->getUser()->username + , 'roomID' => $this->roomID + , 'time' => $this->time + , 'expires' => $this->expires + , 'reason' => $this->reason + , 'objectType' => $this->getSuspensionType()->objectType + , 'judgeID' => $this->judgeID + , 'judge' => $this->judge + ]; + } +} diff --git a/files/lib/data/suspension/SuspensionAction.class.php b/files/lib/data/suspension/SuspensionAction.class.php new file mode 100644 index 0000000..fedc9a6 --- /dev/null +++ b/files/lib/data/suspension/SuspensionAction.class.php @@ -0,0 +1,68 @@ +objects)) { + $this->readObjects(); + + if (empty($this->objects)) { + throw new UserInputException('objectIDs'); + } + } + + unset($this->parameters['revoker']); + + WCF::getSession()->checkPermissions([ 'admin.chat.canManageSuspensions' ]); + + foreach ($this->getObjects() as $object) { + if (!$object->isActive()) throw new UserInputException('objectIDs', 'nonActive'); + } + } + + /** + * Revokes the suspensions + */ + public function revoke() { + if (empty($this->objects)) { + $this->readObjects(); + } + + // User cannot be set during an AJAX request, but may be set by Tim’s Chat itself. + if (!isset($this->parameters['revoker'])) $this->parameters['revoker'] = WCF::getUser(); + + $data = [ 'revoked' => TIME_NOW + , 'revokerID' => $this->parameters['revoker']->userID + , 'revoker' => $this->parameters['revoker']->username + ]; + + $objectAction = new static($this->getObjects(), 'update', [ 'data' => $data ]); + $objectAction->executeAction(); + } +} diff --git a/files/lib/data/suspension/SuspensionEditor.class.php b/files/lib/data/suspension/SuspensionEditor.class.php new file mode 100644 index 0000000..174f0f0 --- /dev/null +++ b/files/lib/data/suspension/SuspensionEditor.class.php @@ -0,0 +1,25 @@ +roomToUser === null || $skipCache) { + $sql = "SELECT * + FROM chat".WCF_N."_room_to_user + WHERE userID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ $this->userID ]); + $this->roomToUser = [ ]; + while (($row = $statement->fetchArray())) { + $this->roomToUser[$row['roomID']] = $row; + } + } + + return $this->roomToUser; + } + + /** + * Returns an array of Rooms this user is part of. + * + * @return \chat\data\room\Room[] + */ + public function getRooms($skipCache = false) { + return array_map(function ($assoc) { + return \chat\data\room\RoomCache::getInstance()->getRoom($assoc['roomID']); + }, array_filter($this->getRoomAssociations($skipCache), function ($assoc) { + return $assoc['active'] === 1; + })); + } + + /** + * Returns whether the user is in the given room. + * + * @param \chat\data\room\Room $room + * @return boolean + */ + public function isInRoom(\chat\data\room\Room $room, $skipCache = false) { + $assoc = $this->getRoomAssociations($skipCache); + + if (!isset($assoc[$room->roomID])) return false; + return $assoc[$room->roomID]['active'] === 1; + } + + /** + * Returns (userID, roomID, sessionID) triples where the client died. + * + * @return mixed[][] + */ + public static function getDeadSessions() { + $sql = "SELECT userID, roomID, sessionID + FROM chat".WCF_N."_session + WHERE lastRequest < ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ TIME_NOW - 60 * 3 ]); + + return $statement->fetchAll(\PDO::FETCH_ASSOC); + } + + /** + * @inheritDoc + */ + public function jsonSerialize() { + return [ 'userID' => $this->userID + , 'username' => $this->username + , 'link' => $this->getLink() + ]; + } +} diff --git a/files/lib/data/user/UserAction.class.php b/files/lib/data/user/UserAction.class.php new file mode 100644 index 0000000..5045c13 --- /dev/null +++ b/files/lib/data/user/UserAction.class.php @@ -0,0 +1,118 @@ +readIntegerArray('userIDs'); + } + + /** + * Returns information about the users identified by the given userIDs. + */ + public function getUsersByID() { + $userList = UserProfileRuntimeCache::getInstance()->getObjects($this->parameters['userIDs']); + + return array_map(function ($user) { + if (!$user) return null; + + $payload = [ 'image16' => $user->getAvatar()->getImageTag(16) + , 'image24' => $user->getAvatar()->getImageTag(24) + , 'image32' => $user->getAvatar()->getImageTag(32) + , 'image48' => $user->getAvatar()->getImageTag(48) + , 'imageUrl' => $user->getAvatar()->getURL() + , 'link' => $user->getLink() + , 'anchor' => $user->getAnchorTag() + , 'userID' => $user->userID + , 'username' => $user->username + , 'userTitle' => $user->getUserTitle() + , 'userRankClass' => $user->getRank() ? $user->getRank()->cssClassName : null + , 'formattedUsername' => $user->getFormattedUsername() + , 'away' => $user->chatAway + , 'color1' => $user->chatColor1 + , 'color2' => $user->chatColor2 + ]; + + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getUsersByID', $payload); + + return $payload; + }, $userList); + } + + /** + * Clears dead clients. + */ + public function clearDeadSessions() { + $sessions = User::getDeadSessions(); + if (empty($sessions)) return; + $userIDs = array_map(function ($item) { + return $item['userID']; + }, $sessions); + $users = UserRuntimeCache::getInstance()->getObjects($userIDs); + foreach ($sessions as $session) { + $parameters = [ 'user' => $users[$session['userID']] + , 'roomID' => $session['roomID'] + , 'sessionID' => $session['sessionID'] + ]; + try { + (new \chat\data\room\RoomAction([ ], 'leave', $parameters))->executeAction(); + } + catch (UserInputException $e) { + // Probably some other request has been faster to remove this session, ignore + } + } + } + + /** + * @inheritDoc + */ + public function create() { + throw new \BadMethodCallException(); + } + + /** + * @inheritDoc + */ + public function update() { + throw new \BadMethodCallException(); + } + + /** + * @inheritDoc + */ + public function delete() { + throw new \BadMethodCallException(); + } +} diff --git a/files/lib/data/user/UserList.class.php b/files/lib/data/user/UserList.class.php new file mode 100644 index 0000000..363fda9 --- /dev/null +++ b/files/lib/data/user/UserList.class.php @@ -0,0 +1,25 @@ +roomID = intval($_GET['id']); + $this->room = \chat\data\room\RoomCache::getInstance()->getRoom($this->roomID); + + if ($this->room === null) throw new IllegalLinkException(); + if (!$this->room->canSee($user = null, $reason)) throw $reason; + if (!$this->room->canSeeLog($user = null, $reason)) throw $reason; + + if (isset($_GET['messageid'])) $this->messageID = intval($_GET['messageid']); + if ($this->messageID) { + $this->message = new \chat\data\message\Message($this->messageID); + if (!$this->message->getMessageType()->getProcessor()->canSeeInLog($this->message, $this->room)) { + throw new PermissionDeniedException(); + } + } + + if (isset($_REQUEST['datetime'])) $this->datetime = strtotime($_REQUEST['datetime']); + } + + /** + * @inheritDoc + */ + public function readData() { + parent::readData(); + + if ($this->datetime) { + // Determine message types supporting fast select + $objectTypes = \wcf\data\object\type\ObjectTypeCache::getInstance()->getObjectTypes('be.bastelstu.chat.messageType'); + $fastSelect = array_map(function ($item) { + return $item->objectTypeID; + }, array_filter($objectTypes, function ($item) { + // TODO: Consider a method couldAppearInLog(): bool + return $item->getProcessor()->supportsFastSelect(); + })); + + $minimum = 0; + $loops = 0; + do { + // Build fast select filter + $condition = new \wcf\system\database\util\PreparedStatementConditionBuilder(); + $condition->add('((roomID = ? AND objectTypeID IN (?)) OR objectTypeID NOT IN (?))', [ $this->room->roomID, $fastSelect, $fastSelect ]); + $condition->add('time >= ?', [ $this->datetime ]); + if ($minimum) { + $condition->add('messageID > ?', [ $minimum ]); + } + + $sql = "SELECT messageID + FROM chat".WCF_N."_message + ".$condition." + ORDER BY messageID ASC"; + $statement = WCF::getDB()->prepareStatement($sql, 20); + $statement->execute($condition->getParameters()); + $messageIDs = $statement->fetchAll(\PDO::FETCH_COLUMN); + + $objectList = new MessageList(); + $objectList->setObjectIDs($messageIDs); + $objectList->readObjects(); + $objects = $objectList->getObjects(); + if (empty($objects)) { + // TODO: UserInputException? + throw new IllegalLinkException(); + } + + foreach ($objects as $message) { + if ($message->getMessageType()->getProcessor()->canSeeInLog($message, $this->room)) { + $parameters = [ 'application' => 'chat' + , 'messageid' => $message->messageID + , 'object' => $this->room + ]; + \wcf\util\HeaderUtil::redirect(\wcf\system\request\LinkHandler::getInstance()->getLink('Log', $parameters)); + exit; + } + $minimum = $message->messageID; + } + } + while (++$loops <= 3); + + // Do a best guess redirect to an ID that is as near as possible + $parameters = [ 'application' => 'chat' + , 'messageid' => $minimum + , 'object' => $this->room + ]; + \wcf\util\HeaderUtil::redirect(\wcf\system\request\LinkHandler::getInstance()->getLink('Log', $parameters)); + exit; + } + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + PageLocationManager::getInstance()->addParentLocation('be.bastelstu.chat.Room', $this->room->roomID, $this->room); + WCF::getTPL()->assign([ 'room' => $this->room + , 'roomList' => \chat\data\room\RoomCache::getInstance()->getRooms() + , 'messageID' => $this->messageID + , 'message' => $this->message + , 'config' => $this->getConfig() + ]); + } +} diff --git a/files/lib/page/RoomListPage.class.php b/files/lib/page/RoomListPage.class.php new file mode 100644 index 0000000..9e8ce0a --- /dev/null +++ b/files/lib/page/RoomListPage.class.php @@ -0,0 +1,61 @@ +rooms = \chat\data\room\RoomCache::getInstance()->getRooms(); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ 'rooms' => $this->rooms ]); + } +} diff --git a/files/lib/page/RoomPage.class.php b/files/lib/page/RoomPage.class.php new file mode 100644 index 0000000..d6bbf2d --- /dev/null +++ b/files/lib/page/RoomPage.class.php @@ -0,0 +1,109 @@ +roomID = intval($_GET['id']); + $this->room = \chat\data\room\RoomCache::getInstance()->getRoom($this->roomID); + + if ($this->room === null) throw new IllegalLinkException(); + if (!$this->room->canSee($user = null, $reason)) throw $reason; + if (!$this->room->canJoin($user = null, $reason)) throw $reason; + + $this->canonicalURL = $this->room->getLink(); + } + + /** + * @inheritDoc + */ + public function checkPermissions() { + parent::checkPermissions(); + + $package = \wcf\data\package\PackageCache::getInstance()->getPackageByIdentifier('be.bastelstu.chat'); + if (stripos($package->packageVersion, 'Alpha') !== false) { + $sql = "SELECT COUNT(*) FROM wcf".WCF_N."_user"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(); + $userCount = $statement->fetchSingleColumn(); + if ((($userCount > 5 && !OFFLINE) || ($userCount > 30 && OFFLINE)) && sha1(WCF_UUID) !== '643a6b3af2a6ea3d393c4d8371e75d7d1b66e0d0') { + throw new PermissionDeniedException("Do not use alpha versions of Tims Chat in production communities!"); + } + } + } + + /** + * @inheritDoc + */ + public function readData() { + $sql = "SELECT 1"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(); + if ($statement->fetchSingleColumn() !== 1) { + throw new NamedUserException('PHP must be configured to use the MySQLnd driver, instead of libmysqlclient.'); + } + + parent::readData(); + + $pushHandler = \wcf\system\push\PushHandler::getInstance(); + $pushHandler->joinChannel('be.bastelstu.chat'); + $pushHandler->joinChannel('be.bastelstu.chat.room-'.$this->room->roomID); + } + + /** + * @inheritDoc + */ + public function assignVariables() { + parent::assignVariables(); + + WCF::getTPL()->assign([ 'room' => $this->room + , 'config' => $this->getConfig() + ]); + } +} diff --git a/files/lib/page/TConfiguredPage.class.php b/files/lib/page/TConfiguredPage.class.php new file mode 100644 index 0000000..c33e681 --- /dev/null +++ b/files/lib/page/TConfiguredPage.class.php @@ -0,0 +1,60 @@ +getTriggers(); + + $commands = array_map(function (Command $item) { + $package = PackageCache::getInstance()->getPackage($item->packageID)->package; + return [ 'package' => $package + , 'identifier' => $item->identifier + , 'commandID' => $item->commandID + , 'module' => $item->getProcessor()->getJavaScriptModuleName() + , 'isAvailable' => $item->getProcessor()->isAvailable($this->room) && ($item->hasTriggers() || $item->getProcessor()->allowWithoutTrigger()) + ]; + }, CommandCache::getInstance()->getCommands()); + + $messageTypes = array_map(function ($item) { + return [ 'module' => $item->getProcessor()->getJavaScriptModuleName() + ]; + }, ObjectTypeCache::getInstance()->getObjectTypes('be.bastelstu.chat.messageType')); + + $config = [ 'clientVersion' => 1 + , 'reloadTime' => (int) CHAT_RELOADTIME + , 'autoAwayTime' => (int) CHAT_AUTOAWAYTIME + , 'commands' => $commands + , 'triggers' => $triggers + , 'messageTypes' => $messageTypes + ]; + + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'config', $config); + + return \wcf\util\JSON::encode($config); + } +} diff --git a/files/lib/system/CHATCore.class.php b/files/lib/system/CHATCore.class.php new file mode 100644 index 0000000..e11d996 --- /dev/null +++ b/files/lib/system/CHATCore.class.php @@ -0,0 +1,38 @@ +setStaticController('chat', 'Log'); + $route->setBuildSchema('/{controller}/{id}-{title}/{messageid}'); + $route->setPattern('~^/?(?P[^/]+)/(?P\d+)(?:-(?P[^/]+))?/(?P<messageid>\d+)~x'); + $route->setRequiredComponents([ 'id' => '~^\d+$~' + , 'messageid' => '~^\d+$~' + ]); + $route->setMatchController(true); + + \wcf\system\request\RouteHandler::getInstance()->addRoute($route); + } +} diff --git a/files/lib/system/box/RoomListBoxController.class.php b/files/lib/system/box/RoomListBoxController.class.php new file mode 100644 index 0000000..f3f59fb --- /dev/null +++ b/files/lib/system/box/RoomListBoxController.class.php @@ -0,0 +1,120 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\box; + +use \wcf\system\request\RequestHandler; +use \wcf\system\WCF; + +/** + * Dynamic box controller implementation for a list of rooms. + */ +class RoomListBoxController extends \wcf\system\box\AbstractDatabaseObjectListBoxController { + /** + * @inheritDoc + */ + protected static $supportedPositions = [ 'contentBottom', 'contentTop', 'sidebarLeft', 'sidebarRight' ]; + + /** + * @inheritDoc + */ + protected $conditionDefinition = 'be.bastelstu.chat.box.roomList.condition'; + + /** + * @var int + */ + protected $activeRoomID = null; + + /** + * @inheritDoc + */ + public function __construct() { + parent::__construct(); + + $activeRequest = RequestHandler::getInstance()->getActiveRequest(); + if ($activeRequest && $activeRequest->getRequestObject() instanceof \chat\page\RoomPage) { + $this->activeRoomID = $activeRequest->getRequestObject()->room->roomID; + } + } + + /** + * Sets the active room ID. + */ + public function setActiveRoomID($activeRoomID) { + $this->activeRoomID = $activeRoomID; + } + + /** + * Returns the active room ID. + * + * @return int + */ + public function getActiveRoomID() { + return $this->activeRoomID; + } + + /** + * @inheritDoc + */ + public function hasLink() { + return true; + } + + /** + * @inheritDoc + */ + public function getLink() { + return \wcf\system\request\LinkHandler::getInstance()->getLink('RoomList', [ 'application' => 'chat' ]); + } + + /** + * @inheritDoc + */ + protected function getObjectList() { + return new \chat\data\room\RoomList(); + } + + /** + * @inheritDoc + */ + protected function getTemplate() { + $templateName = 'boxRoomList'; + if ($this->box->position === 'sidebarLeft' || $this->box->position === 'sidebarRight') { + $templateName = 'boxRoomListSidebar'; + } + + return WCF::getTPL()->fetch($templateName, 'chat', [ 'boxRoomList' => $this->objectList + , 'boxID' => $this->getBox()->boxID + , 'activeRoomID' => $this->activeRoomID ?: 0 + ], true); + } + + /** + * @inheritDoc + */ + public function hasContent() { + if ($this->box->position === 'sidebarLeft' || $this->box->position === 'sidebarRight') { + parent::hasContent(); + + foreach ($this->objectList as $room) { + if ($room->canSee()) return true; + } + + return false; + } + else { + return \chat\data\room\Room::canSeeAny(); + } + } +} diff --git a/files/lib/system/cache/builder/CommandCacheBuilder.class.php b/files/lib/system/cache/builder/CommandCacheBuilder.class.php new file mode 100644 index 0000000..f9262bb --- /dev/null +++ b/files/lib/system/cache/builder/CommandCacheBuilder.class.php @@ -0,0 +1,52 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\cache\builder; + +use \wcf\system\WCF; + +/** + * Caches all chat commands. + */ +class CommandCacheBuilder extends \wcf\system\cache\builder\AbstractCacheBuilder { + /** + * @see \wcf\system\cache\AbstractCacheBuilder::rebuild() + */ + public function rebuild(array $parameters) { + $data = [ 'commands' => [ ] + , 'triggers' => [ ] + , 'packages' => [ ] + ]; + + $commandList = new \chat\data\command\CommandList(); + $commandList->sqlOrderBy = 'command.commandID'; + $commandList->readObjects(); + + $data['commands'] = $commandList->getObjects(); + + foreach ($data['commands'] as $command) { + if (!isset($data['packages'][$command->packageID])) $data['packages'][$command->packageID] = [ ]; + $data['packages'][$command->packageID][$command->identifier] = $command; + } + + $sql = "SELECT * + FROM chat".WCF_N."_command_trigger"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute(); + + $data['triggers'] = $statement->fetchMap('commandTrigger', 'commandID'); + + return $data; + } +} diff --git a/files/lib/system/cache/builder/PermissionCacheBuilder.class.php b/files/lib/system/cache/builder/PermissionCacheBuilder.class.php new file mode 100644 index 0000000..5e2671f --- /dev/null +++ b/files/lib/system/cache/builder/PermissionCacheBuilder.class.php @@ -0,0 +1,61 @@ +<?php +/** + * Copyright (C) 2010-2017 Tim Düsterhus + * Copyright (C) 2010-2017 Woltlab GmbH + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +namespace chat\system\cache\builder; + +use \wcf\system\acl\ACLHandler; +use \wcf\system\WCF; + +/** + * Caches the chat permissions for a combination of user groups. + */ +class PermissionCacheBuilder extends \wcf\system\cache\builder\AbstractCacheBuilder { + /** + * @inheritDoc + */ + public function rebuild(array $parameters) { + $data = [ ]; + + if (!empty($parameters)) { + $conditionBuilder = new \wcf\system\database\util\PreparedStatementConditionBuilder(); + $conditionBuilder->add('acl_option.objectTypeID = ?', [ ACLHandler::getInstance()->getObjectTypeID('be.bastelstu.chat.room') ]); + $conditionBuilder->add('option_to_group.groupID IN (?)', [ $parameters ]); + $sql = "SELECT option_to_group.objectID AS roomID, + option_to_group.optionValue, + acl_option.optionName AS permission + FROM wcf".WCF_N."_acl_option acl_option + INNER JOIN wcf".WCF_N."_acl_option_to_group option_to_group + ON option_to_group.optionID = acl_option.optionID + ".$conditionBuilder; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($conditionBuilder->getParameters()); + while (($row = $statement->fetchArray())) { + if (!isset($data[$row['roomID']][$row['permission']])) { + $data[$row['roomID']][$row['permission']] = $row['optionValue']; + } + else { + $data[$row['roomID']][$row['permission']] = $row['optionValue'] || $data[$row['roomID']][$row['permission']]; + } + } + } + + return $data; + } +} diff --git a/files/lib/system/cache/builder/RoomCacheBuilder.class.php b/files/lib/system/cache/builder/RoomCacheBuilder.class.php new file mode 100644 index 0000000..ba7c89e --- /dev/null +++ b/files/lib/system/cache/builder/RoomCacheBuilder.class.php @@ -0,0 +1,31 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\cache\builder; + +/** + * Caches all chat rooms. + */ +class RoomCacheBuilder extends \wcf\system\cache\builder\AbstractCacheBuilder { + /** + * @inheritDoc + */ + public function rebuild(array $parameters) { + $roomList = new \chat\data\room\RoomList(); + $roomList->sqlOrderBy = "room.position"; + $roomList->readObjects(); + + return $roomList->getObjects(); + } +} diff --git a/files/lib/system/cache/runtime/UserRuntimeCache.class.php b/files/lib/system/cache/runtime/UserRuntimeCache.class.php new file mode 100644 index 0000000..b25aea1 --- /dev/null +++ b/files/lib/system/cache/runtime/UserRuntimeCache.class.php @@ -0,0 +1,25 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\cache\runtime; + +/** + * Runtime cache implementation for chat users. + */ +class UserRuntimeCache extends \wcf\system\cache\runtime\AbstractRuntimeCache { + /** + * @inheritDoc + */ + protected $listClassName = \chat\data\user\UserList::class; +} diff --git a/files/lib/system/command/AbstractCommand.class.php b/files/lib/system/command/AbstractCommand.class.php new file mode 100644 index 0000000..32bb2b1 --- /dev/null +++ b/files/lib/system/command/AbstractCommand.class.php @@ -0,0 +1,76 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\room\Room; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\data\user\UserProfile; + +/** + * Default implemention for command processors. + */ +abstract class AbstractCommand extends \wcf\data\DatabaseObjectDecorator implements ICommand + , \wcf\data\IDatabaseObjectProcessor { + /** + * @inheritDoc + */ + protected static $baseClass = \chat\data\command\Command::class; + + /** + * @inheritDoc + */ + public function isAvailable(Room $room, UserProfile $user = null) { + return true; + } + + /** + * @inheritDoc + */ + public function allowWithoutTrigger() { + return false; + } + + /** + * Returns the object type ID for the given message type. + * + * @param string + * @return int + */ + public function getMessageObjectTypeID($objectType) { + $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.messageType', $objectType); + + if (!$objectType) { + throw new \LogicException('Missing object type'); + } + + return $objectTypeID; + } + + /** + * Ensures that the given parameter exists in the parameter array and + * throws otherwise. + * + * @param array $parameters + * @param string $key + * @return mixed The value. + */ + public function assertParameter($parameters, $key) { + if (array_key_exists($key, $parameters)) { + return $parameters[$key]; + } + + throw new UserInputException('message'); + } +} diff --git a/files/lib/system/command/AbstractInputProcessedCommand.class.php b/files/lib/system/command/AbstractInputProcessedCommand.class.php new file mode 100644 index 0000000..8bbd004 --- /dev/null +++ b/files/lib/system/command/AbstractInputProcessedCommand.class.php @@ -0,0 +1,84 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \wcf\system\exception\UserInputException; +use \wcf\system\bbcode\BBCodeHandler; +use \wcf\system\message\censorship\Censorship; +use \wcf\system\WCF; + +/** + * Represents a command that processes the input using HtmlInputProcessor. + */ +abstract class AbstractInputProcessedCommand extends AbstractCommand { + /** + * HtmlInputProcessor to use. + * @var \wcf\system\html\input\HtmlInputProcessor + */ + protected $processor = null; + + /** + * The text processed last. + * @var string + */ + private $text = null; + + public function __construct(\wcf\data\DatabaseObject $object) { + parent::__construct($object); + + $this->processor = new \wcf\system\html\input\HtmlInputProcessor(); + $this->setDisallowedBBCodes(); + } + + private function setDisallowedBBCodes() { + BBCodeHandler::getInstance()->setDisallowedBBCodes(explode(',', WCF::getSession()->getPermission('user.chat.disallowedBBCodes'))); + } + + public function setText($text) { + if ($this->text === $text) return; + + $this->text = $text; + $this->setDisallowedBBCodes(); + $this->processor->process($text, 'be.bastelstu.chat.message', 0); + } + + public function validateText() { + if ($this->processor->appearsToBeEmpty()) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.global.form.error.empty')); + } + + $message = $this->processor->getTextContent(); + + // validate message length + if (mb_strlen($message) > CHAT_MAX_LENGTH) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.tooLong', [ 'maxTextLength' => CHAT_MAX_LENGTH ])); + } + + // search for disallowed bbcodes + $this->setDisallowedBBCodes(); + $disallowedBBCodes = $this->processor->validate(); + if (!empty($disallowedBBCodes)) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.disallowedBBCodes', [ 'disallowedBBCodes' => $disallowedBBCodes ])); + } + + // search for censored words + if (ENABLE_CENSORSHIP) { + $result = Censorship::getInstance()->test($message); + if ($result) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.censoredWordsFound', [ 'censoredWords' => $result ])); + } + } + } +} diff --git a/files/lib/system/command/AbstractSuspensionCommand.class.php b/files/lib/system/command/AbstractSuspensionCommand.class.php new file mode 100644 index 0000000..e29780a --- /dev/null +++ b/files/lib/system/command/AbstractSuspensionCommand.class.php @@ -0,0 +1,170 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \chat\data\suspension\Suspension; +use \chat\data\suspension\SuspensionAction; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\UserInputException; +use \wcf\system\WCF; + +/** + * Represents a command that creates suspensions + */ +abstract class AbstractSuspensionCommand extends AbstractCommand { + use TNeedsUser; + + /** + * Returns the name of the object type for this suspension. + * + * @return string + */ + abstract public function getObjectTypeName(); + + /** + * Checks the permissions to execute this command. + * Throws if necessary. + * + * @see \chat\system\command\ICommand::validate() + */ + abstract protected function checkPermissions($parameters, Room $room, UserProfile $user); + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $this->assertParameter($parameters, 'username'); + $this->assertParameter($parameters, 'globally'); + $this->assertParameter($parameters, 'duration'); + $this->assertParameter($parameters, 'reason'); + + $this->assertUser($parameters['username']); + if ($parameters['duration'] !== null && $parameters['duration'] < TIME_NOW) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.datePast')); + } + if (!empty($parameters['reason']) && mb_strlen($parameters['reason']) > 100) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.tooLong', [ 'maxTextLength' => 250 ])); + } + $this->checkPermissions($parameters, $room, $user); + + $test = new Suspension(null, $this->getSuspensionData($parameters, $room, $user)); + if (!$test->isActive()) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.suspension.noEffect')); + } + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $data = $this->getSuspensionData($parameters, $room, $user); + $test = new Suspension(null, $data); + if (!$test->isActive()) { + return; + } + + WCF::getDB()->beginTransaction(); + $suspension = (new SuspensionAction([ ], 'create', [ 'data' => $data ]))->executeAction()['returnValues']; + + $this->afterCreate($suspension, $parameters, $room, $user); + WCF::getDB()->commitTransaction(); + } + + /** + * Creates chat messages informing about the suspension. + * + * @param \chat\data\suspension\Suspension $suspension + * @param mixed[] $parameters + * @param \chat\data\room\Room $room + * @param \wcf\data\user\UserProfile $user + */ + protected function afterCreate(Suspension $suspension, $parameters, Room $room, UserProfile $user) { + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.suspend'); + $target = $suspension->getUser(); + + if ($suspension->getRoom() === null) { + $roomIDs = array_map(function (Room $room) use ($user) { + return $room->roomID; + }, (new \chat\data\user\User($target))->getRooms()); + $roomIDs[] = $room->roomID; + } + else { + $roomIDs = [ $suspension->getRoom()->roomID ]; + } + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'suspension' => $suspension + , 'roomIDs' => $roomIDs + , 'globally' => $this->isGlobally($parameters) + , 'target' => [ 'userID' => $target->userID + , 'username' => $target->username + ] + ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + } + + /** + * Returns the database fields. + * + * @param mixed[] $parameters + * @param \chat\data\room\Room $room + * @param \wcf\data\user\UserProfile $user + * @return mixed[] + */ + protected function getSuspensionData($parameters, Room $room, UserProfile $user = null) { + $target = $this->getUser($parameters['username']); + $globally = $this->isGlobally($parameters); + $expires = $parameters['duration']; + $reason = $parameters['reason'] ?: ''; + + $roomID = $globally ? null : $room->roomID; + $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.suspension', $this->getObjectTypeName()); + + return [ 'time' => TIME_NOW + , 'expires' => $expires + , 'roomID' => $roomID + , 'userID' => $target->userID + , 'objectTypeID' => $objectTypeID + , 'reason' => $reason + , 'judgeID' => $user->userID + , 'judge' => $user->username + ]; + } + + /** + * Returns whether a global suspension was requested. + * + * @param mixed[] $parameters + * @return boolean + */ + protected function isGlobally($parameters) { + return $parameters['globally'] === true; + } +} diff --git a/files/lib/system/command/AbstractUnsuspensionCommand.class.php b/files/lib/system/command/AbstractUnsuspensionCommand.class.php new file mode 100644 index 0000000..4bdb21f --- /dev/null +++ b/files/lib/system/command/AbstractUnsuspensionCommand.class.php @@ -0,0 +1,162 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \chat\data\suspension\Suspension; +use \chat\data\suspension\SuspensionAction; +use \chat\data\suspension\SuspensionList; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\UserInputException; +use \wcf\system\WCF; + +/** + * Represents a command that revokes suspensions + */ +abstract class AbstractUnsuspensionCommand extends AbstractCommand { + use TNeedsUser; + + /** + * Returns the name of the object type for this suspension. + * + * @return string + */ + abstract public function getObjectTypeName(); + + /** + * Checks the permissions to execute this command. + * Throws if necessary. + * + * @see \chat\system\command\ICommand::validate() + */ + abstract protected function checkPermissions($parameters, Room $room, UserProfile $user); + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $this->assertParameter($parameters, 'username'); + $this->assertParameter($parameters, 'globally'); + + $this->assertUser($parameters['username']); + + $suspensions = $this->getSuspensionData($parameters, $room, $user); + if (empty($suspensions)) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.suspension.remove.empty')); + } + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $suspensions = $this->getSuspensionData($parameters, $room, $user); + + WCF::getDB()->beginTransaction(); + (new SuspensionAction($suspensions, 'revoke', [ ]))->executeAction(); + $this->afterCreate($suspensions, $parameters, $room, $user); + WCF::getDB()->commitTransaction(); + } + + /** + * Creates chat messages informing about the removed suspensions. + * + * @param \chat\data\suspension\Suspension[] $suspension + * @param mixed[] $parameters + * @param \chat\data\room\Room $room + * @param \wcf\data\user\UserProfile $user + */ + protected function afterCreate($suspensions, $parameters, Room $room, UserProfile $user) { + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.unsuspend'); + $target = $this->getUser($parameters['username']); + if ($this->isGlobally($parameters)) { + $roomIDs = array_map(function (Room $room) use ($user) { + return $room->roomID; + }, (new \chat\data\user\User($target))->getRooms()); + $roomIDs[] = $room->roomID; + } + else { + $roomIDs = [ $room->roomID ]; + } + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'objectType' => $this->getObjectTypeName() + , 'roomIDs' => $roomIDs + , 'globally' => $this->isGlobally($parameters) + , 'target' => [ 'userID' => $target->userID + , 'username' => $target->username + ] + ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + } + + /** + * Returns the active suspensions. + * + * @param mixed[] $parameters + * @param \chat\data\room\Room $room + * @param \wcf\data\user\UserProfile $user + * @return mixed[] + */ + protected function getSuspensionData($parameters, Room $room, UserProfile $user = null) { + $target = $this->getUser($parameters['username']); + + $roomID = $this->isGlobally($parameters) ? null : $room->roomID; + $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.suspension', $this->getObjectTypeName()); + + $suspensionList = new SuspensionList(); + + $suspensionList->getConditionBuilder()->add('(expires IS NULL OR expires > ?)', [ TIME_NOW ]); + $suspensionList->getConditionBuilder()->add('revoked IS NULL'); + $suspensionList->getConditionBuilder()->add('userID = ?', [ $target->userID ]); + $suspensionList->getConditionBuilder()->add('objectTypeID = ?', [ $objectTypeID ]); + if ($roomID === null) { + $suspensionList->getConditionBuilder()->add('roomID IS NULL'); + } + else { + $suspensionList->getConditionBuilder()->add('roomID = ?', [ $room->roomID ]); + } + + $suspensionList->readObjects(); + + return array_filter($suspensionList->getObjects(), function (Suspension $suspension) { + return $suspension->isActive(); + }); + } + + /** + * Returns whether a global suspension removal was requested. + * + * @param mixed[] $parameters + * @return boolean + */ + protected function isGlobally($parameters) { + return $parameters['globally'] === true; + } +} diff --git a/files/lib/system/command/AwayCommand.class.php b/files/lib/system/command/AwayCommand.class.php new file mode 100644 index 0000000..6b69044 --- /dev/null +++ b/files/lib/system/command/AwayCommand.class.php @@ -0,0 +1,87 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\exception\UserInputException; +use \wcf\system\message\censorship\Censorship; +use \wcf\system\WCF; + +/** + * The away command marks the user as being away. + */ +class AwayCommand extends AbstractCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Away'; + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + + $reason = $this->assertParameter($parameters, 'reason'); + + // search for censored words + if (ENABLE_CENSORSHIP) { + $result = Censorship::getInstance()->test($reason); + if ($result) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.censoredWordsFound', [ 'censoredWords' => $result ])); + } + } + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + + $reason = $this->assertParameter($parameters, 'reason'); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.away'); + $rooms = array_map(function (Room $room) use ($user) { + return [ 'roomID' => $room->roomID + , 'isSilent' => !$room->canWritePublicly($user) + ]; + }, (new \chat\data\user\User($user->getDecoratedObject()))->getRooms()); + + WCF::getDB()->beginTransaction(); + $editor = new \wcf\data\user\UserEditor($user->getDecoratedObject()); + $editor->update([ 'chatAway' => $reason ]); + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'message' => $reason + , 'rooms' => array_values($rooms) + ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + WCF::getDB()->commitTransaction(); + } +} diff --git a/files/lib/system/command/BackCommand.class.php b/files/lib/system/command/BackCommand.class.php new file mode 100644 index 0000000..c64cd6d --- /dev/null +++ b/files/lib/system/command/BackCommand.class.php @@ -0,0 +1,80 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * The back command marks the user as being back. + */ +class BackCommand extends AbstractCommand implements ICommand { + /** + * @inheritDoc + */ + public function allowWithoutTrigger() { + return true; + } + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Back'; + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + + if ($user->chatAway === null) throw new PermissionDeniedException(); + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.back'); + $rooms = array_map(function (Room $room) use ($user) { + return [ 'roomID' => $room->roomID + , 'isSilent' => !$room->canWritePublicly($user) + ]; + }, (new \chat\data\user\User($user->getDecoratedObject()))->getRooms()); + + WCF::getDB()->beginTransaction(); + $editor = new \wcf\data\user\UserEditor($user->getDecoratedObject()); + $editor->update([ 'chatAway' => null ]); + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'rooms' => array_values($rooms) ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + WCF::getDB()->commitTransaction(); + } +} diff --git a/files/lib/system/command/BanCommand.class.php b/files/lib/system/command/BanCommand.class.php new file mode 100644 index 0000000..fc784b9 --- /dev/null +++ b/files/lib/system/command/BanCommand.class.php @@ -0,0 +1,94 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\room\Room; +use \chat\data\suspension\Suspension; +use \chat\data\suspension\SuspensionAction; +use \chat\system\permission\PermissionHandler; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * The ban command creates a new be.bastelstu.chat.suspension.ban suspension. + */ +class BanCommand extends AbstractSuspensionCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Ban'; + } + + /** + * @inheritDoc + */ + public function isAvailable(Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + return $user->getPermission('mod.chat.canBan') || PermissionHandler::get($user)->getPermission($room, 'mod.canBan'); + } + + /** + * @inheritDoc + */ + public function getObjectTypeName() { + return 'be.bastelstu.chat.suspension.ban'; + } + + /** + * @inheritDoc + */ + protected function checkPermissions($parameters, Room $room, UserProfile $user) { + $permission = $user->getPermission('mod.chat.canBan'); + + if (!$this->isGlobally($parameters)) { + $permission = $permission || PermissionHandler::get($user)->getPermission($room, 'mod.canBan'); + } + + if (!$permission) throw new PermissionDeniedException(); + } + + /** + * @inheritDoc + */ + protected function afterCreate(Suspension $suspension, $parameters, Room $room, UserProfile $user) { + parent::afterCreate($suspension, $parameters, $room, $user); + + $user = new \chat\data\user\User($suspension->getUser()); + $rooms = [ ]; + if ($suspension->getRoom() === null) { + $rooms = $user->getRooms(); + } + else { + if ($user->isInRoom($suspension->getRoom())) { + $rooms = [ $suspension->getRoom() ]; + } + } + + foreach ($rooms as $room) { + $parameters = [ 'user' => $suspension->getUser() + , 'roomID' => $room->roomID + ]; + try { + (new \chat\data\room\RoomAction([ ], 'leave', $parameters))->executeAction(); + } + catch (UserInputException $e) { + // User already left + } + } + } +} diff --git a/files/lib/system/command/BroadcastCommand.class.php b/files/lib/system/command/BroadcastCommand.class.php new file mode 100644 index 0000000..446f494 --- /dev/null +++ b/files/lib/system/command/BroadcastCommand.class.php @@ -0,0 +1,85 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\message\MessageEditor; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * BroadcastCommand sends a broadcast into all channels. + */ +class BroadcastCommand extends AbstractInputProcessedCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Broadcast'; + } + + /** + * @inheritDoc + */ + public function isAvailable(Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + return $user->getPermission('mod.chat.canBroadcast'); + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + if (!$user->getPermission('mod.chat.canBroadcast')) throw new PermissionDeniedException(); + + $this->setText($this->assertParameter($parameters, 'text')); + $this->validateText(); + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.broadcast'); + $this->setText($this->assertParameter($parameters, 'text')); + + WCF::getDB()->beginTransaction(); + $message = (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'message' => $this->processor->getHtml() ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction()['returnValues']; + + $this->processor->setObjectID($message->messageID); + if (\wcf\system\message\embedded\object\MessageEmbeddedObjectManager::getInstance()->registerObjects($this->processor)) { + (new MessageEditor($message))->update([ + 'hasEmbeddedObjects' => 1 + ]); + } + WCF::getDB()->commitTransaction(); + } +} diff --git a/files/lib/system/command/ColorCommand.class.php b/files/lib/system/command/ColorCommand.class.php new file mode 100644 index 0000000..e8e705f --- /dev/null +++ b/files/lib/system/command/ColorCommand.class.php @@ -0,0 +1,285 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\exception\UserInputException; +use \wcf\system\WCF; +use \wcf\util\StringUtil; + +/** + * The color command allows a user to set a color for their username + */ +class ColorCommand extends AbstractCommand implements ICommand { + /** + * Regular expression matching RGB values in hexadecimal notation + * @var \wcf\system\Regex + */ + protected $colorRegex = null; + + public function __construct(\wcf\data\DatabaseObject $object) { + parent::__construct($object); + + $this->colorRegex = new \wcf\system\Regex('^#?([a-f0-9]{6})$', \wcf\system\Regex::CASE_INSENSITIVE); + } + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Color'; + } + + /** + * Map CSS color names to hexcodes. + * See: https://www.w3.org/TR/css3-color/#svg-color + * + * @var int[] + */ + public static $colors = [ + 'aliceblue' => 0xF0F8FF, + 'antiquewhite' => 0xFAEBD7, + 'aqua' => 0x00FFFF, + 'aquamarine' => 0x7FFFD4, + 'azure' => 0xF0FFFF, + 'beige' => 0xF5F5DC, + 'bisque' => 0xFFE4C4, + 'black' => 0x000000, + 'blanchedalmond' => 0xFFEBCD, + 'blue' => 0x0000FF, + 'bluescreenblue' => 0x0000AA, + 'blueviolet' => 0x8A2BE2, + 'brown' => 0xA52A2A, + 'burlywood' => 0xDEB887, + 'cadetblue' => 0x5F9EA0, + 'chartreuse' => 0x7FFF00, + 'chocolate' => 0xD2691E, + 'coral' => 0xFF7F50, + 'cornflowerblue' => 0x6495ED, + 'cornsilk' => 0xFFF8DC, + 'crimson' => 0xDC143C, + 'cyan' => 0x00FFFF, + 'darkblue' => 0x00008B, + 'darkcyan' => 0x008B8B, + 'darkgoldenrod' => 0xB8860B, + 'darkgray' => 0xA9A9A9, + 'darkgrey' => 0xA9A9A9, + 'darkgreen' => 0x006400, + 'darkkhaki' => 0xBDB76B, + 'darkmagenta' => 0x8B008B, + 'darkolivegreen' => 0x556B2F, + 'darkorange' => 0xFF8C00, + 'darkorchid' => 0x9932CC, + 'darkred' => 0x8B0000, + 'darksalmon' => 0xE9967A, + 'darkseagreen' => 0x8FBC8F, + 'darkslateblue' => 0x483D8B, + 'darkslategray' => 0x2F4F4F, + 'darkslategrey' => 0x2F4F4F, + 'darkturquoise' => 0x00CED1, + 'darkviolet' => 0x9400D3, + 'deeppink' => 0xFF1493, + 'deepskyblue' => 0x00BFFF, + 'dimgray' => 0x696969, + 'dimgrey' => 0x696969, + 'dodgerblue' => 0x1E90FF, + 'firebrick' => 0xB22222, + 'floralwhite' => 0xFFFAF0, + 'forestgreen' => 0x228B22, + 'fuchsia' => 0xFF00FF, + 'gainsboro' => 0xDCDCDC, + 'ghostwhite' => 0xF8F8FF, + 'gold' => 0xFFD700, + 'goldenrod' => 0xDAA520, + 'gray' => 0x808080, + 'grey' => 0x808080, + 'green' => 0x008000, + 'greenyellow' => 0xADFF2F, + 'honeydew' => 0xF0FFF0, + 'hotpink' => 0xFF69B4, + 'indianred' => 0xCD5C5C, + 'indigo' => 0x4B0082, + 'ivory' => 0xFFFFF0, + 'khaki' => 0xF0E68C, + 'lavender' => 0xE6E6FA, + 'lavenderblush' => 0xFFF0F5, + 'lawngreen' => 0x7CFC00, + 'lemonchiffon' => 0xFFFACD, + 'lightblue' => 0xADD8E6, + 'lightcoral' => 0xF08080, + 'lightcyan' => 0xE0FFFF, + 'lightgoldenrodyellow' => 0xFAFAD2, + 'lightgray' => 0xD3D3D3, + 'lightgrey' => 0xD3D3D3, + 'lightgreen' => 0x90EE90, + 'lightpink' => 0xFFB6C1, + 'lightsalmon' => 0xFFA07A, + 'lightseagreen' => 0x20B2AA, + 'lightskyblue' => 0x87CEFA, + 'lightslategray' => 0x778899, + 'lightslategrey' => 0x778899, + 'lightsteelblue' => 0xB0C4DE, + 'lightyellow' => 0xFFFFE0, + 'lime' => 0x00FF00, + 'limegreen' => 0x32CD32, + 'linen' => 0xFAF0E6, + 'magenta' => 0xFF00FF, + 'maroon' => 0x800000, + 'mediumaquamarine' => 0x66CDAA, + 'mediumblue' => 0x0000CD, + 'mediumorchid' => 0xBA55D3, + 'mediumpurple' => 0x9370D8, + 'mediumseagreen' => 0x3CB371, + 'mediumslateblue' => 0x7B68EE, + 'mediumspringgreen' => 0x00FA9A, + 'mediumturquoise' => 0x48D1CC, + 'mediumvioletred' => 0xC71585, + 'midnightblue' => 0x191970, + 'mintcream' => 0xF5FFFA, + 'mistyrose' => 0xFFE4E1, + 'moccasin' => 0xFFE4B5, + 'navajowhite' => 0xFFDEAD, + 'navy' => 0x000080, + 'oldlace' => 0xFDF5E6, + 'olive' => 0x808000, + 'olivedrab' => 0x6B8E23, + 'orange' => 0xFFA500, + 'orangered' => 0xFF4500, + 'orchid' => 0xDA70D6, + 'oxford' => 0xF02D, // looks like green + 'palegoldenrod' => 0xEEE8AA, + 'palegreen' => 0x98FB98, + 'paleturquoise' => 0xAFEEEE, + 'palevioletred' => 0xD87093, + 'papayawhip' => 0xFFEFD5, + 'peachpuff' => 0xFFDAB9, + 'peru' => 0xCD853F, + 'pink' => 0xFFC0CB, + 'plum' => 0xDDA0DD, + 'powderblue' => 0xB0E0E6, + 'purple' => 0x800080, + 'red' => 0xFF0000, + 'rosybrown' => 0xBC8F8F, + 'royalblue' => 0x4169E1, + 'saddlebrown' => 0x8B4513, + 'sadwin' => 0x2067B2, + 'salmon' => 0xFA8072, + 'sandybrown' => 0xF4A460, + 'seagreen' => 0x2E8B57, + 'seashell' => 0xFFF5EE, + 'sienna' => 0xA0522D, + 'silver' => 0xC0C0C0, + 'skyblue' => 0x87CEEB, + 'slateblue' => 0x6A5ACD, + 'slategray' => 0x708090, + 'slategrey' => 0x708090, + 'snow' => 0xFFFAFA, + 'springgreen' => 0x00FF7F, + 'steelblue' => 0x4682B4, + 'tan' => 0xD2B48C, + 'teal' => 0x008080, + 'thistle' => 0xD8BFD8, + 'tomato' => 0xFF6347, + 'turquoise' => 0x40E0D0, + 'violet' => 0xEE82EE, + 'wheat' => 0xF5DEB3, + 'white' => 0xFFFFFF, + 'whitesmoke' => 0xF5F5F5, + 'yellow' => 0xFFFF00, + 'yellowgreen' => 0x9ACD32 + ]; + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + + if (!$user->getPermission('user.chat.canSetColor')) throw new PermissionDeniedException(); + + foreach ($parameters as $parameter) { + $value = StringUtil::trim($this->assertParameter($parameter, 'value')); + $valid = true; + + switch ($this->assertParameter($parameter, 'type')) { + case 'hex': + if (!$this->colorRegex->match($value)) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.invalidColor', [ 'color' => $value ])); + } + break; + case 'word': + if (!isset(self::$colors[$value])) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.invalidColor', [ 'color' => $value ])); + } + break; + + default: + throw new UserInputException('message'); + } + } + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.color'); + $colors = [ ]; + + if (!isset($parameters[1])) $parameters[1] = $parameters[0]; + + foreach ($parameters as $key => $parameter) { + $value = StringUtil::trim($this->assertParameter($parameter, 'value')); + + switch ($this->assertParameter($parameter, 'type')) { + case 'hex': + $colors[$key] = hexdec($value); + break; + case 'word': + if (!isset(self::$colors[$value])) throw new UserInputException('message'); + $colors[$key] = self::$colors[$value]; + break; + default: + throw new UserInputException('message'); + } + } + + WCF::getDB()->beginTransaction(); + $editor = new \wcf\data\user\UserEditor($user->getDecoratedObject()); + $editor->update([ 'chatColor1' => $colors[0] + , 'chatColor2' => $colors[1] + ]); + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'color1' => $colors[0] + , 'color2' => $colors[1] + ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + WCF::getDB()->commitTransaction(); + } +} diff --git a/files/lib/system/command/ICommand.class.php b/files/lib/system/command/ICommand.class.php new file mode 100644 index 0000000..8c143f0 --- /dev/null +++ b/files/lib/system/command/ICommand.class.php @@ -0,0 +1,76 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * Interface for Command processors. + */ +interface ICommand { + /** + * Returns whether the command can be used even when + * no trigger is configured for it. + * + * @return boolean + */ + public function allowWithoutTrigger(); + + /** + * Returns the name of the JavaScript module. + * + * @return string + */ + public function getJavaScriptModuleName(); + + /** + * Returns whether this command theoretically is available + * in the given room, for the given user. + * If no user is given the active user should be assumed. + * + * The return value sets a flag for the JavaScript to + * consume. You still need to validate() this as well! + * + * @param Room $room + * @param UserProfile $user + * @return boolean + */ + public function isAvailable(Room $room, UserProfile $user = null); + + /** + * Validates the execution of the command with the given parameters + * in the given room for the given user. + * If no user is given the active user should be assumed. + * This method must throw if the command may not be executed in this form. + * + * @param mixed $parameters + * @param Room $room + * @param UserProfile $user + */ + public function validate($parameters, Room $room, UserProfile $user = null); + + /** + * Executes the command with the given parameters in the given room in + * the context of the given user. + * If no user is given the active user should be assumed. + * This method must throw if the command may not be executed in this form. + * + * @param mixed $parameters + * @param Room $room + * @param UserProfile $user + */ + public function execute($parameters, Room $room, UserProfile $user = null); +} diff --git a/files/lib/system/command/InfoCommand.class.php b/files/lib/system/command/InfoCommand.class.php new file mode 100644 index 0000000..5a09644 --- /dev/null +++ b/files/lib/system/command/InfoCommand.class.php @@ -0,0 +1,88 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \chat\data\room\RoomCache; +use \wcf\data\user\User; +use \wcf\data\user\UserProfile; + +/** + * The info command shows information about a single user. + */ +class InfoCommand extends AbstractCommand implements ICommand { + use TNeedsUser; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Info'; + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $this->assertUser($this->assertParameter($parameters, 'username')); + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.info'); + $target = new \chat\data\user\User($this->getUser($this->assertParameter($parameters, 'username'))); + $rooms = array_values(array_map(function ($assoc) { + $room = RoomCache::getInstance()->getRoom($assoc['roomID']); + + return [ 'title' => (string) $room + , 'roomID' => $assoc['roomID'] + , 'lastPush' => $assoc['lastPush'] + , 'lastPull' => $assoc['lastPull'] + , 'active' => $assoc['active'] + , 'link' => $room->getLink() + ]; + }, array_filter($target->getRoomAssociations(), function ($assoc) { + return RoomCache::getInstance()->getRoom($assoc['roomID'])->canSee(); + }))); + + $payload = [ 'data' => [ 'rooms' => $rooms + , 'away' => $target->chatAway + , 'user' => $target + ] + , 'caller' => $user + ]; + + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'execute', $payload); + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize($payload['data']) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + } +} diff --git a/files/lib/system/command/MeCommand.class.php b/files/lib/system/command/MeCommand.class.php new file mode 100644 index 0000000..600784d --- /dev/null +++ b/files/lib/system/command/MeCommand.class.php @@ -0,0 +1,84 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\exception\UserInputException; +use \wcf\system\message\censorship\Censorship; +use \wcf\system\WCF; + +/** + * MeCommand represents an action message. + */ +class MeCommand extends AbstractCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Me'; + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + if (!$room->canWritePublicly($user)) throw new PermissionDeniedException(); + + $text = $this->assertParameter($parameters, 'text'); + + if (mb_strlen($text) === 0) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.global.form.error.empty')); + } + + // validate message length + if (mb_strlen($text) > CHAT_MAX_LENGTH) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.tooLong', [ 'maxTextLength' => CHAT_MAX_LENGTH ])); + } + + // search for censored words + if (ENABLE_CENSORSHIP) { + $result = Censorship::getInstance()->test($text); + if ($result) { + throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('wcf.message.error.censoredWordsFound', [ 'censoredWords' => $result ])); + } + } + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.me'); + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'message' => $this->assertParameter($parameters, 'text') ]) + ] + , 'updateTimestamp' => true + , 'grantPoints' => true + ] + ) + )->executeAction(); + } +} diff --git a/files/lib/system/command/MuteCommand.class.php b/files/lib/system/command/MuteCommand.class.php new file mode 100644 index 0000000..1f463a1 --- /dev/null +++ b/files/lib/system/command/MuteCommand.class.php @@ -0,0 +1,63 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\room\Room; +use \chat\data\suspension\SuspensionAction; +use \chat\system\permission\PermissionHandler; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * The mute command creates a new be.bastelstu.chat.suspension.mute suspension. + */ +class MuteCommand extends AbstractSuspensionCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Mute'; + } + + /** + * @inheritDoc + */ + public function isAvailable(Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + return $user->getPermission('mod.chat.canMute') || PermissionHandler::get($user)->getPermission($room, 'mod.canMute'); + } + + /** + * @inheritDoc + */ + public function getObjectTypeName() { + return 'be.bastelstu.chat.suspension.mute'; + } + + /** + * @inheritDoc + */ + protected function checkPermissions($parameters, Room $room, UserProfile $user) { + $permission = $user->getPermission('mod.chat.canMute'); + + if (!$this->isGlobally($parameters)) { + $permission = $permission || PermissionHandler::get($user)->getPermission($room, 'mod.canMute'); + } + + if (!$permission) throw new PermissionDeniedException(); + } +} diff --git a/files/lib/system/command/PlainCommand.class.php b/files/lib/system/command/PlainCommand.class.php new file mode 100644 index 0000000..2bc02a4 --- /dev/null +++ b/files/lib/system/command/PlainCommand.class.php @@ -0,0 +1,81 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\message\MessageEditor; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; + +/** + * The plain command creates a normal chat message + */ +class PlainCommand extends AbstractInputProcessedCommand implements ICommand { + /** + * @inheritDoc + */ + public function allowWithoutTrigger() { + return true; + } + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Plain'; + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + if (!$room->canWritePublicly($user)) throw new PermissionDeniedException(); + + $this->setText($this->assertParameter($parameters, 'text')); + $this->validateText(); + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.plain'); + $this->setText($this->assertParameter($parameters, 'text')); + $message = (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'message' => $this->processor->getHtml() ]) + ] + , 'updateTimestamp' => true + , 'grantPoints' => true + ] + ) + )->executeAction()['returnValues']; + + $this->processor->setObjectID($message->messageID); + if (\wcf\system\message\embedded\object\MessageEmbeddedObjectManager::getInstance()->registerObjects($this->processor)) { + (new MessageEditor($message))->update([ + 'hasEmbeddedObjects' => 1 + ]); + } + } +} diff --git a/files/lib/system/command/TNeedsUser.class.php b/files/lib/system/command/TNeedsUser.class.php new file mode 100644 index 0000000..8a12413 --- /dev/null +++ b/files/lib/system/command/TNeedsUser.class.php @@ -0,0 +1,53 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \wcf\data\user\User; +use \wcf\system\exception\UserInputException; +use \wcf\system\WCF; + +/** + * Adds helpful functions for commands that operate on a user. + */ +trait TNeedsUser { + /** + * Returns the user with the given username. + * + * @param string $username + * @return \wcf\data\user\User + */ + protected function getUser($username) { + static $cache = [ ]; + if (!isset($cache[$username])) { + $cache[$username] = User::getUserByUsername($username); + } + + return $cache[$username]; + } + + /** + * Checks whether the given username is valid and throws otherwise. + * + * @param string $username + * @return \wcf\data\user\User + */ + protected function assertUser($username) { + $user = $this->getUser($username); + + if (!$user->userID) throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.userNotFound', [ 'username' => $username ])); + + return $user; + } +} diff --git a/files/lib/system/command/TeamCommand.class.php b/files/lib/system/command/TeamCommand.class.php new file mode 100644 index 0000000..981572a --- /dev/null +++ b/files/lib/system/command/TeamCommand.class.php @@ -0,0 +1,86 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\message\MessageEditor; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\exception\UserInputException; +use \wcf\system\WCF; + +/** + * TeamCommand sends a broadcast to all team members. + */ +class TeamCommand extends AbstractInputProcessedCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Team'; + } + + /** + * @inheritDoc + */ + public function isAvailable(Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + return $user->getPermission('mod.chat.canTeam'); + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + if (!$user->getPermission('mod.chat.canTeam')) throw new PermissionDeniedException(); + + $this->setText($this->assertParameter($parameters, 'text')); + $this->validateText(); + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.team'); + $this->setText($this->assertParameter($parameters, 'text')); + + WCF::getDB()->beginTransaction(); + $message = (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'message' => $this->processor->getHtml() ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction()['returnValues']; + + $this->processor->setObjectID($message->messageID); + if (\wcf\system\message\embedded\object\MessageEmbeddedObjectManager::getInstance()->registerObjects($this->processor)) { + (new MessageEditor($message))->update([ + 'hasEmbeddedObjects' => 1 + ]); + } + WCF::getDB()->commitTransaction(); + } +} diff --git a/files/lib/system/command/TemproomCommand.class.php b/files/lib/system/command/TemproomCommand.class.php new file mode 100644 index 0000000..4c77298 --- /dev/null +++ b/files/lib/system/command/TemproomCommand.class.php @@ -0,0 +1,136 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\exception\UserInputException; +use \wcf\system\WCF; + +/** + * The temproom command allows a user to manage temporary rooms. + */ +class TemproomCommand extends AbstractCommand implements ICommand { + use TNeedsUser; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Temproom'; + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + + switch ($this->assertParameter($parameters, 'type')) { + case 'create': + if (!$user->getPermission('user.chat.canTemproom')) throw new PermissionDeniedException(); + break; + case 'invite': + if (!$room->isTemporary) throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.notInTemproom')); + if ($room->ownerID !== $user->userID) throw new PermissionDeniedException(); + + $recipient = new UserProfile($this->assertUser($this->assertParameter($parameters, 'username'))); + if ($recipient->isIgnoredUser($user->userID)) throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.userIgnoresYou', [ 'user' => $recipient ])); + break; + case 'delete': + if (!$room->isTemporary) throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.notInTemproom')); + if ($room->ownerID !== $user->userID) throw new PermissionDeniedException(); + break; + default: + throw new UserInputException('message'); + } + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + + switch ($this->assertParameter($parameters, 'type')) { + case 'create': + $fields = [ 'title' => WCF::getLanguage()->getDynamicVariable('chat.room.temporary.blueprint', [ 'user' => $user ]) + , 'topic' => '' + , 'position' => 999 + , 'isTemporary' => true + , 'ownerID' => $user->userID + ]; + + WCF::getDB()->beginTransaction(); + // create room + $tempRoom = (new \chat\data\room\RoomAction([], 'create', [ 'data' => $fields ]))->executeAction()['returnValues']; + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.temproomCreated'); + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'room' => $tempRoom ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + WCF::getDB()->commitTransaction(); + return; + case 'invite': + $recipient = $this->getUser($this->assertParameter($parameters, 'username')); + WCF::getDB()->beginTransaction(); + try { + $sql = "INSERT INTO chat".WCF_N."_room_temporary_invite + (userID, roomID) + VALUES (?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ $recipient->userID, $room->roomID ]); + } + catch (\wcf\system\database\DatabaseException $e) { + WCF::getDB()->rollBackTransaction(); + // Duplicate key errors don't cause harm. + if ((string) $e->getCode() !== '23000') throw $e; + return; + } + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.temproomInvited'); + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'recipient' => $recipient->userID + , 'recipientName' => $recipient->username + ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + WCF::getDB()->commitTransaction(); + + return; + case 'delete': + (new \chat\data\room\RoomAction([ $room ], 'delete'))->executeAction(); + return; + default: + throw new UserInputException('message'); + } + } +} diff --git a/files/lib/system/command/UnbanCommand.class.php b/files/lib/system/command/UnbanCommand.class.php new file mode 100644 index 0000000..b4976af --- /dev/null +++ b/files/lib/system/command/UnbanCommand.class.php @@ -0,0 +1,64 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\room\Room; +use \chat\data\suspension\Suspension; +use \chat\data\suspension\SuspensionAction; +use \chat\system\permission\PermissionHandler; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * The unban command revokes a new be.bastelstu.chat.suspension.ban suspension. + */ +class UnbanCommand extends AbstractUnsuspensionCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Unban'; + } + + /** + * @inheritDoc + */ + public function isAvailable(Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + return $user->getPermission('mod.chat.canBan') || PermissionHandler::get($user)->getPermission($room, 'mod.canBan'); + } + + /** + * @inheritDoc + */ + public function getObjectTypeName() { + return 'be.bastelstu.chat.suspension.ban'; + } + + /** + * @inheritDoc + */ + protected function checkPermissions($parameters, Room $room, UserProfile $user) { + $permission = $user->getPermission('mod.chat.canBan'); + + if (!$this->isGlobally($parameters)) { + $permission = $permission || PermissionHandler::get($user)->getPermission($room, 'mod.canBan'); + } + + if (!$permission) throw new PermissionDeniedException(); + } +} diff --git a/files/lib/system/command/UnmuteCommand.class.php b/files/lib/system/command/UnmuteCommand.class.php new file mode 100644 index 0000000..0b7410a --- /dev/null +++ b/files/lib/system/command/UnmuteCommand.class.php @@ -0,0 +1,63 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\room\Room; +use \chat\data\suspension\SuspensionAction; +use \chat\system\permission\PermissionHandler; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * The unmute command revokes a new be.bastelstu.chat.suspension.mute suspension. + */ +class UnmuteCommand extends AbstractUnsuspensionCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Unmute'; + } + + /** + * @inheritDoc + */ + public function isAvailable(Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(WCF::getUser()); + return $user->getPermission('mod.chat.canMute') || PermissionHandler::get($user)->getPermission($room, 'mod.canMute'); + } + + /** + * @inheritDoc + */ + public function getObjectTypeName() { + return 'be.bastelstu.chat.suspension.mute'; + } + + /** + * @inheritDoc + */ + protected function checkPermissions($parameters, Room $room, UserProfile $user) { + $permission = $user->getPermission('mod.chat.canMute'); + + if (!$this->isGlobally($parameters)) { + $permission = $permission || PermissionHandler::get($user)->getPermission($room, 'mod.canMute'); + } + + if (!$permission) throw new PermissionDeniedException(); + } +} diff --git a/files/lib/system/command/WhereCommand.class.php b/files/lib/system/command/WhereCommand.class.php new file mode 100644 index 0000000..a637b03 --- /dev/null +++ b/files/lib/system/command/WhereCommand.class.php @@ -0,0 +1,74 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\room\Room; +use \wcf\data\user\User; +use \wcf\data\user\UserProfile; + +/** + * The where command shows the distribution of users among + * the different chat rooms. + */ +class WhereCommand extends AbstractCommand implements ICommand { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Where'; + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.where'); + $roomList = new \chat\data\room\RoomList(); + $roomList->readObjects(); + $rooms = array_map(function (Room $room) { + $users = array_map(function (\chat\data\user\User $user) { + return $user->jsonSerialize(); + }, $room->getUsers()); + + return [ 'roomID' => $room->roomID + , 'users' => array_values($users) + ]; + }, array_filter($roomList->getObjects(), function (Room $room) { + return $room->canSee(); + })); + + (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize($rooms) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction(); + } +} diff --git a/files/lib/system/command/WhisperCommand.class.php b/files/lib/system/command/WhisperCommand.class.php new file mode 100644 index 0000000..b69c5e7 --- /dev/null +++ b/files/lib/system/command/WhisperCommand.class.php @@ -0,0 +1,83 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\command; + +use \chat\data\message\MessageAction; +use \chat\data\message\MessageEditor; +use \chat\data\room\Room; +use \wcf\data\user\User; +use \wcf\data\user\UserProfile; +use \wcf\system\exception\UserInputException; +use \wcf\system\WCF; + +/** + * The whisper command creates a private message + * between two chat users. + */ +class WhisperCommand extends AbstractInputProcessedCommand implements ICommand { + use TNeedsUser; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/Command/Whisper'; + } + + /** + * @inheritDoc + */ + public function validate($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $recipient = new UserProfile($this->assertUser($this->assertParameter($parameters, 'username'))); + if ($recipient->isIgnoredUser($user->userID)) throw new UserInputException('message', WCF::getLanguage()->getDynamicVariable('chat.error.userIgnoresYou', [ 'user' => $recipient ])); + + $this->setText($this->assertParameter($parameters, 'text')); + $this->validateText(); + } + + /** + * @inheritDoc + */ + public function execute($parameters, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $objectTypeID = $this->getMessageObjectTypeID('be.bastelstu.chat.messageType.whisper'); + $recipient = $this->assertUser($this->assertParameter($parameters, 'username')); + $this->setText($this->assertParameter($parameters, 'text')); + $message = (new MessageAction([ ], 'create', [ 'data' => [ 'roomID' => $room->roomID + , 'userID' => $user->userID + , 'username' => $user->username + , 'time' => TIME_NOW + , 'objectTypeID' => $objectTypeID + , 'payload' => serialize([ 'message' => $this->processor->getHtml() + , 'recipient' => $recipient->userID + , 'recipientName' => $recipient->username + ]) + ] + , 'updateTimestamp' => true + ] + ) + )->executeAction()['returnValues']; + + $this->processor->setObjectID($message->messageID); + if (\wcf\system\message\embedded\object\MessageEmbeddedObjectManager::getInstance()->registerObjects($this->processor)) { + (new MessageEditor($message))->update([ + 'hasEmbeddedObjects' => 1 + ]); + } + } +} diff --git a/files/lib/system/condition/room/RoomFilledCondition.class.php b/files/lib/system/condition/room/RoomFilledCondition.class.php new file mode 100644 index 0000000..8e6b942 --- /dev/null +++ b/files/lib/system/condition/room/RoomFilledCondition.class.php @@ -0,0 +1,46 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\condition\room; + +use \chat\data\room\RoomList; +use \wcf\data\DatabaseObject; +use \wcf\data\DatabaseObjectList; +use \wcf\system\exception\SystemException; + +/** + * Condition implementation for rooms to only include non-empty rooms in lists. + */ +class RoomFilledCondition extends \wcf\system\condition\AbstractCheckboxCondition implements \wcf\system\condition\IObjectListCondition { + /** + * @inheritDoc + */ + protected $fieldName = 'chatRoomIsFilled'; + + /** + * @inheritDoc + */ + protected $label = 'chat.room.condition.isFilled'; + + /** + * @inheritDoc + */ + public function addObjectListCondition(DatabaseObjectList $objectList, array $conditionData) { + if (!($objectList instanceof RoomList)) { + throw new \wcf\system\exception\ParentClassException(get_class($objectList), RoomList::class); + } + + $objectList->getConditionBuilder()->add("EXISTS (SELECT 1 FROM chat".WCF_N."_room_to_user r2u WHERE r2u.roomID = room.roomID AND active = ?)", [ 1 ]); + } +} diff --git a/files/lib/system/event/listener/HourlyCleanUpCronjobExecuteChatCleanUpListener.class.php b/files/lib/system/event/listener/HourlyCleanUpCronjobExecuteChatCleanUpListener.class.php new file mode 100644 index 0000000..fb47586 --- /dev/null +++ b/files/lib/system/event/listener/HourlyCleanUpCronjobExecuteChatCleanUpListener.class.php @@ -0,0 +1,40 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \wcf\system\WCF; + +/** + * Vaporizes unneeded data. + */ +class HourlyCleanUpCronjobExecuteChatCleanUpListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + (new \chat\data\message\MessageAction([ ], 'prune'))->executeAction(); + (new \chat\data\user\UserAction([], 'clearDeadSessions'))->executeAction(); + + $sql = "UPDATE chat".WCF_N."_room_to_user + SET active = ? + WHERE (roomID, userID) NOT IN (SELECT roomID, userID FROM chat".WCF_N."_session) + AND active = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ 0, 1 ]); + if ($statement->getAffectedRows()) { + \wcf\functions\exception\logThrowable(new \Exception('Unreachable')); + } + } +} diff --git a/files/lib/system/event/listener/HourlyCleanUpCronjobExecuteTemproomListener.class.php b/files/lib/system/event/listener/HourlyCleanUpCronjobExecuteTemproomListener.class.php new file mode 100644 index 0000000..215c4a6 --- /dev/null +++ b/files/lib/system/event/listener/HourlyCleanUpCronjobExecuteTemproomListener.class.php @@ -0,0 +1,43 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \wcf\system\WCF; + +/** + * Removes empty temporary rooms. + */ +class HourlyCleanUpCronjobExecuteTemproomListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + $roomList = new \chat\data\room\RoomList(); + $roomList->getConditionBuilder()->add('isTemporary = ?', [ 1 ]); + $roomList->readObjects(); + + $toDelete = [ ]; + WCF::getDB()->beginTransaction(); + foreach ($roomList as $room) { + if (count($room->getUsers()) === 0) { + $toDelete[] = $room; + } + } + if (!empty($toDelete)) { + (new \chat\data\room\RoomAction($toDelete, 'delete'))->executeAction(); + } + WCF::getDB()->commitTransaction(); + } +} diff --git a/files/lib/system/event/listener/InfoCommandSuspensionsListener.class.php b/files/lib/system/event/listener/InfoCommandSuspensionsListener.class.php new file mode 100644 index 0000000..9f075e2 --- /dev/null +++ b/files/lib/system/event/listener/InfoCommandSuspensionsListener.class.php @@ -0,0 +1,61 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \chat\data\suspension\Suspension; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\system\WCF; + +/** + * Fetches information about the users suspensions + */ +class InfoCommandSuspensionsListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + if (!$parameters['caller']->getPermission('admin.chat.canManageSuspensions')) { + return; + } + + $target = $parameters['data']['user']; + + $parameters['data']['suspensions'] = [ ]; + + $suspensionList = new \chat\data\suspension\SuspensionList(); + $suspensionList->getConditionBuilder()->add('(expires IS NULL OR expires > ?)', [ TIME_NOW ]); + $suspensionList->getConditionBuilder()->add('revoked IS NULL'); + $suspensionList->getConditionBuilder()->add('userID = ?', [ $target->userID ]); + $suspensionList->sqlOrderBy = 'expires ASC, time ASC'; + $suspensionList->readObjects(); + + $suspensions = array_filter($suspensionList->getObjects(), function (Suspension $suspension) { + return $suspension->isActive(); + }); + + $parameters['data']['suspensions'] = array_values(array_map(function ($suspension) { + $room = \chat\data\room\RoomCache::getInstance()->getRoom($suspension->roomID); + + $suspension = $suspension->jsonSerialize(); + if ($room) { + $suspension['room'] = [ 'title' => $room->getTitle() + , 'link' => $room->getLink() + ]; + } + + return $suspension; + }, $suspensions)); + } +} diff --git a/files/lib/system/event/listener/RoomActionGetUsersModeratorListener.class.php b/files/lib/system/event/listener/RoomActionGetUsersModeratorListener.class.php new file mode 100644 index 0000000..5ca555d --- /dev/null +++ b/files/lib/system/event/listener/RoomActionGetUsersModeratorListener.class.php @@ -0,0 +1,44 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \chat\data\command\CommandCache; +use \wcf\system\cache\runtime\UserProfileRuntimeCache; + +/** + * Adds moderator permissiosn to the user object. + */ +class RoomActionGetUsersModeratorListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$users) { + $room = $eventObj->getObjects()[0]->getDecoratedObject(); + + $package = \wcf\data\package\PackageCache::getInstance()->getPackageByIdentifier('be.bastelstu.chat'); + $muteCommand = CommandCache::getInstance()->getCommandByPackageAndIdentifier($package, 'mute')->getProcessor(); + $banCommand = CommandCache::getInstance()->getCommandByPackageAndIdentifier($package, 'ban')->getProcessor(); + + $users = array_map(function (array $user) use ($room, $muteCommand, $banCommand) { + $userProfile = UserProfileRuntimeCache::getInstance()->getObject($user['userID']); + if (!isset($user['permissions'])) $user['permissions'] = []; + + $user['permissions']['canMute'] = $muteCommand->isAvailable($room, $userProfile); + $user['permissions']['canBan'] = $banCommand->isAvailable($room, $userProfile); + + return $user; + }, $users); + } +} diff --git a/files/lib/system/event/listener/RoomCanJoinBanListener.class.php b/files/lib/system/event/listener/RoomCanJoinBanListener.class.php new file mode 100644 index 0000000..130d17f --- /dev/null +++ b/files/lib/system/event/listener/RoomCanJoinBanListener.class.php @@ -0,0 +1,38 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \chat\data\suspension\Suspension; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * Denies access to banned users. + */ +class RoomCanJoinBanListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.suspension', 'be.bastelstu.chat.suspension.ban'); + if (!$objectTypeID) throw new \LogicException('Unreachable'); + + $suspensions = Suspension::getActiveSuspensionsByTriple($objectTypeID, $parameters['user']->getDecoratedObject(), $eventObj); + if (!empty($suspensions)) { + $parameters['result'] = new PermissionDeniedException(WCF::getLanguage()->getDynamicVariable('chat.suspension.info.be.bastelstu.chat.suspension.ban')); + } + } +} diff --git a/files/lib/system/event/listener/RoomCanJoinUserLimitListener.class.php b/files/lib/system/event/listener/RoomCanJoinUserLimitListener.class.php new file mode 100644 index 0000000..f417686 --- /dev/null +++ b/files/lib/system/event/listener/RoomCanJoinUserLimitListener.class.php @@ -0,0 +1,42 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \chat\system\permission\PermissionHandler; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * Denies access when room is full. + */ +class RoomCanJoinUserLimitListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + if ($eventObj->userLimit === 0) return; + + $users = $eventObj->getUsers(); + if (count($users) < $eventObj->userLimit) return; + + $user = new \chat\data\user\User($parameters['user']->getDecoratedObject()); + if ($user->isInRoom($eventObj)) return; + + $canIgnoreLimit = PermissionHandler::get($parameters['user'])->getPermission($eventObj, 'mod.canIgnoreUserLimit'); + if ($canIgnoreLimit) return; + + $parameters['result'] = new PermissionDeniedException(WCF::getLanguage()->get('chat.error.roomFull')); + } +} diff --git a/files/lib/system/event/listener/RoomCanSeeTemproomListener.class.php b/files/lib/system/event/listener/RoomCanSeeTemproomListener.class.php new file mode 100644 index 0000000..e10245c --- /dev/null +++ b/files/lib/system/event/listener/RoomCanSeeTemproomListener.class.php @@ -0,0 +1,44 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \chat\system\permission\PermissionHandler; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * Denies access to temporary rooms, unless invited. + */ +class RoomCanSeeTemproomListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + if (!$eventObj->isTemporary) return; + + $user = new \chat\data\user\User($parameters['user']->getDecoratedObject()); + if ($eventObj->ownerID === $user->userID) return; + + $sql = "SELECT COUNT(*) + FROM chat".WCF_N."_room_temporary_invite + WHERE userID = ? + AND roomID = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ $user->userID, $eventObj->roomID ]); + if ($statement->fetchSingleColumn() > 0) return; + + $parameters['result'] = new PermissionDeniedException(); + } +} diff --git a/files/lib/system/event/listener/RoomCanWritePubliclyMuteListener.class.php b/files/lib/system/event/listener/RoomCanWritePubliclyMuteListener.class.php new file mode 100644 index 0000000..76cbec5 --- /dev/null +++ b/files/lib/system/event/listener/RoomCanWritePubliclyMuteListener.class.php @@ -0,0 +1,38 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \chat\data\suspension\Suspension; +use \wcf\data\object\type\ObjectTypeCache; +use \wcf\system\exception\PermissionDeniedException; +use \wcf\system\WCF; + +/** + * Denies access to muted users. + */ +class RoomCanWritePubliclyMuteListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + $objectTypeID = ObjectTypeCache::getInstance()->getObjectTypeIDByName('be.bastelstu.chat.suspension', 'be.bastelstu.chat.suspension.mute'); + if (!$objectTypeID) throw new \LogicException('Unreachable'); + + $suspensions = Suspension::getActiveSuspensionsByTriple($objectTypeID, $parameters['user']->getDecoratedObject(), $eventObj); + if (!empty($suspensions)) { + $parameters['result'] = new PermissionDeniedException(WCF::getLanguage()->getDynamicVariable('chat.suspension.info.be.bastelstu.chat.suspension.mute')); + } + } +} diff --git a/files/lib/system/event/listener/RoomEditFormTemproomListener.class.php b/files/lib/system/event/listener/RoomEditFormTemproomListener.class.php new file mode 100644 index 0000000..46837ba --- /dev/null +++ b/files/lib/system/event/listener/RoomEditFormTemproomListener.class.php @@ -0,0 +1,29 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +/** + * Disallow editing of temprooms in ACP. + */ +class RoomEditFormTemproomListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + if ($eventObj->room->isTemporary) { + throw new \wcf\system\exception\PermissionDeniedException(); + } + } +} diff --git a/files/lib/system/event/listener/RoomListPageTemproomListener.class.php b/files/lib/system/event/listener/RoomListPageTemproomListener.class.php new file mode 100644 index 0000000..a22d6c9 --- /dev/null +++ b/files/lib/system/event/listener/RoomListPageTemproomListener.class.php @@ -0,0 +1,27 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +/** + * Hides temprooms in ACP. + */ +class RoomListPageTemproomListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + $eventObj->objectList->getConditionBuilder()->add('isTemporary = ?', [ 0 ]); + } +} diff --git a/files/lib/system/event/listener/SuspensionListPageTemproomListener.class.php b/files/lib/system/event/listener/SuspensionListPageTemproomListener.class.php new file mode 100644 index 0000000..d46e138 --- /dev/null +++ b/files/lib/system/event/listener/SuspensionListPageTemproomListener.class.php @@ -0,0 +1,31 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\event\listener; + +use \chat\data\room\Room; + +/** + * Hides temprooms in ACP. + */ +class SuspensionListPageTemproomListener implements \wcf\system\event\listener\IParameterizedEventListener { + /** + * @see \wcf\system\event\listener\IParameterizedEventListener::execute() + */ + public function execute($eventObj, $className, $eventName, array &$parameters) { + $eventObj->availableRooms = array_filter($eventObj->availableRooms, function (Room $room) { + return !$room->isTemporary; + }); + } +} diff --git a/files/lib/system/message/type/AwayMessageType.class.php b/files/lib/system/message/type/AwayMessageType.class.php new file mode 100644 index 0000000..41328db --- /dev/null +++ b/files/lib/system/message/type/AwayMessageType.class.php @@ -0,0 +1,80 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * AwayMessageType represents a notice that a user now is away from chat. + */ +class AwayMessageType implements IMessageType { + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Away'; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $roomIDs = array_map(function ($item) { + return $item['roomID']; + }, $message->payload['rooms']); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => in_array($room->roomID, $roomIDs, true) + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $roomIDs = array_map(function ($item) { + return $item['roomID']; + }, $message->payload['rooms']); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => in_array($room->roomID, $roomIDs, true) + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/BackMessageType.class.php b/files/lib/system/message/type/BackMessageType.class.php new file mode 100644 index 0000000..d1e98cf --- /dev/null +++ b/files/lib/system/message/type/BackMessageType.class.php @@ -0,0 +1,80 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * BackMessageType represents a notice that a user now is now back. + */ +class BackMessageType implements IMessageType { + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Back'; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $roomIDs = array_map(function ($item) { + return $item['roomID']; + }, $message->payload['rooms']); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => in_array($room->roomID, $roomIDs, true) + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $roomIDs = array_map(function ($item) { + return $item['roomID']; + }, $message->payload['rooms']); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => in_array($room->roomID, $roomIDs, true) + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/BroadcastMessageType.class.php b/files/lib/system/message/type/BroadcastMessageType.class.php new file mode 100644 index 0000000..da70d29 --- /dev/null +++ b/files/lib/system/message/type/BroadcastMessageType.class.php @@ -0,0 +1,89 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * BroadcastMessageType represents a broadcasted message. + */ +class BroadcastMessageType extends PlainMessageType { + /** + * HtmlOutputProcessor to use. + * @var \wcf\system\html\output\HtmlOutputProcessor + */ + protected $processor = null; + + public function __construct() { + $this->processor = new \wcf\system\html\output\HtmlOutputProcessor(); + } + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Broadcast'; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => true + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => true + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @inheritDoc + */ + public function canDelete(\chat\data\message\Message $message, \wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + return $user->getPermission('mod.chat.canDelete'); + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/ChatUpdateMessageType.class.php b/files/lib/system/message/type/ChatUpdateMessageType.class.php new file mode 100644 index 0000000..8c14f33 --- /dev/null +++ b/files/lib/system/message/type/ChatUpdateMessageType.class.php @@ -0,0 +1,54 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * ChatUpdateMessageType informs the chat about a back end update. + */ +class ChatUpdateMessageType implements IMessageType { + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/ChatUpdate'; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + return true; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + return true; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/ColorMessageType.class.php b/files/lib/system/message/type/ColorMessageType.class.php new file mode 100644 index 0000000..39a800e --- /dev/null +++ b/files/lib/system/message/type/ColorMessageType.class.php @@ -0,0 +1,63 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * ColorMessageType represents a color message. + */ +class ColorMessageType implements IMessageType { + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Color'; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => true + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + return false; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/IDeletableMessageType.class.php b/files/lib/system/message/type/IDeletableMessageType.class.php new file mode 100644 index 0000000..e99defb --- /dev/null +++ b/files/lib/system/message/type/IDeletableMessageType.class.php @@ -0,0 +1,33 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \wcf\data\user\UserProfile; + +/** + * An IDeletableMessageType defines that the implementing message type supports message deletion. + */ +interface IDeletableMessageType extends IMessageType { + /** + * Returns whether the given user may delete the given message. If no + * user is given the active user should be assumed. + * + * @param Message $message + * @param UserProfile $user + * @return boolean + */ + public function canDelete(Message $message, UserProfile $user = null); +} diff --git a/files/lib/system/message/type/IMessageType.class.php b/files/lib/system/message/type/IMessageType.class.php new file mode 100644 index 0000000..ddd814b --- /dev/null +++ b/files/lib/system/message/type/IMessageType.class.php @@ -0,0 +1,74 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * An IMessageType defines how a message of a certain type is acted upon. + */ +interface IMessageType { + /** + * Returns the name of the JavaScript module. + * + * @return string + */ + public function getJavaScriptModuleName(); + + /** + * Returns whether the given user may see the given message. If no + * user is given the active user should be assumed. + * + * @param Message $message + * @param Room $room + * @param UserProfile $user + * @return boolean + */ + public function canSee(Message $message, Room $room, UserProfile $user = null); + + /** + * Returns whether the given user may see the given message in the + * protocol. If no user is given the active user should be assumed. + * + * @param Message $message + * @param Room $room + * @param UserProfile $user + * @return boolean + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null); + + /** + * Returns a filtered / extended version of the message payload. If no + * user is given the active user should be assumed. + * + * @param Message $message + * @param UserProfile $user + * @return array + */ + public function getPayload(Message $message, UserProfile $user = null); + + /** + * Returns whether this message type supports fast select of applicable messages: + * If this method returns true messages with this message type will only be selected + * if the room ID matches. If this method returns false messages will always be selected + * and filtered afterwards using canSee(). Returning false is useful e.g. for broadcasts. + * + * You SHOULD return true whenever possible, for performance reasons. You MUST only return + * true if canSee() would return false if the given $room is not equal to the $message's room. + */ + public function supportsFastSelect(); +} diff --git a/files/lib/system/message/type/InfoMessageType.class.php b/files/lib/system/message/type/InfoMessageType.class.php new file mode 100644 index 0000000..31c4479 --- /dev/null +++ b/files/lib/system/message/type/InfoMessageType.class.php @@ -0,0 +1,30 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +/** + * InfoMessageType represents the reply to InfoCommand. + */ +class InfoMessageType implements IMessageType { + use TCanSeeCreator; + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Info'; + } +} diff --git a/files/lib/system/message/type/JoinMessageType.class.php b/files/lib/system/message/type/JoinMessageType.class.php new file mode 100644 index 0000000..af1361b --- /dev/null +++ b/files/lib/system/message/type/JoinMessageType.class.php @@ -0,0 +1,30 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +/** + * JoinMessageType represents a join message. + */ +class JoinMessageType implements IMessageType { + use TCanSeeInSameRoom; + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Join'; + } +} diff --git a/files/lib/system/message/type/LeaveMessageType.class.php b/files/lib/system/message/type/LeaveMessageType.class.php new file mode 100644 index 0000000..83a3734 --- /dev/null +++ b/files/lib/system/message/type/LeaveMessageType.class.php @@ -0,0 +1,30 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +/** + * LeaveMessageType represents a leave message. + */ +class LeaveMessageType implements IMessageType { + use TCanSeeInSameRoom; + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Leave'; + } +} diff --git a/files/lib/system/message/type/MeMessageType.class.php b/files/lib/system/message/type/MeMessageType.class.php new file mode 100644 index 0000000..3a25253 --- /dev/null +++ b/files/lib/system/message/type/MeMessageType.class.php @@ -0,0 +1,39 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +/** + * MeMessageType represents an action message. + */ +class MeMessageType implements IMessageType, IDeletableMessageType { + use TCanSeeInSameRoom; + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Me'; + } + + /** + * @inheritDoc + */ + public function canDelete(\chat\data\message\Message $message, \wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + return $user->getPermission('mod.chat.canDelete'); + } +} diff --git a/files/lib/system/message/type/PlainMessageType.class.php b/files/lib/system/message/type/PlainMessageType.class.php new file mode 100644 index 0000000..118ebca --- /dev/null +++ b/files/lib/system/message/type/PlainMessageType.class.php @@ -0,0 +1,78 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +/** + * PlainMessageType represents a normal message. + */ +class PlainMessageType implements IMessageType, IDeletableMessageType { + use TCanSeeInSameRoom; + + /** + * HtmlOutputProcessor to use. + * @var \wcf\system\html\output\HtmlOutputProcessor + */ + protected $processor = null; + + public function __construct() { + $this->processor = new \wcf\system\html\output\HtmlOutputProcessor(); + } + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Plain'; + } + + /** + * @inheritDoc + */ + public function canDelete(\chat\data\message\Message $message, \wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + return $user->getPermission('mod.chat.canDelete'); + } + + /** + * @see \chat\system\message\type\IMessageType::getPayload() + */ + public function getPayload(\chat\data\message\Message $message, \wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + $payload = $message->payload; + $payload['formattedMessage'] = null; + $payload['plaintextMessage'] = null; + + $parameters = [ 'message' => $message + , 'user' => $user + , 'payload' => $payload + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters); + + if ($parameters['payload']['formattedMessage'] === null) { + $this->processor->setOutputType('text/html'); + $this->processor->process($parameters['payload']['message'], 'be.bastelstu.chat.message', $message->messageID); + $parameters['payload']['formattedMessage'] = $this->processor->getHtml(); + } + if ($parameters['payload']['plaintextMessage'] === null) { + $this->processor->setOutputType('text/plain'); + $this->processor->process($parameters['payload']['message'], 'be.bastelstu.chat.message', $message->messageID); + $parameters['payload']['plaintextMessage'] = $this->processor->getHtml(); + } + + return $parameters['payload']; + } +} diff --git a/files/lib/system/message/type/SuspendMessageType.class.php b/files/lib/system/message/type/SuspendMessageType.class.php new file mode 100644 index 0000000..b0e31bf --- /dev/null +++ b/files/lib/system/message/type/SuspendMessageType.class.php @@ -0,0 +1,88 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * SuspendMessageType informs about suspensions. + */ +class SuspendMessageType implements IMessageType { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Suspend'; + } + + /** + * @see \chat\system\message\type\IMessageType::getPayload() + */ + public function getPayload(Message $message, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $payload = $message->payload; + unset($payload['roomIDs']); + + $parameters = [ 'message' => $message + , 'user' => $user + , 'payload' => $payload + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters); + + return $parameters['payload']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => in_array($room->roomID, $message->payload['roomIDs'], true) + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => in_array($room->roomID, $message->payload['roomIDs'], true) + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/TCanSeeCreator.class.php b/files/lib/system/message/type/TCanSeeCreator.class.php new file mode 100644 index 0000000..f9cdb83 --- /dev/null +++ b/files/lib/system/message/type/TCanSeeCreator.class.php @@ -0,0 +1,67 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * Adds a default canSee implementation that checks whether the message was created by the user and + * whether the message belongs to the user's active room. + */ +trait TCanSeeCreator { + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => $user->userID === $message->userID && $message->getRoom()->roomID === $room->roomID + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => false + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + $parameters = [ 'result' => true ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'supportsFastSelect', $parameters); + + return $parameters['result']; + } +} diff --git a/files/lib/system/message/type/TCanSeeInSameRoom.class.php b/files/lib/system/message/type/TCanSeeInSameRoom.class.php new file mode 100644 index 0000000..ce263c2 --- /dev/null +++ b/files/lib/system/message/type/TCanSeeInSameRoom.class.php @@ -0,0 +1,66 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * Adds a default canSee implementation that checks whether the message belongs to the user's active room. + */ +trait TCanSeeInSameRoom { + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => $message->getRoom()->roomID === $room->roomID + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => $message->getRoom()->roomID === $room->roomID + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + $parameters = [ 'result' => true ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'supportsFastSelect', $parameters); + + return $parameters['result']; + } +} diff --git a/files/lib/system/message/type/TDefaultPayload.class.php b/files/lib/system/message/type/TDefaultPayload.class.php new file mode 100644 index 0000000..d575ae9 --- /dev/null +++ b/files/lib/system/message/type/TDefaultPayload.class.php @@ -0,0 +1,37 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +/** + * Default implementation for 'getPayload'. + */ +trait TDefaultPayload { + /** + * @see \chat\system\message\type\IMessageType::getPayload() + */ + public function getPayload(\chat\data\message\Message $message, \wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + $payload = $message->payload; + + $parameters = [ 'message' => $message + , 'user' => $user + , 'payload' => $payload + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters); + + return $parameters['payload']; + } +} diff --git a/files/lib/system/message/type/TeamMessageType.class.php b/files/lib/system/message/type/TeamMessageType.class.php new file mode 100644 index 0000000..abf2483 --- /dev/null +++ b/files/lib/system/message/type/TeamMessageType.class.php @@ -0,0 +1,89 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * TeamMessageType represents a team internal message. + */ +class TeamMessageType extends PlainMessageType { + /** + * HtmlOutputProcessor to use. + * @var \wcf\system\html\output\HtmlOutputProcessor + */ + protected $processor = null; + + public function __construct() { + $this->processor = new \wcf\system\html\output\HtmlOutputProcessor(); + } + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Team'; + } + + /** + * @inheritDoc + */ + public function canDelete(\chat\data\message\Message $message, \wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + return $user->getPermission('mod.chat.canDelete'); + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => $user->getPermission('mod.chat.canTeam') + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => $user->getPermission('mod.chat.canTeam') + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/TemproomCreatedMessageType.class.php b/files/lib/system/message/type/TemproomCreatedMessageType.class.php new file mode 100644 index 0000000..f5707a1 --- /dev/null +++ b/files/lib/system/message/type/TemproomCreatedMessageType.class.php @@ -0,0 +1,30 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +/** + * TemproomCreatedMessageType informs a user that a temporary room was created. + */ +class TemproomCreatedMessageType implements IMessageType { + use TCanSeeCreator; + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/TemproomCreated'; + } +} diff --git a/files/lib/system/message/type/TemproomInvitedMessageType.class.php b/files/lib/system/message/type/TemproomInvitedMessageType.class.php new file mode 100644 index 0000000..f7479a9 --- /dev/null +++ b/files/lib/system/message/type/TemproomInvitedMessageType.class.php @@ -0,0 +1,95 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * TemproomInvitedMessageType informs a user that they were invited to a temporary room. + */ +class TemproomInvitedMessageType implements IMessageType { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/TemproomInvited'; + } + + /** + * @inheritDoc + */ + public function getPayload(Message $message, UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + $payload = $message->payload; + $room = $message->getRoom(); + $payload['room'] = [ 'roomID' => $room->roomID + , 'title' => $room->title + , 'link' => $room->getLink() + ]; + + $parameters = [ 'message' => $message + , 'user' => $user + , 'payload' => $payload + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters); + + return $parameters['payload']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => $user->userID === $message->userID || $user->userID === $message->payload['recipient'] + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => false + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + $parameters = [ 'result' => false ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'supportsFastSelect', $parameters); + + return $parameters['result']; + } +} diff --git a/files/lib/system/message/type/TombstoneMessageType.class.php b/files/lib/system/message/type/TombstoneMessageType.class.php new file mode 100644 index 0000000..97e5218 --- /dev/null +++ b/files/lib/system/message/type/TombstoneMessageType.class.php @@ -0,0 +1,63 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * TombstoneMessageType marks a different message as dead. + */ +class TombstoneMessageType implements IMessageType { + use TDefaultPayload; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Tombstone'; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => true + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + return false; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/UnsuspendMessageType.class.php b/files/lib/system/message/type/UnsuspendMessageType.class.php new file mode 100644 index 0000000..763336b --- /dev/null +++ b/files/lib/system/message/type/UnsuspendMessageType.class.php @@ -0,0 +1,88 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * UnsuspendMessageType informs about removed suspensions. + */ +class UnsuspendMessageType implements IMessageType { + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Unsuspend'; + } + + /** + * @see \chat\system\message\type\IMessageType::getPayload() + */ + public function getPayload(Message $message, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $payload = $message->payload; + unset($payload['roomIDs']); + + $parameters = [ 'message' => $message + , 'user' => $user + , 'payload' => $payload + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters); + + return $parameters['payload']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => in_array($room->roomID, $message->payload['roomIDs'], true) + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => in_array($room->roomID, $message->payload['roomIDs'], true) + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + return false; + } +} diff --git a/files/lib/system/message/type/WhereMessageType.class.php b/files/lib/system/message/type/WhereMessageType.class.php new file mode 100644 index 0000000..878384c --- /dev/null +++ b/files/lib/system/message/type/WhereMessageType.class.php @@ -0,0 +1,58 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\RoomCache; +use \wcf\data\user\UserProfile; + +/** + * WhereMessageType represents the reply to WhereCommand. + */ +class WhereMessageType implements IMessageType { + use TCanSeeCreator; + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Where'; + } + + /** + * @inheritDoc + */ + public function getPayload(Message $message, UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(\wcf\system\WCF::getUser()); + + $payload = $message->payload; + $payload = array_map(function ($item) { + $room = RoomCache::getInstance()->getRoom($item['roomID']); + $item['room'] = [ 'roomID' => $room->roomID + , 'title' => $room->title + , 'link' => $room->getLink() + ]; + return $item; + }, $payload); + + $parameters = [ 'message' => $message + , 'user' => $user + , 'payload' => $payload + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters); + + return $parameters['payload']; + } +} diff --git a/files/lib/system/message/type/WhisperMessageType.class.php b/files/lib/system/message/type/WhisperMessageType.class.php new file mode 100644 index 0000000..1c69a73 --- /dev/null +++ b/files/lib/system/message/type/WhisperMessageType.class.php @@ -0,0 +1,113 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\message\type; + +use \chat\data\message\Message; +use \chat\data\room\Room; +use \wcf\data\user\UserProfile; + +/** + * WhisperMessageType represents a whispered message. + */ +class WhisperMessageType implements IMessageType { + /** + * HtmlOutputProcessor to use. + * @var \wcf\system\html\output\HtmlOutputProcessor + */ + protected $processor = null; + + public function __construct() { + $this->processor = new \wcf\system\html\output\HtmlOutputProcessor(); + } + + /** + * @inheritDoc + */ + public function getJavaScriptModuleName() { + return 'Bastelstu.be/Chat/MessageType/Whisper'; + } + + /** + * @see \chat\system\message\type\IMessageType::getPayload() + */ + public function getPayload(Message $message, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $payload = $message->payload; + $payload['formattedMessage'] = null; + $payload['plaintextMessage'] = null; + + $parameters = [ 'message' => $message + , 'user' => $user + , 'payload' => $payload + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'getPayload', $parameters); + + if ($parameters['payload']['formattedMessage'] === null) { + $this->processor->process($parameters['payload']['message'], 'be.bastelstu.chat.message', $message->messageID); + $parameters['payload']['formattedMessage'] = $this->processor->getHtml(); + } + + if ($parameters['payload']['plaintextMessage'] === null) { + $this->processor->setOutputType('text/plain'); + $this->processor->process($parameters['payload']['message'], 'be.bastelstu.chat.message', $message->messageID); + $parameters['payload']['plaintextMessage'] = $this->processor->getHtml(); + } + + return $parameters['payload']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSee() + */ + public function canSee(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => $user->userID === $message->userID || $user->userID === $message->payload['recipient'] + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSee', $parameters); + + return $parameters['canSee']; + } + + /** + * @see \chat\system\message\type\IMessageType::canSeeInLog() + */ + public function canSeeInLog(Message $message, Room $room, UserProfile $user = null) { + if ($user === null) $user = new UserProfile(\wcf\system\WCF::getUser()); + + $parameters = [ 'message' => $message + , 'room' => $room + , 'user' => $user + , 'canSee' => false + ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'canSeeInLog', $parameters); + + return $parameters['canSee']; + } + + /** + * @see»\chat\system\message\type\IMessageType::supportsFastSelect() + */ + public function supportsFastSelect() { + $parameters = [ 'result' => false ]; + \wcf\system\event\EventHandler::getInstance()->fireAction($this, 'supportsFastSelect', $parameters); + + return $parameters['result']; + } +} diff --git a/files/lib/system/page/handler/LogPageHandler.class.php b/files/lib/system/page/handler/LogPageHandler.class.php new file mode 100644 index 0000000..d88c389 --- /dev/null +++ b/files/lib/system/page/handler/LogPageHandler.class.php @@ -0,0 +1,65 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\page\handler; + +use \chat\data\room\RoomCache; +use \wcf\system\request\LinkHandler; +use \wcf\system\WCF; + +/** + * Allows to choose a room in the menu item management. + */ +class LogPageHandler extends \wcf\system\page\handler\AbstractLookupPageHandler implements \wcf\system\page\handler\IOnlineLocationPageHandler { + use TRoomPageHandler; + use \wcf\system\page\handler\TOnlineLocationPageHandler; + + /** + * @inheritDoc + */ + public function getLink($objectID) { + $room = RoomCache::getInstance()->getRoom($objectID); + if ($room === null) throw new \InvalidArgumentException('Invalid room ID given'); + + $link = LinkHandler::getInstance()->getLink('Log', [ 'application' => 'chat' + , 'object' => $room + ]); + return $link; + } + + /** + * @inheritDoc + */ + public function isVisible($objectID = null) { + if (!WCF::getUser()->userID) return false; + + if ($objectID === null) throw new \InvalidArgumentException('Invalid room ID given'); + $room = RoomCache::getInstance()->getRoom($objectID); + if ($room === null) throw new \InvalidArgumentException('Invalid room ID given'); + + return $room->canSee() && $room->canSeeLog(); + } + + /** + * @inheritDoc + */ + public function getOnlineLocation(\wcf\data\page\Page $page, \wcf\data\user\online\UserOnline $user) { + if ($user->pageObjectID === null) return ''; + $room = RoomCache::getInstance()->getRoom($user->pageObjectID); + if ($room === null) return ''; + if (!$room->canSeeLog()) return ''; + + return WCF::getLanguage()->getDynamicVariable('wcf.page.onlineLocation.'.$page->identifier, [ 'room' => $room ]); + } +} diff --git a/files/lib/system/page/handler/RoomListPageHandler.class.php b/files/lib/system/page/handler/RoomListPageHandler.class.php new file mode 100644 index 0000000..fec8c5c --- /dev/null +++ b/files/lib/system/page/handler/RoomListPageHandler.class.php @@ -0,0 +1,47 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\page\handler; + +use \chat\data\room\Room; +use \chat\data\room\RoomCache; +use \wcf\system\WCF; + +/** + * Shows the number of chatters in the RoomList menu item. + */ +class RoomListPageHandler extends \wcf\system\page\handler\AbstractMenuPageHandler { + /** + * @inheritDoc + */ + public function getOutstandingItemCount($objectID = null) { + $rooms = RoomCache::getInstance()->getRooms(); + $users = array_map(function (Room $room) { + return array_keys($room->getUsers()); + }, array_filter($rooms, function (Room $room) { + return $room->canSee(); + })); + + if (empty($users)) return 0; + + return count(array_unique(call_user_func_array('array_merge', $users))); + } + + /** + * @inheritDoc + */ + public function isVisible($objectID = null) { + return Room::canSeeAny(); + } +} diff --git a/files/lib/system/page/handler/RoomPageHandler.class.php b/files/lib/system/page/handler/RoomPageHandler.class.php new file mode 100644 index 0000000..4bbb55c --- /dev/null +++ b/files/lib/system/page/handler/RoomPageHandler.class.php @@ -0,0 +1,68 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\page\handler; + +use \chat\data\room\RoomCache; +use \wcf\system\WCF; + +/** + * Allows to choose a room in the menu item management. + */ +class RoomPageHandler extends \wcf\system\page\handler\AbstractLookupPageHandler implements \wcf\system\page\handler\IOnlineLocationPageHandler { + use TRoomPageHandler; + use \wcf\system\page\handler\TOnlineLocationPageHandler; + + /** + * @inheritDoc + */ + public function getOutstandingItemCount($objectID = null) { + return count(RoomCache::getInstance()->getRoom($objectID)->getUsers()); + } + + /** + * @inheritDoc + */ + public function getLink($objectID) { + $room = RoomCache::getInstance()->getRoom($objectID); + if ($room === null) throw new \InvalidArgumentException('Invalid room ID given'); + + return $room->getLink(); + } + + /** + * @inheritDoc + */ + public function isVisible($objectID = null) { + if (!WCF::getUser()->userID) return false; + + if ($objectID === null) throw new \InvalidArgumentException('Invalid room ID given'); + $room = RoomCache::getInstance()->getRoom($objectID); + if ($room === null) throw new \InvalidArgumentException('Invalid room ID given'); + + return $room->canSee(); + } + + /** + * @inheritDoc + */ + public function getOnlineLocation(\wcf\data\page\Page $page, \wcf\data\user\online\UserOnline $user) { + if ($user->pageObjectID === null) return ''; + $room = RoomCache::getInstance()->getRoom($user->pageObjectID); + if ($room === null) return ''; + if (!$room->canSee()) return ''; + + return WCF::getLanguage()->getDynamicVariable('wcf.page.onlineLocation.'.$page->identifier, [ 'room' => $room ]); + } +} diff --git a/files/lib/system/page/handler/TRoomPageHandler.class.php b/files/lib/system/page/handler/TRoomPageHandler.class.php new file mode 100644 index 0000000..c5c6ec4 --- /dev/null +++ b/files/lib/system/page/handler/TRoomPageHandler.class.php @@ -0,0 +1,73 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace chat\system\page\handler; + +use \chat\data\room\RoomCache; +use \wcf\system\request\LinkHandler; +use \wcf\system\WCF; + +/** + * Default implementations for page handlers of + * pages that operate on a specific chat room. + */ +trait TRoomPageHandler { + /** + * @inheritDoc + */ + public function isValid($objectID) { + $room = RoomCache::getInstance()->getRoom($objectID); + + return $room !== null; + } + + /** + * @inheritDoc + */ + public function lookup($searchString) { + $sql = "(SELECT ('chat.room.room' || roomID || '.title') AS languageItem + FROM chat".WCF_N."_room + WHERE title LIKE ? + ) + UNION + (SELECT languageItem + FROM wcf".WCF_N."_language_item + WHERE languageItemValue LIKE ? + AND languageItem LIKE ? + AND languageID = ? + )"; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute([ '%'.$searchString.'%' + , '%'.$searchString.'%' + , 'chat.room.room%.title' + , WCF::getLanguage()->languageID + ]); + + $results = [ ]; + while (($row = $statement->fetchArray())) { + $roomID = preg_replace('/chat\.room\.room(\d+)\.title/', '\1', $row['languageItem']); + $room = RoomCache::getInstance()->getRoom($roomID); + if (!$room) continue; + + $results[] = [ 'title' => $room->getTitle() + , 'description' => $room->getTopic() + , 'link' => $room->getLink() + , 'objectID' => $room->roomID + , 'image' => 'fa-comments-o' + ]; + } + + return $results; + } +} diff --git a/files/lib/system/permission/PermissionHandler.class.php b/files/lib/system/permission/PermissionHandler.class.php new file mode 100644 index 0000000..dd226e1 --- /dev/null +++ b/files/lib/system/permission/PermissionHandler.class.php @@ -0,0 +1,133 @@ +<?php +/** + * Copyright (C) 2010-2017 Tim Düsterhus + * Copyright (C) 2010-2017 Woltlab GmbH + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +namespace chat\system\permission; + +use \wcf\system\acl\ACLHandler; +use \wcf\system\user\storage\UserStorageHandler; +use \wcf\system\WCF; + +/** + * Handles chat permissions. + */ +class PermissionHandler { + /** + * permissions set for the given user + * @var boolean[] + */ + protected $chatPermissions = [ ]; + + /** + * given user decorated in a user profile + * @var \wcf\data\user\UserProfile + */ + protected $user = null; + + /** + * Cache of PermissionHandlers. + * @var \chat\system\permission\PermissionHandler[] + */ + protected static $cache = [ ]; + + public function __construct(\wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(WCF::getUser()); + $this->user = $user; + + $this->chatPermissions = \chat\system\cache\builder\PermissionCacheBuilder::getInstance()->getData($user->getGroupIDs()); + + // get user permissions + if ($user->userID) { + $ush = UserStorageHandler::getInstance(); + + // get ids + $data = $ush->getField('chatUserPermissions', $user->userID); + + // cache does not exist or is outdated + if ($data === null) { + $userPermissions = [ ]; + + $conditionBuilder = new \wcf\system\database\util\PreparedStatementConditionBuilder(); + $conditionBuilder->add('acl_option.objectTypeID = ?', [ ACLHandler::getInstance()->getObjectTypeID('be.bastelstu.chat.room') ]); + $conditionBuilder->add('option_to_user.userID = ?', [ $user->userID ]); + $sql = "SELECT option_to_user.objectID AS roomID, + option_to_user.optionValue, + acl_option.optionName AS permission + FROM wcf".WCF_N."_acl_option acl_option + INNER JOIN wcf".WCF_N."_acl_option_to_user option_to_user + ON option_to_user.optionID = acl_option.optionID + ".$conditionBuilder; + $statement = WCF::getDB()->prepareStatement($sql); + $statement->execute($conditionBuilder->getParameters()); + while (($row = $statement->fetchArray())) { + $userPermissions[$row['roomID']][$row['permission']] = $row['optionValue']; + } + + // update cache + $ush->update($user->userID, 'chatUserPermissions', serialize($userPermissions)); + } + else { + $userPermissions = unserialize($data); + } + + foreach ($userPermissions as $roomID => $permissions) { + foreach ($permissions as $name => $value) { + $this->chatPermissions[$roomID][$name] = $value; + } + } + } + } + + public static function get(\wcf\data\user\UserProfile $user = null) { + if ($user === null) $user = new \wcf\data\user\UserProfile(WCF::getUser()); + if (!isset(static::$cache[$user->userID])) { + static::$cache[$user->userID] = new static($user); + } + + return static::$cache[$user->userID]; + } + + /** + * Fetches the given permission for the given room + * + * @param \chat\data\room\Room $room + * @param string $permission + * @return boolean + */ + public function getPermission(\chat\data\room\Room $room, $permission) { + $groupPermission = str_replace([ 'user.', 'mod.' ], [ 'user.chat.', 'mod.chat.' ], $permission); + + if (method_exists($this->user, 'getNeverPermission') && $this->user->getNeverPermission($groupPermission)) { + return false; + } + + if (!isset($this->chatPermissions[$room->roomID][$permission])) { + return $this->user->getPermission($groupPermission); + } + return (boolean) $this->chatPermissions[$room->roomID][$permission]; + } + + /** + * Clears the cache. + */ + public static function resetCache() { + UserStorageHandler::getInstance()->resetAll('chatUserPermissions'); + \chat\system\cache\builder\PermissionCacheBuilder::getInstance()->reset(); + } +} diff --git a/files/lib/system/suspension/BanSuspension.class.php b/files/lib/system/suspension/BanSuspension.class.php new file mode 100644 index 0000000..8c54a06 --- /dev/null +++ b/files/lib/system/suspension/BanSuspension.class.php @@ -0,0 +1,48 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\suspension; + +use \chat\data\suspension\Suspension; +use \chat\system\permission\PermissionHandler; +use \wcf\data\user\UserProfile; + +/** + * BanSuspension removes join privileges. + */ +class BanSuspension implements ISuspension { + /** + * @inheritDoc + */ + public function hasEffect(Suspension $suspension) { + $user = new UserProfile($suspension->getUser()); + $room = $suspension->getRoom(); + + if ($user->getPermission('mod.chat.canBan')) { + return false; + } + if ($room !== null) { + if (PermissionHandler::get($user)->getPermission($room, 'mod.canBan') || PermissionHandler::get($user)->getPermission($room, 'mod.canIgnoreBan')) { + return false; + } + } + else { + if ($user->getPermission('mod.chat.canIgnoreBan')) { + return false; + } + } + + return true; + } +} diff --git a/files/lib/system/suspension/ISuspension.class.php b/files/lib/system/suspension/ISuspension.class.php new file mode 100644 index 0000000..993701c --- /dev/null +++ b/files/lib/system/suspension/ISuspension.class.php @@ -0,0 +1,31 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\suspension; + +use \chat\data\suspension\Suspension; +use \wcf\data\user\UserProfile; + +/** + * An ISuspension defines how a suspension of a certain type is acted upon. + */ +interface ISuspension { + /** + * Returns whether the suspension actually has an effect. + * + * @param \chat\data\suspension\Suspension $suspension + * @return bool + */ + public function hasEffect(Suspension $suspension); +} diff --git a/files/lib/system/suspension/MuteSuspension.class.php b/files/lib/system/suspension/MuteSuspension.class.php new file mode 100644 index 0000000..0a2ef10 --- /dev/null +++ b/files/lib/system/suspension/MuteSuspension.class.php @@ -0,0 +1,48 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +namespace chat\system\suspension; + +use \chat\data\suspension\Suspension; +use \chat\system\permission\PermissionHandler; +use \wcf\data\user\UserProfile; + +/** + * MuteSuspension removes write privileges. + */ +class MuteSuspension implements ISuspension { + /** + * @inheritDoc + */ + public function hasEffect(Suspension $suspension) { + $user = new UserProfile($suspension->getUser()); + $room = $suspension->getRoom(); + + if ($user->getPermission('mod.chat.canMute')) { + return false; + } + if ($room !== null) { + if (PermissionHandler::get($user)->getPermission($room, 'mod.canMute') || PermissionHandler::get($user)->getPermission($room, 'mod.canIgnoreMute')) { + return false; + } + } + else { + if ($user->getPermission('mod.chat.canIgnoreMute')) { + return false; + } + } + + return true; + } +} diff --git a/files/style/be.bastelstu.chat.messageTypes.scss b/files/style/be.bastelstu.chat.messageTypes.scss new file mode 100644 index 0000000..175034c --- /dev/null +++ b/files/style/be.bastelstu.chat.messageTypes.scss @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +li[data-object-type="be.bastelstu.chat.messageType.info"] { + .chatMessage > .box48 + .containerList { + margin-top: 20px; + } + + .hideIcon { + float: right; + } +} + +li[data-object-type="be.bastelstu.chat.messageType.where"] { + .containerList > li { + &:first-child { + border-top: none; + } + + &:last-child { + border-bottom: none; + } + + .hideIcon { + float: right; + } + } +} diff --git a/files/style/be.bastelstu.chat.scss b/files/style/be.bastelstu.chat.scss new file mode 100644 index 0000000..f3ed25f --- /dev/null +++ b/files/style/be.bastelstu.chat.scss @@ -0,0 +1,635 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +$chatEmbedMaxWidth: 400px; + +#tpl_chat_room #chatMessageStream { + margin-top: 0; +} + +#tpl_chat_room, +#tpl_chat_log { + @include screen-md-down { + .main > .layoutBoundary { + display: flex; + flex-direction: column; + flex: 1 1 auto; + } + + .sidebar { + display: none; + } + } + + // Enable WSC 3.1 sidebar toggle on smartphones + @include screen-xs { + .sidebar { + &[data-show-sidebar][data-hide-sidebar] { + display: block; + flex: 0 0 auto; + } + } + + .boxesSidebarLeft, + .boxesSidebarRight { + .box .boxMenu { + .boxMenuLink, + .boxMenuLinkTitle { + white-space: pre-wrap; + } + } + } + } + + @include screen-sm-up { + .sidebar { + overflow-y: auto; + } + } + + @include screen-sm-md { + .main > .layoutBoundary { + flex-direction: row !important; + } + + #content { + width: auto !important; + } + + #chatMessageStream { + margin-right: 10px; + } + + .sidebar.boxesSidebarRight { + display: flex; + flex: 0.5 0 auto; + flex-direction: column; + margin-left: 10px; + max-width: 310px; + + > .boxContainer { + -webkit-columns: 1; + -moz-columns: 1; + columns: 1; + } + } + } + + @include screen-lg { + .boxesSidebarRight { + &, + > .boxContainer { + display: flex; + flex-direction: column; + } + + > .boxContainer { + &, + > .box.chatUserList, + > [data-box-identifier="be.bastelstu.chat.roomListSidebar"] { + flex: 1 1 0px; + } + + > .box { + &.chatUserList { + min-height: 15rem; + + > .boxContent { + flex-basis: 6rem; + } + } + + &[data-box-identifier="be.bastelstu.chat.roomListSidebar"] { + min-height: 12rem; + + .badge { + float: right; + padding-left: 7px; + } + + > .boxContent { + height: 6rem; + } + } + + &.chatUserList, + &[data-box-identifier="be.bastelstu.chat.roomListSidebar"] { + display: flex; + flex-direction: column; + + > .boxContent { + overflow-y: auto; + flex: 1 1 auto; + } + } + } + } + } + + #chatQuickSettings { + display: none; + } + } + + .main { + display: flex; + + > .layoutBoundary { + flex: 1 1 auto; + } + } + + .chatRoomTopic { + border-left: 5px solid $wcfContentBorderInner; + padding: 5px 0px 5px 10px; + margin-bottom: 10px; + + .jsDismissRoomTopicButton { + float: right; + } + } + + #content { + display: flex; + flex-direction: column; + flex: 1 1 auto; + width: 100%; + } + + #chatMessageStream > .scrollContainer, + #chatUserList > .boxContent, + [data-box-identifier="be.bastelstu.chat.roomListSidebar"] > .boxContent { + position: relative; + overflow-y: scroll; + padding-right: 5px; + } + + [data-box-identifier="be.bastelstu.chat.roomListSidebar"] > .boxContent { + overflow-x: hidden; + + // Fixes issues with backgrounds being cut by the overflow-x + margin-left: -20px; + + > div > .boxMenu { + margin-left: 0; + } + } + + #chatUserList { + li.box24 { + > :nth-child(2) { + flex: 1 1 auto; + overflow: hidden; + } + + > :last-child.iconColumn { + flex: 0 1 auto; + } + } + } + + #chatMessageStream { + display: flex; + flex: 1 1 auto; + flex-direction: column; + + &:not(.activity) .activityInfo { + @extend .invisible; + } + + > .infoMessages { + position: relative; + + > * { + margin-top: 0; + margin-bottom: 20px; + } + } + + > .scrollContainer { + display: flex; + flex: 1 1 15em; + flex-direction: column; + + > ul { + > li { + &.dateMarker { + text-align: center; + @include wcfFontBold; + } + + &:target { + background-color: rgba(255, 255, 102, 0.4); // .codeBoxJumpAnchor:target::after + } + + &.readMarker { + border-bottom: 2px dashed rgba(204, 0, 0, 1); // .badge.red + margin-bottom: 0px !important; + + & + .first { + border-top: none; + } + } + + &:first-child.first { + border-top: none; + } + + .chatMessageContainer { + display: flex; + margin-top: 3px; + margin-bottom: 3px; + position: relative; + + // Allows to easily add a marker for special messages like mentions + border-left: 3px solid transparent; + + .chatMessageContent { + flex: 1 1 auto; + + // Limit embedded images and videos to a reasonable size + img:not(.smiley):not(.userAvatarImage) { + width: 100%; + max-width: $chatEmbedMaxWidth; + } + + .videoContainer { + @media screen and (min-width: $chatEmbedMaxWidth) { + padding-bottom: ($chatEmbedMaxWidth / 16 * 9); + } + + > iframe { + max-width: $chatEmbedMaxWidth; + max-height: ($chatEmbedMaxWidth / 16 * 9); + } + } + } + + .chatMessageIcon { + float: left; + margin-right: 5px; + } + + &, + &.inline { + .chatMessageSide > .chatUserAvatar, + .chatMessageContent > .chatMessageHeader { + display: none; + } + } + + .chatMessageSide { + min-width: 58px; + display: flex; + flex: 0 0 auto; + flex-direction: column; + align-items: center; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + + > time { + @extend .small; + display: none; + } + } + + .chatMessageHeader { + .username { + font-weight: bold; + } + } + } + + &.first, + &:hover { + .chatMessageSide > time { + display: inline-block; + } + } + + &.first { + border-top: 1px solid $wcfContentBorderInner; + + .chatMessageContainer { + .chatMessageSide { + > .chatUserAvatar { + margin-top: 3px; + display: block; + } + + > time { + display: none; + } + } + + .chatMessageContent { + > .chatMessageHeader { + display: block; + } + } + + &.inline { + .chatMessageSide { + > .chatUserAvatar { + display: none; + } + + > time { + display: inline-block; + } + } + + .chatMessageContent { + > .chatMessageHeader { + display: none; + } + } + } + } + } + + .buttonList { + display: none; + + -webkit-user-select: none; + -moz-user-select: none; + user-select: none; + } + } + } + } + } + + #chatInputContainer { + margin-top: 10px; + clear: both; + + textarea { + resize: none; + } + + .charCounter { + float: right; + color: $wcfContentDimmedText; + } + + > div { + display: flex; + align-items: center; + + > .flexibleTextarea { + flex: 1 0 auto; + max-width: 100%; + } + + > #chatQuickSettings { + flex: 0 0 auto; + } + } + + .innerError { + float: left; + } + } + + #chatQuickSettingsNavigation { + @extend .buttonGroupNavigation; + + position: relative; + + > .buttonGroup { + @include screen-lg { + justify-content: flex-end; + + > li > .button { + @extend .small; + } + } + + @include screen-md-down { + @include dropdownMenu; + + &.open { + display: block; + visibility: visible; + position: absolute; + right: 24px !important; + bottom: 0; + + > li { + margin-right: 0; + } + + // these rules are required to work around the .button default styling + .button { + @include wcfFontDefault; + + &.active, + &.active:hover { + color: $wcfButtonTextActive !important; + } + + &:not(.active) { + background-color: transparent; + color: $wcfDropdownLink; + } + + border-radius: 0; + } + } + } + } + } + + .smiliesToggleMobileButton { + margin-right: 5px; + } + + #chatQuickSettings { + margin-left: 5px; + } + + #smileyPickerContainer { + #smilies-text { + @if variable_exists(wcfContentContainerBackground) { + background-color: $wcfContentContainerBackground; + } + @else { + // Compatibility with API_VERSION 3.0 + background-color: rgba(255, 255, 255, 1); + } + + border: 1px solid $wcfContentBorderInner; + padding: 20px; + margin-top: 20px; + margin-bottom: 20px; + + > .smileyList { + overflow: auto; + } + } + + #smileyPickerCloseButton { + display: none; + } + + @include screen-md-down { + &[data-show="true"] { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 9001; + display: flex; + flex-direction: column; + pointer-events: all; + + #smileyPickerCloseButton { + background-color: $wcfSidebarBackground; + color: $wcfSidebarLink; + display: block; + padding: 10px 20px; + text-align: center; + flex: 0 0 auto; + cursor: pointer; + } + + #smilies-text { + border-top: none; + border-right: none; + border-left: none; + + margin: 0; + height: 0; + flex: 1 1 auto; + position: relative; + display: flex; + flex-direction: column; + + > nav > ul { + margin-bottom: -5px; + + > li { + margin-right: 10px; + margin-bottom: 5px; + border-right: 1px solid $wcfContentBorderInner; + padding-right: 9px; + + > a { + font-size: 15px; + } + } + } + + .messageTabMenuContent { + overflow: auto; + flex: 1 1 auto; + } + } + } + } + } +} + +html.fullscreen { + #tpl_chat_room, + #tpl_chat_log { + .pageHeaderContainer, + .pageNavigation, + .pageFooter { + display: none; + } + + .main { + @include screen-sm-up { + height: 0; // Workaround to get Firefox and Chrome to behave the same regarding page overflow + } + + padding: 14px 0; + + .layoutBoundary { + max-width: none; + width: auto; + } + } + + #chatMessageStream { + > .scrollContainer { + // flex: 1 1 0; // Disable min height in fullscreen mode + } + } + + // The to top button is clickable even when invisible and may lay over the chat input + // This button is unnecessary in the chat, therefore we hide it completely. + // If necessary, we should change the selector to .toTop[aria-hidden="true"]. + .pageAction > .toTop { + display: none; + pointer-events: none; + } + } +} + +html:not(.mobile) { + #tpl_chat_room, + #tpl_chat_log { + #chatMessageStream { + .chatMessageContainer { + .buttonList { + position: absolute; + bottom: -1px; + right: 0px; + + .button { + padding: 4px 6px; + } + } + + &:hover { + > .buttonList { + display: flex; + } + } + } + } + } +} + +// based on https://github.com/alexdunphy/flexText +.flexibleTextarea { + position: relative; + + > .flexibleTextareaContent, + > .flexibleTextareaMirror { + max-height: 200px; + overflow: auto; + } + + > .flexibleTextareaContent { + position: absolute; + top: 0; + left: 0; + height: 100%; + width: 100%; + resize: none; + } + + > .flexibleTextareaMirror { + display: block; + visibility: hidden; + + @extend textarea; + } +} diff --git a/files_wcf/js/Bastelstu.be/Chat.js b/files_wcf/js/Bastelstu.be/Chat.js new file mode 100644 index 0000000..d06c92a --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat.js @@ -0,0 +1,454 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Chat/console' + , 'Bastelstu.be/bottle' + , 'Bastelstu.be/_Push' + , 'WoltLabSuite/Core/Core' + , 'WoltLabSuite/Core/Language' + , 'WoltLabSuite/Core/Timer/Repeating' + , 'WoltLabSuite/Core/User' + , './Chat/Autocompleter' + , './Chat/CommandHandler' + , './Chat/DataStructure/Throttle' + , './Chat/Message' + , './Chat/Messenger' + , './Chat/ParseError' + , './Chat/ProfileStore' + , './Chat/Room' + , './Chat/Template' + , './Chat/Ui/AutoAway' + , './Chat/Ui/Chat' + , './Chat/Ui/ConnectionWarning' + , './Chat/Ui/ErrorDialog' + , './Chat/Ui/Input' + , './Chat/Ui/Input/Autocompleter' + , './Chat/Ui/MessageStream' + , './Chat/Ui/MessageActions/Delete' + , './Chat/Ui/Mobile' + , './Chat/Ui/Notification' + , './Chat/Ui/ReadMarker' + , './Chat/Ui/Settings' + , './Chat/Ui/Topic' + , './Chat/Ui/UserActionDropdownHandler' + , './Chat/Ui/UserList' + ], function (console, Bottle, Push, Core, Language, RepeatingTimer, CoreUser, Autocompleter, + CommandHandler, Throttle, Message, Messenger, ParseError, ProfileStore, Room, Template, UiAutoAway, Ui, + UiConnectionWarning, ErrorDialog, UiInput, UiInputAutocompleter, UiMessageStream, UiMessageActionDelete, UiMobile, UiNotification, + UiReadMarker, UiSettings, UiTopic, UiUserActionDropdownHandler, UiUserList) { + "use strict"; + + class Chat { + constructor(roomID, config) { + console.debug('Chat.constructor', 'Constructing …') + + this.config = config + + this.sessionID = Core.getUuid() + + // Setup Bottle containers + this.bottle = new Bottle() + this.bottle.value('bottle', this.bottle) + this.bottle.value('config', config) + this.bottle.constant('sessionID', this.sessionID) + this.bottle.constant('roomID', roomID) + + // Register chat components + this.service('Autocompleter', Autocompleter) + this.service('CommandHandler', CommandHandler) + this.service('Messenger', Messenger) + this.service('ProfileStore', ProfileStore) + this.service('Room', Room) + + // Register UI components + this.service('Ui', Ui) + this.service('UiAutoAway', UiAutoAway) + this.service('UiConnectionWarning', UiConnectionWarning) + this.service('UiInput', UiInput) + this.service('UiInputAutocompleter', UiInputAutocompleter) + this.service('UiMessageActionDelete', UiMessageActionDelete) + this.service('UiMessageStream', UiMessageStream) + this.service('UiMobile', UiMobile) + this.service('UiNotification', UiNotification) + this.service('UiReadMarker', UiReadMarker) + this.service('UiSettings', UiSettings) + this.service('UiTopic', UiTopic) + this.service('UiUserActionDropdownHandler', UiUserActionDropdownHandler) + this.service('UiUserList', UiUserList) + + // Register Models + this.bottle.instanceFactory('Message', (container, m) => { + return new Message(container.MessageType, m) + }) + + // Register Templates + const selector = [ '[type="x-text/template"]' + , '[data-application="be.bastelstu.chat"]' + , '[data-template-name]' + ].join('') + + const templates = elBySelAll(selector) + + Array.prototype.forEach.call(templates, (function (template) { + this.bottle.factory(`Template.${template.dataset.templateName}`, function (container) { + const includeNames = (template.dataset.templateIncludes || '').split(/ /).filter(item => item !== "") + const includes = { } + includeNames.forEach(item => includes[item] = container[item]) + + return new Template(template.textContent, includes) + }) + }).bind(this)) + + // Register MessageTypes + Object.entries(this.config.messageTypes) + .forEach(([ objectType, messageType ]) => { + const MessageType = require(messageType.module) + + this.bottle.factory(`MessageType.${objectType.replace(/\./g, '-')}`, _ => { + const deps = this.bottle.digest(MessageType.DEPENDENCIES || []) + + return new MessageType(...deps, objectType) + }) + }) + + // Register Commands + Object.values(this.config.commands).forEach(command => { + const Command = require(command.module) + + this.bottle.factory(`Command.${command.package.replace(/\./g, '-')}:${command.identifier}`, _ => { + const deps = this.bottle.digest(Command.DEPENDENCIES || []) + + return new Command(...deps, command) + }) + }) + this.bottle.constant('Trigger', new Map(Object.entries(this.config.triggers).map(([ trigger, commandID ]) => { + const command = this.config.commands[commandID] + const key = [ command.package, command.identifier ] + return [ trigger, key ] + }))) + + // Register Settings + Array.from(elBySelAll('#chatQuickSettingsNavigation .button[data-module]')).forEach(item => { + const Button = require(item.dataset.module) + + this.bottle.instanceFactory(`UiSettingsButton.${item.dataset.module.replace(/\./g, '-')}`, (_, element) => { + const deps = this.bottle.digest(Button.DEPENDENCIES || []) + return new Button(element, ...deps) + }) + }) + + this.knows = { from: undefined + , to: undefined + } + + this.processMessagesThrottled = Throttle(this.processMessages.bind(this)) + this.queuedMessages = [ ] + this.messageSinks = new Set() + + this.pullTimer = undefined + this.pullUserListTimer = undefined + this.pushConnected = false + + this.firstFailure = null + } + + service(name, _constructor, args = [ ]) { + this.bottle.factory(name, _ => { + const deps = this.bottle.digest(_constructor.DEPENDENCIES || [ ]) + + return new _constructor(...deps, ...args) + }) + } + + async bootstrap() { + console.debug('Chat.bootstrap', 'Initializing …') + + this.ui = this.bottle.container.Ui + this.ui.bootstrap() + + this.bottle.container.UiInput.on('submit', this.onSubmit.bind(this)) + this.bottle.container.UiInput.on('autocomplete', this.onAutocomplete.bind(this)) + + await this.bottle.container.Room.join() + + // Bind unload event to leave the Chat + window.addEventListener('unload', this.bottle.container.Room.leave.bind(this.bottle.container.Room, true)) + document.addEventListener('visibilitychange', _ => { + this.processMessagesThrottled.setDelay(document.hidden ? 10000 : 125) + }) + + this.pullTimer = new RepeatingTimer(Throttle(this.pullMessages.bind(this)), this.config.reloadTime * 1e3) + + Push.onConnect(_ => { + console.debug('Chat.bootstrap', 'Push connected') + this.pushConnected = true + this.pullTimer.setDelta(30e3) + }) + .catch(error => { console.debug(error) }) + + Push.onDisconnect(_ => { + console.debug('Chat.bootstrap', 'Push disconnected') + this.pushConnected = false + this.pullTimer.setDelta(this.config.reloadTime * 1e3) + }) + .catch(error => { console.debug(error) }) + + Push.onMessage('be.bastelstu.chat.message', this.pullMessages.bind(this)) + .catch(error => { console.debug(error) }) + + // Fetch user list every 60 seconds + // This acts as a safety net: It should be kept current by messages whenever possible. + this.pullUserListTimer = new RepeatingTimer(this.updateUsers.bind(this), 60e3) + + this.registerMessageSink(this.bottle.container.UiMessageStream) + this.registerMessageSink(this.bottle.container.UiNotification) + this.registerMessageSink(this.bottle.container.UiAutoAway) + + await Promise.all([ this.pullMessages() + , this.updateUsers() + , this.bottle.container.ProfileStore.ensureUsersByIDs([ CoreUser.userId ]) + ]) + + return this + } + + registerMessageSink(sink) { + if (typeof sink.ingest !== 'function') { + throw new Error('The given sink does not provide a .ingest function.') + } + + this.messageSinks.add(sink) + } + + unregisterMessageSink(sink) { + this.messageSinks.delete(sink) + } + + hcf(err = undefined) { + console.debug('Chat.hcf', 'Gotcha! FIRE was caught! FIRE’s data was newly added to the POKéDEX.', err) + + this.pullTimer.stop() + this.pullUserListTimer.stop() + + new ErrorDialog(Language.get('chat.error.hcf', { err })) + } + + async onSubmit(event) { + const input = event.target + const value = input.getText() + + console.debug('Chat.onSubmit', `Pushing message: ${value}`) + + // Clear message input + input.insertText('', { append: false }) + + this.markAsBack() + + let [ trigger, parameterString ] = this.bottle.container.CommandHandler.splitCommand(value) + let command = null + if (trigger === null) { + command = this.bottle.container.CommandHandler.getCommandByIdentifier('be.bastelstu.chat', 'plain') + } + else { + command = this.bottle.container.CommandHandler.getCommandByTrigger(trigger) + } + + if (command === null) { + this.ui.input.inputError(Language.get('chat.error.triggerNotFound', { trigger })) + return + } + + try { + let parameters + try { + parameters = this.bottle.container.CommandHandler.applyCommand(command, parameterString) + } + catch (e) { + if (e instanceof ParseError) { + e = new Error(Language.get('chat.error.invalidParameters', { data: e.data })) + } + throw e + } + + const payload = { commandID: command.id + , parameters + } + + try { + await this.bottle.container.Messenger.push(payload) + this.ui.input.hideInputError() + } + catch (error) { + let seriousError = true + if (error.returnValues && error.returnValues.fieldName === 'message' && (error.returnValues.realErrorMessage || error.returnValues.errorType)) { + this.ui.input.inputError(error.returnValues.realErrorMessage || error.returnValues.errorType) + seriousError = false + } + else { + this.ui.input.inputError(error.message) + } + + if (seriousError) { + this.handleError(error) + } + } + + // We assume that a running push server will push us our own message + if (!this.pushConnected) { + this.pullMessages() + } + + console.debug('Chat.onSubmit', `Done`) + } + catch (e) { + this.ui.input.inputError(e.message) + } + } + + async markAsBack() { + try { + if (this.bottle.container.ProfileStore.getSelf().away == null) return + console.debug('Chat.markAsBack', `Marking as back`) + + const command = this.bottle.container.CommandHandler.getCommandByIdentifier('be.bastelstu.chat', 'back') + return this.bottle.container.Messenger.push({ commandID: command.id, parameters: { } }) + } + catch (err) { + console.error('Chat.markAsBack', err) + } + } + + onAutocomplete(event) { + const input = event.target + const value = input.getText(true) + + console.debug('Chat.onAutocomplete', `Autocompleting message: ${value}`) + + const result = this.bottle.container.Autocompleter.autocomplete(value) + const returnValues = [] + for (const item of result) { + returnValues.push({ label: item, objectID: item }) + if (returnValues.length == 5) break + } + + const payload = { returnValues } + this.ui.autocompleter._ajaxSuccess(payload) + } + + async pullMessages() { + console.debug('Chat.pullMessages', `Pulling new messages, starting at ${this.knows.to ? this.knows.to + 1 : ''}`) + + let payload + try { + if (this.knows.to === undefined) { + payload = await this.bottle.container.Messenger.pull() + } + else { + payload = await this.bottle.container.Messenger.pull(this.knows.to + 1) + } + } + catch (e) { + this.handleError(e) + return + } + + console.debug('Chat.pullMessages', `Handling result: `, payload) + const start = (performance ? performance : Date).now() + this.ui.connectionWarning.hide() + this.firstFailure = null + + // Null range: No messages satisfy the constraints + if (payload.from > payload.to) { + const end = (performance ? performance : Date).now() + console.debug('Chat.pullMessages', `took ${(end - start) / 1000}s`) + return + } + + let messages = payload.messages + + if (this.knows.from !== undefined && this.knows.to !== undefined) { + messages = messages.filter((message) => { + return !(this.knows.from <= message.messageID && message.messageID <= this.knows.to) + }) + } + + if (this.knows.from === undefined || payload.from < this.knows.from) this.knows.from = payload.from + if (this.knows.to === undefined || payload.to > this.knows.to) this.knows.to = payload.to + + this.queuedMessages.push(messages) + const end = (performance ? performance : Date).now() + console.debug('Chat.pullMessages', `took ${(end - start) / 1000}s`) + + this.processMessagesThrottled() + } + + handleError(error) { + if (this.firstFailure === null) { + console.error('Chat.handleError', `Request failed, 30 seconds until shutdown`) + this.firstFailure = Date.now() + this.ui.connectionWarning.show() + } + + console.debugException(error) + + if ((Date.now() - this.firstFailure) >= 30e3) { + console.error('Chat.handleError', ' Failures for 30 seconds, aborting') + + this.hcf(error) + } + } + + async processMessages() { + console.debug('Chat.processMessages', 'Processing messages') + const start = (performance ? performance : Date).now() + const messages = [ ].concat(...this.queuedMessages) + this.queuedMessages = [] + + if (messages.length === 0) return + + await Promise.all(messages.map(async (message) => { + this.bottle.container.ProfileStore.pushLastActivity(message.userID) + + return message.getMessageType().preProcess(message) + })) + + const updateUserList = messages.some((message) => { + return message.getMessageType().shouldUpdateUserList(message) + }) + + if (updateUserList) { + this.updateUsers() + } + + await this.bottle.container.ProfileStore.ensureUsersByIDs([ ].concat(...messages.map(message => message.getMessageType().getReferencedUsers(message)))) + + messages.forEach((message) => { + message.getMessageType().preRender(message) + }) + + this.messageSinks.forEach(sink => sink.ingest(messages)) + const end = (performance ? performance : Date).now() + console.debug('Chat.processMessages', `took ${(end - start) / 1000}s`) + } + + async updateUsers() { + console.debug('Chat.updateUsers') + + const users = await this.bottle.container.Room.getUsers() + await this.bottle.container.ProfileStore.ensureUsersByIDs(users.map(user => user.userID)) + this.ui.userList.render(users) + } + } + + return Chat +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Autocompleter.js b/files_wcf/js/Bastelstu.be/Chat/Autocompleter.js new file mode 100644 index 0000000..25292fc --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Autocompleter.js @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './CommandHandler' + , './Parser' + ], function (CommandHandler, Parser) { + "use strict"; + + const DEPENDENCIES = [ 'CommandHandler' ] + class Autocompleter { + constructor(commandHandler) { + if (!(commandHandler instanceof CommandHandler)) throw new TypeError('You must pass a CommandHandler to the Autocompleter') + + this.commandHandler = commandHandler + } + + * autocomplete(text) { + if (text === '/') { + yield * this.autocompleteCommandTrigger(text, '') + return + } + + const [ trigger, parameterString ] = this.commandHandler.splitCommand(text) + + let command + if (trigger === null) { + command = this.commandHandler.getCommandByIdentifier('be.bastelstu.chat', 'plain') + } + else { + const triggerDone = Parser.Slash.thenRight(Parser.AlnumTrigger.or(Parser.SymbolicTrigger).thenLeft(Parser.Whitespace)).parse(Parser.Streams.ofString(text)) + if (!triggerDone.isAccepted()) { + yield * this.autocompleteCommandTrigger(text, trigger) + return + } + + command = this.commandHandler.getCommandByTrigger(trigger) + } + + if (command === null) { + return + } + + const values = command.autocomplete(parameterString) + + if (trigger !== null) { + for (const item of values) { + yield `/${trigger} ${item}` + } + } + else { + yield * values + } + } + + * autocompleteCommandTrigger(text, prefix) { + const triggers = Array.from(this.commandHandler.getTriggers()) + + triggers.sort() + + for (const trigger of triggers) { + if (trigger === '') continue + if (!trigger.startsWith(prefix)) continue + if (!this.commandHandler.getCommandByTrigger(trigger).isAvailable) continue + + yield `/${trigger} ` + } + } + } + Autocompleter.DEPENDENCIES = DEPENDENCIES + + return Autocompleter +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/BoxRoomList.js b/files_wcf/js/Bastelstu.be/Chat/BoxRoomList.js new file mode 100644 index 0000000..30a23e7 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/BoxRoomList.js @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './console' + , 'Bastelstu.be/_Push' + , 'WoltLabSuite/Core/Dom/Util' + , 'WoltLabSuite/Core/Timer/Repeating' + , 'Bastelstu.be/PromiseWrap/Ajax' + ], function (console, Push, DomUtil, RepeatingTimer, Ajax) { + "use strict"; + + let timer = undefined + const mapping = new Map() + + class BoxRoomList { + constructor(container) { + this.container = container + + mapping.set(container, this) + + if (timer == null) { + timer = new RepeatingTimer(BoxRoomList.updateBoxes.bind(BoxRoomList), 60e3) + } + + Push.onConnect(timer.setDelta.bind(timer, 300e3)).catch(error => { console.debug(error) }) + Push.onDisconnect(timer.setDelta.bind(timer, 60e3)).catch(error => { console.debug(error) }) + Push.onMessage('be.bastelstu.chat.join', BoxRoomList.updateBoxes.bind(BoxRoomList)).catch(error => { console.debug(error) }) + Push.onMessage('be.bastelstu.chat.leave', BoxRoomList.updateBoxes.bind(BoxRoomList)).catch(error => { console.debug(error) }) + } + + static updateBoxes() { + mapping.forEach(object => { + object.update() + }) + } + + async update() { + const payload = { className: 'chat\\data\\room\\RoomAction' + , actionName: 'getBoxRoomList' + , parameters: { } + } + + payload.parameters.activeRoomID = this.container.dataset.activeRoomId + payload.parameters.boxID = this.container.dataset.boxId + payload.parameters.isSidebar = this.container.dataset.isSidebar + payload.parameters.skipEmptyRooms = this.container.dataset.skipEmptyRooms + + this.replace(await Ajax.api(this, payload)) + } + + replace(data) { + if (data.returnValues.template == null) throw new Error('template could not be found in returnValues') + + const fragment = DomUtil.createFragmentFromHtml(data.returnValues.template) + const oldRoomList = this.container.querySelector('.chatBoxRoomList') + const newRoomList = fragment.querySelector('.chatBoxRoomList') + + if (oldRoomList == null) { + throw new Error('.chatBoxRoomList could not be found in container') + } + if (newRoomList == null) { + throw new Error('.chatBoxRoomList could not be found in returned template') + } + + if (oldRoomList.dataset.hash !== newRoomList.dataset.hash) { + this.container.replaceChild(newRoomList, oldRoomList) + } + } + + _ajaxSetup() { + return { silent: true + , ignoreError: true + } + } + } + + return BoxRoomList +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command.js b/files_wcf/js/Bastelstu.be/Chat/Command.js new file mode 100644 index 0000000..a70732c --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command.js @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Parser' ], function (Parser) { + "use strict"; + + const data = Symbol('data') + + /** + * Represents a chat command. + */ + class Command { + constructor(_data) { + this[data] = _data + } + + getParameterParser() { + return Parser.Rest + } + + * autocomplete(parameterString) { + + } + + get id() { + return this[data].commandID + } + + get package() { + return this[data].package + } + + get identifier() { + return this[data].identifier + } + + get module() { + return this[data].module + } + + get isAvailable() { + return this[data].isAvailable + } + } + + return Command +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Away.js b/files_wcf/js/Bastelstu.be/Chat/Command/Away.js new file mode 100644 index 0000000..503181a --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Away.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + ], function (Command, Parser) { + "use strict"; + + class Away extends Command { + getParameterParser() { + return Parser.Rest.map(reason => ({ reason })) + } + } + + return Away +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Back.js b/files_wcf/js/Bastelstu.be/Chat/Command/Back.js new file mode 100644 index 0000000..dd05a0b --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Back.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + ], function (Command, Parser) { + "use strict"; + + class Back extends Command { + getParameterParser() { + return Parser.F.eos + } + } + + return Back +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Ban.js b/files_wcf/js/Bastelstu.be/Chat/Command/Ban.js new file mode 100644 index 0000000..eb89882 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Ban.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +define([ './_Suspension' ], function (Suspension) { + "use strict"; + + class Ban extends Suspension { + + } + + return Ban +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Broadcast.js b/files_wcf/js/Bastelstu.be/Chat/Command/Broadcast.js new file mode 100644 index 0000000..0df7012 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Broadcast.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Plain' ], function (Plain) { + "use strict"; + + class Broadcast extends Plain { + + } + + return Broadcast +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Color.js b/files_wcf/js/Bastelstu.be/Chat/Command/Color.js new file mode 100644 index 0000000..4be3e38 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Color.js @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + ], function (Command, Parser) { + "use strict"; + + class Color extends Command { + getParameterParser() { + // Either match a color in hexadecimal RGB notation or a color name (just letters) + const color = Parser.F.try(Parser.RGBHex.map(color => ({ type: 'hex', value: color }))) + .or(new Parser.X().word().map(word => ({ type: 'word', value: word }))) + + // Either match a single color or two colors separated by a space + return Parser.F.try(color.then(Parser.C.char(' ').thenRight(color))).or(color.map(item => [ item ])) + } + } + + return Color +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Info.js b/files_wcf/js/Bastelstu.be/Chat/Command/Info.js new file mode 100644 index 0000000..d204766 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Info.js @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + ], function (Command, Parser) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore' ] + class Info extends Command { + constructor(profileStore, id) { + super(id) + this.profileStore = profileStore + } + + getParameterParser() { + return Parser.Username.map(username => ({ username })) + } + + * autocomplete(parameterString) { + for (const userID of this.profileStore.getLastActivity()) { + const user = this.profileStore.get(userID) + if (!user.username.startsWith(parameterString)) continue + + yield `"${user.username.replace(/"/g, '""')}" ` + } + } + } + Info.DEPENDENCIES = DEPENDENCIES + + return Info +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Me.js b/files_wcf/js/Bastelstu.be/Chat/Command/Me.js new file mode 100644 index 0000000..5a47523 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Me.js @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Plain', '../Parser' ], function (Plain, Parser) { + "use strict"; + + class Me extends Plain { + getParameterParser() { + return Parser.Rest1.map(text => ({ text })) + } + } + + return Me +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Mute.js b/files_wcf/js/Bastelstu.be/Chat/Command/Mute.js new file mode 100644 index 0000000..393a72c --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Mute.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +define([ './_Suspension' ], function (Suspension) { + "use strict"; + + class Mute extends Suspension { + + } + + return Mute +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Plain.js b/files_wcf/js/Bastelstu.be/Chat/Command/Plain.js new file mode 100644 index 0000000..2c0bb4e --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Plain.js @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + , 'WoltLabSuite/Core/StringUtil' + ], function (Command, Parser, StringUtil) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore' ] + class Plain extends Command { + constructor(profileStore, id) { + super(id) + this.profileStore = profileStore + } + + getParameterParser() { + return Parser.Rest1 + .map(StringUtil.escapeHTML.bind(StringUtil)) + .map(text => ({ text })) + } + + * autocomplete(parameterString) { + const parts = parameterString.split(/ /) + const lastWord = parts.pop().toLowerCase() + + if (lastWord === '') { + return + } + + for (const userID of this.profileStore.getLastActivity()) { + const user = this.profileStore.get(userID) + const username = user.username.toLowerCase() + if (!username.startsWith(parameterString) && !username.startsWith(lastWord.replace(/^@/, ''))) continue + + yield `${parts.concat([ lastWord.startsWith('@') ? `@${user.username}` : user.username ]).join(' ')} ` + } + } + } + Plain.DEPENDENCIES = DEPENDENCIES + + return Plain +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Team.js b/files_wcf/js/Bastelstu.be/Chat/Command/Team.js new file mode 100644 index 0000000..f90a467 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Team.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Plain' ], function (Plain) { + "use strict"; + + class Team extends Plain { + + } + + return Team +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Temproom.js b/files_wcf/js/Bastelstu.be/Chat/Command/Temproom.js new file mode 100644 index 0000000..5f8ffaa --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Temproom.js @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + ], function (Command, Parser) { + "use strict"; + + class Temproom extends Command { + getParameterParser() { + const Create = Parser.C.string('create').thenReturns({ type: 'create' }) + const Invite = Parser.C.string('invite').thenLeft(Parser.Whitespace.rep()).thenRight(Parser.Username).map((username) => { + return { type: 'invite' + , username + } + }) + const Delete = Parser.C.string('delete').thenReturns({ type: 'delete' }) + + return Create.or(Invite).or(Delete) + } + + * autocomplete(parameterString) { + const Create = Parser.C.string('create') + const Invite = Parser.C.string('invite') + const Delete = Parser.C.string('delete') + + const subcommandDone = Create.or(Invite).or(Delete).thenLeft(Parser.Whitespace) + + const subcommandCheck = subcommandDone.parse(Parser.Streams.ofString(parameterString)) + if (subcommandCheck.isAccepted()) { + return + } + + yield * [ 'create', 'invite ', 'delete' ].filter(item => item.startsWith(parameterString)) + } + } + + return Temproom +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Unban.js b/files_wcf/js/Bastelstu.be/Chat/Command/Unban.js new file mode 100644 index 0000000..df3011e --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Unban.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +define([ './_Unsuspension' ], function (Unsuspension) { + "use strict"; + + class Unban extends Unsuspension { + + } + + return Unban +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Unmute.js b/files_wcf/js/Bastelstu.be/Chat/Command/Unmute.js new file mode 100644 index 0000000..b8b0828 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Unmute.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +define([ './_Unsuspension' ], function (Unsuspension) { + "use strict"; + + class Unmute extends Unsuspension { + + } + + return Unmute +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Where.js b/files_wcf/js/Bastelstu.be/Chat/Command/Where.js new file mode 100644 index 0000000..2dd9d49 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Where.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + ], function (Command, Parser) { + "use strict"; + + class Where extends Command { + getParameterParser() { + return Parser.F.eos + } + } + + return Where +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/Whisper.js b/files_wcf/js/Bastelstu.be/Chat/Command/Whisper.js new file mode 100644 index 0000000..8ff90b9 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/Whisper.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Parser' + , './Plain' + ], function (Parser, Plain) { + "use strict"; + + class Whisper extends Plain { + getParameterParser() { + return Parser.Username.thenLeft(Parser.Whitespace.rep()).then(super.getParameterParser()).map(([ username, object ]) => { + object.username = username + + return object + }) + } + + * autocomplete(parameterString) { + const usernameDone = Parser.Username.thenLeft(Parser.Whitespace).parse(Parser.Streams.ofString(parameterString)) + + if (usernameDone.isAccepted()) { + yield * super.autocomplete(parameterString) + return + } + + for (const userID of this.profileStore.getLastActivity()) { + const user = this.profileStore.get(userID) + if (!user.username.startsWith(parameterString)) continue + + yield `"${user.username.replace(/"/g, '""')}" ` + } + } + } + + return Whisper +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/_Suspension.js b/files_wcf/js/Bastelstu.be/Chat/Command/_Suspension.js new file mode 100644 index 0000000..1417340 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/_Suspension.js @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + ], function (Command, Parser) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore' ] + class Suspension extends Command { + constructor(profileStore, id) { + super(id) + this.profileStore = profileStore + } + + getParameterParser() { + const Globally = Parser.C.string('global').thenLeft(Parser.C.string('ly').opt()) + const Forever = Parser.C.string('forever').thenReturns(null) + const Timespan = Parser.N.digits.then(Parser.C.charIn('dhm')).map(function ([ span, unit ]) { + switch (unit) { + case 'd': + return span * 86400; + case 'h': + return span * 3600; + case 'm': + return span * 60; + } + throw new Error('Unreachable') + }) + .rep() + .map(parts => parts.array().reduce((carry, item) => carry + item, 0)) + .map(offset => Math.floor(Date.now() / 1000) + offset) + + const Duration = Forever.or(Timespan).or(Parser.ISODate.map(item => Math.floor(item.valueOf() / 1000))) + + return Parser.Username.thenLeft(Parser.Whitespace.rep()) + .then(Globally.thenLeft(Parser.Whitespace.rep()).thenReturns(true).or(Parser.F.returns(false))) + .then(Duration) + .then(Parser.Whitespace.rep().thenRight(Parser.Rest1).or(Parser.F.eos.thenReturns(null))) + .map(([ username, globally, duration, reason ]) => { + return { username + , globally + , duration + , reason + } + }) + } + + * autocomplete(parameterString) { + const usernameDone = Parser.Username.thenLeft(Parser.Whitespace.rep()).map(username => `"${username.replace(/"/g, '""')}"`) + const globallyDone = usernameDone.then(Parser.C.string('global').thenLeft(Parser.C.string('ly').opt())).thenLeft(Parser.Whitespace.rep()) + + const usernameCheck = usernameDone.parse(Parser.Streams.ofString(parameterString)) + if (usernameCheck.isAccepted()) { + const globallyCheck = globallyDone.parse(Parser.Streams.ofString(parameterString)) + let prefix, rest + if (globallyCheck.isAccepted()) { + prefix = parameterString.substring(0, globallyCheck.offset) + rest = parameterString.substring(globallyCheck.offset) + } + else { + prefix = parameterString.substring(0, usernameCheck.offset) + rest = parameterString.substring(usernameCheck.offset) + } + + if (!globallyCheck.isAccepted() && 'globally'.startsWith(rest)) { + yield `${prefix}globally ` + } + if (/^[0-9]+$/.test(rest)) { + yield `${prefix}${rest}h ` + yield `${prefix}${rest}d ` + yield `${prefix}${rest}m ` + } + if (rest === '') { + yield `${prefix}1h ` + yield `${prefix}1d ` + yield `${prefix}5m ` + } + + return + } + + for (const userID of this.profileStore.getLastActivity()) { + const user = this.profileStore.get(userID) + if (!user.username.startsWith(parameterString)) continue + + yield `"${user.username.replace(/"/g, '""')}" ` + } + } + } + Suspension.DEPENDENCIES = DEPENDENCIES + + return Suspension +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Command/_Unsuspension.js b/files_wcf/js/Bastelstu.be/Chat/Command/_Unsuspension.js new file mode 100644 index 0000000..3b95a82 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Command/_Unsuspension.js @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 3 + * or later of the General Public License. + */ + +define([ '../Command' + , '../Parser' + ], function (Command, Parser) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore' ] + class Unsuspension extends Command { + constructor(profileStore, id) { + super(id) + this.profileStore = profileStore + } + + getParameterParser() { + const Globally = Parser.C.string('global').thenLeft(Parser.C.string('ly').opt()) + + return Parser.Username + .then(Parser.Whitespace.rep().thenRight(Globally.thenReturns(true)).or(Parser.F.returns(false))) + .map(([ username, globally ]) => { + return { username + , globally + } + }) + } + + * autocomplete(parameterString) { + const usernameDone = Parser.Username.thenLeft(Parser.Whitespace.rep()).map(username => `"${username.replace(/"/g, '""')}"`) + const globallyDone = usernameDone.then(Parser.C.string('global').thenLeft(Parser.C.string('ly').opt())).thenLeft(Parser.Whitespace.rep()) + + const usernameCheck = usernameDone.parse(Parser.Streams.ofString(parameterString)) + if (usernameCheck.isAccepted()) { + const globallyCheck = globallyDone.parse(Parser.Streams.ofString(parameterString)) + let prefix, rest + if (globallyCheck.isAccepted()) { + prefix = parameterString.substring(0, globallyCheck.offset) + rest = parameterString.substring(globallyCheck.offset) + } + else { + prefix = parameterString.substring(0, usernameCheck.offset) + rest = parameterString.substring(usernameCheck.offset) + } + + if (!globallyCheck.isAccepted() && 'globally'.startsWith(rest)) { + yield `${prefix}globally ` + } + } + + for (const userID of this.profileStore.getLastActivity()) { + const user = this.profileStore.get(userID) + if (!user.username.startsWith(parameterString)) continue + + yield `"${user.username.replace(/"/g, '""')}" ` + } + } + } + Unsuspension.DEPENDENCIES = DEPENDENCIES + + return Unsuspension +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/CommandHandler.js b/files_wcf/js/Bastelstu.be/Chat/CommandHandler.js new file mode 100644 index 0000000..da87fdd --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/CommandHandler.js @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Parser' + , './ParseError' + ], function (Parser, ParseError) { + "use strict"; + + const DEPENDENCIES = [ 'Trigger', 'Command' ] + class CommandHandler { + constructor(triggers, commands) { + this.triggers = triggers + this.commands = commands + } + + splitCommand(input) { + const result = Parser.Command.parse(Parser.Streams.ofString(input)) + + if (result.isAccepted()) { + return result.value + } + else { + throw new ParseError('Empty trigger') + } + } + + applyCommand(command, parameterString) { + const result = command.getParameterParser().parse(Parser.Streams.ofString(parameterString)) + + if (result.isAccepted()) { + return result.value + } + else { + throw new ParseError('Could not parse', { result, parameterString }) + } + } + + getTriggers() { + return this.triggers.keys() + } + + getCommandByTrigger(trigger) { + const data = this.triggers.get(trigger) + + if (data == null) return null + + return this.getCommandByIdentifier(...data) + } + + getCommandByIdentifier(packageName, identifier) { + return this.commands[`${packageName.replace(/\./g, '-')}:${identifier}`] + } + } + CommandHandler.DEPENDENCIES = DEPENDENCIES + + return CommandHandler +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/DataStructure/EventEmitter.js b/files_wcf/js/Bastelstu.be/Chat/DataStructure/EventEmitter.js new file mode 100644 index 0000000..1d8db3b --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/DataStructure/EventEmitter.js @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + const listeners = new WeakMap() + const EventEmitter = function (target) { + Object.assign(target, { + on(type, listener, options = { }) { + if (!listeners.has(this)) { + listeners.set(this, new Map()) + } + if (!listeners.get(this).has(type)) { + listeners.get(this).set(type, new Set()) + } + + if (!options.once) options.once = false + listeners.get(this).get(type).add({ listener, options }) + }, + + off(type, listener) { + listeners.get(this).get(type).delete(listener) + }, + + emit(type, detail = { }) { + if (!listeners.has(this)) return + if (!listeners.get(this).has(type)) return + + const set = listeners.get(this).get(type) + + set.forEach((function ({ listener, options }) { + if (options.once) { + set.delete(listener) + } + + listener({ target: this, detail }) + }).bind(this)) + } + }) + } + + return EventEmitter +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/DataStructure/LRU.js b/files_wcf/js/Bastelstu.be/Chat/DataStructure/LRU.js new file mode 100644 index 0000000..7797fc3 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/DataStructure/LRU.js @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + const s = Symbol('s') + const start = Symbol('start') + + class LRU { + constructor() { + this[s] = new Map() + this[start] = undefined + } + + add(value) { + if (this[start] && this[start].value === value) { + return + } + + if (this[s].has(value)) { + const entry = this[s].get(value) + if (entry.prev) { + entry.prev.next = entry.next + } + if (entry.next) { + entry.next.prev = entry.prev + } + } + const obj = { value, next: this[start], prev: undefined } + this[start] = obj + if (this[start].next) { + this[start].next.prev = obj + } + this[s].set(value, obj) + } + + * [Symbol.iterator]() { + let current = this[start] + do { + yield current.value + } + while ((current = current.next)) + } + } + + return LRU +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/DataStructure/RedBlackTree/Node.js b/files_wcf/js/Bastelstu.be/Chat/DataStructure/RedBlackTree/Node.js new file mode 100644 index 0000000..e73e144 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/DataStructure/RedBlackTree/Node.js @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + class Node { + constructor(value) { + this.value = value + this._left = undefined + this._right = undefined + this.parent = undefined + this.color = 'RED' + } + + get left() { + return this._left + } + + set left(node) { + if (this._left) this._left.parent = undefined + if (node !== undefined) { + if (node.parent !== undefined) { + if (node.isLeftChild) node.parent.left = undefined + else if (node.isRightChild) node.parent.right = undefined + else throw new Error('Unreachable') + } + node.parent = this + } + + this._left = node + } + + get right() { + return this._right + } + + set right(node) { + if (this._right) this._right.parent = undefined + if (node !== undefined) { + if (node.parent !== undefined) { + if (node.isLeftChild) node.parent.left = undefined + else if (node.isRightChild) node.parent.right = undefined + else throw new Error('Unreachable') + } + node.parent = this + } + this._right = node + } + + get isRoot() { + return this.parent === undefined + } + + get isLeaf() { + return this.left === undefined && this.right === undefined + } + + get isLeftChild() { + if (this.parent === undefined) return false + return this.parent.left === this + } + + get isRightChild() { + if (this.parent === undefined) return false + return this.parent.right === this + } + + get grandparent() { + if (this.parent === undefined) return undefined + return this.parent.parent + } + + get sibling() { + if (this.parent === undefined) return undefined + if (this.isLeftChild) return this.parent.right + return this.parent.left + } + + get uncle() { + if (this.parent === undefined) return undefined + return this.parent.sibling + } + + search(value) { + if (value === this.value) return [ 'IS', this ] + if (value < this.value) { + if (this.left !== undefined) return this.left.search(value) + return [ 'LEFT', this ] + } + if (value > this.value) { + if (this.right !== undefined) return this.right.search(value) + return [ 'RIGHT', this ] + } + throw new Error('Unreachable') + } + + print(depth = 0) { + console.log(" ".repeat(depth) + `${this.value}: ${this.color} (Parent: ${this.parent ? this.parent.value : '-'})`) + if (this.left) this.left.print(depth + 1) + else console.log(" ".repeat(depth + 1) + '-') + if (this.right) this.right.print(depth + 1) + else console.log(" ".repeat(depth + 1) + '-') + } + + check() { + if (this.left && this.left.value >= this.value) throw new Error('Invalid' + this.value); + if (this.right && this.right.value <= this.value) throw new Error('Invalid' + this.value); + if (this.color === 'RED' && ((this.left && this.left.color !== 'BLACK') || (this.right && this.right.color !== 'BLACK'))) throw new Error('Invalid' + this.value); + + let leftBlacks = 1, rightBlacks = 1 + if (this.left) { + leftBlacks = this.left.check() + } + if (this.right) { + rightBlacks = this.right.check() + } + if (leftBlacks !== rightBlacks) throw new Error('Invalid' + this.value); + + if (this.color === 'BLACK') return leftBlacks + 1 + return leftBlacks + } + } + + return Node +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/DataStructure/RedBlackTree/Tree.js b/files_wcf/js/Bastelstu.be/Chat/DataStructure/RedBlackTree/Tree.js new file mode 100644 index 0000000..9285a9c --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/DataStructure/RedBlackTree/Tree.js @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Node' ], function (Node) { + "use strict"; + + class Tree { + constructor() { + this.root = undefined + } + + search(value) { + if (this.root !== undefined) return this.root.search(value) + return undefined + } + + insert(value) { + const node = new Node(value) + if (this.root === undefined) { + this.root = node + this.fix(node) + return [ 'RIGHT', undefined ] + } + + const search = this.search(value) + const [ side, parent ] = search + + if (side === 'IS') return [ side, parent.value ] + if (side === 'LEFT') { + parent.left = node + this.fix(node) + return [ side, parent.value ] + } + if (side === 'RIGHT') { + parent.right = node + this.fix(node) + return [ side, parent.value ] + } + throw new Error('Unreachable') + } + + fix(N) { + // Case 1: + if (N.parent === undefined) { + N.color = 'BLACK' + return + } + // Case 2: + if (N.parent.color === 'BLACK') { + return + } + + // Case 3: + const U = N.uncle + if (U !== undefined && U.color === 'RED') { + N.parent.color = 'BLACK' + U.color = 'BLACK' + const G = N.grandparent + G.color = 'RED' + this.fix(G) + return + } + // Case 4: + if (N.isRightChild && N.parent.isLeftChild) { + this.rotateLeft(N.parent) + N = N.left + } + else if (N.isLeftChild && N.parent.isRightChild) { + this.rotateRight(N.parent) + N = N.right + } + + // Case 5 + const G = N.grandparent + N.parent.color = 'BLACK' + G.color = 'RED' + if (N.isLeftChild) { + this.rotateRight(G) + } + else { + this.rotateLeft(G) + } + } + + rotateLeft(N) { + if (N.right === undefined) return + + const right = N.right + N.right = right.left + if (N.parent === undefined) { + this.root = right + } + else if (N.isLeftChild) { + N.parent.left = right + } + else if (N.isRightChild) { + N.parent.right = right + } + + right.left = N + } + + rotateRight(N) { + if (N.left === undefined) return + + const left = N.left + N.left = left.right + if (N.parent === undefined) { + this.root = left + } + else if (N.isLeftChild) { + N.parent.left = left + } + else if (N.isRightChild) { + N.parent.right = left + } + left.right = N + } + } + + return Tree +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/DataStructure/Throttle.js b/files_wcf/js/Bastelstu.be/Chat/DataStructure/Throttle.js new file mode 100644 index 0000000..1e6813a --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/DataStructure/Throttle.js @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + class Throttler { + constructor(callback, delay = 125) { + if (!(typeof callback === 'function')) { + throw new Error('Callback must be a function.') + } + if (delay < 0) { + throw new Error('Delay must be non-negative.') + } + + this.callback = callback + this._delay = delay + + this.hot = false + this.awaiting = false + this.timer = null + this.last = Date.now() + } + + setTimer() { + if (this.timer != null) { + clearTimeout(this.timer) + } + + this.timer = setTimeout(_ => { + this.timer = null + this.hot = false + + if (this.awaiting) { + this.execute() + } + }, this.delay) + } + + execute() { + this.awaiting = false + this.hot = true + + this.last = Date.now() + + this.setTimer() + this.callback() + } + + guardedExecute() { + if (this.hot) { + this.awaiting = true + } + else { + this.execute() + } + } + + get delay() { + return this._delay + } + + set delay(newDelay) { + if (this.awaiting && (Date.now() - this.last) > newDelay) { + this._delay = 0 + this.setTimer() + } + else if (this.timer) { + this._delay = Math.max(0, newDelay - (Date.now() - this.last)) + this.setTimer() + } + + this._delay = newDelay + } + } + + const throttle = function (callback, delay = 125) { + const throttler = new Throttler(callback, delay) + const result = throttler.guardedExecute.bind(throttler) + result.setDelay = function (newDelay) { + throttler.delay = newDelay + } + result.getDelay = function () { + return throttler.delay + } + + return result + } + + return throttle +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Helper.js b/files_wcf/js/Bastelstu.be/Chat/Helper.js new file mode 100644 index 0000000..4f591f9 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Helper.js @@ -0,0 +1,378 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Date/Util' + , 'WoltLabSuite/Core/Language' + ], function (DateUtil, Language) { + "use strict"; + + class Helper { + static deepFreeze(obj) { + const propNames = Object.getOwnPropertyNames(obj) + + propNames.forEach(function (name) { + let prop = obj[name] + + if (typeof prop === 'object' && prop !== null) Helper.deepFreeze(prop) + }) + + return Object.freeze(obj) + } + + /** + * Returns true if the given element is an input[type=text], + * input[type=password] or textarea. + * + * @param {Node} element + * @returns {boolean} + */ + static isInput(element) { + if (element.tagName === 'INPUT') { + if (element.getAttribute('type') !== 'text' && element.getAttribute('type') !== 'password') { + return false + } + } + else if (element.tagName !== 'TEXTAREA') { + return false + } + + return true + } + + static throttle(fn, threshold = 250, scope) { + let last = 0 + let deferTimer = null + + return function() { + const now = new Date().getTime() + const args = arguments + const context = scope || this + + if (last && (now < (last + threshold))) { + clearTimeout(deferTimer) + + return deferTimer = setTimeout(function() { + last = now + + return fn.apply(context, args) + }, threshold) + } + else { + last = now + + return fn.apply(context, args) + } + } + } + + /** + * Returns the caret position of the given element. If the element + * is not an input or textarea element -1 is returned. + * + * @param {Node} element + * @returns {number} + */ + static getCaret(element) { + if (!Helper.isInput(element)) throw new Error('Unsupported element') + + let position = 0 + + if (element.selectionStart) { + position = element.selectionStart + } + + return position + } + + static setCaret(element, position) { + if (!Helper.isInput(element)) throw new Error('Unsupported element') + + if (element.selectionStart) { + element.focus() + element.setSelectionRange(position, position) + } + } + + static wrapElement(element, wrapper) { + wrapper = wrapper || document.createElement('div') + + if (element.nextSibling) { + element.parentNode.insertBefore(wrapper, element.nextSibling) + } + else { + element.parentNode.appendChild(wrapper) + } + + return wrapper.appendChild(element) + } + + // Based on https://github.com/alexdunphy/flexText + static makeFlexible(textarea) { + if (textarea.tagName !== 'TEXTAREA') { + throw new Error(`Unsupported element type: ${textarea.tagName}`) + } + + const pre = document.createElement('pre') + const span = document.createElement('span') + + const mirror = function () { + span.textContent = textarea.value + } + + if (!textarea.parentNode.classList.contains('flexibleTextarea')) { + Helper.wrapElement(textarea) + textarea.parentNode.classList.add('flexibleTextarea') + } + + textarea.classList.add('flexibleTextareaContent') + pre.classList.add('flexibleTextareaMirror') + + pre.appendChild(span) + pre.appendChild(document.createElement('br')) + textarea.parentNode.insertBefore(pre, textarea) + + textarea.addEventListener('input', mirror) + mirror() + } + + static getCircularArray(size) { + class CircularArray extends Array { + constructor(size) { + super() + + Object.defineProperty(this, 'size', { enumerable: false + , value: size + , writable: false + , configurable: false + }); + } + + push() { + super.push.apply(this, arguments) + + if (this.length > this.size) { + super.shift() + } + + return this.length; + } + + unshift() { + super.unshift.apply(this, arguments) + + if (this.length > this.size) { + super.pop() + } + + return this.length; + } + + first() { + return this[0] + } + + last() { + return this[this.length - 1] + } + } + + return new CircularArray(size) + } + + static intToRGBHex(integer) { + const r = ((integer >> 16) & 0xFF).toString(16) + const g = ((integer >> 8) & 0xFF).toString(16) + const b = ((integer >> 0) & 0xFF).toString(16) + + const rr = r.length == 1 ? `0${r}` : r + const gg = g.length == 1 ? `0${g}` : g + const bb = b.length == 1 ? `0${b}` : b + + return `#${rr}${gg}${bb}` + } + + /** + * Returns the markup of a `time` element based on the given date just like a `time` + * element created by `wcf\system\template\plugin\TimeModifierTemplatePlugin`. + * + * @param {Date} date displayed date + * @returns {string} `time` element + */ + static getTimeElementHTML(date) { + const isFutureDate = date.getTime() > new Date().getTime() + let dateTime = '' + + if (isFutureDate) { + dateTime = DateUtil.formatDateTime(date) + } + + // WSC 3.1 + if (typeof DateUtil.getTimeElement === 'function') { + const elem = DateUtil.getTimeElement(date) + + // Work around a bug in DateUtil paired with Time/Relative + if (isFutureDate) elem.innerText = dateTime + + return elem.outerHTML + } + + return `<time class="datetime" + datetime="${DateUtil.format(date, 'c')}" + data-date="${DateUtil.formatDate(date)}" + data-time="${DateUtil.formatTime(date)}" + data-offset="${date.getTimezoneOffset() * 60}" + data-timestamp="${(date.getTime() - date.getMilliseconds()) / 1000}" + ${isFutureDate ? 'data-is-future-date="true"' : ''} + >${dateTime}</time>` + } + + /** + * Returns whether the supplied selection range covers the whole text inside the given node + * + * Source: https://stackoverflow.com/a/27686686/1112384 + * + * @param {Range} range Selection range + * @param {Node} + * @return {Boolean} + */ + static rangeSpansTextContent(range, node) { + const treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT) + + let firstTextNode, lastTextNode + while (treeWalker.nextNode()) { + if (treeWalker.currentNode.nodeValue.trim() === '') continue + + if (!firstTextNode) { + firstTextNode = treeWalker.currentNode + } + + lastTextNode = treeWalker.currentNode + } + + const nodeRange = range.cloneRange() + if (firstTextNode) { + nodeRange.setStart(firstTextNode, 0) + nodeRange.setEnd(lastTextNode, lastTextNode.length) + } + else { + nodeRange.selectNodeContents(node) + } + + const bp1 = range.compareBoundaryPoints(Range.START_TO_START, nodeRange) + const bp2 = range.compareBoundaryPoints(Range.END_TO_END, nodeRange) + + return bp1 < 1 && bp2 > -1 + } + + /** + * Returns the text of a node and its children. + * + * @see {@link https://github.com/WoltLab/WCF/blob/a20be4267fc711299d6bde7c34a8b36199ae393f/wcfsetup/install/files/js/WCF.Message.js#L1180-L1264} + * @param {Node} node + * @return {String} + */ + static getTextContent(node) { + const acceptNode = node => { + if (node instanceof Element) { + if (node.tagName === 'SCRIPT' || node.tagName === 'STYLE') return NodeFilter.FILTER_REJECT + } + + return NodeFilter.FILTER_ACCEPT + } + + let out = '' + + const flags = NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT + const treeWalker = document.createTreeWalker(node, flags, { acceptNode }) + + const ignoredLinks = [ ] + + while (treeWalker.nextNode()) { + const node = treeWalker.currentNode + + if (node instanceof Text) { + if (node.parentNode.tagName === 'A' && ignoredLinks.indexOf(node.parentNode) >= 0) { + continue + } + + out += node.nodeValue.replace(/\n/g, '') + } + else { + switch (node.tagName) { + case 'IMG': { + const alt = node.getAttribute('alt') + + if (node.classList.contains('smiley')) { + out += ` ${alt} ` + } + else if (alt && alt !== '') { + out += ` ${alt} [Image ${node.src}] ` + } + else { + out += ` [Image ${node.src}] ` + } + break } + + case 'BR': + case 'LI': + case 'UL': + case 'DIV': + case 'TR': + out += '\n' + break + + case 'TH': + case 'TD': + out += '\t' + break + + case 'P': + out += '\n\n' + break + + case 'A': { + let link = node.href + const text = node.textContent.trim() + + // handle named anchors + if (text !== '' && text !== node.href) { + ignoredLinks.push(node) + + let truncated = false + + if (text.indexOf('\u2026') >= 0) { + const parts = text.split(/\u2026/) + + if (parts.length === 2) { + truncated = node.href.startsWith(parts[0]) && node.href.endsWith(parts[1]) + } + } + + if (!truncated) { + link = `${node.textContent} [URL:${node.href}]` + } + } + + out += link + break } + } + } + } + + return out + } + } + + return Helper +}); + diff --git a/files_wcf/js/Bastelstu.be/Chat/LocalStorage.js b/files_wcf/js/Bastelstu.be/Chat/LocalStorage.js new file mode 100644 index 0000000..d310625 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/LocalStorage.js @@ -0,0 +1,201 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Core', './LocalStorageEmulator' ], function (Core, LocalStorageEmulator) { + 'use strict'; + + const DEPENDENCIES = [ ] + class LocalStorage { + constructor(subprefix) { + this.subprefix = subprefix + this.hasLocalStorage = false + this.setupStorage() + } + + static isQuotaExceeded(error) { + return error instanceof DOMException && ( + // everything except Firefox + error.code === 22 || + // Firefox + error.code === 1014 || + // everything except Firefox + error.name === 'QuotaExceededError' || + // Firefox + error.name === 'NS_ERROR_DOM_QUOTA_REACHED') + } + + static isAvailable() { + try { + const x = '__storage_test__' + window.localStorage.setItem(x, x) + window.localStorage.removeItem(x) + return true + } + catch (error) { + return LocalStorage.isQuotaExceeded(error) + } + } + + setupStorage() { + if (LocalStorage.isAvailable()) { + this.storage = window.localStorage + this.hasLocalStorage = true + } + else { + console.info('Falling back to in-memory local storage emulation') + this.storage = new LocalStorageEmulator() + } + } + + /** + * Return the prefix to use for the local storage + * + * @returns {string} The storage prefix + */ + get storagePrefix() { + let prefix = '' + + // WSC 3.1 + if (typeof Core.getStoragePrefix === 'function') { + prefix = Core.getStoragePrefix() + } + + return `${prefix}be.bastelstu.Chat.${this.subprefix}` + } + + /** + * Calls listener, whenever key changes. + * + * @param {string} key The key to observe. + * @param {*} listener The listener to call. + */ + observe(key, listener) { + window.addEventListener('storage', (event) => { + if (event.storageArea !== window.localStorage) return + if (event.key !== `${this.storagePrefix}${key}`) return + + listener(event) + }) + } + + /** + * Sets the value of a setting + * + * @param {string} key The key of the setting to set + * @param {string} value The new value of the setting + * @returns {string} + */ + set(key, value) { + try { + this.storage.setItem(`${this.storagePrefix}${key}`, JSON.stringify(value)) + } + catch (error) { + if (!LocalStorage.isQuotaExceeded(error)) throw error + + console.warn(`Your localStorage has exceeded the size quota for this domain`) + console.warn(`We are falling back to an in-memory storage, this does not persist data!`) + console.error(error) + + const storage = new LocalStorageEmulator() + + // Make a copy of the current localStorage + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i) + const value = localStorage.getItem(key) + storage.setItem(key, value) + } + + // Replace the localStorage with our in-memory variant + this.storage = storage + } + + return this.get(key) + } + + /** + * Retrieves the value of a setting + * + * @param {string} key The key of the setting to retrieve + * @returns {string} The current value of the setting + */ + get(key) { + const value = this.storage.getItem(`${this.storagePrefix}${key}`) + + if (value == null) return null + return JSON.parse(value) + } + + /** + * Returns whether the given setting has a value. + * + * @param {string} key The key of the setting to check + * @returns {boolean} + */ + has(key) { + return this.storage.getItem(`${this.storagePrefix}${key}`) != null + } + + /** + * Removes a single setting + * + * @param {string} key The key of the setting to remove + * @returns {string} The last value of the provided setting + */ + remove(key) { + const value = this.get(key) + const storageKey = `${this.storagePrefix}${key}` + + this.storage.removeItem(storageKey) + + return value + } + + /** + * Removes all of the chat settings with the right prefix + * and try to use the real localStorage again, if the qouta isn’t exceeded anymore + */ + clear() { + const _clear = (target) => { + for (let key in target) { + if (!key.startsWith(this.storagePrefix) || !target.hasOwnProperty(key)) continue + + target.removeItem(key) + } + } + + if (this.hasLocalStorage && this.storage instanceof LocalStorageEmulator) { + try { + // Try to clear the real localStorage + _clear(localStorage) + + // Check if we can use the localStorage again + const x = '__storage_test__' + window.localStorage.setItem(x, x) + window.localStorage.removeItem(x) + + // It should be safe to use the localStorage again, as the storage + // of this instance (given by the prefix) has been cleared completely + this.storage = localStorage + + console.log('Switched back to using the localStorage') + } + catch (error) { /* no we can’t */ } + } + + _clear(this.storage) + } + } + LocalStorage.DEPENDENCIES = DEPENDENCIES + + return LocalStorage +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/LocalStorageEmulator.js b/files_wcf/js/Bastelstu.be/Chat/LocalStorageEmulator.js new file mode 100644 index 0000000..2eb918a --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/LocalStorageEmulator.js @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + 'use strict'; + + class LocalStorageEmulator { + constructor () { + this._data = new Map() + return new Proxy(this, { + get(target, property) { + // Check if the property exists on the object or its prototype + if (target.hasOwnProperty(property) || Object.getPrototypeOf(target)[property]) { + return target[property] + } + + // Otherwise proxy to the underlying map + return target.getItem(property) + }, + set(target, property, value) { + // Check if the property exists on the object or its prototype + if (target.hasOwnProperty(property) || Object.getPrototypeOf(target)[property]) { + target[property] = value + } + else { + // Proxy to the underlying map + target.setItem(property, value) + } + }, + has(target, property) { + return target.hasOwnProperty(property) // check the properties of the object + || Object.getPrototypeOf(target)[property] // check its prototype + || target._data.has(property) // check the underlying map + }, + ownKeys(target) { + // Proxy to the underlying map + return Array.from(target._data.keys()) + }, + getOwnPropertyDescriptor(target, property) { + // Make the properties of the map visible + return { + enumerable: true, + configurable: true + } + } + }) + } + + get length() { + return this._data.size + } + + key(n = 0) { + return Array.from(this._data.keys())[n] + } + + getItem(key) { + return this._data.get(key) + } + + setItem(key, value) { + this._data.set(key, value) + } + + removeItem(key) { + this._data.delete(key) + } + + clear() { + this._data.clear() + } + + * [Symbol.iterator]() { + yield * this._data.values() + } + } + + return LocalStorageEmulator +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Log.js b/files_wcf/js/Bastelstu.be/Chat/Log.js new file mode 100644 index 0000000..e864eb5 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Log.js @@ -0,0 +1,223 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './console' + , 'Bastelstu.be/bottle' + , 'WoltLabSuite/Core/Core' + , './Message' + , './Messenger' + , './ProfileStore' + , './Room' + , './Template' + , './Ui/Log' + , './Ui/MessageStream' + , './Ui/MessageActions/Delete' + ], function (console, Bottle, Core, Message, Messenger, ProfileStore, Room, Template, Ui, UiMessageStream, UiMessageActionDelete) { + "use strict"; + + const loader = Symbol('loader') + + class Log { + constructor(params, config) { + console.debug('ChatLog.constructor', 'Constructing …') + + this.config = config + + this.sessionID = Core.getUuid() + this.bottle = new Bottle() + this.bottle.value('bottle', this.bottle) + this.bottle.value('config', config) + this.bottle.constant('sessionID', this.sessionID) + this.bottle.constant('roomID', params.roomID) + + // Register chat components + this.service('Messenger', Messenger) + this.service('ProfileStore', ProfileStore) + this.service('Room', Room) + + // Register UI components + this.service('Ui', Ui) + this.service('UiMessageActionDelete', UiMessageActionDelete) + this.service('UiMessageStream', UiMessageStream) + + // Register Models + this.bottle.instanceFactory('Message', (container, m) => { + return new Message(container.MessageType, m) + }) + + // Register Templates + const selector = [ '[type="x-text/template"]' + , '[data-application="be.bastelstu.chat"]' + , '[data-template-name]' + ].join('') + const templates = elBySelAll(selector) + templates.forEach((function (template) { + this.bottle.factory(`Template.${elData(template, 'template-name')}`, function (container) { + const includeNames = (elData(template, 'template-includes') || '').split(/ /).filter(item => item !== "") + const includes = { } + includeNames.forEach(item => includes[item] = container[item]) + + return new Template(template.textContent, includes) + }) + }).bind(this)) + + // Register MessageTypes + const messageTypes = Object.entries(this.config.messageTypes) + messageTypes.forEach(([ objectType, messageType ]) => { + const MessageType = require(messageType.module) + + this.bottle.factory(`MessageType.${objectType.replace(/\./g, '-')}`, _ => { + const deps = this.bottle.digest(MessageType.DEPENDENCIES || []) + + return new MessageType(...deps, objectType) + }) + }) + + this.knows = { from: undefined + , to: undefined + } + + this.messageSinks = new Set() + + this.params = params + + this.pulling = false + } + + service(name, _constructor, args = [ ]) { + this.bottle.factory(name, function (container) { + const deps = (_constructor.DEPENDENCIES || [ ]).map(dep => container[dep]) + + return new _constructor(...deps, ...args) + }) + } + + async bootstrap() { + console.debug('ChatLog.bootstrap', 'Initializing …') + + this.ui = this.bottle.container.Ui + this.ui.bootstrap() + + this.registerMessageSink(this.bottle.container.UiMessageStream) + + if (this.params.messageID > 0) { + await Promise.all([ this.pull(undefined, this.params.messageID) + , this.pull(this.params.messageID + 1) + ]) + } + else { + await this.pull() + } + + this.bottle.container.UiMessageStream.on('nearTop', this.pullOlder.bind(this)) + this.bottle.container.UiMessageStream.on('reachedTop', this.pullOlder.bind(this)) + this.bottle.container.UiMessageStream.on('nearBottom', this.pullNewer.bind(this)) + this.bottle.container.UiMessageStream.on('reachedBottom', this.pullNewer.bind(this)) + + const element = document.querySelector(`#message-${this.params.messageID}`) + + // Force changing the hash to trigger a new lookup of the element. + // At least Chrome won’t target an element if it is not in the DOM + // on the initial page load with an hash set. + window.location.hash = '' + window.location.hash = `message-${this.params.messageID}` + + if (element && element.scrollIntoView) element.scrollIntoView() + + return this + } + + registerMessageSink(sink) { + if (typeof sink.ingest !== 'function') { + throw new Error('The given sink does not provide a .ingest function.') + } + + this.messageSinks.add(sink) + } + + unregisterMessageSink(sink) { + this.messageSinks.delete(sink) + } + + async pull(from, to) { + try { + await this.handlePull(await this.performPull(from, to)) + } + catch (e) { + this.handleError(e) + } + } + + async pullOlder() { + if (this.pulling) return + if (this.knows.from <= 1) return + + this.pulling = true + + await this.pull(undefined, this.knows.from - 1) + + this.pulling = false + } + + async pullNewer() { + if (this.pulling) return + + this.pulling = true + + await this.pull(this.knows.to + 1) + + this.pulling = false + } + + async performPull(from = undefined, to = undefined) { + console.debug('ChatLog.performPull', `Pulling new messages; from: ${from !== undefined ? from : 'undefined'}, to: ${to !== undefined ? to : 'undefined'}`) + + return this.bottle.container.Messenger.pull(from, to, true) + } + + handleError(error) { + console.debug('ChatLog.handleError', `Request failed`) + console.debugException(error) + } + + async handlePull(payload) { + console.debug('ChatLog.handlePull', payload) + + // Null range: No messages satisfy the constraints + if (payload.from > payload.to) return + + let messages = payload.messages + + if (this.knows.from !== undefined && this.knows.to !== undefined) { + messages = messages.filter((function (message) { + return !(this.knows.from <= message.messageID && message.messageID <= this.knows.to) + }).bind(this)) + } + + if (this.knows.from === undefined || payload.from < this.knows.from) this.knows.from = payload.from + if (this.knows.to === undefined || payload.to > this.knows.to) this.knows.to = payload.to + + await Promise.all(messages.map((message) => { + return message.getMessageType().preProcess(message) + })) + + const userIDs = messages.map(message => message.userID) + .filter(userID => userID !== null) + await this.bottle.container.ProfileStore.ensureUsersByIDs(userIDs) + + this.messageSinks.forEach(sink => sink.ingest(messages)) + } + } + + return Log +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Message.js b/files_wcf/js/Bastelstu.be/Chat/Message.js new file mode 100644 index 0000000..661a259 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Message.js @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Helper' + , 'WoltLabSuite/Core/Date/Util' + , 'WoltLabSuite/Core/User' + ], function (Helper, DateUtil, User) { + "use strict"; + + const m = Symbol('message') + + class Message { + constructor(MessageType, message) { + this[m] = Helper.deepFreeze(message) + this.MessageType = MessageType + } + + get messageID() { + return this[m].messageID + } + + get objectType() { + return this[m].objectType + } + + getMessageType() { + return this.MessageType[this.objectType.replace(/\./g, '-')] + } + + get time() { + return this[m].time + } + + get formattedTime() { + return DateUtil.format(this.date, 'H:i:s') + } + + get date() { + return new Date(this[m].time * 1000) + } + + get link() { + return this[m].link + } + + get userID() { + return this[m].userID + } + + get username() { + return this[m].username + } + + get isIgnored() { + return this[m].isIgnored + } + + get isDeleted() { + return this[m].isDeleted + } + + get payload() { + return this[m].payload + } + + isOwnMessage() { + return this.userID === User.userId + } + + wrap() { + return { message: this[m] } + } + + toJSON() { + return this[m] + } + } + + return Message +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType.js b/files_wcf/js/Bastelstu.be/Chat/MessageType.js new file mode 100644 index 0000000..152209e --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType.js @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Date/Util' + , 'WoltLabSuite/Core/Language' + , 'WoltLabSuite/Core/Dom/Util' + , 'Bastelstu.be/Chat/User' + ], function (DateUtil, Language, DomUtil, User) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore', 'Template' ] + class MessageType { + constructor(profileStore, templates, objectType) { + this.profileStore = profileStore + this.templates = templates + + this.objectType = objectType + } + + shouldUpdateUserList() { + return false + } + + getReferencedUsers(message) { + if (message.userID === null) return [ ] + + return [ message.userID ] + } + + preProcess(message) { + + } + + preRender(message) { + + } + + render(message) { + const variables = { message + , users: this.profileStore + , author: this.profileStore.get(message.userID) + , DateUtil + , Language + } + + if (variables.author == null) { + variables.author = User.getGuest(message.username) + } + + return DomUtil.createFragmentFromHtml(this.templates[message.objectType.replace(/\./g, '-')].fetch(variables)) + } + + renderPlainText(message) { + return false + } + + joinable(messageA, messageB) { + return false + } + } + MessageType.DEPENDENCIES = DEPENDENCIES + + return MessageType +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Away.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Away.js new file mode 100644 index 0000000..eb11c35 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Away.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore', 'roomID' ].concat(MessageType.DEPENDENCIES || [ ]) + class Away extends MessageType { + constructor(profileStore, roomID, ...superDeps) { + super(...superDeps) + + this.profileStore = profileStore + this.roomID = roomID + } + + render(message) { + const isSilent = message.payload.rooms.find(room => room.roomID === this.roomID).isSilent + + if (!isSilent) { + return super.render(message) + } + else { + return false + } + } + + shouldUpdateUserList(message) { + return true + } + + preProcess(message) { + this.profileStore.expire(message.userID) + } + } + Away.DEPENDENCIES = DEPENDENCIES + + return Away +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Back.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Back.js new file mode 100644 index 0000000..420f79a --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Back.js @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore', 'roomID' ].concat(MessageType.DEPENDENCIES || [ ]) + class Back extends MessageType { + constructor(profileStore, roomID, ...superDeps) { + super(...superDeps) + + this.profileStore = profileStore + this.roomID = roomID + } + + render(message) { + const isSilent = message.payload.rooms.find(room => room.roomID === this.roomID).isSilent + + if (!isSilent) { + return super.render(message) + } + else { + return false + } + } + + shouldUpdateUserList(message) { + return true + } + + preProcess(message) { + this.profileStore.expire(message.userID) + } + } + Back.DEPENDENCIES = DEPENDENCIES + + return Back +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Broadcast.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Broadcast.js new file mode 100644 index 0000000..9574f88 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Broadcast.js @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Plain' ], function (Plain) { + "use strict"; + + class Broadcast extends Plain { + renderPlainText(message) { + return `[📢] ${message.payload.plaintextMessage}` + } + } + + return Broadcast +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/ChatUpdate.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/ChatUpdate.js new file mode 100644 index 0000000..987f77c --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/ChatUpdate.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + class ChatUpdate extends MessageType { + preRender(message) { + // TODO: hcf()? + } + + render(message) { + return false + } + } + + return ChatUpdate +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Color.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Color.js new file mode 100644 index 0000000..fd6c494 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Color.js @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + class Color extends MessageType { + render(message) { + if (message.isOwnMessage()) { + return super.render(message) + } + else { + return false + } + } + + shouldUpdateUserList(message) { + return true + } + + preProcess(message) { + this.profileStore.expire(message.userID) + } + } + + return Color +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Info.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Info.js new file mode 100644 index 0000000..37c9e45 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Info.js @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Dom/Traverse' + , 'WoltLabSuite/Core/Language' + , '../Helper' + , '../MessageType' + ], function (DomTraverse, Language, Helper, MessageType) { + "use strict"; + + const decorators = Symbol('decorators') + + class Info extends MessageType { + constructor(...superArgs) { + super(...superArgs) + + this[decorators] = new Set() + } + + addDecorator(decorator) { + if (typeof decorator !== 'function') { + throw new TypeError('Supplied argument must be a function') + } + + this[decorators].add(decorator) + } + + getReferencedUsers(message) { + return super.getReferencedUsers(message).concat([ message.payload.user.userID ]) + } + + render(message) { + const rooms = message.payload.rooms.map(function (item) { + const aug = { lastPull: null + , lastPullHTML: null + , lastPush: null + , lastPushHTML: null + } + + if (item.lastPull) { + aug.lastPull = new Date(item.lastPull * 1000) + aug.lastPullHTML = Helper.getTimeElementHTML(aug.lastPull) + } + + if (item.lastPush) { + aug.lastPush = new Date(item.lastPush * 1000) + aug.lastPushHTML = Helper.getTimeElementHTML(aug.lastPush) + } + + return Object.assign({ }, item, aug) + }) + + const payload = Helper.deepFreeze( + Array.from(this[decorators]).reduce( (payload, decorator) => decorator(payload) + , Object.assign({ }, message.payload, { rooms }) + ) + ) + + const fragment = super.render(new Proxy(message, { + get: function (target, property) { + if (property === 'payload') return payload + return target[property] + } + })) + + const icon = elCreate('span') + icon.classList.add('icon', 'icon16', 'fa-times', 'jsTooltip', 'hideIcon') + icon.setAttribute('title', Language.get('wcf.global.button.hide')) + icon.addEventListener('click', () => elHide(DomTraverse.parentBySel(icon, '.chatMessageBoundary'))) + + const elem = fragment.querySelector('.chatMessage .containerList > li:first-child .containerHeadline') + elem.insertBefore(icon, elem.firstChild) + + return fragment + } + } + + return Info +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Join.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Join.js new file mode 100644 index 0000000..c2da914 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Join.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType, Language) { + "use strict"; + + class Join extends MessageType { + shouldUpdateUserList(message) { + return true + } + + renderPlainText(message) { + return '[➡️] ' + Language.get('chat.messageType.be.bastelstu.chat.messageType.join.plain', { author: { username: message.username } }) + } + } + + return Join +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Leave.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Leave.js new file mode 100644 index 0000000..db5ccfe --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Leave.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType', 'WoltLabSuite/Core/Language' ], function (MessageType, Language) { + "use strict"; + + class Leave extends MessageType { + shouldUpdateUserList(message) { + return true + } + + renderPlainText(message) { + return '[⬅️️] ' + Language.get('chat.messageType.be.bastelstu.chat.messageType.leave.plain', { author: { username: message.username } }) + } + } + + return Leave +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Me.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Me.js new file mode 100644 index 0000000..fe030d1 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Me.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + class Me extends MessageType { + + } + + return Me +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Plain.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Plain.js new file mode 100644 index 0000000..e001219 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Plain.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + class Plain extends MessageType { + joinable(a, b) { + return a.userID === b.userID + } + + renderPlainText(message) { + return message.payload.plaintextMessage + } + } + + return Plain +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Suspend.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Suspend.js new file mode 100644 index 0000000..469b685 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Suspend.js @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Helper' + , 'WoltLabSuite/Core/Date/Util' + , '../MessageType' + ], function (Helper, DateUtil, MessageType) { + "use strict"; + + class Suspend extends MessageType { + render(message) { + const expires = message.payload.suspension.expires !== null ? new Date(message.payload.suspension.expires * 1000) : null + const formattedExpires = expires !== null ? DateUtil.formatDateTime(expires) : null + const aug = { expires + , formattedExpires + } + const suspension = Object.assign({ }, message.payload.suspension, aug) + const payload = Helper.deepFreeze(Object.assign({ }, message.payload, { suspension })) + + return super.render(new Proxy(message, { + get: function (target, property) { + if (property === 'payload') return payload + return target[property] + } + })) + } + + shouldUpdateUserList(message) { + return true + } + } + + return Suspend +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Team.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Team.js new file mode 100644 index 0000000..35033a5 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Team.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Plain' ], function (Plain) { + "use strict"; + + class Team extends Plain { + joinable(a, b) { + return a.userID === b.userID + } + + renderPlainText(message) { + return `[⭐] ${message.payload.plaintextMessage}` + } + } + + return Team +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/TemproomCreated.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/TemproomCreated.js new file mode 100644 index 0000000..c1355d9 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/TemproomCreated.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + class TemproomCreated extends MessageType { + + } + + return TemproomCreated +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/TemproomInvited.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/TemproomInvited.js new file mode 100644 index 0000000..09cf9f7 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/TemproomInvited.js @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + class TemproomInvited extends MessageType { + + } + + return TemproomInvited +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Tombstone.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Tombstone.js new file mode 100644 index 0000000..4aa6d9f --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Tombstone.js @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + const DEPENDENCIES = [ 'UiMessageStream' ].concat(MessageType.DEPENDENCIES || [ ]) + class Tombstone extends MessageType { + constructor(messageStream, ...superDeps) { + super(...superDeps) + + this.messageStream = messageStream + } + + render(message) { + if (message.isDeleted) { + return super.render(message) + } + + const messageID = message.payload.messageID + const node = elById(`message-${messageID}`) + if (!node) return false + + node.classList.add('tombstone') + + const chatMessage = node.querySelector('.chatMessage') + if (!chatMessage) return false + + const rendered = super.render(message) + const oldIcon = node.querySelector('.chatMessageContent > .chatMessageIcon') + const newIcon = rendered.querySelector('.chatMessageIcon') + + if (oldIcon) { + oldIcon.parentNode.replaceChild(newIcon, oldIcon) + } + else { + chatMessage.parentNode.insertBefore(newIcon, chatMessage) + } + + chatMessage.parentNode.replaceChild(rendered.querySelector('.chatMessage'), chatMessage) + + return false + } + } + Tombstone.DEPENDENCIES = DEPENDENCIES + + return Tombstone +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Unsuspend.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Unsuspend.js new file mode 100644 index 0000000..5784da2 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Unsuspend.js @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../MessageType' ], function (MessageType) { + "use strict"; + + class Unsuspend extends MessageType { + shouldUpdateUserList(message) { + return true + } + } + + return Unsuspend +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Where.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Where.js new file mode 100644 index 0000000..feb35df --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Where.js @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Dom/Traverse' + , 'WoltLabSuite/Core/Language' + , '../MessageType' + ], function (DomTraverse, Language, MessageType) { + "use strict"; + + class Where extends MessageType { + render(message) { + const fragment = super.render(message) + + const icon = elCreate('span') + icon.classList.add('icon', 'icon16', 'fa-times', 'jsTooltip', 'hideIcon') + icon.setAttribute('title', Language.get('wcf.global.button.hide')) + icon.addEventListener('click', () => elHide(DomTraverse.parentBySel(icon, '.chatMessageBoundary'))) + + const elem = fragment.querySelector('.jsRoomInfo > .containerHeadline') + elem.insertBefore(icon, elem.firstChild) + + return fragment + } + } + + return Where +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/MessageType/Whisper.js b/files_wcf/js/Bastelstu.be/Chat/MessageType/Whisper.js new file mode 100644 index 0000000..626bd31 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/MessageType/Whisper.js @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Plain' ], function (Plain) { + "use strict"; + + const DEPENDENCIES = [ 'UiInput' ].concat(Plain.DEPENDENCIES || [ ]) + class Whisper extends Plain { + constructor(input, ...superDeps) { + super(...superDeps) + + this.input = input + } + + render(message) { + const fragment = super.render(message) + + if (this.input != null) { + Array.prototype.forEach.call(fragment.querySelectorAll('[data-insert-whisper]'), (function (el) { + el.addEventListener('click', (function () { + const username = el.dataset.insertWhisper + const sanitizedUsername = username.replace(/"/g, '""') + const command = `/whisper "${sanitizedUsername}"` + + if (this.input.getText().indexOf(command) !== 0) { + this.input.insertText(`${command} `, { prepend: true, append: false }) + this.input.focus() + } + }).bind(this)) + }).bind(this)) + } + + return fragment + } + + joinable(a, b) { + return a.userID === b.userID && a.payload.recipient === b.payload.recipient + } + } + Whisper.DEPENDENCIES = DEPENDENCIES + + return Whisper +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Messenger.js b/files_wcf/js/Bastelstu.be/Chat/Messenger.js new file mode 100644 index 0000000..90083bf --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Messenger.js @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './console' + , 'Bastelstu.be/PromiseWrap/Ajax' + , './Room' + ], function (console, Ajax, Room) { + "use strict"; + + const DEPENDENCIES = [ 'sessionID', 'Room', 'Message' ] + class Messenger { + constructor(sessionID, room, Message) { + if (!(room instanceof Room)) throw new TypeError('You must pass a Room to the Messenger') + + this.sessionID = sessionID + this.room = room + this.Message = Message + } + + async pull(from = 0, to = 0, inLog = false) { + console.debug(`Messenger.pull`, 'from', from, 'to', to, 'inLog', inLog) + + const payload = { actionName: 'pull' + , parameters: { inLog } + } + if (from !== 0 && to !== 0) { + throw new Error('You must not set both from and to') + } + if (from !== 0) payload.parameters.from = from + if (to !== 0) payload.parameters.to = to + + const data = await Ajax.api(this, payload) + const messages = Object.values(data.returnValues.messages).map((item) => this.Message.instance(item)) + const { from: newFrom, to: newTo } = data.returnValues + + return { messages, from: newFrom, to: newTo } + } + + async push({ commandID, parameters }) { + const payload = { actionName: 'push' + , parameters: { commandID + , parameters: JSON.stringify(parameters) + } + } + + return Ajax.api(this, payload) + } + + _ajaxSetup() { + return { silent: true + , ignoreError: true + , data: { className: 'chat\\data\\message\\MessageAction' + , parameters: { roomID: this.room.roomID + , sessionID: this.sessionID + } + } + } + } + } + Messenger.DEPENDENCIES = DEPENDENCIES + + return Messenger +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/ParseError.js b/files_wcf/js/Bastelstu.be/Chat/ParseError.js new file mode 100644 index 0000000..a9e2179 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/ParseError.js @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + class ParseError extends Error { + constructor(message, data) { + super(message) + + this.data = data + } + } + + return ParseError +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Parser.js b/files_wcf/js/Bastelstu.be/Chat/Parser.js new file mode 100644 index 0000000..fc0de16 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Parser.js @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'Bastelstu.be/parser-combinator' + ], function (parsec) { + "use strict"; + + const { C, F, N, X, parser, Streams } = parsec + const response = parsec.parsec.response + + const peek = function (p) { + return new parser((input, index = 0) => + p + .parse(input, index) + .fold( + accept => response.accept(accept.value, accept.input, index, false), + reject => response.reject(input.location(reject.offset), false) + ) + ); + } + + const Whitespace = F.satisfy(item => /\s/.test(item)) + + const Rest = F.any.optrep().map(item => item.join('')) + const Rest1 = F.any.rep().map(item => item.join('')) + + const AlnumTrigger = C.letter.or(N.digit).rep().map(item => item.join('')) + const SymbolicTrigger = F.not(C.letter.or(N.digit).or(Whitespace)).rep().map(item => item.join('')) + const Slash = C.char('/') + const Trigger = Slash.thenRight( + peek(Slash.map(item => null)).or(AlnumTrigger.thenLeft(Whitespace.rep().or(F.eos))).or(SymbolicTrigger.thenLeft(Whitespace.optrep())) + ).or(F.returns(null)) + const Command = Trigger.then(Rest) + + const Quote = C.char('"') + const QuotedUsername = Quote.thenRight( + ((Quote.thenRight(Quote)).or(F.not(Quote))).rep() + ).thenLeft(Quote).map(item => item.join('')) + const Comma = C.char(',') + const UnquotedUsername = F.not(Comma.or(Quote).or(Whitespace)).then(F.not(Comma.or(Whitespace)).optrep().map(item => item.join(''))).map(item => item.join('')) + const Username = QuotedUsername.or(UnquotedUsername) + + const Decimal = (length) => N.digit.occurrence(length).map(item => parseInt(item.join(''), 10)) + + const Hexadecimal = N.digit + .or(C.charIn('abcdefABCDEF')) + .rep() + .map(x => x.join('')) + + const RGBHex = (C.char('#').opt()) + .thenRight( + Hexadecimal.filter(x => x.length === 3 || x.length === 6) + .map(item => { + if (item.length === 3) { + item = `${item[0]}${item[0]}${item[1]}${item[1]}${item[2]}${item[2]}` + } + + return item + }) + ).map(item => `#${item}`) + + const Dash = C.char('-') + const Datestring = Decimal(4).filter(item => 2000 <= item && item <= 2030) + .thenLeft(Dash).then(Decimal(2).filter(item => 1 <= item && item <= 12)) + .thenLeft(Dash).then(Decimal(2).filter(item => 1 <= item)) + + const Colon = C.char(':') + const Timestring = Decimal(2).filter(item => 0 <= item && item <= 23) + .thenLeft(Colon).then(Decimal(2).filter(item => 0 <= item && item <= 59)) + .thenLeft(Colon).then(Decimal(2).filter(item => 0 <= item && item <= 59)) + + const ISODate = Datestring.then(C.char('T').thenRight(Timestring).opt()).map(function ([ year, month, day, time ]) { + const date = new Date() + date.setFullYear(year) + date.setMonth(month - 1) + date.setDate(day) + + time.map(function ([ hour, minute, second ]) { + date.setHours(hour) + date.setMinutes(minute) + date.setSeconds(second) + }) + + return date + }) + + return { + Streams, + stream: Streams, + AlnumTrigger, + Colon, + Command, + Dash, + Datestring, + Decimal, + Hexadecimal, + ISODate, + Quote, + QuotedUsername, + RGBHex, + Rest, + Rest1, + Slash, + SymbolicTrigger, + Timestring, + Trigger, + UnquotedUsername, + Username, + Whitespace, + C, + F, + N, + X, + } +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/ProfileStore.js b/files_wcf/js/Bastelstu.be/Chat/ProfileStore.js new file mode 100644 index 0000000..969bf83 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/ProfileStore.js @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'Bastelstu.be/PromiseWrap/Ajax' + , './DataStructure/LRU' + , './User' + , 'WoltLabSuite/Core/User' + ], function (Ajax, LRU, User, CoreUser) { + "use strict"; + + const DEPENDENCIES = [ ] + /** + * ProfileStore stores information about users. + */ + class ProfileStore { + constructor() { + this.users = new Map() + this.processing = new Map() + + this.lastActivity = new LRU() + } + + /** + * Ensures that information about the given userIDs are available + * in the store. The returned promise resolves once all the requests + * to fetch the data finished successfully. + * + * @param {number[]} userIDs + * @returns {Promise} + */ + async ensureUsersByIDs(userIDs) { + // Dedup + userIDs = userIDs.filter((value, index, self) => self.indexOf(value) === index) + .map(userID => parseInt(userID, 10)) + + const missing = [ ] + const promises = [ ] + userIDs.forEach((function (userID) { + if (this.isRecent(userID)) return + if (this.processing.has(userID)) { + promises.push(this.processing.get(userID)) + return + } + missing.push(userID) + }).bind(this)) + + if (missing.length > 0) { + const payload = { actionName: 'getUsersByID' + , parameters: { userIDs: missing } + } + const request = (async _ => { + try { + const response = await Ajax.api(this, payload) + return Object.entries(response.returnValues).forEach(([ userID, user ]) => { + userID = parseInt(userID, 10) + const data = { user: new User(user) + , date: Date.now() + } + this.users.set(userID, data) + this.processing.delete(userID) + }) + } + catch (err) { + missing.forEach(userID => this.processing.delete(userID)) + + throw err + } + })() + + missing.forEach(userID => this.processing.set(userID, request)) + promises.push(request) + } + + await Promise.all(promises) + } + + /** + * Returns information about the given userIDs. + * + * @param {number[]} userIDs + * @returns {Promise} + */ + async getUsersByIDs(userIDs) { + await this.ensureUsersByIDs(userIDs) + + return new Map(userIDs.map(userID => [ userID, this.get(userID) ])) + } + + /** + * Returns information about the currently logged in user. + * + * @returns {Promise} + */ + getSelf() { + const self = this.get(CoreUser.userId) + if (self == null) { + throw new Error('Unreachable') + } + + return self + } + + /** + * Returns information about the given userID. + * + * @param {number} userID + * @returns {?User} null if no information are known + */ + get(userID) { + const user = this.users.get(userID) + + if (user != null) { + return user.user + } + + return user + } + + /** + * Returns whether information about the given userID are known. + * + * @param {number} userID + * @returns {boolean} + */ + has(userID) { + return this.users.has(userID) + } + + /** + * Forces an update of the information about the given userID. + * + * @param {number} userID + */ + expire(userID) { + if (!this.users.has(userID)) return + + this.users.get(userID).date = 0 + } + + /** + * Returns whether the information about the given userID are recent. + * + * @param {number} userID + * @returns {boolean} + */ + isRecent(userID) { + const user = this.users.get(userID) + + if (user != null) { + return user.date > (Date.now() - (5 * 60e3)) + } + + return false + } + + /** + * Returns the stored information. + * + * @returns {User[]} + */ + values() { + return Array.from(this.users.values()).map(item => item.user) + } + + pushLastActivity(userID) { + if (!userID) return + + this.lastActivity.add(userID) + } + + * getLastActivity() { + yield * this.lastActivity + } + + _ajaxSetup() { + return { silent: true + , ignoreError: true + , data: { className: 'chat\\data\\user\\UserAction' } + } + } + } + ProfileStore.DEPENDENCIES = DEPENDENCIES + + return ProfileStore +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Room.js b/files_wcf/js/Bastelstu.be/Chat/Room.js new file mode 100644 index 0000000..3d8a12a --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Room.js @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'Bastelstu.be/PromiseWrap/Ajax' + , 'WoltLabSuite/Core/Core' + , './User' + ], function (Ajax, Core, User) { + "use strict"; + + const DEPENDENCIES = [ 'sessionID', 'roomID' ] + /** + * Represents a chat room. + */ + class Room { + constructor(sessionID, roomID) { + this.sessionID = sessionID + this.roomID = roomID + } + + /** + * Sends a request to join the room. + * + * @returns {Promise} + */ + async join() { + const payload = { className: 'chat\\data\\room\\RoomAction' + , actionName: 'join' + , parameters: { roomID: this.roomID + , sessionID: this.sessionID + } + } + + return Ajax.api(this, payload) + } + + /** + * Sends a request to leave the room. + * + * @param {boolean} unload Send a beacon if true'ish and a regular AJAX request otherwise. + */ + leave(unload = false) { + const payload = { className: 'chat\\data\\room\\RoomAction' + , actionName: 'leave' + , parameters: { roomID: this.roomID + , sessionID: this.sessionID + } + } + + if (unload && FormData && (navigator.sendBeacon || window.fetch)) { + // Ordinary AJAX requests are unreliable during unload: + // Use navigator.sendBeacon if available, otherwise hope + // for the best and clean up based on a time out. + + const url = WSC_API_URL + 'index.php?ajax-proxy/&t=' + SECURITY_TOKEN + + const formData = new FormData() + Core.serialize(payload) + .split('&') + .map((item) => item.split('=')) + .map((item) => item.map(decodeURIComponent)) + .forEach((item) => formData.append(item[0], item[1])) + + if (navigator.sendBeacon) { + navigator.sendBeacon(url, formData) + } + + if (window.fetch) { + fetch(url, { method: 'POST', keepalive: true, redirect: 'follow', body: formData }) + } + + return Promise.resolve() + } + else { + return Ajax.api(this, payload) + } + } + + /** + * Sends a request to retrieve the userIDs inhabiting this room. + * + * @returns {Promise} + */ + async getUsers() { + const payload = { className: 'chat\\data\\room\\RoomAction' + , actionName: 'getUsers' + , objectIDs: [ this.roomID ] + } + + const result = await Ajax.api(this, payload) + + return Object.values(result.returnValues).map(user => new User(user)) + } + + _ajaxSetup() { + return { silent: true + , ignoreError: true + } + } + } + Room.DEPENDENCIES = DEPENDENCIES + + return Room +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Template.js b/files_wcf/js/Bastelstu.be/Chat/Template.js new file mode 100644 index 0000000..4f1f7a1 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Template.js @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Template' ], function (_Template) { + "use strict"; + + /** + * Template extends WoltLab Suite's Templates by passing in a list of + * re-usable sub-templates. + */ + class Template extends _Template { + constructor(string, templates = { }) { + super(string) + + this.templates = templates + + const oldFetch = this.fetch + this.fetch = (function (variables) { + variables = Object.assign({ }, variables) + + const templates = Object.assign({ }, this.templates, variables.t || { }) + variables.t = templates + + return oldFetch(variables) + }).bind(this) + } + } + + return Template +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui.js b/files_wcf/js/Bastelstu.be/Chat/Ui.js new file mode 100644 index 0000000..4e7f45f --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui.js @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + class Ui { + constructor() { + } + } + + return Ui +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/AutoAway.js b/files_wcf/js/Bastelstu.be/Chat/Ui/AutoAway.js new file mode 100644 index 0000000..fff546e --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/AutoAway.js @@ -0,0 +1,90 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../console' + , '../CommandHandler' + , '../LocalStorage' + , '../Messenger' + , '../ProfileStore' + , 'WoltLabSuite/Core/Language' + , 'WoltLabSuite/Core/Timer/Repeating' + ], function (console, CommandHandler, LocalStorage, Messenger, ProfileStore, Language, RepeatingTimer) { + "use strict"; + + const DEPENDENCIES = [ 'config', 'CommandHandler', 'Messenger', 'ProfileStore', 'UiInput' ] + class AutoAway { + constructor(config, commandHandler, messenger, profileStore, input) { + if (!(commandHandler instanceof CommandHandler)) throw new TypeError('You must pass a CommandHandler to the AutoAway') + if (!(messenger instanceof Messenger)) throw new TypeError('You must pass a Messenger to the AutoAway') + if (!(profileStore instanceof ProfileStore)) throw new TypeError('You must pass a ProfileStore to the AutoAway') + + this.storage = new LocalStorage('AutoAway.') + this.awayCommand = commandHandler.getCommandByIdentifier('be.bastelstu.chat', 'away') + if (this.awayCommand == null) { + throw new Error('Unreachable') + } + this.config = config + this.messenger = messenger + this.input = input + this.profileStore = profileStore + } + + bootstrap() { + if (this.config.autoAwayTime === 0) { + return + } + if (!this.awayCommand.isAvailable) { + return + } + + this.timer = new RepeatingTimer(this.setAway.bind(this), this.config.autoAwayTime * 60e3) + this.input.on('input', this.inputListener = (event) => { + this.storage.set('channel', Date.now()) + this.reset() + }) + this.storage.observe('channel', this.reset.bind(this)) + } + + ingest(messages) { + if (messages.some(message => message.isOwnMessage())) this.reset() + } + + reset() { + console.debug('AutoAway.reset', `Resetting timer`) + + if (!this.timer) return + + this.timer.setDelta(this.config.autoAwayTime * 60e3) + } + + async setAway() { + console.debug('AutoAway.setAway', `Attempting to set as away`) + + if (this.storage.get('setAway') >= (Date.now() - 10e3)) { + console.debug('AutoAway.setAway', `setAway called within the last 10 seconds in another Tab`) + return + } + this.storage.set('setAway', Date.now()) + + if (this.profileStore.getSelf().away) { + console.debug('AutoAway.setAway', `User is already away`) + return + } + + this.messenger.push({ commandID: this.awayCommand.id, parameters: { reason: Language.get('chat.user.autoAway') } }) + } + } + AutoAway.DEPENDENCIES = DEPENDENCIES + + return AutoAway +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js new file mode 100644 index 0000000..3ec5161 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Environment' + , '../Ui' + ], function (Environment, Ui) { + "use strict"; + + const DEPENDENCIES = [ 'UiAutoAway' + , 'UiConnectionWarning' + , 'UiInput' + , 'UiInputAutocompleter' + , 'UiMessageStream' + , 'UiMessageActionDelete' + , 'UiMobile' + , 'UiNotification' + , 'UiReadMarker' + , 'UiSettings' + , 'UiTopic' + , 'UiUserActionDropdownHandler' + , 'UiUserList' + ] + class Chat extends Ui { + constructor(autoAway, connectionWarning, input, autocompleter, messageStream, messageActionDelete, mobile, notification, readMarker, settings, topic, userActionDropdownHandler, userList) { + super() + + this.actionDropdownHandler = userActionDropdownHandler + this.autoAway = autoAway + this.autocompleter = autocompleter + this.connectionWarning = connectionWarning + this.input = input + this.messageStream = messageStream + this.messageActionDelete = messageActionDelete + this.mobile = mobile + this.notification = notification + this.readMarker = readMarker + this.settings = settings + this.topic = topic + this.userList = userList + } + + bootstrap() { + this.actionDropdownHandler.bootstrap() + this.autoAway.bootstrap() + this.autocompleter.bootstrap() + this.connectionWarning.bootstrap() + this.input.bootstrap() + this.messageStream.bootstrap() + this.messageActionDelete.bootstrap() + this.mobile.bootstrap() + this.notification.bootstrap() + this.readMarker.bootstrap() + this.settings.bootstrap() + this.topic.bootstrap() + this.userList.bootstrap() + } + } + Chat.DEPENDENCIES = DEPENDENCIES + + return Chat +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/ConnectionWarning.js b/files_wcf/js/Bastelstu.be/Chat/Ui/ConnectionWarning.js new file mode 100644 index 0000000..142c59c --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/ConnectionWarning.js @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../console' + ], function (console) { + "use strict"; + + class ConnectionWarning { + constructor() { + this.warning = elById('chatConnectionWarning') + } + + bootstrap() { + + } + + show() { + elShow(this.warning) + if (this.timeout) return + + console.debug('ConnectionWarning.show', 'Setting timeout') + this.timeout = setTimeout(_ => { + console.debug('ConnectionWarning.show', 'Timeout has passed') + this.timeout = undefined + + if (this.autoHide) { + console.debug('ConnectionWarning.show', 'Hiding connection warning') + this.hide() + } + }, 10e3) + } + + hide(force = false) { + if (!this.timeout || force) { + elHide(this.warning) + window.clearTimeout(this.timeout) + } + else { + console.debug('ConnectionWarning.hide', 'Automatically hiding after timeout has passed') + this.autoHide = true + } + } + + toggle() { + elToggle(this.warning) + } + } + + return ConnectionWarning +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/ErrorDialog.js b/files_wcf/js/Bastelstu.be/Chat/Ui/ErrorDialog.js new file mode 100644 index 0000000..10965e2 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/ErrorDialog.js @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Language' + , 'WoltLabSuite/Core/Template' + , 'WoltLabSuite/Core/Ui/Dialog' + ], function (Language, Template, UiDialog) { + "use strict"; + + const html = [ '[type="x-text/template"]' + , '[data-application="be.bastelstu.chat"]' + , '[data-template-name="be-bastelstu-chat-errorDialog"]' + ].join('') + + const wrapper = new Template(elBySel(html).textContent) + + class ErrorDialog { + constructor(message) { + const options = { title: Language.get('wcf.global.error.title') + , closable: false + } + + UiDialog.openStatic('chatError', wrapper.fetch({ message }), options) + } + } + + return ErrorDialog +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Input.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Input.js new file mode 100644 index 0000000..dfe4f2c --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Input.js @@ -0,0 +1,139 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../console' + , '../Helper' + , 'WoltLabSuite/Core/Core' + , 'WoltLabSuite/Core/Event/Key' + , '../DataStructure/EventEmitter' + , '../DataStructure/Throttle' + ], function (console, Helper, Core, EventKey, EventEmitter, Throttle) { + "use strict"; + + class Input { + constructor() { + this.inputContainer = elById('chatInputContainer') + this.input = elBySel('textarea', this.inputContainer) + this.charCounter = elBySel('.charCounter', this.inputContainer) + this.errorElement = elBySel('.innerError', this.inputContainer) + } + + bootstrap() { + if (typeof window.elInnerError === 'function') { + elRemove(this.errorElement) + } + + this.input.addEventListener('keydown', this.handleInputKeyDown.bind(this)) + this.input.addEventListener('input', Throttle(this.handleInput.bind(this))) + + Helper.makeFlexible(this.input) + this.handleInput() + } + + handleInput(event) { + this.charCounter.textContent = `${this.input.value.length} / ${this.input.getAttribute('maxlength')}` + this.emit('input') + } + + handleInputKeyDown(event) { + if (EventKey.Enter(event) && !event.shiftKey) { + // prevent generation of a new line + event.preventDefault() + + if (this.getText().length === 0) return + + const parameters = { cancel: false, input: this } + this.emit('beforeSubmit', parameters) + if (!parameters.cancel) { + this.emit('submit') + } + } + else if (EventKey.Tab(event)) { + // prevent leaving the input + event.preventDefault() + + this.emit('autocomplete') + } + } + + getText(raw = false) { + if (raw) { + return this.input.value + } + + return this.input.value.trim() + } + + select(start, end = undefined) { + if (end === undefined) end = this.getText(true).length + + this.input.setSelectionRange(start, end) + } + + focus() { + this.input.focus() + } + + insertText(text, options) { + this.focus() + + options = Object.assign({ append: true + , prepend: false + }, options) + + if (!(options.append || options.prepend)) { + // replace + this.input.value = text + } + + if (options.append) { + this.input.value += text; + } + + if (options.prepend) { + this.input.value = text + this.input.value; + } + + // always position caret at the end + const length = this.input.value.length + this.input.setSelectionRange(length, length) + + Core.triggerEvent(this.input, 'input') + } + + inputError(message) { + if (typeof window.elInnerError === 'function') { + elInnerError(this.inputContainer.firstElementChild, message) + } + else { + this.inputContainer.classList.add('formError') + this.errorElement.textContent = message + elShow(this.errorElement) + } + } + + hideInputError() { + if (typeof window.elInnerError === 'function') { + elInnerError(this.inputContainer.firstElementChild, false) + } + else { + this.inputContainer.classList.remove('formError') + this.errorElement.textContent = '' + elHide(this.errorElement) + } + } + } + EventEmitter(Input.prototype) + + return Input +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Input/Autocompleter.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Input/Autocompleter.js new file mode 100644 index 0000000..436aae7 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Input/Autocompleter.js @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Dom/Util' + , 'WoltLabSuite/Core/Event/Key' + , 'WoltLabSuite/Core/Ui/Suggestion' + ], function (DomUtil, EventKey, Suggestion) { + "use strict"; + + const DEPENDENCIES = [ 'UiInput' ] + class Autocompleter extends Suggestion { + constructor(input) { + const elementId = DomUtil.identify(input.input) + const options = { callbackSelect: (() => null) } + + super(elementId, options) + + this.input = input + this._options.callbackSelect = this.callbackSelect.bind(this) + } + + bootstrap() { + this.input.on('beforeSubmit', (event) => { + if (event.target !== this.input) return + + if (this.isActive() || this.cancelNextSubmit) { + event.detail.cancel = true + } + this.cancelNextSubmit = false + }) + } + + _keyDown(event) { + const result = super._keyDown(event) + + if (!result && EventKey.Enter(event)) { + this.cancelNextSubmit = true + } + } + + _keyUp(event) { + const value = this.input.getText(true) + + if (this._value !== value) { + this._ajaxSuccess({ returnValues: [] }) + this._value = value + } + } + + callbackSelect(_, selected) { + this.input.insertText(selected.objectId, { append: false }) + } + + _ajaxSuccess(...args) { + this._value = this.input.getText(true) + return super._ajaxSuccess(...args) + } + } + Autocompleter.DEPENDENCIES = DEPENDENCIES + + return Autocompleter +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Log.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Log.js new file mode 100644 index 0000000..64e016d --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Log.js @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Ui' ], function (Ui) { + "use strict"; + + const DEPENDENCIES = [ 'UiMessageStream', 'UiMessageActionDelete' ] + class Log extends Ui { + constructor(messageStream, messageActionDelete) { + super() + + this.messageStream = messageStream + this.messageActionDelete = messageActionDelete + } + + bootstrap() { + this.messageStream.bootstrap() + this.messageStream.enableAutoscroll = false + this.messageActionDelete.bootstrap() + } + } + Log.DEPENDENCIES = DEPENDENCIES + + return Log +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/MessageActions/Delete.js b/files_wcf/js/Bastelstu.be/Chat/Ui/MessageActions/Delete.js new file mode 100644 index 0000000..b0f88e5 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/MessageActions/Delete.js @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'Bastelstu.be/Chat/Message', 'Bastelstu.be/PromiseWrap/Ajax', 'Bastelstu.be/PromiseWrap/Ui/Confirmation' ], function (Message, Ajax, Confirmation) { + "use strict"; + + const DEPENDENCIES = [ 'UiMessageStream', 'Message' ] + class Delete { + constructor(messageStream, message) { + this.messageStream = messageStream + this.Message = message + } + + bootstrap() { + this.messageStream.on('ingested', this.bindListener.bind(this)) + } + + bindListener({ detail }) { + detail.forEach(item => { + if (!item) return + + const { node, message } = item + const button = node.querySelector('.jsDeleteButton') + if (!button) return + + button.addEventListener('click', async event => { + event.preventDefault() + + await Confirmation.show({ + message: button.dataset.confirmMessageHtml, + messageIsHtml: true + }) + + await this.delete(message.messageID) + + elRemove(button.closest('li')) + }) + }) + } + + async delete(messageID) { + { + const payload = { objectIDs: [ messageID ] } + + await Ajax.api(this, payload) + } + + { + const objectType = 'be.bastelstu.chat.messageType.tombstone' + const payload = { messageID + , userID: null + } + const message = this.Message.instance({ objectType, payload }) + message.getMessageType().render(message) + } + } + + _ajaxSetup() { + return { silent: true + , ignoreError: true + , data: { className: 'chat\\data\\message\\MessageAction' + , actionName: 'trash' + } + } + } + } + Delete.DEPENDENCIES = DEPENDENCIES + + return Delete +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/MessageStream.js b/files_wcf/js/Bastelstu.be/Chat/Ui/MessageStream.js new file mode 100644 index 0000000..7f95ae5 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/MessageStream.js @@ -0,0 +1,343 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../Helper' + , 'WoltLabSuite/Core/Date/Util' + , 'WoltLabSuite/Core/Dom/Change/Listener' + , 'WoltLabSuite/Core/User' + , 'WoltLabSuite/Core/Dom/Traverse' + , '../DataStructure/EventEmitter' + , '../DataStructure/RedBlackTree/Tree' + ], function (Helper, DateUtil, DomChangeListener, User, DOMTraverse, EventEmitter, Tree) { + "use strict"; + + const enableAutoscroll = Symbol('enableAutoscroll') + + const DEPENDENCIES = [ ] + class MessageStream { + constructor() { + this.stream = elById('chatMessageStream') + this.scrollContainer = elBySel('.scrollContainer', this.stream) + + this[enableAutoscroll] = true + this.lastScrollPosition = undefined + this.nodeMap = new WeakMap() + this.positions = new Tree() + } + + get enableAutoscroll() { + return this[enableAutoscroll] + } + + set enableAutoscroll(value) { + this[enableAutoscroll] = value + + if (this[enableAutoscroll]) { + this.scrollToBottom() + } + } + + bootstrap() { + this.scrollContainer.addEventListener('copy', this.onCopy.bind(this)) + this.scrollContainer.addEventListener('scroll', Helper.throttle(this.onScroll, 100, this), { passive: true }) + } + + getDateMarker(date) { + const dateMarker = elCreate('li') + dateMarker.classList.add('dateMarker') + const time = elCreate('time') + time.innerText = DateUtil.formatDate(date) + time.setAttribute('datetime', DateUtil.format(date, 'Y-m-d')) + dateMarker.appendChild(time) + + return dateMarker + } + + onDifferentDays(a, b) { + return DateUtil.format(a, 'Y-m-d') !== DateUtil.format(b, 'Y-m-d') + } + + ingest(messages) { + let scrollTopBefore = this.enableAutoscroll ? 0 : this.scrollContainer.scrollTop + let prependedHeight = 0 + + const ul = elBySel('ul', this.scrollContainer) + const first = ul.firstElementChild + + const ingested = messages.map((function (item) { + let currentScrollHeight = 0 + + const li = elCreate('li') + + // Allow messages types to not render a messages + // This can be used for status messages like ChatUpdate + let fragment + if ((fragment = item.getMessageType().render(item)) === false) return + + if (fragment.querySelector(`.userMention[data-user-id="${User.userId}"]`)) li.classList.add('mentioned') + + li.appendChild(fragment) + + li.classList.add('chatMessageBoundary') + li.setAttribute('id', `message-${item.messageID}`) + li.dataset.objectType = item.objectType + li.dataset.userId = item.userID + if (item.isOwnMessage()) li.classList.add('own') + if (item.isDeleted) li.classList.add('tombstone') + + const position = this.positions.insert(item.messageID) + if (position[1] !== undefined) { + const sibling = elById(`message-${position[1]}`) + if (!sibling) throw new Error('Unreachable') + + let nodeBefore, nodeAfter + let dateMarkerBetween = false + if (position[0] === 'LEFT') { + nodeAfter = sibling + nodeBefore = sibling.previousElementSibling + + if (nodeBefore && nodeBefore.classList.contains('dateMarker')) { + elRemove(nodeBefore) + nodeBefore = sibling.previousElementSibling + } + } + else if (position[0] === 'RIGHT') { + nodeBefore = sibling + nodeAfter = sibling.nextElementSibling + + if (nodeAfter && nodeAfter.classList.contains('dateMarker')) { + elRemove(nodeAfter) + nodeAfter = sibling.nextElementSibling + } + } + else { + throw new Error('Unreachable') + } + + const messageBefore = this.nodeMap.get(nodeBefore) + if (nodeBefore && !messageBefore) throw new Error('Unreachable') + const messageAfter = this.nodeMap.get(nodeAfter) + if (nodeAfter && !messageAfter) throw new Error('Unreachable') + + if (!this.enableAutoscroll && nodeAfter) currentScrollHeight = this.scrollContainer.scrollHeight + + let context = nodeAfter + if (nodeAfter) nodeAfter.classList.remove('first') + if (messageBefore) { + if (this.onDifferentDays(messageBefore.date, item.date)) { + const dateMarker = this.getDateMarker(item.date) + ul.insertBefore(dateMarker, nodeAfter) + li.classList.add('first') + } + else { + if (messageBefore.objectType !== item.objectType || !item.getMessageType().joinable(messageBefore, item)) { + li.classList.add('first') + } + } + } + else { + li.classList.add('first') + } + if (messageAfter) { + if (this.onDifferentDays(messageAfter.date, item.date)) { + const dateMarker = this.getDateMarker(messageAfter.date) + ul.insertBefore(dateMarker, nodeAfter) + context = dateMarker + nodeAfter.classList.add('first') + } + else { + if (messageAfter.objectType !== item.objectType || !item.getMessageType().joinable(item, messageAfter)) { + nodeAfter.classList.add('first') + } + } + } + + ul.insertBefore(li, context); + + if (!this.enableAutoscroll && nodeAfter) { + prependedHeight += this.scrollContainer.scrollHeight - currentScrollHeight + } + } + else { + li.classList.add('first') + ul.insertBefore(li, null) + } + + this.nodeMap.set(li, item) + + return { node: li + , message: item + } + }).bind(this)); + + if (ingested.some(item => item != null)) { + if (this.enableAutoscroll) { + this.scrollToBottom() + } + else { + this.stream.classList.add('activity') + this.scrollContainer.scrollTop = scrollTopBefore + prependedHeight + } + } + + DomChangeListener.trigger() + + this.emit('ingested', ingested) + } + + scrollToBottom() { + this.scrollContainer.scrollTop = this.scrollContainer.scrollHeight + this.stream.classList.remove('activity') + } + + onScroll() { + const { scrollTop, scrollHeight, clientHeight } = this.scrollContainer + const distanceFromTop = scrollTop + const distanceFromBottom = scrollHeight - scrollTop - clientHeight + + let direction = 'down' + + if (this.lastScrollPosition != null && scrollTop < this.lastScrollPosition) { + direction = 'up' + } + + if (direction === 'up') { + if (distanceFromBottom > 7) { + this.emit('scrollUp') + } + + if (distanceFromTop <= 7) { + this.emit('reachedTop') + } + else if (distanceFromTop <= 300) { + this.emit('nearTop') + } + } + else if (direction === 'down') { + if (distanceFromTop > 7) { + this.emit('scrollDown') + } + + if (distanceFromBottom <= 7) { + this.scrollToBottom() + this.emit('reachedBottom') + } + else if (distanceFromBottom <= 300) { + this.emit('nearBottom') + } + } + + this.lastScrollPosition = scrollTop + } + + onCopy(event) { + const selection = window.getSelection() + + // Similar to selecting nothing + if (selection.isCollapsed) return + + // Get the first and last node in the selection + let originalStart, start, end, originalEnd + start = originalStart = selection.getRangeAt(0).startContainer + end = originalEnd = selection.getRangeAt(selection.rangeCount - 1).endContainer + + const startOffset = selection.getRangeAt(0).startOffset + const endOffset = selection.getRangeAt(selection.rangeCount - 1).endOffset + + // The Traverse module needs nodes of the Element type, the selected elements could be of type Text + while (!(start instanceof Element) && start.parentNode) start = start.parentNode + while (!(end instanceof Element) && end.parentNode) end = end.parentNode + + if (!start || !end) throw new Error('Unexpected error, no element nodes in selection') + + // Try to find the starting li element in the selection + if (!start.id || start.id.indexOf('message-') !== 0) { + start = DOMTraverse.parentBySel(start, "li[id^='message']", this.stream) + } + + // Try to find the ending li element in the selection + if (!end.id || end.id.indexOf('message-') !== 0) { + end = DOMTraverse.parentBySel(end, "li[id^='message']", this.stream) + } + + // Do not select a message if we selected only a new line + if (originalStart instanceof Text && originalStart.textContent.substring(startOffset) === "") { + start = DOMTraverse.next(start) + } + + // The selection went outside of the stream container, end at the last li element + if (end === null) { + end = elBySel('ul > li:last-child', this.stream) + } + + // Discard the selection, we selected only whitespace between two messages + if (start === end && endOffset === 0) return + + // Do not include the ending message if there is no visible selection + if (start !== end && endOffset === 0) { + end = DOMTraverse.prev(end) + } + + const elements = [ ] + let next = start + + do { + elements.push(next) + + if (next === end) break + } + while (next = DOMTraverse.next(next)) + + // Only apply our custom formatting when selecting multiple or whole messages + if (elements.length === 1) { + const range = document.createRange() + range.setStart(originalStart, startOffset) + range.setEnd(originalEnd, endOffset) + + if (!Helper.rangeSpansTextContent(range, start.querySelector('.chatMessage'))) return + } + + try { + event.clipboardData.setData('text/plain', elements.map((el, index, arr) => { + const message = this.nodeMap.get(el) + + if (el.classList.contains('dateMarker')) return `== ${el.textContent.trim()} ==` + + if (!message) return + + const elem = elBySel('.chatMessage', el) + + let body + if (typeof (body = message.getMessageType().renderPlainText(message)) === 'undefined' || body === false) { + body = Helper.getTextContent(elem).replace(/\t+/g, '\t') // collapse multiple tabs + .replace(/ +/g, ' ') // collapse multiple spaces + .replace(/([\t ]*\n){2,}/g, '\n') // collapse line consisting of tabs, spaces and newlines + .replace(/^[\t ]+|[\t ]+$/gm, '') // remove leading and trailing whitespace per line + } + + return `[${message.formattedTime}] <${message.username}> ${body.trim()}` + }).filter(x => x).join('\n')) + + event.preventDefault() + } + catch (e) { + console.error('Unable to use the clipboard API') + console.error(e) + } + } + } + EventEmitter(MessageStream.prototype) + MessageStream.DEPENDENCIES = DEPENDENCIES + + return MessageStream +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Mobile.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Mobile.js new file mode 100644 index 0000000..c89b546 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Mobile.js @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Ui/Screen' ], function (UiScreen) { + "use strict"; + + const initialized = Symbol('initialized') + + class Mobile { + constructor() { + this[initialized] = false + } + + bootstrap() { + UiScreen.on('screen-md-down', { match: this.enable.bind(this) + , unmatch: this.disable.bind(this) + , setup: this.init.bind(this) + }) + } + + init() { + if (this[initialized]) return + + this[initialized] = true + + this.initQuickSettings() + } + + enable() { + + } + + disable() { + + } + + initQuickSettings() { + const navigation = elBySel('#chatQuickSettingsNavigation > ul') + const quickSettings = elById('chatQuickSettings') + + navigation.addEventListener(WCF_CLICK_EVENT, event => { + event.stopPropagation() + + // mimic dropdown behavior + window.setTimeout(() => { + navigation.classList.remove('open') + }, 10) + }) + + quickSettings.addEventListener(WCF_CLICK_EVENT, event => { + event.preventDefault() + event.stopPropagation() + + navigation.classList.toggle('open') + }) + } + } + + return Mobile +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Notification.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Notification.js new file mode 100644 index 0000000..c420c95 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Notification.js @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Language' ], function (Language) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore' ] + class Notification { + constructor(profileStore) { + this.profileStore = profileStore + + this.unread = 0 + this.active = true + this.browserTitle = document.title + this.systemEnabled = false + this.lastSeen = 0 + } + + bootstrap() { + document.addEventListener('visibilitychange', this.onVisibilitychange.bind(this)) + } + + get systemSupported() { + return "Notification" in window + } + + get systemDenied() { + return window.Notification.permission === 'denied' + } + + get systemGranted() { + if (this.systemDenied) { + console.warn('[Notification]', 'System Notifications: permission denied') + } + + return window.Notification.permission === 'granted' + } + + onVisibilitychange() { + this.active = !document.hidden + + if (this.active) { + this.unread = 0 + this.updateBrowserTitle() + } + } + + ingest(messages) { + if (!this.active) { + messages.forEach(message => { + const body = message.getMessageType().renderPlainText(message) + + if (body === false) return + if (message.messageID < this.lastSeen) return + + this.lastSeen = message.messageID + this.unread++ + + if (this.systemEnabled && this.systemGranted) { + // The user information is guaranteed to be cached at this point + const user = this.profileStore.get(message.userID) + const title = Language.get('chat.notification.title', { message }) + const options = { body + , icon: user.imageUrl + , badge: user.imageUrl + } + + const notification = new window.Notification(title, options) + + setTimeout(notification.close.bind(notification), 5e3) + } + }) + } + + this.updateBrowserTitle() + } + + updateBrowserTitle() { + if (this.unread > 0) { + document.title = `(${this.unread}) ${this.browserTitle}` + } + else { + document.title = this.browserTitle + } + } + + enableSystemNotifications() { + if (!this.systemSupported) return Promise.reject(new Error('Notifications are not supported')) + + if (this.systemGranted) { + this.systemEnabled = true + + return Promise.resolve() + } + + return new Promise((resolve, reject) => { + window.Notification.requestPermission(permission => { + this.systemEnabled = permission === 'granted' + + if (this.systemEnabled) { + resolve() + } + else { + reject(new Error(permission)) + } + }) + }) + } + + disableSystemNotifications() { + this.systemEnabled = false + } + } + Notification.DEPENDENCIES = DEPENDENCIES + + return Notification +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/ReadMarker.js b/files_wcf/js/Bastelstu.be/Chat/Ui/ReadMarker.js new file mode 100644 index 0000000..32da37f --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/ReadMarker.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + const DEPENDENCIES = [ 'UiMessageStream' ] + class ReadMarker { + constructor(messageStream) { + this.messageStream = messageStream + } + + bootstrap() { + document.addEventListener('visibilitychange', this.onVisibilitychange.bind(this)) + } + + onVisibilitychange() { + if (document.hidden) { + const ul = elBySel('ul', this.messageStream.stream) + let lc = ul.lastElementChild + + // delete previous markers + Array.prototype.forEach.call(document.querySelectorAll('.readMarker'), marker => { + marker.classList.remove('readMarker') + }) + + if (lc) { + lc.classList.add('readMarker') + } + } + } + } + ReadMarker.DEPENDENCIES = DEPENDENCIES + + return ReadMarker +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Settings.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings.js new file mode 100644 index 0000000..27bfcf6 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings.js @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + 'use strict'; + + const DEPENDENCIES = [ 'UiSettingsButton' ] + class Settings { + constructor(modules) { + this.modules = modules + this.buttons = Array.from(elBySelAll('#chatQuickSettingsNavigation .button[data-module]')) + } + + bootstrap() { + this.buttons.forEach(element => { + this.modules[element.dataset.module.replace(/\./g, '-')].instance(element).bootstrap() + }) + } + } + Settings.DEPENDENCIES = DEPENDENCIES + + return Settings +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/AutoscrollButton.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/AutoscrollButton.js new file mode 100644 index 0000000..cc2bb50 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/AutoscrollButton.js @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './ToggleButton' ], function (ToggleButton) { + 'use strict'; + + const DEPENDENCIES = [ 'UiMessageStream' ].concat(ToggleButton.DEPENDENCIES || [ ]) + class AutoscrollButton extends ToggleButton { + constructor(element, messageStream, ...superDeps) { + super(element, true, undefined, ...superDeps) + + this.messageStream = messageStream + + this.messageStream.on('reachedBottom', this.enable.bind(this)) + this.messageStream.on('scrollUp', this.disable.bind(this)) + } + + enable() { + super.enable() + + this.messageStream.enableAutoscroll = true + } + + disable() { + super.disable() + + this.messageStream.enableAutoscroll = false + } + } + AutoscrollButton.DEPENDENCIES = DEPENDENCIES + + return AutoscrollButton +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/Button.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/Button.js new file mode 100644 index 0000000..d6ff216 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/Button.js @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + 'use strict'; + + const DEPENDENCIES = [ ] + class Button { + constructor(element) { + if (!element || !element instanceof Element) throw new Error('No DOM element provided') + + this.element = element + } + + bootstrap() { + this.element.addEventListener('click', this.onClick.bind(this)) + } + + onClick(event) { + event.preventDefault() + } + } + Button.DEPENDENCIES = DEPENDENCIES + + return Button +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/FullscreenButton.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/FullscreenButton.js new file mode 100644 index 0000000..fdb6045 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/FullscreenButton.js @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './ToggleButton' ], function (ToggleButton) { + 'use strict'; + + class FullscreenButton extends ToggleButton { + constructor(element, ...superDeps) { + super(element, false, 'Bastelstu.be/Chat/Ui/Settings/FullscreenButton', ...superDeps) + } + + enable() { + super.enable() + document.querySelector('html').classList.add('fullscreen') + } + + disable() { + super.disable() + document.querySelector('html').classList.remove('fullscreen') + } + } + + return FullscreenButton +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/NotificationsButton.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/NotificationsButton.js new file mode 100644 index 0000000..cf3c3cf --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/NotificationsButton.js @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './ToggleButton' ], function (ToggleButton) { + 'use strict'; + + const DEPENDENCIES = [ 'UiNotification' ].concat(ToggleButton.DEPENDENCIES || [ ]) + class NotificationsButton extends ToggleButton { + constructor(element, notification, ...superDeps) { + super(element, false, 'Bastelstu.be/Chat/Ui/Settings/NotificationsButton', ...superDeps) + + this.notification = notification + } + + bootstrap() { + super.bootstrap() + + // Hide the button if notifications are not supported or the permission has been denied + if (!this.notification.systemSupported || this.notification.systemDenied) { + elRemove(this.element.closest('li')) + } + } + + enable() { + super.enable() + this.notification.enableSystemNotifications().catch(error => { + this.disable() + + if (this.notification.systemDenied) elRemove(this.element) + }) + } + + disable() { + super.disable() + this.notification.disableSystemNotifications() + } + } + NotificationsButton.DEPENDENCIES = DEPENDENCIES + + return NotificationsButton +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/SmiliesButton.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/SmiliesButton.js new file mode 100644 index 0000000..cce9d60 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/SmiliesButton.js @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './ToggleButton' + , 'WoltLabSuite/Core/Ui/Screen' + ], function (ToggleButton, UiScreen) { + 'use strict'; + + const DEPENDENCIES = [ 'UiInput' ].concat(ToggleButton.DEPENDENCIES || [ ]) + class SmiliesButton extends ToggleButton { + constructor(element, input, ...superDeps) { + super(element, false, undefined, ...superDeps) + + this.input = input + } + + bootstrap() { + this.container = elById('smileyPickerContainer') + + // Remove this button if smileys are disabled + if (!this.container) { + elRemove(this.element.closest('li')) + } + + this.closeButton = elById('smileyPickerCloseButton') + + // Initialize the smiley picker tab menu + $('.messageTabMenu').messageTabMenu() + + $('#smilies-text').on('mousedown', '.jsSmiley', this.insertSmiley.bind(this)) + this.closeButton.addEventListener('mousedown', this.disable.bind(this)) + + // Start in desktop mode + this.mobile = false + + // Do not persist the state + super.bootstrap() + + // Setup media queries + UiScreen.on('screen-md-down', { + match: this.enableMobile.bind(this), + unmatch: this.disableMobile.bind(this), + setup: this.setupMobile.bind(this) + }) + } + + /** + * Initializes and enables the mobile smiley picker UI components. + * + * A second button mirroring this button’s click handler is + * inserted next to the message input while this button will + * be hidden. + */ + setupMobile() { + this.shadowToggleButton = document.createElement('span') + this.shadowToggleButton.classList.add('smiliesToggleMobileButton') + this.shadowToggleButton.innerHTML = '<span class="icon icon24 fa-smile-o"></span>' + this.shadowToggleButton.addEventListener('mousedown', this.onClick.bind(this)) + + const shadowContainer = elBySel('#chatInputContainer > div') + shadowContainer.insertBefore(this.shadowToggleButton, shadowContainer.firstChild) + + this.enableMobile() + } + + /** + * Enables the mobile smiley picker components. + * + * Hides this button and shows it’s mirror next to the message input. + */ + enableMobile() { + this.mobile = true + + elHide(this.element) + elShow(this.shadowToggleButton) + + // Do not show the overlay when the viewport changes + // and becomes smaller + this.disable() + } + + /** + * Disables the mobile smiley picker components. + * + * Shows this button and hides it’s mirror next to the message input. + * Also re-enables scrolling of the main body. + */ + disableMobile() { + this.mobile = false + + elShow(this.element) + elHide(this.shadowToggleButton) + + UiScreen.scrollEnable() + } + + /** + * Event handler to handle the insertion of smilies into the message input. + * This handler closes the fulls creen overlay of the mobile view after insertion. + * + * @param {Event} event The event bound in the init() function + */ + insertSmiley(event) { + event.preventDefault() + event.stopPropagation() + + const smileyCode = event.currentTarget.children[0].getAttribute('alt') + + this.input.insertText(` ${smileyCode} `) + + if (this.mobile) { + this.disable() + } + } + + /** + * Enables the smiley picker. + * If the mobile view is active, scrolling of the main body will be disabled. + */ + enable() { + super.enable() + + elShow(this.container) + elData(this.container, 'show', 'true') + + if (this.mobile) { + UiScreen.scrollDisable() + } + } + + /** + * Disables the smiley picker. + * If the mobile view is active, scrolling of the main body will be re-enabled. + */ + disable() { + super.disable() + + elHide(this.container) + elData(this.container, 'show', 'false') + + if (this.mobile) { + UiScreen.scrollEnable() + } + } + } + SmiliesButton.DEPENDENCIES = DEPENDENCIES + + return SmiliesButton +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/ToggleButton.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/ToggleButton.js new file mode 100644 index 0000000..05c5139 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Settings/ToggleButton.js @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Button' + , '../../LocalStorage' + , '../../DataStructure/EventEmitter' + ], function (Button, LocalStorage, EventEmitter) { + 'use strict'; + + const DEPENDENCIES = [ ].concat(Button.DEPENDENCIES || [ ]) + class ToggleButton extends Button { + constructor(element, defaultState, storageKey, ...superDeps) { + super(element, ...superDeps) + + this.initialized = false + this.storage = new LocalStorage('Settings.') + + this.storageKey = storageKey + if (this.storage.has(this.storageKey)) { + defaultState = this.storage.get(this.storageKey) + } + + this.defaultState = defaultState + } + + bootstrap() { + super.bootstrap() + + if (this.defaultState) { + this.enable() + } + else { + this.disable() + } + } + + get enabled() { + return this.element.classList.contains('active') + } + + enable() { + this.element.classList.add('active') + + if (this.storageKey != null) { + this.storage.set(this.storageKey, true) + } + } + + disable() { + this.element.classList.remove('active') + + if (this.storageKey != null) { + this.storage.set(this.storageKey, false) + } + } + + onClick(event) { + super.onClick(event) + + if (this.enabled) { + this.disable() + } + else { + this.enable() + } + } + } + EventEmitter(ToggleButton.prototype) + ToggleButton.DEPENDENCIES = DEPENDENCIES + + return ToggleButton +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/Topic.js b/files_wcf/js/Bastelstu.be/Chat/Ui/Topic.js new file mode 100644 index 0000000..ec98bde --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/Topic.js @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Dom/Traverse' ], function (Traverse) { + "use strict"; + + class Topic { + bootstrap() { + elBySelAll('.chatRoomTopic', document, function (element) { + elBySel('.jsDismissRoomTopicButton', element).addEventListener('click', function (event) { + elRemove(element) + }) + }) + } + } + + return Topic +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/UserActionDropdownHandler.js b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActionDropdownHandler.js new file mode 100644 index 0000000..2f10760 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActionDropdownHandler.js @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Dom/Traverse' + , 'WoltLabSuite/Core/Dom/Util' + , 'WoltLabSuite/Core/Ui/Dropdown/Simple' + ], function (DomTraverse, DomUtil, SimpleDropdown) { + "use strict"; + + const DEPENDENCIES = [ 'ProfileStore', 'Template.UserListDropdownMenuItems', 'bottle' ] + class UserActionDropdownHandler { + constructor(profiles, dropdownTemplate, bottle) { + this.profiles = profiles + this.dropdownTemplate = dropdownTemplate + this.bottle = bottle + + this.container = elById('main') + } + + bootstrap() { + this.container.addEventListener('click', this.onClick.bind(this)) + } + + onClick(event) { + const userElement = event.target.classList.contains('jsUserActionDropdown') ? event.target : DomTraverse.parentByClass(event.target, 'jsUserActionDropdown', this.container) + + if (!userElement) return + + event.preventDefault() + event.stopPropagation() + + const user = this.profiles.get(parseInt(userElement.dataset.userId, 10)) + if (user == null) { + throw new Error('Unreachable') + } + + // Note: We would usually use firstElementChild here, but this + // is not supported in Safari and Edge + const dropdown = DomUtil.createFragmentFromHtml(this.dropdownTemplate.fetch({ user })).querySelector('*') + + Array.from(elBySelAll('[data-module]', dropdown)).forEach(element => { + const moduleName = element.dataset.module + let userAction + if (!this.bottle.container.UserAction || (userAction = this.bottle.container.UserAction[`${moduleName.replace(/\./g, '-')}`]) == null) { + this.bottle.factory(`UserAction.${moduleName.replace(/\./g, '-')}`, _ => { + const UserAction = require(moduleName) + const deps = this.bottle.digest(UserAction.DEPENDENCIES || []) + + return new UserAction(...deps) + }) + + userAction = this.bottle.container.UserAction[`${moduleName.replace(/\./g, '-')}`] + } + + element.addEventListener(WCF_CLICK_EVENT, (event) => userAction.onClick(user, event)) + }) + + SimpleDropdown.initFragment(userElement, dropdown) + SimpleDropdown.registerCallback(userElement.id, (container, action) => { + if (action === 'close') { + SimpleDropdown.destroy(container) + } + }) + SimpleDropdown.toggleDropdown(userElement.id) + } + } + UserActionDropdownHandler.DEPENDENCIES = DEPENDENCIES + + return UserActionDropdownHandler +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/Action.js b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/Action.js new file mode 100644 index 0000000..402ca7e --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/Action.js @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + class Action { + constructor() { } + + onClick(userID, event) { } + } + + return Action +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/BanAction.js b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/BanAction.js new file mode 100644 index 0000000..e3a8114 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/BanAction.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../../console' + , './Action' + ], function (console, Action) { + "use strict"; + + const DEPENDENCIES = [ 'UiInput' ] + class BanAction extends Action { + constructor(input) { + super() + + this.input = input + } + + onClick(user, event) { + if (!event.target.dataset.trigger) { + console.warn('[WhisperAction]', `Missing trigger`) + return + } + + const sanitizedUsername = user.username.replace(/"/g, '""') + const command = `/${event.target.dataset.trigger} "${sanitizedUsername}" ` + + this.input.insertText(command, { append: false, prepend: true }) + this.input.focus() + setTimeout(_ => { + this.input.emit('autocomplete') + }, 1) + } + } + BanAction.DEPENDENCIES = DEPENDENCIES + + return BanAction +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/MuteAction.js b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/MuteAction.js new file mode 100644 index 0000000..f22ed78 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/MuteAction.js @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ '../../console' + , './Action' + ], function (console, Action) { + "use strict"; + + const DEPENDENCIES = [ 'UiInput' ] + class MuteAction extends Action { + constructor(input) { + super() + + this.input = input + } + + onClick(user, event) { + if (!event.target.dataset.trigger) { + console.warn('[WhisperAction]', `Missing trigger`) + return + } + + const sanitizedUsername = user.username.replace(/"/g, '""') + const command = `/${event.target.dataset.trigger} "${sanitizedUsername}" ` + + this.input.insertText(command, { append: false, prepend: true }) + this.input.focus() + setTimeout(_ => { + this.input.emit('autocomplete') + }, 1) + } + } + MuteAction.DEPENDENCIES = DEPENDENCIES + + return MuteAction +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/WhisperAction.js b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/WhisperAction.js new file mode 100644 index 0000000..3de94d9 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/WhisperAction.js @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ './Action', '../../console' ], function (Action, console) { + "use strict"; + + const DEPENDENCIES = [ 'UiInput' ] + class WhisperAction extends Action { + constructor(input) { + super() + + this.input = input + } + + onClick(user, event) { + if (!event.target.dataset.trigger) { + console.warn('[WhisperAction]', `Missing trigger`) + return + } + + const sanitizedUsername = user.username.replace(/"/g, '""') + const command = `/${event.target.dataset.trigger} "${sanitizedUsername}" ` + + this.input.insertText(command, { append: false, prepend: true }) + this.input.focus() + } + } + WhisperAction.DEPENDENCIES = DEPENDENCIES + + return WhisperAction +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/Ui/UserList.js b/files_wcf/js/Bastelstu.be/Chat/Ui/UserList.js new file mode 100644 index 0000000..f0157e4 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/Ui/UserList.js @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/Dom/Util' ], function (DomUtil) { + "use strict"; + + const DEPENDENCIES = [ 'Template.UserList' ] + class UserList { + constructor(userListTemplate) { + this.userListTemplate = userListTemplate + this.chatUserList = elById('chatUserList') + } + + bootstrap() { + + } + + render(users) { + users.sort((a, b) => a.username.localeCompare(b.username)) + const html = this.userListTemplate.fetch({ users }) + const fragment = DomUtil.createFragmentFromHtml(html) + + // Replace the current user list with the new one + const currentList = elBySel('#chatUserList > .boxContent > ul') + const parentNode = currentList.parentNode + parentNode.removeChild(currentList) + parentNode.appendChild(fragment) + } + } + UserList.DEPENDENCIES = DEPENDENCIES + + return UserList +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/User.js b/files_wcf/js/Bastelstu.be/Chat/User.js new file mode 100644 index 0000000..353f678 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/User.js @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ 'WoltLabSuite/Core/User' + , 'WoltLabSuite/Core/StringUtil' + , './Helper' + ], function (CoreUser, StringUtil, Helper) { + "use strict"; + + const u = Symbol('user') + + /** + * Represents a user. + */ + class User { + constructor(user) { + this[u] = Helper.deepFreeze(user) + + Object.getOwnPropertyNames(this[u]).forEach(key => { + if (this[key]) { + throw new Error('Attempting to override existing property') + } + + Object.defineProperty(this, key, { value: this[u][key] + , enumerable: true + }) + }) + } + + get coloredUsername() { + // No color + if (this.color1 === null && this.color2 === null) return this.username + + // Single color + if (this.color1 === this.color2) return `<span style="color: ${Helper.intToRGBHex(this.color1)};">${StringUtil.escapeHTML(this.username)}</span>` + + // Gradient + const r1 = (this.color1 >> 16) & 0xFF + const r2 = (this.color2 >> 16) & 0xFF + const g1 = (this.color1 >> 8) & 0xFF + const g2 = (this.color2 >> 8) & 0xFF + const b1 = this.color1 & 0xFF + const b2 = this.color2 & 0xFF + + const steps = this.username.length - 1 + const r = (r1 - r2) / steps + const g = (g1 - g2) / steps + const b = (b1 - b2) / steps + + return this[u].username.split('').map((letter, index) => { + const R = Math.round(r1 - index * r) + const G = Math.round(g1 - index * g) + const B = Math.round(b1 - index * b) + + return `<span style="color: rgb(${R}, ${G}, ${B})">${StringUtil.escapeHTML(letter)}</span>` + }).join('') + } + + get self() { + return this.userID === CoreUser.userId + } + + static getGuest(username) { + const payload = { username + , userID: null + , color1: null + , color2: null + } + + return new User(payload) + } + + wrap() { + return { user: this[u] } + } + + toJSON() { + return this[u] + } + } + + return User +}); diff --git a/files_wcf/js/Bastelstu.be/Chat/console.js b/files_wcf/js/Bastelstu.be/Chat/console.js new file mode 100644 index 0000000..035b153 --- /dev/null +++ b/files_wcf/js/Bastelstu.be/Chat/console.js @@ -0,0 +1,119 @@ +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +define([ ], function () { + "use strict"; + + const start = Date.now() + let last = start + + const group = function () { + if (window.console.group) window.console.group() + } + + const groupCollapsed = function () { + if (window.console.groupCollapsed) window.console.groupCollapsed() + } + + const groupEnd = function () { + if (window.console.groupEnd) window.console.groupEnd() + } + + const println = function (type, ...args) { + window.console[type](...args) + } + + const log = function (...args) { + println('log', ...args) + } + + const warn = function (...args) { + println('warn', ...args) + } + + const error = function (...args) { + println('error', ...args) + } + + const debug = function (handler, ...args) { + const now = Date.now() + const time = [ (now - start), `\t+${(now - last)}ms\t` ] + + if (args.length) { + println('debug', ...time, `[${handler}]\t`, ...args) + } + else { + println('debug', ...time, handler) + } + + last = now + } + + const debugException = function (error) { + if (error instanceof Error) { + let message = `[${error.name}] „${error.message}“ in ${error.fileName} on line ${error.lineNumber}\n` + + if (error.stack) { + message += 'Stacktrace:\n' + message += error.stack + } + + println('error', message) + } + else if (error.code && error.message) { + debugAjaxException(error) + } + } + + const debugAjaxException = function (error) { + groupCollapsed() + let details = `[${error.code}] ${error.message}` + + const br2nl = (string) => string.split('\n') + .map(line => line.replace(/<br\s*\/?>$/i, '')) + .join('\n') + + if (error.stacktrace) { + details += `\nStacktrace:\n${br2nl(error.stacktrace)}` + } + else if (error.exceptionID) { + details += `\nException ID: ${error.exceptionID}` + } + + println('debug', details) + + error.previous.forEach(previous => { + let details = '' + + group() + + details += `${previous.message}\n` + details += `Stacktrace:\n${br2nl(previous.stacktrace)}` + + println('debug', details) + }) + + error.previous.forEach(_ => groupEnd()) + groupEnd() + } + + return { log + , warn + , error + , debug + , debugException + , group + , groupCollapsed + , groupEnd + } +}); diff --git a/files_wcf/lib/system/package/plugin/ChatCommandPackageInstallationPlugin.class.php b/files_wcf/lib/system/package/plugin/ChatCommandPackageInstallationPlugin.class.php new file mode 100644 index 0000000..0766527 --- /dev/null +++ b/files_wcf/lib/system/package/plugin/ChatCommandPackageInstallationPlugin.class.php @@ -0,0 +1,172 @@ +<?php +/* + * Copyright (c) 2010-2018 Tim Düsterhus. + * + * Use of this software is governed by the Business Source License + * included in the LICENSE file. + * + * Change Date: 2022-08-16 + * + * On the date above, in accordance with the Business Source + * License, use of this software will be governed by version 2 + * or later of the General Public License. + */ + +namespace wcf\system\package\plugin; + +use \wcf\system\exception\SystemException; +use \wcf\system\WCF; + +/** + * Installs, updates and deletes chat commands. + */ +class ChatCommandPackageInstallationPlugin extends AbstractXMLPackageInstallationPlugin implements \wcf\system\devtools\pip\IIdempotentPackageInstallationPlugin { + /** + * @inheritDoc + */ + public $className = \chat\data\command\CommandEditor::class; + + /** + * @inheritDoc + */ + public $application = 'chat'; + + /** + * Removing this and relying on table name guessing breaks uninstallation + * as the application autoloader is unavailable there. + * + * @inheritDoc + */ + public $tableName = 'command'; + + /** + * @inheritDoc + */ + protected function handleDelete(array $items) { + $sql = "DELETE FROM ".$this->application.WCF_N."_".$this->tableName." + WHERE packageID = ? + AND identifier = ?"; + $statement = WCF::getDB()->prepareStatement($sql); + + WCF::getDB()->beginTransaction(); + foreach ($items as $item) { + $statement->execute([ + $this->installation->getPackageID(), + $item['attributes']['name'] + ]); + } + WCF::getDB()->commitTransaction(); + } + + /** + * @inheritDoc + */ + protected function getElement(\DOMXPath $xpath, array &$elements, \DOMElement $element) { + $nodeValue = $element->nodeValue; + + if ($element->tagName === 'triggers') { + $nodeValue = [ ]; + $triggers = $xpath->query('child::*', $element); + + foreach ($triggers as $trigger) { + $nodeValue[] = $trigger->nodeValue; + } + } + + $elements[$element->tagName] = $nodeValue; + } + + /** + * @inheritDoc + */ + protected function prepareImport(array $data) { + return [ + 'identifier' => $data['attributes']['name'], + 'className' => $data['elements']['classname'], + 'triggers' => isset($data['elements']['triggers']) ? $data['elements']['triggers'] : [ ] + ]; + } + + /** + * @inheritDoc + */ + protected function validateImport(array $data) { + if ($data['identifier'] === '') { + throw new SystemException('Command identifier (name attribute) may not be empty'); + } + if (!class_exists($data['className'])) { + throw new SystemException("'".$data['className']."' does not exist."); + } + if (!\wcf\util\ClassUtil::isInstanceOf($data['className'], \chat\system\command\ICommand::class)) { + throw new SystemException("'".$data['className']."' does not implement '\chat\system\command\ICommand.'"); + } + } + + /** + * @inheritDoc + */ + protected function findExistingItem(array $data) { + $sql = "SELECT * + FROM ".$this->application.WCF_N."_".$this->tableName." + WHERE packageID = ? + AND identifier = ?"; + $parameters = [ + $this->installation->getPackageID(), + $data['identifier'] + ]; + + return [ + 'sql' => $sql, + 'parameters' => $parameters + ]; + } + + /** + * @inheritDoc + */ + protected function import(array $row, array $data) { + $triggers = $data['triggers']; + unset($data['triggers']); + + $result = parent::import($row, $data); + + if (empty($row)) { + // import initial triggers + $sql = "INSERT INTO ".$this->application.WCF_N."_command_trigger (commandTrigger, commandID) + VALUES (?, ?)"; + $statement = WCF::getDB()->prepareStatement($sql); + + try { + WCF::getDB()->beginTransaction(); + + foreach ($triggers as $trigger) { + try { + $statement->execute(array( + $trigger, + $result->commandID + )); + } + catch (\wcf\system\database\DatabaseException $e) { + // Duplicate key errors don't cause harm. + if ((string) $e->getCode() !== '23000') throw $e; + } + } + + WCF::getDB()->commitTransaction(); + } + catch (\Exception $e) { + WCF::getDB()->rollBackTransaction(); + throw $e; + } + } + + return $result; + } + + /** + * @inheritDoc + */ + public static function getSyncDependencies() { + return [ 'file' ]; + } +} diff --git a/language/de.xml b/language/de.xml new file mode 100644 index 0000000..a9c6d86 --- /dev/null +++ b/language/de.xml @@ -0,0 +1,236 @@ +<?xml version="1.0" encoding="UTF-8"?> +<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/language.xsd" languagecode="de"> + <category name="chat.acp.index"> + <item name="chat.acp.index.system.software.chatVersion"><![CDATA[Tims Chat-Version]]></item> + </category> + + <category name="chat.acp.menu"> + <item name="chat.acp.menu.link.chat"><![CDATA[Chat]]></item> + <item name="chat.acp.menu.link.command.trigger.add"><![CDATA[Befehls-Trigger hinzufügen]]></item> + <item name="chat.acp.menu.link.command.trigger.list"><![CDATA[Befehls-Trigger]]></item> + <item name="chat.acp.menu.link.room.add"><![CDATA[Chatraum hinzufügen]]></item> + <item name="chat.acp.menu.link.room.list"><![CDATA[Chatraum]]></item> + <item name="chat.acp.menu.link.suspension.list"><![CDATA[Sanktionen]]></item> + </category> + + <category name="chat.acp.room"> + <item name="chat.acp.room.list"><![CDATA[Chatraum]]></item> + <item name="chat.acp.room.add"><![CDATA[Chatraum hinzufügen]]></item> + <item name="chat.acp.room.edit"><![CDATA[Chatraum bearbeiten]]></item> + <item name="chat.acp.room.delete.sure"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Chatraum <strong>{$room}</strong> wirklich löschen?]]></item> + + <item name="chat.acp.room.topic"><![CDATA[Thema]]></item> + <item name="chat.acp.room.topic.error.tooLong"><![CDATA[Das Raumthema ist zu lang.]]></item> + <item name="chat.acp.room.topicUseHtml"><![CDATA[HTML im Thema verwenden]]></item> + <item name="chat.acp.room.userLimit"><![CDATA[Benutzerlimit]]></item> + </category> + + <category name="chat.acp.command"> + <item name="chat.acp.command.className"><![CDATA[PHP-Klassenname]]></item> + <item name="chat.acp.command.trigger"><![CDATA[Trigger]]></item> + <item name="chat.acp.command.trigger.add"><![CDATA[Trigger hinzufügen]]></item> + <item name="chat.acp.command.trigger.className.error.notFound"><![CDATA[Eine Klasse mit dem angegebenen Namen existiert nicht.]]></item> + <item name="chat.acp.command.trigger.commandTrigger.error.duplicate"><![CDATA[Dieser Trigger ist bereits in Verwendung.]]></item> + <item name="chat.acp.command.trigger.commandTrigger.error.invalid"><![CDATA[Trigger dürfen keine Leerzeichen enthalten.]]></item> + <item name="chat.acp.command.trigger.delete.sure"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den Trigger <span class="confirmationObject">/{$trigger->commandTrigger}</span> wirklich löschen?]]></item> + <item name="chat.acp.command.trigger.edit"><![CDATA[Trigger bearbeiten]]></item> + <item name="chat.acp.command.trigger.list"><![CDATA[Befehls-Trigger]]></item> + </category> + + <category name="chat.acp.suspension"> + <item name="chat.acp.suspension.list"><![CDATA[Sanktionen]]></item> + <item name="chat.acp.suspension.type"><![CDATA[Art]]></item> + <item name="chat.acp.suspension.type.be.bastelstu.chat.suspension.ban"><![CDATA[Bann]]></item> + <item name="chat.acp.suspension.type.be.bastelstu.chat.suspension.mute"><![CDATA[Knebel]]></item> + <item name="chat.acp.suspension.username"><![CDATA[Benutzername]]></item> + <item name="chat.acp.suspension.judge"><![CDATA[Richter]]></item> + <item name="chat.acp.suspension.room"><![CDATA[Chatraum]]></item> + <item name="chat.acp.suspension.time"><![CDATA[Zeitpunkt]]></item> + <item name="chat.acp.suspension.expires"><![CDATA[Läuft ab]]></item> + <item name="chat.acp.suspension.expires.forever"><![CDATA[Nie]]></item> + <item name="chat.acp.suspension.showExpired"><![CDATA[Abgelaufene Sanktionen anzeigen]]></item> + <item name="chat.acp.suspension.objectType.allTypes"><![CDATA[Alle Arten]]></item> + <item name="chat.acp.suspension.room.all"><![CDATA[Überall]]></item> + <item name="chat.acp.suspension.room.global"><![CDATA[Globale Sanktionen]]></item> + <item name="chat.acp.suspension.revoke"><![CDATA[Zurückziehen]]></item> + <item name="chat.acp.suspension.revoke.sure"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} den <strong>{lang}chat.acp.suspension.type.{$suspension->getSuspensionType()->objectType}{/lang}</strong> von {$suspension->getUser()->username} wirklich zurück ziehen?]]></item> + <item name="chat.acp.suspension.revoked"><![CDATA[Frühzeitig zurückgezogen von {$suspension->revoker}, {$suspension->revoked|plainTime}.]]></item> + </category> + + <category name="chat.connection"> + <item name="chat.connection.warning"><![CDATA[Es gibt Probleme mit der Verbindung zum Server.]]></item> + </category> + + <category name="chat.box"> + <item name="chat.box.noRooms"><![CDATA[Es erfüllen keine Chaträume die Anzeigebedingungen (beispielsweise, weil aktuell niemand chattet). {if LANGUAGE_USE_INFORMAL_VARIANT}Verwende{else}Verwenden Sie{/if} die <a href="{link controller='RoomList' application='chat'}{/link}">Raumübersicht</a>, um einen Chatraum zu betreten.]]></item> + </category> + + <category name="chat.error"> + <item name="chat.error.datePast"><![CDATA[Dieses Datum liegt in der Vergangenheit.]]></item> + <item name="chat.error.back"><![CDATA[Chat verlassen]]></item> + <item name="chat.error.hcf"><![CDATA[<p>Der Chat wurde aufgrund von anhaltenden Verbindungsproblemen oder einem anderen schwerwiegenden Problem deaktiviert{if $err.message}: {$err.message}{else}.{/if}</p>{if $err.exceptionID}<p>Exception ID: <code>{$err.exceptionID}</code></p>{/if}]]></item> + <item name="chat.error.initialization"><![CDATA[<p>Der Chat konnte nicht ordnungsgemäß initialisiert werden{if $err.message}: {$err.message}{else}.{/if}</p>{if $err.exceptionID}<p>Exception ID: <code>{$err.exceptionID}</code></p>{/if}]]></item> + <item name="chat.error.invalidColor"><![CDATA[Die Farbe „{$color}“ ist ungültig.]]></item> + <item name="chat.error.invalidParameters"><![CDATA[{if $data.result.offset >= $data.parameterString.length}{if $__window.LANGUAGE_USE_INFORMAL_VARIANT}Du hast{else}Sie haben{/if} nicht alle notwendigen Parameter übergeben{else}Die angegebenen Parameter sind an der Position „{$data.parameterString.substr($data.result.offset, 5)}“ ungültig{/if}.]]></item> + <item name="chat.error.roomFull"><![CDATA[Dieser Raum ist voll.]]></item> + <item name="chat.error.suspension.noEffect"><![CDATA[Diese Sanktion hat keine Wirkung auf diesen Benutzer.]]></item> + <item name="chat.error.suspension.remove.empty"><![CDATA[Es gibt keine passenden Sanktionen.]]></item> + <item name="chat.error.notInTemproom"><![CDATA[Dieser Befehl kann nur in temporären Räumen verwendet werden.]]></item> + <item name="chat.error.triggerNotFound"><![CDATA[Der Befehl „{$trigger}“ existiert nicht.]]></item> + <item name="chat.error.userIgnoresYou"><![CDATA[„{$user->username}“ blockiert {if LANGUAGE_USE_INFORMAL_VARIANT}dich{else}Sie{/if}.]]></item> + <item name="chat.error.userNotFound"><![CDATA[Der Benutzer „{$username}“ existiert nicht.]]></item> + </category> + + <category name="chat.log"> + <item name="chat.log.title"><![CDATA[Protokoll]]></item> + <item name="chat.log.date"><![CDATA[Datum und Uhrzeit]]></item> + <item name="chat.log.jumpToDate"><![CDATA[Zum Datum springen]]></item> + </category> + + <category name="chat.messageType"> + <item name="chat.messageType.information"><![CDATA[Information]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.away"><![CDATA[<span class="username">{@$author.coloredUsername}</span> ist jetzt abwesend{if $message.payload.message}: {@$message.payload.message}{/if}.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.away.title"><![CDATA[Dieser Benutzer ist abwesend]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.back"><![CDATA[<span class="username">{@$author.coloredUsername}</span> ist nun zurück.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.broadcast.tooltip"><![CDATA[Durchsage an alle Räume]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.color"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Deine{else}Ihre{/if} Farbe wurde erfolgreich geändert.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.info.lastActivity"><![CDATA[Letzte Aktivität]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.info.suspensions"><![CDATA[Aktive Sanktionen]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.join"><![CDATA[<span class="username">{@$author.coloredUsername}</span> hat den Chat betreten.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.join.plain"><![CDATA[{@$author.username} hat den Chat betreten.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.leave"><![CDATA[<span class="username">{@$author.coloredUsername}</span> hat den Chat verlassen.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.leave.plain"><![CDATA[{@$author.username} hat den Chat verlassen.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.team.tooltip"><![CDATA[Teaminterne Nachricht]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.temproomCreated"><![CDATA[Der temporäre Raum „<a href="{$message.payload.room.link}">{$message.payload.room.title}</a>“ wurde erfolgreich erstellt.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.temproomInvited.invitee"><![CDATA[<span class="username">{@$author.coloredUsername}</span> hat {if LANGUAGE_USE_INFORMAL_VARIANT}dich{else}Sie{/if} in den temporären Raum „<a href="{$message.payload.room.link}">{$message.payload.room.title}</a>“ eingeladen.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.temproomInvited.invitor"><![CDATA[{$message.payload.recipientName} wurde in den Raum eingeladen.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.where.lastAction"><![CDATA[Letzte Aktion]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.where.lastFetch"><![CDATA[Zuletzt gesehen]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.where.room"><![CDATA[Raum]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.tombstone.message"><![CDATA[Diese Nachricht wurde gelöscht.]]></item> + </category> + + <category name="chat.notification"> + <item name="chat.notification.title"><![CDATA[Neue Nachricht von {$message.username}]]></item> + </category> + + <category name="chat.page"> + <item name="chat.page.copyright"><![CDATA[<a href="https://tims.bastelstu.be"{if EXTERNAL_LINK_TARGET_BLANK} rel="noopener noreferrer" target="_blank"{/if}>Tims Chat{if SHOW_VERSION_NUMBER} v{@PACKAGE_VERSION}{/if}</a>]]></item> + </category> + + <category name="chat.room"> + <item name="chat.room.button.autoscroll"><![CDATA[Automatisches Scrollen umschalten]]></item> + <item name="chat.room.button.fullscreen"><![CDATA[Vollbild umschalten]]></item> + <item name="chat.room.button.leave"><![CDATA[Chat verlassen]]></item> + <item name="chat.room.button.notifications"><![CDATA[Benachrichtigungen umschalten]]></item> + <item name="chat.room.userList"><![CDATA[Benutzer]]></item> + <item name="chat.room.userList.away"><![CDATA[Abwesend{if $user.away}: {$user.away}{/if}]]></item> + <item name="chat.room.userList.moderator"><![CDATA[Moderator]]></item> + <item name="chat.room.userList.mute"><![CDATA[Stumm]]></item> + <item name="chat.room.temporary.blueprint"><![CDATA[{assign var='microtime' value=true|microtime}Temproom#{$microtime*1000%1000} ({$user->username})]]></item> + </category> + + <category name="chat.room.condition"> + <item name="chat.room.condition.isFilled"><![CDATA[Raum ist nicht leer]]></item> + </category> + + <category name="chat.stream"> + <item name="chat.stream.activity"><![CDATA[Es gibt neue Nachrichten, automatisches Scrollen ist aber deaktiviert.]]></item> + <item name="chat.stream.button.delete.sure"><![CDATA[{if $__window.LANGUAGE_USE_INFORMAL_VARIANT}Willst du{else}Wollen Sie{/if} die Nachricht wirklich löschen?]]></item> + </category> + + <category name="chat.suspension"> + <item name="chat.suspension.type"><![CDATA[Art]]></item> + <item name="chat.suspension.judge"><![CDATA[Richter]]></item> + <item name="chat.suspension.room"><![CDATA[Raum]]></item> + + <item name="chat.suspension.type.be.bastelstu.chat.suspension.ban"><![CDATA[Bann]]></item> + <item name="chat.suspension.type.be.bastelstu.chat.suspension.mute"><![CDATA[Knebel]]></item> + + <item name="chat.suspension.message.new.be.bastelstu.chat.suspension.ban"><![CDATA[<span class="username">{@$author.coloredUsername}</span> hat <span class="username">{@$message.payload.target.username}</span>{if $message.payload.globally} global{/if} {if $message.payload.suspension.expires === null}für immer{else}bis {$message.payload.suspension.formattedExpires}{/if} gebannt{if $message.payload.suspension.reason}: {$message.payload.suspension.reason}{else}.{/if}]]></item> + <item name="chat.suspension.message.new.be.bastelstu.chat.suspension.mute"><![CDATA[<span class="username">{@$author.coloredUsername}</span> hat <span class="username">{@$message.payload.target.username}</span> {if $message.payload.globally} global{/if} {if $message.payload.suspension.expires === null}für immer{else}bis {$message.payload.suspension.formattedExpires}{/if} geknebelt{if $message.payload.suspension.reason}: {$message.payload.suspension.reason}{else}.{/if}]]></item> + <item name="chat.suspension.message.revoke.be.bastelstu.chat.suspension.ban"><![CDATA[<span class="username">{@$author.coloredUsername}</span> hat <span class="username">{@$message.payload.target.username}</span>{if $message.payload.globally} global{/if} entbannt.]]></item> + <item name="chat.suspension.message.revoke.be.bastelstu.chat.suspension.mute"><![CDATA[<span class="username">{@$author.coloredUsername}</span> hat <span class="username">{@$message.payload.target.username}</span>{if $message.payload.globally} global{/if} entknebelt.]]></item> + <item name="chat.suspension.info.be.bastelstu.chat.suspension.ban"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Du{else}Sie{/if} sind aus diesem Chatraum gebannt.]]></item> + <item name="chat.suspension.info.be.bastelstu.chat.suspension.mute"><![CDATA[{if LANGUAGE_USE_INFORMAL_VARIANT}Du{else}Sie{/if} sind geknebelt.]]></item> + </category> + + <category name="chat.user"> + <item name="chat.user.action.ban"><![CDATA[Bannen]]></item> + <item name="chat.user.action.mute"><![CDATA[Knebeln]]></item> + <item name="chat.user.action.profile"><![CDATA[Profil]]></item> + <item name="chat.user.action.whisper"><![CDATA[Flüstern]]></item> + <item name="chat.user.autoAway"><![CDATA[Automatische Abwesenheit]]></item> + </category> + + <category name="wcf.acl.option"> + <item name="wcf.acl.option.category.be.bastelstu.chat.room.user"><![CDATA[Allgemein]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.user.canSee"><![CDATA[Kann sehen]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.user.canSeeLog"><![CDATA[Kann Log sehen]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.user.canWrite"><![CDATA[Kann Nachrichten senden]]></item> + <item name="wcf.acl.option.category.be.bastelstu.chat.room.mod"><![CDATA[Moderativ]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canBan"><![CDATA[Kann bannen]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canIgnoreBan"><![CDATA[Immun vor Banns]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canIgnoreMute"><![CDATA[Immun vor Knebeln]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canIgnoreUserLimit"><![CDATA[Ausgenommen vom Benutzerlimit]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canMute"><![CDATA[Kann knebeln]]></item> + </category> + + <category name="wcf.acp.box"> + <item name="wcf.acp.box.boxController.be.bastelstu.chat.roomList"><![CDATA[Chaträume]]></item> + </category> + + <category name="wcf.acp.group"> + <item name="wcf.acp.group.option.category.admin.chat"><![CDATA[Chat]]></item> + <item name="wcf.acp.group.option.category.mod.chat"><![CDATA[Chat]]></item> + <item name="wcf.acp.group.option.category.user.chat"><![CDATA[Chat]]></item> + + <item name="wcf.acp.group.option.admin.chat.canManageRoom"><![CDATA[Kann Chaträume verwalten]]></item> + <item name="wcf.acp.group.option.admin.chat.canManageSuspensions"><![CDATA[Kann Sanktionen verwalten]]></item> + <item name="wcf.acp.group.option.admin.chat.canManageTriggers"><![CDATA[Kann Befehls-Trigger verwalten]]></item> + <item name="wcf.acp.group.option.mod.chat.canBan"><![CDATA[Kann bannen]]></item> + <item name="wcf.acp.group.option.mod.chat.canBan.description"><![CDATA[Achtung: Diese Berechtigung kann nicht über Raumspezifische Rechte entzogen werden.]]></item> + <item name="wcf.acp.group.option.mod.chat.canBroadcast"><![CDATA[Kann Durchsagen versenden]]></item> + <item name="wcf.acp.group.option.mod.chat.canDelete"><![CDATA[Kann Nachrichten löschen]]></item> + <item name="wcf.acp.group.option.mod.chat.canIgnoreBan"><![CDATA[Immun vor Banns]]></item> + <item name="wcf.acp.group.option.mod.chat.canIgnoreMute"><![CDATA[Immun vor Knebeln]]></item> + <item name="wcf.acp.group.option.mod.chat.canIgnoreUserLimit"><![CDATA[Ausgenommen vom Benutzerlimit]]></item> + <item name="wcf.acp.group.option.mod.chat.canMute"><![CDATA[Kann knebeln]]></item> + <item name="wcf.acp.group.option.mod.chat.canMute.description"><![CDATA[Achtung: Diese Berechtigung kann nicht über Raumspezifische Rechte entzogen werden.]]></item> + <item name="wcf.acp.group.option.mod.chat.canTeam"><![CDATA[Kann Teamnachrichten versenden]]></item> + <item name="wcf.acp.group.option.user.chat.canSee"><![CDATA[Kann Chaträume sehen]]></item> + <item name="wcf.acp.group.option.user.chat.canSeeLog"><![CDATA[Kann das Protokoll sehen]]></item> + <item name="wcf.acp.group.option.user.chat.canSetColor"><![CDATA[Kann den Benutzernamen färben]]></item> + <item name="wcf.acp.group.option.user.chat.canTemproom"><![CDATA[Kann temporäre Räume erstellen]]></item> + <item name="wcf.acp.group.option.user.chat.canWrite"><![CDATA[Kann Nachrichten senden]]></item> + <item name="wcf.acp.group.option.user.chat.disallowedBBCodes"><![CDATA[Nicht erlaubte BBCodes]]></item> + <item name="wcf.acp.group.option.user.chat.disallowedBBCodes.description"><![CDATA[Die hier ausgewählten BBCodes dürfen von Mitgliedern dieser Benutzergruppe <em>nicht</em> verwendet werden.]]></item> + </category> + + <category name="wcf.acp.option"> + <item name="wcf.acp.option.category.chat"><![CDATA[Chat]]></item> + <item name="wcf.acp.option.category.chat.general"><![CDATA[Allgemein]]></item> + + <item name="wcf.acp.option.chat_archive_after"><![CDATA[Archivieren nach]]></item> + <item name="wcf.acp.option.chat_archive_after.description"><![CDATA[Nachrichten werden nach dieser Zeit als archiviert betrachtet und sind nur noch im Protokoll verfügbar.]]></item> + <item name="wcf.acp.option.chat_autoawaytime"><![CDATA[Automatische Abwesenheit]]></item> + <item name="wcf.acp.option.chat_autoawaytime.description"><![CDATA[Gibt an, nach welcher Zeit ein Benutzer automatisch als abwesend markiert wird. Use 0 to disable.]]></item> + <item name="wcf.acp.option.chat_log_archivetime"><![CDATA[Maximales Nachrichtenalter]]></item> + <item name="wcf.acp.option.chat_log_archivetime.description"><![CDATA[Nachrichten werden nach dieser Zeit aus der Datenbank entfernt. Use 0 to disable.]]></item> + <item name="wcf.acp.option.chat_max_length"><![CDATA[Maximale Nachrichtenlänge]]></item> + <item name="wcf.acp.option.chat_reloadtime"><![CDATA[Nachladeintervall]]></item> + <item name="wcf.acp.option.chat_reloadtime.description"><![CDATA[Gibt an, wie lange der Chat zwischen zwei Serveranfragen pausiert. Wird nicht genutzt, wenn ein Push-Dienst eingerichtet ist.]]></item> + </category> + + <category name="wcf.page"> + <item name="wcf.page.onlineLocation.be.bastelstu.chat.Log"><![CDATA[Chatprotokoll (<a href="{$room->getLink()}">{$room}</a>)]]></item> + <item name="wcf.page.onlineLocation.be.bastelstu.chat.Room"><![CDATA[Chatraum <a href="{$room->getLink()}">{$room}</a>]]></item> + <item name="wcf.page.pageObjectID.be.bastelstu.chat.Room"><![CDATA[Raum-ID]]></item> + <item name="wcf.page.pageObjectID.search.be.bastelstu.chat.Room"><![CDATA[Raumtitel suchen]]></item> + </category> + + <category name="wcf.user"> + <item name="wcf.user.activityPoint.objectType.be.bastelstu.chat.activityPointEvent.join"><![CDATA[Chatlogins]]></item> + <item name="wcf.user.activityPoint.objectType.be.bastelstu.chat.activityPointEvent.message"><![CDATA[Chatnachrichten]]></item> + </category> +</language> diff --git a/language/en.xml b/language/en.xml new file mode 100644 index 0000000..8aa7cfa --- /dev/null +++ b/language/en.xml @@ -0,0 +1,236 @@ +<?xml version="1.0" encoding="UTF-8"?> +<language xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/maelstrom/language.xsd" languagecode="en"> + <category name="chat.acp.index"> + <item name="chat.acp.index.system.software.chatVersion"><![CDATA[Tim’s Chat-Version]]></item> + </category> + + <category name="chat.acp.menu"> + <item name="chat.acp.menu.link.chat"><![CDATA[Chat]]></item> + <item name="chat.acp.menu.link.command.trigger.add"><![CDATA[Add Command Trigger]]></item> + <item name="chat.acp.menu.link.command.trigger.list"><![CDATA[Command Triggers]]></item> + <item name="chat.acp.menu.link.room.add"><![CDATA[Add Chat Room]]></item> + <item name="chat.acp.menu.link.room.list"><![CDATA[Chat Rooms]]></item> + <item name="chat.acp.menu.link.suspension.list"><![CDATA[Suspensions]]></item> + </category> + + <category name="chat.acp.room"> + <item name="chat.acp.room.list"><![CDATA[Chat Rooms]]></item> + <item name="chat.acp.room.add"><![CDATA[Add Chat Room]]></item> + <item name="chat.acp.room.edit"><![CDATA[Edit Chat Room]]></item> + <item name="chat.acp.room.delete.sure"><![CDATA[Do you really want to delete the chat room <strong>{$room}</strong>?]]></item> + + <item name="chat.acp.room.topic"><![CDATA[Topic]]></item> + <item name="chat.acp.room.topic.error.tooLong"><![CDATA[The topic is too long.]]></item> + <item name="chat.acp.room.topicUseHtml"><![CDATA[Enable HTML code in topic]]></item> + <item name="chat.acp.room.userLimit"><![CDATA[User Limit]]></item> + </category> + + <category name="chat.acp.command"> + <item name="chat.acp.command.className"><![CDATA[PHP Class Name]]></item> + <item name="chat.acp.command.trigger"><![CDATA[Trigger]]></item> + <item name="chat.acp.command.trigger.add"><![CDATA[Add Trigger]]></item> + <item name="chat.acp.command.trigger.className.error.notFound"><![CDATA[Unable to find specified class.]]></item> + <item name="chat.acp.command.trigger.commandTrigger.error.duplicate"><![CDATA[This trigger is already in use.]]></item> + <item name="chat.acp.command.trigger.commandTrigger.error.invalid"><![CDATA[Triggers must not contain spaces.]]></item> + <item name="chat.acp.command.trigger.delete.sure"><![CDATA[Do you really want to delete the trigger <span class="confirmationObject">/{$trigger->commandTrigger}</span>?]]></item> + <item name="chat.acp.command.trigger.edit"><![CDATA[Edit Trigger]]></item> + <item name="chat.acp.command.trigger.list"><![CDATA[Command Triggers]]></item> + </category> + + <category name="chat.acp.suspension"> + <item name="chat.acp.suspension.list"><![CDATA[Suspensions]]></item> + <item name="chat.acp.suspension.type"><![CDATA[Type]]></item> + <item name="chat.acp.suspension.type.be.bastelstu.chat.suspension.ban"><![CDATA[Ban]]></item> + <item name="chat.acp.suspension.type.be.bastelstu.chat.suspension.mute"><![CDATA[Mute]]></item> + <item name="chat.acp.suspension.username"><![CDATA[Username]]></item> + <item name="chat.acp.suspension.judge"><![CDATA[Judge]]></item> + <item name="chat.acp.suspension.room"><![CDATA[Chat Room]]></item> + <item name="chat.acp.suspension.time"><![CDATA[Time]]></item> + <item name="chat.acp.suspension.expires"><![CDATA[Expires]]></item> + <item name="chat.acp.suspension.expires.forever"><![CDATA[Never]]></item> + <item name="chat.acp.suspension.showExpired"><![CDATA[Show expired suspensions]]></item> + <item name="chat.acp.suspension.objectType.allTypes"><![CDATA[All Suspension Types]]></item> + <item name="chat.acp.suspension.room.all"><![CDATA[Everywhere]]></item> + <item name="chat.acp.suspension.room.global"><![CDATA[Global Suspensions]]></item> + <item name="chat.acp.suspension.revoke"><![CDATA[Revoke]]></item> + <item name="chat.acp.suspension.revoke.sure"><![CDATA[Do you really want to revoke the <strong>{lang}chat.acp.suspension.type.{$suspension->getSuspensionType()->objectType}{/lang}</strong> of {$suspension->getUser()->username}?]]></item> + <item name="chat.acp.suspension.revoked"><![CDATA[Revoked early by {$suspension->revoker}, {$suspension->revoked|plainTime}.]]></item> + </category> + + <category name="chat.connection"> + <item name="chat.connection.warning"><![CDATA[There seem to be problems with your connection to the server, or the server seems to have have gone down.]]></item> + </category> + + <category name="chat.box"> + <item name="chat.box.noRooms"><![CDATA[There are no chat rooms that match the criteria (for example because no one is chatting at this moment). Use the <a href="{link controller='RoomList' application='chat'}{/link}">Room Overview</a> to enter a chat room.]]></item> + </category> + + <category name="chat.error"> + <item name="chat.error.datePast"><![CDATA[The given date is in the past.]]></item> + <item name="chat.error.back"><![CDATA[Leave Chat]]></item> + <item name="chat.error.hcf"><![CDATA[<p>The chat was shut down because of persisting connection problems or another serious error{if $err.message}: {$err.message}{else}.{/if}</p>{if $err.exceptionID}<p>Exception ID: <code>{$err.exceptionID}</code></p>{/if}]]></item> + <item name="chat.error.initialization"><![CDATA[<p>The chat could not be properly initialized{if $err.message}: {$err.message}{else}.{/if}</p>{if $err.exceptionID}<p>Exception ID: <code>{$err.exceptionID}</code></p>{/if}]]></item> + <item name="chat.error.invalidColor"><![CDATA[The color “{$color}” is not valid.]]></item> + <item name="chat.error.invalidParameters"><![CDATA[{if $data.result.offset >= $data.parameterString.length}There are parameters missing to the given command{else}The parameters to the given command are invalid at “{$data.parameterString.substr($data.result.offset, 5)}”{/if}.]]></item> + <item name="chat.error.roomFull"><![CDATA[The maximum number of users has been reached.]]></item> + <item name="chat.error.suspension.noEffect"><![CDATA[This suspension has no effect on this user.]]></item> + <item name="chat.error.suspension.remove.empty"><![CDATA[There are no matching suspensions.]]></item> + <item name="chat.error.notInTemproom"><![CDATA[This command must be used in a temporary room.]]></item> + <item name="chat.error.triggerNotFound"><![CDATA[The command “{$trigger}” does not exist.]]></item> + <item name="chat.error.userIgnoresYou"><![CDATA[“{$user->username}” is blocking you.]]></item> + <item name="chat.error.userNotFound"><![CDATA[The username “{$username}” does not exist.]]></item> + </category> + + <category name="chat.log"> + <item name="chat.log.title"><![CDATA[Chat Log]]></item> + <item name="chat.log.date"><![CDATA[Time and date]]></item> + <item name="chat.log.jumpToDate"><![CDATA[Jump to date]]></item> + </category> + + <category name="chat.messageType"> + <item name="chat.messageType.information"><![CDATA[Information]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.away"><![CDATA[<span class="username">{@$author.coloredUsername}</span> is now away{if $message.payload.message}: {@$message.payload.message}{/if}.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.away.title"><![CDATA[This user is currently away]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.back"><![CDATA[<span class="username">{@$author.coloredUsername}</span> is now back.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.broadcast.tooltip"><![CDATA[Broadcast Across All Rooms]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.color"><![CDATA[Your color has been changed successfully.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.info.lastActivity"><![CDATA[Last Activity]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.info.suspensions"><![CDATA[Active Suspensions]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.join"><![CDATA[<span class="username">{@$author.coloredUsername}</span> joined.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.join.plain"><![CDATA[{@$author.username} joined.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.leave"><![CDATA[<span class="username">{@$author.coloredUsername}</span> left.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.leave.plain"><![CDATA[{@$author.username} left.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.team.tooltip"><![CDATA[Internal Team Message]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.temproomCreated"><![CDATA[The temporary room “<a href="{$message.payload.room.link}">{$message.payload.room.title}</a>” has been created successfully.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.temproomInvited.invitee"><![CDATA[<span class="username">{@$author.coloredUsername}</span> invited you to the temporary room “<a href="{$message.payload.room.link}">{$message.payload.room.title}</a>”.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.temproomInvited.invitor"><![CDATA[You invited {$message.payload.recipientName} to this temporary room.]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.where.lastAction"><![CDATA[Last Action]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.where.lastFetch"><![CDATA[Last Fetch]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.where.room"><![CDATA[Room]]></item> + <item name="chat.messageType.be.bastelstu.chat.messageType.tombstone.message"><![CDATA[This message has been deleted.]]></item> + </category> + + <category name="chat.notification"> + <item name="chat.notification.title"><![CDATA[New Chat Message by {$message.username}]]></item> + </category> + + <category name="chat.page"> + <item name="chat.page.copyright"><![CDATA[<a href="https://tims.bastelstu.be"{if EXTERNAL_LINK_TARGET_BLANK} rel="noopener noreferrer" target="_blank"{/if}>Tim’s Chat{if SHOW_VERSION_NUMBER} v{@PACKAGE_VERSION}{/if}</a>]]></item> + </category> + + <category name="chat.room"> + <item name="chat.room.button.autoscroll"><![CDATA[Toggle Auto Scrolling]]></item> + <item name="chat.room.button.fullscreen"><![CDATA[Toggle Fullscreen]]></item> + <item name="chat.room.button.leave"><![CDATA[Leave Chat]]></item> + <item name="chat.room.button.notifications"><![CDATA[Toggle Notifications]]></item> + <item name="chat.room.userList"><![CDATA[Users]]></item> + <item name="chat.room.userList.away"><![CDATA[Away{if $user.away}: {$user.away}{/if}]]></item> + <item name="chat.room.userList.moderator"><![CDATA[Moderator]]></item> + <item name="chat.room.userList.mute"><![CDATA[Mute]]></item> + <item name="chat.room.temporary.blueprint"><![CDATA[{assign var='microtime' value=true|microtime}Temproom#{$microtime*1000%1000} ({$user->username})]]></item> + </category> + + <category name="chat.room.condition"> + <item name="chat.room.condition.isFilled"><![CDATA[Room is not empty]]></item> + </category> + + <category name="chat.stream"> + <item name="chat.stream.activity"><![CDATA[New messages arrived, while automated scrolling is disabled.]]></item> + <item name="chat.stream.button.delete.sure"><![CDATA[Do you really want to delete the message?]]></item> + </category> + + <category name="chat.suspension"> + <item name="chat.suspension.type"><![CDATA[Type]]></item> + <item name="chat.suspension.judge"><![CDATA[Judge]]></item> + <item name="chat.suspension.room"><![CDATA[Room]]></item> + + <item name="chat.suspension.type.be.bastelstu.chat.suspension.ban"><![CDATA[Ban]]></item> + <item name="chat.suspension.type.be.bastelstu.chat.suspension.mute"><![CDATA[Mute]]></item> + + <item name="chat.suspension.message.new.be.bastelstu.chat.suspension.ban"><![CDATA[<span class="username">{@$author.coloredUsername}</span> banned <span class="username">{@$message.payload.target.username}</span>{if $message.payload.globally} globally{/if} {if $message.payload.suspension.expires === null}forever{else}until {$message.payload.suspension.formattedExpires}{/if}{if $message.payload.suspension.reason}: {$message.payload.suspension.reason}{else}.{/if}]]></item> + <item name="chat.suspension.message.new.be.bastelstu.chat.suspension.mute"><![CDATA[<span class="username">{@$author.coloredUsername}</span> muted <span class="username">{@$message.payload.target.username}</span>{if $message.payload.globally} globally{/if} {if $message.payload.suspension.expires === null}forever{else}until {$message.payload.suspension.formattedExpires}{/if}{if $message.payload.suspension.reason}: {$message.payload.suspension.reason}{else}.{/if}]]></item> + <item name="chat.suspension.message.revoke.be.bastelstu.chat.suspension.ban"><![CDATA[<span class="username">{@$author.coloredUsername}</span> unbanned <span class="username">{@$message.payload.target.username}</span>{if $message.payload.globally} globally{/if}.]]></item> + <item name="chat.suspension.message.revoke.be.bastelstu.chat.suspension.mute"><![CDATA[<span class="username">{@$author.coloredUsername}</span> unmuted <span class="username">{@$message.payload.target.username}</span>{if $message.payload.globally} globally{/if}.]]></item> + <item name="chat.suspension.info.be.bastelstu.chat.suspension.ban"><![CDATA[You are banned from this chat room.]]></item> + <item name="chat.suspension.info.be.bastelstu.chat.suspension.mute"><![CDATA[You are muted.]]></item> + </category> + + <category name="chat.user"> + <item name="chat.user.action.ban"><![CDATA[Ban]]></item> + <item name="chat.user.action.mute"><![CDATA[Mute]]></item> + <item name="chat.user.action.profile"><![CDATA[Profile]]></item> + <item name="chat.user.action.whisper"><![CDATA[Whisper]]></item> + <item name="chat.user.autoAway"><![CDATA[Automated away]]></item> + </category> + + <category name="wcf.acl.option"> + <item name="wcf.acl.option.category.be.bastelstu.chat.room.user"><![CDATA[General Permissions]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.user.canSee"><![CDATA[Can see]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.user.canSeeLog"><![CDATA[Can see log]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.user.canWrite"><![CDATA[Can send messages]]></item> + <item name="wcf.acl.option.category.be.bastelstu.chat.room.mod"><![CDATA[Moderator Permissions]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canBan"><![CDATA[Can ban]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canIgnoreBan"><![CDATA[Immune from bans]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canIgnoreMute"><![CDATA[Immune from mutes]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canIgnoreUserLimit"><![CDATA[Exempt from user limit]]></item> + <item name="wcf.acl.option.be.bastelstu.chat.room.mod.canMute"><![CDATA[Can mute]]></item> + </category> + + <category name="wcf.acp.box"> + <item name="wcf.acp.box.boxController.be.bastelstu.chat.roomList"><![CDATA[Chat Rooms]]></item> + </category> + + <category name="wcf.acp.group"> + <item name="wcf.acp.group.option.category.admin.chat"><![CDATA[Chat]]></item> + <item name="wcf.acp.group.option.category.mod.chat"><![CDATA[Chat]]></item> + <item name="wcf.acp.group.option.category.user.chat"><![CDATA[Chat]]></item> + + <item name="wcf.acp.group.option.admin.chat.canManageRoom"><![CDATA[Can manage chat rooms]]></item> + <item name="wcf.acp.group.option.admin.chat.canManageSuspensions"><![CDATA[Can manage suspensions]]></item> + <item name="wcf.acp.group.option.admin.chat.canManageTriggers"><![CDATA[Can manage command triggers]]></item> + <item name="wcf.acp.group.option.mod.chat.canBan"><![CDATA[Can ban]]></item> + <item name="wcf.acp.group.option.mod.chat.canBan.description"><![CDATA[Note: If this permission is granted it cannot be revoked in the room specific permissions.]]></item> + <item name="wcf.acp.group.option.mod.chat.canBroadcast"><![CDATA[Can send broadcasts]]></item> + <item name="wcf.acp.group.option.mod.chat.canDelete"><![CDATA[Can delete messages]]></item> + <item name="wcf.acp.group.option.mod.chat.canIgnoreBan"><![CDATA[Immune from bans]]></item> + <item name="wcf.acp.group.option.mod.chat.canIgnoreMute"><![CDATA[Immune from mutes]]></item> + <item name="wcf.acp.group.option.mod.chat.canIgnoreUserLimit"><![CDATA[Exempt from user limit]]></item> + <item name="wcf.acp.group.option.mod.chat.canMute"><![CDATA[Can mute]]></item> + <item name="wcf.acp.group.option.mod.chat.canMute.description"><![CDATA[Note: If this permission is granted it cannot be revoked in the room specific permissions.]]></item> + <item name="wcf.acp.group.option.mod.chat.canTeam"><![CDATA[Can use team internal messages]]></item> + <item name="wcf.acp.group.option.user.chat.canSee"><![CDATA[Can see chat rooms]]></item> + <item name="wcf.acp.group.option.user.chat.canSeeLog"><![CDATA[Can see chat log]]></item> + <item name="wcf.acp.group.option.user.chat.canSetColor"><![CDATA[Can choose to color their name]]></item> + <item name="wcf.acp.group.option.user.chat.canTemproom"><![CDATA[Can create temporary rooms]]></item> + <item name="wcf.acp.group.option.user.chat.canWrite"><![CDATA[Can send messages to chat rooms]]></item> + <item name="wcf.acp.group.option.user.chat.disallowedBBCodes"><![CDATA[Disallowed BBCodes]]></item> + <item name="wcf.acp.group.option.user.chat.disallowedBBCodes.description"><![CDATA[Selected BBCodes <em>cannot</em> be used by the users of this user group.]]></item> + </category> + + <category name="wcf.acp.option"> + <item name="wcf.acp.option.category.chat"><![CDATA[Chat]]></item> + <item name="wcf.acp.option.category.chat.general"><![CDATA[General]]></item> + + <item name="wcf.acp.option.chat_archive_after"><![CDATA[Archive After]]></item> + <item name="wcf.acp.option.chat_archive_after.description"><![CDATA[Messages are considered archived by this time and are not available in the regular message stream any more.]]></item> + <item name="wcf.acp.option.chat_autoawaytime"><![CDATA[Automated Away]]></item> + <item name="wcf.acp.option.chat_autoawaytime.description"><![CDATA[Specifies how long it takes for a user to be marked as away automatically. Use 0 to disable.]]></item> + <item name="wcf.acp.option.chat_log_archivetime"><![CDATA[Maximum Message Age]]></item> + <item name="wcf.acp.option.chat_log_archivetime.description"><![CDATA[Messages are pruned from the database by this time. Use 0 to disable.]]></item> + <item name="wcf.acp.option.chat_max_length"><![CDATA[Maximum Message Length]]></item> + <item name="wcf.acp.option.chat_reloadtime"><![CDATA[Reload Interval]]></item> + <item name="wcf.acp.option.chat_reloadtime.description"><![CDATA[Specifies how long the chat waits between two attempts to pull the server for new messages. Does not apply if a push service is being used.]]></item> + </category> + + <category name="wcf.page"> + <item name="wcf.page.onlineLocation.be.bastelstu.chat.Log"><![CDATA[Chatlog (<a href="{$room->getLink()}">{$room}</a>)]]></item> + <item name="wcf.page.onlineLocation.be.bastelstu.chat.Room"><![CDATA[Chat Room <a href="{$room->getLink()}">{$room}</a>]]></item> + <item name="wcf.page.pageObjectID.be.bastelstu.chat.Room"><![CDATA[ID of the Room]]></item> + <item name="wcf.page.pageObjectID.search.be.bastelstu.chat.Room"><![CDATA[Search Room Titles]]></item> + </category> + + <category name="wcf.user"> + <item name="wcf.user.activityPoint.objectType.be.bastelstu.chat.activityPointEvent.join"><![CDATA[Chat Joins]]></item> + <item name="wcf.user.activityPoint.objectType.be.bastelstu.chat.activityPointEvent.message"><![CDATA[Chat Messages]]></item> + </category> +</language> diff --git a/menuItem.xml b/menuItem.xml new file mode 100644 index 0000000..c605625 --- /dev/null +++ b/menuItem.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="UTF-8"?> +<data xmlns="http://www.woltlab.com" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.woltlab.com http://www.woltlab.com/XSD/vortex/menuItem.xsd"> + <import> + <item identifier="be.bastelstu.chat.RoomList"> + <menu>com.woltlab.wcf.MainMenu</menu> + <title language="de">Chat + Chat + be.bastelstu.chat.RoomList + + + diff --git a/objectType.xml b/objectType.xml new file mode 100644 index 0000000..d0699cd --- /dev/null +++ b/objectType.xml @@ -0,0 +1,172 @@ + + + + + + be.bastelstu.chat.room + com.woltlab.wcf.acl + + + + + + be.bastelstu.chat.roomList + com.woltlab.wcf.boxController + chat\system\box\RoomListBoxController + + + + + + be.bastelstu.chat.roomFilled + be.bastelstu.chat.box.roomList.condition + chat\system\condition\room\RoomFilledCondition + + + + + + be.bastelstu.chat.messageType.away + be.bastelstu.chat.messageType + chat\system\message\type\AwayMessageType + + + + be.bastelstu.chat.messageType.back + be.bastelstu.chat.messageType + chat\system\message\type\BackMessageType + + + + be.bastelstu.chat.messageType.broadcast + be.bastelstu.chat.messageType + chat\system\message\type\BroadcastMessageType + + + + be.bastelstu.chat.messageType.chatUpdate + be.bastelstu.chat.messageType + chat\system\message\type\ChatUpdateMessageType + + + + be.bastelstu.chat.messageType.color + be.bastelstu.chat.messageType + chat\system\message\type\ColorMessageType + + + + be.bastelstu.chat.messageType.info + be.bastelstu.chat.messageType + chat\system\message\type\InfoMessageType + + + + be.bastelstu.chat.messageType.join + be.bastelstu.chat.messageType + chat\system\message\type\JoinMessageType + + + + be.bastelstu.chat.messageType.leave + be.bastelstu.chat.messageType + chat\system\message\type\LeaveMessageType + + + + be.bastelstu.chat.messageType.me + be.bastelstu.chat.messageType + chat\system\message\type\MeMessageType + + + + be.bastelstu.chat.messageType.plain + be.bastelstu.chat.messageType + chat\system\message\type\PlainMessageType + + + + be.bastelstu.chat.messageType.suspend + be.bastelstu.chat.messageType + chat\system\message\type\SuspendMessageType + + + + be.bastelstu.chat.messageType.team + be.bastelstu.chat.messageType + chat\system\message\type\TeamMessageType + + + + be.bastelstu.chat.messageType.temproomCreated + be.bastelstu.chat.messageType + chat\system\message\type\TemproomCreatedMessageType + + + + be.bastelstu.chat.messageType.temproomInvited + be.bastelstu.chat.messageType + chat\system\message\type\TemproomInvitedMessageType + + + + be.bastelstu.chat.messageType.tombstone + be.bastelstu.chat.messageType + chat\system\message\type\TombstoneMessageType + + + + be.bastelstu.chat.messageType.unsuspend + be.bastelstu.chat.messageType + chat\system\message\type\UnsuspendMessageType + + + + be.bastelstu.chat.messageType.where + be.bastelstu.chat.messageType + chat\system\message\type\WhereMessageType + + + + be.bastelstu.chat.messageType.whisper + be.bastelstu.chat.messageType + chat\system\message\type\WhisperMessageType + + + + + + be.bastelstu.chat.suspension.ban + be.bastelstu.chat.suspension + chat\system\suspension\BanSuspension + + + + be.bastelstu.chat.suspension.mute + be.bastelstu.chat.suspension + chat\system\suspension\MuteSuspension + + + + + + be.bastelstu.chat.message + com.woltlab.wcf.message + + + + + + be.bastelstu.chat.activityPointEvent.join + com.woltlab.wcf.user.activityPointEvent + 10 + + + + be.bastelstu.chat.activityPointEvent.message + com.woltlab.wcf.user.activityPointEvent + 1 + + + + diff --git a/objectTypeDefinition.xml b/objectTypeDefinition.xml new file mode 100644 index 0000000..cf46501 --- /dev/null +++ b/objectTypeDefinition.xml @@ -0,0 +1,19 @@ + + + + + be.bastelstu.chat.messageType + chat\system\message\type\IMessageType + + + + be.bastelstu.chat.box.roomList.condition + wcf\system\condition\IObjectListCondition + + + + be.bastelstu.chat.suspension + chat\system\suspension\ISuspension + + + diff --git a/option.xml b/option.xml new file mode 100644 index 0000000..b108554 --- /dev/null +++ b/option.xml @@ -0,0 +1,56 @@ + + + + + + + chat + + + + + + + + + + + + + + + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..7db2afb --- /dev/null +++ b/package.json @@ -0,0 +1,8 @@ +{ + "dependencies": { + "babel-cli": "^6.26.0", + "babel-preset-env": "^1.7.0", + "requirejs": "^2.3.5", + "terser": "^3.8.1" + } +} diff --git a/package.xml b/package.xml new file mode 100644 index 0000000..17d611a --- /dev/null +++ b/package.xml @@ -0,0 +1,113 @@ + + + + Tim’s Chat + Tims Chat + 1 + chat + 4.0.0 RC 2 + 2018-08-16 + + + + Tim Düsterhus + http://tims.bastelstu.be + + + + com.woltlab.wcf + be.bastelstu.core-js + be.bastelstu.promiseWrap + be.bastelstu.parserCombinator + be.bastelstu.bottle + be.bastelstu.wcf.push + + + + + + + + + sql/0001-chat1_room.sql + sql/0002-Default-Room.sql + sql/0003-chat1_room_to_user.sql + sql/0004-chat1_message.sql + sql/0005-chat1_room_to_user-FOREIGN_KEY.sql + sql/0006-chat1_room_to_user-Timestamps.sql + sql/0007-chat1_room_to_user_lastPull.sql + sql/0008-chat1_message-Username-Width.sql + sql/0009-chat1_command.sql + sql/0010-chat1_command_trigger.sql + sql/0011-chat1_session.sql + sql/0012-chat1_message-Nullroom.sql + sql/0013-chat1_session-Index.sql + sql/0014-chat1_message-Embedded-Objects.sql + sql/0015-chat1_user-Away.sql + sql/0016-chat1_command_trigger-PRIMARY_KEY.sql + sql/0017-chat1_command-Unique-className.sql + sql/0018-wcf1_user-Color.sql + sql/0019-chat1_room-User-Limit.sql + sql/0019-chat1_suspension.sql + sql/0020-chat1_suspension-Revoked.sql + sql/0021-chat1_room-Temporary.sql + sql/0022-chat1_room_temporary_invite.sql + sql/0023-chat1_message-isDeleted.sql + sql/0024-chat1_room-topicUseHtml.sql + sql/0025-chat1_room-topic-text.sql + + files_wcf.tar + + + + + + + + + + + + + + + + + + + + + + + + + acp/be.bastelstu.chat_install.php + + + + files_wcf.tar + + + + + + + + + + + + + + + + + + + + + + + acp/be.bastelstu.chat_update.php + + diff --git a/packageInstallationPlugin.xml b/packageInstallationPlugin.xml new file mode 100644 index 0000000..2212b56 --- /dev/null +++ b/packageInstallationPlugin.xml @@ -0,0 +1,6 @@ + + + + wcf\system\package\plugin\ChatCommandPackageInstallationPlugin + + diff --git a/page.xml b/page.xml new file mode 100644 index 0000000..cd456b0 --- /dev/null +++ b/page.xml @@ -0,0 +1,47 @@ + + + + + system + chat\page\RoomListPage + chat\system\page\handler\RoomListPageHandler + Chatraum-Liste + Chat Room List + + + Chat + + + Chat + + + + + system + chat\page\RoomPage + chat\system\page\handler\RoomPageHandler + Chatraum + Chat Room + 1 + be.bastelstu.chat.RoomList + + + + system + chat\page\LogPage + chat\system\page\handler\LogPageHandler + Chatlog + Chat Log + 1 + 1 + be.bastelstu.chat.Room + + + Chat Log + + + Chatlog + + + + diff --git a/require.build.js b/require.build.js new file mode 100644 index 0000000..f170a6c --- /dev/null +++ b/require.build.js @@ -0,0 +1,73 @@ +(function () { + var config = { + name: "_Meta", + out: "Bastelstu.be.Chat.js", + useStrict: true, + preserveLicenseComments: false, + optimize: 'none', + excludeShallow: [ + '_Meta' + ], + rawText: { + '_Meta': 'define([], function() {});' + }, + paths: { + 'Bastelstu.be': 'files_wcf/js/Bastelstu.be' + }, + onBuildRead: function(moduleName, path, contents) { + if (!process.versions.node) { + throw new Error('You need to run node.js'); + } + + if (moduleName === '_Meta') { + if (global.allModules === undefined) { + var fs = module.require('fs'), + path = module.require('path'); + global.allModules = []; + + var queue = ['Bastelstu.be']; + var folder; + while (folder = queue.shift()) { + var files = fs.readdirSync('files_wcf/js/' + folder); + for (var i = 0; i < files.length; i++) { + var filename = path.join(folder, files[i]).replace(/\\/g, '/'); + + if (path.extname(filename) === '.js') { + global.allModules.push(filename); + } + else if (fs.statSync('files_wcf/js/' + filename).isDirectory()) { + queue.push(filename); + } + } + } + } + + return 'define([' + global.allModules.map(function (item) { return "'" + item.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\.js$/, '') + "'"; }).join(', ') + '], function() { });'; + } + + return contents; + } + }; + + var _isSupportedBuildUrl = require._isSupportedBuildUrl; + require._isSupportedBuildUrl = function (url) { + var result = _isSupportedBuildUrl(url); + if (!result) return result; + if (Object.keys(config.rawText).some(module => url.endsWith(`${module}.js`))) return result; + + var fs = module.require('fs'); + try { + fs.statSync(url); + } + catch (e) { + console.log('Unable to find module:', url, 'ignoring.'); + + return false; + } + return true; + }; + + if (module) module.exports = config; + + return config; +})(); diff --git a/sql/0001-chat1_room.sql b/sql/0001-chat1_room.sql new file mode 100644 index 0000000..ef9edf1 --- /dev/null +++ b/sql/0001-chat1_room.sql @@ -0,0 +1,5 @@ +CREATE TABLE chat1_room ( roomID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY + , title VARCHAR(255) NOT NULL + , topic VARCHAR(255) NOT NULL + , position SMALLINT(5) NOT NULL + ); diff --git a/sql/0002-Default-Room.sql b/sql/0002-Default-Room.sql new file mode 100644 index 0000000..ebfcd75 --- /dev/null +++ b/sql/0002-Default-Room.sql @@ -0,0 +1 @@ +INSERT INTO chat1_room (title, topic, position) VALUES ('Default', '', 0); diff --git a/sql/0003-chat1_room_to_user.sql b/sql/0003-chat1_room_to_user.sql new file mode 100644 index 0000000..71d8aa1 --- /dev/null +++ b/sql/0003-chat1_room_to_user.sql @@ -0,0 +1,6 @@ +CREATE TABLE chat1_room_to_user ( roomID INT(10) NOT NULL + , userID INT(10) NOT NULL + + , PRIMARY KEY (roomID, userID) + , KEY (userID) + ); diff --git a/sql/0004-chat1_message.sql b/sql/0004-chat1_message.sql new file mode 100644 index 0000000..c6a341c --- /dev/null +++ b/sql/0004-chat1_message.sql @@ -0,0 +1,16 @@ +CREATE TABLE chat1_message ( messageID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY + , time INT(10) NOT NULL + , roomID INT(10) NOT NULL + , userID INT(10) DEFAULT NULL + , username VARCHAR(255) NOT NULL + , objectTypeID INT(10) NOT NULL + , payload MEDIUMBLOB NOT NULL + + , KEY (roomID) + , KEY (userID) + , KEY (time) + ); + +ALTER TABLE chat1_message ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE SET NULL; +ALTER TABLE chat1_message ADD FOREIGN KEY (roomID) REFERENCES chat1_room (roomID) ON DELETE CASCADE; +ALTER TABLE chat1_message ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; diff --git a/sql/0005-chat1_room_to_user-FOREIGN_KEY.sql b/sql/0005-chat1_room_to_user-FOREIGN_KEY.sql new file mode 100644 index 0000000..2a3a397 --- /dev/null +++ b/sql/0005-chat1_room_to_user-FOREIGN_KEY.sql @@ -0,0 +1,2 @@ +ALTER TABLE chat1_room_to_user ADD FOREIGN KEY (roomID) REFERENCES chat1_room (roomID) ON DELETE CASCADE; +ALTER TABLE chat1_room_to_user ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE; diff --git a/sql/0006-chat1_room_to_user-Timestamps.sql b/sql/0006-chat1_room_to_user-Timestamps.sql new file mode 100644 index 0000000..77393bb --- /dev/null +++ b/sql/0006-chat1_room_to_user-Timestamps.sql @@ -0,0 +1,5 @@ +ALTER TABLE chat1_room_to_user ADD lastFetch INT(10) NOT NULL DEFAULT 0; +ALTER TABLE chat1_room_to_user ADD lastPush INT(10) NOT NULL DEFAULT 0; +ALTER TABLE chat1_room_to_user ADD active TINYINT(1) NOT NULL DEFAULT 0; +ALTER TABLE chat1_room_to_user ADD KEY (roomID, active); +ALTER TABLE chat1_room_to_user ADD KEY (active); diff --git a/sql/0007-chat1_room_to_user_lastPull.sql b/sql/0007-chat1_room_to_user_lastPull.sql new file mode 100644 index 0000000..4aba540 --- /dev/null +++ b/sql/0007-chat1_room_to_user_lastPull.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_room_to_user CHANGE lastFetch lastPull INT(10) NOT NULL DEFAULT 0; diff --git a/sql/0008-chat1_message-Username-Width.sql b/sql/0008-chat1_message-Username-Width.sql new file mode 100644 index 0000000..0137e57 --- /dev/null +++ b/sql/0008-chat1_message-Username-Width.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_message CHANGE username username VARCHAR(100) NOT NULL; diff --git a/sql/0009-chat1_command.sql b/sql/0009-chat1_command.sql new file mode 100644 index 0000000..7a152d4 --- /dev/null +++ b/sql/0009-chat1_command.sql @@ -0,0 +1,9 @@ +CREATE TABLE chat1_command ( commandID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY + , packageID INT(10) NOT NULL + , identifier VARCHAR(191) NOT NULL + , className VARCHAR(191) NOT NULL + + , UNIQUE KEY command (packageID, identifier) + ); + +ALTER TABLE chat1_command ADD FOREIGN KEY (packageID) REFERENCES wcf1_package (packageID) ON DELETE CASCADE; diff --git a/sql/0010-chat1_command_trigger.sql b/sql/0010-chat1_command_trigger.sql new file mode 100644 index 0000000..4b714c0 --- /dev/null +++ b/sql/0010-chat1_command_trigger.sql @@ -0,0 +1,7 @@ +CREATE TABLE chat1_command_trigger ( commandTrigger VARCHAR(191) NOT NULL PRIMARY KEY + , commandID INT(10) NOT NULL + + , KEY commandID (commandID) + ); + +ALTER TABLE chat1_command_trigger ADD FOREIGN KEY (commandID) REFERENCES chat1_command (commandID) ON DELETE CASCADE; diff --git a/sql/0011-chat1_session.sql b/sql/0011-chat1_session.sql new file mode 100644 index 0000000..66690df --- /dev/null +++ b/sql/0011-chat1_session.sql @@ -0,0 +1,12 @@ +CREATE TABLE chat1_session ( roomID INT(10) NOT NULL + , userID INT(10) NOT NULL + , sessionID BINARY(16) NOT NULL + , lastRequest INT(10) NOT NULL + + , PRIMARY KEY (roomID, userID, sessionID) + , KEY (userID, sessionID) + , KEY (sessionID) + ); + +ALTER TABLE chat1_session ADD FOREIGN KEY (roomID) REFERENCES chat1_room (roomID) ON DELETE CASCADE; +ALTER TABLE chat1_session ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE; diff --git a/sql/0012-chat1_message-Nullroom.sql b/sql/0012-chat1_message-Nullroom.sql new file mode 100644 index 0000000..4a3bc67 --- /dev/null +++ b/sql/0012-chat1_message-Nullroom.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_message CHANGE roomID roomID INT(10) DEFAULT NULL; diff --git a/sql/0013-chat1_session-Index.sql b/sql/0013-chat1_session-Index.sql new file mode 100644 index 0000000..3479f15 --- /dev/null +++ b/sql/0013-chat1_session-Index.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_session ADD KEY (lastRequest); diff --git a/sql/0014-chat1_message-Embedded-Objects.sql b/sql/0014-chat1_message-Embedded-Objects.sql new file mode 100644 index 0000000..84fbc37 --- /dev/null +++ b/sql/0014-chat1_message-Embedded-Objects.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_message ADD hasEmbeddedObjects TINYINT(1) NOT NULL DEFAULT 0; diff --git a/sql/0015-chat1_user-Away.sql b/sql/0015-chat1_user-Away.sql new file mode 100644 index 0000000..381759b --- /dev/null +++ b/sql/0015-chat1_user-Away.sql @@ -0,0 +1 @@ +ALTER TABLE wcf1_user ADD COLUMN chatAway TEXT DEFAULT NULL; diff --git a/sql/0016-chat1_command_trigger-PRIMARY_KEY.sql b/sql/0016-chat1_command_trigger-PRIMARY_KEY.sql new file mode 100644 index 0000000..410156a --- /dev/null +++ b/sql/0016-chat1_command_trigger-PRIMARY_KEY.sql @@ -0,0 +1,3 @@ +ALTER TABLE chat1_command_trigger DROP PRIMARY KEY; +ALTER TABLE chat1_command_trigger ADD COLUMN triggerID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY; +ALTER TABLE chat1_command_trigger ADD UNIQUE KEY (commandTrigger); diff --git a/sql/0017-chat1_command-Unique-className.sql b/sql/0017-chat1_command-Unique-className.sql new file mode 100644 index 0000000..a55127e --- /dev/null +++ b/sql/0017-chat1_command-Unique-className.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_command ADD UNIQUE KEY (className); diff --git a/sql/0018-wcf1_user-Color.sql b/sql/0018-wcf1_user-Color.sql new file mode 100644 index 0000000..1d4e2f1 --- /dev/null +++ b/sql/0018-wcf1_user-Color.sql @@ -0,0 +1,2 @@ +ALTER TABLE wcf1_user ADD COLUMN chatColor1 INT(10) DEFAULT NULL; +ALTER TABLE wcf1_user ADD COLUMN chatColor2 INT(10) DEFAULT NULL; diff --git a/sql/0019-chat1_room-User-Limit.sql b/sql/0019-chat1_room-User-Limit.sql new file mode 100644 index 0000000..b0ad6dc --- /dev/null +++ b/sql/0019-chat1_room-User-Limit.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_room ADD COLUMN userLimit INT(10) NOT NULL DEFAULT 0; diff --git a/sql/0019-chat1_suspension.sql b/sql/0019-chat1_suspension.sql new file mode 100644 index 0000000..8e09018 --- /dev/null +++ b/sql/0019-chat1_suspension.sql @@ -0,0 +1,25 @@ +CREATE TABLE chat1_suspension ( suspensionID INT(10) NOT NULL AUTO_INCREMENT PRIMARY KEY + , time INT(10) NOT NULL + , expires INT(10) NULL + , roomID INT(10) NULL + , userID INT(10) NOT NULL + , objectTypeID INT(10) NOT NULL + , reason VARCHAR(255) NOT NULL + , judgeID INT(10) NULL + , judge VARCHAR(100) NOT NULL + , revoked TINYINT(1) NOT NULL DEFAULT 0 + , revokerID INT(10) DEFAULT NULL + , revoker VARCHAR(100) DEFAULT NULL + + , KEY (roomID, userID, objectTypeID) + , KEY (userID) + , KEY (objectTypeID, roomID) + , KEY (time) + , KEY (judgeID) + ); + +ALTER TABLE chat1_suspension ADD FOREIGN KEY (roomID) REFERENCES chat1_room (roomID) ON DELETE CASCADE; +ALTER TABLE chat1_suspension ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE; +ALTER TABLE chat1_suspension ADD FOREIGN KEY (judgeID) REFERENCES wcf1_user (userID) ON DELETE SET NULL; +ALTER TABLE chat1_suspension ADD FOREIGN KEY (revokerID) REFERENCES wcf1_user (userID) ON DELETE SET NULL; +ALTER TABLE chat1_suspension ADD FOREIGN KEY (objectTypeID) REFERENCES wcf1_object_type (objectTypeID) ON DELETE CASCADE; diff --git a/sql/0020-chat1_suspension-Revoked.sql b/sql/0020-chat1_suspension-Revoked.sql new file mode 100644 index 0000000..b140e28 --- /dev/null +++ b/sql/0020-chat1_suspension-Revoked.sql @@ -0,0 +1,2 @@ +ALTER TABLE chat1_suspension CHANGE revoked revoked INT(10) DEFAULT NULL; +UPDATE chat1_suspension SET revoked = NULL WHERE revoked = 0; diff --git a/sql/0021-chat1_room-Temporary.sql b/sql/0021-chat1_room-Temporary.sql new file mode 100644 index 0000000..fe1b4a4 --- /dev/null +++ b/sql/0021-chat1_room-Temporary.sql @@ -0,0 +1,4 @@ +ALTER TABLE chat1_room ADD isTemporary TINYINT(1) NOT NULL DEFAULT 0; +ALTER TABLE chat1_room ADD ownerID INT(10) DEFAULT NULL; + +ALTER TABLE chat1_room ADD FOREIGN KEY (ownerID) REFERENCES wcf1_user (userID) ON DELETE SET NULL; diff --git a/sql/0022-chat1_room_temporary_invite.sql b/sql/0022-chat1_room_temporary_invite.sql new file mode 100644 index 0000000..8e114b5 --- /dev/null +++ b/sql/0022-chat1_room_temporary_invite.sql @@ -0,0 +1,8 @@ +CREATE TABLE chat1_room_temporary_invite ( roomID INT(10) NOT NULL + , userID INT(10) NOT NULL + , PRIMARY KEY (roomID, userID) + , KEY (userID) + ); + +ALTER TABLE chat1_room_temporary_invite ADD FOREIGN KEY (roomID) REFERENCES chat1_room (roomID) ON DELETE CASCADE; +ALTER TABLE chat1_room_temporary_invite ADD FOREIGN KEY (userID) REFERENCES wcf1_user (userID) ON DELETE CASCADE; diff --git a/sql/0023-chat1_message-isDeleted.sql b/sql/0023-chat1_message-isDeleted.sql new file mode 100644 index 0000000..191d914 --- /dev/null +++ b/sql/0023-chat1_message-isDeleted.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_message ADD isDeleted TINYINT(1) NOT NULL DEFAULT 0; diff --git a/sql/0024-chat1_room-topicUseHtml.sql b/sql/0024-chat1_room-topicUseHtml.sql new file mode 100644 index 0000000..4f3ea45 --- /dev/null +++ b/sql/0024-chat1_room-topicUseHtml.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_room ADD topicUseHtml TINYINT(1) NOT NULL DEFAULT 0; diff --git a/sql/0025-chat1_room-topic-text.sql b/sql/0025-chat1_room-topic-text.sql new file mode 100644 index 0000000..759ca43 --- /dev/null +++ b/sql/0025-chat1_room-topic-text.sql @@ -0,0 +1 @@ +ALTER TABLE chat1_room CHANGE topic topic TEXT NOT NULL; diff --git a/templateListener.xml b/templateListener.xml new file mode 100644 index 0000000..72d903e --- /dev/null +++ b/templateListener.xml @@ -0,0 +1,62 @@ + + + + + + admin + index + softwareVersions + + + + + + + user + pageFooterCopyright + copyright + + + + + user + messageTypes + infoCommandContents + + + + user + room + beforeBootstrap + + + + user + room + language + + + + user + userList + icons + + + + + user + messageTypes + messageTypes + + + + + user + messageTypes + language + + + + + diff --git a/templates/__chatCopyright.tpl b/templates/__chatCopyright.tpl new file mode 100644 index 0000000..6b0d140 --- /dev/null +++ b/templates/__chatCopyright.tpl @@ -0,0 +1,3 @@ +{if $__chat->isActiveApplication()} + +{/if} diff --git a/templates/boxRoomList.tpl b/templates/boxRoomList.tpl new file mode 100644 index 0000000..298a3dd --- /dev/null +++ b/templates/boxRoomList.tpl @@ -0,0 +1,67 @@ +
+ {capture assign='chatBoxRoomList'} + {foreach from=$boxRoomList item='room'} + {if $room->canSee() && (!$skipEmptyRooms|isset || !$skipEmptyRooms || !$room->getUsers()|empty)} + roomID === $activeRoomID} class="active"{/if}> +
+
+ {assign var='disallowJoinReason' value=null} +
+ {if $room->canJoin(null, $disallowJoinReason)} +

+ {$room->getTitle()} + {#$room->getUsers()|count}{if $room->userLimit} / {#$room->userLimit}{/if} +

+ + {if $room->getTopic()} +

{@$room->getTopic()}

+ {/if} + {else} +

{$room->getTitle()} {#$room->getUsers()|count}{if $room->userLimit} / {#$room->userLimit}{/if}

+ {/if} +
+ + {if !$room->getUsers()|empty || $disallowJoinReason !== null} +
+ {if !$room->getUsers()|empty} + + {/if} + + {if $disallowJoinReason !== null} +
{$disallowJoinReason->getMessage()}
+ {/if} +
+ {/if} +
+
+ + {/if} + {/foreach} + {/capture} + {if $chatBoxRoomList|trim} +
    + {@$chatBoxRoomList} +
+ {else} +

{lang}chat.box.noRooms{/lang}

+ {/if} + + +
diff --git a/templates/boxRoomListSidebar.tpl b/templates/boxRoomListSidebar.tpl new file mode 100644 index 0000000..2d831e1 --- /dev/null +++ b/templates/boxRoomListSidebar.tpl @@ -0,0 +1,44 @@ +
+ {capture assign='chatBoxRoomList'} + {foreach from=$boxRoomList item='room'} + {if $room->canSee() && (!$skipEmptyRooms|isset || !$skipEmptyRooms || !$room->getUsers()|empty)} + roomID === $activeRoomID} class="active"{/if}> + {if $room->canJoin()} + + {else} + + {/if} + {$room->getTitle()} + {$room->getUsers()|count} + {if $room->canJoin()} + + {else} + + {/if} + + {/if} + {/foreach} + {/capture} + {if $chatBoxRoomList|trim} +
    + {@$chatBoxRoomList} +
+ {else} +
+ {/if} + + +
diff --git a/templates/errorDialog.tpl b/templates/errorDialog.tpl new file mode 100644 index 0000000..dfee1b1 --- /dev/null +++ b/templates/errorDialog.tpl @@ -0,0 +1,6 @@ + diff --git a/templates/infoCommandSuspensions.tpl b/templates/infoCommandSuspensions.tpl new file mode 100644 index 0000000..b40d693 --- /dev/null +++ b/templates/infoCommandSuspensions.tpl @@ -0,0 +1,38 @@ +{literal} +{if $message.payload.suspensions && $__window.Object.keys($message.payload.suspensions).length > 0} +
  • +
    +

    {lang}chat.messageType.be.bastelstu.chat.messageType.info.suspensions{/lang}

    +
    +
    +
    + + + + + + + + + {event name='columnHeads'} + + + + + {foreach from=$message.payload.suspensions item="suspension"} + + + + + + + {event name='columns'} + + {/foreach} + +
    {lang}chat.suspension.type{/lang}{lang}chat.suspension.judge{/lang}{lang}chat.suspension.room{/lang}{lang}chat.acp.suspension.time{/lang}{lang}chat.acp.suspension.expires{/lang}
    {lang}chat.suspension.type.{$suspension.objectType}{/lang}{$suspension.judge}{if $suspension.roomID !== null}{$suspension.room.title}{else}–{/if}{@$suspension.timeElement}{if $suspension.expires !== null}{@$suspension.expiresElement}{else}{lang}chat.acp.suspension.expires.forever{/lang}{/if}
    +
    +
    +
  • +{/if} +{/literal} diff --git a/templates/infoCommandSuspensionsDecorator.tpl b/templates/infoCommandSuspensionsDecorator.tpl new file mode 100644 index 0000000..5ea7389 --- /dev/null +++ b/templates/infoCommandSuspensionsDecorator.tpl @@ -0,0 +1,29 @@ +const infoCommandSuspensionDecorator = new Promise((resolve, reject) => { + require([ 'Bastelstu.be/Chat/Helper' ], Helper => { + chat.bottle.decorator('MessageType.be-bastelstu-chat-messageType-info', messageType => { + messageType.addDecorator(payload => { + if (payload.suspensions) { + payload.suspensions = payload.suspensions.map(suspension => { + suspension = Object.assign({ }, suspension) + + suspension.timeElement = Helper.getTimeElementHTML(new Date(suspension.time * 1000)) + + if (suspension.expires) { + suspension.expiresElement = Helper.getTimeElementHTML(new Date(suspension.expires * 1000)) + } + + return suspension + }) + } + + return payload + }) + + return messageType + }) + + resolve() + }, reject) +}) + +promises.add(infoCommandSuspensionDecorator) diff --git a/templates/log.tpl b/templates/log.tpl new file mode 100644 index 0000000..2aac7b7 --- /dev/null +++ b/templates/log.tpl @@ -0,0 +1,104 @@ +{include file='header'} + +{capture assign='sidebarRight'} +
    +
    +

    {lang}chat.log.jumpToDate{/lang}

    + +
    +
    +
    +
    + + {@SECURITY_TOKEN_INPUT_TAG} +
    +
    + +
    + +
    +
    +
    +
    + +
    +

    {lang}wcf.acp.box.boxController.be.bastelstu.chat.roomList{/lang}

    + +
    +
      + {foreach from=$roomList item='_room'} + {if $_room->canSee() && $_room->canSeeLog()} + roomID === $_room->roomID} class="active"{/if}> + + {$_room->getTitle()} + + + {/if} + {/foreach} +
    +
    +
    +{/capture} + + +
    +
    + +
    + +
    +
      +
    +
    +
    + +{include file='errorDialog' application='chat'} +{include file='messageTypes' application='chat'} + + + +{include file='footer'} diff --git a/templates/messageTypes.tpl b/templates/messageTypes.tpl new file mode 100644 index 0000000..12f0ab2 --- /dev/null +++ b/templates/messageTypes.tpl @@ -0,0 +1,481 @@ + + +{literal} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +{/literal} +{event name='messageTypes'} diff --git a/templates/quickSettings.tpl b/templates/quickSettings.tpl new file mode 100644 index 0000000..83da260 --- /dev/null +++ b/templates/quickSettings.tpl @@ -0,0 +1,9 @@ + diff --git a/templates/room.tpl b/templates/room.tpl new file mode 100644 index 0000000..4c31354 --- /dev/null +++ b/templates/room.tpl @@ -0,0 +1,147 @@ +{assign var='pageTitle' value=$room->getTitle()} + +{capture assign='sidebarRight'} +
    +

    {lang}chat.room.userList{/lang}

    + +
    +
      +
      +
      +{/capture} + +{capture assign='headerNavigation'} + {if $room->canSeeLog()} +
    • + + + +
    • + {/if} +
    • + + + +
    • +{/capture} + +{include file='header'} + +{if $room->getTopic()} +
      + + {@$room->getTopic()} +
      +{/if} + +
      +
      + +
      + {lang}chat.stream.activity{/lang} +
      +
      + +
      +
        +
      +
      +
      + +
      +
      + + + + +
      + + +
      + +{assign var=smileyCategories value=$__wcf->getSmileyCache()->getVisibleCategories()} +{include file='quickSettings' application='chat'} +{include file='smileyPicker' application='chat'} + +{include file='errorDialog' application='chat'} +{include file='messageTypes' application='chat'} +{include file='userList' application='chat'} +{include file='userListDropdownMenuItems' application='chat'} + +{if !ENABLE_DEBUG_MODE}{js application='wcf' file='Bastelstu.be.Chat'}{/if} + + +{include file='footer'} diff --git a/templates/roomList.tpl b/templates/roomList.tpl new file mode 100644 index 0000000..9a8262b --- /dev/null +++ b/templates/roomList.tpl @@ -0,0 +1,9 @@ +{include file='header'} +
      +
      +
      + {include application='chat' file='boxRoomList' boxRoomList=$rooms} +
      +
      +
      +{include file='footer'} diff --git a/templates/smileyPicker.tpl b/templates/smileyPicker.tpl new file mode 100644 index 0000000..f1d70e1 --- /dev/null +++ b/templates/smileyPicker.tpl @@ -0,0 +1,6 @@ +{if MODULE_SMILEY && !$smileyCategories|empty} + +{/if} \ No newline at end of file diff --git a/templates/temproomCommandLanguage.tpl b/templates/temproomCommandLanguage.tpl new file mode 100644 index 0000000..8e66589 --- /dev/null +++ b/templates/temproomCommandLanguage.tpl @@ -0,0 +1,5 @@ +Language.addObject({ + 'chat.messageType.be.bastelstu.chat.messageType.temproomCreated': '{lang __literal=true}chat.messageType.be.bastelstu.chat.messageType.temproomCreated{/lang}', + 'chat.messageType.be.bastelstu.chat.messageType.temproomInvited.invitee': '{lang __literal=true}chat.messageType.be.bastelstu.chat.messageType.temproomInvited.invitee{/lang}', + 'chat.messageType.be.bastelstu.chat.messageType.temproomInvited.invitor': '{lang __literal=true}chat.messageType.be.bastelstu.chat.messageType.temproomInvited.invitor{/lang}' +}) diff --git a/templates/temproomCommandMessageTypes.tpl b/templates/temproomCommandMessageTypes.tpl new file mode 100644 index 0000000..43f2f3b --- /dev/null +++ b/templates/temproomCommandMessageTypes.tpl @@ -0,0 +1,33 @@ +{literal} + + + +{/literal} diff --git a/templates/userList.tpl b/templates/userList.tpl new file mode 100644 index 0000000..2a3c916 --- /dev/null +++ b/templates/userList.tpl @@ -0,0 +1,27 @@ +{literal} + +{/literal} diff --git a/templates/userListDropdownMenuItems.tpl b/templates/userListDropdownMenuItems.tpl new file mode 100644 index 0000000..e782816 --- /dev/null +++ b/templates/userListDropdownMenuItems.tpl @@ -0,0 +1,20 @@ + diff --git a/templates/userListModerator.tpl b/templates/userListModerator.tpl new file mode 100644 index 0000000..953009d --- /dev/null +++ b/templates/userListModerator.tpl @@ -0,0 +1,3 @@ +{ldelim}if $user.permissions.canMute || $user.permissions.canBan} + +{ldelim}/if} diff --git a/userGroupOption.xml b/userGroupOption.xml new file mode 100644 index 0000000..78acaf8 --- /dev/null +++ b/userGroupOption.xml @@ -0,0 +1,144 @@ + + + + + + user + + + mod + + + admin.application + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..d99ce05 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1397 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +abbrev@1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" + +ansi-regex@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df" + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + +ansi-styles@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" + +anymatch@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" + dependencies: + micromatch "^2.1.5" + normalize-path "^2.0.0" + +aproba@^1.0.3: + version "1.2.0" + resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" + +are-we-there-yet@~1.1.2: + version "1.1.5" + resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-1.1.5.tgz#4b35c2944f062a8bfcda66410760350fe9ddfc21" + dependencies: + delegates "^1.0.0" + readable-stream "^2.0.6" + +arr-diff@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf" + dependencies: + arr-flatten "^1.0.1" + +arr-flatten@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/arr-flatten/-/arr-flatten-1.1.0.tgz#36048bbff4e7b47e136644316c99669ea5ae91f1" + +array-unique@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/array-unique/-/array-unique-0.2.1.tgz#a1d97ccafcbc2625cc70fadceb36a50c58b01a53" + +async-each@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" + +babel-cli@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-cli/-/babel-cli-6.26.0.tgz#502ab54874d7db88ad00b887a06383ce03d002f1" + dependencies: + babel-core "^6.26.0" + babel-polyfill "^6.26.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + commander "^2.11.0" + convert-source-map "^1.5.0" + fs-readdir-recursive "^1.0.0" + glob "^7.1.2" + lodash "^4.17.4" + output-file-sync "^1.1.2" + path-is-absolute "^1.0.1" + slash "^1.0.0" + source-map "^0.5.6" + v8flags "^2.1.1" + optionalDependencies: + chokidar "^1.6.1" + +babel-code-frame@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" + dependencies: + chalk "^1.1.3" + esutils "^2.0.2" + js-tokens "^3.0.2" + +babel-core@^6.26.0: + version "6.26.3" + resolved "https://registry.yarnpkg.com/babel-core/-/babel-core-6.26.3.tgz#b2e2f09e342d0f0c88e2f02e067794125e75c207" + dependencies: + babel-code-frame "^6.26.0" + babel-generator "^6.26.0" + babel-helpers "^6.24.1" + babel-messages "^6.23.0" + babel-register "^6.26.0" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + convert-source-map "^1.5.1" + debug "^2.6.9" + json5 "^0.5.1" + lodash "^4.17.4" + minimatch "^3.0.4" + path-is-absolute "^1.0.1" + private "^0.1.8" + slash "^1.0.0" + source-map "^0.5.7" + +babel-generator@^6.26.0: + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" + dependencies: + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + detect-indent "^4.0.0" + jsesc "^1.3.0" + lodash "^4.17.4" + source-map "^0.5.7" + trim-right "^1.0.1" + +babel-helper-builder-binary-assignment-operator-visitor@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-builder-binary-assignment-operator-visitor/-/babel-helper-builder-binary-assignment-operator-visitor-6.24.1.tgz#cce4517ada356f4220bcae8a02c2b346f9a56664" + dependencies: + babel-helper-explode-assignable-expression "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-call-delegate@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-call-delegate/-/babel-helper-call-delegate-6.24.1.tgz#ece6aacddc76e41c3461f88bfc575bd0daa2df8d" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-define-map@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-define-map/-/babel-helper-define-map-6.26.0.tgz#a5f56dab41a25f97ecb498c7ebaca9819f95be5f" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-explode-assignable-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-explode-assignable-expression/-/babel-helper-explode-assignable-expression-6.24.1.tgz#f25b82cf7dc10433c55f70592d5746400ac22caa" + dependencies: + babel-runtime "^6.22.0" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-function-name@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-function-name/-/babel-helper-function-name-6.24.1.tgz#d3475b8c03ed98242a25b48351ab18399d3580a9" + dependencies: + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-get-function-arity@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-get-function-arity/-/babel-helper-get-function-arity-6.24.1.tgz#8f7782aa93407c41d3aa50908f89b031b1b6853d" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-hoist-variables@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-hoist-variables/-/babel-helper-hoist-variables-6.24.1.tgz#1ecb27689c9d25513eadbc9914a73f5408be7a76" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-optimise-call-expression@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-optimise-call-expression/-/babel-helper-optimise-call-expression-6.24.1.tgz#f7a13427ba9f73f8f4fa993c54a97882d1244257" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-helper-regex@^6.24.1: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-helper-regex/-/babel-helper-regex-6.26.0.tgz#325c59f902f82f24b74faceed0363954f6495e72" + dependencies: + babel-runtime "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-helper-remap-async-to-generator@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-remap-async-to-generator/-/babel-helper-remap-async-to-generator-6.24.1.tgz#5ec581827ad723fecdd381f1c928390676e4551b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helper-replace-supers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helper-replace-supers/-/babel-helper-replace-supers-6.24.1.tgz#bf6dbfe43938d17369a213ca8a8bf74b6a90ab1a" + dependencies: + babel-helper-optimise-call-expression "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-helpers@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-helpers/-/babel-helpers-6.24.1.tgz#3471de9caec388e5c850e597e58a26ddf37602b2" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-messages@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-messages/-/babel-messages-6.23.0.tgz#f3cdf4703858035b2a2951c6ec5edf6c62f2630e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-check-es2015-constants@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-check-es2015-constants/-/babel-plugin-check-es2015-constants-6.22.0.tgz#35157b101426fd2ffd3da3f75c7d1e91835bbf8a" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-syntax-async-functions@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz#cad9cad1191b5ad634bf30ae0872391e0647be95" + +babel-plugin-syntax-exponentiation-operator@^6.8.0: + version "6.13.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz#9ee7e8337290da95288201a6a57f4170317830de" + +babel-plugin-syntax-trailing-function-commas@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-syntax-trailing-function-commas/-/babel-plugin-syntax-trailing-function-commas-6.22.0.tgz#ba0360937f8d06e40180a43fe0d5616fff532cf3" + +babel-plugin-transform-async-to-generator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-async-to-generator/-/babel-plugin-transform-async-to-generator-6.24.1.tgz#6536e378aff6cb1d5517ac0e40eb3e9fc8d08761" + dependencies: + babel-helper-remap-async-to-generator "^6.24.1" + babel-plugin-syntax-async-functions "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-arrow-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-arrow-functions/-/babel-plugin-transform-es2015-arrow-functions-6.22.0.tgz#452692cb711d5f79dc7f85e440ce41b9f244d221" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoped-functions@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoped-functions/-/babel-plugin-transform-es2015-block-scoped-functions-6.22.0.tgz#bbc51b49f964d70cb8d8e0b94e820246ce3a6141" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-block-scoping@^6.23.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-block-scoping/-/babel-plugin-transform-es2015-block-scoping-6.26.0.tgz#d70f5299c1308d05c12f463813b0a09e73b1895f" + dependencies: + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + lodash "^4.17.4" + +babel-plugin-transform-es2015-classes@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-classes/-/babel-plugin-transform-es2015-classes-6.24.1.tgz#5a4c58a50c9c9461e564b4b2a3bfabc97a2584db" + dependencies: + babel-helper-define-map "^6.24.1" + babel-helper-function-name "^6.24.1" + babel-helper-optimise-call-expression "^6.24.1" + babel-helper-replace-supers "^6.24.1" + babel-messages "^6.23.0" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-computed-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-computed-properties/-/babel-plugin-transform-es2015-computed-properties-6.24.1.tgz#6fe2a8d16895d5634f4cd999b6d3480a308159b3" + dependencies: + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-destructuring@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-destructuring/-/babel-plugin-transform-es2015-destructuring-6.23.0.tgz#997bb1f1ab967f682d2b0876fe358d60e765c56d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-duplicate-keys@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-duplicate-keys/-/babel-plugin-transform-es2015-duplicate-keys-6.24.1.tgz#73eb3d310ca969e3ef9ec91c53741a6f1576423e" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-for-of@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-for-of/-/babel-plugin-transform-es2015-for-of-6.23.0.tgz#f47c95b2b613df1d3ecc2fdb7573623c75248691" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-function-name@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-function-name/-/babel-plugin-transform-es2015-function-name-6.24.1.tgz#834c89853bc36b1af0f3a4c5dbaa94fd8eacaa8b" + dependencies: + babel-helper-function-name "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-literals/-/babel-plugin-transform-es2015-literals-6.22.0.tgz#4f54a02d6cd66cf915280019a31d31925377ca2e" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-modules-amd@^6.22.0, babel-plugin-transform-es2015-modules-amd@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-amd/-/babel-plugin-transform-es2015-modules-amd-6.24.1.tgz#3b3e54017239842d6d19c3011c4bd2f00a00d154" + dependencies: + babel-plugin-transform-es2015-modules-commonjs "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-commonjs@^6.23.0, babel-plugin-transform-es2015-modules-commonjs@^6.24.1: + version "6.26.2" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-commonjs/-/babel-plugin-transform-es2015-modules-commonjs-6.26.2.tgz#58a793863a9e7ca870bdc5a881117ffac27db6f3" + dependencies: + babel-plugin-transform-strict-mode "^6.24.1" + babel-runtime "^6.26.0" + babel-template "^6.26.0" + babel-types "^6.26.0" + +babel-plugin-transform-es2015-modules-systemjs@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-systemjs/-/babel-plugin-transform-es2015-modules-systemjs-6.24.1.tgz#ff89a142b9119a906195f5f106ecf305d9407d23" + dependencies: + babel-helper-hoist-variables "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-modules-umd@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-modules-umd/-/babel-plugin-transform-es2015-modules-umd-6.24.1.tgz#ac997e6285cd18ed6176adb607d602344ad38468" + dependencies: + babel-plugin-transform-es2015-modules-amd "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + +babel-plugin-transform-es2015-object-super@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-object-super/-/babel-plugin-transform-es2015-object-super-6.24.1.tgz#24cef69ae21cb83a7f8603dad021f572eb278f8d" + dependencies: + babel-helper-replace-supers "^6.24.1" + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-parameters@^6.23.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-parameters/-/babel-plugin-transform-es2015-parameters-6.24.1.tgz#57ac351ab49caf14a97cd13b09f66fdf0a625f2b" + dependencies: + babel-helper-call-delegate "^6.24.1" + babel-helper-get-function-arity "^6.24.1" + babel-runtime "^6.22.0" + babel-template "^6.24.1" + babel-traverse "^6.24.1" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-shorthand-properties@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-shorthand-properties/-/babel-plugin-transform-es2015-shorthand-properties-6.24.1.tgz#24f875d6721c87661bbd99a4622e51f14de38aa0" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-spread@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-spread/-/babel-plugin-transform-es2015-spread-6.22.0.tgz#d6d68a99f89aedc4536c81a542e8dd9f1746f8d1" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-sticky-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-sticky-regex/-/babel-plugin-transform-es2015-sticky-regex-6.24.1.tgz#00c1cdb1aca71112cdf0cf6126c2ed6b457ccdbc" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-plugin-transform-es2015-template-literals@^6.22.0: + version "6.22.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-template-literals/-/babel-plugin-transform-es2015-template-literals-6.22.0.tgz#a84b3450f7e9f8f1f6839d6d687da84bb1236d8d" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-typeof-symbol@^6.23.0: + version "6.23.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-typeof-symbol/-/babel-plugin-transform-es2015-typeof-symbol-6.23.0.tgz#dec09f1cddff94b52ac73d505c84df59dcceb372" + dependencies: + babel-runtime "^6.22.0" + +babel-plugin-transform-es2015-unicode-regex@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-es2015-unicode-regex/-/babel-plugin-transform-es2015-unicode-regex-6.24.1.tgz#d38b12f42ea7323f729387f18a7c5ae1faeb35e9" + dependencies: + babel-helper-regex "^6.24.1" + babel-runtime "^6.22.0" + regexpu-core "^2.0.0" + +babel-plugin-transform-exponentiation-operator@^6.22.0: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-exponentiation-operator/-/babel-plugin-transform-exponentiation-operator-6.24.1.tgz#2ab0c9c7f3098fa48907772bb813fe41e8de3a0e" + dependencies: + babel-helper-builder-binary-assignment-operator-visitor "^6.24.1" + babel-plugin-syntax-exponentiation-operator "^6.8.0" + babel-runtime "^6.22.0" + +babel-plugin-transform-regenerator@^6.22.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-regenerator/-/babel-plugin-transform-regenerator-6.26.0.tgz#e0703696fbde27f0a3efcacf8b4dca2f7b3a8f2f" + dependencies: + regenerator-transform "^0.10.0" + +babel-plugin-transform-strict-mode@^6.24.1: + version "6.24.1" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-strict-mode/-/babel-plugin-transform-strict-mode-6.24.1.tgz#d5faf7aa578a65bbe591cf5edae04a0c67020758" + dependencies: + babel-runtime "^6.22.0" + babel-types "^6.24.1" + +babel-polyfill@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-polyfill/-/babel-polyfill-6.26.0.tgz#379937abc67d7895970adc621f284cd966cf2153" + dependencies: + babel-runtime "^6.26.0" + core-js "^2.5.0" + regenerator-runtime "^0.10.5" + +babel-preset-env@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/babel-preset-env/-/babel-preset-env-1.7.0.tgz#dea79fa4ebeb883cd35dab07e260c1c9c04df77a" + dependencies: + babel-plugin-check-es2015-constants "^6.22.0" + babel-plugin-syntax-trailing-function-commas "^6.22.0" + babel-plugin-transform-async-to-generator "^6.22.0" + babel-plugin-transform-es2015-arrow-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoped-functions "^6.22.0" + babel-plugin-transform-es2015-block-scoping "^6.23.0" + babel-plugin-transform-es2015-classes "^6.23.0" + babel-plugin-transform-es2015-computed-properties "^6.22.0" + babel-plugin-transform-es2015-destructuring "^6.23.0" + babel-plugin-transform-es2015-duplicate-keys "^6.22.0" + babel-plugin-transform-es2015-for-of "^6.23.0" + babel-plugin-transform-es2015-function-name "^6.22.0" + babel-plugin-transform-es2015-literals "^6.22.0" + babel-plugin-transform-es2015-modules-amd "^6.22.0" + babel-plugin-transform-es2015-modules-commonjs "^6.23.0" + babel-plugin-transform-es2015-modules-systemjs "^6.23.0" + babel-plugin-transform-es2015-modules-umd "^6.23.0" + babel-plugin-transform-es2015-object-super "^6.22.0" + babel-plugin-transform-es2015-parameters "^6.23.0" + babel-plugin-transform-es2015-shorthand-properties "^6.22.0" + babel-plugin-transform-es2015-spread "^6.22.0" + babel-plugin-transform-es2015-sticky-regex "^6.22.0" + babel-plugin-transform-es2015-template-literals "^6.22.0" + babel-plugin-transform-es2015-typeof-symbol "^6.23.0" + babel-plugin-transform-es2015-unicode-regex "^6.22.0" + babel-plugin-transform-exponentiation-operator "^6.22.0" + babel-plugin-transform-regenerator "^6.22.0" + browserslist "^3.2.6" + invariant "^2.2.2" + semver "^5.3.0" + +babel-register@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-register/-/babel-register-6.26.0.tgz#6ed021173e2fcb486d7acb45c6009a856f647071" + dependencies: + babel-core "^6.26.0" + babel-runtime "^6.26.0" + core-js "^2.5.0" + home-or-tmp "^2.0.0" + lodash "^4.17.4" + mkdirp "^0.5.1" + source-map-support "^0.4.15" + +babel-runtime@^6.18.0, babel-runtime@^6.22.0, babel-runtime@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-runtime/-/babel-runtime-6.26.0.tgz#965c7058668e82b55d7bfe04ff2337bc8b5647fe" + dependencies: + core-js "^2.4.0" + regenerator-runtime "^0.11.0" + +babel-template@^6.24.1, babel-template@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-template/-/babel-template-6.26.0.tgz#de03e2d16396b069f46dd9fff8521fb1a0e35e02" + dependencies: + babel-runtime "^6.26.0" + babel-traverse "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + lodash "^4.17.4" + +babel-traverse@^6.24.1, babel-traverse@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-traverse/-/babel-traverse-6.26.0.tgz#46a9cbd7edcc62c8e5c064e2d2d8d0f4035766ee" + dependencies: + babel-code-frame "^6.26.0" + babel-messages "^6.23.0" + babel-runtime "^6.26.0" + babel-types "^6.26.0" + babylon "^6.18.0" + debug "^2.6.8" + globals "^9.18.0" + invariant "^2.2.2" + lodash "^4.17.4" + +babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: + version "6.26.0" + resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" + dependencies: + babel-runtime "^6.26.0" + esutils "^2.0.2" + lodash "^4.17.4" + to-fast-properties "^1.0.3" + +babylon@^6.18.0: + version "6.18.0" + resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3" + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + +binary-extensions@^1.0.0: + version "1.11.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.11.0.tgz#46aa1751fb6a2f93ee5e689bb1087d4b14c6c205" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^1.8.2: + version "1.8.5" + resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7" + dependencies: + expand-range "^1.8.1" + preserve "^0.2.0" + repeat-element "^1.1.2" + +browserslist@^3.2.6: + version "3.2.8" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-3.2.8.tgz#b0005361d6471f0f5952797a76fc985f1f978fc6" + dependencies: + caniuse-lite "^1.0.30000844" + electron-to-chromium "^1.3.47" + +buffer-from@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.0.tgz#87fcaa3a298358e0ade6e442cfce840740d1ad04" + +caniuse-lite@^1.0.30000844: + version "1.0.30000865" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000865.tgz#70026616e8afe6e1442f8bb4e1092987d81a2f25" + +chalk@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" + dependencies: + ansi-styles "^2.2.1" + escape-string-regexp "^1.0.2" + has-ansi "^2.0.0" + strip-ansi "^3.0.0" + supports-color "^2.0.0" + +chokidar@^1.6.1: + version "1.7.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.7.0.tgz#798e689778151c8076b4b360e5edd28cda2bb468" + dependencies: + anymatch "^1.3.0" + async-each "^1.0.0" + glob-parent "^2.0.0" + inherits "^2.0.1" + is-binary-path "^1.0.0" + is-glob "^2.0.0" + path-is-absolute "^1.0.0" + readdirp "^2.0.0" + optionalDependencies: + fsevents "^1.0.0" + +chownr@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.0.1.tgz#e2a75042a9551908bebd25b8523d5f9769d79181" + +code-point-at@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" + +commander@^2.11.0, commander@~2.16.0: + version "2.16.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.16.0.tgz#f16390593996ceb4f3eeb020b31d78528f7f8a50" + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + +console-control-strings@^1.0.0, console-control-strings@~1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" + +convert-source-map@^1.5.0, convert-source-map@^1.5.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" + +core-js@^2.4.0, core-js@^2.5.0: + version "2.5.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" + +core-util-is@~1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" + +debug@^2.1.2, debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +deep-extend@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.6.0.tgz#c4fa7c95404a17a9c3e8ca7e1537312b736330ac" + +delegates@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" + +detect-indent@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-4.0.0.tgz#f76d064352cdf43a1cb6ce619c4ee3a9475de208" + dependencies: + repeating "^2.0.0" + +detect-libc@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + +electron-to-chromium@^1.3.47: + version "1.3.52" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.52.tgz#d2d9f1270ba4a3b967b831c40ef71fb4d9ab5ce0" + +escape-string-regexp@^1.0.2: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + +esutils@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b" + +expand-brackets@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" + dependencies: + is-posix-bracket "^0.1.0" + +expand-range@^1.8.1: + version "1.8.2" + resolved "https://registry.yarnpkg.com/expand-range/-/expand-range-1.8.2.tgz#a299effd335fe2721ebae8e257ec79644fc85337" + dependencies: + fill-range "^2.1.0" + +extglob@^0.3.1: + version "0.3.2" + resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" + dependencies: + is-extglob "^1.0.0" + +filename-regex@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" + +fill-range@^2.1.0: + version "2.2.4" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-2.2.4.tgz#eb1e773abb056dcd8df2bfdf6af59b8b3a936565" + dependencies: + is-number "^2.1.0" + isobject "^2.0.0" + randomatic "^3.0.0" + repeat-element "^1.1.2" + repeat-string "^1.5.2" + +for-in@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" + +for-own@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/for-own/-/for-own-0.1.5.tgz#5265c681a4f294dabbf17c9509b6763aa84510ce" + dependencies: + for-in "^1.0.1" + +fs-minipass@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-1.2.5.tgz#06c277218454ec288df77ada54a03b8702aacb9d" + dependencies: + minipass "^2.2.1" + +fs-readdir-recursive@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz#e32fc030a2ccee44a6b5371308da54be0b397d27" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + +fsevents@^1.0.0: + version "1.2.4" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.4.tgz#f41dcb1af2582af3692da36fc55cbd8e1041c426" + dependencies: + nan "^2.9.2" + node-pre-gyp "^0.10.0" + +gauge@~2.7.3: + version "2.7.4" + resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" + dependencies: + aproba "^1.0.3" + console-control-strings "^1.0.0" + has-unicode "^2.0.0" + object-assign "^4.1.0" + signal-exit "^3.0.0" + string-width "^1.0.1" + strip-ansi "^3.0.1" + wide-align "^1.1.0" + +glob-base@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4" + dependencies: + glob-parent "^2.0.0" + is-glob "^2.0.0" + +glob-parent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-2.0.0.tgz#81383d72db054fcccf5336daa902f182f6edbb28" + dependencies: + is-glob "^2.0.0" + +glob@^7.0.5, glob@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^9.18.0: + version "9.18.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" + +graceful-fs@^4.1.2, graceful-fs@^4.1.4: + version "4.1.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658" + +has-ansi@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91" + dependencies: + ansi-regex "^2.0.0" + +has-unicode@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" + +home-or-tmp@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/home-or-tmp/-/home-or-tmp-2.0.0.tgz#e36c3f2d2cae7d746a857e38d18d5f32a7882db8" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.1" + +iconv-lite@^0.4.4: + version "0.4.23" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.23.tgz#297871f63be507adcfbfca715d0cd0eed84e9a63" + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore-walk@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/ignore-walk/-/ignore-walk-3.0.1.tgz#a83e62e7d272ac0e3b551aaa82831a19b69f82f8" + dependencies: + minimatch "^3.0.4" + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.1, inherits@~2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ini@~1.3.0: + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" + +invariant@^2.2.2: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + dependencies: + loose-envify "^1.0.0" + +is-binary-path@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-1.0.1.tgz#75f16642b480f187a711c814161fd3a4a7655898" + dependencies: + binary-extensions "^1.0.0" + +is-buffer@^1.1.5: + version "1.1.6" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" + +is-dotfile@^1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" + +is-equal-shallow@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/is-equal-shallow/-/is-equal-shallow-0.1.3.tgz#2238098fc221de0bcfa5d9eac4c45d638aa1c534" + dependencies: + is-primitive "^2.0.0" + +is-extendable@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-extendable/-/is-extendable-0.1.1.tgz#62b110e289a471418e3ec36a617d472e301dfc89" + +is-extglob@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" + +is-finite@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz#ef9e31386f031a7f0d643af82fde50c457ef00cb" + dependencies: + number-is-nan "^1.0.0" + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + +is-glob@^2.0.0, is-glob@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863" + dependencies: + is-extglob "^1.0.0" + +is-number@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" + dependencies: + kind-of "^3.0.2" + +is-number@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-4.0.0.tgz#0026e37f5454d73e356dfe6564699867c6a7f0ff" + +is-posix-bracket@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" + +is-primitive@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" + +isarray@1.0.0, isarray@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + +isobject@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/isobject/-/isobject-2.1.0.tgz#f065561096a3f1da2ef46272f815c840d87e0c89" + dependencies: + isarray "1.0.0" + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + +js-tokens@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" + +jsesc@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" + +jsesc@~0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" + +json5@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821" + +kind-of@^3.0.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-3.2.2.tgz#31ea21a734bab9bbb0f32466d893aea51e4a3c64" + dependencies: + is-buffer "^1.1.5" + +kind-of@^6.0.0: + version "6.0.2" + resolved "https://registry.yarnpkg.com/kind-of/-/kind-of-6.0.2.tgz#01146b36a6218e64e58f3a8d66de5d7fc6f6d051" + +lodash@^4.17.4: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + +loose-envify@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +math-random@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/math-random/-/math-random-1.0.1.tgz#8b3aac588b8a66e4975e3cdea67f7bb329601fac" + +micromatch@^2.1.5: + version "2.3.11" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-2.3.11.tgz#86677c97d1720b363431d04d0d15293bd38c1565" + dependencies: + arr-diff "^2.0.0" + array-unique "^0.2.1" + braces "^1.8.2" + expand-brackets "^0.1.4" + extglob "^0.3.1" + filename-regex "^2.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.1" + kind-of "^3.0.2" + normalize-path "^2.0.1" + object.omit "^2.0.0" + parse-glob "^3.0.4" + regex-cache "^0.4.2" + +minimatch@^3.0.2, minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + +minimist@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284" + +minipass@^2.2.1, minipass@^2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.3.3.tgz#a7dcc8b7b833f5d368759cce544dccb55f50f233" + dependencies: + safe-buffer "^5.1.2" + yallist "^3.0.0" + +minizlib@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.1.0.tgz#11e13658ce46bc3a70a267aac58359d1e0c29ceb" + dependencies: + minipass "^2.2.1" + +mkdirp@^0.5.0, mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +nan@^2.9.2: + version "2.10.0" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" + +needle@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/needle/-/needle-2.2.1.tgz#b5e325bd3aae8c2678902fa296f729455d1d3a7d" + dependencies: + debug "^2.1.2" + iconv-lite "^0.4.4" + sax "^1.2.4" + +node-pre-gyp@^0.10.0: + version "0.10.3" + resolved "https://registry.yarnpkg.com/node-pre-gyp/-/node-pre-gyp-0.10.3.tgz#3070040716afdc778747b61b6887bf78880b80fc" + dependencies: + detect-libc "^1.0.2" + mkdirp "^0.5.1" + needle "^2.2.1" + nopt "^4.0.1" + npm-packlist "^1.1.6" + npmlog "^4.0.2" + rc "^1.2.7" + rimraf "^2.6.1" + semver "^5.3.0" + tar "^4" + +nopt@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.1.tgz#d0d4685afd5415193c8c7505602d0d17cd64474d" + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-path@^2.0.0, normalize-path@^2.0.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" + dependencies: + remove-trailing-separator "^1.0.1" + +npm-bundled@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/npm-bundled/-/npm-bundled-1.0.3.tgz#7e71703d973af3370a9591bafe3a63aca0be2308" + +npm-packlist@^1.1.6: + version "1.1.11" + resolved "https://registry.yarnpkg.com/npm-packlist/-/npm-packlist-1.1.11.tgz#84e8c683cbe7867d34b1d357d893ce29e28a02de" + dependencies: + ignore-walk "^3.0.1" + npm-bundled "^1.0.1" + +npmlog@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" + dependencies: + are-we-there-yet "~1.1.2" + console-control-strings "~1.1.0" + gauge "~2.7.3" + set-blocking "~2.0.0" + +number-is-nan@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/number-is-nan/-/number-is-nan-1.0.1.tgz#097b602b53422a522c1afb8790318336941a011d" + +object-assign@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + +object.omit@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/object.omit/-/object.omit-2.0.1.tgz#1a9c744829f39dbb858c76ca3579ae2a54ebd1fa" + dependencies: + for-own "^0.1.4" + is-extendable "^0.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + dependencies: + wrappy "1" + +os-homedir@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" + +os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + +osenv@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" + dependencies: + os-homedir "^1.0.0" + os-tmpdir "^1.0.0" + +output-file-sync@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/output-file-sync/-/output-file-sync-1.1.2.tgz#d0a33eefe61a205facb90092e826598d5245ce76" + dependencies: + graceful-fs "^4.1.4" + mkdirp "^0.5.1" + object-assign "^4.1.0" + +parse-glob@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" + dependencies: + glob-base "^0.3.0" + is-dotfile "^1.0.0" + is-extglob "^1.0.0" + is-glob "^2.0.0" + +path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + +preserve@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" + +private@^0.1.6, private@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" + +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +randomatic@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/randomatic/-/randomatic-3.0.0.tgz#d35490030eb4f7578de292ce6dfb04a91a128923" + dependencies: + is-number "^4.0.0" + kind-of "^6.0.0" + math-random "^1.0.1" + +rc@^1.2.7: + version "1.2.8" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed" + dependencies: + deep-extend "^0.6.0" + ini "~1.3.0" + minimist "^1.2.0" + strip-json-comments "~2.0.1" + +readable-stream@^2.0.2, readable-stream@^2.0.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.6.tgz#b11c27d88b8ff1fbe070643cf94b0c79ae1b0aaf" + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + +readdirp@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.1.0.tgz#4ed0ad060df3073300c48440373f72d1cc642d78" + dependencies: + graceful-fs "^4.1.2" + minimatch "^3.0.2" + readable-stream "^2.0.2" + set-immediate-shim "^1.0.1" + +regenerate@^1.2.1: + version "1.4.0" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" + +regenerator-runtime@^0.10.5: + version "0.10.5" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.10.5.tgz#336c3efc1220adcedda2c9fab67b5a7955a33658" + +regenerator-runtime@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" + +regenerator-transform@^0.10.0: + version "0.10.1" + resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd" + dependencies: + babel-runtime "^6.18.0" + babel-types "^6.19.0" + private "^0.1.6" + +regex-cache@^0.4.2: + version "0.4.4" + resolved "https://registry.yarnpkg.com/regex-cache/-/regex-cache-0.4.4.tgz#75bdc58a2a1496cec48a12835bc54c8d562336dd" + dependencies: + is-equal-shallow "^0.1.3" + +regexpu-core@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-2.0.0.tgz#49d038837b8dcf8bfa5b9a42139938e6ea2ae240" + dependencies: + regenerate "^1.2.1" + regjsgen "^0.2.0" + regjsparser "^0.1.4" + +regjsgen@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.2.0.tgz#6c016adeac554f75823fe37ac05b92d5a4edb1f7" + +regjsparser@^0.1.4: + version "0.1.5" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.1.5.tgz#7ee8f84dc6fa792d3fd0ae228d24bd949ead205c" + dependencies: + jsesc "~0.5.0" + +remove-trailing-separator@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz#c24bce2a283adad5bc3f58e0d48249b92379d8ef" + +repeat-element@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/repeat-element/-/repeat-element-1.1.2.tgz#ef089a178d1483baae4d93eb98b4f9e4e11d990a" + +repeat-string@^1.5.2: + version "1.6.1" + resolved "https://registry.yarnpkg.com/repeat-string/-/repeat-string-1.6.1.tgz#8dcae470e1c88abc2d600fff4a776286da75e637" + +repeating@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/repeating/-/repeating-2.0.1.tgz#5214c53a926d3552707527fbab415dbc08d06dda" + dependencies: + is-finite "^1.0.0" + +requirejs@^2.3.5: + version "2.3.5" + resolved "https://registry.yarnpkg.com/requirejs/-/requirejs-2.3.5.tgz#617b9acbbcb336540ef4914d790323a8d4b861b0" + +rimraf@^2.6.1: + version "2.6.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36" + dependencies: + glob "^7.0.5" + +safe-buffer@^5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + +sax@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" + +semver@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" + +set-blocking@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + +set-immediate-shim@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/set-immediate-shim/-/set-immediate-shim-1.0.1.tgz#4b2b1b27eb808a9f8dcc481a58e5e56f599f3f61" + +signal-exit@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + +slash@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" + +source-map-support@^0.4.15: + version "0.4.18" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.4.18.tgz#0286a6de8be42641338594e97ccea75f0a2c585f" + dependencies: + source-map "^0.5.6" + +source-map-support@~0.5.6: + version "0.5.6" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.6.tgz#4435cee46b1aab62b8e8610ce60f788091c51c13" + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.5.6, source-map@^0.5.7: + version "0.5.7" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" + +source-map@^0.6.0, source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +string-width@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" + dependencies: + code-point-at "^1.0.0" + is-fullwidth-code-point "^1.0.0" + strip-ansi "^3.0.0" + +"string-width@^1.0.2 || 2": + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + dependencies: + safe-buffer "~5.1.0" + +strip-ansi@^3.0.0, strip-ansi@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf" + dependencies: + ansi-regex "^2.0.0" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + dependencies: + ansi-regex "^3.0.0" + +strip-json-comments@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" + +supports-color@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" + +tar@^4: + version "4.4.4" + resolved "https://registry.yarnpkg.com/tar/-/tar-4.4.4.tgz#ec8409fae9f665a4355cc3b4087d0820232bb8cd" + dependencies: + chownr "^1.0.1" + fs-minipass "^1.2.5" + minipass "^2.3.3" + minizlib "^1.1.0" + mkdirp "^0.5.0" + safe-buffer "^5.1.2" + yallist "^3.0.2" + +terser@^3.8.1: + version "3.8.1" + resolved "https://registry.yarnpkg.com/terser/-/terser-3.8.1.tgz#cb70070ac9e0a71add169dfb63c0a64fca2738ac" + dependencies: + commander "~2.16.0" + source-map "~0.6.1" + source-map-support "~0.5.6" + +to-fast-properties@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" + +trim-right@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/trim-right/-/trim-right-1.0.1.tgz#cb2e1203067e0c8de1f614094b9fe45704ea6003" + +user-home@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" + +util-deprecate@~1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + +v8flags@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/v8flags/-/v8flags-2.1.1.tgz#aab1a1fa30d45f88dd321148875ac02c0b55e5b4" + dependencies: + user-home "^1.1.1" + +wide-align@^1.1.0: + version "1.1.3" + resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457" + dependencies: + string-width "^1.0.2 || 2" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + +yallist@^3.0.0, yallist@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.0.2.tgz#8452b4bb7e83c7c188d8041c1a837c773d6d8bb9"