From 317ee2946183ee0f7723ebc9ec19facf157885db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20D=C3=BCsterhus?= Date: Fri, 17 Aug 2018 00:30:59 +0200 Subject: [PATCH] Initial import --- .babelrc | 17 + .gitignore | 13 + LICENSE | 104 ++ Makefile | 43 + aclOption.xml | 48 + acpMenu.xml | 42 + acptemplates/__chatVersion.tpl | 5 + acptemplates/commandTriggerAdd.tpl | 72 + acptemplates/commandTriggerList.tpl | 86 + acptemplates/roomAdd.tpl | 113 ++ acptemplates/roomList.tpl | 81 + acptemplates/suspensionList.tpl | 192 +++ box.xml | 42 + chatCommand.xml | 103 ++ eventListener.xml | 166 ++ files/acp/be.bastelstu.chat_install.php | 21 + files/acp/be.bastelstu.chat_update.php | 35 + files/acp/global.php | 18 + files/acp/index.php | 16 + files/global.php | 16 + files/index.php | 16 + .../acp/form/CommandTriggerAddForm.class.php | 159 ++ .../acp/form/CommandTriggerEditForm.class.php | 112 ++ files/lib/acp/form/RoomAddForm.class.php | 215 +++ files/lib/acp/form/RoomEditForm.class.php | 117 ++ .../acp/page/CommandTriggerListPage.class.php | 55 + files/lib/acp/page/RoomListPage.class.php | 45 + .../lib/acp/page/SuspensionListPage.class.php | 225 +++ files/lib/data/command/Command.class.php | 51 + files/lib/data/command/CommandCache.class.php | 110 ++ .../lib/data/command/CommandEditor.class.php | 25 + files/lib/data/command/CommandList.class.php | 21 + .../lib/data/command/CommandTrigger.class.php | 54 + .../command/CommandTriggerAction.class.php | 30 + .../command/CommandTriggerEditor.class.php | 32 + .../data/command/CommandTriggerList.class.php | 20 + files/lib/data/message/Message.class.php | 59 + .../lib/data/message/MessageAction.class.php | 307 ++++ .../lib/data/message/MessageEditor.class.php | 25 + files/lib/data/message/MessageList.class.php | 21 + .../data/message/ViewableMessage.class.php | 63 + files/lib/data/room/Room.class.php | 262 ++++ files/lib/data/room/RoomAction.class.php | 376 +++++ files/lib/data/room/RoomCache.class.php | 66 + files/lib/data/room/RoomEditor.class.php | 33 + files/lib/data/room/RoomList.class.php | 25 + .../lib/data/suspension/Suspension.class.php | 111 ++ .../suspension/SuspensionAction.class.php | 68 + .../suspension/SuspensionEditor.class.php | 25 + .../data/suspension/SuspensionList.class.php | 22 + files/lib/data/user/User.class.php | 106 ++ files/lib/data/user/UserAction.class.php | 118 ++ files/lib/data/user/UserList.class.php | 25 + files/lib/page/LogPage.class.php | 170 ++ files/lib/page/RoomListPage.class.php | 61 + files/lib/page/RoomPage.class.php | 109 ++ files/lib/page/TConfiguredPage.class.php | 60 + files/lib/system/CHATCore.class.php | 38 + .../box/RoomListBoxController.class.php | 120 ++ .../builder/CommandCacheBuilder.class.php | 52 + .../builder/PermissionCacheBuilder.class.php | 61 + .../cache/builder/RoomCacheBuilder.class.php | 31 + .../cache/runtime/UserRuntimeCache.class.php | 25 + .../system/command/AbstractCommand.class.php | 76 + .../AbstractInputProcessedCommand.class.php | 84 + .../AbstractSuspensionCommand.class.php | 170 ++ .../AbstractUnsuspensionCommand.class.php | 162 ++ .../lib/system/command/AwayCommand.class.php | 87 + .../lib/system/command/BackCommand.class.php | 80 + files/lib/system/command/BanCommand.class.php | 94 ++ .../system/command/BroadcastCommand.class.php | 85 + .../lib/system/command/ColorCommand.class.php | 285 ++++ files/lib/system/command/ICommand.class.php | 76 + .../lib/system/command/InfoCommand.class.php | 88 ++ files/lib/system/command/MeCommand.class.php | 84 + .../lib/system/command/MuteCommand.class.php | 63 + .../lib/system/command/PlainCommand.class.php | 81 + files/lib/system/command/TNeedsUser.class.php | 53 + .../lib/system/command/TeamCommand.class.php | 86 + .../system/command/TemproomCommand.class.php | 136 ++ .../lib/system/command/UnbanCommand.class.php | 64 + .../system/command/UnmuteCommand.class.php | 63 + .../lib/system/command/WhereCommand.class.php | 74 + .../system/command/WhisperCommand.class.php | 83 + .../room/RoomFilledCondition.class.php | 46 + ...ronjobExecuteChatCleanUpListener.class.php | 40 + ...UpCronjobExecuteTemproomListener.class.php | 43 + .../InfoCommandSuspensionsListener.class.php | 61 + ...mActionGetUsersModeratorListener.class.php | 44 + .../listener/RoomCanJoinBanListener.class.php | 38 + .../RoomCanJoinUserLimitListener.class.php | 42 + .../RoomCanSeeTemproomListener.class.php | 44 + ...RoomCanWritePubliclyMuteListener.class.php | 38 + .../RoomEditFormTemproomListener.class.php | 29 + .../RoomListPageTemproomListener.class.php | 27 + ...spensionListPageTemproomListener.class.php | 31 + .../message/type/AwayMessageType.class.php | 80 + .../message/type/BackMessageType.class.php | 80 + .../type/BroadcastMessageType.class.php | 89 ++ .../type/ChatUpdateMessageType.class.php | 54 + .../message/type/ColorMessageType.class.php | 63 + .../type/IDeletableMessageType.class.php | 33 + .../message/type/IMessageType.class.php | 74 + .../message/type/InfoMessageType.class.php | 30 + .../message/type/JoinMessageType.class.php | 30 + .../message/type/LeaveMessageType.class.php | 30 + .../message/type/MeMessageType.class.php | 39 + .../message/type/PlainMessageType.class.php | 78 + .../message/type/SuspendMessageType.class.php | 88 ++ .../message/type/TCanSeeCreator.class.php | 67 + .../message/type/TCanSeeInSameRoom.class.php | 66 + .../message/type/TDefaultPayload.class.php | 37 + .../message/type/TeamMessageType.class.php | 89 ++ .../type/TemproomCreatedMessageType.class.php | 30 + .../type/TemproomInvitedMessageType.class.php | 95 ++ .../type/TombstoneMessageType.class.php | 63 + .../type/UnsuspendMessageType.class.php | 88 ++ .../message/type/WhereMessageType.class.php | 58 + .../message/type/WhisperMessageType.class.php | 113 ++ .../page/handler/LogPageHandler.class.php | 65 + .../handler/RoomListPageHandler.class.php | 47 + .../page/handler/RoomPageHandler.class.php | 68 + .../page/handler/TRoomPageHandler.class.php | 73 + .../permission/PermissionHandler.class.php | 133 ++ .../system/suspension/BanSuspension.class.php | 48 + .../system/suspension/ISuspension.class.php | 31 + .../suspension/MuteSuspension.class.php | 48 + .../style/be.bastelstu.chat.messageTypes.scss | 38 + files/style/be.bastelstu.chat.scss | 635 ++++++++ files_wcf/js/Bastelstu.be/Chat.js | 454 ++++++ .../js/Bastelstu.be/Chat/Autocompleter.js | 82 + files_wcf/js/Bastelstu.be/Chat/BoxRoomList.js | 88 ++ files_wcf/js/Bastelstu.be/Chat/Command.js | 57 + .../js/Bastelstu.be/Chat/Command/Away.js | 26 + .../js/Bastelstu.be/Chat/Command/Back.js | 26 + files_wcf/js/Bastelstu.be/Chat/Command/Ban.js | 22 + .../js/Bastelstu.be/Chat/Command/Broadcast.js | 22 + .../js/Bastelstu.be/Chat/Command/Color.js | 31 + .../js/Bastelstu.be/Chat/Command/Info.js | 42 + files_wcf/js/Bastelstu.be/Chat/Command/Me.js | 24 + .../js/Bastelstu.be/Chat/Command/Mute.js | 22 + .../js/Bastelstu.be/Chat/Command/Plain.js | 53 + .../js/Bastelstu.be/Chat/Command/Team.js | 22 + .../js/Bastelstu.be/Chat/Command/Temproom.js | 49 + .../js/Bastelstu.be/Chat/Command/Unban.js | 22 + .../js/Bastelstu.be/Chat/Command/Unmute.js | 22 + .../js/Bastelstu.be/Chat/Command/Where.js | 26 + .../js/Bastelstu.be/Chat/Command/Whisper.js | 46 + .../Bastelstu.be/Chat/Command/_Suspension.js | 104 ++ .../Chat/Command/_Unsuspension.js | 71 + .../js/Bastelstu.be/Chat/CommandHandler.js | 67 + .../Chat/DataStructure/EventEmitter.js | 54 + .../js/Bastelstu.be/Chat/DataStructure/LRU.js | 58 + .../Chat/DataStructure/RedBlackTree/Node.js | 136 ++ .../Chat/DataStructure/RedBlackTree/Tree.js | 132 ++ .../Chat/DataStructure/Throttle.js | 101 ++ files_wcf/js/Bastelstu.be/Chat/Helper.js | 378 +++++ .../js/Bastelstu.be/Chat/LocalStorage.js | 201 +++ .../Bastelstu.be/Chat/LocalStorageEmulator.js | 89 ++ files_wcf/js/Bastelstu.be/Chat/Log.js | 223 +++ files_wcf/js/Bastelstu.be/Chat/Message.js | 90 ++ files_wcf/js/Bastelstu.be/Chat/MessageType.js | 74 + .../js/Bastelstu.be/Chat/MessageType/Away.js | 48 + .../js/Bastelstu.be/Chat/MessageType/Back.js | 48 + .../Chat/MessageType/Broadcast.js | 24 + .../Chat/MessageType/ChatUpdate.js | 28 + .../js/Bastelstu.be/Chat/MessageType/Color.js | 37 + .../js/Bastelstu.be/Chat/MessageType/Info.js | 89 ++ .../js/Bastelstu.be/Chat/MessageType/Join.js | 28 + .../js/Bastelstu.be/Chat/MessageType/Leave.js | 28 + .../js/Bastelstu.be/Chat/MessageType/Me.js | 22 + .../js/Bastelstu.be/Chat/MessageType/Plain.js | 28 + .../Bastelstu.be/Chat/MessageType/Suspend.js | 44 + .../js/Bastelstu.be/Chat/MessageType/Team.js | 28 + .../Chat/MessageType/TemproomCreated.js | 22 + .../Chat/MessageType/TemproomInvited.js | 22 + .../Chat/MessageType/Tombstone.js | 58 + .../Chat/MessageType/Unsuspend.js | 24 + .../js/Bastelstu.be/Chat/MessageType/Where.js | 37 + .../Bastelstu.be/Chat/MessageType/Whisper.js | 53 + files_wcf/js/Bastelstu.be/Chat/Messenger.js | 73 + files_wcf/js/Bastelstu.be/Chat/ParseError.js | 26 + files_wcf/js/Bastelstu.be/Chat/Parser.js | 125 ++ .../js/Bastelstu.be/Chat/ProfileStore.js | 195 +++ files_wcf/js/Bastelstu.be/Chat/Room.js | 113 ++ files_wcf/js/Bastelstu.be/Chat/Template.js | 40 + files_wcf/js/Bastelstu.be/Chat/Ui.js | 23 + files_wcf/js/Bastelstu.be/Chat/Ui/AutoAway.js | 90 ++ files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js | 71 + .../Bastelstu.be/Chat/Ui/ConnectionWarning.js | 60 + .../js/Bastelstu.be/Chat/Ui/ErrorDialog.js | 38 + files_wcf/js/Bastelstu.be/Chat/Ui/Input.js | 139 ++ .../Chat/Ui/Input/Autocompleter.js | 72 + files_wcf/js/Bastelstu.be/Chat/Ui/Log.js | 35 + .../Chat/Ui/MessageActions/Delete.js | 80 + .../js/Bastelstu.be/Chat/Ui/MessageStream.js | 343 ++++ files_wcf/js/Bastelstu.be/Chat/Ui/Mobile.js | 70 + .../js/Bastelstu.be/Chat/Ui/Notification.js | 127 ++ .../js/Bastelstu.be/Chat/Ui/ReadMarker.js | 46 + files_wcf/js/Bastelstu.be/Chat/Ui/Settings.js | 33 + .../Chat/Ui/Settings/AutoscrollButton.js | 43 + .../Bastelstu.be/Chat/Ui/Settings/Button.js | 36 + .../Chat/Ui/Settings/FullscreenButton.js | 34 + .../Chat/Ui/Settings/NotificationsButton.js | 51 + .../Chat/Ui/Settings/SmiliesButton.js | 159 ++ .../Chat/Ui/Settings/ToggleButton.js | 82 + files_wcf/js/Bastelstu.be/Chat/Ui/Topic.js | 28 + .../Chat/Ui/UserActionDropdownHandler.js | 80 + .../Chat/Ui/UserActions/Action.js | 24 + .../Chat/Ui/UserActions/BanAction.js | 46 + .../Chat/Ui/UserActions/MuteAction.js | 46 + .../Chat/Ui/UserActions/WhisperAction.js | 41 + files_wcf/js/Bastelstu.be/Chat/Ui/UserList.js | 43 + files_wcf/js/Bastelstu.be/Chat/User.js | 93 ++ files_wcf/js/Bastelstu.be/Chat/console.js | 119 ++ ...CommandPackageInstallationPlugin.class.php | 172 ++ language/de.xml | 236 +++ language/en.xml | 236 +++ menuItem.xml | 11 + objectType.xml | 172 ++ objectTypeDefinition.xml | 19 + option.xml | 56 + package.json | 8 + package.xml | 113 ++ packageInstallationPlugin.xml | 6 + page.xml | 47 + require.build.js | 73 + sql/0001-chat1_room.sql | 5 + sql/0002-Default-Room.sql | 1 + sql/0003-chat1_room_to_user.sql | 6 + sql/0004-chat1_message.sql | 16 + sql/0005-chat1_room_to_user-FOREIGN_KEY.sql | 2 + sql/0006-chat1_room_to_user-Timestamps.sql | 5 + sql/0007-chat1_room_to_user_lastPull.sql | 1 + sql/0008-chat1_message-Username-Width.sql | 1 + sql/0009-chat1_command.sql | 9 + sql/0010-chat1_command_trigger.sql | 7 + sql/0011-chat1_session.sql | 12 + sql/0012-chat1_message-Nullroom.sql | 1 + sql/0013-chat1_session-Index.sql | 1 + sql/0014-chat1_message-Embedded-Objects.sql | 1 + sql/0015-chat1_user-Away.sql | 1 + ...0016-chat1_command_trigger-PRIMARY_KEY.sql | 3 + sql/0017-chat1_command-Unique-className.sql | 1 + sql/0018-wcf1_user-Color.sql | 2 + sql/0019-chat1_room-User-Limit.sql | 1 + sql/0019-chat1_suspension.sql | 25 + sql/0020-chat1_suspension-Revoked.sql | 2 + sql/0021-chat1_room-Temporary.sql | 4 + sql/0022-chat1_room_temporary_invite.sql | 8 + sql/0023-chat1_message-isDeleted.sql | 1 + sql/0024-chat1_room-topicUseHtml.sql | 1 + sql/0025-chat1_room-topic-text.sql | 1 + templateListener.xml | 62 + templates/__chatCopyright.tpl | 3 + templates/boxRoomList.tpl | 67 + templates/boxRoomListSidebar.tpl | 44 + templates/errorDialog.tpl | 6 + templates/infoCommandSuspensions.tpl | 38 + templates/infoCommandSuspensionsDecorator.tpl | 29 + templates/log.tpl | 104 ++ templates/messageTypes.tpl | 481 ++++++ templates/quickSettings.tpl | 9 + templates/room.tpl | 147 ++ templates/roomList.tpl | 9 + templates/smileyPicker.tpl | 6 + templates/temproomCommandLanguage.tpl | 5 + templates/temproomCommandMessageTypes.tpl | 33 + templates/userList.tpl | 27 + templates/userListDropdownMenuItems.tpl | 20 + templates/userListModerator.tpl | 3 + userGroupOption.xml | 144 ++ yarn.lock | 1397 +++++++++++++++++ 273 files changed, 20383 insertions(+) create mode 100644 .babelrc create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 Makefile create mode 100644 aclOption.xml create mode 100644 acpMenu.xml create mode 100644 acptemplates/__chatVersion.tpl create mode 100644 acptemplates/commandTriggerAdd.tpl create mode 100644 acptemplates/commandTriggerList.tpl create mode 100644 acptemplates/roomAdd.tpl create mode 100644 acptemplates/roomList.tpl create mode 100644 acptemplates/suspensionList.tpl create mode 100644 box.xml create mode 100644 chatCommand.xml create mode 100644 eventListener.xml create mode 100644 files/acp/be.bastelstu.chat_install.php create mode 100644 files/acp/be.bastelstu.chat_update.php create mode 100644 files/acp/global.php create mode 100644 files/acp/index.php create mode 100644 files/global.php create mode 100644 files/index.php create mode 100644 files/lib/acp/form/CommandTriggerAddForm.class.php create mode 100644 files/lib/acp/form/CommandTriggerEditForm.class.php create mode 100644 files/lib/acp/form/RoomAddForm.class.php create mode 100644 files/lib/acp/form/RoomEditForm.class.php create mode 100644 files/lib/acp/page/CommandTriggerListPage.class.php create mode 100644 files/lib/acp/page/RoomListPage.class.php create mode 100644 files/lib/acp/page/SuspensionListPage.class.php create mode 100644 files/lib/data/command/Command.class.php create mode 100644 files/lib/data/command/CommandCache.class.php create mode 100644 files/lib/data/command/CommandEditor.class.php create mode 100644 files/lib/data/command/CommandList.class.php create mode 100644 files/lib/data/command/CommandTrigger.class.php create mode 100644 files/lib/data/command/CommandTriggerAction.class.php create mode 100644 files/lib/data/command/CommandTriggerEditor.class.php create mode 100644 files/lib/data/command/CommandTriggerList.class.php create mode 100644 files/lib/data/message/Message.class.php create mode 100644 files/lib/data/message/MessageAction.class.php create mode 100644 files/lib/data/message/MessageEditor.class.php create mode 100644 files/lib/data/message/MessageList.class.php create mode 100644 files/lib/data/message/ViewableMessage.class.php create mode 100644 files/lib/data/room/Room.class.php create mode 100644 files/lib/data/room/RoomAction.class.php create mode 100644 files/lib/data/room/RoomCache.class.php create mode 100644 files/lib/data/room/RoomEditor.class.php create mode 100644 files/lib/data/room/RoomList.class.php create mode 100644 files/lib/data/suspension/Suspension.class.php create mode 100644 files/lib/data/suspension/SuspensionAction.class.php create mode 100644 files/lib/data/suspension/SuspensionEditor.class.php create mode 100644 files/lib/data/suspension/SuspensionList.class.php create mode 100644 files/lib/data/user/User.class.php create mode 100644 files/lib/data/user/UserAction.class.php create mode 100644 files/lib/data/user/UserList.class.php create mode 100644 files/lib/page/LogPage.class.php create mode 100644 files/lib/page/RoomListPage.class.php create mode 100644 files/lib/page/RoomPage.class.php create mode 100644 files/lib/page/TConfiguredPage.class.php create mode 100644 files/lib/system/CHATCore.class.php create mode 100644 files/lib/system/box/RoomListBoxController.class.php create mode 100644 files/lib/system/cache/builder/CommandCacheBuilder.class.php create mode 100644 files/lib/system/cache/builder/PermissionCacheBuilder.class.php create mode 100644 files/lib/system/cache/builder/RoomCacheBuilder.class.php create mode 100644 files/lib/system/cache/runtime/UserRuntimeCache.class.php create mode 100644 files/lib/system/command/AbstractCommand.class.php create mode 100644 files/lib/system/command/AbstractInputProcessedCommand.class.php create mode 100644 files/lib/system/command/AbstractSuspensionCommand.class.php create mode 100644 files/lib/system/command/AbstractUnsuspensionCommand.class.php create mode 100644 files/lib/system/command/AwayCommand.class.php create mode 100644 files/lib/system/command/BackCommand.class.php create mode 100644 files/lib/system/command/BanCommand.class.php create mode 100644 files/lib/system/command/BroadcastCommand.class.php create mode 100644 files/lib/system/command/ColorCommand.class.php create mode 100644 files/lib/system/command/ICommand.class.php create mode 100644 files/lib/system/command/InfoCommand.class.php create mode 100644 files/lib/system/command/MeCommand.class.php create mode 100644 files/lib/system/command/MuteCommand.class.php create mode 100644 files/lib/system/command/PlainCommand.class.php create mode 100644 files/lib/system/command/TNeedsUser.class.php create mode 100644 files/lib/system/command/TeamCommand.class.php create mode 100644 files/lib/system/command/TemproomCommand.class.php create mode 100644 files/lib/system/command/UnbanCommand.class.php create mode 100644 files/lib/system/command/UnmuteCommand.class.php create mode 100644 files/lib/system/command/WhereCommand.class.php create mode 100644 files/lib/system/command/WhisperCommand.class.php create mode 100644 files/lib/system/condition/room/RoomFilledCondition.class.php create mode 100644 files/lib/system/event/listener/HourlyCleanUpCronjobExecuteChatCleanUpListener.class.php create mode 100644 files/lib/system/event/listener/HourlyCleanUpCronjobExecuteTemproomListener.class.php create mode 100644 files/lib/system/event/listener/InfoCommandSuspensionsListener.class.php create mode 100644 files/lib/system/event/listener/RoomActionGetUsersModeratorListener.class.php create mode 100644 files/lib/system/event/listener/RoomCanJoinBanListener.class.php create mode 100644 files/lib/system/event/listener/RoomCanJoinUserLimitListener.class.php create mode 100644 files/lib/system/event/listener/RoomCanSeeTemproomListener.class.php create mode 100644 files/lib/system/event/listener/RoomCanWritePubliclyMuteListener.class.php create mode 100644 files/lib/system/event/listener/RoomEditFormTemproomListener.class.php create mode 100644 files/lib/system/event/listener/RoomListPageTemproomListener.class.php create mode 100644 files/lib/system/event/listener/SuspensionListPageTemproomListener.class.php create mode 100644 files/lib/system/message/type/AwayMessageType.class.php create mode 100644 files/lib/system/message/type/BackMessageType.class.php create mode 100644 files/lib/system/message/type/BroadcastMessageType.class.php create mode 100644 files/lib/system/message/type/ChatUpdateMessageType.class.php create mode 100644 files/lib/system/message/type/ColorMessageType.class.php create mode 100644 files/lib/system/message/type/IDeletableMessageType.class.php create mode 100644 files/lib/system/message/type/IMessageType.class.php create mode 100644 files/lib/system/message/type/InfoMessageType.class.php create mode 100644 files/lib/system/message/type/JoinMessageType.class.php create mode 100644 files/lib/system/message/type/LeaveMessageType.class.php create mode 100644 files/lib/system/message/type/MeMessageType.class.php create mode 100644 files/lib/system/message/type/PlainMessageType.class.php create mode 100644 files/lib/system/message/type/SuspendMessageType.class.php create mode 100644 files/lib/system/message/type/TCanSeeCreator.class.php create mode 100644 files/lib/system/message/type/TCanSeeInSameRoom.class.php create mode 100644 files/lib/system/message/type/TDefaultPayload.class.php create mode 100644 files/lib/system/message/type/TeamMessageType.class.php create mode 100644 files/lib/system/message/type/TemproomCreatedMessageType.class.php create mode 100644 files/lib/system/message/type/TemproomInvitedMessageType.class.php create mode 100644 files/lib/system/message/type/TombstoneMessageType.class.php create mode 100644 files/lib/system/message/type/UnsuspendMessageType.class.php create mode 100644 files/lib/system/message/type/WhereMessageType.class.php create mode 100644 files/lib/system/message/type/WhisperMessageType.class.php create mode 100644 files/lib/system/page/handler/LogPageHandler.class.php create mode 100644 files/lib/system/page/handler/RoomListPageHandler.class.php create mode 100644 files/lib/system/page/handler/RoomPageHandler.class.php create mode 100644 files/lib/system/page/handler/TRoomPageHandler.class.php create mode 100644 files/lib/system/permission/PermissionHandler.class.php create mode 100644 files/lib/system/suspension/BanSuspension.class.php create mode 100644 files/lib/system/suspension/ISuspension.class.php create mode 100644 files/lib/system/suspension/MuteSuspension.class.php create mode 100644 files/style/be.bastelstu.chat.messageTypes.scss create mode 100644 files/style/be.bastelstu.chat.scss create mode 100644 files_wcf/js/Bastelstu.be/Chat.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Autocompleter.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/BoxRoomList.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Away.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Back.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Ban.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Broadcast.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Color.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Info.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Me.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Mute.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Plain.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Team.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Temproom.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Unban.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Unmute.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Where.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/Whisper.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/_Suspension.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Command/_Unsuspension.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/CommandHandler.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/DataStructure/EventEmitter.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/DataStructure/LRU.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/DataStructure/RedBlackTree/Node.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/DataStructure/RedBlackTree/Tree.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/DataStructure/Throttle.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Helper.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/LocalStorage.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/LocalStorageEmulator.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Log.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Message.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Away.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Back.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Broadcast.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/ChatUpdate.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Color.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Info.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Join.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Leave.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Me.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Plain.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Suspend.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Team.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/TemproomCreated.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/TemproomInvited.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Tombstone.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Unsuspend.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Where.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/MessageType/Whisper.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Messenger.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/ParseError.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Parser.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/ProfileStore.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Room.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Template.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/AutoAway.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Chat.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/ConnectionWarning.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/ErrorDialog.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Input.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Input/Autocompleter.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Log.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/MessageActions/Delete.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/MessageStream.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Mobile.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Notification.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/ReadMarker.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Settings.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Settings/AutoscrollButton.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Settings/Button.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Settings/FullscreenButton.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Settings/NotificationsButton.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Settings/SmiliesButton.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Settings/ToggleButton.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/Topic.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/UserActionDropdownHandler.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/Action.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/BanAction.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/MuteAction.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/UserActions/WhisperAction.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/Ui/UserList.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/User.js create mode 100644 files_wcf/js/Bastelstu.be/Chat/console.js create mode 100644 files_wcf/lib/system/package/plugin/ChatCommandPackageInstallationPlugin.class.php create mode 100644 language/de.xml create mode 100644 language/en.xml create mode 100644 menuItem.xml create mode 100644 objectType.xml create mode 100644 objectTypeDefinition.xml create mode 100644 option.xml create mode 100644 package.json create mode 100644 package.xml create mode 100644 packageInstallationPlugin.xml create mode 100644 page.xml create mode 100644 require.build.js create mode 100644 sql/0001-chat1_room.sql create mode 100644 sql/0002-Default-Room.sql create mode 100644 sql/0003-chat1_room_to_user.sql create mode 100644 sql/0004-chat1_message.sql create mode 100644 sql/0005-chat1_room_to_user-FOREIGN_KEY.sql create mode 100644 sql/0006-chat1_room_to_user-Timestamps.sql create mode 100644 sql/0007-chat1_room_to_user_lastPull.sql create mode 100644 sql/0008-chat1_message-Username-Width.sql create mode 100644 sql/0009-chat1_command.sql create mode 100644 sql/0010-chat1_command_trigger.sql create mode 100644 sql/0011-chat1_session.sql create mode 100644 sql/0012-chat1_message-Nullroom.sql create mode 100644 sql/0013-chat1_session-Index.sql create mode 100644 sql/0014-chat1_message-Embedded-Objects.sql create mode 100644 sql/0015-chat1_user-Away.sql create mode 100644 sql/0016-chat1_command_trigger-PRIMARY_KEY.sql create mode 100644 sql/0017-chat1_command-Unique-className.sql create mode 100644 sql/0018-wcf1_user-Color.sql create mode 100644 sql/0019-chat1_room-User-Limit.sql create mode 100644 sql/0019-chat1_suspension.sql create mode 100644 sql/0020-chat1_suspension-Revoked.sql create mode 100644 sql/0021-chat1_room-Temporary.sql create mode 100644 sql/0022-chat1_room_temporary_invite.sql create mode 100644 sql/0023-chat1_message-isDeleted.sql create mode 100644 sql/0024-chat1_room-topicUseHtml.sql create mode 100644 sql/0025-chat1_room-topic-text.sql create mode 100644 templateListener.xml create mode 100644 templates/__chatCopyright.tpl create mode 100644 templates/boxRoomList.tpl create mode 100644 templates/boxRoomListSidebar.tpl create mode 100644 templates/errorDialog.tpl create mode 100644 templates/infoCommandSuspensions.tpl create mode 100644 templates/infoCommandSuspensionsDecorator.tpl create mode 100644 templates/log.tpl create mode 100644 templates/messageTypes.tpl create mode 100644 templates/quickSettings.tpl create mode 100644 templates/room.tpl create mode 100644 templates/roomList.tpl create mode 100644 templates/smileyPicker.tpl create mode 100644 templates/temproomCommandLanguage.tpl create mode 100644 templates/temproomCommandMessageTypes.tpl create mode 100644 templates/userList.tpl create mode 100644 templates/userListDropdownMenuItems.tpl create mode 100644 templates/userListModerator.tpl create mode 100644 userGroupOption.xml create mode 100644 yarn.lock 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"