diff --git a/app/Chat/AuthInterface.php b/app/Chat/AuthInterface.php new file mode 100644 index 0000000..8d8abea --- /dev/null +++ b/app/Chat/AuthInterface.php @@ -0,0 +1,17 @@ + + */ +interface AuthInterface +{ + public function attempt(); +} diff --git a/app/Chat/LinkInfo.php b/app/Chat/LinkInfo.php new file mode 100644 index 0000000..34f120b --- /dev/null +++ b/app/Chat/LinkInfo.php @@ -0,0 +1,81 @@ + + */ +class LinkInfo +{ + /** + * Types for $Type. + */ + const TYPES = [ + 'PLAIN' => 0, + 'META' => 1, + 'VIDEO' => 2, + 'AUDIO' => 3, + 'IMAGE' => 4, + 'EMBED' => 5, + ]; + + /** + * Modifiable url. + * @var string + */ + public $URL; + + /** + * Original url. + * @var string + */ + public $OriginalURL; + + /** + * Type (from const TYPES). + * @var int + */ + public $Type; + + /** + * Full image or thumbnail, depends on Type. + * @var string + */ + public $Image; + + /** + * Title/header text. + * @var string + */ + public $Title; + + /** + * Description text. + * @var string + */ + public $Description; + + /** + * The content type to assign if applicable. + * @var string + */ + public $ContentType; + + /** + * The width of an image if applicable. + * @var int + */ + public $Width; + + /** + * The height of an image if applicable. + * @var int + */ + public $Height; +} diff --git a/app/Chat/Settings.php b/app/Chat/Settings.php new file mode 100644 index 0000000..1919963 --- /dev/null +++ b/app/Chat/Settings.php @@ -0,0 +1,289 @@ + + */ +class Settings +{ + /** + * Protocol the chat will use. + * @var string + */ + public $protocol = 'TestRepeater'; + + /** + * Server address the chat will connect to. + * @var string + */ + public $server = null; + + /** + * Title to display on the window/tab. + * @var string + */ + public $title = 'Sakurako'; + + /** + * Location to redirect to when the authentication failed. + * @var string + */ + public $authRedir = null; + + /** + * Cookies to send to the server for authentication (in proper order). + * @var array + */ + public $authCookies = []; + + /** + * URL format for avatars, {0} gets replaced with the user's id and set to null to disable. + * @var string + */ + public $avatarUrl = null; + + /** + * URL format for profile links, works the same as avatars. + * @var string + */ + public $profileUrl = null; + + /** + * Enabling compact (classic) by default. + * @var bool + */ + public $compactView = false; + + /** + * Strobe the tab title on new message. + * @var bool + */ + public $flashTitle = true; + + /** + * Enabling browser notifications. + * @var bool + */ + public $enableNotifications = true; + + /** + * Words that trigger a notification separated with spaces. + * @var string + */ + public $notificationTriggers = ''; + + /** + * Show the contents of the message in the notification. + * @var bool + */ + public $notificationShowMessage = false; + + /** + * Enabling development mode (e.g. loading eruda). + * @var bool + */ + public $development = false; + + /** + * Default style. + * @var string + */ + public $style = 'dark'; + + /** + * Path to language files relative to the chat client's index. + * @var string + */ + public $languagePath = './languages/'; + + /** + * Default language file to use. + * @var string + */ + public $language = 'en-gb'; + + /** + * Available languages. + * @var array + */ + public $languages = [ + 'en-gb' => 'English', + ]; + + /** + * Formatting string to the timestamp, uses the PHP syntax. + * @var string + */ + public $dateTimeFormat = 'H:i:s'; + + /** + * Markup parser to use. + * @var string + */ + public $parser = 'WaterDown'; + + /** + * Enabling the markup parser. + * @var bool + */ + public $enableParser = true; + + /** + * Enabling emoticon parsing. + * @var bool + */ + public $enableEmoticons = true; + + /** + * Whether urls should be automatically detected in message. + * @var bool + */ + public $autoParseUrls = true; + + /** + * Whether the chat should embed url macros like image embedding. + * @var bool + */ + public $autoEmbed = true; + + /** + * Enabling automatically scrolling down when a new message is received. + * @var bool + */ + public $autoScroll = true; + + /** + * Enabling notification sounds. + * @var bool + */ + public $soundEnable = true; + + /** + * The volume percentage for sounds. + * @var int + */ + public $soundVolume = 80; + + /** + * The default sound pack. + * @var string + */ + public $soundPack = 'default'; + + /** + * Available sound packs. + * @var array + */ + public $soundPacks = [ + 'default' => 'Default', + ]; + + /** + * Enabling the user join sound. + * @var bool + */ + public $soundEnableJoin = true; + + /** + * Enabling the user leave sound. + * @var bool + */ + public $soundEnableLeave = true; + + /** + * Enabling the error sound. + * @var bool + */ + public $soundEnableError = true; + + /** + * Enabling the server broadcast sound. + * @var bool + */ + public $soundEnableServer = true; + + /** + * Enabling the incoming message sound. + * @var bool + */ + public $soundEnableIncoming = true; + + /** + * Enabling the outgoing message sound. + * @var bool + */ + public $soundEnableOutgoing = true; + + /** + * Enabling the private message sound. + * @var bool + */ + public $soundEnablePrivate = true; + + /** + * Enabling the forceful leave (kick/ban/etc) sound. + * @var bool + */ + public $soundEnableForceLeave = true; + + /** + * Whether to let the user confirm before closing the tab. + * @var bool + */ + public $closeTabConfirm = false; + + /** + * Emoticons to be loaded. + * @var array + */ + public $emoticons = []; + + /** + * Applies settings based on Sakura's configuration. + */ + public function loadStandard() + { + $this->protocol = config('chat.protocol'); + $this->server = config('chat.server'); + $this->title = config('chat.title'); + $this->authRedir = route('auth.login', null, true); + $cpfx = config('cookie.prefix'); + $this->authCookies = [ + "{$cpfx}id", + "{$cpfx}session", + ]; + $this->avatarUrl = route('file.avatar', '{0}', true); + $this->profileUrl = route('user.profile', '{0}', true); + $this->development = config('dev.show_errors'); + $this->languagePath = config('chat.language_path'); + $this->language = config('chat.language'); + $this->languages = config('chat.languages'); + $this->dateTimeFormat = config('chat.date_format'); + $this->parser = config('chat.parser'); + $this->soundPack = config('chat.sound_pack'); + $this->soundPacks = config('chat.sound_packs'); + } + + /** + * Adding an emoticon to the list. + * @param array $triggers + * @param string $image + * @param int $hierarchy + * @param bool $relativePath + */ + public function addEmoticon($triggers, $image, $hierarchy = 0, $relativePath = false) + { + $this->emoticons[] = [ + 'Text' => $triggers, + 'Image' => ($relativePath ? full_domain() : '') . $image, + 'Hierarchy' => $hierarchy, + ]; + } +} diff --git a/app/Chat/URLResolver.php b/app/Chat/URLResolver.php new file mode 100644 index 0000000..a352381 --- /dev/null +++ b/app/Chat/URLResolver.php @@ -0,0 +1,36 @@ + + */ +class URLResolver +{ + /** + * Resolves a url. + * @param string $protocol + * @param string $slashes + * @param string $authority + * @param string $host + * @param string $port + * @param string $path + * @param string $query + * @param string $hash + * @return LinkInfo + */ + public static function resolve($protocol, $slashes, $authority, $host, $port, $path, $query, $hash) + { + $url = "{$protocol}:{$slashes}{$authority}{$host}{$port}{$path}{$query}{$hash}"; + $info = new LinkInfo; + $info->URL = $info->OriginalURL = $url; + $info->Type = LinkInfo::TYPES['PLAIN']; + return $info; + } +} diff --git a/app/Controllers/ChatController.php b/app/Controllers/ChatController.php index 5f0a1c5..b263801 100644 --- a/app/Controllers/ChatController.php +++ b/app/Controllers/ChatController.php @@ -6,6 +6,16 @@ namespace Sakura\Controllers; +use Sakura\Chat\LinkInfo; +use Sakura\Chat\Settings; +use Sakura\Chat\URLResolver; +use Sakura\DB; +use Sakura\Perms; +use Sakura\Perms\Manage; +use Sakura\Perms\Site; +use Sakura\Session; +use Sakura\User; + /** * Chat related controller. * @package Sakura @@ -13,12 +23,20 @@ namespace Sakura\Controllers; */ class ChatController extends Controller { + /** + * Middlewares! + * @var array + */ + protected $middleware = [ + 'EnableCORS', + ]; + /** * Redirects the user to the chat client. */ public function redirect() { - return; + header('Location: ' . config('chat.webclient')); } /** @@ -26,7 +44,78 @@ class ChatController extends Controller * @return string */ public function settings() + { + $settings = new Settings; + $settings->loadStandard(); + + $emotes = DB::table('emoticons') + ->get(); + + foreach ($emotes as $emote) { + $settings->addEmoticon([$emote->emote_string], $emote->emote_path, 1, true); + } + + return $this->json($settings); + } + + /** + * Resolves urls. + * @return string + */ + public function resolve() + { + $data = json_decode(file_get_contents('php://input')); + $info = new LinkInfo; + + if (json_last_error() === JSON_ERROR_NONE) { + $info = URLResolver::resolve( + $data->Protocol ?? null, + $data->Slashes ?? null, + $data->Authority ?? null, + $data->Host ?? null, + $data->Port ?? null, + $data->Path ?? null, + $data->Query ?? null, + $data->Hash ?? null + ); + } + + return $info; + } + + /** + * Handles the authentication for a chat server. + * @return string + */ + public function auth() { return; } + + /** + * Legacy auth, for SockLegacy. Remove when the old chat server finally dies. + * @return string + */ + public function authLegacy() + { + $user = User::construct($_GET['arg1'] ?? null); + $session = new Session($_GET['arg2'] ?? null); + + if ($session->validate($user->id) + && !$user->permission(Site::DEACTIVATED) + && !$user->permission(Site::RESTRICTED)) { + $hierarchy = $user->hierarchy(); + $moderator = $user->permission(Manage::USE_MANAGE, Perms::MANAGE) ? 1 : 0; + $changeName = $user->permission(Site::CHANGE_USERNAME) ? 1 : 0; + $createChans = $user->permission(Site::MULTIPLE_GROUPS) ? 2 : ( + $user->permission(Site::CREATE_GROUP) ? 1 : 0 + ); + + // The single 0 in here is used to determine log access, which isn't supported by sakurako anymore since it + // required direct database access and the chat is databaseless now. + return "yes{$user->id}\n{$user->username}\n{$user->colour}\n{$hierarchy}\f{$moderator}\f0\f{$changeName}\f{$createChans}\f"; + } + + return "no"; + } } diff --git a/app/Middleware/EnableCORS.php b/app/Middleware/EnableCORS.php new file mode 100644 index 0000000..e579c51 --- /dev/null +++ b/app/Middleware/EnableCORS.php @@ -0,0 +1,31 @@ + + */ +class EnableCORS implements MiddlewareInterface +{ + /** + * Enables CORS. + */ + public function run() + { + if (isset($_SERVER['HTTP_ORIGIN'])) { + header("Access-Control-Allow-Origin: {$_SERVER['HTTP_ORIGIN']}"); + header('Access-Control-Allow-Credentials: true'); + header('Access-Control-Max-Age: 86400'); + header("Access-Control-Allow-Methods: GET, POST"); + if (isset($_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])) { + header("Access-Control-Allow-Headers: {$_SERVER['HTTP_ACCESS_CONTROL_REQUEST_HEADERS']}"); + } + } + } +} diff --git a/app/User.php b/app/User.php index 1f7d285..a99c9de 100644 --- a/app/User.php +++ b/app/User.php @@ -1079,4 +1079,16 @@ class User { return Template::exists($this->design) ? $this->design : config('general.design'); } + + /** + * Gets the user's proper (highest) hierarchy. + * @return int + */ + public function hierarchy() + { + return DB::table('ranks') + ->join('user_ranks', 'ranks.rank_id', '=', 'user_ranks.rank_id') + ->where('user_id', $this->id) + ->max('ranks.rank_hierarchy'); + } } diff --git a/config/config.example.ini b/config/config.example.ini index 17b5078..5d4e314 100644 --- a/config/config.example.ini +++ b/config/config.example.ini @@ -93,7 +93,7 @@ report_host = ; Mailing settings [mail] contact_address = sakura@localhost -signature = Sakura | http://localhost/ +signature = "Sakura | http://localhost/" ; SMTP settings [mail.smtp] @@ -208,3 +208,39 @@ mail['Administrator'] = sakura@localghost twit['smugwave'] = "Sakura's main developer" repo['Sakura'] = https://github.com/flashwave/sakura + +; Chat specific settings +[chat] +; Path to the webclient +webclient = http://localhost/chat/ + +; Protocol to use +protocol = Sock + +; Server address +server = ws://localhost + +; Window/tab title +title = Sakurako + +; Path to language files directory for the chat (relative to the chat's client) +language_path = ./languages/ + +; Available languages +languages[en-gb] = English +languages[nl-nl] = Nederlands + +; Default language +language = en-gb + +; Date formatting used for chat message (standard PHP format) +date_format = H:i:s + +; Markup parser to use +parser = WaterDown + +; Soundpacks +sound_packs[default] = Default + +; Default soundpack to use +sound_pack = default diff --git a/resources/assets/typescript/Sakura/Notifications.ts b/resources/assets/typescript/Sakura/Notifications.ts index 13548f1..01e6fb3 100644 --- a/resources/assets/typescript/Sakura/Notifications.ts +++ b/resources/assets/typescript/Sakura/Notifications.ts @@ -17,6 +17,13 @@ namespace Sakura Notifications.Start(); } + public static Delete(id: number): void + { + var deleter: AJAX = new AJAX; + deleter.SetUrl("/notifications/" + id + "/mark"); + deleter.Start(HTTPMethod.GET); + } + public static Poll(): void { this.Client.Start(HTTPMethod.GET); diff --git a/resources/assets/typescript/Yuuno/Notifications.ts b/resources/assets/typescript/Yuuno/Notifications.ts index 07027b9..d1f625b 100644 --- a/resources/assets/typescript/Yuuno/Notifications.ts +++ b/resources/assets/typescript/Yuuno/Notifications.ts @@ -49,7 +49,7 @@ namespace Yuuno Sakura.DOM.Append(inner, text); Sakura.DOM.Append(container, inner); - close.setAttribute('onclick', 'Yuuno.Notifications.CloseAlert(this.parentNode.id);'); + close.setAttribute('onclick', (alert.id ? 'Sakura.Notifications.Delete(' + alert.id + ');' : '') + 'Yuuno.Notifications.CloseAlert(this.parentNode.id)'); Sakura.DOM.Append(close, closeIcon); Sakura.DOM.Append(container, close); @@ -58,7 +58,12 @@ namespace Yuuno if (alert.timeout > 0) { setTimeout(() => { - Notifications.CloseAlert(id); + if (Sakura.DOM.ID(id)) { + if (alert.id) { + Sakura.Notifications.Delete(alert.id); + } + Notifications.CloseAlert(id); + } }, alert.timeout); } } diff --git a/routes.php b/routes.php index af58b19..0d42f69 100644 --- a/routes.php +++ b/routes.php @@ -79,9 +79,6 @@ Router::group(['before' => 'maintenance'], function () { 'mcptest' => 'manage.index', //'report' => 'report.something', //'osu' => 'eventual link to flashii team', - //'filehost' => '???', - //'fhscript' => '???', - //'fhmanager' => '???', 'everlastingness' => 'https://i.flash.moe/18661469927746.txt', 'fuckingdone' => 'https://i.flash.moe/18671469927761.txt', ]; @@ -119,8 +116,13 @@ Router::group(['before' => 'maintenance'], function () { Router::group(['prefix' => 'chat'], function () { Router::get('/redirect', 'ChatController@redirect', 'chat.redirect'); Router::get('/settings', 'ChatController@settings', 'chat.settings'); + Router::get('/auth', 'ChatController@auth', 'chat.auth'); + Router::get('/resolve', 'Chatcontroller@resolve', 'chat.resolve'); }); + // Authentication for the "old" chat + Router::get('/web/sock-auth.php', 'ChatController@authLegacy'); + // Forum Router::group(['prefix' => 'forum'], function () { // Post diff --git a/utility.php b/utility.php index 557ecff..7c985bf 100644 --- a/utility.php +++ b/utility.php @@ -23,9 +23,15 @@ function config($value) } // Alias for Router::route -function route($name, $args = null) +function route($name, $args = null, $full = false) { - return Router::route($name, $args); + return ($full ? full_domain() : '') . Router::route($name, $args); +} + +// Getting the full domain (+protocol) of the current host, only works for http +function full_domain() +{ + return 'http' . ($_SERVER['HTTPS'] ?? false ? 's' : '') . '://' . $_SERVER['HTTP_HOST']; } // Checking if a parameter is equal to session_id()