From ab1c6dc84356728294228aa404d31b85ef5d89ee Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Wed, 11 Oct 2023 18:21:30 +0300 Subject: [PATCH 001/134] Fix for deleting photos that don't have albums --- Web/Presenters/PhotosPresenter.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 0a8b87e4..aeb8ba1e 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -336,7 +336,10 @@ final class PhotosPresenter extends OpenVKPresenter if(is_null($this->user) || $this->user->id != $ownerId) $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + if(!is_null($album = $photo->getAlbum())) + $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + else + $redirect = "/id0"; $photo->isolate(); $photo->delete(); From e3311fbf972d03cf5d6f02d991be6407ed3dfe66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=BA=D1=80=D0=B5=D0=BC=D0=B5=D0=BD=D1=87=D1=83=D0=BA?= =?UTF-8?q?=D1=87=D0=B0?= <147275844+JBLHRD@users.noreply.github.com> Date: Mon, 16 Oct 2023 00:51:50 +0200 Subject: [PATCH 002/134] Locales: Update ukrainian localization (#993) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add localization uk.strings * Чорт забирай! * LanguageTool №1 * Чорт забирай! №2 --- locales/uk.strings | 311 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 309 insertions(+), 2 deletions(-) diff --git a/locales/uk.strings b/locales/uk.strings index 7ccfb367..b2b8a914 100644 --- a/locales/uk.strings +++ b/locales/uk.strings @@ -134,6 +134,10 @@ "updated_at" = "Оновлено $1"; "user_banned" = "На жаль, нам довелося заблокувати сторінку користувача $1."; "user_banned_comment" = "Коментар модератора:"; +"verified_page" = "Верифікована сторінка"; +"user_is_blocked" = "Користувача заблоковано"; +"before" = "до"; +"forever" = "назавжди"; /* Wall */ @@ -184,6 +188,7 @@ "nsfw_warning" = "Даний запис може містити контент 18+"; "report" = "Поскаржитися"; "attach" = "Прикріпити"; +"detach" = "Відкріпити"; "attach_photo" = "Прикріпити фото"; "attach_photo" = "Прикріпити відео"; "draw_graffiti" = "Намалювати графіті"; @@ -193,6 +198,8 @@ "version_incompatibility" = "Не вдалося показати це вкладення. Можливо, база даних несумісна з поточною версією OpenVK."; "graffiti" = "Графіті"; "reply" = "Відповісти"; +"post_is_ad" = "Цей пост було проплачено Держдепом США"; +"edited_short" = "ред."; /* Friends */ @@ -317,14 +324,21 @@ "search_group" = "Пошук групи"; "search_by_groups" = "Пошук за групами"; "search_group_desc" = "Тут Ви можете переглянути існуючи групи та обрати групу до вподоби."; +"group_banned" = "Спільнота $1 заблокована." /* Albums */ "create" = "Створити"; +"album" = "Альбом"; "albums" = "Альбоми"; +"photos" = "фотографій"; +"photo" = "Фотографія"; "create_album" = "Створити альбом"; "edit_album" = "Редагувати альбом"; +"edit_photo" = "Змінити фотографію"; "creating_album" = "Створення альбому"; +"delete_photo" = "Видалити фотографію"; +"sure_deleting_photo" = "Ви впевнені, що бажаєте видалити цю світлину?"; "upload_photo" = "Завантажити фотографію"; "photo" = "Фотографія"; "upload_button" = "Завантажити"; @@ -366,6 +380,39 @@ "upd_f" = "оновила фотографію на своїй сторінці"; "upd_g" = "оновило фотографію групи"; +"add_photos" = "Додати фотографії"; +"upload_picts" = "Завантажити фотографії"; +"end_uploading" = "Завершити завантаження"; +"photos_successfully_uploaded" = "Фотографії було завантажено"; +"click_to_go_to_album" = "Натисніть, щоб перейти до альбому."; +"error_uploading_photo" = "Помилка завантаження фотографії"; +"too_many_pictures" = "Не більше 10 фотографій"; + +"drag_files_here" = "Перетягніть файли сюди"; +"only_images_accepted" = "Файл \"$1\" не є зображенням"; +"max_filesize" = "Максимальний розмір файлу — $1 мегабайт"; + +"uploading_photos_from_computer" = "Завантаження фотографій з Вашого ПК"; +"supported_formats" = "Підтримувані формати зображень: JPG, PNG й GIF."; +"max_load_photos" = "Ви можете завантажити до 10 фотографій за один раз."; +"tip" = "Порада"; +"tip_ctrl" = "щоб обрати кілька фотографій одразу, утримуйте клавішу Ctrl під час вибору файлів в OS Windows або клавішу CMD у Mac OS."; +"album_poster" = "Обкладинка альбому"; +"select_photo" = "Оберіть фотографію"; +"upload_new_photo" = "Завантажте нову світлину"; + +"is_x_photos_zero" = "Усього 0 фотографій."; +"is_x_photos_one" = "Всього 1 фотографія."; +"is_x_photos_few" = "Всього $1 фотографій."; +"is_x_photos_many" = "Всього $1 фотографій."; +"is_x_photos_other" = "Всього $1 фотографій."; + +"all_photos" = "Всі фотографії"; +"error_uploading_photo" = "Помилка завантаження фотографії. Текст помилки: "; +"too_many_photos" = "Надто багато фотографій."; + +"photo_x_from_y" = "Фотографія $1 з $2"; + /* Notes */ "notes" = "Нотатки"; @@ -395,6 +442,18 @@ /* Notes: Article Viewer */ "aw_legacy_ui" = "Класичне дієвидло"; +"select_note" = "Вибір нотатки"; +"no_notes" = "Ви не маєте жодної нотатки"; + +"error_attaching_note" = "Не вдалося закріпити нотатку"; + +"select_or_create_new" = "Оберіть існуючу нотатку або створіть нову"; + +"notes_closed" = "Ви не можете прикріпити нотатку до запису, оскільки ваші нотатки видно тільки вам.

Ви можете змінити це в налаштуваннях."; +"do_not_attach_note" = "Не прикріплювати нотатку"; +"something" = "Щось"; +"supports_xhtml" = "з (X)HTML підтримується."; + /* Menus */ "edit_button" = "ред."; @@ -489,7 +548,7 @@ "privacy_value_friends" = "Друзі"; "privacy_value_friends_dative" = "Друзям"; "privacy_value_only_me" = "Тільки я"; -"privacy_value_only_me_dative" = "Тільки мені"; +"privacy_value_only_me_dative" = "Тільки мені та Кирилу Буданову"; "privacy_value_nobody" = "Ніхто"; "your_email_address" = "Адрес Вашої електронної пошти"; "your_page_address" = "Адрес Вашої сторінки"; @@ -581,6 +640,9 @@ "two_factor_authentication_backup_codes_1" = "Резервні коди дозволяють підтверджувати вхід, коли у вас немає доступу до телефону, наприклад, у подорожі."; "two_factor_authentication_backup_codes_2" = "У вас є ще 10 кодів, кожним кодом можна скористатися тільки один раз. Надрукуйте їх, приберіть в надійне місце і використовуйте, коли будуть потрібні коди для підтвердження входу."; "two_factor_authentication_backup_codes_3" = "Ви можете отримати нові коди, якщо вони закінчуються. Дійсні тільки останні створені резервні коди."; +"viewing_backup_codes" = "Перегляд резервних кодів"; +"disable_2fa" = "Вимкнути 2FA"; +"viewing" = "Переглянути"; /* Sorting */ @@ -606,6 +668,14 @@ "videos_many" = "$1 відеозаписів"; "videos_other" = "$1 відеозаписів"; "view_video" = "Перегляд"; +"change_video" = "Редагувати відеозапис"; +"unknown_video" = "ЦЕЙ ВІДЕОЗАПИС НЕ ПІДТРИМУЄТЬСЯ В ЦІЙ ВЕРСІЇ OPENVK."; +"selecting_video" = "Вибір відеозаписів"; +"upload_new_video" = "Завантажити нове відео"; +"max_attached_videos" = "Максимум 10 відеозаписів"; +"max_attached_photos" = "Максимум 10 фотографій"; +"no_videos" = "Ви не маєте відео."; +"no_videos_results" = "Немає результатів."; /* Notifications */ @@ -642,6 +712,7 @@ "nt_mention_in_video" = "в обговоренні відеозапису"; "nt_mention_in_note" = "в обговоренні під"; "nt_mention_in_topic" = "в обговоренні"; +"nt_sent_gift" = "відправив вам подарунок"; /* Time */ @@ -815,6 +886,7 @@ "support_new" = "Нове звернення"; "support_new_title" = "Введіть тему вашого звернення"; "support_new_content" = "Опишіть проблему чи пропозицію"; +"reports" = "Скарги"; "support_rate_good_answer" = "Це хороша відповідь"; "support_rate_bad_answer" = "Це погана відповідь"; "support_good_answer_user" = "Ви залишили позитивний відгук."; @@ -825,6 +897,26 @@ "support_rated_bad" = "Ви залишили негативний відгук про відповідь."; "wrong_parameters" = "Неправильні параметри запиту."; "fast_answers" = "Швидкі відповіді"; +"ignore_report" = "Ігнорувати скаргу"; +"report_number" = "Скарга №"; +"list_of_reports" = "Лист скарг"; +"text_of_the_post" = "Текст допису"; +"today" = "сьогодні"; + +"will_be_watched" = "Скоро її розглянуть модератори"; + +"report_question" = "Поскаржитись?"; +"report_question_text" = "Що саме Ви вважаєте неприпустимим у цьому матеріалі?"; +"report_reason" = "Причина скарги"; +"reason" = "Причина"; +"going_to_report_app" = "Ви збираєтеся поскаржитися на цей додаток."; +"going_to_report_club" = "Ви збираєтеся поскаржитися на цю спільноту."; +"going_to_report_photo" = "Ви збираєтеся поскаржитися на цю фотографію."; +"going_to_report_user" = "Ви збираєтеся поскаржитися на цього користувача."; +"going_to_report_video" = "Ви збираєтеся поскаржитися на цей відеозапис."; +"going_to_report_post" = "Ви збираєтеся поскаржитися на цей запис."; +"going_to_report_comment" = "Ви збираєтеся поскаржитися на цей коментар."; + "comment" = "Коментар"; "sender" = "Відправник"; "author" = "Автор"; @@ -834,6 +926,12 @@ "ticket_changed_comment" = "Зміни набудуть чинності через кілька секунд."; "banned_in_support_1" = "Вибачте, $1, але тепер вам не можна створювати звернення."; "banned_in_support_2" = "Підстава: $1. Цього разу нам довелося забрати у вас цю можливість назавжди."; +"you_can_close_this_ticket_1" = "Якщо ви не маєте запитань, Ви можете "; +"you_can_close_this_ticket_2" = "закрити це звернення"; +"agent_profile_created_1" = "Профіль створено"; +"agent_profile_created_2" = "Тепер користувачі бачать Ваш псевдонім і аватар замість стандартного поличчя та ID."; +"agent_profile_edited" = "Профіль відредагований"; +"agent_profile" = "Картка агента"; /* Invite */ @@ -958,6 +1056,7 @@ "error_repost_fail" = "Не вдалося поділитися записом"; "error_data_too_big" = "Атрибут '$1' не може бути довше $2 $3"; "forbidden" = "Помилка доступу"; +"unknown_error" = "Невідома помилка"; "forbidden_comment" = "Налаштування приватності цього користувача не дозволяють дивитися на його сторінку."; "changes_saved" = "Зміни збережені"; "changes_saved_comment" = "Нові дані з'являться на вашій сторінці"; @@ -993,6 +1092,96 @@ "media_file_corrupted_or_too_large" = "Файл медіаконтенту пошкоджений або файл занадто великий."; "post_is_empty_or_too_big" = "Пост порожній чи надто великий."; "post_is_too_big" = "Пост надто великий."; +"error_sending_report" = "Не вдалося подати скаргу..."; +"error_when_saving_gift" = "Не вдалося зберегти подарунок"; +"error_when_saving_gift_bad_image" = "Зображення подарунка пошкоджене."; +"error_when_saving_gift_no_image" = "Будь ласка, завантажте зображення подарунка."; +"video_uploads_disabled" = "Завантаження відео вимкнено адміністратором."; + +"error_when_publishing_comment" = "Не вдалося опублікувати коментар"; +"error_when_publishing_comment_description" = "Файл зображення пошкоджено, він занадто великий або один бік зображення в рази більший за інший."; +"error_comment_empty" = "Коментар порожній або занадто великий."; +"error_comment_too_big" = "Коментар занадто великий."; +"error_comment_file_too_big" = "Файл медіаконтенту пошкоджений або занадто великий."; + +"comment_is_added" = "Коментар додано"; +"comment_is_added_desc" = "Ваш коментар з'явиться на сторінці."; + +"error_access_denied_short" = "Помилка доступу"; +"error_access_denied" = "Ви не маєте права на редагування цього ресурсу"; +"success" = "Успіх"; +"comment_will_not_appear" = "Цей коментар більше не буде відображатися."; + +"error_when_gifting" = "Не вдалося подарувати"; +"error_user_not_exists" = "Користувач або набір не існують."; +"error_no_rights_gifts" = "Не вдалося підтвердити права на подарунок."; +"error_no_more_gifts" = "У вас більше не залишилось цих подарунків."; +"error_no_money" = "АХАХАХА ЛОШАРА ПІЗДУЙ НА ЗАРОБІТКИ У ПОЛЬЩУ"; +/* трррр шкібіді доп доп доп доп єс єс єс) */ + +"description_too_long" = "Опис надто довгий."; + +"gift_sent" = "Подарунок відправлено"; +"gift_sent_desc" = "Ви відправили $1 за $2 голосів"; + +"error_on_server_side" = "Виникла помилка на боці сервера. Зверніться до системного адміністратора."; +"error_no_group_name" = "Ви не ввели назву групи."; + +"success_action" = "Операція пройшла успішно"; +"connection_error" = "Помилка з'єднання"; +"connection_error_desc" = "Не вдалося з'єднатися до служби телеметрії"; + +"error_when_uploading_photo" = "Не вдалося зберегти фотографію."; + +"new_changes_desc" = "Нові дані з'являться у вашій групі."; +"comment_is_changed" = "Коментар до адміністратора змінено"; +"comment_is_deleted" = "Коментар до адміністратора видалено"; +"comment_is_too_long" = "Коментар надто довгий ($1 символів замість 36 символів)"; +"x_no_more_admin" = "$1 більше не є адміністратором."; +"x_is_admin" = "$1 призначено адміністратором."; + +"x_is_now_hidden" = "Тепер $1 буде відображатися як звичайний підписник усім, окрім інших адміністраторів"; +"x_is_now_showed" = "Тепер $1 буде відображатися як звичайний адміністратор."; + +"note_is_deleted" = "Нотатка видалена"; +"note_x_is_now_deleted" = "Нотатка \"$1\" була успішно видалена."; +"new_data_accepted" = "Нові дані прийняті."; + +"album_is_deleted" = "Альбом видалено"; +"album_x_is_deleted" = "Альбом $1 було видалено."; + +"error_adding_to_deleted" = "Не вдалося зберегти фотографію у DELETED."; +"error_adding_to_x" = "Не вдалося зберегти фотографію в $1."; +"no_photo" = "Нема фотографій"; + +"select_file" = "Оберіть файл"; +"new_description_will_appear" = "Оновлений опис з'явиться на сторінці з фото."; +"photo_is_deleted" = "Фотографія видалена"; +"photo_is_deleted_desc" = "Ця світлина була успішно видалена."; + +"no_video" = "Немає відеозапису"; +"no_video_desc" = "Оберіть файл або вкажіть URL."; +"error_occured" = "Виникла помилка"; +"error_video_damaged_file" = "Файл пошкоджений або не має відеозапису."; +"error_video_incorrect_link" = "Вірогідно, посилання некоректне."; +"error_video_no_title" = "Відео не може бути опубліковано без назви."; + +"new_data_video" = "Оновлений опис з'явиться на сторінці з відео."; +"error_deleting_video" = "Не вдалося видалити відео"; +"login_please" = "Ви не увійшли в аккаунт."; +"invalid_code" = "Не вдалося підтвердити номер телефону: неправильний код."; + +"error_max_pinned_clubs" = "Знаходитись у лівому меню можуть максимум 10 спільнот"; +"error_viewing_subs" = "Ви не можете переглядати лист підписок $1."; +"error_status_too_long" = "Статус надто довгий ($1 символів замість 255 символів)"; +"death" = "Сміерць..."; +"nehay" = "Няхай жыве!"; +"user_successfully_banned" = "Користувача успішно заблоковано."; + +"content_is_deleted" = "Коментар видалено, а користувач отримав попередження."; +"report_is_ignored" = "Скаргу проігноровано."; +"group_owner_is_banned" = "Творець спільноти успішно заблоковано."; +"group_is_banned" = "Спільноту успішно заблоковано"; /* Admin actions */ @@ -1000,14 +1189,18 @@ "manage_user_action" = "Керування користувачем"; "manage_group_action" = "Керування групою"; "ban_user_action" = "Заблокувати користувача"; +"blocks" = "Блокування"; +"last_actions" = "Останні дії"; "unban_user_action" = "Розблокувати користувача"; "warn_user_action" = "Попередити користувача"; "ban_in_support_user_action" = "Заблокувати у тех.підтримці"; "unban_in_support_user_action" = "Розблокувати у тех.підтримці"; +"changes_history" = "Історія редагування"; /* Admin panel */ "admin" = "Адмін панель"; +"sandbox_for_developers" = "Sandbox для розробників"; "admin_ownerid" = "ID власника"; "admin_author" = "Автор"; "admin_name" = "Ім'я"; @@ -1082,6 +1275,40 @@ "admin_banned_link_initiator" = "Ініціатор"; "admin_banned_link_not_specified" = "Посилання не зазначено"; "admin_banned_link_not_found" = "Посилання не знайдено"; +"admin_gift_moved_successfully" = "Подарунок успішно переміщено"; +"admin_gift_moved_to_recycle" = "Тепер подарунок у кошику."; +"logs" = "Логи"; +"logs_anything" = "Будь-яке"; +"logs_adding" = "Створення"; +"logs_editing" = "Редагування"; +"logs_removing" = "Видалення"; +"logs_restoring" = "Відновлення"; +"logs_added" = "додав"; +"logs_edited" = "відредагував"; +"logs_removed" = "видалив"; +"logs_restored" = "відновив"; +"logs_id_post" = "ID допису"; +"logs_id_object" = "ID об'єкту"; +"logs_uuid_user" = "UUID користувача"; +"logs_change_type" = "Тип зміни"; +"logs_change_object" = "Тип об'єкта"; + +"logs_user" = "Користувач"; +"logs_object" = "Об'єкт"; +"logs_type" = "Тип"; +"logs_changes" = "Зміни"; +"logs_time" = "Час"; + +"bans_history" = "Історія блокувань"; +"bans_history_blocked" = "Заблоковано"; +"bans_history_initiator" = "Ініціатор"; +"bans_history_start" = "Початок"; +"bans_history_end" = "Кінець"; +"bans_history_time" = "Час"; +"bans_history_reason" = "Причина"; +"bans_history_start" = "Початок"; +"bans_history_removed" = "Знята"; +"bans_history_active" = "Активне блокування"; /* Paginator (deprecated) */ @@ -1130,7 +1357,9 @@ "transfer" = "Передати"; "close" = "Закрити"; "warning" = "Увага"; -"question_confirm" = "Цю дію не можна скасувати. Ви дійсно впевнені, що хочете зробити?"; +"question_confirm" = "Цю дію не можна скасувати. Ви переконані що хочете це зробити?"; +"confirm_m" = "Підтвердити"; +"action_successfully" = "Операція виконана успішно"; /* User alerts */ @@ -1144,6 +1373,8 @@ /* Away */ +"transition_is_blocked" = "Перехід за посиланням заборонений"; +"caution" = "Попередження"; "url_is_banned" = "Перехід неможливий"; "url_is_banned_comment" = "Адміністрація $1 не рекомендує переходити за цим посиланням."; "url_is_banned_comment_r" = "Адміністрація $1 не рекомендує переходити за цим посиланням.

Підстава: $2"; @@ -1151,6 +1382,8 @@ "url_is_banned_title" = "Посилання на підозрілий сайт"; "url_is_banned_proceed" = "Перейти за посиланням"; +"recently" = "Нещодавно"; + /* Chandler */ "c_user_removed_from_group" = "Користувача було видалено з групи"; @@ -1422,6 +1655,43 @@ "closed_group_post" = "Цей допис з приватної групи"; "deleted_target_comment" = "Цей коментар належить до видаленого допису"; +"no_results" = "Немає результатів"; + +/* BadBrowser */ + +"deprecated_browser" = "Застарілий браузер"; +"deprecated_browser_description" = "Для перегляду цього контенту вам необхідний >Firefox ESR 52 або еквівалент по функціоналу навігатор по всесвітньою мережею інтернет. Співчуваємо про це."; + +/* Statistics */ + +"coverage" = "Обхват"; +"coverage_this_week" = "Цей графік відображає обхват за останні 7 днів."; +"views" = "Перегляди"; +"views_this_week" = "Цей графік відображає перегляди дописів спільноти за остані 7 днів."; + +"full_coverage" = "Повний обхват"; +"all_views" = "Усі перегляди"; + +"subs_coverage" = "обхват підписників"; +"subs_views" = "Перегляди підписників"; + +"viral_coverage" = "Віральний обхват"; +"viral_views" = "Віральні перегляди"; + +/* Sudo */ + +"you_entered_as" = "Ви увійшли як"; +"please_rights" = "наполегливо просимо, шануйте право на таємницю листування інших людей та не зловживайте підміною користувача."; +"click_on" = "Натисніть"; +"there" = "тут"; +"to_leave" = "щоб вийти"; + +/* Phone number */ + +"verify_phone_number" = "Підтвердити номер телефону"; +"we_sended_first" = "Ми надіслали SMS з кодом на номер"; +"we_sended_end" = "уведіть його сюди"; + /* Mobile */ "mobile_friends" = "Друзі"; @@ -1438,3 +1708,40 @@ "mobile_like" = "Подобається"; "mobile_user_info_hide" = "Приховувати"; "mobile_user_info_show_details" = "Показати докладніше"; + +/* Moderation */ + +"section" = "Розділ"; +"template_ban" = "Блокування за шаблоном"; +"active_templates" = "Чинні шаблони"; +"users_reports" = "Скарги користувачів"; +"substring" = "Підрядок"; +"n_user" = "Користувач"; +"time_before" = "Годину раніше, ніж"; +"time_after" = "Годиною пізніше, ніж"; +"where_for_search" = "WHERE для пошуку по розділу"; +"block_params" = "Параметри блокувань"; +"only_rollback" = "Тільки відкат"; +"only_block" = "Тільки блокування"; +"rollback_and_block" = "Відкат та блокування"; +"subm" = "Застосувати"; + +"select_section_for_start" = "Для початку роботи, оберіть розділ"; +"results_will_be_there" = "Тут будуть відображатися результати пошуку"; +"search_results" = "Результати пошуку"; +"cnt" = "шт."; + +"link_to_page" = "Посилання на сторінку"; +"or_subnet" = "або підмережа"; +"error_when_searching" = "Помилка при виконанні запиту"; +"no_found" = "Нічого не знайдено"; +"operation_successfully" = "Операцію успішно виконано"; + +"unknown_error" = "Невідома помилка"; +"templates" = "Шаблони"; +"type" = "Тип"; +"count" = "Кількість"; +"time" = "Час"; + +"roll_back" = "відкотити"; +"roll_backed" = "відкачано"; From fef0203aa4b2cd9abf54171bad424670b18638d9 Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:02:39 +0700 Subject: [PATCH 003/134] UPD code --- Web/Presenters/PhotosPresenter.php | 5 +- locales/uk.strings | 311 +---------------------------- 2 files changed, 3 insertions(+), 313 deletions(-) diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index aeb8ba1e..0a8b87e4 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -336,10 +336,7 @@ final class PhotosPresenter extends OpenVKPresenter if(is_null($this->user) || $this->user->id != $ownerId) $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - if(!is_null($album = $photo->getAlbum())) - $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; - else - $redirect = "/id0"; + $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; $photo->isolate(); $photo->delete(); diff --git a/locales/uk.strings b/locales/uk.strings index b2b8a914..7ccfb367 100644 --- a/locales/uk.strings +++ b/locales/uk.strings @@ -134,10 +134,6 @@ "updated_at" = "Оновлено $1"; "user_banned" = "На жаль, нам довелося заблокувати сторінку користувача $1."; "user_banned_comment" = "Коментар модератора:"; -"verified_page" = "Верифікована сторінка"; -"user_is_blocked" = "Користувача заблоковано"; -"before" = "до"; -"forever" = "назавжди"; /* Wall */ @@ -188,7 +184,6 @@ "nsfw_warning" = "Даний запис може містити контент 18+"; "report" = "Поскаржитися"; "attach" = "Прикріпити"; -"detach" = "Відкріпити"; "attach_photo" = "Прикріпити фото"; "attach_photo" = "Прикріпити відео"; "draw_graffiti" = "Намалювати графіті"; @@ -198,8 +193,6 @@ "version_incompatibility" = "Не вдалося показати це вкладення. Можливо, база даних несумісна з поточною версією OpenVK."; "graffiti" = "Графіті"; "reply" = "Відповісти"; -"post_is_ad" = "Цей пост було проплачено Держдепом США"; -"edited_short" = "ред."; /* Friends */ @@ -324,21 +317,14 @@ "search_group" = "Пошук групи"; "search_by_groups" = "Пошук за групами"; "search_group_desc" = "Тут Ви можете переглянути існуючи групи та обрати групу до вподоби."; -"group_banned" = "Спільнота $1 заблокована." /* Albums */ "create" = "Створити"; -"album" = "Альбом"; "albums" = "Альбоми"; -"photos" = "фотографій"; -"photo" = "Фотографія"; "create_album" = "Створити альбом"; "edit_album" = "Редагувати альбом"; -"edit_photo" = "Змінити фотографію"; "creating_album" = "Створення альбому"; -"delete_photo" = "Видалити фотографію"; -"sure_deleting_photo" = "Ви впевнені, що бажаєте видалити цю світлину?"; "upload_photo" = "Завантажити фотографію"; "photo" = "Фотографія"; "upload_button" = "Завантажити"; @@ -380,39 +366,6 @@ "upd_f" = "оновила фотографію на своїй сторінці"; "upd_g" = "оновило фотографію групи"; -"add_photos" = "Додати фотографії"; -"upload_picts" = "Завантажити фотографії"; -"end_uploading" = "Завершити завантаження"; -"photos_successfully_uploaded" = "Фотографії було завантажено"; -"click_to_go_to_album" = "Натисніть, щоб перейти до альбому."; -"error_uploading_photo" = "Помилка завантаження фотографії"; -"too_many_pictures" = "Не більше 10 фотографій"; - -"drag_files_here" = "Перетягніть файли сюди"; -"only_images_accepted" = "Файл \"$1\" не є зображенням"; -"max_filesize" = "Максимальний розмір файлу — $1 мегабайт"; - -"uploading_photos_from_computer" = "Завантаження фотографій з Вашого ПК"; -"supported_formats" = "Підтримувані формати зображень: JPG, PNG й GIF."; -"max_load_photos" = "Ви можете завантажити до 10 фотографій за один раз."; -"tip" = "Порада"; -"tip_ctrl" = "щоб обрати кілька фотографій одразу, утримуйте клавішу Ctrl під час вибору файлів в OS Windows або клавішу CMD у Mac OS."; -"album_poster" = "Обкладинка альбому"; -"select_photo" = "Оберіть фотографію"; -"upload_new_photo" = "Завантажте нову світлину"; - -"is_x_photos_zero" = "Усього 0 фотографій."; -"is_x_photos_one" = "Всього 1 фотографія."; -"is_x_photos_few" = "Всього $1 фотографій."; -"is_x_photos_many" = "Всього $1 фотографій."; -"is_x_photos_other" = "Всього $1 фотографій."; - -"all_photos" = "Всі фотографії"; -"error_uploading_photo" = "Помилка завантаження фотографії. Текст помилки: "; -"too_many_photos" = "Надто багато фотографій."; - -"photo_x_from_y" = "Фотографія $1 з $2"; - /* Notes */ "notes" = "Нотатки"; @@ -442,18 +395,6 @@ /* Notes: Article Viewer */ "aw_legacy_ui" = "Класичне дієвидло"; -"select_note" = "Вибір нотатки"; -"no_notes" = "Ви не маєте жодної нотатки"; - -"error_attaching_note" = "Не вдалося закріпити нотатку"; - -"select_or_create_new" = "Оберіть існуючу нотатку або створіть нову"; - -"notes_closed" = "Ви не можете прикріпити нотатку до запису, оскільки ваші нотатки видно тільки вам.

Ви можете змінити це в налаштуваннях."; -"do_not_attach_note" = "Не прикріплювати нотатку"; -"something" = "Щось"; -"supports_xhtml" = "з (X)HTML підтримується."; - /* Menus */ "edit_button" = "ред."; @@ -548,7 +489,7 @@ "privacy_value_friends" = "Друзі"; "privacy_value_friends_dative" = "Друзям"; "privacy_value_only_me" = "Тільки я"; -"privacy_value_only_me_dative" = "Тільки мені та Кирилу Буданову"; +"privacy_value_only_me_dative" = "Тільки мені"; "privacy_value_nobody" = "Ніхто"; "your_email_address" = "Адрес Вашої електронної пошти"; "your_page_address" = "Адрес Вашої сторінки"; @@ -640,9 +581,6 @@ "two_factor_authentication_backup_codes_1" = "Резервні коди дозволяють підтверджувати вхід, коли у вас немає доступу до телефону, наприклад, у подорожі."; "two_factor_authentication_backup_codes_2" = "У вас є ще 10 кодів, кожним кодом можна скористатися тільки один раз. Надрукуйте їх, приберіть в надійне місце і використовуйте, коли будуть потрібні коди для підтвердження входу."; "two_factor_authentication_backup_codes_3" = "Ви можете отримати нові коди, якщо вони закінчуються. Дійсні тільки останні створені резервні коди."; -"viewing_backup_codes" = "Перегляд резервних кодів"; -"disable_2fa" = "Вимкнути 2FA"; -"viewing" = "Переглянути"; /* Sorting */ @@ -668,14 +606,6 @@ "videos_many" = "$1 відеозаписів"; "videos_other" = "$1 відеозаписів"; "view_video" = "Перегляд"; -"change_video" = "Редагувати відеозапис"; -"unknown_video" = "ЦЕЙ ВІДЕОЗАПИС НЕ ПІДТРИМУЄТЬСЯ В ЦІЙ ВЕРСІЇ OPENVK."; -"selecting_video" = "Вибір відеозаписів"; -"upload_new_video" = "Завантажити нове відео"; -"max_attached_videos" = "Максимум 10 відеозаписів"; -"max_attached_photos" = "Максимум 10 фотографій"; -"no_videos" = "Ви не маєте відео."; -"no_videos_results" = "Немає результатів."; /* Notifications */ @@ -712,7 +642,6 @@ "nt_mention_in_video" = "в обговоренні відеозапису"; "nt_mention_in_note" = "в обговоренні під"; "nt_mention_in_topic" = "в обговоренні"; -"nt_sent_gift" = "відправив вам подарунок"; /* Time */ @@ -886,7 +815,6 @@ "support_new" = "Нове звернення"; "support_new_title" = "Введіть тему вашого звернення"; "support_new_content" = "Опишіть проблему чи пропозицію"; -"reports" = "Скарги"; "support_rate_good_answer" = "Це хороша відповідь"; "support_rate_bad_answer" = "Це погана відповідь"; "support_good_answer_user" = "Ви залишили позитивний відгук."; @@ -897,26 +825,6 @@ "support_rated_bad" = "Ви залишили негативний відгук про відповідь."; "wrong_parameters" = "Неправильні параметри запиту."; "fast_answers" = "Швидкі відповіді"; -"ignore_report" = "Ігнорувати скаргу"; -"report_number" = "Скарга №"; -"list_of_reports" = "Лист скарг"; -"text_of_the_post" = "Текст допису"; -"today" = "сьогодні"; - -"will_be_watched" = "Скоро її розглянуть модератори"; - -"report_question" = "Поскаржитись?"; -"report_question_text" = "Що саме Ви вважаєте неприпустимим у цьому матеріалі?"; -"report_reason" = "Причина скарги"; -"reason" = "Причина"; -"going_to_report_app" = "Ви збираєтеся поскаржитися на цей додаток."; -"going_to_report_club" = "Ви збираєтеся поскаржитися на цю спільноту."; -"going_to_report_photo" = "Ви збираєтеся поскаржитися на цю фотографію."; -"going_to_report_user" = "Ви збираєтеся поскаржитися на цього користувача."; -"going_to_report_video" = "Ви збираєтеся поскаржитися на цей відеозапис."; -"going_to_report_post" = "Ви збираєтеся поскаржитися на цей запис."; -"going_to_report_comment" = "Ви збираєтеся поскаржитися на цей коментар."; - "comment" = "Коментар"; "sender" = "Відправник"; "author" = "Автор"; @@ -926,12 +834,6 @@ "ticket_changed_comment" = "Зміни набудуть чинності через кілька секунд."; "banned_in_support_1" = "Вибачте, $1, але тепер вам не можна створювати звернення."; "banned_in_support_2" = "Підстава: $1. Цього разу нам довелося забрати у вас цю можливість назавжди."; -"you_can_close_this_ticket_1" = "Якщо ви не маєте запитань, Ви можете "; -"you_can_close_this_ticket_2" = "закрити це звернення"; -"agent_profile_created_1" = "Профіль створено"; -"agent_profile_created_2" = "Тепер користувачі бачать Ваш псевдонім і аватар замість стандартного поличчя та ID."; -"agent_profile_edited" = "Профіль відредагований"; -"agent_profile" = "Картка агента"; /* Invite */ @@ -1056,7 +958,6 @@ "error_repost_fail" = "Не вдалося поділитися записом"; "error_data_too_big" = "Атрибут '$1' не може бути довше $2 $3"; "forbidden" = "Помилка доступу"; -"unknown_error" = "Невідома помилка"; "forbidden_comment" = "Налаштування приватності цього користувача не дозволяють дивитися на його сторінку."; "changes_saved" = "Зміни збережені"; "changes_saved_comment" = "Нові дані з'являться на вашій сторінці"; @@ -1092,96 +993,6 @@ "media_file_corrupted_or_too_large" = "Файл медіаконтенту пошкоджений або файл занадто великий."; "post_is_empty_or_too_big" = "Пост порожній чи надто великий."; "post_is_too_big" = "Пост надто великий."; -"error_sending_report" = "Не вдалося подати скаргу..."; -"error_when_saving_gift" = "Не вдалося зберегти подарунок"; -"error_when_saving_gift_bad_image" = "Зображення подарунка пошкоджене."; -"error_when_saving_gift_no_image" = "Будь ласка, завантажте зображення подарунка."; -"video_uploads_disabled" = "Завантаження відео вимкнено адміністратором."; - -"error_when_publishing_comment" = "Не вдалося опублікувати коментар"; -"error_when_publishing_comment_description" = "Файл зображення пошкоджено, він занадто великий або один бік зображення в рази більший за інший."; -"error_comment_empty" = "Коментар порожній або занадто великий."; -"error_comment_too_big" = "Коментар занадто великий."; -"error_comment_file_too_big" = "Файл медіаконтенту пошкоджений або занадто великий."; - -"comment_is_added" = "Коментар додано"; -"comment_is_added_desc" = "Ваш коментар з'явиться на сторінці."; - -"error_access_denied_short" = "Помилка доступу"; -"error_access_denied" = "Ви не маєте права на редагування цього ресурсу"; -"success" = "Успіх"; -"comment_will_not_appear" = "Цей коментар більше не буде відображатися."; - -"error_when_gifting" = "Не вдалося подарувати"; -"error_user_not_exists" = "Користувач або набір не існують."; -"error_no_rights_gifts" = "Не вдалося підтвердити права на подарунок."; -"error_no_more_gifts" = "У вас більше не залишилось цих подарунків."; -"error_no_money" = "АХАХАХА ЛОШАРА ПІЗДУЙ НА ЗАРОБІТКИ У ПОЛЬЩУ"; -/* трррр шкібіді доп доп доп доп єс єс єс) */ - -"description_too_long" = "Опис надто довгий."; - -"gift_sent" = "Подарунок відправлено"; -"gift_sent_desc" = "Ви відправили $1 за $2 голосів"; - -"error_on_server_side" = "Виникла помилка на боці сервера. Зверніться до системного адміністратора."; -"error_no_group_name" = "Ви не ввели назву групи."; - -"success_action" = "Операція пройшла успішно"; -"connection_error" = "Помилка з'єднання"; -"connection_error_desc" = "Не вдалося з'єднатися до служби телеметрії"; - -"error_when_uploading_photo" = "Не вдалося зберегти фотографію."; - -"new_changes_desc" = "Нові дані з'являться у вашій групі."; -"comment_is_changed" = "Коментар до адміністратора змінено"; -"comment_is_deleted" = "Коментар до адміністратора видалено"; -"comment_is_too_long" = "Коментар надто довгий ($1 символів замість 36 символів)"; -"x_no_more_admin" = "$1 більше не є адміністратором."; -"x_is_admin" = "$1 призначено адміністратором."; - -"x_is_now_hidden" = "Тепер $1 буде відображатися як звичайний підписник усім, окрім інших адміністраторів"; -"x_is_now_showed" = "Тепер $1 буде відображатися як звичайний адміністратор."; - -"note_is_deleted" = "Нотатка видалена"; -"note_x_is_now_deleted" = "Нотатка \"$1\" була успішно видалена."; -"new_data_accepted" = "Нові дані прийняті."; - -"album_is_deleted" = "Альбом видалено"; -"album_x_is_deleted" = "Альбом $1 було видалено."; - -"error_adding_to_deleted" = "Не вдалося зберегти фотографію у DELETED."; -"error_adding_to_x" = "Не вдалося зберегти фотографію в $1."; -"no_photo" = "Нема фотографій"; - -"select_file" = "Оберіть файл"; -"new_description_will_appear" = "Оновлений опис з'явиться на сторінці з фото."; -"photo_is_deleted" = "Фотографія видалена"; -"photo_is_deleted_desc" = "Ця світлина була успішно видалена."; - -"no_video" = "Немає відеозапису"; -"no_video_desc" = "Оберіть файл або вкажіть URL."; -"error_occured" = "Виникла помилка"; -"error_video_damaged_file" = "Файл пошкоджений або не має відеозапису."; -"error_video_incorrect_link" = "Вірогідно, посилання некоректне."; -"error_video_no_title" = "Відео не може бути опубліковано без назви."; - -"new_data_video" = "Оновлений опис з'явиться на сторінці з відео."; -"error_deleting_video" = "Не вдалося видалити відео"; -"login_please" = "Ви не увійшли в аккаунт."; -"invalid_code" = "Не вдалося підтвердити номер телефону: неправильний код."; - -"error_max_pinned_clubs" = "Знаходитись у лівому меню можуть максимум 10 спільнот"; -"error_viewing_subs" = "Ви не можете переглядати лист підписок $1."; -"error_status_too_long" = "Статус надто довгий ($1 символів замість 255 символів)"; -"death" = "Сміерць..."; -"nehay" = "Няхай жыве!"; -"user_successfully_banned" = "Користувача успішно заблоковано."; - -"content_is_deleted" = "Коментар видалено, а користувач отримав попередження."; -"report_is_ignored" = "Скаргу проігноровано."; -"group_owner_is_banned" = "Творець спільноти успішно заблоковано."; -"group_is_banned" = "Спільноту успішно заблоковано"; /* Admin actions */ @@ -1189,18 +1000,14 @@ "manage_user_action" = "Керування користувачем"; "manage_group_action" = "Керування групою"; "ban_user_action" = "Заблокувати користувача"; -"blocks" = "Блокування"; -"last_actions" = "Останні дії"; "unban_user_action" = "Розблокувати користувача"; "warn_user_action" = "Попередити користувача"; "ban_in_support_user_action" = "Заблокувати у тех.підтримці"; "unban_in_support_user_action" = "Розблокувати у тех.підтримці"; -"changes_history" = "Історія редагування"; /* Admin panel */ "admin" = "Адмін панель"; -"sandbox_for_developers" = "Sandbox для розробників"; "admin_ownerid" = "ID власника"; "admin_author" = "Автор"; "admin_name" = "Ім'я"; @@ -1275,40 +1082,6 @@ "admin_banned_link_initiator" = "Ініціатор"; "admin_banned_link_not_specified" = "Посилання не зазначено"; "admin_banned_link_not_found" = "Посилання не знайдено"; -"admin_gift_moved_successfully" = "Подарунок успішно переміщено"; -"admin_gift_moved_to_recycle" = "Тепер подарунок у кошику."; -"logs" = "Логи"; -"logs_anything" = "Будь-яке"; -"logs_adding" = "Створення"; -"logs_editing" = "Редагування"; -"logs_removing" = "Видалення"; -"logs_restoring" = "Відновлення"; -"logs_added" = "додав"; -"logs_edited" = "відредагував"; -"logs_removed" = "видалив"; -"logs_restored" = "відновив"; -"logs_id_post" = "ID допису"; -"logs_id_object" = "ID об'єкту"; -"logs_uuid_user" = "UUID користувача"; -"logs_change_type" = "Тип зміни"; -"logs_change_object" = "Тип об'єкта"; - -"logs_user" = "Користувач"; -"logs_object" = "Об'єкт"; -"logs_type" = "Тип"; -"logs_changes" = "Зміни"; -"logs_time" = "Час"; - -"bans_history" = "Історія блокувань"; -"bans_history_blocked" = "Заблоковано"; -"bans_history_initiator" = "Ініціатор"; -"bans_history_start" = "Початок"; -"bans_history_end" = "Кінець"; -"bans_history_time" = "Час"; -"bans_history_reason" = "Причина"; -"bans_history_start" = "Початок"; -"bans_history_removed" = "Знята"; -"bans_history_active" = "Активне блокування"; /* Paginator (deprecated) */ @@ -1357,9 +1130,7 @@ "transfer" = "Передати"; "close" = "Закрити"; "warning" = "Увага"; -"question_confirm" = "Цю дію не можна скасувати. Ви переконані що хочете це зробити?"; -"confirm_m" = "Підтвердити"; -"action_successfully" = "Операція виконана успішно"; +"question_confirm" = "Цю дію не можна скасувати. Ви дійсно впевнені, що хочете зробити?"; /* User alerts */ @@ -1373,8 +1144,6 @@ /* Away */ -"transition_is_blocked" = "Перехід за посиланням заборонений"; -"caution" = "Попередження"; "url_is_banned" = "Перехід неможливий"; "url_is_banned_comment" = "Адміністрація $1 не рекомендує переходити за цим посиланням."; "url_is_banned_comment_r" = "Адміністрація $1 не рекомендує переходити за цим посиланням.

Підстава: $2"; @@ -1382,8 +1151,6 @@ "url_is_banned_title" = "Посилання на підозрілий сайт"; "url_is_banned_proceed" = "Перейти за посиланням"; -"recently" = "Нещодавно"; - /* Chandler */ "c_user_removed_from_group" = "Користувача було видалено з групи"; @@ -1655,43 +1422,6 @@ "closed_group_post" = "Цей допис з приватної групи"; "deleted_target_comment" = "Цей коментар належить до видаленого допису"; -"no_results" = "Немає результатів"; - -/* BadBrowser */ - -"deprecated_browser" = "Застарілий браузер"; -"deprecated_browser_description" = "Для перегляду цього контенту вам необхідний >Firefox ESR 52 або еквівалент по функціоналу навігатор по всесвітньою мережею інтернет. Співчуваємо про це."; - -/* Statistics */ - -"coverage" = "Обхват"; -"coverage_this_week" = "Цей графік відображає обхват за останні 7 днів."; -"views" = "Перегляди"; -"views_this_week" = "Цей графік відображає перегляди дописів спільноти за остані 7 днів."; - -"full_coverage" = "Повний обхват"; -"all_views" = "Усі перегляди"; - -"subs_coverage" = "обхват підписників"; -"subs_views" = "Перегляди підписників"; - -"viral_coverage" = "Віральний обхват"; -"viral_views" = "Віральні перегляди"; - -/* Sudo */ - -"you_entered_as" = "Ви увійшли як"; -"please_rights" = "наполегливо просимо, шануйте право на таємницю листування інших людей та не зловживайте підміною користувача."; -"click_on" = "Натисніть"; -"there" = "тут"; -"to_leave" = "щоб вийти"; - -/* Phone number */ - -"verify_phone_number" = "Підтвердити номер телефону"; -"we_sended_first" = "Ми надіслали SMS з кодом на номер"; -"we_sended_end" = "уведіть його сюди"; - /* Mobile */ "mobile_friends" = "Друзі"; @@ -1708,40 +1438,3 @@ "mobile_like" = "Подобається"; "mobile_user_info_hide" = "Приховувати"; "mobile_user_info_show_details" = "Показати докладніше"; - -/* Moderation */ - -"section" = "Розділ"; -"template_ban" = "Блокування за шаблоном"; -"active_templates" = "Чинні шаблони"; -"users_reports" = "Скарги користувачів"; -"substring" = "Підрядок"; -"n_user" = "Користувач"; -"time_before" = "Годину раніше, ніж"; -"time_after" = "Годиною пізніше, ніж"; -"where_for_search" = "WHERE для пошуку по розділу"; -"block_params" = "Параметри блокувань"; -"only_rollback" = "Тільки відкат"; -"only_block" = "Тільки блокування"; -"rollback_and_block" = "Відкат та блокування"; -"subm" = "Застосувати"; - -"select_section_for_start" = "Для початку роботи, оберіть розділ"; -"results_will_be_there" = "Тут будуть відображатися результати пошуку"; -"search_results" = "Результати пошуку"; -"cnt" = "шт."; - -"link_to_page" = "Посилання на сторінку"; -"or_subnet" = "або підмережа"; -"error_when_searching" = "Помилка при виконанні запиту"; -"no_found" = "Нічого не знайдено"; -"operation_successfully" = "Операцію успішно виконано"; - -"unknown_error" = "Невідома помилка"; -"templates" = "Шаблони"; -"type" = "Тип"; -"count" = "Кількість"; -"time" = "Час"; - -"roll_back" = "відкотити"; -"roll_backed" = "відкачано"; From 1c3d4d84291c5010d3e9942bbc82f8529e71b5c5 Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Thu, 19 Oct 2023 12:18:59 +0700 Subject: [PATCH 004/134] Revert "UPD code" This reverts commit fef0203aa4b2cd9abf54171bad424670b18638d9. --- Web/Presenters/PhotosPresenter.php | 5 +- locales/uk.strings | 311 ++++++++++++++++++++++++++++- 2 files changed, 313 insertions(+), 3 deletions(-) diff --git a/Web/Presenters/PhotosPresenter.php b/Web/Presenters/PhotosPresenter.php index 0a8b87e4..aeb8ba1e 100644 --- a/Web/Presenters/PhotosPresenter.php +++ b/Web/Presenters/PhotosPresenter.php @@ -336,7 +336,10 @@ final class PhotosPresenter extends OpenVKPresenter if(is_null($this->user) || $this->user->id != $ownerId) $this->flashFail("err", tr("error_access_denied_short"), tr("error_access_denied")); - $redirect = $photo->getAlbum()->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + if(!is_null($album = $photo->getAlbum())) + $redirect = $album->getOwner() instanceof User ? "/id0" : "/club" . $ownerId; + else + $redirect = "/id0"; $photo->isolate(); $photo->delete(); diff --git a/locales/uk.strings b/locales/uk.strings index 7ccfb367..b2b8a914 100644 --- a/locales/uk.strings +++ b/locales/uk.strings @@ -134,6 +134,10 @@ "updated_at" = "Оновлено $1"; "user_banned" = "На жаль, нам довелося заблокувати сторінку користувача $1."; "user_banned_comment" = "Коментар модератора:"; +"verified_page" = "Верифікована сторінка"; +"user_is_blocked" = "Користувача заблоковано"; +"before" = "до"; +"forever" = "назавжди"; /* Wall */ @@ -184,6 +188,7 @@ "nsfw_warning" = "Даний запис може містити контент 18+"; "report" = "Поскаржитися"; "attach" = "Прикріпити"; +"detach" = "Відкріпити"; "attach_photo" = "Прикріпити фото"; "attach_photo" = "Прикріпити відео"; "draw_graffiti" = "Намалювати графіті"; @@ -193,6 +198,8 @@ "version_incompatibility" = "Не вдалося показати це вкладення. Можливо, база даних несумісна з поточною версією OpenVK."; "graffiti" = "Графіті"; "reply" = "Відповісти"; +"post_is_ad" = "Цей пост було проплачено Держдепом США"; +"edited_short" = "ред."; /* Friends */ @@ -317,14 +324,21 @@ "search_group" = "Пошук групи"; "search_by_groups" = "Пошук за групами"; "search_group_desc" = "Тут Ви можете переглянути існуючи групи та обрати групу до вподоби."; +"group_banned" = "Спільнота $1 заблокована." /* Albums */ "create" = "Створити"; +"album" = "Альбом"; "albums" = "Альбоми"; +"photos" = "фотографій"; +"photo" = "Фотографія"; "create_album" = "Створити альбом"; "edit_album" = "Редагувати альбом"; +"edit_photo" = "Змінити фотографію"; "creating_album" = "Створення альбому"; +"delete_photo" = "Видалити фотографію"; +"sure_deleting_photo" = "Ви впевнені, що бажаєте видалити цю світлину?"; "upload_photo" = "Завантажити фотографію"; "photo" = "Фотографія"; "upload_button" = "Завантажити"; @@ -366,6 +380,39 @@ "upd_f" = "оновила фотографію на своїй сторінці"; "upd_g" = "оновило фотографію групи"; +"add_photos" = "Додати фотографії"; +"upload_picts" = "Завантажити фотографії"; +"end_uploading" = "Завершити завантаження"; +"photos_successfully_uploaded" = "Фотографії було завантажено"; +"click_to_go_to_album" = "Натисніть, щоб перейти до альбому."; +"error_uploading_photo" = "Помилка завантаження фотографії"; +"too_many_pictures" = "Не більше 10 фотографій"; + +"drag_files_here" = "Перетягніть файли сюди"; +"only_images_accepted" = "Файл \"$1\" не є зображенням"; +"max_filesize" = "Максимальний розмір файлу — $1 мегабайт"; + +"uploading_photos_from_computer" = "Завантаження фотографій з Вашого ПК"; +"supported_formats" = "Підтримувані формати зображень: JPG, PNG й GIF."; +"max_load_photos" = "Ви можете завантажити до 10 фотографій за один раз."; +"tip" = "Порада"; +"tip_ctrl" = "щоб обрати кілька фотографій одразу, утримуйте клавішу Ctrl під час вибору файлів в OS Windows або клавішу CMD у Mac OS."; +"album_poster" = "Обкладинка альбому"; +"select_photo" = "Оберіть фотографію"; +"upload_new_photo" = "Завантажте нову світлину"; + +"is_x_photos_zero" = "Усього 0 фотографій."; +"is_x_photos_one" = "Всього 1 фотографія."; +"is_x_photos_few" = "Всього $1 фотографій."; +"is_x_photos_many" = "Всього $1 фотографій."; +"is_x_photos_other" = "Всього $1 фотографій."; + +"all_photos" = "Всі фотографії"; +"error_uploading_photo" = "Помилка завантаження фотографії. Текст помилки: "; +"too_many_photos" = "Надто багато фотографій."; + +"photo_x_from_y" = "Фотографія $1 з $2"; + /* Notes */ "notes" = "Нотатки"; @@ -395,6 +442,18 @@ /* Notes: Article Viewer */ "aw_legacy_ui" = "Класичне дієвидло"; +"select_note" = "Вибір нотатки"; +"no_notes" = "Ви не маєте жодної нотатки"; + +"error_attaching_note" = "Не вдалося закріпити нотатку"; + +"select_or_create_new" = "Оберіть існуючу нотатку або створіть нову"; + +"notes_closed" = "Ви не можете прикріпити нотатку до запису, оскільки ваші нотатки видно тільки вам.

Ви можете змінити це в налаштуваннях."; +"do_not_attach_note" = "Не прикріплювати нотатку"; +"something" = "Щось"; +"supports_xhtml" = "з (X)HTML підтримується."; + /* Menus */ "edit_button" = "ред."; @@ -489,7 +548,7 @@ "privacy_value_friends" = "Друзі"; "privacy_value_friends_dative" = "Друзям"; "privacy_value_only_me" = "Тільки я"; -"privacy_value_only_me_dative" = "Тільки мені"; +"privacy_value_only_me_dative" = "Тільки мені та Кирилу Буданову"; "privacy_value_nobody" = "Ніхто"; "your_email_address" = "Адрес Вашої електронної пошти"; "your_page_address" = "Адрес Вашої сторінки"; @@ -581,6 +640,9 @@ "two_factor_authentication_backup_codes_1" = "Резервні коди дозволяють підтверджувати вхід, коли у вас немає доступу до телефону, наприклад, у подорожі."; "two_factor_authentication_backup_codes_2" = "У вас є ще 10 кодів, кожним кодом можна скористатися тільки один раз. Надрукуйте їх, приберіть в надійне місце і використовуйте, коли будуть потрібні коди для підтвердження входу."; "two_factor_authentication_backup_codes_3" = "Ви можете отримати нові коди, якщо вони закінчуються. Дійсні тільки останні створені резервні коди."; +"viewing_backup_codes" = "Перегляд резервних кодів"; +"disable_2fa" = "Вимкнути 2FA"; +"viewing" = "Переглянути"; /* Sorting */ @@ -606,6 +668,14 @@ "videos_many" = "$1 відеозаписів"; "videos_other" = "$1 відеозаписів"; "view_video" = "Перегляд"; +"change_video" = "Редагувати відеозапис"; +"unknown_video" = "ЦЕЙ ВІДЕОЗАПИС НЕ ПІДТРИМУЄТЬСЯ В ЦІЙ ВЕРСІЇ OPENVK."; +"selecting_video" = "Вибір відеозаписів"; +"upload_new_video" = "Завантажити нове відео"; +"max_attached_videos" = "Максимум 10 відеозаписів"; +"max_attached_photos" = "Максимум 10 фотографій"; +"no_videos" = "Ви не маєте відео."; +"no_videos_results" = "Немає результатів."; /* Notifications */ @@ -642,6 +712,7 @@ "nt_mention_in_video" = "в обговоренні відеозапису"; "nt_mention_in_note" = "в обговоренні під"; "nt_mention_in_topic" = "в обговоренні"; +"nt_sent_gift" = "відправив вам подарунок"; /* Time */ @@ -815,6 +886,7 @@ "support_new" = "Нове звернення"; "support_new_title" = "Введіть тему вашого звернення"; "support_new_content" = "Опишіть проблему чи пропозицію"; +"reports" = "Скарги"; "support_rate_good_answer" = "Це хороша відповідь"; "support_rate_bad_answer" = "Це погана відповідь"; "support_good_answer_user" = "Ви залишили позитивний відгук."; @@ -825,6 +897,26 @@ "support_rated_bad" = "Ви залишили негативний відгук про відповідь."; "wrong_parameters" = "Неправильні параметри запиту."; "fast_answers" = "Швидкі відповіді"; +"ignore_report" = "Ігнорувати скаргу"; +"report_number" = "Скарга №"; +"list_of_reports" = "Лист скарг"; +"text_of_the_post" = "Текст допису"; +"today" = "сьогодні"; + +"will_be_watched" = "Скоро її розглянуть модератори"; + +"report_question" = "Поскаржитись?"; +"report_question_text" = "Що саме Ви вважаєте неприпустимим у цьому матеріалі?"; +"report_reason" = "Причина скарги"; +"reason" = "Причина"; +"going_to_report_app" = "Ви збираєтеся поскаржитися на цей додаток."; +"going_to_report_club" = "Ви збираєтеся поскаржитися на цю спільноту."; +"going_to_report_photo" = "Ви збираєтеся поскаржитися на цю фотографію."; +"going_to_report_user" = "Ви збираєтеся поскаржитися на цього користувача."; +"going_to_report_video" = "Ви збираєтеся поскаржитися на цей відеозапис."; +"going_to_report_post" = "Ви збираєтеся поскаржитися на цей запис."; +"going_to_report_comment" = "Ви збираєтеся поскаржитися на цей коментар."; + "comment" = "Коментар"; "sender" = "Відправник"; "author" = "Автор"; @@ -834,6 +926,12 @@ "ticket_changed_comment" = "Зміни набудуть чинності через кілька секунд."; "banned_in_support_1" = "Вибачте, $1, але тепер вам не можна створювати звернення."; "banned_in_support_2" = "Підстава: $1. Цього разу нам довелося забрати у вас цю можливість назавжди."; +"you_can_close_this_ticket_1" = "Якщо ви не маєте запитань, Ви можете "; +"you_can_close_this_ticket_2" = "закрити це звернення"; +"agent_profile_created_1" = "Профіль створено"; +"agent_profile_created_2" = "Тепер користувачі бачать Ваш псевдонім і аватар замість стандартного поличчя та ID."; +"agent_profile_edited" = "Профіль відредагований"; +"agent_profile" = "Картка агента"; /* Invite */ @@ -958,6 +1056,7 @@ "error_repost_fail" = "Не вдалося поділитися записом"; "error_data_too_big" = "Атрибут '$1' не може бути довше $2 $3"; "forbidden" = "Помилка доступу"; +"unknown_error" = "Невідома помилка"; "forbidden_comment" = "Налаштування приватності цього користувача не дозволяють дивитися на його сторінку."; "changes_saved" = "Зміни збережені"; "changes_saved_comment" = "Нові дані з'являться на вашій сторінці"; @@ -993,6 +1092,96 @@ "media_file_corrupted_or_too_large" = "Файл медіаконтенту пошкоджений або файл занадто великий."; "post_is_empty_or_too_big" = "Пост порожній чи надто великий."; "post_is_too_big" = "Пост надто великий."; +"error_sending_report" = "Не вдалося подати скаргу..."; +"error_when_saving_gift" = "Не вдалося зберегти подарунок"; +"error_when_saving_gift_bad_image" = "Зображення подарунка пошкоджене."; +"error_when_saving_gift_no_image" = "Будь ласка, завантажте зображення подарунка."; +"video_uploads_disabled" = "Завантаження відео вимкнено адміністратором."; + +"error_when_publishing_comment" = "Не вдалося опублікувати коментар"; +"error_when_publishing_comment_description" = "Файл зображення пошкоджено, він занадто великий або один бік зображення в рази більший за інший."; +"error_comment_empty" = "Коментар порожній або занадто великий."; +"error_comment_too_big" = "Коментар занадто великий."; +"error_comment_file_too_big" = "Файл медіаконтенту пошкоджений або занадто великий."; + +"comment_is_added" = "Коментар додано"; +"comment_is_added_desc" = "Ваш коментар з'явиться на сторінці."; + +"error_access_denied_short" = "Помилка доступу"; +"error_access_denied" = "Ви не маєте права на редагування цього ресурсу"; +"success" = "Успіх"; +"comment_will_not_appear" = "Цей коментар більше не буде відображатися."; + +"error_when_gifting" = "Не вдалося подарувати"; +"error_user_not_exists" = "Користувач або набір не існують."; +"error_no_rights_gifts" = "Не вдалося підтвердити права на подарунок."; +"error_no_more_gifts" = "У вас більше не залишилось цих подарунків."; +"error_no_money" = "АХАХАХА ЛОШАРА ПІЗДУЙ НА ЗАРОБІТКИ У ПОЛЬЩУ"; +/* трррр шкібіді доп доп доп доп єс єс єс) */ + +"description_too_long" = "Опис надто довгий."; + +"gift_sent" = "Подарунок відправлено"; +"gift_sent_desc" = "Ви відправили $1 за $2 голосів"; + +"error_on_server_side" = "Виникла помилка на боці сервера. Зверніться до системного адміністратора."; +"error_no_group_name" = "Ви не ввели назву групи."; + +"success_action" = "Операція пройшла успішно"; +"connection_error" = "Помилка з'єднання"; +"connection_error_desc" = "Не вдалося з'єднатися до служби телеметрії"; + +"error_when_uploading_photo" = "Не вдалося зберегти фотографію."; + +"new_changes_desc" = "Нові дані з'являться у вашій групі."; +"comment_is_changed" = "Коментар до адміністратора змінено"; +"comment_is_deleted" = "Коментар до адміністратора видалено"; +"comment_is_too_long" = "Коментар надто довгий ($1 символів замість 36 символів)"; +"x_no_more_admin" = "$1 більше не є адміністратором."; +"x_is_admin" = "$1 призначено адміністратором."; + +"x_is_now_hidden" = "Тепер $1 буде відображатися як звичайний підписник усім, окрім інших адміністраторів"; +"x_is_now_showed" = "Тепер $1 буде відображатися як звичайний адміністратор."; + +"note_is_deleted" = "Нотатка видалена"; +"note_x_is_now_deleted" = "Нотатка \"$1\" була успішно видалена."; +"new_data_accepted" = "Нові дані прийняті."; + +"album_is_deleted" = "Альбом видалено"; +"album_x_is_deleted" = "Альбом $1 було видалено."; + +"error_adding_to_deleted" = "Не вдалося зберегти фотографію у DELETED."; +"error_adding_to_x" = "Не вдалося зберегти фотографію в $1."; +"no_photo" = "Нема фотографій"; + +"select_file" = "Оберіть файл"; +"new_description_will_appear" = "Оновлений опис з'явиться на сторінці з фото."; +"photo_is_deleted" = "Фотографія видалена"; +"photo_is_deleted_desc" = "Ця світлина була успішно видалена."; + +"no_video" = "Немає відеозапису"; +"no_video_desc" = "Оберіть файл або вкажіть URL."; +"error_occured" = "Виникла помилка"; +"error_video_damaged_file" = "Файл пошкоджений або не має відеозапису."; +"error_video_incorrect_link" = "Вірогідно, посилання некоректне."; +"error_video_no_title" = "Відео не може бути опубліковано без назви."; + +"new_data_video" = "Оновлений опис з'явиться на сторінці з відео."; +"error_deleting_video" = "Не вдалося видалити відео"; +"login_please" = "Ви не увійшли в аккаунт."; +"invalid_code" = "Не вдалося підтвердити номер телефону: неправильний код."; + +"error_max_pinned_clubs" = "Знаходитись у лівому меню можуть максимум 10 спільнот"; +"error_viewing_subs" = "Ви не можете переглядати лист підписок $1."; +"error_status_too_long" = "Статус надто довгий ($1 символів замість 255 символів)"; +"death" = "Сміерць..."; +"nehay" = "Няхай жыве!"; +"user_successfully_banned" = "Користувача успішно заблоковано."; + +"content_is_deleted" = "Коментар видалено, а користувач отримав попередження."; +"report_is_ignored" = "Скаргу проігноровано."; +"group_owner_is_banned" = "Творець спільноти успішно заблоковано."; +"group_is_banned" = "Спільноту успішно заблоковано"; /* Admin actions */ @@ -1000,14 +1189,18 @@ "manage_user_action" = "Керування користувачем"; "manage_group_action" = "Керування групою"; "ban_user_action" = "Заблокувати користувача"; +"blocks" = "Блокування"; +"last_actions" = "Останні дії"; "unban_user_action" = "Розблокувати користувача"; "warn_user_action" = "Попередити користувача"; "ban_in_support_user_action" = "Заблокувати у тех.підтримці"; "unban_in_support_user_action" = "Розблокувати у тех.підтримці"; +"changes_history" = "Історія редагування"; /* Admin panel */ "admin" = "Адмін панель"; +"sandbox_for_developers" = "Sandbox для розробників"; "admin_ownerid" = "ID власника"; "admin_author" = "Автор"; "admin_name" = "Ім'я"; @@ -1082,6 +1275,40 @@ "admin_banned_link_initiator" = "Ініціатор"; "admin_banned_link_not_specified" = "Посилання не зазначено"; "admin_banned_link_not_found" = "Посилання не знайдено"; +"admin_gift_moved_successfully" = "Подарунок успішно переміщено"; +"admin_gift_moved_to_recycle" = "Тепер подарунок у кошику."; +"logs" = "Логи"; +"logs_anything" = "Будь-яке"; +"logs_adding" = "Створення"; +"logs_editing" = "Редагування"; +"logs_removing" = "Видалення"; +"logs_restoring" = "Відновлення"; +"logs_added" = "додав"; +"logs_edited" = "відредагував"; +"logs_removed" = "видалив"; +"logs_restored" = "відновив"; +"logs_id_post" = "ID допису"; +"logs_id_object" = "ID об'єкту"; +"logs_uuid_user" = "UUID користувача"; +"logs_change_type" = "Тип зміни"; +"logs_change_object" = "Тип об'єкта"; + +"logs_user" = "Користувач"; +"logs_object" = "Об'єкт"; +"logs_type" = "Тип"; +"logs_changes" = "Зміни"; +"logs_time" = "Час"; + +"bans_history" = "Історія блокувань"; +"bans_history_blocked" = "Заблоковано"; +"bans_history_initiator" = "Ініціатор"; +"bans_history_start" = "Початок"; +"bans_history_end" = "Кінець"; +"bans_history_time" = "Час"; +"bans_history_reason" = "Причина"; +"bans_history_start" = "Початок"; +"bans_history_removed" = "Знята"; +"bans_history_active" = "Активне блокування"; /* Paginator (deprecated) */ @@ -1130,7 +1357,9 @@ "transfer" = "Передати"; "close" = "Закрити"; "warning" = "Увага"; -"question_confirm" = "Цю дію не можна скасувати. Ви дійсно впевнені, що хочете зробити?"; +"question_confirm" = "Цю дію не можна скасувати. Ви переконані що хочете це зробити?"; +"confirm_m" = "Підтвердити"; +"action_successfully" = "Операція виконана успішно"; /* User alerts */ @@ -1144,6 +1373,8 @@ /* Away */ +"transition_is_blocked" = "Перехід за посиланням заборонений"; +"caution" = "Попередження"; "url_is_banned" = "Перехід неможливий"; "url_is_banned_comment" = "Адміністрація $1 не рекомендує переходити за цим посиланням."; "url_is_banned_comment_r" = "Адміністрація $1 не рекомендує переходити за цим посиланням.

Підстава: $2"; @@ -1151,6 +1382,8 @@ "url_is_banned_title" = "Посилання на підозрілий сайт"; "url_is_banned_proceed" = "Перейти за посиланням"; +"recently" = "Нещодавно"; + /* Chandler */ "c_user_removed_from_group" = "Користувача було видалено з групи"; @@ -1422,6 +1655,43 @@ "closed_group_post" = "Цей допис з приватної групи"; "deleted_target_comment" = "Цей коментар належить до видаленого допису"; +"no_results" = "Немає результатів"; + +/* BadBrowser */ + +"deprecated_browser" = "Застарілий браузер"; +"deprecated_browser_description" = "Для перегляду цього контенту вам необхідний >Firefox ESR 52 або еквівалент по функціоналу навігатор по всесвітньою мережею інтернет. Співчуваємо про це."; + +/* Statistics */ + +"coverage" = "Обхват"; +"coverage_this_week" = "Цей графік відображає обхват за останні 7 днів."; +"views" = "Перегляди"; +"views_this_week" = "Цей графік відображає перегляди дописів спільноти за остані 7 днів."; + +"full_coverage" = "Повний обхват"; +"all_views" = "Усі перегляди"; + +"subs_coverage" = "обхват підписників"; +"subs_views" = "Перегляди підписників"; + +"viral_coverage" = "Віральний обхват"; +"viral_views" = "Віральні перегляди"; + +/* Sudo */ + +"you_entered_as" = "Ви увійшли як"; +"please_rights" = "наполегливо просимо, шануйте право на таємницю листування інших людей та не зловживайте підміною користувача."; +"click_on" = "Натисніть"; +"there" = "тут"; +"to_leave" = "щоб вийти"; + +/* Phone number */ + +"verify_phone_number" = "Підтвердити номер телефону"; +"we_sended_first" = "Ми надіслали SMS з кодом на номер"; +"we_sended_end" = "уведіть його сюди"; + /* Mobile */ "mobile_friends" = "Друзі"; @@ -1438,3 +1708,40 @@ "mobile_like" = "Подобається"; "mobile_user_info_hide" = "Приховувати"; "mobile_user_info_show_details" = "Показати докладніше"; + +/* Moderation */ + +"section" = "Розділ"; +"template_ban" = "Блокування за шаблоном"; +"active_templates" = "Чинні шаблони"; +"users_reports" = "Скарги користувачів"; +"substring" = "Підрядок"; +"n_user" = "Користувач"; +"time_before" = "Годину раніше, ніж"; +"time_after" = "Годиною пізніше, ніж"; +"where_for_search" = "WHERE для пошуку по розділу"; +"block_params" = "Параметри блокувань"; +"only_rollback" = "Тільки відкат"; +"only_block" = "Тільки блокування"; +"rollback_and_block" = "Відкат та блокування"; +"subm" = "Застосувати"; + +"select_section_for_start" = "Для початку роботи, оберіть розділ"; +"results_will_be_there" = "Тут будуть відображатися результати пошуку"; +"search_results" = "Результати пошуку"; +"cnt" = "шт."; + +"link_to_page" = "Посилання на сторінку"; +"or_subnet" = "або підмережа"; +"error_when_searching" = "Помилка при виконанні запиту"; +"no_found" = "Нічого не знайдено"; +"operation_successfully" = "Операцію успішно виконано"; + +"unknown_error" = "Невідома помилка"; +"templates" = "Шаблони"; +"type" = "Тип"; +"count" = "Кількість"; +"time" = "Час"; + +"roll_back" = "відкотити"; +"roll_backed" = "відкачано"; From e3b9fb9f41f4c2bc236dd040914d611b18798fd5 Mon Sep 17 00:00:00 2001 From: IsamiRi <53663257+isamirivers@users.noreply.github.com> Date: Mon, 23 Oct 2023 22:28:41 +0300 Subject: [PATCH 005/134] Fix for #995 (#996) --- Web/static/css/main.css | 1 + 1 file changed, 1 insertion(+) diff --git a/Web/static/css/main.css b/Web/static/css/main.css index 47d67317..7b63d6f6 100644 --- a/Web/static/css/main.css +++ b/Web/static/css/main.css @@ -405,6 +405,7 @@ h1 { width: 200px; text-align: left; cursor: pointer; + font-family: tahoma, verdana, arial, sans-serif; } .profile_link_form { From 49a7047773f30c5dea1da1076e69746edb497ece Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:42:26 +0700 Subject: [PATCH 006/134] Patch array objects for PHP 8.1 (#999) * Patch array objects for PHP 8 * Reswitch getting counts * Update WallPresenter.php * Update WallPresenter.php * Fix --- Web/Presenters/WallPresenter.php | 10 +++++----- Web/Presenters/templates/Admin/Logs.xml | 2 +- Web/Presenters/templates/Search/Index.xml | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index 2f9d611d..f7928db8 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -74,7 +74,7 @@ final class WallPresenter extends OpenVKPresenter $this->template->paginatorConf = (object) [ "count" => $this->template->count, "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($this->template->posts), + "amount" => $this->template->posts->getRowCount(), "perPage" => OPENVK_DEFAULT_PER_PAGE, ]; @@ -152,9 +152,9 @@ final class WallPresenter extends OpenVKPresenter ->where("deleted", 0) ->order("created DESC"); $this->template->paginatorConf = (object) [ - "count" => sizeof($posts), + "count" => $posts->getRowCount(), "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($posts->page((int) ($_GET["p"] ?? 1), $perPage)), + "amount" => $posts->page((int) ($_GET["p"] ?? 1), $perPage)->getRowCount(), "perPage" => $perPage, ]; $this->template->posts = []; @@ -182,7 +182,7 @@ final class WallPresenter extends OpenVKPresenter $this->template->paginatorConf = (object) [ "count" => $count, "page" => (int) ($_GET["p"] ?? 1), - "amount" => sizeof($posts), + "amount" => $posts->getRowCount(), "perPage" => $pPage, ]; foreach($posts as $post) @@ -332,7 +332,7 @@ final class WallPresenter extends OpenVKPresenter foreach($photos as $photo) $post->attach($photo); - if(sizeof($videos) > 0) + if($videos->count() > 0) foreach($videos as $vid) $post->attach($vid); diff --git a/Web/Presenters/templates/Admin/Logs.xml b/Web/Presenters/templates/Admin/Logs.xml index ab5e62f5..8b279008 100644 --- a/Web/Presenters/templates/Admin/Logs.xml +++ b/Web/Presenters/templates/Admin/Logs.xml @@ -9,7 +9,7 @@ {/block} {block content} - {var $amount = sizeof($logs)} + {var $amount = $logs->getRowCount()}
+ {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)} + {include "../components/paginator.xml", conf => (object) [ "page" => $page, "count" => $count, @@ -153,7 +155,6 @@
{include searchOptions} - {var $data = is_array($iterator) ? $iterator : iterator_to_array($iterator)}
{if sizeof($data) > 0} {if $type == "users" || $type == "groups" || $type == "apps"} From 390b4f6c24799330eabd2957eafb89491e614204 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:25:54 +0300 Subject: [PATCH 021/134] Fix followers list in group --- Web/Presenters/templates/Group/Followers.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Presenters/templates/Group/Followers.xml b/Web/Presenters/templates/Group/Followers.xml index 482d156a..67e6af9c 100644 --- a/Web/Presenters/templates/Group/Followers.xml +++ b/Web/Presenters/templates/Group/Followers.xml @@ -64,7 +64,7 @@ {_role}: - {$club->getOwner()->getId() == $user->getId() ? !$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser) : !is_null($manager) ? tr("administrator") : tr("follower")} + {($club->getOwner()->getId() == $user->getId() ? !$club->isOwnerHidden() || $club->canBeModifiedBy($thisUser) : !is_null($manager)) ? tr("administrator") : tr("follower")} From e2dee72c69940a7f69055f065600d46ccee22a85 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:28:19 +0300 Subject: [PATCH 022/134] Fix notes list --- Web/Presenters/templates/Notes/List.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Presenters/templates/Notes/List.xml b/Web/Presenters/templates/Notes/List.xml index 36c48fd3..7f2fcf7c 100644 --- a/Web/Presenters/templates/Notes/List.xml +++ b/Web/Presenters/templates/Notes/List.xml @@ -90,7 +90,7 @@
- {if sizeof($dat->getCommentsCount()) > 0} + {if $dat->getCommentsCount() > 0} {_comments} ({$dat->getCommentsCount()}) {else} {_no_comments} From e48b696aeb4364246f8e9e167097c7801ed4955a Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:33:52 +0300 Subject: [PATCH 023/134] Maybe fix tickets list --- Web/Presenters/SupportPresenter.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Presenters/SupportPresenter.php b/Web/Presenters/SupportPresenter.php index 8163dbf8..b834d712 100644 --- a/Web/Presenters/SupportPresenter.php +++ b/Web/Presenters/SupportPresenter.php @@ -67,7 +67,7 @@ final class SupportPresenter extends OpenVKPresenter $this->template->count = $this->tickets->getTicketsCountByUserId($this->user->id); if($this->template->mode === "list") { $this->template->page = (int) ($this->queryParam("p") ?? 1); - $this->template->tickets = $this->tickets->getTicketsByUserId($this->user->id, $this->template->page); + $this->template->tickets = iterator_to_array($this->tickets->getTicketsByUserId($this->user->id, $this->template->page)); } if($this->template->mode === "new") From f57a470c5d6c3a5b70595a03ec6fb0dca0e81f57 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 14:37:28 +0300 Subject: [PATCH 024/134] Fix gifts template (stolen from #512) --- Web/Presenters/OpenVKPresenter.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Web/Presenters/OpenVKPresenter.php b/Web/Presenters/OpenVKPresenter.php index 80ab0621..a171c2e3 100644 --- a/Web/Presenters/OpenVKPresenter.php +++ b/Web/Presenters/OpenVKPresenter.php @@ -198,6 +198,9 @@ abstract class OpenVKPresenter extends SimplePresenter { $user = Authenticator::i()->getUser(); + if(!$this->template) + $this->template = new \stdClass; + $this->template->isXmas = intval(date('d')) >= 1 && date('m') == 12 || intval(date('d')) <= 15 && date('m') == 1 ? true : false; $this->template->isTimezoned = Session::i()->get("_timezoneOffset"); From 3f1c9cd5ff3543fed935a40a6707eaa141592c7b Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sat, 28 Oct 2023 15:47:46 +0300 Subject: [PATCH 025/134] Fix token creation in php 8.1 --- Web/Models/Entities/APIToken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Web/Models/Entities/APIToken.php b/Web/Models/Entities/APIToken.php index f5744ec3..0cc53148 100644 --- a/Web/Models/Entities/APIToken.php +++ b/Web/Models/Entities/APIToken.php @@ -48,7 +48,7 @@ class APIToken extends RowModel $this->delete(); } - function save(): void + function save(?bool $log = false): void { if(is_null($this->getRecord())) $this->stateChanges("secret", bin2hex(openssl_random_pseudo_bytes(36))); From f492353ae8a8a7074709c108996a748c3249af65 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 29 Oct 2023 13:05:47 +0300 Subject: [PATCH 026/134] Some php 8.2 fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix privacy settings, friends pagination and возможно fix messages. Resolves #1007 and resolves #1006 --- Web/Models/Entities/Correspondence.php | 2 +- Web/Presenters/MessengerPresenter.php | 2 +- Web/Presenters/UserPresenter.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Web/Models/Entities/Correspondence.php b/Web/Models/Entities/Correspondence.php index e4b7d96e..a972425e 100644 --- a/Web/Models/Entities/Correspondence.php +++ b/Web/Models/Entities/Correspondence.php @@ -131,7 +131,7 @@ class Correspondence */ function getPreviewMessage(): ?Message { - $messages = $this->getMessages(1, NULL, 1); + $messages = $this->getMessages(1, NULL, 1, 0); return $messages[0] ?? NULL; } diff --git a/Web/Presenters/MessengerPresenter.php b/Web/Presenters/MessengerPresenter.php index d5ffb988..e04e1adc 100644 --- a/Web/Presenters/MessengerPresenter.php +++ b/Web/Presenters/MessengerPresenter.php @@ -128,7 +128,7 @@ final class MessengerPresenter extends OpenVKPresenter $messages = []; $correspondence = new Correspondence($this->user->identity, $correspondent); - foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg) as $message) + foreach($correspondence->getMessages(1, $lastMsg === 0 ? NULL : $lastMsg, NULL, 0) as $message) $messages[] = $message->simplify(); header("Content-Type: application/json"); diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index 51ddc6aa..e645f888 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -55,7 +55,7 @@ final class UserPresenter extends OpenVKPresenter $this->assertUserLoggedIn(); $user = $this->users->get($id); - $page = abs($this->queryParam("p") ?? 1); + $page = abs((int)($this->queryParam("p") ?? 1)); if(!$user) $this->notFound(); elseif (!$user->getPrivacyPermission('friends.read', $this->user->identity ?? NULL)) @@ -433,7 +433,7 @@ final class UserPresenter extends OpenVKPresenter ]; foreach($settings as $setting) { $input = $this->postParam(str_replace(".", "_", $setting)); - $user->setPrivacySetting($setting, min(3, abs($input ?? $user->getPrivacySetting($setting)))); + $user->setPrivacySetting($setting, min(3, (int)abs((int)$input ?? $user->getPrivacySetting($setting)))); } } else if($_GET['act'] === "finance.top-up") { $token = $this->postParam("key0") . $this->postParam("key1") . $this->postParam("key2") . $this->postParam("key3"); From abc307bfadab93845b1b845724b3d519f52c34f8 Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:41:36 +0700 Subject: [PATCH 027/134] Update PHP 8 testing status --- README_RU.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_RU.md b/README_RU.md index cc4f672f..6da17408 100644 --- a/README_RU.md +++ b/README_RU.md @@ -30,7 +30,7 @@ _[English](README.md)_ 1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) -* PHP 8 еще **не** тестировался, поэтому не стоит ожидать, что он будет работать (UPD: он не работает). +* PHP 8 is still being tested; performance on this version of PHP is not yet guaranteed. 2. Установите MySQL-совместимую базу данных. From 46f7e7ceebc11b009f11bed3a4b2df2cb097064e Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:42:55 +0700 Subject: [PATCH 028/134] Update PHP 8 testing status --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 152e05bf..934ff0b7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ If you want, you can add your instance to the list above so that people can regi 1. Install PHP 7.4, web-server, Composer, Node.js, Yarn and [Chandler](https://github.com/openvk/chandler) -* PHP 8.1 is supported too, however it was not tested carefully, so be aware. +* PHP 8 is still being tested; the functionality of the engine on this version of PHP is not yet guaranteed. 2. Install MySQL-compatible database. From 1a17a245c5dab15bf04e0b7bd4950f33fd580ed5 Mon Sep 17 00:00:00 2001 From: Dmitry Tretyakov <76806170+tretdm@users.noreply.github.com> Date: Sun, 29 Oct 2023 21:55:02 +0700 Subject: [PATCH 029/134] i forgor momento --- README_RU.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_RU.md b/README_RU.md index 6da17408..7de91c39 100644 --- a/README_RU.md +++ b/README_RU.md @@ -30,7 +30,7 @@ _[English](README.md)_ 1. Установите PHP 7.4, веб-сервер, Composer, Node.js, Yarn и [Chandler](https://github.com/openvk/chandler) -* PHP 8 is still being tested; performance on this version of PHP is not yet guaranteed. +* PHP 8 пока ещё тестируется, работоспособность движка на этой версии PHP пока не гарантируется. 2. Установите MySQL-совместимую базу данных. From 9a4309ae01588e9bc2e9926bd538016f32e4bf76 Mon Sep 17 00:00:00 2001 From: Vladimir Barinov Date: Tue, 31 Oct 2023 00:13:36 +0300 Subject: [PATCH 030/134] =?UTF-8?q?=D1=89=D0=B0=D1=81=20=D0=B4=D0=BE=D0=BB?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=20=D0=BD=D0=BE=D1=83=D1=81=D0=BF=D0=B0=D0=BC?= =?UTF-8?q?=20=D0=B7=D0=B0=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=D1=82=D1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/Models/Entities/PasswordReset.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Web/Models/Entities/PasswordReset.php b/Web/Models/Entities/PasswordReset.php index cf0e73ea..372c63f8 100644 --- a/Web/Models/Entities/PasswordReset.php +++ b/Web/Models/Entities/PasswordReset.php @@ -54,11 +54,11 @@ class PasswordReset extends RowModel } } - function save(): void + function save(?bool $log = false): void { $this->stateChanges("key", base64_encode(openssl_random_pseudo_bytes(46))); $this->stateChanges("timestamp", time()); - parent::save(); + parent::save($log); } } From fd11dfcdd919854f2d5c4834413a0a1a81b01bbe Mon Sep 17 00:00:00 2001 From: Vladimir Barinov Date: Tue, 31 Oct 2023 00:14:18 +0300 Subject: [PATCH 031/134] =?UTF-8?q?=D0=B2=D1=8B=20=D0=B4=D0=BE=D0=BB=D0=B1?= =?UTF-8?q?=D0=BE=D1=91=D0=B1=D1=8B=20=D0=B1=D0=BB=D1=8F=D1=82=D1=8C=20?= =?UTF-8?q?=D0=B2=D1=8B=20=D0=BD=D0=B0=D1=85=D1=83=D1=8F=20=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D0=B2=D0=B8=D0=BB=D0=B8=20=D0=B4=D0=B5=D0=B1=D0=B0?= =?UTF-8?q?=D0=B3=20=D1=84=D0=B8=D1=87=D1=83=20=D0=B2=20=D1=80=D0=B5=D0=BB?= =?UTF-8?q?=D0=B8=D0=B7=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Web/Models/Repositories/Messages.php | 1 - 1 file changed, 1 deletion(-) diff --git a/Web/Models/Repositories/Messages.php b/Web/Models/Repositories/Messages.php index 4c870806..538338ed 100644 --- a/Web/Models/Repositories/Messages.php +++ b/Web/Models/Repositories/Messages.php @@ -52,7 +52,6 @@ class Messages $query = file_get_contents(__DIR__ . "/../sql/get-correspondencies-count.tsql"); DatabaseConnection::i()->getConnection()->query(file_get_contents(__DIR__ . "/../sql/mysql-msg-fix.tsql")); $count = DatabaseConnection::i()->getConnection()->query($query, $id, $class, $id, $class)->fetch()->cnt; - bdump($count); return $count; } } From a2473c68fe8522a8fc39a95ab01c1e66eb8e6c2e Mon Sep 17 00:00:00 2001 From: veselcraft Date: Wed, 1 Nov 2023 14:52:24 +0300 Subject: [PATCH 032/134] VKAPI: PHP 8.2 fixes --- Web/Presenters/VKAPIPresenter.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Web/Presenters/VKAPIPresenter.php b/Web/Presenters/VKAPIPresenter.php index 431379c5..963c9ccc 100644 --- a/Web/Presenters/VKAPIPresenter.php +++ b/Web/Presenters/VKAPIPresenter.php @@ -233,8 +233,13 @@ final class VKAPIPresenter extends OpenVKPresenter $this->badMethodCall($object, $method, $parameter->getName()); } - settype($val, $parameter->getType()->getName()); - $params[] = $val; + try { + settype($val, $parameter->getType()->getName()); + $params[] = $val; + } catch (\Throwable $e) { + // Just ignore the exception, since + // some args are intended for internal use + } } define("VKAPI_DECL_VER", $this->requestParam("v") ?? "4.100", false); From 49d62543ba2374a61948176fc36392627e9881df Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Sun, 5 Nov 2023 15:19:06 +0300 Subject: [PATCH 033/134] Fix 500 in photos.get when album does not exists --- VKAPI/Handlers/Photos.php | 13 +++++-------- Web/Models/Repositories/Albums.php | 4 ++-- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/VKAPI/Handlers/Photos.php b/VKAPI/Handlers/Photos.php index e3e9abac..bb1a22f0 100644 --- a/VKAPI/Handlers/Photos.php +++ b/VKAPI/Handlers/Photos.php @@ -432,13 +432,11 @@ final class Photos extends VKAPIRequestHandler if(empty($photo_ids)) { $album = (new Albums)->getAlbumByOwnerAndId($owner_id, $album_id); - if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) { - $this->fail(21, "This user chose to hide his albums."); - } - - if(!$album || $album->isDeleted()) { + if(!$album || $album->isDeleted()) $this->fail(21, "Invalid album"); - } + + if(!$album->getOwner()->getPrivacyPermission('photos.read', $this->getUser())) + $this->fail(21, "This user chose to hide his albums."); $photos = array_slice(iterator_to_array($album->getPhotos(1, $count + $offset)), $offset); $res["count"] = sizeof($photos); @@ -456,8 +454,7 @@ final class Photos extends VKAPIRequestHandler "items" => [] ]; - foreach($photos as $photo) - { + foreach($photos as $photo) { $id = explode("_", $photo); $phot = (new PhotosRepo)->getByOwnerAndVID((int)$id[0], (int)$id[1]); diff --git a/Web/Models/Repositories/Albums.php b/Web/Models/Repositories/Albums.php index b38ee462..f99848c4 100644 --- a/Web/Models/Repositories/Albums.php +++ b/Web/Models/Repositories/Albums.php @@ -130,7 +130,7 @@ class Albums "owner" => $owner, "id" => $id ])->fetch(); - - return new Album($album); + + return $album ? new Album($album) : NULL; } } From c9665ac77d7a7601e18e602e0346af0d4405bf31 Mon Sep 17 00:00:00 2001 From: lalka2018 <99399973+lalka2016@users.noreply.github.com> Date: Tue, 7 Nov 2023 20:32:04 +0300 Subject: [PATCH 034/134] Resolves #1017 --- Web/Models/Entities/User.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index aaf00ec9..dc227039 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -720,8 +720,8 @@ class User extends RowModel for($i = 0; $i < 10 - $this->get2faBackupCodeCount(); $i++) { $codes[] = [ - owner => $this->getId(), - code => random_int(10000000, 99999999) + "owner" => $this->getId(), + "code" => random_int(10000000, 99999999) ]; } From 9d7a465d0d993319bf21b4af941d07b1228f9ef4 Mon Sep 17 00:00:00 2001 From: celestora Date: Sat, 11 Nov 2023 23:41:07 +0200 Subject: [PATCH 035/134] Music, finally! (#512) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add audio upload feature * Add audio embed thing * Move bullet.gif to ovk * Draft some music API methods * Add support for base64 ids to Audios.getById * Disallow having more than 65k audios in playlist * Add playlist model * Draft some playlist-related API methods * Fix behabiour of album-related methods Generators f***** me in le a** * Add IDv3 autofill * Add sql dumps i forgor to upload it xdddd * Add playlists sql * Fix audio upload not working on Windows 11 because Windows is the worst operating system which doesn't work properly under any circumstances * Fix cocksex in audio.get yes * Интерфейсы * Interface updade * Update en.strings * Add audio queue * Make repeat button work * Some improvements to audio queue * Фгвшщ йгугу шьзкщмуьутеы * Make shuffle and "наушники" buttons work, add f... avicons when playing audio, save some values (like volume and last played track) to localstorage, add ability to toggle time type in player, fix uploading audios with cover (maybe) and add dragndrop to upload page * Add funny tip with time when hover track div * Add something * Add audios picker & move track in smal player вниз * Summary (required) Description * [WIP] Add calls, stories and clips. Изменены фавиконки (поменьше стали) У миниплеера ползунок теперь в стиле bsdn и большого плеера, добавлен ползунок громкости Добавлена кнопка добавления аудио в группу (у миниплеера) Если вы смотрите аудио группы, которой можете управлять, появляется кнопка "удалить аудио из группы" Снизу плейлиста в списке теперь показывается автор. При прикреплении аудиозаписей к посту теперь есть поиск "по композиции" и "по исполнителю" Добавил explicit.svg, который я забыл добавить в предыдущем коммите. Вкладочки немного переделаны При наведении на кнопки "трек вперёд" или "трек назад" показывается название предыдущего или следующего трека соответственно * 1 new commit to master: [WIP]: Add audios - Теперь группа может разрешать загружать всем треки в неё - Теперь треки загружаются на сервер ajax'ом, и так можно очень много аудио загружать - Вёрстка списка плейлистов изменена, теперь она на гридах - Немного изменено апи, теперь метод editAlbum сохраняет новую информацию ee объект плейлистов теперь возвращают реальное время - Удалены лишние пути из routes.yml - При переключении страниц теперь если на текущей странице есть играющий трек, он нормально подсвечивается - Из init-db.sql удалены таблицы аудиозаписей - В Groups.getSettings и groups.edit теперь есть информация о аудиозаписях * (смешное название коммита) - Теперь на странице пользователя/группы показываются три случайные песни, а не первые три как раньше - Теперь пробел на странице аудио не перемещает вас в низ страницы - Оптимизирован мини-плеер, теперь он инициализируется при любом нажатии на него, а не при наведении - Теперь при завершении проигрывания трека в мини-плеере он ищет другой трек рядом, и если находит то воспроизводит. Будет удобно для постов с подборками треков - Поиск теперь показывает 14 результатов - Теперь при возникновении ошибки загрузки аудио она нормально отображается - Вместе с плеером на странице с аудиозаписями теперь двигаются и вкладки - Добавление аудио в группу по идее должно нормально работать * Implement playlists listens - У плейлистов теперь есть прослушивания в общем. - Прослушивания у большого плеера теперь засчитываются, если трек был дослушан до конца - В объекте плейлистов теперь возвращается listens и cover_url - Получение плееров через /audios/context переписано, повторяющийся код удалён, правда сильно количество строк сократить не получилось - Теперь цвета плеера темнее, а иконка проигрывания изменена - Теперь, если очередь из треков кончилась, то плеер перенаправляет вас в начало очереди. * php 8.2 fixxxxxxxxxxxxxxxxxxxxxxx * Implement audiostatuses Добавлены аудиостатусы (у пользователей), блок с друзьями, слушающих музыку на странице аудиозаписей, объект status_audio в users.get, улучшены настройки приватности и ещё что-то * ? - Переделан метод в классе user для получения друзей с проигрываемыми песнями. Теперь среди них могут появляться и группы (хз стоит ли оставлять это или нет). Так же больше не показываются удалённые пользователи - Трек у плеера теперь двигается немного плавнее. Ещё теперь нету смешных багов с подсказкой времени, когда можно было увести её за экран или промотать дальше трека. Переключить повторение трека теперь можно нажатием кнопки R. - Длинное название трека больше не сносит время - Наверное, теперь аудиозаписи нормально отображаются в темах midnight и modern - Аудиозаписи больше не крашаются, если пользователь неавторизован. - Немного переделан миниплеер. - В миниплеере теперь громкость берётся из локалсторейджа. - Улучшено редактирование аудиозаписей. Теперь данные в дата атрибуты нормально сохраняются, а так же слова песни и метка "explicit" меняются - Удалён css, оставшийся ещё от public technical preview 1, а так же путь /audios{num} - При наведении на трек теперь пропадает время, и на его месте появляются кнопки - Стандартная аватарка в midnight теперь инвертируется - В админке в редактировании аудио теперь показывается дата редактирования, дата создания, длина и оригинальный файл аудио. Так же на странице редактирования больше нет вылетов, если вы задали несуществующий аккаунт * ! - Добавлены строки для мобильной темы - Добавлено предупреждение перед полным удалением плейлиста - Нажатие кнопки M = нажатие кнопки наушников - В классе апи Audio поставлены willExecuteWriteAction, ещё теперь нельзя получить число аудиозаписей у пользователей, которые их закрыли. Ещё теперь нельзя получать uploaded_only аудиозаписи у тех ну вы поняли короче. - При наведении на длинное название песни оно теперь показывается полностью - Надо ещё что-то сюда написать, так что: При редактировании аудиозаписи название окна теперь не "Редактировать", а "Редактировать аудиозапись", а вместо кнопки OK кнопка "Сохранить" * . - Добавлен тур по аудиозаписям, но пока без скриншотов. - "Мои Аудиозаписи" в меню теперь располагаются под Моими Видеозаписями для канона - В настройках приватности "кто может видеть мои аудиозаписи" теперь располагаются под "кто может видеть мои видеозаписи" - В настройках внешнего вида мои аудиозаписи тоже под видео - Изменён на странице аудиозаписей. Теперь показывается "Аудиозаписи" + имя пользователя в родительном падеже. А если это группа, то "Аудиозаписи группы". То же самое с плейлистами - Исправлены ссылка в ссылке на странице с плейлистами - При наведении на название песни больше не сносится иконка explicit - Добавлена максимальная длина названия и описания плейлиста при редактировании. * М - Долокализована админка (точно помню, что уже делал это, но ладно) - Удалён лишний пункт "audios" в getLeftMenuItemStatus (реально) - Если. У плеера есть параметр "hideButtons", то при наведении на него не пропадает время. - На странице редактирования/создания плейлиста если у песни длинное название, то оно да похуй короче. Ну в общем лучше стало - Там где нужно, добавлена строка в конце файла - Возвращена строка "photo" в английской локали (я её случайно удалил :+1: ) * у - У изъятых аудиозаписей больше не показывается кнопка "добавить в группу". Так же при нажатии на кнопку удаления из коллекции окно не всплывает. - "Удаление аудио из группы" тоже лучше работать стало с изъятыми аудио. * з - В пикере аудиозаписей "more..." заменено на "показать больше аудиозаписей" - Если включен режим показа оставшегося времени, то при окончании песни больше не показывается "--1:--1" - В пикере аудиозаписей, если у вас нет аудиозаписей и вы ничего не искали, показывается "Вы ещё не добавляли аудиозаписей" - <hr>'ы стали серыми - Добавлены title'ы у кнопок в большом плеере - Проставлены alt'ы у плейлистов * Musique: linux saport) назар хуйню релизнул кста, плейерс клаб два не слушайте не рекомендую * Update and rename gamma-00000-disco.sql to 00041-music.sql * Update 00041-music.sql --------- Co-authored-by: Ilya Prokopenko <dsrev@protonmail.com> Co-authored-by: n1rwana <aydashkin@vk.com> Co-authored-by: lalka2018 <99399973+lalka2016@users.noreply.github.com> Co-authored-by: veselcraft <veselcraft@icloud.com> Co-authored-by: DeathPleiad <43928323+Parad1seF0x@users.noreply.github.com> --- VKAPI/Handlers/Audio.php | 794 ++++++++- VKAPI/Handlers/Groups.php | 32 +- VKAPI/Handlers/Status.php | 24 +- VKAPI/Handlers/Users.php | 6 + VKAPI/Handlers/Wall.php | 25 + Web/Models/Entities/Audio.php | 469 ++++++ Web/Models/Entities/Club.php | 38 +- Web/Models/Entities/MediaCollection.php | 72 +- Web/Models/Entities/Playlist.php | 256 +++ Web/Models/Entities/Report.php | 3 +- Web/Models/Entities/Traits/TAudioStatuses.php | 38 + Web/Models/Entities/Traits/TOwnable.php | 6 + Web/Models/Entities/User.php | 55 +- Web/Models/Entities/Video.php | 2 +- Web/Models/Repositories/Audios.php | 296 ++++ Web/Models/shell/processAudio.ps1 | 39 + Web/Models/shell/processAudio.sh | 35 + Web/Presenters/AdminPresenter.php | 77 +- Web/Presenters/AudioPresenter.php | 696 ++++++++ Web/Presenters/BlobPresenter.php | 4 +- Web/Presenters/CommentPresenter.php | 26 +- Web/Presenters/GroupPresenter.php | 5 +- Web/Presenters/ReportPresenter.php | 4 +- Web/Presenters/SearchPresenter.php | 31 +- Web/Presenters/UserPresenter.php | 11 +- Web/Presenters/WallPresenter.php | 26 +- Web/Presenters/templates/@layout.xml | 14 +- Web/Presenters/templates/About/Tour.xml | 24 +- Web/Presenters/templates/Admin/@layout.xml | 3 + Web/Presenters/templates/Admin/EditMusic.xml | 81 + .../templates/Admin/EditPlaylist.xml | 54 + Web/Presenters/templates/Admin/Music.xml | 135 ++ .../templates/Audio/ApiGetContext.xml | 7 + .../templates/Audio/EditPlaylist.xml | 95 ++ Web/Presenters/templates/Audio/Embed.xml | 20 + Web/Presenters/templates/Audio/List.xml | 126 ++ .../templates/Audio/NewPlaylist.xml | 109 ++ Web/Presenters/templates/Audio/Playlist.xml | 81 + Web/Presenters/templates/Audio/Upload.xml | 212 +++ Web/Presenters/templates/Audio/bigplayer.xml | 56 + Web/Presenters/templates/Audio/player.xml | 69 + Web/Presenters/templates/Audio/tabs.xml | 40 + Web/Presenters/templates/Group/Edit.xml | 9 + Web/Presenters/templates/Group/View.xml | 19 + .../templates/Photos/UploadPhoto.xml | 2 +- Web/Presenters/templates/Report/Tabs.xml | 3 + .../templates/Report/ViewContent.xml | 2 + Web/Presenters/templates/Search/Index.xml | 42 +- Web/Presenters/templates/User/Edit.xml | 7 + Web/Presenters/templates/User/Settings.xml | 24 + Web/Presenters/templates/User/View.xml | 46 +- Web/Presenters/templates/_includeCSS.xml | 4 +- .../templates/components/attachment.xml | 4 + .../templates/components/textArea.xml | 7 + Web/di.yml | 2 + Web/routes.yml | 52 +- Web/static/css/audios.css | 661 ++++++++ Web/static/css/main.css | 229 +-- Web/static/img/audio.png | Bin 0 -> 560 bytes Web/static/img/audios_controls.png | Bin 0 -> 3706 bytes Web/static/img/bullet.gif | Bin 0 -> 53 bytes Web/static/img/explicit.svg | 4 + Web/static/img/favicons/favicon24_paused.png | Bin 0 -> 932 bytes Web/static/img/favicons/favicon24_playing.png | Bin 0 -> 1081 bytes Web/static/img/play_buttons.gif | Bin 0 -> 103 bytes Web/static/img/progressbar.gif | Bin 0 -> 1018 bytes Web/static/img/song.jpg | Bin 0 -> 2481 bytes Web/static/img/tour/audios.png | Bin 0 -> 6234 bytes Web/static/img/tour/audios_playlists.png | Bin 0 -> 6234 bytes Web/static/img/tour/audios_search.png | Bin 0 -> 6234 bytes Web/static/img/tour/audios_upload.png | Bin 0 -> 6234 bytes Web/static/js/al_music.js | 1433 +++++++++++++++++ Web/static/js/al_playlists.js | 113 ++ Web/static/js/al_wall.js | 24 +- Web/static/js/package.json | 2 + Web/static/js/yarn.lock | 67 + bootstrap.php | 27 + composer.json | 1 + install/init-static-db.sql | 21 - install/sqls/00041-music.sql | 100 ++ locales/en.strings | 165 +- locales/ru.strings | 165 +- openvk-example.yml | 2 + themepacks/midnight/stylesheet.css | 127 +- themepacks/openvk_modern/stylesheet.css | 47 +- 85 files changed, 7333 insertions(+), 274 deletions(-) create mode 100644 Web/Models/Entities/Audio.php create mode 100644 Web/Models/Entities/Playlist.php create mode 100644 Web/Models/Entities/Traits/TAudioStatuses.php create mode 100644 Web/Models/Repositories/Audios.php create mode 100644 Web/Models/shell/processAudio.ps1 create mode 100644 Web/Models/shell/processAudio.sh create mode 100644 Web/Presenters/AudioPresenter.php create mode 100644 Web/Presenters/templates/Admin/EditMusic.xml create mode 100644 Web/Presenters/templates/Admin/EditPlaylist.xml create mode 100644 Web/Presenters/templates/Admin/Music.xml create mode 100644 Web/Presenters/templates/Audio/ApiGetContext.xml create mode 100644 Web/Presenters/templates/Audio/EditPlaylist.xml create mode 100644 Web/Presenters/templates/Audio/Embed.xml create mode 100644 Web/Presenters/templates/Audio/List.xml create mode 100644 Web/Presenters/templates/Audio/NewPlaylist.xml create mode 100644 Web/Presenters/templates/Audio/Playlist.xml create mode 100644 Web/Presenters/templates/Audio/Upload.xml create mode 100644 Web/Presenters/templates/Audio/bigplayer.xml create mode 100644 Web/Presenters/templates/Audio/player.xml create mode 100644 Web/Presenters/templates/Audio/tabs.xml create mode 100644 Web/static/css/audios.css create mode 100644 Web/static/img/audio.png create mode 100644 Web/static/img/audios_controls.png create mode 100644 Web/static/img/bullet.gif create mode 100644 Web/static/img/explicit.svg create mode 100644 Web/static/img/favicons/favicon24_paused.png create mode 100644 Web/static/img/favicons/favicon24_playing.png create mode 100644 Web/static/img/play_buttons.gif create mode 100644 Web/static/img/progressbar.gif create mode 100644 Web/static/img/song.jpg create mode 100644 Web/static/img/tour/audios.png create mode 100644 Web/static/img/tour/audios_playlists.png create mode 100644 Web/static/img/tour/audios_search.png create mode 100644 Web/static/img/tour/audios_upload.png create mode 100644 Web/static/js/al_music.js create mode 100644 Web/static/js/al_playlists.js create mode 100644 install/sqls/00041-music.sql diff --git a/VKAPI/Handlers/Audio.php b/VKAPI/Handlers/Audio.php index 3fa68e72..413a2a3a 100644 --- a/VKAPI/Handlers/Audio.php +++ b/VKAPI/Handlers/Audio.php @@ -1,22 +1,788 @@ <?php declare(strict_types=1); namespace openvk\VKAPI\Handlers; +use Chandler\Database\DatabaseConnection; +use openvk\Web\Models\Entities\Audio as AEntity; +use openvk\Web\Models\Entities\Playlist; +use openvk\Web\Models\Repositories\Audios; +use openvk\Web\Models\Repositories\Clubs; +use openvk\Web\Models\Repositories\Util\EntityStream; final class Audio extends VKAPIRequestHandler { - function get(): object + private function toSafeAudioStruct(?AEntity $audio, ?string $hash = NULL, bool $need_user = false): object + { + if(!$audio) + $this->fail(0404, "Audio not found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to audio(" . $audio->getPrettyId() . ")"); + + # рофлан ебало + $privApi = $hash && $GLOBALS["csrfCheck"]; + $audioObj = $audio->toVkApiStruct($this->getUser()); + if(!$privApi) { + $audioObj->manifest = false; + $audioObj->keys = false; + } + + if($need_user) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($audio->getOwner()->getId()); + $audioObj->user = (object) [ + "id" => $user->getId(), + "photo" => $user->getAvatarUrl(), + "name" => $user->getCanonicalName(), + "name_gen" => $user->getCanonicalName(), + ]; + } + + return $audioObj; + } + + private function streamToResponse(EntityStream $es, int $offset, int $count, ?string $hash = NULL): object + { + $items = []; + foreach($es->offsetLimit($offset, $count) as $audio) { + $items[] = $this->toSafeAudioStruct($audio, $hash); + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + private function validateGenre(?string& $genre_str, ?int $genre_id): void + { + if(!is_null($genre_str)) { + if(!in_array($genre_str, AEntity::genres)) + $this->fail(8, "Invalid genre_str"); + } else if(!is_null($genre_id)) { + $genre_str = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL; + if(!$genre_str) + $this->fail(8, "Invalid genre ID $genre_id"); + } + } + + private function audioFromAnyId(string $id): ?AEntity + { + $descriptor = explode("_", $id); + if(sizeof($descriptor) === 1) { + if(ctype_digit($descriptor[0])) { + $audio = (new Audios)->get((int) $descriptor[0]); + } else { + $aid = base64_decode($descriptor[0], true); + if(!$aid) + $this->fail(8, "Invalid audio $id"); + + $audio = (new Audios)->get((int) $aid); + } + } else if(sizeof($descriptor) === 2) { + $audio = (new Audios)->getByOwnerAndVID((int) $descriptor[0], (int) $descriptor[1]); + } else { + $this->fail(8, "Invalid audio $id"); + } + + return $audio; + } + + function getById(string $audios, ?string $hash = NULL, int $need_user = 0): object + { + $this->requireUser(); + + $audioIds = array_unique(explode(",", $audios)); + if(sizeof($audioIds) === 1) { + $audio = $this->audioFromAnyId($audioIds[0]); + + return (object) [ + "count" => 1, + "items" => [ + $this->toSafeAudioStruct($audio, $hash, (bool) $need_user), + ], + ]; + } else if(sizeof($audioIds) > 6000) { + $this->fail(1980, "Can't get more than 6000 audios at once"); + } + + $audios = []; + foreach($audioIds as $id) + $audios[] = $this->getById($id, $hash)->items[0]; + + return (object) [ + "count" => sizeof($audios), + "items" => $audios, + ]; + } + + function isLagtrain(string $audio_id): int + { + $this->requireUser(); + + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + $this->fail(0404, "Audio not found"); + + # Possible information disclosure risks are acceptable :D + return (int) (strpos($audio->getName(), "Lagtrain") !== false); + } + + // TODO stub + function getRecommendations(): object + { + return (object) [ + "count" => 0, + "items" => [], + ]; + } + + function getPopular(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios)->getGlobal(Audios::ORDER_POPULAR, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function getFeed(?int $genre_id = NULL, ?string $genre_str = NULL, int $offset = 0, int $count = 100, ?string $hash = NULL): object + { + $this->requireUser(); + $this->validateGenre($genre_str, $genre_id); + + $results = (new Audios)->getGlobal(Audios::ORDER_NEW, $genre_str); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function search(string $q, int $auto_complete = 0, int $lyrics = 0, int $performer_only = 0, int $sort = 2, int $search_own = 0, int $offset = 0, int $count = 30, ?string $hash = NULL): object + { + $this->requireUser(); + + if(($auto_complete + $search_own) != 0) + $this->fail(10, "auto_complete and search_own are not supported"); + else if($count > 300 || $count < 1) + $this->fail(8, "count is invalid: $count"); + + $results = (new Audios)->search($q, $sort, (bool) $performer_only, (bool) $lyrics); + + return $this->streamToResponse($results, $offset, $count, $hash); + } + + function getCount(int $owner_id, int $uploaded_only = 0): int + { + $this->requireUser(); + + if($owner_id < 0) { + $owner_id *= -1; + $group = (new Clubs)->get($owner_id); + if(!$group) + $this->fail(0404, "Group not found"); + + return (new Audios)->getClubCollectionSize($group); + } + + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + if(!$user) + $this->fail(0404, "User not found"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied"); + + if($uploaded_only) { + return DatabaseConnection::i()->getContext()->table("audios") + ->where([ + "deleted" => false, + "owner" => $owner_id, + ])->count(); + } + + return (new Audios)->getUserCollectionSize($user); + } + + function get(int $owner_id = 0, int $album_id = 0, string $audio_ids = '', int $need_user = 1, int $offset = 0, int $count = 100, int $uploaded_only = 0, int $need_seed = 0, ?string $shuffle_seed = NULL, int $shuffle = 0, ?string $hash = NULL): object { - $serverUrl = ovk_scheme(true) . $_SERVER["SERVER_NAME"]; - - return (object) [ - "count" => 1, - "items" => [(object) [ - "id" => 1, - "owner_id" => 1, - "artist" => "В ОВК ПОКА НЕТ МУЗЫКИ", - "title" => "ЖДИТЕ :)))", - "duration" => 22, - "url" => $serverUrl . "/assets/packages/static/openvk/audio/nomusic.mp3" - ]] - ]; + $this->requireUser(); + + $shuffleSeed = NULL; + $shuffleSeedStr = NULL; + if($shuffle == 1) { + if(!$shuffle_seed) { + if($need_seed == 1) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeedStr = base64_encode($shuffleSeed); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + } else { + $hOffset = ((int) date("i") * 60) + (int) date("s"); + $thisHour = time() - $hOffset; + $shuffleSeed = $thisHour + $this->getUser()->getId(); + $shuffleSeedStr = base64_encode(hex2bin(dechex($shuffleSeed))); + } + } else { + $shuffleSeed = hexdec(bin2hex(base64_decode($shuffle_seed))); + $shuffleSeedStr = $shuffle_seed; + } + } + + if($album_id != 0) { + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "album_id invalid"); + else if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Can't open this album for reading"); + + $songs = []; + $list = $album->getAudios($offset, $count, $shuffleSeed); + + foreach($list as $song) + $songs[] = $this->toSafeAudioStruct($song, $hash, $need_user == 1); + + $response = (object) [ + "count" => sizeof($songs), + "items" => $songs, + ]; + if(!is_null($shuffleSeed)) + $response->shuffle_seed = $shuffleSeedStr; + + return $response; + } + + if(!empty($audio_ids)) { + $audio_ids = explode(",", $audio_ids); + if(!$audio_ids) + $this->fail(10, "Audio::get@L0d186:explode(string): Unknown error"); + else if(sizeof($audio_ids) < 1) + $this->fail(8, "Invalid audio_ids syntax"); + + if(!is_null($shuffleSeed)) + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + + $obj = $this->getById(implode(",", $audio_ids), $hash, $need_user); + if(!is_null($shuffleSeed)) + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $dbCtx = DatabaseConnection::i()->getContext(); + if($uploaded_only == 1) { + if($owner_id <= 0) + $this->fail(8, "uploaded_only can only be used with owner_id > 0"); + + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user) + $this->fail(0602, "Invalid user"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his audios"); + + if(!is_null($shuffleSeed)) { + $audio_ids = []; + $query = $dbCtx->table("audios")->select("virtual_id")->where([ + "owner" => $owner_id, + "deleted" => 0, + ]); + + foreach($query as $res) + $audio_ids[] = $res->virtual_id; + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; # audio.getById query + foreach($audio_ids as $aid) + $audio_q .= ",$owner_id" . "_$aid"; + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $res = (new Audios)->getByUploader((new \openvk\Web\Models\Repositories\Users)->get($owner_id)); + + return $this->streamToResponse($res, $offset, $count, $hash, $need_user); + } + + $query = $dbCtx->table("audio_relations")->select("audio")->where("entity", $owner_id); + if(!is_null($shuffleSeed)) { + $audio_ids = []; + foreach($query as $aid) + $audio_ids[] = $aid->audio; + + $audio_ids = knuth_shuffle($audio_ids, $shuffleSeed); + $audio_ids = array_slice($audio_ids, $offset, $count); + $audio_q = ""; + foreach($audio_ids as $aid) + $audio_q .= ",$aid"; + + $obj = $this->getById(substr($audio_q, 1), $hash, $need_user); + $obj->shuffle_seed = $shuffleSeedStr; + + return $obj; + } + + $items = []; + + if($owner_id > 0) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user) + $this->fail(50, "Invalid user"); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(15, "Access denied: this user chose to hide his audios"); + } + + $audios = (new Audios)->getByEntityID($owner_id, $offset, $count); + foreach($audios as $audio) + $items[] = $this->toSafeAudioStruct($audio, $hash, $need_user == 1); + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; } + + function getLyrics(int $lyrics_id): object + { + $this->requireUser(); + + $audio = (new Audios)->get($lyrics_id); + if(!$audio || !$audio->getLyrics()) + $this->fail(0404, "Not found"); + + if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to lyrics"); + + return (object) [ + "lyrics_id" => $lyrics_id, + "text" => preg_replace("%\r\n?%", "\n", $audio->getLyrics()), + ]; + } + + function beacon(int $aid, ?int $gid = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios)->get($aid); + if(!$audio) + $this->fail(0404, "Not Found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Insufficient permissions to listen this audio"); + + $group = NULL; + if(!is_null($gid)) { + $group = (new Clubs)->get($gid); + if(!$group) + $this->fail(0404, "Not Found"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + } + + return (int) $audio->listen($group ?? $this->getUser()); + } + + function setBroadcast(string $audio, string $target_ids): array + { + $this->requireUser(); + + [$owner, $aid] = explode("_", $audio); + $song = (new Audios)->getByOwnerAndVID((int) $owner, (int) $aid); + $ids = []; + foreach(explode(",", $target_ids) as $id) { + $id = (int) $id; + if($id > 0) { + if ($id != $this->getUser()->getId()) { + $this->fail(600, "Can't listen on behalf of $id"); + } else { + $ids[] = $id; + $this->beacon($song->getId()); + continue; + } + } + + $group = (new Clubs)->get($id * -1); + if(!$group) + $this->fail(0404, "Not Found"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203,"Insufficient rights to this group"); + + $ids[] = $id; + $this->beacon($song ? $song->getId() : 0, $id * -1); + } + + return $ids; + } + + function getBroadcastList(string $filter = "all", int $active = 0, ?string $hash = NULL): object + { + $this->requireUser(); + + if(!in_array($filter, ["all", "friends", "groups"])) + $this->fail(8, "Invalid filter $filter"); + + $broadcastList = $this->getUser()->getBroadcastList($filter); + $items = []; + foreach($broadcastList as $res) { + $struct = $res->toVkApiStruct(); + $status = $res->getCurrentAudioStatus(); + + $struct->status_audio = $status ? $this->toSafeAudioStruct($status) : NULL; + $items[] = $struct; + } + + return (object) [ + "count" => sizeof($items), + "items" => $items, + ]; + } + + function edit(int $owner_id, int $audio_id, ?string $artist = NULL, ?string $title = NULL, ?string $text = NULL, ?int $genre_id = NULL, ?string $genre_str = NULL, int $no_search = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not Found"); + else if(!$audio->canBeModifiedBy($this->getUser())) + $this->fail(201, "Insufficient permissions to edit this audio"); + + if(!is_null($genre_id)) { + $genre = array_flip(AEntity::vkGenres)[$genre_id] ?? NULL; + if(!$genre) + $this->fail(8, "Invalid genre ID $genre_id"); + + $audio->setGenre($genre); + } else if(!is_null($genre_str)) { + if(!in_array($genre_str, AEntity::genres)) + $this->fail(8, "Invalid genre ID $genre_str"); + + $audio->setGenre($genre_str); + } + + $lyrics = 0; + if(!is_null($text)) { + $audio->setLyrics($text); + $lyrics = $audio->getId(); + } + + if(!is_null($artist)) + $audio->setPerformer($artist); + + if(!is_null($title)) + $audio->setName($title); + + $audio->setSearchability(!((bool) $no_search)); + $audio->setEdited(time()); + $audio->save(); + + return $lyrics; + } + + function add(int $audio_id, int $owner_id, ?int $group_id = NULL, ?int $album_id = NULL): string + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + if(!is_null($album_id)) + $this->fail(10, "album_id not implemented"); + + // TODO get rid of dups + $to = $this->getUser(); + if(!is_null($group_id)) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + + $to = $group; + } + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not found"); + else if(!$audio->canBeViewedBy($this->getUser())) + $this->fail(201, "Access denied to audio(owner=$owner_id, vid=$audio_id)"); + + try { + $audio->add($to); + } catch(\OverflowException $ex) { + $this->fail(300, "Album is full"); + } + + return $audio->getPrettyId(); + } + + function delete(int $audio_id, int $owner_id, ?int $group_id = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $from = $this->getUser(); + if(!is_null($group_id)) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(203, "Insufficient rights to this group"); + + $from = $group; + } + + $audio = (new Audios)->getByOwnerAndVID($owner_id, $audio_id); + if(!$audio) + $this->fail(0404, "Not found"); + + $audio->remove($from); + + return 1; + } + + function restore(int $audio_id, int $owner_id, ?int $group_id = NULL, ?string $hash = NULL): object + { + $this->requireUser(); + + $vid = $this->add($audio_id, $owner_id, $group_id); + + return $this->getById($vid, $hash)->items[0]; + } + + function getAlbums(int $owner_id = 0, int $offset = 0, int $count = 50, int $drop_private = 1): object + { + $this->requireUser(); + + $owner_id = $owner_id == 0 ? $this->getUser()->getId() : $owner_id; + $playlists = []; + + if($owner_id > 0 && $owner_id != $this->getUser()->getId()) { + $user = (new \openvk\Web\Models\Repositories\Users)->get($owner_id); + + if(!$user->getPrivacyPermission("audios.read", $this->getUser())) + $this->fail(50, "Access to playlists denied"); + } + + foreach((new Audios)->getPlaylistsByEntityId($owner_id, $offset, $count) as $playlist) { + if(!$playlist->canBeViewedBy($this->getUser())) { + if($drop_private == 1) + continue; + + $playlists[] = NULL; + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => sizeof($playlists), + "items" => $playlists, + ]; + } + + function searchAlbums(string $query, int $offset = 0, int $limit = 25, int $drop_private = 0): object + { + $this->requireUser(); + + $playlists = []; + $search = (new Audios)->searchPlaylists($query)->offsetLimit($offset, $limit); + foreach($search as $playlist) { + if(!$playlist->canBeViewedBy($this->getUser())) { + if($drop_private == 0) + $playlists[] = NULL; + + continue; + } + + $playlists[] = $playlist->toVkApiStruct($this->getUser()); + } + + return (object) [ + "count" => sizeof($playlists), + "items" => $playlists, + ]; + } + + function addAlbum(string $title, ?string $description = NULL, int $group_id = 0): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $group = NULL; + if($group_id != 0) { + $group = (new Clubs)->get($group_id); + if(!$group) + $this->fail(0404, "Invalid group_id"); + else if(!$group->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this group"); + } + + $album = new Playlist; + $album->setName($title); + if(!is_null($group)) + $album->setOwner($group_id * -1); + else + $album->setOwner($this->getUser()->getId()); + + if(!is_null($description)) + $album->setDescription($description); + + $album->save(); + if(!is_null($group)) + $album->bookmark($group); + else + $album->bookmark($this->getUser()); + + return $album->getId(); + } + + function editAlbum(int $album_id, ?string $title = NULL, ?string $description = NULL): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + if(!is_null($title)) + $album->setName($title); + + if(!is_null($description)) + $album->setDescription($description); + + $album->setEdited(time()); + $album->save(); + + return (int) !(!$title && !$description); + } + + function deleteAlbum(int $album_id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $album->delete(); + + return 1; + } + + function moveToAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + + foreach($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + continue; + else if(!$audio->canBeViewedBy($this->getUser())) + continue; + + $audios[] = $audio; + } + + if(sizeof($audios) < 1) + return 0; + + $res = 1; + try { + foreach ($audios as $audio) + $res = min($res, (int) $album->add($audio)); + } catch(\OutOfBoundsException $ex) { + return 0; + } + + return $res; + } + + function removeFromAlbum(int $album_id, string $audio_ids): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($album_id); + if(!$album) + $this->fail(0404, "Album not found"); + else if(!$album->canBeModifiedBy($this->getUser())) + $this->fail(600, "Insufficient rights to this album"); + + $audios = []; + $audio_ids = array_unique(explode(",", $audio_ids)); + if(sizeof($audio_ids) < 1 || sizeof($audio_ids) > 1000) + $this->fail(8, "audio_ids must contain at least 1 audio and at most 1000"); + + foreach($audio_ids as $audio_id) { + $audio = $this->audioFromAnyId($audio_id); + if(!$audio) + continue; + else if($audio->canBeViewedBy($this->getUser())) + continue; + + $audios[] = $audio; + } + + if(sizeof($audios) < 1) + return 0; + + foreach($audios as $audio) + $album->remove($audio); + + return 1; + } + + function copyToAlbum(int $album_id, string $audio_ids): int + { + return $this->moveToAlbum($album_id, $audio_ids); + } + + function bookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($id); + if(!$album) + $this->fail(0404, "Not found"); + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Access error"); + + return (int) $album->bookmark($this->getUser()); + } + + function unBookmarkAlbum(int $id): int + { + $this->requireUser(); + $this->willExecuteWriteAction(); + + $album = (new Audios)->getPlaylist($id); + if(!$album) + $this->fail(0404, "Not found"); + + if(!$album->canBeViewedBy($this->getUser())) + $this->fail(600, "Access error"); + + return (int) $album->unbookmark($this->getUser()); + } } diff --git a/VKAPI/Handlers/Groups.php b/VKAPI/Handlers/Groups.php index 3123a43f..85a251da 100644 --- a/VKAPI/Handlers/Groups.php +++ b/VKAPI/Handlers/Groups.php @@ -292,7 +292,8 @@ final class Groups extends VKAPIRequestHandler int $topics = NULL, int $adminlist = NULL, int $topicsAboveWall = NULL, - int $hideFromGlobalFeed = NULL) + int $hideFromGlobalFeed = NULL, + int $audio = NULL) { $this->requireUser(); $this->willExecuteWriteAction(); @@ -303,17 +304,22 @@ final class Groups extends VKAPIRequestHandler if(!$club || !$club->canBeModifiedBy($this->getUser())) $this->fail(15, "You can't modify this group."); if(!empty($screen_name) && !$club->setShortcode($screen_name)) $this->fail(103, "Invalid shortcode."); - !is_null($title) ? $club->setName($title) : NULL; - !is_null($description) ? $club->setAbout($description) : NULL; - !is_null($screen_name) ? $club->setShortcode($screen_name) : NULL; - !is_null($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; - !is_null($wall) ? $club->setWall($wall) : NULL; - !is_null($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; - !is_null($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; - !is_null($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; - !is_null($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + !empty($title) ? $club->setName($title) : NULL; + !empty($description) ? $club->setAbout($description) : NULL; + !empty($screen_name) ? $club->setShortcode($screen_name) : NULL; + !empty($website) ? $club->setWebsite((!parse_url($website, PHP_URL_SCHEME) ? "https://" : "") . $website) : NULL; + !empty($wall) ? $club->setWall($wall) : NULL; + !empty($topics) ? $club->setEveryone_Can_Create_Topics($topics) : NULL; + !empty($adminlist) ? $club->setAdministrators_List_Display($adminlist) : NULL; + !empty($topicsAboveWall) ? $club->setDisplay_Topics_Above_Wall($topicsAboveWall) : NULL; + !empty($hideFromGlobalFeed) ? $club->setHide_From_Global_Feed($hideFromGlobalFeed) : NULL; + in_array($audio, [0, 1]) ? $club->setEveryone_can_upload_audios($audio) : NULL; - $club->save(); + try { + $club->save(); + } catch(\TypeError $e) { + $this->fail(8, "Nothing changed"); + } return 1; } @@ -370,7 +376,7 @@ final class Groups extends VKAPIRequestHandler $arr->items[$i]->can_see_all_posts = 1; break; case "can_see_audio": - $arr->items[$i]->can_see_audio = 0; + $arr->items[$i]->can_see_audio = 1; break; case "can_write_private_message": $arr->items[$i]->can_write_private_message = 0; @@ -469,7 +475,7 @@ final class Groups extends VKAPIRequestHandler "wall" => $club->canPost() == true ? 1 : 0, "photos" => 1, "video" => 0, - "audio" => 0, + "audio" => $club->isEveryoneCanUploadAudios() ? 1 : 0, "docs" => 0, "topics" => $club->isEveryoneCanCreateTopics() == true ? 1 : 0, "wiki" => 0, diff --git a/VKAPI/Handlers/Status.php b/VKAPI/Handlers/Status.php index 843f42bd..a1b104a2 100644 --- a/VKAPI/Handlers/Status.php +++ b/VKAPI/Handlers/Status.php @@ -8,13 +8,23 @@ final class Status extends VKAPIRequestHandler function get(int $user_id = 0, int $group_id = 0) { $this->requireUser(); - if($user_id == 0 && $group_id == 0) { - return $this->getUser()->getStatus(); - } else { - if($group_id > 0) - $this->fail(501, "Group statuses are not implemented"); - else - return (new UsersRepo)->get($user_id)->getStatus(); + + if($user_id == 0 && $group_id == 0) + $user_id = $this->getUser()->getId(); + + if($group_id > 0) + $this->fail(501, "Group statuses are not implemented"); + else { + $user = (new UsersRepo)->get($user_id); + $audioStatus = $user->getCurrentAudioStatus(); + if($audioStatus) { + return [ + "status" => $user->getStatus(), + "audio" => $audioStatus->toVkApiStruct(), + ]; + } + + return $user->getStatus(); } } diff --git a/VKAPI/Handlers/Users.php b/VKAPI/Handlers/Users.php index a4298787..68bf828f 100644 --- a/VKAPI/Handlers/Users.php +++ b/VKAPI/Handlers/Users.php @@ -95,6 +95,12 @@ final class Users extends VKAPIRequestHandler case "status": if($usr->getStatus() != NULL) $response[$i]->status = $usr->getStatus(); + + $audioStatus = $usr->getCurrentAudioStatus(); + + if($audioStatus) + $response[$i]->status_audio = $audioStatus->toVkApiStruct(); + break; case "screen_name": if($usr->getShortCode() != NULL) diff --git a/VKAPI/Handlers/Wall.php b/VKAPI/Handlers/Wall.php index 6b78a0b0..693ca3cb 100644 --- a/VKAPI/Handlers/Wall.php +++ b/VKAPI/Handlers/Wall.php @@ -15,6 +15,7 @@ use openvk\Web\Models\Entities\Video; use openvk\Web\Models\Repositories\Videos as VideosRepo; use openvk\Web\Models\Entities\Note; use openvk\Web\Models\Repositories\Notes as NotesRepo; +use openvk\Web\Models\Repositories\Audios as AudiosRepo; final class Wall extends VKAPIRequestHandler { @@ -58,6 +59,8 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $attachment->getApiStructure(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = $attachment->toVkApiStruct($this->getUser()); } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -233,6 +236,8 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $attachment->getApiStructure(); } else if ($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } else if ($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = $attachment->toVkApiStruct($this->getUser()); } else if ($attachment instanceof \openvk\Web\Models\Entities\Post) { $repostAttachments = []; @@ -450,6 +455,8 @@ final class Wall extends VKAPIRequestHandler $attachmentType = "video"; elseif(str_contains($attac, "note")) $attachmentType = "note"; + elseif(str_contains($attac, "audio")) + $attachmentType = "audio"; else $this->fail(205, "Unknown attachment type"); @@ -483,6 +490,12 @@ final class Wall extends VKAPIRequestHandler if(!$attacc->getOwner()->getPrivacyPermission('notes.read', $this->getUser())) $this->fail(11, "Access to note denied"); + $post->attach($attacc); + } elseif($attachmentType == "audio") { + $attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); + if(!$attacc || $attacc->isDeleted()) + $this->fail(100, "Audio does not exist"); + $post->attach($attacc); } } @@ -562,6 +575,8 @@ final class Wall extends VKAPIRequestHandler $attachments[] = $this->getApiPhoto($attachment); } elseif($attachment instanceof \openvk\Web\Models\Entities\Note) { $attachments[] = $attachment->toVkApiStruct(); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = $attachment->toVkApiStruct($this->getUser()); } } @@ -628,6 +643,8 @@ final class Wall extends VKAPIRequestHandler foreach($comment->getChildren() as $attachment) { if($attachment instanceof \openvk\Web\Models\Entities\Photo) { $attachments[] = $this->getApiPhoto($attachment); + } elseif($attachment instanceof \openvk\Web\Models\Entities\Audio) { + $attachments[] = $attachment->toVkApiStruct($this->getUser()); } } @@ -719,6 +736,8 @@ final class Wall extends VKAPIRequestHandler $attachmentType = "photo"; elseif(str_contains($attac, "video")) $attachmentType = "video"; + elseif(str_contains($attac, "audio")) + $attachmentType = "audio"; else $this->fail(205, "Unknown attachment type"); @@ -744,6 +763,12 @@ final class Wall extends VKAPIRequestHandler if(!$attacc->getOwner()->getPrivacyPermission('videos.read', $this->getUser())) $this->fail(11, "Access to video denied"); + $comment->attach($attacc); + } elseif($attachmentType == "audio") { + $attacc = (new AudiosRepo)->getByOwnerAndVID($attachmentOwner, $attachmentId); + if(!$attacc || $attacc->isDeleted()) + $this->fail(100, "Audio does not exist"); + $comment->attach($attacc); } } diff --git a/Web/Models/Entities/Audio.php b/Web/Models/Entities/Audio.php new file mode 100644 index 00000000..11d8502b --- /dev/null +++ b/Web/Models/Entities/Audio.php @@ -0,0 +1,469 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Models\Entities; +use Chandler\Database\DatabaseConnection; +use openvk\Web\Util\Shell\Exceptions\UnknownCommandException; +use openvk\Web\Util\Shell\Shell; + +/** + * @method setName(string) + * @method setPerformer(string) + * @method setLyrics(string) + * @method setExplicit(bool) + */ +class Audio extends Media +{ + protected $tableName = "audios"; + protected $fileExtension = "mpd"; + + # Taken from winamp :D + const genres = [ + 'Blues','Big Band','Classic Rock','Chorus','Country','Easy Listening','Dance','Acoustic','Disco','Humour','Funk','Speech','Grunge','Chanson','Hip-Hop','Opera','Jazz','Chamber Music','Metal','Sonata','New Age','Symphony','Oldies','Booty Bass','Other','Primus','Pop','Porn Groove','R&B','Satire','Rap','Slow Jam','Reggae','Club','Rock','Tango','Techno','Samba','Industrial','Folklore','Alternative','Ballad','Ska','Power Ballad','Death Metal','Rhythmic Soul','Pranks','Freestyle','Soundtrack','Duet','Euro-Techno','Punk Rock','Ambient','Drum Solo','Trip-Hop','A Cappella','Vocal','Euro-House','Jazz+Funk','Dance Hall','Fusion','Goa','Trance','Drum & Bass','Classical','Club-House','Instrumental','Hardcore','Acid','Terror','House','Indie','Game','BritPop','Sound Clip','Negerpunk','Gospel','Polsk Punk','Noise','Beat','AlternRock','Christian Gangsta Rap','Bass','Heavy Metal','Soul','Black Metal','Punk','Crossover','Space','Contemporary Christian','Meditative','Christian Rock','Instrumental Pop','Merengue','Instrumental Rock','Salsa','Ethnic','Thrash Metal','Gothic','Anime','Darkwave','JPop','Techno-Industrial','Synthpop','Electronic','Abstract','Pop-Folk','Art Rock','Eurodance','Baroque','Dream','Bhangra','Southern Rock','Big Beat','Comedy','Breakbeat','Cult','Chillout','Gangsta Rap','Downtempo','Top 40','Dub','Christian Rap','EBM','Pop / Funk','Eclectic','Jungle','Electro','Native American','Electroclash','Cabaret','Emo','New Wave','Experimental','Psychedelic','Garage','Rave','Global','Showtunes','IDM','Trailer','Illbient','Lo-Fi','Industro-Goth','Tribal','Jam Band','Acid Punk','Krautrock','Acid Jazz','Leftfield','Polka','Lounge','Retro','Math Rock','Musical','New Romantic','Rock & Roll','Nu-Breakz','Hard Rock','Post-Punk','Folk','Post-Rock','Folk-Rock','Psytrance','National Folk','Shoegaze','Swing','Space Rock','Fast Fusion','Trop Rock','Bebob','World Music','Latin','Neoclassical','Revival','Audiobook','Celtic','Audio Theatre','Bluegrass','Neue Deutsche Welle','Avantgarde','Podcast','Gothic Rock','Indie Rock','Progressive Rock','G-Funk','Psychedelic Rock','Dubstep','Symphonic Rock','Garage Rock','Slow Rock','Psybient','Psychobilly','Touhou' + ]; + + # Taken from: https://web.archive.org/web/20220322153107/https://dev.vk.com/reference/objects/audio-genres + const vkGenres = [ + "Rock" => 1, + "Pop" => 2, + "Rap" => 3, + "Hip-Hop" => 3, # VK API lists №3 as Rap & Hip-Hop, but these genres are distinct in OpenVK + "Easy Listening" => 4, + "House" => 5, + "Dance" => 5, + "Instrumental" => 6, + "Metal" => 7, + "Alternative" => 21, + "Dubstep" => 8, + "Jazz" => 1001, + "Blues" => 1001, + "Drum & Bass" => 10, + "Trance" => 11, + "Chanson" => 12, + "Ethnic" => 13, + "Acoustic" => 14, + "Vocal" => 14, + "Reggae" => 15, + "Classical" => 16, + "Indie Pop" => 17, + "Speech" => 19, + "Disco" => 22, + "Other" => 18, + ]; + + private function fileLength(string $filename): int + { + if(!Shell::commandAvailable("ffmpeg") || !Shell::commandAvailable("ffprobe")) + throw new \Exception(); + + $error = NULL; + $streams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams a", "-loglevel error")->execute($error); + if($error !== 0) + throw new \DomainException("$filename is not recognized as media container"); + else if(empty($streams) || ctype_space($streams)) + throw new \DomainException("$filename does not contain any audio streams"); + + $vstreams = Shell::ffprobe("-i", $filename, "-show_streams", "-select_streams v", "-loglevel error")->execute($error); + + # check if audio has cover (attached_pic) + preg_match("%attached_pic=([0-1])%", $vstreams, $hasCover); + if(!empty($vstreams) && !ctype_space($vstreams) && ((int)($hasCover[1]) !== 1)) + throw new \DomainException("$filename is a video"); + + $durations = []; + preg_match_all('%duration=([0-9\.]++)%', $streams, $durations); + if(sizeof($durations[1]) === 0) + throw new \DomainException("$filename does not contain any meaningful audio streams"); + + $length = 0; + foreach($durations[1] as $duration) { + $duration = floatval($duration); + if($duration < 1.0 || $duration > 65536.0) + throw new \DomainException("$filename does not contain any meaningful audio streams"); + else + $length = max($length, $duration); + } + + return (int) round($length, 0, PHP_ROUND_HALF_EVEN); + } + + /** + * @throws \Exception + */ + protected function saveFile(string $filename, string $hash): bool + { + $duration = $this->fileLength($filename); + + $kid = openssl_random_pseudo_bytes(16); + $key = openssl_random_pseudo_bytes(16); + $tok = openssl_random_pseudo_bytes(28); + $ss = ceil($duration / 15); + + $this->stateChanges("kid", $kid); + $this->stateChanges("key", $key); + $this->stateChanges("token", $tok); + $this->stateChanges("segment_size", $ss); + $this->stateChanges("length", $duration); + + try { + $args = [ + str_replace("enabled", "available", OPENVK_ROOT), + str_replace("enabled", "available", $this->getBaseDir()), + $hash, + $filename, + + bin2hex($kid), + bin2hex($key), + bin2hex($tok), + $ss, + ]; + + if(Shell::isPowershell()) { + Shell::powershell("-executionpolicy bypass", "-File", __DIR__ . "/../shell/processAudio.ps1", ...$args) + ->start(); + } else { + Shell::bash(__DIR__ . "/../shell/processAudio.sh", ...$args) // Pls workkkkk + ->start(); // idk, not tested :") + } + + # Wait until processAudio will consume the file + $start = time(); + while(file_exists($filename)) + if(time() - $start > 5) + throw new \RuntimeException("Timed out waiting FFMPEG"); + + } catch(UnknownCommandException $ucex) { + exit(OPENVK_ROOT_CONF["openvk"]["debug"] ? "bash/pwsh is not installed" : VIDEOS_FRIENDLY_ERROR); + } + + return true; + } + + function getTitle(): string + { + return $this->getRecord()->name; + } + + function getPerformer(): string + { + return $this->getRecord()->performer; + } + + function getName(): string + { + return $this->getPerformer() . " — " . $this->getTitle(); + } + + function getGenre(): ?string + { + return $this->getRecord()->genre; + } + + function getLyrics(): ?string + { + return !is_null($this->getRecord()->lyrics) ? htmlspecialchars($this->getRecord()->lyrics, ENT_DISALLOWED | ENT_XHTML) : NULL; + } + + function getLength(): int + { + return $this->getRecord()->length; + } + + function getFormattedLength(): string + { + $len = $this->getLength(); + $mins = floor($len / 60); + $secs = $len - ($mins * 60); + + return ( + str_pad((string) $mins, 2, "0", STR_PAD_LEFT) + . ":" . + str_pad((string) $secs, 2, "0", STR_PAD_LEFT) + ); + } + + function getSegmentSize(): float + { + return $this->getRecord()->segment_size; + } + + function getListens(): int + { + return $this->getRecord()->listens; + } + + function getOriginalURL(bool $force = false): string + { + $disallowed = !OPENVK_ROOT_CONF["openvk"]["preferences"]["music"]["exposeOriginalURLs"] && !$force; + if(!$this->isAvailable() || $disallowed) + return ovk_scheme(true) + . $_SERVER["HTTP_HOST"] . ":" + . $_SERVER["HTTP_PORT"] + . "/assets/packages/static/openvk/audio/nomusic.mp3"; + + $key = bin2hex($this->getRecord()->token); + + return str_replace(".mpd", "_fragments", $this->getURL()) . "/original_$key.mp3"; + } + + function getURL(?bool $force = false): string + { + if ($this->isWithdrawn()) return ""; + + return parent::getURL(); + } + + function getKeys(): array + { + $keys[bin2hex($this->getRecord()->kid)] = bin2hex($this->getRecord()->key); + + return $keys; + } + + function isAnonymous(): bool + { + return false; + } + + function isExplicit(): bool + { + return (bool) $this->getRecord()->explicit; + } + + function isWithdrawn(): bool + { + return (bool) $this->getRecord()->withdrawn; + } + + function isUnlisted(): bool + { + return (bool) $this->getRecord()->unlisted; + } + + # NOTICE may flush model to DB if it was just processed + function isAvailable(): bool + { + if($this->getRecord()->processed) + return true; + + # throttle requests to isAvailable to prevent DoS attack if filesystem is actually an S3 storage + if(time() - $this->getRecord()->checked < 5) + return false; + + try { + $fragments = str_replace(".mpd", "_fragments", $this->getFileName()); + $original = "original_" . bin2hex($this->getRecord()->token) . ".mp3"; + if(file_exists("$fragments/$original")) { + # Original gets uploaded after fragments + $this->stateChanges("processed", 0x01); + + return true; + } + } finally { + $this->stateChanges("checked", time()); + $this->save(); + } + + return false; + } + + function isInLibraryOf($entity): bool + { + return sizeof(DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])) != 0; + } + + function add($entity): bool + { + if($this->isInLibraryOf($entity)) + return false; + + $entityId = $entity->getId() * ($entity instanceof Club ? -1 : 1); + $audioRels = DatabaseConnection::i()->getContext()->table("audio_relations"); + if(sizeof($audioRels->where("entity", $entityId)) > 65536) + throw new \OverflowException("Can't have more than 65536 audios in a playlist"); + + $audioRels->insert([ + "entity" => $entityId, + "audio" => $this->getId(), + ]); + + return true; + } + + function remove($entity): bool + { + if(!$this->isInLibraryOf($entity)) + return false; + + DatabaseConnection::i()->getContext()->table("audio_relations")->where([ + "entity" => $entity->getId() * ($entity instanceof Club ? -1 : 1), + "audio" => $this->getId(), + ])->delete(); + + return true; + } + + function listen($entity, Playlist $playlist = NULL): bool + { + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + ])->order("index DESC")->fetch(); + + if(!$lastListen || (time() - $lastListen->time >= $this->getLength())) { + $listensTable->insert([ + "entity" => $entity->getRealId(), + "audio" => $this->getId(), + "time" => time(), + "playlist" => $playlist ? $playlist->getId() : NULL, + ]); + + if($entity instanceof User) { + $this->stateChanges("listens", ($this->getListens() + 1)); + $this->save(); + + if($playlist) { + $playlist->incrementListens(); + $playlist->save(); + } + } + + $entity->setLast_played_track($this->getId()); + $entity->save(); + + return true; + } + + $lastListen->update([ + "time" => time(), + ]); + + return false; + } + + /** + * Returns compatible with VK API 4.x, 5.x structure. + * + * Always sets album(_id) to NULL at this time. + * If genre is not present in VK genre list, fallbacks to "Other". + * The url and manifest properties will be set to false if the audio can't be played (processing, removed). + * + * Aside from standard VK properties, this method will also return some OVK extended props: + * 1. added - Is in the library of $user? + * 2. editable - Can be edited by $user? + * 3. withdrawn - Removed due to copyright request? + * 4. ready - Can be played at this time? + * 5. genre_str - Full name of genre, NULL if it's undefined + * 6. manifest - URL to MPEG-DASH manifest + * 7. keys - ClearKey DRM keys + * 8. explicit - Marked as NSFW? + * 9. searchable - Can be found via search? + * 10. unique_id - Unique ID of audio + * + * @notice that in case if exposeOriginalURLs is set to false in config, "url" will always contain link to nomusic.mp3, + * unless $forceURLExposure is set to true. + * + * @notice may trigger db flush if the audio is not processed yet, use with caution on unsaved models. + * + * @param ?User $user user, relative to whom "added", "editable" will be set + * @param bool $forceURLExposure force set "url" regardless of config + */ + function toVkApiStruct(?User $user = NULL, bool $forceURLExposure = false): object + { + $obj = (object) []; + $obj->unique_id = base64_encode((string) $this->getId()); + $obj->id = $obj->aid = $this->getVirtualId(); + $obj->artist = $this->getPerformer(); + $obj->title = $this->getTitle(); + $obj->duration = $this->getLength(); + $obj->album_id = $obj->album = NULL; # i forgor to implement + $obj->url = false; + $obj->manifest = false; + $obj->keys = false; + $obj->genre_id = $obj->genre = self::vkGenres[$this->getGenre() ?? ""] ?? 18; # return Other if no match + $obj->genre_str = $this->getGenre(); + $obj->owner_id = $this->getOwner()->getId(); + if($this->getOwner() instanceof Club) + $obj->owner_id *= -1; + + $obj->lyrics = NULL; + if(!is_null($this->getLyrics())) + $obj->lyrics = $this->getId(); + + $obj->added = $user && $this->isInLibraryOf($user); + $obj->editable = $user && $this->canBeModifiedBy($user); + $obj->searchable = !$this->isUnlisted(); + $obj->explicit = $this->isExplicit(); + $obj->withdrawn = $this->isWithdrawn(); + $obj->ready = $this->isAvailable() && !$obj->withdrawn; + if($obj->ready) { + $obj->url = $this->getOriginalURL($forceURLExposure); + $obj->manifest = $this->getURL(); + $obj->keys = $this->getKeys(); + } + + return $obj; + } + + function setOwner(int $oid): void + { + # WARNING: API implementation won't be able to handle groups like that, don't remove + if($oid <= 0) + throw new \OutOfRangeException("Only users can be owners of audio!"); + + $this->stateChanges("owner", $oid); + } + + function setGenre(string $genre): void + { + if(!in_array($genre, Audio::genres)) { + $this->stateChanges("genre", NULL); + return; + } + + $this->stateChanges("genre", $genre); + } + + function setCopyrightStatus(bool $withdrawn = true): void { + $this->stateChanges("withdrawn", $withdrawn); + } + + function setSearchability(bool $searchable = true): void { + $this->stateChanges("unlisted", !$searchable); + } + + function setToken(string $tok): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setKid(string $kid): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setKey(string $key): void { + throw new \LogicException("Changing keys is not supported."); + } + + function setLength(int $len): void { + throw new \LogicException("Changing length is not supported."); + } + + function setSegment_Size(int $len): void { + throw new \LogicException("Changing length is not supported."); + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("audio_relations")->where("audio", $this->getId()) + ->delete(); + $ctx->table("audio_listens")->where("audio", $this->getId()) + ->delete(); + $ctx->table("playlist_relations")->where("media", $this->getId()) + ->delete(); + + parent::delete($softly); + } +} diff --git a/Web/Models/Entities/Club.php b/Web/Models/Entities/Club.php index fbdc503b..b8b8838a 100644 --- a/Web/Models/Entities/Club.php +++ b/Web/Models/Entities/Club.php @@ -371,36 +371,60 @@ class Club extends RowModel { return $this->getRecord()->alert; } + + function getRealId(): int + { + return $this->getId() * -1; + } + + function isEveryoneCanUploadAudios(): bool + { + return (bool) $this->getRecord()->everyone_can_upload_audios; + } + + function canUploadAudio(?User $user): bool + { + if(!$user) + return NULL; + + return $this->isEveryoneCanUploadAudios() || $this->canBeModifiedBy($user); + } + + function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios)->getClubCollectionSize($this); + } function toVkApiStruct(?User $user = NULL): object { - $res = []; + $res = (object)[]; $res->id = $this->getId(); $res->name = $this->getName(); $res->screen_name = $this->getShortCode(); $res->is_closed = 0; $res->deactivated = NULL; - $res->is_admin = $this->canBeModifiedBy($user); + $res->is_admin = $user && $this->canBeModifiedBy($user); - if($this->canBeModifiedBy($user)) { + if($user && $this->canBeModifiedBy($user)) { $res->admin_level = 3; } - $res->is_member = $this->getSubscriptionStatus($user) ? 1 : 0; + $res->is_member = $user && $this->getSubscriptionStatus($user) ? 1 : 0; $res->type = "group"; $res->photo_50 = $this->getAvatarUrl("miniscule"); $res->photo_100 = $this->getAvatarUrl("tiny"); $res->photo_200 = $this->getAvatarUrl("normal"); - $res->can_create_topic = $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); + $res->can_create_topic = $user && $this->canBeModifiedBy($user) ? 1 : ($this->isEveryoneCanCreateTopics() ? 1 : 0); - $res->can_post = $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); + $res->can_post = $user && $this->canBeModifiedBy($user) ? 1 : ($this->canPost() ? 1 : 0); - return (object) $res; + return $res; } use Traits\TBackDrops; use Traits\TSubscribable; + use Traits\TAudioStatuses; } diff --git a/Web/Models/Entities/MediaCollection.php b/Web/Models/Entities/MediaCollection.php index 05f3835c..1f061988 100644 --- a/Web/Models/Entities/MediaCollection.php +++ b/Web/Models/Entities/MediaCollection.php @@ -17,7 +17,17 @@ abstract class MediaCollection extends RowModel protected $specialNames = []; - private $relations; + protected $relations; + + /** + * Maximum amount of items Collection can have + */ + const MAX_ITEMS = INF; + + /** + * Maximum amount of Collections with same "owner" allowed + */ + const MAX_COUNT = INF; function __construct(?ActiveRow $ar = NULL) { @@ -70,18 +80,29 @@ abstract class MediaCollection extends RowModel } abstract function getCoverURL(): ?string; - - function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + + function fetchClassic(int $offset = 0, ?int $limit = NULL): \Traversable { - $related = $this->getRecord()->related("$this->relTableName.collection")->page($page, $perPage ?? OPENVK_DEFAULT_PER_PAGE)->order("media ASC"); + $related = $this->getRecord()->related("$this->relTableName.collection") + ->limit($limit ?? OPENVK_DEFAULT_PER_PAGE, $offset) + ->order("media ASC"); + foreach($related as $rel) { $media = $rel->ref($this->entityTableName, "media"); if(!$media) continue; - + yield new $this->entityClassName($media); } } + + function fetch(int $page = 1, ?int $perPage = NULL): \Traversable + { + $page = max(1, $page); + $perPage ??= OPENVK_DEFAULT_PER_PAGE; + + return $this->fetchClassic($perPage * ($page - 1), $perPage); + } function size(): int { @@ -110,7 +131,7 @@ abstract class MediaCollection extends RowModel { return $this->getRecord()->special_type !== 0; } - + function add(RowModel $entity): bool { $this->entitySuitable($entity); @@ -118,6 +139,10 @@ abstract class MediaCollection extends RowModel if(!$this->allowDuplicates) if($this->has($entity)) return false; + + if(self::MAX_ITEMS != INF) + if(sizeof($this->relations->where("collection", $this->getId())) > self::MAX_ITEMS) + throw new \OutOfBoundsException("Collection is full"); $this->relations->insert([ "collection" => $this->getId(), @@ -127,14 +152,14 @@ abstract class MediaCollection extends RowModel return true; } - function remove(RowModel $entity): void + function remove(RowModel $entity): bool { $this->entitySuitable($entity); - $this->relations->where([ + return $this->relations->where([ "collection" => $this->getId(), "media" => $entity->getId(), - ])->delete(); + ])->delete() > 0; } function has(RowModel $entity): bool @@ -148,6 +173,33 @@ abstract class MediaCollection extends RowModel return !is_null($rel); } - + + function save(?bool $log = false): void + { + $thisTable = DatabaseConnection::i()->getContext()->table($this->tableName); + if(self::MAX_COUNT != INF) + if(isset($this->changes["owner"])) + if(sizeof($thisTable->where("owner", $this->changes["owner"])) > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of collections"); + + if(is_null($this->getRecord())) + if(!isset($this->changes["created"])) + $this->stateChanges("created", time()); + else + $this->stateChanges("edited", time()); + + parent::save($log); + } + + function delete(bool $softly = true): void + { + if(!$softly) { + $this->relations->where("collection", $this->getId()) + ->delete(); + } + + parent::delete($softly); + } + use Traits\TOwnable; } diff --git a/Web/Models/Entities/Playlist.php b/Web/Models/Entities/Playlist.php new file mode 100644 index 00000000..c027a038 --- /dev/null +++ b/Web/Models/Entities/Playlist.php @@ -0,0 +1,256 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Models\Entities; +use Chandler\Database\DatabaseConnection; +use Nette\Database\Table\ActiveRow; +use openvk\Web\Models\Repositories\Audios; +use openvk\Web\Models\Repositories\Photos; +use openvk\Web\Models\RowModel; +use openvk\Web\Models\Entities\Photo; + +/** + * @method setName(string $name) + * @method setDescription(?string $desc) + */ +class Playlist extends MediaCollection +{ + protected $tableName = "playlists"; + protected $relTableName = "playlist_relations"; + protected $entityTableName = "audios"; + protected $entityClassName = 'openvk\Web\Models\Entities\Audio'; + protected $allowDuplicates = false; + + private $importTable; + + const MAX_COUNT = 1000; + const MAX_ITEMS = 10000; + + function __construct(?ActiveRow $ar = NULL) + { + parent::__construct($ar); + + $this->importTable = DatabaseConnection::i()->getContext()->table("playlist_imports"); + } + + function getCoverURL(string $size = "normal"): ?string + { + $photo = (new Photos)->get((int) $this->getRecord()->cover_photo_id); + return is_null($photo) ? "/assets/packages/static/openvk/img/song.jpg" : $photo->getURLBySizeId($size); + } + + function getLength(): int + { + return $this->getRecord()->length; + } + + function getAudios(int $offset = 0, ?int $limit = NULL, ?int $shuffleSeed = NULL): \Traversable + { + if(!$shuffleSeed) { + foreach ($this->fetchClassic($offset, $limit) as $e) + yield $e; # No, I can't return, it will break with [] + + return; + } + + $ids = []; + foreach($this->relations->select("media AS i")->where("collection", $this->getId()) as $rel) + $ids[] = $rel->i; + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, $offset, $limit ?? OPENVK_DEFAULT_PER_PAGE); + foreach($ids as $id) + yield (new Audios)->get($id); + } + + function add(RowModel $audio): bool + { + if($res = parent::add($audio)) { + $this->stateChanges("length", $this->getRecord()->length + $audio->getLength()); + $this->save(); + } + + return $res; + } + + function remove(RowModel $audio): bool + { + if($res = parent::remove($audio)) { + $this->stateChanges("length", $this->getRecord()->length - $audio->getLength()); + $this->save(); + } + + return $res; + } + + function isBookmarkedBy(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + return !is_null($this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->fetch()); + } + + function bookmark(RowModel $entity): bool + { + if($this->isBookmarkedBy($entity)) + return false; + + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + if($this->importTable->where("entity", $id)->count() > self::MAX_COUNT) + throw new \OutOfBoundsException("Maximum amount of playlists"); + + $this->importTable->insert([ + "entity" => $id, + "playlist" => $this->getId(), + ]); + + return true; + } + + function unbookmark(RowModel $entity): bool + { + $id = $entity->getId(); + if($entity instanceof Club) + $id *= -1; + + $count = $this->importTable->where([ + "entity" => $id, + "playlist" => $this->getId(), + ])->delete(); + + return $count > 0; + } + + function getDescription(): ?string + { + return $this->getRecord()->description; + } + + function getDescriptionHTML(): ?string + { + return htmlspecialchars($this->getRecord()->description, ENT_DISALLOWED | ENT_XHTML); + } + + function getListens() + { + return $this->getRecord()->listens; + } + + function toVkApiStruct(?User $user = NULL): object + { + $oid = $this->getOwner()->getId(); + if($this->getOwner() instanceof Club) + $oid *= -1; + + return (object) [ + "id" => $this->getId(), + "owner_id" => $oid, + "title" => $this->getName(), + "description" => $this->getDescription(), + "size" => $this->size(), + "length" => $this->getLength(), + "created" => $this->getCreationTime()->timestamp(), + "modified" => $this->getEditTime() ? $this->getEditTime()->timestamp() : NULL, + "accessible" => $this->canBeViewedBy($user), + "editable" => $this->canBeModifiedBy($user), + "bookmarked" => $this->isBookmarkedBy($user), + "listens" => $this->getListens(), + "cover_url" => $this->getCoverURL(), + ]; + } + + function setLength(): void + { + throw new \LogicException("Can't set length of playlist manually"); + } + + function resetLength(): bool + { + $this->stateChanges("length", 0); + + return true; + } + + function delete(bool $softly = true): void + { + $ctx = DatabaseConnection::i()->getContext(); + $ctx->table("playlist_imports")->where("playlist", $this->getId()) + ->delete(); + + parent::delete($softly); + } + + function hasAudio(Audio $audio): bool + { + $ctx = DatabaseConnection::i()->getContext(); + return !is_null($ctx->table("playlist_relations")->where([ + "collection" => $this->getId(), + "media" => $audio->getId() + ])->fetch()); + } + + function getCoverPhotoId(): ?int + { + return $this->getRecord()->cover_photo_id; + } + + function canBeModifiedBy(User $user): bool + { + if(!$user) + return false; + + if($this->getOwner() instanceof User) + return $user->getId() == $this->getOwner()->getId(); + else + return $this->getOwner()->canBeModifiedBy($user); + } + + function getLengthInMinutes(): int + { + return (int)round($this->getLength() / 60, PHP_ROUND_HALF_DOWN); + } + + function fastMakeCover(int $owner, array $file) + { + $cover = new Photo; + $cover->setOwner($owner); + $cover->setDescription("Playlist cover image"); + $cover->setFile($file); + $cover->setCreated(time()); + $cover->save(); + + $this->setCover_photo_id($cover->getId()); + + return $cover; + } + + function getURL(): string + { + return "/playlist" . $this->getOwner()->getRealId() . "_" . $this->getId(); + } + + function incrementListens() + { + $this->stateChanges("listens", ($this->getListens() + 1)); + } + + function getMetaDescription(): string + { + $length = $this->getLengthInMinutes(); + + $props = []; + $props[] = tr("audios_count", $this->size()); + $props[] = "<span id='listensCount'>" . tr("listens_count", $this->getListens()) . "</span>"; + if($length > 0) $props[] = tr("minutes_count", $length); + $props[] = tr("created_playlist") . " " . $this->getPublicationTime(); + # if($this->getEditTime()) $props[] = tr("updated_playlist") . " " . $this->getEditTime(); + + return implode(" • ", $props); + } +} diff --git a/Web/Models/Entities/Report.php b/Web/Models/Entities/Report.php index d449a2e8..5487056f 100644 --- a/Web/Models/Entities/Report.php +++ b/Web/Models/Entities/Report.php @@ -5,7 +5,7 @@ use Nette\Database\Table\ActiveRow; use openvk\Web\Models\RowModel; use openvk\Web\Models\Entities\Club; use Chandler\Database\DatabaseConnection; -use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Users, Posts, Photos, Videos, Clubs}; +use openvk\Web\Models\Repositories\{Applications, Comments, Notes, Reports, Audios, Users, Posts, Photos, Videos, Clubs}; use Chandler\Database\DatabaseConnection as DB; use Nette\InvalidStateException as ISE; use Nette\Database\Table\Selection; @@ -74,6 +74,7 @@ class Report extends RowModel else if ($this->getContentType() == "note") return (new Notes)->get($this->getContentId()); else if ($this->getContentType() == "app") return (new Applications)->get($this->getContentId()); else if ($this->getContentType() == "user") return (new Users)->get($this->getContentId()); + else if ($this->getContentType() == "audio") return (new Audios)->get($this->getContentId()); else return null; } diff --git a/Web/Models/Entities/Traits/TAudioStatuses.php b/Web/Models/Entities/Traits/TAudioStatuses.php new file mode 100644 index 00000000..f957a104 --- /dev/null +++ b/Web/Models/Entities/Traits/TAudioStatuses.php @@ -0,0 +1,38 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Models\Entities\Traits; +use openvk\Web\Models\Repositories\Audios; +use Chandler\Database\DatabaseConnection; + +trait TAudioStatuses +{ + function isBroadcastEnabled(): bool + { + if($this->getRealId() < 0) return true; + return (bool) $this->getRecord()->audio_broadcast_enabled; + } + + function getCurrentAudioStatus() + { + if(!$this->isBroadcastEnabled()) return NULL; + + $audioId = $this->getRecord()->last_played_track; + + if(!$audioId) return NULL; + $audio = (new Audios)->get($audioId); + + if(!$audio || $audio->isDeleted()) + return NULL; + + $listensTable = DatabaseConnection::i()->getContext()->table("audio_listens"); + $lastListen = $listensTable->where([ + "entity" => $this->getRealId(), + "audio" => $audio->getId(), + "time >" => (time() - $audio->getLength()) - 10, + ])->fetch(); + + if($lastListen) + return $audio; + + return NULL; + } +} diff --git a/Web/Models/Entities/Traits/TOwnable.php b/Web/Models/Entities/Traits/TOwnable.php index 9dc9ce2a..08e5fde3 100644 --- a/Web/Models/Entities/Traits/TOwnable.php +++ b/Web/Models/Entities/Traits/TOwnable.php @@ -4,6 +4,12 @@ use openvk\Web\Models\Entities\User; trait TOwnable { + function canBeViewedBy(?User $user): bool + { + // TODO implement normal check in master + return true; + } + function canBeModifiedBy(User $user): bool { if(method_exists($this, "isCreatedBySystem")) diff --git a/Web/Models/Entities/User.php b/Web/Models/Entities/User.php index dc227039..291dfb6a 100644 --- a/Web/Models/Entities/User.php +++ b/Web/Models/Entities/User.php @@ -4,7 +4,7 @@ use morphos\Gender; use openvk\Web\Themes\{Themepack, Themepacks}; use openvk\Web\Util\DateTime; use openvk\Web\Models\RowModel; -use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift}; +use openvk\Web\Models\Entities\{Photo, Message, Correspondence, Gift, Audio}; use openvk\Web\Models\Repositories\{Applications, Bans, Comments, Notes, Posts, Users, Clubs, Albums, Gifts, Notifications, Videos, Photos}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use Nette\Database\Table\ActiveRow; @@ -190,7 +190,7 @@ class User extends RowModel function getMorphedName(string $case = "genitive", bool $fullName = true): string { $name = $fullName ? ($this->getLastName() . " " . $this->getFirstName()) : $this->getFirstName(); - if(!preg_match("%^[А-яё\-]+$%", $name)) + if(!preg_match("%[А-яё\-]+$%", $name)) return $name; # name is probably not russian $inflected = inflectName($name, $case, $this->isFemale() ? Gender::FEMALE : Gender::MALE); @@ -455,6 +455,7 @@ class User extends RowModel "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -462,7 +463,7 @@ class User extends RowModel "news", "links", "poster", - "apps" + "apps", ], ])->get($id); } @@ -482,6 +483,7 @@ class User extends RowModel "friends.add", "wall.write", "messages.write", + "audios.read", ], ])->get($id); } @@ -1010,6 +1012,7 @@ class User extends RowModel "friends.add", "wall.write", "messages.write", + "audios.read", ], ])->set($id, $status)->toInteger()); } @@ -1020,6 +1023,7 @@ class User extends RowModel "length" => 1, "mappings" => [ "photos", + "audios", "videos", "messages", "notes", @@ -1027,7 +1031,7 @@ class User extends RowModel "news", "links", "poster", - "apps" + "apps", ], ])->set($id, (int) $status)->toInteger(); @@ -1223,6 +1227,11 @@ class User extends RowModel return $response; } + function getRealId() + { + return $this->getId(); + } + function toVkApiStruct(): object { $res = (object) []; @@ -1239,7 +1248,45 @@ class User extends RowModel return $res; } + + function getAudiosCollectionSize() + { + return (new \openvk\Web\Models\Repositories\Audios)->getUserCollectionSize($this); + } + + function getBroadcastList(string $filter = "friends", bool $shuffle = false) + { + $dbContext = DatabaseConnection::i()->getContext(); + $entityIds = []; + $query = $dbContext->table("subscriptions")->where("follower", $this->getRealId()); + + if($filter != "all") + $query = $query->where("model = ?", "openvk\\Web\\Models\\Entities\\" . ($filter == "groups" ? "Club" : "User")); + + foreach($query as $_rel) { + $entityIds[] = $_rel->model == "openvk\\Web\\Models\\Entities\\Club" ? $_rel->target * -1 : $_rel->target; + } + + if($shuffle) { + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + $entityIds = knuth_shuffle($entityIds, $shuffleSeed); + } + + $returnArr = []; + + foreach($entityIds as $id) { + $entit = $id > 0 ? (new Users)->get($id) : (new Clubs)->get(abs($id)); + + if($id > 0 && $entit->isDeleted()) return; + $returnArr[] = $entit; + } + + return $returnArr; + } + use Traits\TBackDrops; use Traits\TSubscribable; + use Traits\TAudioStatuses; } diff --git a/Web/Models/Entities/Video.php b/Web/Models/Entities/Video.php index cef48e27..db520ee1 100644 --- a/Web/Models/Entities/Video.php +++ b/Web/Models/Entities/Video.php @@ -1,7 +1,7 @@ <?php declare(strict_types=1); namespace openvk\Web\Models\Entities; use openvk\Web\Util\Shell\Shell; -use openvk\Web\Util\Shell\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException}; +use openvk\Web\Util\Shell\Exceptions\{ShellUnavailableException, UnknownCommandException}; use openvk\Web\Models\VideoDrivers\VideoDriver; use Nette\InvalidStateException as ISE; diff --git a/Web/Models/Repositories/Audios.php b/Web/Models/Repositories/Audios.php new file mode 100644 index 00000000..ad5e822e --- /dev/null +++ b/Web/Models/Repositories/Audios.php @@ -0,0 +1,296 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Models\Repositories; +use Chandler\Database\DatabaseConnection; +use openvk\Web\Models\Entities\Audio; +use openvk\Web\Models\Entities\Club; +use openvk\Web\Models\Entities\Playlist; +use openvk\Web\Models\Entities\User; +use openvk\Web\Models\Repositories\Util\EntityStream; + +class Audios +{ + private $context; + private $audios; + private $rels; + private $playlists; + private $playlistImports; + private $playlistRels; + + const ORDER_NEW = 0; + const ORDER_POPULAR = 1; + + const VK_ORDER_NEW = 0; + const VK_ORDER_LENGTH = 1; + const VK_ORDER_POPULAR = 2; + + function __construct() + { + $this->context = DatabaseConnection::i()->getContext(); + $this->audios = $this->context->table("audios"); + $this->rels = $this->context->table("audio_relations"); + + $this->playlists = $this->context->table("playlists"); + $this->playlistImports = $this->context->table("playlist_imports"); + $this->playlistRels = $this->context->table("playlist_relations"); + } + + function get(int $id): ?Audio + { + $audio = $this->audios->get($id); + if(!$audio) + return NULL; + + return new Audio($audio); + } + + function getPlaylist(int $id): ?Playlist + { + $playlist = $this->playlists->get($id); + if(!$playlist) + return NULL; + + return new Playlist($playlist); + } + + function getByOwnerAndVID(int $owner, int $vId): ?Audio + { + $audio = $this->audios->where([ + "owner" => $owner, + "virtual_id" => $vId, + ])->fetch(); + if(!$audio) return NULL; + + return new Audio($audio); + } + + function getPlaylistByOwnerAndVID(int $owner, int $vId): ?Playlist + { + $playlist = $this->playlists->where([ + "owner" => $owner, + "id" => $vId, + ])->fetch(); + if(!$playlist) return NULL; + + return new Playlist($playlist); + } + + function getByEntityID(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->rels->where("entity", $entity)->limit($limit, $offset); + foreach($iter as $rel) { + $audio = $this->get($rel->audio); + if(!$audio || $audio->isDeleted()) { + $deleted++; + continue; + } + + yield $audio; + } + } + + function getPlaylistsByEntityId(int $entity, int $offset = 0, ?int $limit = NULL, ?int& $deleted = nullptr): \Traversable + { + $limit ??= OPENVK_DEFAULT_PER_PAGE; + $iter = $this->playlistImports->where("entity", $entity)->limit($limit, $offset); + foreach($iter as $rel) { + $playlist = $this->getPlaylist($rel->playlist); + if(!$playlist || $playlist->isDeleted()) { + $deleted++; + continue; + } + + yield $playlist; + } + } + + function getByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getByEntityID($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getRandomThreeAudiosByEntityId(int $id): Array + { + $iter = $this->rels->where("entity", $id); + $ids = []; + + foreach($iter as $it) + $ids[] = $it->audio; + + $shuffleSeed = openssl_random_pseudo_bytes(6); + $shuffleSeed = hexdec(bin2hex($shuffleSeed)); + + $ids = knuth_shuffle($ids, $shuffleSeed); + $ids = array_slice($ids, 0, 3); + $audios = []; + + foreach($ids as $id) { + $audio = $this->get((int)$id); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + + return $audios; + } + + function getByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getByEntityID($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getPlaylistsByUser(User $user, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($user->getId(), ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getPlaylistsByClub(Club $club, int $page = 1, ?int $perPage = NULL, ?int& $deleted = nullptr): \Traversable + { + return $this->getPlaylistsByEntityId($club->getId() * -1, ($perPage * ($page - 1)), $perPage, $deleted); + } + + function getCollectionSizeByEntityId(int $id): int + { + return sizeof($this->rels->where("entity", $id)); + } + + function getUserCollectionSize(User $user): int + { + return sizeof($this->rels->where("entity", $user->getId())); + } + + function getClubCollectionSize(Club $club): int + { + return sizeof($this->rels->where("entity", $club->getId() * -1)); + } + + function getUserPlaylistsCount(User $user): int + { + return sizeof($this->playlistImports->where("entity", $user->getId())); + } + + function getClubPlaylistsCount(Club $club): int + { + return sizeof($this->playlistImports->where("entity", $club->getId() * -1)); + } + + function getByUploader(User $user): EntityStream + { + $search = $this->audios->where([ + "owner" => $user->getId(), + "deleted" => 0, + ]); + + return new EntityStream("Audio", $search); + } + + function getGlobal(int $order, ?string $genreId = NULL): EntityStream + { + $search = $this->audios->where([ + "deleted" => 0, + "unlisted" => 0, + "withdrawn" => 0, + ])->order($order == Audios::ORDER_NEW ? "created DESC" : "listens DESC"); + + if(!is_null($genreId)) + $search = $search->where("genre", $genreId); + + return new EntityStream("Audio", $search); + } + + function search(string $query, int $sortMode = 0, bool $performerOnly = false, bool $withLyrics = false): EntityStream + { + $columns = $performerOnly ? "performer" : "performer, name"; + $order = (["created", "length", "listens"][$sortMode] ?? "") . " DESC"; + + $search = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + ])->where("MATCH ($columns) AGAINST (? WITH QUERY EXPANSION)", $query)->order($order); + + if($withLyrics) + $search = $search->where("lyrics IS NOT NULL"); + + return new EntityStream("Audio", $search); + } + + function searchPlaylists(string $query): EntityStream + { + $search = $this->playlists->where([ + "deleted" => 0, + ])->where("MATCH (`name`, `description`) AGAINST (? IN BOOLEAN MODE)", $query); + + return new EntityStream("Playlist", $search); + } + + function getNew(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("created >= " . (time() - 259200))->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("created DESC")->limit(25)); + } + + function getPopular(): EntityStream + { + return new EntityStream("Audio", $this->audios->where("listens > 0")->where(["withdrawn" => 0, "deleted" => 0, "unlisted" => 0])->order("listens DESC")->limit(25)); + } + + function isAdded(int $user_id, int $audio_id): bool + { + return !is_null($this->rels->where([ + "entity" => $user_id, + "audio" => $audio_id + ])->fetch()); + } + + function find(string $query, array $pars = [], string $sort = "id DESC", int $page = 1, ?int $perPage = NULL): \Traversable + { + $query = "%$query%"; + $result = $this->audios->where([ + "unlisted" => 0, + "deleted" => 0, + ]); + + $notNullParams = []; + + foreach($pars as $paramName => $paramValue) + if($paramName != "before" && $paramName != "after" && $paramName != "only_performers") + $paramValue != NULL ? $notNullParams+=["$paramName" => "%$paramValue%"] : NULL; + else + $paramValue != NULL ? $notNullParams+=["$paramName" => "$paramValue"] : NULL; + + $nnparamsCount = sizeof($notNullParams); + + if($notNullParams["only_performers"] == "1") { + $result->where("performer LIKE ?", $query); + } else { + $result->where("name LIKE ? OR performer LIKE ?", $query, $query); + } + + if($nnparamsCount > 0) { + foreach($notNullParams as $paramName => $paramValue) { + switch($paramName) { + case "before": + $result->where("created < ?", $paramValue); + break; + case "after": + $result->where("created > ?", $paramValue); + break; + case "with_lyrics": + $result->where("lyrics IS NOT NULL"); + break; + } + } + } + + return new Util\EntityStream("Audio", $result->order($sort)); + } + + function findPlaylists(string $query, int $page = 1, ?int $perPage = NULL): \Traversable + { + $query = "%$query%"; + $result = $this->playlists->where("name LIKE ?", $query); + + return new Util\EntityStream("Playlist", $result); + } +} diff --git a/Web/Models/shell/processAudio.ps1 b/Web/Models/shell/processAudio.ps1 new file mode 100644 index 00000000..206e11e1 --- /dev/null +++ b/Web/Models/shell/processAudio.ps1 @@ -0,0 +1,39 @@ +$ovkRoot = $args[0] +$storageDir = $args[1] +$fileHash = $args[2] +$hashPart = $fileHash.substring(0, 2) +$filename = $args[3] +$audioFile = [System.IO.Path]::GetTempFileName() +$temp = [System.IO.Path]::GetTempFileName() + +$keyID = $args[4] +$key = $args[5] +$token = $args[6] +$seg = $args[7] + +$shell = Get-WmiObject Win32_process -filter "ProcessId = $PID" +$shell.SetPriority(16384) # because there's no "nice" program in Windows we just set a lower priority for entire tree + +Remove-Item $temp +Remove-Item $audioFile +New-Item -ItemType "directory" $temp +New-Item -ItemType "directory" ("$temp/$fileHash" + '_fragments') +New-Item -ItemType "directory" ("$storageDir/$hashPart/$fileHash" + '_fragments') +Set-Location -Path $temp + +Move-Item $filename $audioFile +ffmpeg -i $audioFile -f dash -encryption_scheme cenc-aes-ctr -encryption_key $key ` + -encryption_kid $keyID -map 0:a -c:a aac -ar 44100 -seg_duration $seg ` + -use_timeline 1 -use_template 1 -init_seg_name ($fileHash + '_fragments/0_0.$ext$') ` + -media_seg_name ($fileHash + '_fragments/chunk$Number%06d$_$RepresentationID$.$ext$') -adaptation_sets 'id=0,streams=a' ` + "$fileHash.mpd" + +ffmpeg -i $audioFile -vn -ar 44100 "original_$token.mp3" +Move-Item "original_$token.mp3" ($fileHash + '_fragments') + +Get-ChildItem -Path ($fileHash + '_fragments/*') | Move-Item -Destination ("$storageDir/$hashPart/$fileHash" + '_fragments') +Move-Item -Path ("$fileHash.mpd") -Destination "$storageDir/$hashPart" + +cd .. +Remove-Item -Recurse $temp +Remove-Item $audioFile diff --git a/Web/Models/shell/processAudio.sh b/Web/Models/shell/processAudio.sh new file mode 100644 index 00000000..ab5a5c55 --- /dev/null +++ b/Web/Models/shell/processAudio.sh @@ -0,0 +1,35 @@ +ovkRoot=$1 +storageDir=$2 +fileHash=$3 +hashPart=$(echo $fileHash | cut -c1-2) +filename=$4 +audioFile=$(mktemp) +temp=$(mktemp -d) + +keyID=$5 +key=$6 +token=$7 +seg=$8 + +trap 'rm -f "$temp" "$audioFile"' EXIT + +mkdir -p "$temp/$fileHash"_fragments +mkdir -p "$storageDir/$hashPart/$fileHash"_fragments +cd "$temp" + +mv "$filename" "$audioFile" +ffmpeg -i "$audioFile" -f dash -encryption_scheme cenc-aes-ctr -encryption_key "$key" \ + -encryption_kid "$keyID" -map 0 -c:a aac -ar 44100 -seg_duration "$seg" \ + -use_timeline 1 -use_template 1 -init_seg_name "$fileHash"_fragments/0_0."\$ext\$" \ + -media_seg_name "$fileHash"_fragments/chunk"\$Number"%06d\$_"\$RepresentationID\$"."\$ext\$" -adaptation_sets 'id=0,streams=a' \ + "$fileHash.mpd" + +ffmpeg -i "$audioFile" -vn -ar 44100 "original_$token.mp3" +mv "original_$token.mp3" "$fileHash"_fragments + +mv "$fileHash"_fragments "$storageDir/$hashPart" +mv "$fileHash.mpd" "$storageDir/$hashPart" + +cd .. +rm -rf "$temp" +rm -f "$audioFile" diff --git a/Web/Presenters/AdminPresenter.php b/Web/Presenters/AdminPresenter.php index 14fbbc74..658d2f4b 100644 --- a/Web/Presenters/AdminPresenter.php +++ b/Web/Presenters/AdminPresenter.php @@ -3,7 +3,19 @@ namespace openvk\Web\Presenters; use Chandler\Database\Log; use Chandler\Database\Logs; use openvk\Web\Models\Entities\{Voucher, Gift, GiftCategory, User, BannedLink}; -use openvk\Web\Models\Repositories\{Bans, ChandlerGroups, ChandlerUsers, Photos, Posts, Users, Clubs, Videos, Vouchers, Gifts, BannedLinks}; +use openvk\Web\Models\Repositories\{Audios, + ChandlerGroups, + ChandlerUsers, + Users, + Clubs, + Util\EntityStream, + Vouchers, + Gifts, + BannedLinks, + Bans, + Photos, + Posts, + Videos}; use Chandler\Database\DatabaseConnection; final class AdminPresenter extends OpenVKPresenter @@ -14,9 +26,10 @@ final class AdminPresenter extends OpenVKPresenter private $gifts; private $bannedLinks; private $chandlerGroups; + private $audios; private $logs; - function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups) + function __construct(Users $users, Clubs $clubs, Vouchers $vouchers, Gifts $gifts, BannedLinks $bannedLinks, ChandlerGroups $chandlerGroups, Audios $audios) { $this->users = $users; $this->clubs = $clubs; @@ -24,8 +37,9 @@ final class AdminPresenter extends OpenVKPresenter $this->gifts = $gifts; $this->bannedLinks = $bannedLinks; $this->chandlerGroups = $chandlerGroups; + $this->audios = $audios; $this->logs = DatabaseConnection::i()->getContext()->table("ChandlerLogs"); - + parent::__construct(); } @@ -43,6 +57,15 @@ final class AdminPresenter extends OpenVKPresenter $count = $repo->find($query)->size(); return $repo->find($query)->page($page, 20); } + + private function searchPlaylists(&$count) + { + $query = $this->queryParam("q") ?? ""; + $page = (int) ($this->queryParam("p") ?? 1); + + $count = $this->audios->findPlaylists($query)->size(); + return $this->audios->findPlaylists($query)->page($page, 20); + } function onStartup(): void { @@ -578,6 +601,54 @@ final class AdminPresenter extends OpenVKPresenter $this->redirect("/admin/users/id" . $user->getId()); } + function renderMusic(): void + { + $this->template->mode = in_array($this->queryParam("act"), ["audios", "playlists"]) ? $this->queryParam("act") : "audios"; + if ($this->template->mode === "audios") + $this->template->audios = $this->searchResults($this->audios, $this->template->count); + else + $this->template->playlists = $this->searchPlaylists($this->template->count); + } + + function renderEditMusic(int $audio_id): void + { + $audio = $this->audios->get($audio_id); + $this->template->audio = $audio; + + try { + $this->template->owner = $audio->getOwner()->getId(); + } catch(\Throwable $e) { + $this->template->owner = 1; + } + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $audio->setName($this->postParam("name")); + $audio->setPerformer($this->postParam("performer")); + $audio->setLyrics($this->postParam("text")); + $audio->setGenre($this->postParam("genre")); + $audio->setOwner((int) $this->postParam("owner")); + $audio->setExplicit(!empty($this->postParam("explicit"))); + $audio->setDeleted(!empty($this->postParam("deleted"))); + $audio->setWithdrawn(!empty($this->postParam("withdrawn"))); + $audio->save(); + } + } + + function renderEditPlaylist(int $playlist_id): void + { + $playlist = $this->audios->getPlaylist($playlist_id); + $this->template->playlist = $playlist; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $playlist->setName($this->postParam("name")); + $playlist->setDescription($this->postParam("description")); + $playlist->setCover_Photo_Id((int) $this->postParam("photo")); + $playlist->setOwner((int) $this->postParam("owner")); + $playlist->setDeleted(!empty($this->postParam("deleted"))); + $playlist->save(); + } + } + function renderLogs(): void { $filter = []; diff --git a/Web/Presenters/AudioPresenter.php b/Web/Presenters/AudioPresenter.php new file mode 100644 index 00000000..3bae710c --- /dev/null +++ b/Web/Presenters/AudioPresenter.php @@ -0,0 +1,696 @@ +<?php declare(strict_types=1); +namespace openvk\Web\Presenters; +use Chandler\Database\DatabaseConnection; +use openvk\Web\Models\Entities\Audio; +use openvk\Web\Models\Entities\Club; +use openvk\Web\Models\Entities\Photo; +use openvk\Web\Models\Entities\Playlist; +use openvk\Web\Models\Repositories\Audios; +use openvk\Web\Models\Repositories\Clubs; +use openvk\Web\Models\Repositories\Users; + +final class AudioPresenter extends OpenVKPresenter +{ + private $audios; + protected $presenterName = "audios"; + + const MAX_AUDIO_SIZE = 25000000; + + function __construct(Audios $audios) + { + $this->audios = $audios; + } + + function renderPopular(): void + { + $this->renderList(NULL, "popular"); + } + + function renderNew(): void + { + $this->renderList(NULL, "new"); + } + + function renderList(?int $owner = NULL, ?string $mode = "list"): void + { + $this->template->_template = "Audio/List.xml"; + $page = (int)($this->queryParam("p") ?? 1); + $audios = []; + + if ($mode === "list") { + $entity = NULL; + if ($owner < 0) { + $entity = (new Clubs)->get($owner * -1); + if (!$entity || $entity->isBanned()) + $this->redirect("/audios" . $this->user->id); + + $audios = $this->audios->getByClub($entity, $page, 10); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } else { + $entity = (new Users)->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) + $this->redirect("/audios" . $this->user->id); + + if(!$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + + $audios = $this->audios->getByUser($entity, $page, 10); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } + + if (!$entity) + $this->notFound(); + + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : NULL; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } else if ($mode === "new") { + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + } else if ($mode === "playlists") { + if($owner < 0) { + $entity = (new Clubs)->get(abs($owner)); + if (!$entity || $entity->isBanned()) + $this->redirect("/playlists" . $this->user->id); + + $playlists = $this->audios->getPlaylistsByClub($entity, $page, 10); + $playlistsCount = $this->audios->getClubPlaylistsCount($entity); + } else { + $entity = (new Users)->get($owner); + if (!$entity || $entity->isDeleted() || $entity->isBanned()) + $this->redirect("/playlists" . $this->user->id); + + if(!$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("forbidden_comment")); + + $playlists = $this->audios->getPlaylistsByUser($entity, $page, 9); + $playlistsCount = $this->audios->getUserPlaylistsCount($entity); + } + + $this->template->playlists = iterator_to_array($playlists); + $this->template->playlistsCount = $playlistsCount; + $this->template->owner = $entity; + $this->template->ownerId = $owner; + $this->template->club = $owner < 0 ? $entity : NULL; + $this->template->isMy = ($owner > 0 && ($entity->getId() === $this->user->id)); + $this->template->isMyClub = ($owner < 0 && $entity->canBeModifiedBy($this->user->identity)); + } else { + $audios = $this->audios->getPopular(); + $audiosCount = $audios->size(); + } + + // $this->renderApp("owner=$owner"); + if ($audios !== []) { + $this->template->audios = iterator_to_array($audios); + $this->template->audiosCount = $audiosCount; + } + + $this->template->mode = $mode; + $this->template->page = $page; + + if(in_array($mode, ["list", "new", "popular"]) && $this->user->identity) + $this->template->friendsAudios = $this->user->identity->getBroadcastList("all", true); + } + + function renderEmbed(int $owner, int $id): void + { + $audio = $this->audios->getByOwnerAndVID($owner, $id); + if(!$audio) { + header("HTTP/1.1 404 Not Found"); + exit("<b>" . tr("audio_embed_not_found") . ".</b>"); + } else if($audio->isDeleted()) { + header("HTTP/1.1 410 Not Found"); + exit("<b>" . tr("audio_embed_deleted") . ".</b>"); + } else if($audio->isWithdrawn()) { + header("HTTP/1.1 451 Unavailable for legal reasons"); + exit("<b>" . tr("audio_embed_withdrawn") . ".</b>"); + } else if(!$audio->canBeViewedBy(NULL)) { + header("HTTP/1.1 403 Forbidden"); + exit("<b>" . tr("audio_embed_forbidden") . ".</b>"); + } else if(!$audio->isAvailable()) { + header("HTTP/1.1 425 Too Early"); + exit("<b>" . tr("audio_embed_processing") . ".</b>"); + } + + $this->template->audio = $audio; + } + + function renderUpload(): void + { + $this->assertUserLoggedIn(); + + $group = NULL; + $isAjax = $this->postParam("ajax", false) == 1; + if(!is_null($this->queryParam("gid"))) { + $gid = (int) $this->queryParam("gid"); + $group = (new Clubs)->get($gid); + if(!$group) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + + if(!$group->canUploadAudio($this->user->identity)) + $this->flashFail("err", tr("forbidden"), tr("not_enough_permissions_comment"), null, $isAjax); + } + + $this->template->group = $group; + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $upload = $_FILES["blob"]; + if(isset($upload) && file_exists($upload["tmp_name"])) { + if($upload["size"] > self::MAX_AUDIO_SIZE) + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large"), null, $isAjax); + } else { + $err = !isset($upload) ? 65536 : $upload["error"]; + $err = str_pad(dechex($err), 9, "0", STR_PAD_LEFT); + $readableError = tr("error_generic"); + + switch($upload["error"]) { + default: + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $readableError = tr("file_too_big"); + break; + case UPLOAD_ERR_PARTIAL: + $readableError = tr("file_loaded_partially"); + break; + case UPLOAD_ERR_NO_FILE: + $readableError = tr("file_not_uploaded"); + break; + case UPLOAD_ERR_NO_TMP_DIR: + $readableError = "Missing a temporary folder."; + break; + case UPLOAD_ERR_CANT_WRITE: + case UPLOAD_ERR_EXTENSION: + $readableError = "Failed to write file to disk. "; + break; + } + + $this->flashFail("err", tr("error"), $readableError . " " . tr("error_code", $err), null, $isAjax); + } + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "Other" : $this->postParam("genre"); + $nsfw = ($this->postParam("explicit") ?? "off") === "on"; + if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars + $this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, $isAjax); + + $audio = new Audio; + $audio->setOwner($this->user->id); + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? NULL : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + + try { + $audio->setFile($upload); + } catch(\DomainException $ex) { + $e = $ex->getMessage(); + $this->flashFail("err", tr("error"), tr("media_file_corrupted_or_too_large") . " $e.", null, $isAjax); + } catch(\RuntimeException $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_timeout"), null, $isAjax); + } catch(\BadMethodCallException $ex) { + $this->flashFail("err", tr("error"), "Загрузка аудио под Linux на данный момент не реализована. Следите за обновлениями: <a href='https://github.com/openvk/openvk/pull/512/commits'>https://github.com/openvk/openvk/pull/512/commits</a>", null, $isAjax); + } catch(\Exception $ex) { + $this->flashFail("err", tr("error"), tr("ffmpeg_not_installed"), null, $isAjax); + } + + $audio->save(); + $audio->add($group ?? $this->user->identity); + + if(!$isAjax) + $this->redirect(is_null($group) ? "/audios" . $this->user->id : "/audios-" . $group->getId()); + else { + $redirectLink = "/audios"; + + if(!is_null($group)) + $redirectLink .= $group->getRealId(); + else + $redirectLink .= $this->user->id; + + $pagesCount = (int)ceil((new Audios)->getCollectionSizeByEntityId(isset($group) ? $group->getRealId() : $this->user->id) / 10); + $redirectLink .= "?p=".$pagesCount; + + $this->returnJson([ + "success" => true, + "redirect_link" => $redirectLink, + ]); + } + } + + function renderListen(int $id): void + { + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $this->assertNoCSRF(); + + if(is_null($this->user)) + $this->returnJson(["success" => false]); + + $audio = $this->audios->get($id); + + if ($audio && !$audio->isDeleted() && !$audio->isWithdrawn()) { + if(!empty($this->postParam("playlist"))) { + $playlist = (new Audios)->getPlaylist((int)$this->postParam("playlist")); + + if(!$playlist || $playlist->isDeleted() || !$playlist->canBeViewedBy($this->user->identity) || !$playlist->hasAudio($audio)) + $playlist = NULL; + } + + $listen = $audio->listen($this->user->identity, $playlist); + + $returnArr = ["success" => $listen]; + + if($playlist) + $returnArr["new_playlists_listens"] = $playlist->getListens(); + + $this->returnJson($returnArr); + } + + $this->returnJson(["success" => false]); + } else { + $this->redirect("/"); + } + } + + function renderSearch(): void + { + $this->redirect("/search?type=audios"); + } + + function renderNewPlaylist(): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + + $owner = $this->user->id; + + if ($this->requestParam("gid")) { + $club = (new Clubs)->get((int) abs((int)$this->requestParam("gid"))); + if (!$club || $club->isBanned() || !$club->canBeModifiedBy($this->user->identity)) + $this->redirect("/audios" . $this->user->id); + + $owner = ($club->getId() * -1); + + $this->template->club = $club; + } + + $this->template->owner = $owner; + + if ($_SERVER["REQUEST_METHOD"] === "POST") { + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $audios = !empty($this->postParam("audios")) ? array_slice(explode(",", $this->postParam("audios")), 0, 100) : []; + + if(empty($title) || iconv_strlen($title) < 1) + $this->flashFail("err", tr("error"), tr("set_playlist_name")); + + $playlist = new Playlist; + $playlist->setOwner($owner); + $playlist->setName(substr($title, 0, 125)); + $playlist->setDescription(substr($description, 0, 2045)); + + if($_FILES["cover"]["error"] === UPLOAD_ERR_OK) { + if(!str_starts_with($_FILES["cover"]["type"], "image")) + $this->flashFail("err", tr("error"), tr("not_a_photo")); + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["cover"]); + } catch(\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo")); + } + } + + $playlist->save(); + + foreach($audios as $audio) { + $audio = $this->audios->get((int)$audio); + + if(!$audio || $audio->isDeleted() || !$audio->canBeViewedBy($this->user->identity)) + continue; + + $playlist->add($audio); + } + + $playlist->bookmark(isset($club) ? $club : $this->user->identity); + $this->redirect("/playlist" . $owner . "_" . $playlist->getId()); + } else { + if(isset($club)) { + $this->template->audios = iterator_to_array($this->audios->getByClub($club, 1, 10)); + $count = (new Audios)->getClubCollectionSize($club); + } else { + $this->template->audios = iterator_to_array($this->audios->getByUser($this->user->identity, 1, 10)); + $count = (new Audios)->getUserCollectionSize($this->user->identity); + } + + $this->template->pagesCount = ceil($count / 10); + } + } + + function renderPlaylistAction(int $id) { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $playlist = $this->audios->getPlaylist($id); + + if(!$playlist || $playlist->isDeleted()) + $this->flashFail("err", "error", tr("invalid_playlist"), null, true); + + switch ($this->queryParam("act")) { + case "bookmark": + if(!$playlist->isBookmarkedBy($this->user->identity)) + $playlist->bookmark($this->user->identity); + else + $this->flashFail("err", "error", tr("playlist_already_bookmarked"), null, true); + + break; + case "unbookmark": + if($playlist->isBookmarkedBy($this->user->identity)) + $playlist->unbookmark($this->user->identity); + else + $this->flashFail("err", "error", tr("playlist_not_bookmarked"), null, true); + + break; + case "delete": + if($playlist->canBeModifiedBy($this->user->identity)) { + $tmOwner = $playlist->getOwner(); + $playlist->delete(); + } else + $this->flashFail("err", "error", tr("access_denied"), null, true); + + $this->returnJson(["success" => true, "id" => $tmOwner->getRealId()]); + break; + default: + break; + } + + $this->returnJson(["success" => true]); + } + + function renderEditPlaylist(int $owner_id, int $virtual_id) + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(); + + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + $page = (int)($this->queryParam("p") ?? 1); + if (!$playlist || $playlist->isDeleted() || !$playlist->canBeModifiedBy($this->user->identity)) + $this->notFound(); + + $this->template->playlist = $playlist; + $this->template->page = $page; + + $audios = iterator_to_array($playlist->fetch(1, $playlist->size())); + $this->template->audios = array_slice($audios, 0, 10); + $audiosIds = []; + + foreach($audios as $aud) + $audiosIds[] = $aud->getId(); + + $this->template->audiosIds = implode(",", array_unique($audiosIds)) . ","; + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + $this->template->pagesCount = $pagesCount = ceil($playlist->size() / 10); + + if($_SERVER["REQUEST_METHOD"] !== "POST") + return; + + $title = $this->postParam("title"); + $description = $this->postParam("description"); + $new_audios = !empty($this->postParam("audios")) ? explode(",", rtrim($this->postParam("audios"), ",")) : []; + + if(empty($title) || iconv_strlen($title) < 1) + $this->flashFail("err", tr("error"), tr("set_playlist_name")); + + $playlist->setName(ovk_proc_strtr($title, 125)); + $playlist->setDescription(ovk_proc_strtr($description, 2045)); + $playlist->setEdited(time()); + $playlist->resetLength(); + + if($_FILES["new_cover"]["error"] === UPLOAD_ERR_OK) { + if(!str_starts_with($_FILES["new_cover"]["type"], "image")) + $this->flashFail("err", tr("error"), tr("not_a_photo")); + + try { + $playlist->fastMakeCover($this->user->id, $_FILES["new_cover"]); + } catch(\Throwable $e) { + $this->flashFail("err", tr("error"), tr("invalid_cover_photo")); + } + } + + $playlist->save(); + + DatabaseConnection::i()->getContext()->table("playlist_relations")->where([ + "collection" => $playlist->getId() + ])->delete(); + + foreach ($new_audios as $new_audio) { + $audio = (new Audios)->get((int)$new_audio); + + if(!$audio || $audio->isDeleted()) + continue; + + $playlist->add($audio); + } + + $this->redirect("/playlist".$playlist->getPrettyId()); + } + + function renderPlaylist(int $owner_id, int $virtual_id): void + { + $playlist = $this->audios->getPlaylistByOwnerAndVID($owner_id, $virtual_id); + $page = (int)($this->queryParam("p") ?? 1); + if (!$playlist || $playlist->isDeleted()) + $this->notFound(); + + $this->template->playlist = $playlist; + $this->template->page = $page; + $this->template->audios = iterator_to_array($playlist->fetch($page, 10)); + $this->template->ownerId = $owner_id; + $this->template->owner = $playlist->getOwner(); + $this->template->isBookmarked = $playlist->isBookmarkedBy($this->user->identity); + $this->template->isMy = $playlist->getOwner()->getId() === $this->user->id; + $this->template->canEdit = $playlist->canBeModifiedBy($this->user->identity); + } + + function renderAction(int $audio_id): void + { + $this->assertUserLoggedIn(); + $this->willExecuteWriteAction(true); + $this->assertNoCSRF(); + + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $audio = $this->audios->get($audio_id); + + if(!$audio || $audio->isDeleted()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + switch ($this->queryParam("act")) { + case "add": + if($audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if(!$audio->isInLibraryOf($this->user->identity)) + $audio->add($this->user->identity); + else + $this->flashFail("err", "error", tr("do_have_audio"), null, true); + + break; + + case "remove": + if($audio->isInLibraryOf($this->user->identity)) + $audio->remove($this->user->identity); + else + $this->flashFail("err", "error", tr("do_not_have_audio"), null, true); + + break; + case "remove_club": + $club = (new Clubs)->get((int)$this->postParam("club")); + + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + if($audio->isInLibraryOf($club)) + $audio->remove($club); + else + $this->flashFail("err", "error", tr("group_hasnt_audio"), null, true); + + break; + case "add_to_club": + $club = (new Clubs)->get((int)$this->postParam("club")); + + if(!$club || !$club->canBeModifiedBy($this->user->identity)) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + if(!$audio->isInLibraryOf($club)) + $audio->add($club); + else + $this->flashFail("err", "error", tr("group_has_audio"), null, true); + + break; + case "delete": + if($audio->canBeModifiedBy($this->user->identity)) + $audio->delete(); + else + $this->flashFail("err", "error", tr("access_denied"), null, true); + + break; + case "edit": + $audio = $this->audios->get($audio_id); + if (!$audio || $audio->isDeleted() || $audio->isWithdrawn()) + $this->flashFail("err", "error", tr("invalid_audio"), null, true); + + if ($audio->getOwner()->getId() !== $this->user->id) + $this->flashFail("err", "error", tr("access_denied"), null, true); + + $performer = $this->postParam("performer"); + $name = $this->postParam("name"); + $lyrics = $this->postParam("lyrics"); + $genre = empty($this->postParam("genre")) ? "undefined" : $this->postParam("genre"); + $nsfw = (int)($this->postParam("explicit") ?? 0) === 1; + $unlisted = (int)($this->postParam("unlisted") ?? 0) === 1; + if(empty($performer) || empty($name) || iconv_strlen($performer . $name) > 128) # FQN of audio must not be more than 128 chars + $this->flashFail("err", tr("error"), tr("error_insufficient_info"), null, true); + + $audio->setName($name); + $audio->setPerformer($performer); + $audio->setLyrics(empty($lyrics) ? NULL : $lyrics); + $audio->setGenre($genre); + $audio->setExplicit($nsfw); + $audio->setSearchability($unlisted); + $audio->setEdited(time()); + $audio->save(); + + $this->returnJson(["success" => true, "new_info" => [ + "name" => ovk_proc_strtr($audio->getTitle(), 40), + "performer" => ovk_proc_strtr($audio->getPerformer(), 40), + "lyrics" => nl2br($audio->getLyrics() ?? ""), + "lyrics_unformatted" => $audio->getLyrics() ?? "", + "explicit" => $audio->isExplicit(), + "genre" => $audio->getGenre(), + "unlisted" => $audio->isUnlisted(), + ]]); + break; + + default: + break; + } + + $this->returnJson(["success" => true]); + } + + function renderPlaylists(int $owner) + { + $this->renderList($owner, "playlists"); + } + + function renderApiGetContext() + { + if ($_SERVER["REQUEST_METHOD"] !== "POST") { + header("HTTP/1.1 405 Method Not Allowed"); + $this->redirect("/"); + } + + $ctx_type = $this->postParam("context"); + $ctx_id = (int)($this->postParam("context_entity")); + $page = (int)($this->postParam("page") ?? 1); + $perPage = 10; + + switch($ctx_type) { + default: + case "entity_audios": + if($ctx_id >= 0) { + $entity = $ctx_id != 0 ? (new Users)->get($ctx_id) : $this->user->identity; + + if(!$entity || !$entity->getPrivacyPermission("audios.read", $this->user->identity)) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $this->audios->getByUser($entity, $page, $perPage); + $audiosCount = $this->audios->getUserCollectionSize($entity); + } else { + $entity = (new Clubs)->get(abs($ctx_id)); + + if(!$entity || $entity->isBanned()) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $this->audios->getByClub($entity, $page, $perPage); + $audiosCount = $this->audios->getClubCollectionSize($entity); + } + break; + case "new_audios": + $audios = $this->audios->getNew(); + $audiosCount = $audios->size(); + break; + case "popular_audios": + $audios = $this->audios->getPopular(); + $audiosCount = $audios->size(); + break; + case "playlist_context": + $playlist = $this->audios->getPlaylist($ctx_id); + + if (!$playlist || $playlist->isDeleted()) + $this->flashFail("err", "Error", "Can't get queue", 80, true); + + $audios = $playlist->fetch($page, 10); + $audiosCount = $playlist->size(); + break; + case "search_context": + $stream = $this->audios->search($this->postParam("query"), 2, $this->postParam("type") === "by_performer"); + $audios = $stream->page($page, 10); + $audiosCount = $stream->size(); + break; + } + + $pagesCount = ceil($audiosCount / $perPage); + + # костылёк для получения плееров в пикере аудиозаписей + if((int)($this->postParam("returnPlayers")) === 1) { + $this->template->audios = $audios; + $this->template->page = $page; + $this->template->pagesCount = $pagesCount; + $this->template->count = $audiosCount; + + return 0; + } + + $audiosArr = []; + + foreach($audios as $audio) { + $audiosArr[] = [ + "id" => $audio->getId(), + "name" => $audio->getTitle(), + "performer" => $audio->getPerformer(), + "keys" => $audio->getKeys(), + "url" => $audio->getUrl(), + "length" => $audio->getLength(), + "available" => $audio->isAvailable(), + "withdrawn" => $audio->isWithdrawn(), + ]; + } + + $resultArr = [ + "success" => true, + "page" => $page, + "perPage" => $perPage, + "pagesCount" => $pagesCount, + "count" => $audiosCount, + "items" => $audiosArr, + ]; + + $this->returnJson($resultArr); + } +} diff --git a/Web/Presenters/BlobPresenter.php b/Web/Presenters/BlobPresenter.php index 7bb3e2be..5987281d 100644 --- a/Web/Presenters/BlobPresenter.php +++ b/Web/Presenters/BlobPresenter.php @@ -18,6 +18,8 @@ final class BlobPresenter extends OpenVKPresenter function renderFile(/*string*/ $dir, string $name, string $format) { + header("Access-Control-Allow-Origin: *"); + $dir = $this->getDirName($dir); $base = realpath(OPENVK_ROOT . "/storage/$dir"); $path = realpath(OPENVK_ROOT . "/storage/$dir/$name.$format"); @@ -37,5 +39,5 @@ final class BlobPresenter extends OpenVKPresenter readfile($path); exit; - } + } } diff --git a/Web/Presenters/CommentPresenter.php b/Web/Presenters/CommentPresenter.php index e005af86..86ccb126 100644 --- a/Web/Presenters/CommentPresenter.php +++ b/Web/Presenters/CommentPresenter.php @@ -2,7 +2,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Comment, Notifications\MentionNotification, Photo, Video, User, Topic, Post}; use openvk\Web\Models\Entities\Notifications\CommentNotification; -use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos}; +use openvk\Web\Models\Repositories\{Comments, Clubs, Videos, Photos, Audios}; final class CommentPresenter extends OpenVKPresenter { @@ -103,8 +103,27 @@ final class CommentPresenter extends OpenVKPresenter } } } + + $audios = []; + + if(!empty($this->postParam("audios"))) { + $un = rtrim($this->postParam("audios"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + } + } - if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1) + if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1) $this->flashFail("err", tr("error_when_publishing_comment"), tr("error_comment_empty")); try { @@ -126,6 +145,9 @@ final class CommentPresenter extends OpenVKPresenter if(sizeof($videos) > 0) foreach($videos as $vid) $comment->attach($vid); + + foreach($audios as $audio) + $comment->attach($audio); if($entity->getOwner()->getId() !== $this->user->identity->getId()) if(($owner = $entity->getOwner()) instanceof User) diff --git a/Web/Presenters/GroupPresenter.php b/Web/Presenters/GroupPresenter.php index d3a46fd5..eb8f446c 100644 --- a/Web/Presenters/GroupPresenter.php +++ b/Web/Presenters/GroupPresenter.php @@ -3,7 +3,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{Club, Photo, Post}; use Nette\InvalidStateException; use openvk\Web\Models\Entities\Notifications\ClubModeratorNotification; -use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics}; +use openvk\Web\Models\Repositories\{Clubs, Users, Albums, Managers, Topics, Audios}; use Chandler\Security\Authenticator; final class GroupPresenter extends OpenVKPresenter @@ -31,6 +31,8 @@ final class GroupPresenter extends OpenVKPresenter $this->template->albumsCount = (new Albums)->getClubAlbumsCount($club); $this->template->topics = (new Topics)->getLastTopics($club, 3); $this->template->topicsCount = (new Topics)->getClubTopicsCount($club); + $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($club->getRealId()); + $this->template->audiosCount = (new Audios)->getClubCollectionSize($club); } $this->template->club = $club; @@ -218,6 +220,7 @@ final class GroupPresenter extends OpenVKPresenter $club->setAdministrators_List_Display(empty($this->postParam("administrators_list_display")) ? 0 : $this->postParam("administrators_list_display")); $club->setEveryone_Can_Create_Topics(empty($this->postParam("everyone_can_create_topics")) ? 0 : 1); $club->setDisplay_Topics_Above_Wall(empty($this->postParam("display_topics_above_wall")) ? 0 : 1); + $club->setEveryone_can_upload_audios(empty($this->postParam("upload_audios")) ? 0 : 1); $club->setHide_From_Global_Feed(empty($this->postParam("hide_from_global_feed")) ? 0 : 1); $website = $this->postParam("website") ?? ""; diff --git a/Web/Presenters/ReportPresenter.php b/Web/Presenters/ReportPresenter.php index a87154c8..0c4cbd83 100644 --- a/Web/Presenters/ReportPresenter.php +++ b/Web/Presenters/ReportPresenter.php @@ -23,7 +23,7 @@ final class ReportPresenter extends OpenVKPresenter if ($_SERVER["REQUEST_METHOD"] === "POST") $this->assertNoCSRF(); - $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user"]) ? $this->queryParam("act") : NULL; + $act = in_array($this->queryParam("act"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"]) ? $this->queryParam("act") : NULL; if (!$this->queryParam("orig")) { $this->template->reports = $this->reports->getReports(0, (int)($this->queryParam("p") ?? 1), $act, $_SERVER["REQUEST_METHOD"] !== "POST"); @@ -88,7 +88,7 @@ final class ReportPresenter extends OpenVKPresenter if(!$id) exit(json_encode([ "error" => tr("error_segmentation") ])); - if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user"])) { + if(in_array($this->queryParam("type"), ["post", "photo", "video", "group", "comment", "note", "app", "user", "audio"])) { if (count(iterator_to_array($this->reports->getDuplicates($this->queryParam("type"), $id, NULL, $this->user->id))) <= 0) { $report = new Report; $report->setUser_id($this->user->id); diff --git a/Web/Presenters/SearchPresenter.php b/Web/Presenters/SearchPresenter.php index fadf9954..d80c06c3 100644 --- a/Web/Presenters/SearchPresenter.php +++ b/Web/Presenters/SearchPresenter.php @@ -1,7 +1,7 @@ <?php declare(strict_types=1); namespace openvk\Web\Presenters; use openvk\Web\Models\Entities\{User, Club}; -use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes}; +use openvk\Web\Models\Repositories\{Users, Clubs, Posts, Comments, Videos, Applications, Notes, Audios}; use Chandler\Database\DatabaseConnection; final class SearchPresenter extends OpenVKPresenter @@ -13,6 +13,7 @@ final class SearchPresenter extends OpenVKPresenter private $videos; private $apps; private $notes; + private $audios; function __construct(Users $users, Clubs $clubs) { @@ -23,22 +24,21 @@ final class SearchPresenter extends OpenVKPresenter $this->videos = new Videos; $this->apps = new Applications; $this->notes = new Notes; + $this->audios = new Audios; parent::__construct(); } function renderIndex(): void { + $this->assertUserLoggedIn(); + $query = $this->queryParam("query") ?? ""; $type = $this->queryParam("type") ?? "users"; $sorter = $this->queryParam("sort") ?? "id"; $invert = $this->queryParam("invert") == 1 ? "ASC" : "DESC"; $page = (int) ($this->queryParam("p") ?? 1); - $this->willExecuteWriteAction(); - if($query != "") - $this->assertUserLoggedIn(); - # https://youtu.be/pSAWM5YuXx8 $repos = [ @@ -47,7 +47,7 @@ final class SearchPresenter extends OpenVKPresenter "posts" => "posts", "comments" => "comments", "videos" => "videos", - "audios" => "posts", + "audios" => "audios", "apps" => "apps", "notes" => "notes" ]; @@ -62,7 +62,17 @@ final class SearchPresenter extends OpenVKPresenter break; case "rating": $sort = "rating " . $invert; - break; + break; + case "length": + if($type != "audios") break; + + $sort = "length " . $invert; + break; + case "listens": + if($type != "audios") break; + + $sort = "listens " . $invert; + break; } $parameters = [ @@ -86,18 +96,21 @@ final class SearchPresenter extends OpenVKPresenter "hometown" => $this->queryParam("hometown") != "" ? $this->queryParam("hometown") : NULL, "before" => $this->queryParam("datebefore") != "" ? strtotime($this->queryParam("datebefore")) : NULL, "after" => $this->queryParam("dateafter") != "" ? strtotime($this->queryParam("dateafter")) : NULL, - "gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL + "gender" => $this->queryParam("gender") != "" && $this->queryParam("gender") != 2 ? $this->queryParam("gender") : NULL, + "only_performers" => $this->queryParam("only_performers") == "on" ? "1" : NULL, + "with_lyrics" => $this->queryParam("with_lyrics") == "on" ? true : NULL, ]; $repo = $repos[$type] or $this->throwError(400, "Bad Request", "Invalid search entity $type."); $results = $this->{$repo}->find($query, $parameters, $sort); - $iterator = $results->page($page); + $iterator = $results->page($page, 14); $count = $results->size(); $this->template->iterator = iterator_to_array($iterator); $this->template->count = $count; $this->template->type = $type; $this->template->page = $page; + $this->template->perPage = 14; } } diff --git a/Web/Presenters/UserPresenter.php b/Web/Presenters/UserPresenter.php index e645f888..56d5685b 100644 --- a/Web/Presenters/UserPresenter.php +++ b/Web/Presenters/UserPresenter.php @@ -5,7 +5,7 @@ use openvk\Web\Util\Sms; use openvk\Web\Themes\Themepacks; use openvk\Web\Models\Entities\{Photo, Post, EmailChangeVerification}; use openvk\Web\Models\Entities\Notifications\{CoinsTransferNotification, RatingUpNotification}; -use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications}; +use openvk\Web\Models\Repositories\{Users, Clubs, Albums, Videos, Notes, Vouchers, EmailChangeVerifications, Audios}; use openvk\Web\Models\Exceptions\InvalidUserNameException; use openvk\Web\Util\Validator; use Chandler\Security\Authenticator; @@ -45,7 +45,10 @@ final class UserPresenter extends OpenVKPresenter $this->template->videosCount = (new Videos)->getUserVideosCount($user); $this->template->notes = (new Notes)->getUserNotes($user, 1, 4); $this->template->notesCount = (new Notes)->getUserNotesCount($user); - + $this->template->audios = (new Audios)->getRandomThreeAudiosByEntityId($user->getId()); + $this->template->audiosCount = (new Audios)->getUserCollectionSize($user); + $this->template->audioStatus = $user->getCurrentAudioStatus(); + $this->template->user = $user; } } @@ -169,6 +172,7 @@ final class UserPresenter extends OpenVKPresenter if ($this->postParam("gender") <= 1 && $this->postParam("gender") >= 0) $user->setSex($this->postParam("gender")); + $user->setAudio_broadcast_enabled($this->checkbox("broadcast_music")); if(!empty($this->postParam("phone")) && $this->postParam("phone") !== $user->getPhone()) { if(!OPENVK_ROOT_CONF["openvk"]["credentials"]["smsc"]["enable"]) @@ -241,6 +245,7 @@ final class UserPresenter extends OpenVKPresenter } $user->setStatus(empty($this->postParam("status")) ? NULL : $this->postParam("status")); + $user->setAudio_broadcast_enabled($this->postParam("broadcast") == 1); $user->save(); $this->returnJson([ @@ -430,6 +435,7 @@ final class UserPresenter extends OpenVKPresenter "friends.add", "wall.write", "messages.write", + "audios.read", ]; foreach($settings as $setting) { $input = $this->postParam(str_replace(".", "_", $setting)); @@ -474,6 +480,7 @@ final class UserPresenter extends OpenVKPresenter } else if($_GET['act'] === "lMenu") { $settings = [ "menu_bildoj" => "photos", + "menu_muziko" => "audios", "menu_filmetoj" => "videos", "menu_mesagoj" => "messages", "menu_notatoj" => "notes", diff --git a/Web/Presenters/WallPresenter.php b/Web/Presenters/WallPresenter.php index d89b722a..a01c8262 100644 --- a/Web/Presenters/WallPresenter.php +++ b/Web/Presenters/WallPresenter.php @@ -3,7 +3,7 @@ namespace openvk\Web\Presenters; use openvk\Web\Models\Exceptions\TooMuchOptionsException; use openvk\Web\Models\Entities\{Poll, Post, Photo, Video, Club, User}; use openvk\Web\Models\Entities\Notifications\{MentionNotification, RepostNotification, WallPostNotification}; -use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos}; +use openvk\Web\Models\Repositories\{Posts, Users, Clubs, Albums, Notes, Videos, Comments, Photos, Audios}; use Chandler\Database\DatabaseConnection; use Nette\InvalidStateException as ISE; use Bhaktaraz\RSSGenerator\Item; @@ -311,8 +311,27 @@ final class WallPresenter extends OpenVKPresenter } } } + + $audios = []; + + if(!empty($this->postParam("audios"))) { + $un = rtrim($this->postParam("audios"), ","); + $arr = explode(",", $un); + + if(sizeof($arr) < 11) { + foreach($arr as $dat) { + $ids = explode("_", $dat); + $audio = (new Audios)->getByOwnerAndVID((int)$ids[0], (int)$ids[1]); + + if(!$audio || $audio->isDeleted()) + continue; + + $audios[] = $audio; + } + } + } - if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && !$poll && !$note) + if(empty($this->postParam("text")) && sizeof($photos) < 1 && sizeof($videos) < 1 && sizeof($audios) < 1 && !$poll && !$note) $this->flashFail("err", tr("failed_to_publish_post"), tr("post_is_empty_or_too_big")); try { @@ -341,6 +360,9 @@ final class WallPresenter extends OpenVKPresenter if(!is_null($note)) $post->attach($note); + + foreach($audios as $audio) + $post->attach($audio); if($wall > 0 && $wall !== $this->user->identity->getId()) (new WallPostNotification($wallOwner, $post, $this->user->identity))->emit(); diff --git a/Web/Presenters/templates/@layout.xml b/Web/Presenters/templates/@layout.xml index f8a975e0..31e0bc5c 100644 --- a/Web/Presenters/templates/@layout.xml +++ b/Web/Presenters/templates/@layout.xml @@ -16,6 +16,8 @@ {script "js/node_modules/umbrellajs/umbrella.min.js"} {script "js/l10n.js"} {script "js/openvk.cls.js"} + {script "js/node_modules/dashjs/dist/dash.all.min.js"} + {script "js/al_music.js"} {css "js/node_modules/tippy.js/dist/backdrop.css"} {css "js/node_modules/tippy.js/dist/border.css"} @@ -122,6 +124,7 @@ <option value="comments">{_s_by_comments}</option> <option value="videos">{_s_by_videos}</option> <option value="apps">{_s_by_apps}</option> + <option value="audios">{_s_by_audios}</option> </select> </form> <div class="searchTips" id="srcht" hidden> @@ -140,13 +143,13 @@ <option value="comments" {if str_contains($_SERVER['REQUEST_URI'], "type=comments")}selected{/if}>{_s_by_comments}</option> <option value="videos" {if str_contains($_SERVER['REQUEST_URI'], "type=videos")}selected{/if}>{_s_by_videos}</option> <option value="apps" {if str_contains($_SERVER['REQUEST_URI'], "type=apps")}selected{/if}>{_s_by_apps}</option> + <option value="audios" {if str_contains($_SERVER['REQUEST_URI'], "type=audios")}selected{/if}>{_s_by_audios}</option> </select> <button class="searchBtn"><span style="color:white;font-weight: 600;font-size:12px;">{_header_search}</span></button> </form> <script> let els = document.querySelectorAll("div.dec") - for(const element of els) - { + for(const element of els) { element.style.display = "none" } </script> @@ -182,6 +185,7 @@ </a> <a n:if="$thisUser->getLeftMenuItemStatus('photos')" href="/albums{$thisUser->getId()}" class="link">{_my_photos}</a> <a n:if="$thisUser->getLeftMenuItemStatus('videos')" href="/videos{$thisUser->getId()}" class="link">{_my_videos}</a> + <a n:if="$thisUser->getLeftMenuItemStatus('audios')" href="/audios{$thisUser->getId()}" class="link">{_my_audios}</a> <a n:if="$thisUser->getLeftMenuItemStatus('messages')" href="/im" class="link">{_my_messages} <object type="internal/link" n:if="$thisUser->getUnreadMessagesCount() > 0"> (<b>{$thisUser->getUnreadMessagesCount()}</b>) @@ -426,6 +430,12 @@ //]]> </script> + <script> + window.openvk = { + "audio_genres": {\openvk\Web\Models\Entities\Audio::genres} + } + </script> + {ifset bodyScripts} {include bodyScripts} {/ifset} diff --git a/Web/Presenters/templates/About/Tour.xml b/Web/Presenters/templates/About/Tour.xml index cc484c66..0c02b1ca 100755 --- a/Web/Presenters/templates/About/Tour.xml +++ b/Web/Presenters/templates/About/Tour.xml @@ -178,11 +178,29 @@ <h2>{_tour_section_6_title_1|noescape}</h2> <ul class="listing"> - <li><span>{_tour_section_6_text_1|noescape}</span></li> + <li><span>{_tour_section_6_text_1|noescape}</span></li> + <li><span>{_tour_section_6_text_2|noescape}</span></li> + <li><span>{_tour_section_6_text_3|noescape}</span></li> + <img src="assets/packages/static/openvk/img/tour/audios.png" width="440"> </ul> + + <ul class="listing"> + <li><span>{_tour_section_6_text_4|noescape}</span></li> + <img src="assets/packages/static/openvk/img/tour/audios_search.png" width="440"> + <li><span>{_tour_section_6_text_5|noescape}</span></li> + <img src="assets/packages/static/openvk/img/tour/audios_upload.png" width="440"> + </ul> - - + <p class="big">{_tour_section_6_bottom_text_1|noescape}</p> + + <h2>{_tour_section_6_title_2|noescape}</h2> + + <ul class="listing"> + <li><span>{_tour_section_6_text_6|noescape}</span></li> + <li><span>{_tour_section_6_text_7|noescape}</span></li> + <img src="assets/packages/static/openvk/img/tour/audios_playlists.png" width="440"> + </ul> + <br> </div> diff --git a/Web/Presenters/templates/Admin/@layout.xml b/Web/Presenters/templates/Admin/@layout.xml index f254dd9a..055bd0a0 100644 --- a/Web/Presenters/templates/Admin/@layout.xml +++ b/Web/Presenters/templates/Admin/@layout.xml @@ -97,6 +97,9 @@ <li> <a href="/admin/bannedLinks">{_admin_banned_links}</a> </li> + <li> + <a href="/admin/music">{_admin_music}</a> + </li> </ul> <div class="aui-nav-heading"> <strong>Chandler</strong> diff --git a/Web/Presenters/templates/Admin/EditMusic.xml b/Web/Presenters/templates/Admin/EditMusic.xml new file mode 100644 index 00000000..c2584760 --- /dev/null +++ b/Web/Presenters/templates/Admin/EditMusic.xml @@ -0,0 +1,81 @@ +{extends "@layout.xml"} + +{block title} + {_edit} {$audio->getName()} +{/block} + +{block heading} + {$audio->getName()} +{/block} + +{block content} + <div class="aui-tabs horizontal-tabs"> + <form class="aui" method="POST"> + <div class="field-group"> + <label for="id">ID</label> + <input class="text medium-field" type="number" id="id" disabled value="{$audio->getId()}" /> + </div> + <div class="field-group"> + <label>{_created}</label> + {$audio->getPublicationTime()} + </div> + <div class="field-group"> + <label>{_edited}</label> + {$audio->getEditTime() ?? "never"} + </div> + <div class="field-group"> + <label for="name">{_name}</label> + <input class="text medium-field" type="text" id="name" name="name" value="{$audio->getTitle()}" /> + </div> + <div class="field-group"> + <label for="performer">{_performer}</label> + <input class="text medium-field" type="text" id="performer" name="performer" value="{$audio->getPerformer()}" /> + </div> + <div class="field-group"> + <label for="ext">{_lyrics}</label> + <textarea class="text medium-field" type="text" id="text" name="text" style="resize: vertical;">{$audio->getLyrics()}</textarea> + </div> + <div class="field-group"> + <label>{_admin_audio_length}</label> + {$audio->getFormattedLength()} + </div> + <div class="field-group"> + <label for="ext">{_genre}</label> + <select class="select medium-field" name="genre"> + <option n:foreach='\openvk\Web\Models\Entities\Audio::genres as $genre' + n:attr="selected: $genre == $audio->getGenre()" value="{$genre}"> + {$genre} + </option> + </select> + </div> + <div class="field-group"> + <label>{_admin_original_file}</label> + <audio controls src="{$audio->getOriginalURL(true)}"> + </div> + <hr /> + <div class="field-group"> + <label for="owner">{_owner}</label> + <input class="text medium-field" type="number" id="owner_id" name="owner" value="{$owner}" /> + </div> + <div class="field-group"> + <label for="explicit">Explicit</label> + <input class="toggle-large" type="checkbox" id="explicit" name="explicit" value="1" {if $audio->isExplicit()} checked {/if} /> + </div> + <div class="field-group"> + <label for="deleted">{_deleted}</label> + <input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $audio->isDeleted()} checked {/if} /> + </div> + <div class="field-group"> + <label for="withdrawn">{_withdrawn}</label> + <input class="toggle-large" type="checkbox" id="withdrawn" name="withdrawn" value="1" {if $audio->isWithdrawn()} checked {/if} /> + </div> + <hr /> + <div class="buttons-container"> + <div class="buttons"> + <input type="hidden" name="hash" value="{$csrfToken}" /> + <input class="button submit" type="submit" value="{_save}"> + </div> + </div> + </form> + </div> +{/block} diff --git a/Web/Presenters/templates/Admin/EditPlaylist.xml b/Web/Presenters/templates/Admin/EditPlaylist.xml new file mode 100644 index 00000000..b0bd823f --- /dev/null +++ b/Web/Presenters/templates/Admin/EditPlaylist.xml @@ -0,0 +1,54 @@ +{extends "@layout.xml"} + +{block title} + {_edit} {$playlist->getName()} +{/block} + +{block heading} + {$playlist->getName()} +{/block} + +{block content} + <div class="aui-tabs horizontal-tabs"> + <form class="aui" method="POST"> + <div class="field-group"> + <label for="id">ID</label> + <input class="text medium-field" type="number" id="id" disabled value="{$playlist->getId()}" /> + </div> + <div class="field-group"> + <label for="name">{_name}</label> + <input class="text medium-field" type="text" id="name" name="name" value="{$playlist->getName()}" /> + </div> + <div class="field-group"> + <label for="ext">{_description}</label> + <textarea class="text medium-field" type="text" id="description" name="description" style="resize: vertical;">{$playlist->getDescription()}</textarea> + </div> + <div class="field-group"> + <label for="ext">{_admin_cover_id}</label> + <span id="avatar" class="aui-avatar aui-avatar-project aui-avatar-xlarge"> + <span class="aui-avatar-inner"> + <img src="{$playlist->getCoverUrl()}" style="object-fit: cover;"></img> + </span> + </span> + <br /> + <input class="text medium-field" type="number" id="photo" name="photo" value="{$playlist->getCoverPhotoId()}" /> + </div> + <hr /> + <div class="field-group"> + <label for="owner">{_owner}</label> + <input class="text medium-field" type="number" id="owner_id" name="owner" value="{$playlist->getOwner()->getId()}" /> + </div> + <div class="field-group"> + <label for="deleted">{_deleted}</label> + <input class="toggle-large" type="checkbox" id="deleted" name="deleted" value="1" {if $playlist->isDeleted()} checked {/if} /> + </div> + <hr /> + <div class="buttons-container"> + <div class="buttons"> + <input type="hidden" name="hash" value="{$csrfToken}" /> + <input class="button submit" type="submit" value="{_save}"> + </div> + </div> + </form> + </div> +{/block} diff --git a/Web/Presenters/templates/Admin/Music.xml b/Web/Presenters/templates/Admin/Music.xml new file mode 100644 index 00000000..448ee54d --- /dev/null +++ b/Web/Presenters/templates/Admin/Music.xml @@ -0,0 +1,135 @@ +{extends "@layout.xml"} +{var $search = $mode === "audios"} + +{block title} + {_audios} +{/block} + +{block heading} + {_audios} +{/block} + +{block searchTitle} + {include title} +{/block} + +{block content} + <nav class="aui-navgroup aui-navgroup-horizontal"> + <div class="aui-navgroup-inner"> + <div class="aui-navgroup-primary"> + <ul class="aui-nav" resolved=""> + <li n:attr="class => $mode === 'audios' ? 'aui-nav-selected' : ''"> + <a href="?act=audios">{_audios}</a> + </li> + <li n:attr="class => $mode === 'playlists' ? 'aui-nav-selected' : ''"> + <a href="?act=playlists">{_playlists}</a> + </li> + </ul> + </div> + </div> + </nav> + + <table class="aui aui-table-list"> + {if $mode === "audios"} + {var $audios = iterator_to_array($audios)} + {var $amount = sizeof($audios)} + <thead> + <tr> + <th>ID</th> + <th>{_admin_author}</th> + <th>{_peformer}</th> + <th>{_admin_title}</th> + <th>{_genre}</th> + <th>Explicit</th> + <th>{_withdrawn}</th> + <th>{_deleted}</th> + <th>{_created}</th> + <th>{_actions}</th> + </tr> + </thead> + <tbody> + <tr n:foreach="$audios as $audio"> + <td>{$audio->getId()}</td> + <td> + {var $owner = $audio->getOwner()} + <span class="aui-avatar aui-avatar-xsmall"> + <span class="aui-avatar-inner"> + <img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" /> + </span> + </span> + + <a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> + + <span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span> + </td> + <td>{$audio->getPerformer()}</td> + <td>{$audio->getTitle()}</td> + <td>{$audio->getGenre()}</td> + <td>{$audio->isExplicit() ? tr("yes") : tr("no")}</td> + <td n:attr="style => $audio->isWithdrawn() ? 'color: red;' : ''"> + {$audio->isWithdrawn() ? tr("yes") : tr("no")} + </td> + <td n:attr="style => $audio->isDeleted() ? 'color: red;' : ''"> + {$audio->isDeleted() ? tr("yes") : tr("no")} + </td> + <td>{$audio->getPublicationTime()}</td> + <td> + <a class="aui-button aui-button-primary" href="/admin/music/{$audio->getId()}/edit"> + <span class="aui-icon aui-icon-small aui-iconfont-new-edit">Редактировать</span> + </a> + </td> + </tr> + </tbody> + {else} + {var $playlists = iterator_to_array($playlists)} + {var $amount = sizeof($playlists)} + <thead> + <tr> + <th>ID</th> + <th>{_admin_author}</th> + <th>{_name}</th> + <th>{_created_playlist}</th> + <th>{_actions}</th> + </tr> + </thead> + <tbody> + <tr n:foreach="$playlists as $playlist"> + <td>{$playlist->getId()}</td> + <td> + {var $owner = $playlist->getOwner()} + <span class="aui-avatar aui-avatar-xsmall"> + <span class="aui-avatar-inner"> + <img src="{$owner->getAvatarUrl('miniscule')}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" /> + </span> + </span> + + <a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> + + <span n:if="$owner->isBanned()" class="aui-lozenge aui-lozenge-subtle aui-lozenge-removed">{_admin_banned}</span> + </td> + <td> + <span class="aui-avatar aui-avatar-xsmall"> + <span class="aui-avatar-inner"> + <img src="{$playlist->getCoverURL()}" alt="{$owner->getCanonicalName()}" style="object-fit: cover;" role="presentation" /> + </span> + </span> + {ovk_proc_strtr($playlist->getName(), 30)} + </td> + <td>{$playlist->getCreationTime()}</td> + <td> + <a class="aui-button aui-button-primary" href="/admin/playlist/{$playlist->getId()}/edit"> + <span class="aui-icon aui-icon-small aui-iconfont-new-edit">{_edit}</span> + </a> + </td> + </tr> + </tbody> + {/if} + </table> + <br/> + <div align="right"> + {var $isLast = ((10 * (($_GET['p'] ?? 1) - 1)) + $amount) < $count} + + <a n:if="($_GET['p'] ?? 1) > 1" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) - 1}">«</a> + <a n:if="$isLast" class="aui-button" href="/admin/music?act={($_GET['act'] ?? 'audios')}&p={($_GET['p'] ?? 1) + 1}">»</a> + </div> +{/block} diff --git a/Web/Presenters/templates/Audio/ApiGetContext.xml b/Web/Presenters/templates/Audio/ApiGetContext.xml new file mode 100644 index 00000000..77b99990 --- /dev/null +++ b/Web/Presenters/templates/Audio/ApiGetContext.xml @@ -0,0 +1,7 @@ +<input type="hidden" name="count" value="{$count}"> +<input type="hidden" name="pagesCount" value="{$pagesCount}"> +<input type="hidden" name="page" value="{$page}"> + +{foreach $audios as $audio} + {include "player.xml", audio => $audio, hideButtons => true} +{/foreach} diff --git a/Web/Presenters/templates/Audio/EditPlaylist.xml b/Web/Presenters/templates/Audio/EditPlaylist.xml new file mode 100644 index 00000000..50dfcc2a --- /dev/null +++ b/Web/Presenters/templates/Audio/EditPlaylist.xml @@ -0,0 +1,95 @@ +{extends "../@layout.xml"} + +{block title}{_edit_playlist}{/block} + +{block header} + <a href="{$owner->getURL()}">{$owner->getCanonicalName()}</a> + » + <a href="/audios{$ownerId}">{_audios}</a> + » + <a href="/playlist{$playlist->getPrettyId()}">{_playlist}</a> + » + {_edit_playlist} +{/block} + +{block content} + <div class="playlistBlock" style="display: flex;margin-top: 0px;"> + <div class="playlistCover"> + <a> + <img src="{$playlist->getCoverURL('normal')}" alt="{_playlist_cover}"> + </a> + + <div class="profile_links" style="width: 139px;"> + <a class="profile_link" style="width: 98%;" id="_deletePlaylist" data-id="{$playlist->getId()}">{_delete_playlist}</a> + </div> + </div> + + <div style="padding-left: 13px;width:75%"> + <div class="playlistInfo"> + <input value="{$playlist->getName()}" type="text" name="title" maxlength="125"> + </div> + + <div class="moreInfo"> + <textarea name="description" maxlength="2045" style="margin-top: 11px;">{$playlist->getDescription()}</textarea> + </div> + </div> + </div> + + <div style="margin-top: 19px;"> + <input id="playlist_query" type="text" style="height: 26px;" placeholder="{_header_search}"> + <div class="playlistAudiosContainer editContainer"> + <div id="newPlaylistAudios" n:foreach="$audios as $audio"> + <div class="playerContainer"> + {include "player.xml", audio => $audio, hideButtons => true} + </div> + <div class="attachAudio addToPlaylist" data-id="{$audio->getId()}"> + <span>{_remove_from_playlist}</span> + </div> + </div> + </div> + + <div class="showMoreAudiosPlaylist" data-page="2" data-playlist="{$playlist->getId()}" n:if="$pagesCount > 1"> + {_show_more_audios} + </div> + </div> + + <form method="post" id="editPlaylistForm" data-id="{$playlist->getId()}" enctype="multipart/form-data"> + <input type="hidden" name="title" maxlength="128" /> + <input type="hidden" name="hash" value="{$csrfToken}" /> + <textarea style="display:none;" name="description" maxlength="2048" /> + <input type="hidden" name="audios"> + <input type="file" style="display:none;" name="new_cover" accept=".jpg,.png"> + + <div style="float:right;margin-top: 8px;"> + <button class="button" type="submit">{_save}</button> + </div> + </form> + + <script> + document.querySelector("input[name='audios']").value = {$audiosIds} + + u("#editPlaylistForm").on("submit", (e) => { + document.querySelector("#editPlaylistForm input[name='title']").value = document.querySelector(".playlistInfo input[name='title']").value + document.querySelector("#editPlaylistForm textarea[name='description']").value = document.querySelector(".playlistBlock textarea[name='description']").value + }) + + u("#editPlaylistForm input[name='new_cover']").on("change", (e) => { + if(!e.currentTarget.files[0].type.startsWith("image/")) { + fastError(tr("not_a_photo")) + return + } + + let image = URL.createObjectURL(e.currentTarget.files[0]) + + document.querySelector(".playlistCover img").src = image + }) + + u(".playlistCover img").on("click", (e) => { + document.querySelector("input[name='new_cover']").click() + }) + + document.querySelector("#editPlaylistForm input[name='new_cover']").value = "" + </script> + + {script "js/al_playlists.js"} +{/block} diff --git a/Web/Presenters/templates/Audio/Embed.xml b/Web/Presenters/templates/Audio/Embed.xml new file mode 100644 index 00000000..b7540358 --- /dev/null +++ b/Web/Presenters/templates/Audio/Embed.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd"> +<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> + <head> + <meta http-equiv="Content-Type" content="application/xhtml+xml; charset=utf-8" /> + <link rel="icon"> + <title>{$audio->getName()} + + {css "css/main.css"} + {css "css/audios.css"} + {script "js/node_modules/dashjs/dist/dash.all.min.js"} + {script "js/node_modules/jquery/dist/jquery.min.js"} + {script "js/node_modules/umbrellajs/umbrella.min.js"} + + + {include "player.xml", audio => $audio} + + {script "js/al_music.js"} + + diff --git a/Web/Presenters/templates/Audio/List.xml b/Web/Presenters/templates/Audio/List.xml new file mode 100644 index 00000000..ec24f3b3 --- /dev/null +++ b/Web/Presenters/templates/Audio/List.xml @@ -0,0 +1,126 @@ +{extends "../@layout.xml"} + +{block title} + {if $mode == 'list'} + {if $ownerId > 0} + {_audios} {$owner->getMorphedName("genitive", false)} + {else} + {_audios_group} + {/if} + {elseif $mode == 'new'} + {_audio_new} + {elseif $mode == 'popular'} + {_audio_popular} + {else} + {if $ownerId > 0} + {_playlists} {$owner->getMorphedName("genitive", false)} + {else} + {_playlists_group} + {/if} + {/if} +{/block} + +{block header} +
+
{_my_audios_small}
+
+
+ +
+ {_audios} + » + {_audio_new} +
+ +
+ {_audios} + » + {_audio_popular} +
+ +
+ {_audios} + » + {if $isMy}{_my_playlists}{else}{_playlists}{/if} +
+{/block} + +{block content} + {* ref: https://archive.li/P32em *} + + {include "bigplayer.xml"} + + + + +
+ +
+
+
+
+ {include "../components/error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_audios_thisuser") : tr("no_audios_user")) : tr("no_audios_club")} +
+
+
+ {include "player.xml", audio => $audio, club => $club} +
+
+ +
+ {include "../components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $audiosCount, + "amount" => sizeof($audios), + "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, + "atBottom" => true, + ]} +
+
+
+ +
+
+
+ {include "../components/error.xml", description => $ownerId > 0 ? ($ownerId == $thisUser->getId() ? tr("no_playlists_thisuser") : tr("no_playlists_user")) : tr("no_playlists_club")} +
+ + + +
+ {include "../components/paginator.xml", conf => (object) [ + "page" => $page, + "count" => $playlistsCount, + "amount" => sizeof($playlists), + "perPage" => $perPage ?? OPENVK_DEFAULT_PER_PAGE, + "atBottom" => true, + ]} +
+
+
+ {include "tabs.xml"} +
+{/block} diff --git a/Web/Presenters/templates/Audio/NewPlaylist.xml b/Web/Presenters/templates/Audio/NewPlaylist.xml new file mode 100644 index 00000000..42efae7f --- /dev/null +++ b/Web/Presenters/templates/Audio/NewPlaylist.xml @@ -0,0 +1,109 @@ +{extends "../@layout.xml"} + +{block title} + {_new_playlist} +{/block} + +{block header} + {if !$_GET["gid"]} + {$thisUser->getCanonicalName()} + » + {_audios} + {else} + {$club->getCanonicalName()} + » + {_audios} + {/if} + » + {_new_playlist} +{/block} + +{block content} + + +
+
+ + {_playlist_cover} + +
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + +
+
+
+ + +{/block} diff --git a/Web/Presenters/templates/Audio/bigplayer.xml b/Web/Presenters/templates/Audio/bigplayer.xml new file mode 100644 index 00000000..7e8ffbe9 --- /dev/null +++ b/Web/Presenters/templates/Audio/bigplayer.xml @@ -0,0 +1,56 @@ +
+
diff --git a/Web/Presenters/templates/Audio/player.xml b/Web/Presenters/templates/Audio/player.xml new file mode 100644 index 00000000..fb5862b4 --- /dev/null +++ b/Web/Presenters/templates/Audio/player.xml @@ -0,0 +1,69 @@ +{php $id = $audio->getId() . rand(0, 1000)} +{php $isWithdrawn = $audio->isWithdrawn()} +{php $editable = isset($thisUser) && $audio->canBeModifiedBy($thisUser)} +
+
diff --git a/Web/Presenters/templates/Audio/tabs.xml b/Web/Presenters/templates/Audio/tabs.xml new file mode 100644 index 00000000..ed74ca86 --- /dev/null +++ b/Web/Presenters/templates/Audio/tabs.xml @@ -0,0 +1,40 @@ + diff --git a/Web/Presenters/templates/Group/Edit.xml b/Web/Presenters/templates/Group/Edit.xml index 22f0b490..8f85e7e7 100644 --- a/Web/Presenters/templates/Group/Edit.xml +++ b/Web/Presenters/templates/Group/Edit.xml @@ -102,6 +102,15 @@ + + + {_audios}: + + + + + + diff --git a/Web/Presenters/templates/Group/View.xml b/Web/Presenters/templates/Group/View.xml index 0242bbb8..de00d9b0 100644 --- a/Web/Presenters/templates/Group/View.xml +++ b/Web/Presenters/templates/Group/View.xml @@ -90,6 +90,25 @@
+ +
+
+ {_audios} +
+
+
+ {tr("audios_count", $audiosCount)} +
+ {_all_title} +
+
+
+
+ {include "../Audio/player.xml", audio => $audio} +
+
+
+
{presenter "openvk!Wall->wallEmbedded", -$club->getId()}
diff --git a/Web/Presenters/templates/Photos/UploadPhoto.xml b/Web/Presenters/templates/Photos/UploadPhoto.xml index ab81d3aa..4f5f8e70 100644 --- a/Web/Presenters/templates/Photos/UploadPhoto.xml +++ b/Web/Presenters/templates/Photos/UploadPhoto.xml @@ -35,7 +35,7 @@
{_admin_limits} -