Compare commits

...

54 commits

Author SHA1 Message Date
veselcraft
d2fb1c0fd2
merge master 2023-06-06 16:25:44 +03:00
veselcraft
725e68d0e5
VKAPI: Add owner_id support to video.get (not sure if this works cuz it's hot outside and i'm tired) 2023-04-29 16:05:37 +03:00
Ilya Prokopenko
27fb0f1d90
Move the instance list to the wiki (RU) 2023-04-27 01:47:52 +00:00
Ilya Prokopenko
4a366b987a
Move the instance list to the wiki 2023-04-27 01:46:38 +00:00
Dmitry Tretyakov
62b93a3cf2
File picker buttons redesign (#870)
* Refactoring login and register button in sidebar

For Firefox 10.0 and other old 2000s browsers compatibillity

* CSS friends grid workaround for 2000s browsers

* CSS friends grid full fix

* Revert "CSS friends grid full fix"

This reverts commit 9d64cd2d2b.

* restyling file pickers button

* redefined some strings

* да блин

* Translating "Browse" string

* locale syntax and logic fixes

* Update uk.strings

* Added redesign in Photo section

* Structuring commits and adding redisgn in Video section
2023-04-24 22:50:26 +07:00
Dmitry Tretyakov
252ff0ead0
Fix weak password string syntax error 2023-04-20 11:00:08 +07:00
Dmitry Tretyakov
0ceac912ef
(Полу)фикс отображения краткого списка друзей по сетке для Firefox 10.0 и Opera 8.53 (#865)
* Refactoring login and register button in sidebar

For Firefox 10.0 and other old 2000s browsers compatibillity

* CSS friends grid workaround for 2000s browsers
2023-04-19 19:47:05 +03:00
Локоток
2f8612bb24
Проверка на надежный пароль (#862)
* Проверка на надежный пароль

* fixed typos in locales/ru.strings

Co-authored-by: Alexander Minkin <weryskok@gmail.com>

* подправил локаль

---------

Co-authored-by: Alexander Minkin <weryskok@gmail.com>
2023-04-19 14:02:33 +03:00
Dmitry Tretyakov
b89bbdcec2
Update README.md 2023-04-19 17:44:01 +07:00
Dmitry Tretyakov
34b585747f
Refactoring login and register button in sidebar (#864)
For Firefox 10.0 and other old 2000s browsers compatibillity
2023-04-19 11:12:05 +03:00
Dmitry Tretyakov
027a38a69c
Unlined Tinelix OVK Instance without TLS 2023-04-19 14:39:22 +07:00
Dmitry Tretyakov
770e73fc72
Added Tinelix OVK Instance 2023-04-19 14:37:37 +07:00
Ilya Prokopenko
1a56b09430
Change notify sound metadata and reduce its size 2023-04-11 15:43:30 +07:00
veselcraft
940da6eb99
Global: Fix annoying Tracy errors and php notices 2023-03-29 01:08:01 +03:00
ayato
7cf4082691
Grammary Grammar Thingies Fix (Now INSIDE OpenVK (wow)) (#857)
ProTip! Great commit summaries contain fewer than 50 characters. Place extra information in the extended description.
2023-03-28 01:21:26 +03:00
veselcraft
eb439b278c
ёбаный в рот а файл забыл добавить 2023-03-13 17:46:54 +03:00
veselcraft
2ffa4fd916
VKAPI: Add force online activity reporting on Newsfeed.get and Messages.send functions (can be disabled trough forGodSakePleaseDoNotReportAboutMyOnlineActivity parameter) 2023-03-13 17:45:45 +03:00
veselcraft
05a423b30b
лее братан эта че за хуйня (оно 500 ошибку вызывало) 2023-03-09 13:49:36 +03:00
veselcraft
a3db17fc36
whoops, i forgor to add localized string 2023-03-03 21:32:56 +03:00
veselcraft
866d6a8c45
Messenger: Add a warning about privacy settings if user to whom he sends a message cannot reply (it annoys me a lot) 2023-03-03 21:30:36 +03:00
veselcraft
543c46696d
VKAPI: 500 error hotfix 2023-03-02 12:09:33 +03:00
Ilya Prokopenko
299e5c6354
Locales: uk hotfix 2023-03-01 20:43:27 +07:00
aqukie
04444c8354
Ukrainian localization: Updates and fixes (#856)
* Translate

* Added myself as a translator & corrected the name

* Видалено зайвий пробіл

* Update uk.strings
2023-02-28 21:07:02 +02:00
ayato
4a5f1f0019
Readme Grammar Thingies Fix (#855)
ржу
2023-02-27 13:03:57 +00:00
celestora
6abdb0d593
Further improvements to TRichText
Fix link parsing + remove zalgo completely
2023-02-25 18:08:17 +02:00
celestora
56fe715bce
Fix ';' error in posts 2023-02-25 18:07:29 +02:00
veselcraft
185c007b50
BSDN: Tweak design 2023-02-24 21:06:34 +03:00
Ilya Prokopenko
699996210d
Require BOTH imagick and gd
Confused? So am I. gd is still used to process a photo, such as when
creating a new wall post.
2023-02-23 21:46:12 +07:00
veselcraft
9d6e81f990
oops 2023-02-21 00:59:52 +03:00
veselcraft
54092eb6c0
Video: Add section for full url to video, also little refactor and fix compatibility for VK4ME 2023-02-21 00:35:10 +03:00
celestora
0da679fae6
Use simpler regex in TRichText::removeZalgo
lmao i forgor character class selectors are a thing
2023-02-17 14:53:45 +02:00
Ilya Prokopenko
668d4f2ada
Require imagick instead of gd 2023-02-11 16:24:15 +07:00
celestora
cee1b4c8c1
Fix another funny xss thing (not tested sinve since im bisexual 💀) 2023-02-10 08:50:21 +02:00
Vladimir Barinov
cf558d57c5
Fix funny xss thing (not tested sinve since im in hospital 💀) 2023-02-10 08:46:29 +02:00
celestora
0f2a88aa68 Add rate limits for API too 2023-02-08 13:20:50 +02:00
celestora
01bd8f938c Disallow API access to banned users
lmao??
2023-02-08 13:14:47 +02:00
celestora
cbec4b549f Minor improvements in video attachments 2023-02-07 22:45:30 +02:00
veselcraft
7ed870c3c6
whoops 2023-02-03 14:23:22 +03:00
veselcraft
c6c8e8e175
Global: add OpenVK Refresh info 2023-02-03 14:22:19 +03:00
veselcraft
06b77ebad8
VKAPI: Add support for attachments for comments 2023-01-31 14:49:12 +03:00
veselcraft
43e18a9173
VKAPI: Add support for Videos 2023-01-31 14:23:34 +03:00
veselcraft
700dcc121d
VKAPI: descuido del desarrollador momento, number tres
Somehow for Friends.getRequests extended parameter was required to use fiels. Now it's not
2023-01-31 02:40:29 +03:00
celestora
aca48e726e 4 2023-01-29 19:16:10 +02:00
celestora
beb8b4eaa6 Move photo page url resolution to model
мб пофиксит 842 янеипу
2023-01-29 19:14:29 +02:00
celestora
5401871b38 name translit: Add global filter to leave CJK characters intact
Fixes #834
2023-01-29 18:57:19 +02:00
celestora
c6a77de234 Cleanup useless copies in processVideo.sh + completely change everything to mp4 2023-01-28 23:22:01 +02:00
celestora
4845f8f318 Fix Video::getThumbnailURL regex meow
Fixes #841
2023-01-28 23:09:05 +02:00
celestora
934bc9b25c Fix some more contextmenu prikoli in bsdnplayer 2023-01-28 23:05:49 +02:00
celestora
344ba53acd
Смешной мясной плеер (#839)
* Add video title under post attachment box

* Add VK-styled player

* bideoplayer on video page now

sosiski

* Fix context menu display on Chrome

* Videos now use H.264 instead of Theora

hazbin hotel nude mo

а вообще тут мог бы быть AV1, но пидорасы из купертино фанаты libx264
2023-01-28 22:25:00 +02:00
dependabot[bot]
5436656aed
Bump ua-parser-js from 0.7.28 to 0.7.33 in /Web/static/js (#838)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-01-28 08:22:04 +00:00
ayato
db433b627f
Locales: English: Fix grammar (#833) 2023-01-28 08:20:09 +00:00
Ilya Prokopenko
4cb8d2d69d
Push changes when any branch is changed 2023-01-28 08:10:26 +00:00
Ilya Prokopenko
6d26269873
Change the secret name for the workflow 2023-01-28 08:05:02 +00:00
celestora
63f302b2ab Use preg_match_all in Video::saveFile
На самом деле там должен был быть и так preg_match_all но видимо какао((
2023-01-28 00:27:16 +02:00
62 changed files with 1150 additions and 134 deletions

View file

@ -1,8 +1,6 @@
name: Codeberg Mirroring
on:
push:
branches: ["master"]
on: push
jobs:
to_codeberg:
@ -14,4 +12,4 @@ jobs:
- uses: pixta-dev/repository-mirroring-action@v1
with:
target_repo_url: "git@codeberg.org:openvk/openvk.git"
ssh_private_key: ${{ secrets.CODEBERG_PRIVSSH }}
ssh_private_key: ${{ secrets.CODEBERG_MIRRORSSH }}

2
.gitignore vendored
View file

@ -14,3 +14,5 @@ themepacks/*
!themepacks/midnight
storage/*
!storage/.gitkeep
.idea

View file

@ -2,29 +2,25 @@
_[Русский](README_RU.md)_
**OpenVK** is an attempt to create a simple CMS that ~~cosplays~~ imitates old VK. Code provided here is not stable yet.
**OpenVK** is an attempt to create a simple CMS that ~~cosplays~~ imitates old VKontakte. Code provided here is not stable yet.
VKontakte belongs to Pavel Durov and VK Group.
To be honest, we don't know whether it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug-tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OVK account for this).
To be honest, we don't know whether if it even works. However, this version is maintained and we will be happy to accept your bugreports [in our bug tracker](https://github.com/openvk/openvk/projects/1). You should also be able to submit them using [ticketing system](https://openvk.su/support?act=new) (you will need an OpenVK account for this).
## When's the release?
We will release OpenVK as soon as it's ready. As for now you can:
We will release OpenVK as soon as it's ready. As for now, you can:
* `git clone` this repo's master branch (use `git pull` to update)
* Grab a prebuilt OpenVK distro from [GitHub artifacts](https://nightly.link/openvk/archive/workflows/nightly/master/OpenVK%20Archive.zip)
## Instances
* **[openvk.su](https://openvk.su/)**
* **[openvk.uk](https://openvk.uk)** - official mirror of openvk.su (<https://t.me/openvk/1609>)
* **[openvk.co](http://openvk.co)** - yet another official mirror of openvk.su without TLS (<https://t.me/openvk/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/)
* [vepurovk.xyz](http://vepurovk.xyz/)
A list of instances can be found in [our wiki of this repository](https://github.com/openvk/openvk/wiki/Instances).
## Can I create my own OpenVK instance?
Yes! And you're very welcome to.
Yes! And you are very welcome to.
However, OVK makes use of Chandler Application Server. This software requires extensions, that may not be provided by your hosting provider (namely, sodium and yaml. these extensions are available on most of ISPManager hostings).
@ -34,12 +30,12 @@ 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, but it was not tested carefully, be aware of that.
* PHP 8.1 is supported too, however it was not tested carefully, so be aware.
2. Install MySQL-compatible database.
* We recommend using Percona Server, but any MySQL-compatible server should work
* Server should be compatible with at least MySQL 5.6, MySQL 8.0+ recommended.
* We recommend using Percona Server, but any MySQL-compatible server should work too.
* Server should be compatible with at least MySQL 5.6, MySQL 8.0+ is recommended.
* Support for MySQL 4.1+ is WIP, replace `utf8mb4` and `utf8mb4_unicode_520_ci` with `utf8` and `utf8_unicode_ci` in SQLs.
3. Install [commitcaptcha](https://github.com/openvk/commitcaptcha) and OpenVK as Chandler extensions like this:
@ -77,20 +73,20 @@ See `install/automated/docker/README.md` and `install/automated/kubernetes/READM
### If my website uses OpenVK, should I release it's sources?
It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you're planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc).
It depends. You can keep the sources to yourself if you do not plan to distribute your website binaries. If your website software must be distributed, it can stay non-OSS provided the OpenVK is not used as a primary application and is not modified. If you modified OpenVK for your needs or your work is based on it and you are planning to redistribute this, then you should license it under terms of any LGPL-compatible license (like OSL, GPL, LGPL etc).
## Where can I get assistance?
You may reach out to us via:
* [Bug-tracker](https://github.com/openvk/openvk/projects/1)
* [Ticketing system](https://openvk.su/support?act=new)
* Telegram chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu.
* [Bug Tracker](https://github.com/openvk/openvk/projects/1)
* [Ticketing System](https://openvk.su/support?act=new)
* Telegram Chat: Go to [our channel](https://t.me/openvkenglish) and open discussion in our channel menu.
* [Reddit](https://www.reddit.com/r/openvk/)
* [Discussions](https://github.com/openvk/openvk/discussions)
* Matrix chat: #openvk:matrix.org
* [GitHub Discussions](https://github.com/openvk/openvk/discussions)
* Matrix Chat: #openvk:matrix.org
**Attention**: bug tracker, board, telegram and matrix chat are public places. And ticketing system is being served by volunteers. If you need to report something, that shouldn't be immediately disclosed to general public (for instance, vulnerability report), please use contact us directly at this email: **openvk [at] tutanota [dot] com**
**Attention**: bug tracker, board, Telegram and Matrix chat are public places, ticketing system is being served by volunteers. If you need to report something that should not be immediately disclosed to general public (for instance, a vulnerability), please contact us directly via this email: **openvk [at] tutanota [dot] com**
<a href="https://codeberg.org/OpenVK/openvk">
<img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60">

View file

@ -2,11 +2,11 @@
_[English](README.md)_
**OpenVK** - это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент представленный здесь исходный код проекта пока не является стабильным.
**OpenVK** это попытка создать простую CMS, которая ~~косплеит~~ имитирует старый ВКонтакте. На данный момент, представленный здесь исходный код проекта пока не является стабильным.
ВКонтакте принадлежит Павлу Дурову и VK Group.
Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OVK).
Честно говоря, мы даже не знаем, работает ли она вообще. Однако, эта версия поддерживается, и мы будем рады принять ваши сообщения об ошибках [в нашем баг-трекере](https://github.com/openvk/openvk/projects/1). Вы также можете отправлять их через [вкладку "Помощь"](https://openvk.su/support?act=new) (для этого вам понадобится учетная запись OpenVK).
## Когда выйдет релизная версия?
@ -16,19 +16,15 @@ _[English](README.md)_
## Инстанции
* **[openvk.su](https://openvk.su/)**
* **[openvk.uk](https://openvk.uk)** - официальное зеркало openvk.su (<https://t.me/openvk/1609>)
* **[openvk.co](http://openvk.co)** - ещё одно официальное зеркало openvk.su без TLS (<https://t.me/openvk/1654>)
* [social.fetbuk.ru](http://social.fetbuk.ru/)
* [vepurovk.xyz](http://vepurovk.xyz/)
Список инстанций находится в [нашей вики этого репозитория](https://github.com/openvk/openvk/wiki/Instances-(RU)).
## Могу ли я создать свою собственную инстанцию OpenVK?
Да! И всегда пожалуйста.
Однако, OVK использует Chandler Application Server. Это программное обеспечение требует расширений, которые могут быть не предоставлены вашим хостинг-провайдером (а именно, sodium и yaml. эти расширения доступны на большинстве хостингов ISPManager).
Однако, OpenVK использует Chandler Application Server. Это программное обеспечение требует расширений, которые могут быть не предоставлены вашим хостинг-провайдером (а именно, sodium и yaml. Эти расширения доступны на большинстве хостингов ISPManager).
Если вы хотите, вы можете добавить вашу инстанцию в список выше, чтобы люди могли зарегистрироваться там.
Если хотите, вы можете добавить вашу инстанцию в список выше, чтобы люди могли зарегистрироваться там.
### Процедура установки
@ -38,7 +34,7 @@ _[English](README.md)_
2. Установите MySQL-совместимую базу данных.
* Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать
* Мы рекомендуем использовать Persona Server, но любая MySQL-совместимая база данных должна работать.
* Сервер должен поддерживать хотя бы MySQL 5.6, рекомендуется использовать MySQL 8.0+.
* Поддержка для MySQL 4.1+ находится в процессе, а пока замените `utf8mb4` и `utf8mb4_unicode_520_ci` на `utf8` и `utf8_unicode_ci` в SQL-файлах, соответственно.
@ -87,10 +83,10 @@ ln -s /path/to/chandler/extensions/available/openvk /path/to/chandler/extensions
* [Помощь в OVK](https://openvk.su/support?act=new)
* Telegram-чат: Перейдите на [наш канал](https://t.me/openvk) и откройте обсуждение в меню нашего канала.
* [Reddit](https://www.reddit.com/r/openvk/)
* [Обсуждения](https://github.com/openvk/openvk/discussions)
* [GitHub Discussions](https://github.com/openvk/openvk/discussions)
* Чат в Matrix: #ovk:matrix.org
**Внимание**: баг-трекер, форум, телеграм- и matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собака] tutanota [точка] com**.
**Внимание**: баг-трекер, форум, Telegram- и Matrix-чат являются публичными местами, и жалобы в OVK обслуживается волонтерами. Если вам нужно сообщить о чем-то, что не должно быть раскрыто широкой публике (например, сообщение об уязвимости), пожалуйста, свяжитесь с нами напрямую по этому адресу: **openvk [собачка] tutanota [точка] com**.
<a href="https://codeberg.org/OpenVK/openvk">
<img alt="Get it on Codeberg" src="https://codeberg.org/Codeberg/GetItOnCodeberg/media/branch/main/get-it-on-blue-on-white.png" height="60">

View file

@ -45,14 +45,12 @@ final class Account extends VKAPIRequestHandler
{
$this->requireUser();
$this->getUser()->setOnline(time());
$this->getUser()->setClient_name($this->getPlatform());
$this->getUser()->save();
$this->getUser()->updOnline($this->getPlatform());
return 1;
}
function setOffline(): object
function setOffline(): int
{
$this->requireUser();
@ -80,6 +78,8 @@ final class Account extends VKAPIRequestHandler
function saveProfileInfo(string $first_name = "", string $last_name = "", string $screen_name = "", int $sex = -1, int $relation = -1, string $bdate = "", int $bdate_visibility = -1, string $home_town = "", string $status = ""): object
{
$this->requireUser();
$this->willExecuteWriteAction();
$user = $this->getUser();
$output = [

View file

@ -66,6 +66,7 @@ final class Friends extends VKAPIRequestHandler
function add(string $user_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$users = new UsersRepo;
$user = $users->get(intval($user_id));
@ -96,6 +97,7 @@ final class Friends extends VKAPIRequestHandler
function delete(string $user_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$users = new UsersRepo;
@ -152,10 +154,7 @@ final class Friends extends VKAPIRequestHandler
$response = $followers;
$usersApi = new Users($this->getUser());
if($extended == 1)
$response = $usersApi->get(implode(',', $followers), $fields, 0, $count);
else
$response = $usersApi->get(implode(',', $followers), "", 0, $count);
$response = $usersApi->get(implode(',', $followers), $fields, 0, $count);
foreach($response as $user)
$user->user_id = $user->id;

View file

@ -237,6 +237,7 @@ final class Groups extends VKAPIRequestHandler
function join(int $group_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$club = (new ClubsRepo)->get($group_id);
@ -251,6 +252,7 @@ final class Groups extends VKAPIRequestHandler
function leave(int $group_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$club = (new ClubsRepo)->get($group_id);

View file

@ -8,6 +8,7 @@ final class Likes extends VKAPIRequestHandler
function add(string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
$this->willExecuteWriteAction();
switch($type) {
case "post":
@ -28,6 +29,7 @@ final class Likes extends VKAPIRequestHandler
function delete(string $type, int $owner_id, int $item_id): object
{
$this->requireUser();
$this->willExecuteWriteAction();
switch($type) {
case "post":

View file

@ -65,10 +65,16 @@ final class Messages extends VKAPIRequestHandler
];
}
function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $chat_id = -1, string $user_ids = "", string $message = "", int $sticker_id = -1)
function send(int $user_id = -1, int $peer_id = -1, string $domain = "", int $chat_id = -1, string $user_ids = "", string $message = "", int $sticker_id = -1, int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0)
{
$this->requireUser();
$this->willExecuteWriteAction();
if($forGodSakePleaseDoNotReportAboutMyOnlineActivity == 0)
{
$this->getUser()->updOnline($this->getPlatform());
}
if($chat_id !== -1)
$this->fail(946, "Chats are not implemented");
else if($sticker_id !== -1)
@ -117,6 +123,7 @@ final class Messages extends VKAPIRequestHandler
function delete(string $message_ids, int $spam = 0, int $delete_for_all = 0): object
{
$this->requireUser();
$this->willExecuteWriteAction();
$msgs = new MSGRepo;
$ids = preg_split("%, ?%", $message_ids);
@ -136,6 +143,7 @@ final class Messages extends VKAPIRequestHandler
function restore(int $message_id): int
{
$this->requireUser();
$this->willExecuteWriteAction();
$msg = (new MSGRepo)->get($message_id);
if(!$msg)

View file

@ -7,9 +7,14 @@ use openvk\VKAPI\Handlers\Wall;
final class Newsfeed extends VKAPIRequestHandler
{
function get(string $fields = "", int $start_from = 0, int $offset = 0, int $count = 30, int $extended = 0)
function get(string $fields = "", int $start_from = 0, int $offset = 0, int $count = 30, int $extended = 0, int $forGodSakePleaseDoNotReportAboutMyOnlineActivity = 0)
{
$this->requireUser();
if($forGodSakePleaseDoNotReportAboutMyOnlineActivity == 0)
{
$this->getUser()->updOnline($this->getPlatform());
}
$id = $this->getUser()->getId();
$subs = DatabaseConnection::i()

View file

@ -66,6 +66,7 @@ final class Polls extends VKAPIRequestHandler
function addVote(int $poll_id, string $answers_ids)
{
$this->requireUser();
$this->willExecuteWriteAction();
$poll = (new PollsRepo)->get($poll_id);
@ -87,6 +88,7 @@ final class Polls extends VKAPIRequestHandler
function deleteVote(int $poll_id)
{
$this->requireUser();
$this->willExecuteWriteAction();
$poll = (new PollsRepo)->get($poll_id);

View file

@ -1,7 +1,9 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\VKAPI\Exceptions\APIErrorException;
use openvk\Web\Models\Entities\IP;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\IPs;
abstract class VKAPIRequestHandler
{
@ -39,4 +41,19 @@ abstract class VKAPIRequestHandler
if(!$this->userAuthorized())
$this->fail(5, "User authorization failed: no access_token passed.");
}
protected function willExecuteWriteAction(): void
{
$ip = (new IPs)->get(CONNECTING_IP);
$res = $ip->rateLimit();
if(!($res === IP::RL_RESET || $res === IP::RL_CANEXEC)) {
if($res === IP::RL_BANNED && OPENVK_ROOT_CONF["openvk"]["preferences"]["security"]["rateLimits"]["autoban"]) {
$this->user->ban("User account has been suspended for breaking API terms of service", false);
$this->fail(18, "User account has been suspended due to repeated violation of API rate limits.");
}
$this->fail(29, "You have been rate limited.");
}
}
}

57
VKAPI/Handlers/Video.php Executable file
View file

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace openvk\VKAPI\Handlers;
use openvk\Web\Models\Entities\User;
use openvk\Web\Models\Repositories\Users as UsersRepo;
use openvk\Web\Models\Entities\Club;
use openvk\Web\Models\Repositories\Clubs as ClubsRepo;
use openvk\Web\Models\Entities\Video as VideoEntity;
use openvk\Web\Models\Repositories\Videos as VideosRepo;
use openvk\Web\Models\Entities\Comment;
use openvk\Web\Models\Repositories\Comments as CommentsRepo;
final class Video extends VKAPIRequestHandler
{
function get(int $owner_id, string $videos, int $offset = 0, int $count = 30, int $extended = 0): object
{
$this->requireUser();
if ($videos) {
$vids = explode(',', $videos);
foreach($vids as $vid)
{
$id = explode("_", $vid);
$items = [];
$video = (new VideosRepo)->getByOwnerAndVID(intval($id[0]), intval($id[1]));
if($video) {
$items[] = $video->getApiStructure();
}
}
return (object) [
"count" => count($items),
"items" => $items
];
} else {
if ($owner_id > 0)
$user = (new UsersRepo)->get($owner_id);
else
$this->fail(1, "Not implemented");
$videos = (new VideosRepo)->getByUser($user, $offset + 1, $count);
$videosCount = (new VideosRepo)->getUserVideosCount($user);
$items = [];
foreach ($videos as $video) {
$items[] = $video->getApiStructure();
}
return (object) [
"count" => $videosCount,
"items" => $items
];
}
}
}

View file

@ -48,6 +48,8 @@ final class Wall extends VKAPIRequestHandler
$attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $this->getUser());
} else if ($attachment instanceof \openvk\Web\Models\Entities\Video) {
$attachments[] = $attachment->getApiStructure();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
@ -216,6 +218,8 @@ final class Wall extends VKAPIRequestHandler
$attachments[] = $this->getApiPhoto($attachment);
} else if($attachment instanceof \openvk\Web\Models\Entities\Poll) {
$attachments[] = $this->getApiPoll($attachment, $user);
} else if ($attachment instanceof \openvk\Web\Models\Entities\Video) {
$attachments[] = $attachment->getApiStructure();
} else if ($attachment instanceof \openvk\Web\Models\Entities\Post) {
$repostAttachments = [];
@ -362,6 +366,7 @@ final class Wall extends VKAPIRequestHandler
function post(string $owner_id, string $message = "", int $from_group = 0, int $signed = 0): object
{
$this->requireUser();
$this->willExecuteWriteAction();
$owner_id = intval($owner_id);
@ -446,6 +451,7 @@ final class Wall extends VKAPIRequestHandler
function repost(string $object, string $message = "") {
$this->requireUser();
$this->willExecuteWriteAction();
$postArray;
if(preg_match('/wall((?:-?)[0-9]+)_([0-9]+)/', $object, $postArray) == 0)
@ -489,6 +495,14 @@ final class Wall extends VKAPIRequestHandler
$oid = $owner->getId();
if($owner instanceof Club)
$oid *= -1;
$attachments = [];
foreach($comment->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment);
}
}
$item = [
"id" => $comment->getId(),
@ -498,6 +512,7 @@ final class Wall extends VKAPIRequestHandler
"post_id" => $post->getVirtualId(),
"owner_id" => $post->isPostedOnBehalfOfGroup() ? $post->getOwner()->getId() * -1 : $post->getOwner()->getId(),
"parents_stack" => [],
"attachments" => $attachments,
"thread" => [
"count" => 0,
"items" => [],
@ -518,6 +533,9 @@ final class Wall extends VKAPIRequestHandler
$items[] = $item;
if($extended == true)
$profiles[] = $comment->getOwner()->getId();
$attachments = null;
// Reset $attachments to not duplicate prikols
}
$response = [
@ -544,6 +562,14 @@ final class Wall extends VKAPIRequestHandler
$profiles = [];
$attachments = [];
foreach($comment->getChildren() as $attachment) {
if($attachment instanceof \openvk\Web\Models\Entities\Photo) {
$attachments[] = $this->getApiPhoto($attachment);
}
}
$item = [
"id" => $comment->getId(),
"from_id" => $comment->getOwner()->getId(),
@ -552,6 +578,7 @@ final class Wall extends VKAPIRequestHandler
"post_id" => $comment->getTarget()->getVirtualId(),
"owner_id" => $comment->getTarget()->isPostedOnBehalfOfGroup() ? $comment->getTarget()->getOwner()->getId() * -1 : $comment->getTarget()->getOwner()->getId(),
"parents_stack" => [],
"attachments" => $attachments,
"likes" => [
"can_like" => 1,
"count" => $comment->getLikesCount(),
@ -582,10 +609,15 @@ final class Wall extends VKAPIRequestHandler
$response['profiles'] = (!empty($profiles) ? (new Users)->get(implode(',', $profiles), $fields) : []);
}
return $response;
}
function createComment(int $owner_id, int $post_id, string $message, int $from_group = 0) {
$this->requireUser();
$this->willExecuteWriteAction();
$post = (new PostsRepo)->getPostById($owner_id, $post_id);
if(!$post || $post->isDeleted()) $this->fail(100, "One of the parameters specified was missing or invalid");
@ -621,6 +653,7 @@ final class Wall extends VKAPIRequestHandler
function deleteComment(int $comment_id) {
$this->requireUser();
$this->willExecuteWriteAction();
$comment = (new CommentsRepo)->get($comment_id);
if(!$comment) $this->fail(100, "One of the parameters specified was missing or invalid");;
@ -640,7 +673,7 @@ final class Wall extends VKAPIRequestHandler
"date" => $attachment->getPublicationTime()->timestamp(),
"id" => $attachment->getVirtualId(),
"owner_id" => $attachment->getOwner()->getId(),
"sizes" => array_values($attachment->getVkApiSizes()),
"sizes" => !is_null($attachment->getVkApiSizes()) ? array_values($attachment->getVkApiSizes()) : NULL,
"text" => "",
"has_tags" => false
]

View file

@ -283,6 +283,14 @@ class Photo extends Media
return [$x, $y];
}
function getPageURL(): string
{
if($this->isAnonymous())
return "/photos/" . base_convert((string) $this->getId(), 10, 32);
return "/photo" . $this->getPrettyId();
}
function getAlbum(): ?Album
{
return (new Albums)->getAlbumByPhotoId($this);

View file

@ -119,7 +119,7 @@ class Post extends Postable
$platform = $this->getRecord()->api_source_name;
if($forAPI) {
switch ($platform) {
case 'openvk_android':
case 'openvk_refresh_android':
case 'openvk_legacy_android':
return 'android';
break;

View file

@ -36,9 +36,9 @@ trait TRichText
"%(([A-z]++):\/\/(\S*?\.\S*?))([\s)\[\]{},\"\'<]|\.\s|$)%",
(function (array $matches): string {
$href = str_replace("#", "&num;", $matches[1]);
$href = rawurlencode(str_replace(";", "&#59;", $matches[1]));
$href = rawurlencode(str_replace(";", "&#59;", $href));
$link = str_replace("#", "&num;", $matches[3]);
$link = str_replace(";", "&#59;", $matches[3]);
$link = str_replace(";", "&#59;", $link);
$rel = $this->isAd() ? "sponsored" : "ugc";
return "<a href='/away.php?to=$href' rel='$rel' target='_blank'>$link</a>" . htmlentities($matches[4]);
@ -49,7 +49,7 @@ trait TRichText
private function removeZalgo(string $text): string
{
return preg_replace("%[\x{0300}-\x{036F}]{3,}%Xu", "<EFBFBD>", $text);
return preg_replace("%\p{M}{3,}%Xu", "", $text);
}
function resolveMentions(array $skipUsers = []): \Traversable

View file

@ -756,7 +756,7 @@ class User extends RowModel
$platform = $this->getRecord()->client_name;
if($forAPI) {
switch ($platform) {
case 'openvk_android':
case 'openvk_refresh_android':
case 'openvk_legacy_android':
return 'android';
break;
@ -1009,6 +1009,15 @@ class User extends RowModel
return true;
}
function updOnline(string $platform): bool
{
$this->setOnline(time());
$this->setClient_name($platform);
$this->save();
return true;
}
function changeEmail(string $email): void
{
DatabaseConnection::i()->getContext()->table("ChandlerUsers")

View file

@ -13,7 +13,7 @@ class Video extends Media
const TYPE_EMBED = 1;
protected $tableName = "videos";
protected $fileExtension = "ogv";
protected $fileExtension = "mp4";
protected $processingPlaceholder = "video/rendering";
@ -30,7 +30,7 @@ class Video extends Media
throw new \DomainException("$filename does not contain any video streams");
$durations = [];
preg_match('%duration=([0-9\.]++)%', $streams, $durations);
preg_match_all('%duration=([0-9\.]++)%', $streams, $durations);
if(sizeof($durations[1]) === 0)
throw new \DomainException("$filename does not contain any meaningful video streams");
@ -104,7 +104,7 @@ class Video extends Media
if(!$this->isProcessed())
return "/assets/packages/static/openvk/video/rendering.apng";
return preg_replace("%\.[A-z]++$%", ".gif", $this->getURL());
return preg_replace("%\.[A-z0-9]++$%", ".gif", $this->getURL());
} else {
return $this->getVideoDriver()->getThumbnailURL();
}
@ -114,6 +114,56 @@ class Video extends Media
{
return $this->getRecord()->owner;
}
function getApiStructure(): object
{
return (object)[
"type" => "video",
"video" => [
"can_comment" => 1,
"can_like" => 0, // we don't h-have wikes in videos
"can_repost" => 0,
"can_subscribe" => 1,
"can_add_to_faves" => 0,
"can_add" => 0,
"comments" => $this->getCommentsCount(),
"date" => $this->getPublicationTime()->timestamp(),
"description" => $this->getDescription(),
"duration" => 0, // я хуй знает как получить длину видео
"image" => [
[
"url" => $this->getThumbnailURL(),
"width" => 320,
"height" => 240,
"with_padding" => 1
]
],
"width" => 640,
"height" => 480,
"id" => $this->getVirtualId(),
"owner_id" => $this->getOwner()->getId(),
"user_id" => $this->getOwner()->getId(),
"title" => $this->getName(),
"is_favorite" => false,
"player" => $this->getURL(),
"files" => [
"mp4_480" => $this->getURL()
],
"added" => 0,
"repeat" => 0,
"type" => "video",
"views" => 0,
"likes" => [
"count" => 0,
"user_likes" => 0
],
"reposts" => [
"count" => 0,
"user_reposted" => 0
]
]
];
}
function setLink(string $link): string
{

View file

@ -14,5 +14,5 @@ abstract class VideoDriver
abstract function getURL(): string;
abstract function getEmbed(): string;
abstract function getEmbed(string $w = "600", string $h = "340"): string;
}

View file

@ -13,12 +13,12 @@ final class YouTubeVideoDriver extends VideoDriver
return "https://youtu.be/$this->id";
}
function getEmbed(): string
function getEmbed(string $w = "600", string $h = "340"): string
{
return <<<CODE
<iframe
width="600"
height="340"
width="$w"
height="$h"
src="https://www.youtube-nocookie.com/embed/$this->id"
frameborder="0"
sandbox="allow-same-origin allow-scripts allow-popups"

View file

@ -13,8 +13,8 @@ Move-Item $file $temp
# video stub logic was implicitly deprecated, so we start processing at once
ffmpeg -i $temp -ss 00:00:01.000 -vframes 1 "$dir$hashT/$hash.gif"
ffmpeg -i $temp -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2
ffmpeg -i $temp -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y $temp2
Move-Item $temp2 "$dir$hashT/$hash.ogv"
Move-Item $temp2 "$dir$hashT/$hash.mp4"
Remove-Item $temp
Remove-Item $temp2

View file

@ -1,14 +1,12 @@
tmpfile="$RANDOM-$(date +%s%N)"
cp $2 "/tmp/vid_$tmpfile.bin"
cp ../files/video/rendering.apng $3${4:0:2}/$4.gif
cp ../files/video/rendering.ogv $3/${4:0:2}/$4.ogv
nice ffmpeg -i "/tmp/vid_$tmpfile.bin" -ss 00:00:01.000 -vframes 1 $3${4:0:2}/$4.gif
nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libtheora -q:v 7 -c:a libvorbis -q:a 4 -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y "/tmp/ffmOi$tmpfile.ogv"
nice -n 20 ffmpeg -i "/tmp/vid_$tmpfile.bin" -c:v libx264 -q:v 7 -c:a libmp3lame -q:a 4 -tune zerolatency -vf "scale=640:480:force_original_aspect_ratio=decrease,pad=640:480:(ow-iw)/2:(oh-ih)/2,setsar=1" -y "/tmp/ffmOi$tmpfile.mp4"
rm -rf $3${4:0:2}/$4.ogv
mv "/tmp/ffmOi$tmpfile.ogv" $3${4:0:2}/$4.ogv
rm -rf $3${4:0:2}/$4.mp4
mv "/tmp/ffmOi$tmpfile.mp4" $3${4:0:2}/$4.mp4
rm -f "/tmp/ffmOi$tmpfile.ogv"
rm -f "/tmp/ffmOi$tmpfile.mp4"
rm -f "/tmp/vid_$tmpfile.bin"

View file

@ -80,7 +80,11 @@ final class AuthPresenter extends OpenVKPresenter
if(!Validator::i()->emailValid($this->postParam("email")))
$this->flashFail("err", tr("invalid_email_address"), tr("invalid_email_address_comment"));
if(OPENVK_ROOT_CONF['openvk']['preferences']['security']['forceStrongPassword'])
if(!Validator::i()->passwordStrong($this->postParam("password")))
$this->flashFail("err", tr("error"), tr("error_weak_password"));
if (strtotime($this->postParam("birthday")) > time())
$this->flashFail("err", tr("invalid_birth_date"), tr("invalid_birth_date_comment"));

View file

@ -57,6 +57,11 @@ final class MessengerPresenter extends OpenVKPresenter
$correspondent = $this->getCorrespondent($sel);
if(!$correspondent)
$this->notFound();
if(!$this->user->identity->getPrivacyPermission('messages.write', $correspondent))
{
$this->flash("err", tr("warning"), tr("user_may_not_reply"));
}
$this->template->selId = $sel;
$this->template->correspondent = $correspondent;

View file

@ -204,6 +204,9 @@ final class VKAPIPresenter extends OpenVKPresenter
}
}
if(!is_null($identity) && $identity->isBanned())
$this->fail(18, "User account is deactivated", $object, $method);
$object = ucfirst(strtolower($object));
$handlerClass = "openvk\\VKAPI\\Handlers\\$object";
if(!class_exists($handlerClass))

View file

@ -218,8 +218,8 @@
<input id="password" type="password" name="password" required />
<input type="hidden" name="jReturnTo" value="{$_SERVER['REQUEST_URI']}" />
<input type="hidden" name="hash" value="{$csrfToken}" />
<input type="submit" value="{_log_in}" class="button" style="display: inline-block;" />
<a href="/reg" class="button" style="display: inline-block;">{_registration}</a><br><br>
<input type="submit" value="{_log_in}" class="button" style="display: inline-block; font-family: Tahoma" />
<a href="/reg"><input type="button" value="{_registration}" class="button" style="font-family: Tahoma" /></a><br><br>
<a href="/restore">{_forgot_password}</a>
</form>
{/ifset}
@ -294,6 +294,7 @@
{script "js/messagebox.js"}
{script "js/notifications.js"}
{script "js/scroll.js"}
{script "js/player.js"}
{script "js/al_wall.js"}
{script "js/al_api.js"}
{script "js/al_mentions.js"}
@ -310,6 +311,8 @@
</script>
{/if}
<script>bsdnHydrate();</script>
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['enable']" async defer data-domain="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['domain']}" src="{php echo OPENVK_ROOT_CONF['openvk']['telemetry']['plausible']['server']}js/plausible.js"></script>
<script n:if="OPENVK_ROOT_CONF['openvk']['telemetry']['piwik']['enable']">

View file

@ -411,7 +411,7 @@
<tr>
<td class="e">
Vladimir Barinov (veselcraft) and Konstantin Kichulkin (kosfurler)<br/>
OpenVK is a free open-source software that "cosplays" (or imitates) older versions of russian website VKontakte. VKontakte belongs to Pavel Durov and VK Group.
OpenVK is a free open source software that "cosplays" (or imitates) older versions of a Russian social network called VKontakte. VKontakte belongs to Pavel Durov and VK Group.
</td>
</tr>
</tbody>
@ -430,7 +430,7 @@
Native name
</td>
<td>
Author
Author(s)
</td>
</tr>
{foreach $languages as $language}

View file

@ -71,7 +71,10 @@
<span class="nobold">{_avatar}: </span>
</td>
<td>
<input type="file" name="ava" accept="image/*" />
<label class="button" style="">{_browse}
<input type="file" id="ava" name="ava" style="display: none;" onchange="filename.innerHTML=ava.files[0].name" />
</label>
<div id="filename" style="margin-top: 10px;"></div>
</td>
</tr>
<tr>

View file

@ -37,9 +37,12 @@
<form method="POST" enctype="multipart/form-data">
<div id="backdropEditor">
<div id="backdropFilePicker">
<input type="file" accept="image/*" name="backdrop1" />
<div id="spacer"></div>
<input type="file" accept="image/*" name="backdrop2" />
<label class="button" style="">{_browse}
<input type="file" accept="image/*" name="backdrop1" style="display: none;">
</label>
<div id="spacer" style="width: 366px;"></div>
<label class="button" style="">{_browse}<input type="file" accept="image/*" name="backdrop2" style="display: none;"></label>
<div id="spacer" style="width: 366px;"></div>
</div>
</div>

View file

@ -21,7 +21,12 @@
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_photo}:</span></td>
<td><input type="file" name="blob" accept="image/*" /></td>
<td>
<label class="button" style="">{_browse}
<input type="file" id="blob" name="blob" style="display: none;" onchange="filename.innerHTML=blob.files[0].name" />
</label>
<div id="filename" style="margin-top: 10px;"></div>
</td>
</tr>
<tr>
<td width="120" valign="top"></td>

View file

@ -20,7 +20,7 @@
<b>Подтверждение номера телефона</b><br/>
Введите код для подтверждения смены номера: <a href="/edit/verify_phone">ввести код</a>.
</div>
<div class="tabs">
<div n:attr="id => ($isMain ? 'activetabs' : 'ki')" class="tab">
<a n:attr="id => ($isMain ? 'act_tab_a' : 'ki')" href="/edit">{_main}</a>
@ -38,10 +38,10 @@
<a n:attr="id => ($isBackDrop ? 'act_tab_a' : 'ki')" href="/edit?act=backdrop">{_backdrop_short}</a>
</div>
</div>
<div class="container_gray">
{if $isMain}
<h4>{_main_information}</h4>
<form action="/edit?act=main" method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
@ -133,7 +133,7 @@
<option value="7" {if $user->getPoliticalViews() == 7}selected{/if}>{_politViews_7}</option>
<option value="8" {if $user->getPoliticalViews() == 8}selected{/if}>{_politViews_8}</option>
<option value="9" {if $user->getPoliticalViews() == 9}selected{/if}>{_politViews_9}</option>
</select>
</td>
</tr>
@ -162,7 +162,7 @@
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
@ -172,9 +172,9 @@
</tbody>
</table>
</form>
{elseif $isContacts}
<h4>{_contact_information}</h4>
<form action="/edit?act=contacts" method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
@ -221,7 +221,7 @@
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
@ -231,9 +231,9 @@
</tbody>
</table>
</form>
{elseif $isInterests}
<h4>{_personal_information}</h4>
<form action="/edit?act=interests" method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
@ -296,7 +296,7 @@
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
@ -306,9 +306,9 @@
</tbody>
</table>
</form>
{elseif $isAvatar}
<h4>{_profile_picture}</h4>
<form action="/al_avatars" method="POST" enctype="multipart/form-data">
<table cellspacing="7" cellpadding="0" width="60%" border="0" align="center">
@ -318,12 +318,15 @@
<span class="nobold">{_picture}: </span>
</td>
<td>
<input type="file" name="blob" accept="image/*" />
<label class="button" style="">{_browse}
<input type="file" id="blob" name="blob" style="display: none;" onchange="filename.innerHTML=blob.files[0].name" />
</label>
<div id="filename" style="margin-top: 10px;"></div>
</td>
</tr>
<tr>
<td>
</td>
<td>
<input type="hidden" name="hash" value="{$csrfToken}" />
@ -333,7 +336,7 @@
</tbody>
</table>
</form>
{elseif $isBackDrop}
<h4>{_backdrop}</h4>
@ -341,9 +344,10 @@
<form method="POST" enctype="multipart/form-data">
<div id="backdropEditor">
<div id="backdropFilePicker">
<input type="file" accept="image/*" name="backdrop1" />
<div id="spacer"></div>
<input type="file" accept="image/*" name="backdrop2" />
<label class="button" style="">Обзор<input type="file" accept="image/*" name="backdrop1" style="display: none;"></label>
<div id="spacer" style="width: 366px;"></div>
<label class="button" style="">Обзор<input type="file" accept="image/*" name="backdrop2" style="display: none;"></label>
<div id="spacer" style="width: 366px;"></div>
</div>
</div>
@ -366,5 +370,5 @@
{/if}
</div>
{/block}

View file

@ -25,7 +25,12 @@
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_video}:</span></td>
<td><input type="file" name="blob" accept="video/*" /></td>
<td>
<label class="button" style="">{_browse}
<input type="file" id="blob" name="blob" style="display: none;" onchange="filename.innerHTML=blob.files[0].name" accept="video/*" />
</label>
<div id="filename" style="margin-top: 10px;"></div>
</td>
</tr>
<tr>
<td width="120" valign="top"><span class="nobold">{_video_link_to_yt}:</span></td>

View file

@ -13,7 +13,9 @@
{block content}
<center style="margin-bottom: 8pt;">
{if $video->getType() === 0}
<video width="610" src="{$video->getURL()}" controls></video>
<div class="bsdn" data-name="{$video->getName()}" data-author="{$user->getCanonicalName()}">
<video width="610" src="{$video->getURL()}"></video>
</div>
{else}
{var $driver = $video->getVideoDriver()}
{if !$driver}

View file

@ -5,6 +5,7 @@
{if $theme !== NULL}
{if $theme->inheritDefault()}
{css "css/bsdn.css"}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
@ -20,6 +21,7 @@
<link rel="stylesheet" href="/themepack/{$theme->getId()}/{$theme->getVersion()}/resource/xmas.css" />
{/if}
{else}
{css "css/bsdn.css"}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/notifications.css"}
@ -41,6 +43,7 @@
{css "css/microblog.css"}
{/if}
{else}
{css "css/bsdn.css"}
{css "css/style.css"}
{css "css/dialog.css"}
{css "css/nsfw-posts.css"}
@ -49,4 +52,4 @@
{if $isXmas}
{css "css/xmas.css"}
{/if}
{/ifset}
{/ifset}

View file

@ -10,7 +10,23 @@
</a>
{/if}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Video}
<video class="media" src="{$attachment->getURL()}" controls="controls"></video>
{if $attachment->getType() === 0}
<div class="bsdn media" data-name="{$attachment->getName()}" data-author="{$attachment->getOwner()->getCanonicalName()}">
<video class="media" src="{$attachment->getURL()}"></video>
</div>
{else}
{var $driver = $attachment->getVideoDriver()}
{if !$driver}
<span style="color:red;">{_version_incompatibility}</span>
{else}
{$driver->getEmbed("100%")|noescape}
{/if}
{/if}
<div class="video-wowzer">
<img src="/assets/packages/static/openvk/img/videoico.png" />
<a href="/video{$attachment->getPrettyId()}">{$attachment->getName()}</a>
</div>
{elseif $attachment instanceof \openvk\Web\Models\Entities\Poll}
{presenter "openvk!Poll->view", $attachment->getId()}
{elseif $attachment instanceof \openvk\Web\Models\Entities\Post}

View file

@ -1,4 +1,4 @@
{var $user = $notification->getModel(0)}
{var $post = $notification->getModel(1)}
{_nt_you_were_mentioned_u} <a href="{$user->getURL()}"><b>{$user->getCanonicalName()}</b></a> {$notification->getDateTime()} <a href="/photo{$post->getURL()}"><b>{_nt_mention_in_photo}</b></a>: "{$notification->getData()}"
{_nt_you_were_mentioned_u} <a href="{$user->getURL()}"><b>{$user->getCanonicalName()}</b></a> {$notification->getDateTime()} <a href="/photo{$post->getPageURL()}"><b>{_nt_mention_in_photo}</b></a>: "{$notification->getData()}"

View file

@ -8,6 +8,8 @@
{else}
{var $deac = "post_deact_silent"}
{/if}
{var $compact = isset($compact) ? true : false}
{var $club = isset($club) ? $club}
{var $commentTextAreaId = $post === NULL ? rand(1,300) : $post->getId()}
@ -16,7 +18,7 @@
<tr>
<td width="54" valign="top">
<a href="{$author->getURL()}">
<img src="{$author->getAvatarURL('miniscule')}" width="{ifset $compact}25{else}50{/ifset}" {ifset $compact}class="cCompactAvatars"{/ifset} />
<img src="{$author->getAvatarURL('miniscule')}" width="{if $compact}25{else}50{/if}" {if $compact}class="cCompactAvatars"{/if} />
<span n:if="!$post->isPostedOnBehalfOfGroup() && !$compact && $author->isOnline()" class="post-online">{_online}</span>
</a>
</td>
@ -40,18 +42,18 @@
</a>
{/if}
{ifset $compact}
{if $compact}
<br>
<a href="/wall{$post->getPrettyId()}" class="date">
{$post->getPublicationTime()}
</a>
{/ifset}
{/if}
<span n:if="$post->isPinned()" class="nobold">{_pinned}</span>
<a n:if="$post->canBeDeletedBy($thisUser) && !($forceNoDeleteLink ?? false) && !isset($compact)" class="delete" href="/wall{$post->getPrettyId()}/delete"></a>
<a n:if="$post->canBeDeletedBy($thisUser) && !($forceNoDeleteLink ?? false) && $compact == false" class="delete" href="/wall{$post->getPrettyId()}/delete"></a>
{if $post->canBePinnedBy($thisUser) && !($forceNoPinLink ?? false) && !isset($compact)}
{if $post->canBePinnedBy($thisUser) && !($forceNoPinLink ?? false) && $compact == false}
{if $post->isPinned()}
<a class="pin" href="/wall{$post->getPrettyId()}/pin?act=unpin&hash={rawurlencode($csrfToken)}"></a>
{else}
@ -94,7 +96,7 @@
</span>
</div>
</div>
<div class="post-menu" n:if="!isset($compact)">
<div class="post-menu" n:if="$compact == false">
<a href="/wall{$post->getPrettyId()}" class="date">{$post->getPublicationTime()}</a>
<a n:if="!empty($platform)" class="client_app" data-app-tag="{$platform}" data-app-name="{$platformDetails['name']}" data-app-url="{$platformDetails['url']}" data-app-img="{$platformDetails['img']}">
<img src="/assets/packages/static/openvk/img/app_icons_mini/{$post->getPlatform(this)}.svg">

View file

@ -22,5 +22,9 @@ class Validator
return (bool) preg_match("/^(?:t.me\/|@)?([a-zA-Z0-9_]{0,32})$/", $telegram);
}
function passwordStrong(string $password): bool{
return (bool) preg_match("/^(?=.*[A-Z])(?=.*[0-9])(?=.*[a-z]).{8,}$/", $password);
}
use TSimpleSingleton;
}

Binary file not shown.

220
Web/static/css/bsdn.css Normal file
View file

@ -0,0 +1,220 @@
.bsdn-player {
width: 100%;
position: relative;
display: inline-flex;
font-family: Tahoma, sans-serif;
user-select: none;
background-color: black;
}
.bsdn-player > .bsdn_controls, .bsdn-player > .bsdn_teaserWrap, .bsdn-player > .bsdn_contextMenu {
position: absolute;
}
.bsdn_controls {
display: flex;
width: 100%;
bottom: 0;
background-color: hsl(0deg 0% 0% / 59%);
border-top: 1px solid hsl(0deg 0% 100% / 70%);
padding: 7px 12px;
color: #fff;
align-items: center;
box-sizing: border-box;
gap: 6px;
z-index: 2;
opacity: 0;
}
.bsdn-player.bsdn-dirty > .bsdn_controls {
opacity: 1;
}
.bsdn_video {
width: 100%;
display: inline-flex;
justify-content: center;
}
.bsdn_terebilkaUpperWrap {
display: flex;
justify-content: space-between;
align-items: center;
}
.bsdn_terebilkaWrap {
flex-grow: 1;
display: flex;
flex-direction: column;
}
.bsdn_logo {
height: 8px;
}
p.bsdn_timeWrap {
display: inline;
margin: 0;
font-size: 7pt;
font-weight: bold;
}
.bsdn_terebilkaLowerWrap, .bsdn_soundControlSubWrap {
position: relative;
height: 6px;
border-top: 1px solid white;
}
.bsdn_terebilkaBrick, .bsdn_soundControlBrick {
position: absolute;
height: 100%;
width: 15px;
background-color: #fff;
pointer-events: none;
}
.bsdn_soundIcon {
cursor: pointer;
}
.bsdn_soundControl {
width: 64px;
}
.bsdn_soundControlBrick {
width: 10px;
}
.bsdn_soundControlPadding {
height: 8px;
}
button.bsdn_playButton {
appearance: none;
background: none;
border: none;
color: #fff;
padding: 0 8px;
padding-left: 0;
font-size: 22px;
cursor: pointer;
}
.bsdn_fullScreenButton {
cursor: pointer;
}
.bsdn_fullScreenButton > img:hover {
background: url("/assets/packages/static/openvk/img/bsdn/fullscreen_hover.gif");
object-fit: none;
object-position: -64px 0;
}
.bsdn_teaserWrap {
width: 100%;
height: 100%;
z-index: 1;
color: #fff;
display: flex;
justify-content: center;
align-items: center;
background-color: hsl(0deg 0% 0% / 10%);
}
.bsdn_teaser {
display: flex;
padding: 10px 20px;
width: 266px;
background-color: hsl(0deg 0% 14.17% / 74.12%);
border-radius: 10px;
align-items: stretch;
justify-content: space-between;
box-sizing: border-box;
}
.bsdn_teaserTitleBox {
display: flex;
flex-direction: column;
justify-content: space-around;
}
.bsdn_teaserButton {
cursor: pointer;
}
time.bsdn_timeFull {
margin-left: 10px;
color: #b1b1b1;
}
.bsdn-player._bsdn_playing .bsdn_controls {
opacity: 0;
pointer-events: none;
transition: 1s opacity ease-in-out;
transition-delay: 2s;
}
.bsdn-player._bsdn_playing:hover .bsdn_controls {
opacity: 1;
pointer-events: unset;
transition: .2s opacity ease-in-out;
transition-delay: 0;
}
.bsdn_video > video {
height: 100%;
max-width: 100%;
}
.bsdn_contextMenu {
z-index: 3;
background-color: #f4f4f4;
padding: 6px;
border: 1px solid #908f90;
width: 232px;
height: 169px;
font-size: 15px;
box-sizing: border-box;
}
.bsdn_contextMenuElement {
display: block;
color: #666294;
cursor: pointer;
padding: 3px 0 3px 20px;
}
.bsdn_contextMenu hr {
margin: 5px 0;
border-color: #878f8e;
border-bottom: none;
}
.bsdn_contextMenuElement:hover, .bsdn_contextMenuElement:active {
color: #fff;
}
.bsdn_contextMenuElement:hover {
background-color: #9797c8;
}
.bsdn_contextMenuElement:active {
background-color: #5a5a8f;
}
.bsdn_teaserWrap span {
color: #fff;
}
.bsdn_teaserButton > img {
max-height: 50px;
margin: 10px 0;
}
.bsdn_fullScreenButton > img, .bsdn_soundIcon {
vertical-align: middle;
}

View file

@ -231,6 +231,7 @@ h1 {
position: absolute;
top: 140px;
padding: 0 19px;
margin: 24px;
}
#backdropFilePicker > input {
@ -861,6 +862,8 @@ span {
.content_list .cl_element {
width: 33%;
display: inline-block;
text-align: center;
}
.content_list.long .cl_element {
@ -881,6 +884,7 @@ span {
.content_list .cl_element .cl_name .cl_lname {
font-size: 7pt;
display: block;
}
.ava {
@ -2381,7 +2385,7 @@ a.poll-retract-vote {
}
.regform-left{
text-align: right;
text-align: right;
min-width: 110px;
}
@ -2403,5 +2407,23 @@ a.poll-retract-vote {
}
.tour div {
font-size: 11px; color:#000;
font-size: 11px; color:#000;
}
.video-wowzer > img {
vertical-align: bottom;
}
.video-wowzer {
font-weight: bolder;
font-size: 12px;
padding: 3px 0;
}
.video-wowzer a::before {
content: "b";
color: transparent;
width: 12px;
background-image: url(/assets/packages/static/openvk/img/videoico.png);
display: none;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 248 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 B

BIN
Web/static/img/videoico.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 130 B

View file

@ -428,6 +428,18 @@ function showIncreaseRatingDialog(coinsCount, userUrl, hash) {
};
}
function escapeHtml(text) {
var map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
}
$(document).on("scroll", () => {
if($(document).scrollTop() > $(".sidebar").height() + 50) {
$(".floating_sidebar")[0].classList.add("show");

336
Web/static/js/player.js Normal file
View file

@ -0,0 +1,336 @@
function _bsdnUnwrapBitMask(number) {
return number.toString(2).split("").reverse().map(x => x === "1");
}
function _bsdnToHumanTime(time) {
time = Math.ceil(time);
let mins = Math.floor(time / 60);
let secs = (time - (mins * 60));
if(secs < 10)
secs = "0" + secs;
if(mins < 10)
mins = "0" + mins;
return mins + ":" + secs;
}
function _bsdnTpl(name, author) {
name = escapeHtml(name);
author = escapeHtml(author);
return `
<div class="bsdn_contextMenu" style="display: none;">
<span class="bsdn_contextMenuElement bsdn_copyVideoUrl">Copy video link to clipboard</span>
<hr/>
<span class="bsdn_contextMenuElement">OpenVK BSDN///Player 0.1</span>
<hr/>
<span class="bsdn_contextMenuElement">Developers:</span>
<span class="bsdn_contextMenuElement" onclick="window.location.assign('https://github.com/celestora');">
- celstora
</span>
<span class="bsdn_contextMenuElement" onclick="window.location.assign('https://github.com/openvk/openvk/issues');">
- Report a problem...
</span>
<hr/>
<span class="bsdn_contextMenuElement" onclick="confirm('эм это шутка');">About Adobe Flash Player...</span>
</div>
<div class="bsdn_controls">
<div>
<button class="bsdn_playButton">
<img src="/assets/packages/static/openvk/img/bsdn/play.png" style="padding-right: 2px; padding-top: 3px;">
</button>
</div>
<div class="bsdn_terebilkaWrap">
<div class="bsdn_terebilkaUpperWrap">
<img class="bsdn_logo" src="/assets/packages/static/openvk/img/bsdn/logo.gif" style="opacity: 0; /* TODO add logo xdd */" />
<p class="bsdn_timeWrap">
<time class="bsdn_timeReal">--:--</time>
<time class="bsdn_timeFull">--:--</time>
</p>
</div>
<div class="bsdn_terebilkaLowerWrap">
<div class="bsdn_terebilkaBrick"></div>
</div>
</div>
<div>
<img class="bsdn_soundIcon" src="/assets/packages/static/openvk/img/bsdn/speaker.gif" />
</div>
<div class="bsdn_soundControl">
<div class="bsdn_soundControlPadding"></div>
<div class="bsdn_soundControlSubWrap">
<div class="bsdn_soundControlBrick" style="left: calc(100% - 10px);"></div>
</div>
</div>
<div>
<div class="bsdn_fullScreenButton">
<img src="/assets/packages/static/openvk/img/bsdn/fullscreen.gif" />
</div>
</div>
</div>
<div class="bsdn_teaserWrap">
<div class="bsdn_teaser">
<div class="bsdn_teaserTitleBox">
<b>${name}</b>
<span>${author}</span>
</div>
<div class="bsdn_teaserButton">
<img src="/assets/packages/static/openvk/img/bsdn/play.png" />
</div>
</div>
</div>
`;
}
function _bsdnTerebilkaEventFactory(el, terebilka, callback, otherListeners) {
let terebilkaSize = () => el.querySelector(terebilka).getBoundingClientRect().width; // чтобы просралось
let listeners = {
mousemove: [
e => {
let buttonsPresseed = _bsdnUnwrapBitMask(e.buttons);
if(!buttonsPresseed[0])
return; // user doesn't click so nothing should be done
let offset = e.offsetX;
let percents = Math.max(0, Math.min(100, offset / (terebilkaSize() / 100)));
return callback(percents);
}
],
mousedown: [
e => {
let offset = e.offsetX;
let percents = Math.max(0, Math.min(100, offset / (terebilkaSize() / 100)));
return callback(percents);
}
]
};
for(eventName in (otherListeners || {})) {
if(listeners.hasOwnProperty(eventName))
listeners[eventName] = otherListeners[eventName].concat(listeners[eventName]);
else
listeners[eventName] = otherListeners[eventName];
}
return listeners;
}
function _bsdnEventListenerFactory(el, v) {
return {
".bsdn-player": {
click: [
e => {
if(el.querySelector(".bsdn_controls").contains(e.target) || el.querySelector(".bsdn_teaser").contains(e.target) || el.querySelector(".bsdn_contextMenu").contains(e.target))
return;
if(el.querySelector(".bsdn_contextMenu").style.display !== "none") {
el.querySelector(".bsdn_contextMenu").style.display = "none";
return;
}
if(v.paused)
v.play();
else
v.pause();
}
],
contextmenu: [
e => {
e.preventDefault();
if(el.querySelector(".bsdn_controls").contains(e.target) || el.querySelector(".bsdn_contextMenu").contains(e.target))
return;
let rect = el.querySelector(".bsdn-player").getBoundingClientRect();
let h = rect.height, w = rect.width;
let x, y;
if(document.fullscreen) {
x = e.screenX;
y = e.screenY;
} else {
let rx = rect.x + window.scrollX, ry = rect.y + window.scrollY;
x = e.pageX - rx;
y = e.pageY - ry;
}
if(h - y < 169)
y = Math.max(0, y - 169);
if(w - x < 238)
x = Math.max(0, x - 238);
let menu = el.querySelector(".bsdn_contextMenu");
menu.style.top = y + "px";
menu.style.left = x + "px";
menu.style.display = "unset";
}
]
},
".bsdn_contextMenuElement": {
click: [ () => el.querySelector(".bsdn_contextMenu").style.display = "none" ]
},
".bsdn_video > video": {
play: [
() => {
if(!el.querySelector(".bsdn-player").classList.contains("bsdn-dirty"))
el.querySelector(".bsdn-player").classList.add("bsdn-dirty")
el.querySelector(".bsdn_playButton").innerHTML = "<img src='/assets/packages/static/openvk/img/bsdn/pause.gif' style='padding-right: 3px; padding-top: 3px;' />";
el.querySelector(".bsdn-player").classList.add("_bsdn_playing");
el.querySelector(".bsdn_teaserWrap").style.display = "none";
}
],
pause: [
() => {
el.querySelector(".bsdn_playButton").innerHTML = "<img src='/assets/packages/static/openvk/img/bsdn/play.png' style='padding-right: 2px; padding-top: 3px; height: 19px;' />";
el.querySelector(".bsdn-player").classList.remove("_bsdn_playing");
el.querySelector(".bsdn_teaserWrap").style.display = "flex";
}
],
timeupdate: [
() => {
el.querySelector(".bsdn_timeReal").innerHTML = _bsdnToHumanTime(v.currentTime);
let terebilkaSize = el.querySelector(".bsdn_terebilkaLowerWrap").getBoundingClientRect().width;
let brickSize = 15;
let percents = Math.ceil(v.currentTime / (v.duration / 100));
let offset = ((terebilkaSize - brickSize) / 100) * percents;
el.querySelector(".bsdn_terebilkaBrick").style.left = `min(calc(100% - 15px), ${offset}px`; // смешной мясной костыль ибо мне лень делать onresize
}
],
volumechange: [
() => {
if(v.volume === 0)
el.querySelector(".bsdn_soundIcon").src = "/assets/packages/static/openvk/img/bsdn/speaker_muted.gif";
else
el.querySelector(".bsdn_soundIcon").src = "/assets/packages/static/openvk/img/bsdn/speaker.gif";
let scSize = el.querySelector(".bsdn_soundControlSubWrap").getBoundingClientRect().width;
let brickSize = 10;
let offset = (scSize - brickSize) * v.volume;
el.querySelector(".bsdn_soundControlBrick").style.left = offset + "px";
}
],
loadedmetadata: [
() => {
el.querySelector(".bsdn_timeFull").innerHTML = _bsdnToHumanTime(v.duration);
}
]
},
".bsdn_fullScreenButton": {
click: [
() => {
if(document.fullscreen) {
document.exitFullscreen();
} else {
el.querySelector(".bsdn-player").requestFullscreen();
}
}
]
},
".bsdn_teaserButton|.bsdn_playButton": {
click: [
() => {
if(v.paused)
v.play();
else
v.pause();
}
]
},
".bsdn_terebilkaLowerWrap": _bsdnTerebilkaEventFactory(el, ".bsdn_terebilkaLowerWrap", function(p) {
let time = (v.duration / 100) * p;
setTimeout(() => {
v.currentTime = time;
if(v.currentTime === 0) {
console.warn("[!] Хромог момент");
console.warn("Теребилка не работает в хроме если сервер не реализует HTTP полностью.");
console.warn("Встроенный сервер РНР не возвращает заголовки Accept-Range из-за чего хром отказывается seek'ать. Google как всегда.");
console.warn("Установите Firefox для лучшей безопасности в сети: https://www.mozilla.org/ru/firefox/enterprise/#download");
}
}, 0);
}, {
mousedown: [
e => v.pause()
],
mouseup: [
e => v.play()
]
}),
".bsdn_soundControlSubWrap": _bsdnTerebilkaEventFactory(el, ".bsdn_soundControlSubWrap", function(p) {
let volume = p / 100;
v.volume = volume;
}),
".bsdn_soundIcon": {
click: [
e => v.volume = v.volume === 0 ? 0.75 : 0
]
}
}
}
function _bsdnApplyBindings(el, v) {
let listeners = _bsdnEventListenerFactory(el, v);
for(key in listeners) {
let selectors = key.split("|");
selectors.forEach(sel => {
for(eventName in listeners[key]) {
listeners[key][eventName].forEach(listener => {
el.querySelectorAll(sel).forEach(target => {
target.addEventListener(eventName, listener, {
passive: (["contextmenu"]).indexOf(eventName) === -1
});
});
});
}
});
}
}
function bsdnInitElement(el) {
if(el.querySelector(".bdsn-hydrated") != null) {
console.debug(el, " is already hydrated.");
return;
}
let video = el.querySelector("video");
if(!video) {
console.warning(el, " does not contain any <video>s.");
return;
}
el.innerHTML = `
<div class="bsdn-player bdsn-hydrated">
${_bsdnTpl(el.dataset.name, el.dataset.author)}
<div class="bsdn_video">
${video.outerHTML}
</div>
</div>
`;
video = el.querySelector(".bsdn_video > video");
_bsdnApplyBindings(el, video);
video.volume = 0.75;
}
function bsdnHydrate() {
document.querySelectorAll(".bsdn").forEach(bsdnInitElement);
}

View file

@ -321,9 +321,9 @@ trim-extra-html-whitespace@1.3.0:
integrity sha1-tH77DRpfKlaoXMRc6lJWUek0BM8=
ua-parser-js@^0.7.18:
version "0.7.28"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31"
integrity sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==
version "0.7.33"
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
integrity sha512-s8ax/CeZdK9R/56Sui0WM6y9OFREJarMRHqLB2EwkovemBxNQ+Bqu8GAsUnVcXKgphb++ghr/B2BZx4mahujPw==
uglify-js@^3.1.4:
version "3.17.3"

View file

@ -18,6 +18,7 @@ function _ovk_check_environment(): void
$requiredExtensions = [
"gd",
"imagick",
"fileinfo",
"PDO",
"pdo_mysql",

View file

@ -2,4 +2,5 @@
<Clients name="standard">
<Client tag="vk4me" name="VK4ME" url="http://vk4me.crx.moe/" img="/assets/packages/static/openvk/img/app_icons/vk4me.png" />
<Client tag="openvk_legacy_android" name="OpenVK Legacy" url="https://f-droid.org/packages/uk.openvk.android.legacy/" img="/assets/packages/static/openvk/img/app_icons/openvk_legacy.png" />
<Client tag="openvk_refresh_android" name="OpenVK Refresh" url="https://github.com/openvk/mobile-android-refresh" img="/assets/packages/static/openvk/img/app_icons/openvk_refresh.png" />
</Clients>

View file

@ -1,5 +1,5 @@
"__locale" = "en_US.UTF-8;Eng";
"__transNames" = "Russian-Latin/BGN; Any-Latin";
"__transNames" = "[\P{script=Han}]; Russian-Latin/BGN; Any-Latin";
/* Check for https://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html */
@ -66,7 +66,7 @@
"birth_date" = "Birth date";
"registration_date" = "Registration date";
"hometown" = "Hometown";
"this_is_you" = "that is You";
"this_is_you" = "it's you";
"edit_page" = "Edit page";
"edit_group" = "Edit group";
"change_status" = "change status";
@ -546,6 +546,7 @@
"backdrop_succ" = "Backdrop settings saved";
"backdrop_succ_rem" = "Backdrop images have been removed";
"backdrop_succ_desc" = "Users will start seeing changes in 5 minutes.";
"browse" = "Browse";
/* Two-factor authentication */
@ -620,15 +621,15 @@
"notifications_post" = "$1 published $2a post$3 on your wall: $4";
"notifications_appoint" = "$1 appointed you as community manager $2";
"nt_liked_yours" = "liked yours";
"nt_shared_yours" = "shared yours";
"nt_liked_yours" = "liked your";
"nt_shared_yours" = "shared your";
"nt_commented_yours" = "commented";
"nt_written_on_your_wall" = "wrote on your wall";
"nt_made_you_admin" = "appointed you in the community";
"nt_from" = "from";
"nt_yours_adjective" = "yours";
"nt_yours_feminitive_adjective" = "yours";
"nt_yours_adjective" = "your";
"nt_yours_feminitive_adjective" = "your";
"nt_post_nominative" = "post";
"nt_post_instrumental" = "post";
"nt_note_instrumental" = "note";
@ -992,6 +993,7 @@
"error_upload_failed" = "Failed to upload a photo";
"error_old_password" = "Old password does not match";
"error_new_password" = "New password does not match";
"error_weak_password" = "Password isn't strong enough. It should has at least 8 symbols, at least one capital letter and at least one digit.";
"error_shorturl_incorrect" = "The short address has an incorrect format.";
"error_repost_fail" = "Failed to share post";
"error_data_too_big" = "Attribute '$1' must be at most $2 $3 long";
@ -1197,6 +1199,7 @@
/* User alerts */
"user_alert_scam" = "This account has been reported a lot for scam. Please be careful, especially if he asked for money.";
"user_may_not_reply" = "This user may not reply to you because of your privacy settings. <a href='/settings?act=privacy'>Open privacy settings</a>";
/* Cookies pop-up */
@ -1416,4 +1419,4 @@
"tour_section_14_bottom_text_1" = "Screenshots";
"tour_section_14_bottom_text_2" = "This concludes the tour of the site. If you want to try our mobile app, create your own group here, invite your friends or find new ones, or just have some fun in general, you can do it right now with a small <a href='/reg'>registration</a>";
"tour_section_14_bottom_text_3" = "This concludes the tour of the site";
"tour_section_14_bottom_text_3" = "This concludes the tour of the site";

View file

@ -556,6 +556,7 @@
"end_all_sessions_description" = "Եթե ցանկանում եք դուրս գալ $1ից ամեն դեվայսից, սեղմե՛ք ներքևի կոճակը";
"end_all_sessions_done" = "Բոլոր սեսսիաները նետված են, ներառյալ բջջային հավելվածները";
"browse" = "Վերանայում";
/* Two-factor authentication */

View file

@ -12,8 +12,8 @@ list:
- code: "uk"
flag: "ua"
name: "Ukrainian"
native_name: "Україньска"
author: "Andrej Lenťaj, Maxim Hrabovi (dechioyo) and Kirill (mbsoft)"
native_name: "Українcька"
author: "Aqukie (yaroslav.bielograd@ukr.net), Andrej Lenťaj, Maxim Hrabovi (dechioyo) and Kirill (mbsoft)"
- code: "by"
flag: "by"
name: "Belarussian"

View file

@ -506,6 +506,7 @@
"backdrop_succ" = "Фон сохранён";
"backdrop_succ_rem" = "Фон удалён";
"backdrop_succ_desc" = "Изменения будут заметны другим пользователям через 5 минут.";
"browse" = "Обзор";
/* Two-factor authentication */
@ -905,6 +906,7 @@
"error_upload_failed" = "Не удалось загрузить фото";
"error_old_password" = "Старый пароль не совпадает";
"error_new_password" = "Новые пароли не совпадает";
"error_weak_password" = "Ненадёжный пароль. Пароль должен содержать не менее 8 символов, цифры, прописные и строчные буквы";
"error_shorturl_incorrect" = "Короткий адрес имеет некорректный формат.";
"error_repost_fail" = "Не удалось поделиться записью";
"error_data_too_big" = "Аттрибут '$1' не может быть длиннее $2 $3";
@ -1085,6 +1087,7 @@
/* User alerts */
"user_alert_scam" = "На этот аккаунт много жаловались в связи с мошенничеством. Пожалуйста, будьте осторожны, особенно если у вас попросят денег.";
"user_may_not_reply" = "Этот пользователь, возможно, вам не сможет ответить из-за ваших настроек приватности. <a href='/settings?act=privacy'>Открыть настройки приватности</a>";
/* Cookies pop-up */
@ -1306,4 +1309,4 @@
"tour_section_14_bottom_text_1" = "Скриншоты приложения";
"tour_section_14_bottom_text_2" = "На этом экскурсия по сайту завершена. Если вы хотите попробовать наше мобильное приложение, создать здесь свою группу, позвать своих друзей или найти новых, или вообще просто как-нибудь поразвлекаться, то это можно сделать прямо сейчас, пройдя небольшую <a href='/reg'>регистрацию</a>";
"tour_section_14_bottom_text_3" = "На этом экскурсия по сайту завершена."
"tour_section_14_bottom_text_3" = "На этом экскурсия по сайту завершена."

View file

@ -498,6 +498,20 @@
"end_all_sessions_description" = "Якщо ви хочете вийти з $1 на всіх пристроях, натисніть кнопку нижче";
"end_all_sessions_done" = "<b>Усі</b> сеанси було завершено";
"backdrop_short" = "Фон";
"backdrop" = "Фон профілю";
"backdrop_desc" = "Ви можете встановити два зображення як фон вашої сторінки. Вони будуть відображатися з обох боків у тих, хто зайде на вашу сторінку. За допомогою цієї можливості ви можете додати своєму профілю більше краси.";
"backdrop_warn" = "Зображення будуть розташовані так, як на схемі вище. Їхня висота буде автоматично збільшена, щоб вони займали 100% висоти екрана, посередині буде розмиття";
"backdrop_about_adding" = "За потребою, ви можете встановити тільки 1 зображення (але буде негарно), а також замінити тільки одне: якщо у вас уже стоїть два, а ви хочете замінити друге — то завантажуйте тільки друге, перше збережеться. Щоб видалити треба натиснути на відповідну кнопку внизу, видаляти по одній не можна.";
"backdrop_save" = "Зберегти фон";
"backdrop_remove" = "Видалити фон";
"backdrop_error_title" = "Не вдалося зберегти фон";
"backdrop_error_no_media" = "Зображення пошкоджені(-но) або завантажені не повністю";
"backdrop_succ" = "Фон збережено";
"backdrop_succ_rem" = "Фон видалено";
"backdrop_succ_desc" = "Зміни будуть помітні іншим користувачам через 5 хвилин.";
"browse" = "Огляд";
/* Two-factor authentication */
"two_factor_authentication" = "Двофакторна автентифікація";
@ -578,6 +592,14 @@
"nt_photo_instrumental" = "фотографією";
"nt_topic_instrumental" = "темою";
"nt_you_were_mentioned_u" = "Вас згадав користувач";
"nt_you_were_mentioned_g" = "Вас згадала група";
"nt_mention_in_post_or_comms" = "у пості або під коментарями поста";
"nt_mention_in_photo" = "в обговоренні фотографії";
"nt_mention_in_video" = "в обговоренні відеозапису";
"nt_mention_in_note" = "в обговоренні під";
"nt_mention_in_topic" = "в обговоренні";
/* Time */
"time_at_sp" = " в ";
@ -731,6 +753,8 @@
"app_err_note" = "Не вдалося прикріпити замітку новин";
"app_err_note_desc" = "Переконайтеся, що посилання правильне і нотатка належить вам.";
"learn_more" = "Детальніше";
/* Support */
"support_opened" = "Відкриті";
@ -1116,28 +1140,28 @@
"create_poll" = "Нове опитування";
"poll_title" = "Тема опитування";
"poll_add_option" = "Додати новий варіант відповіді...";
"poll_anonymous" = "Анонiмне опитування";
"poll_anonymous" = "Анонімне опитування";
"poll_multiple" = "Множинний вибір";
"poll_locked" = "Заборонити відміняти свій голос";
"poll_locked" = "Заборонити скасовувати свій голос";
"poll_edit_expires" = "Опитування закінчується через: ";
"poll_edit_expires_days" = "днів";
"poll_editor_tips" = "Натискання Backspace у пустому варианті приведе до його видалення. Tab/Enter у останньому додає новий.";
"poll_editor_tips" = "Натискання Backspace у пустому варіанті приведе до його видалення. Tab/Enter у останньому додає новий.";
"poll_embed" = "Отримати код";
"poll_voter_count_zero" = "Станьте <b>першим</b>, хто проголосує!";
"poll_voter_count_one" = "У опитуванні проголосовала <b>одна</b> людина.";
"poll_voter_count_few" = "У опитуванні проголосувало <b>$1</b> людини.";
"poll_voter_count_many" = "У опитуванні проголосувало <b>$1</b> людей.";
"poll_voter_count_other" = "У опитуванні проголосувало <b>$1</b> людей.";
"poll_voters_list" = "Список проголосувавших";
"poll_voter_count_one" = "В опитуванні проголосувала <b>одна</b> людина.";
"poll_voter_count_few" = "В опитуванні проголосувало <b>$1</b> людини.";
"poll_voter_count_many" = "В опитуванні проголосувало <b>$1</b> людей.";
"poll_voter_count_other" = "В опитуванні проголосувало <b>$1</b> людей.";
"poll_voters_list" = "Список голосувальників";
"poll_anon" = "Анонімне опитування";
"poll_public" = "Публічне опитування";
"poll_multi" = "багато вариантів";
"poll_multi" = "багато варіантів";
"poll_lock" = "не можна переголосувати";
"poll_until" = "до $1";
"poll_err_to_much_options" = "Занадто багато варіантів в опитуванні.";
"poll_err_anonymous" = "Неможливо переглянути список тих, хто проголосував в анонімному голосуванні.";
"cast_vote" = "Проголосувати!";
"retract_vote" = "Відмінити голос";
"retract_vote" = "Скасовувати голос";
"attach_video" = "Прикріпити відео";
"friends_list_other" = "Ви маєте $1 друга";
"round_avatars" = "Круглі";
@ -1153,6 +1177,154 @@
"helpdesk_showing_name" = "Зображуване ім'я";
"helpdesk_avatar_url" = "Посилання на аватар";
"email_rate_limit_error" = "Не можна робити це так часто, вибачте.";
"gifts_left_zero" = "Залишилося нуль подарунків";
"gifts_left_zero" = "Залишилось 0 подарунків";
"helpdesk_all_answers" = "всі відповіді";
"helpdesk_show_number" = "Показувати номер";
/* Tutorial */
"tour_title" = "Екскурс по сайту";
"reg_title" = "Реєстрація";
"ifnotlike_title" = " &quot;А якщо мені не сподобається сайт?&quot; ";
"tour_promo" = "Про те, що Вас чекає після реєстрації";
"reg_text" = "<a href='/reg'>Реєстрація</a> акаунту абсолютно безоплатна та дуже легка";
"ifnotlike_text" = "Ви завжди маєте змогу видалити свій акаунт";
"tour_next" = "Далі →";
"tour_reg" = "Реєстрація →";
"tour_section_1" = "Початок";
"tour_section_2" = "Профіль";
"tour_section_3" = "Фотографії";
"tour_section_4" = "Пошук";
"tour_section_5" = "Відеозаписи";
"tour_section_6" = "Аудіозаписи";
"tour_section_7" = "Новинна стрічка";
"tour_section_8" = "Глобальна стрічка";
"tour_section_9" = "Групи";
"tour_section_10" = "Події";
"tour_section_11" = "Теми та дизайн";
"tour_section_12" = "Стилістика";
"tour_section_13" = "Ваучери";
"tour_section_14" = "Мобільна версія";
"tour_section_1_title_1" = "З чого почати?";
"tour_section_1_text_1" = "Реєстрація аккаунту є першим та фундаментальним етапом для початку вашого використання на цьому сайті";
"tour_section_1_text_2" = "Для реєстрації нам знадобиться ім'я, E-Mail та пароль.";
"tour_section_1_text_3" = "<b>Пам'ятайте:</b> Ваш E-mail буде використовуватися як логін для входу на сайт. Також ви можете не зазначати прізвище під час реєстрації. У разі втрати пароля для входу на сайт, скористайтеся розділом <a href='/restore'>відновлення</a>";
"tour_section_1_bottom_text_1" = "Реєструючись на сайті, ви погоджуєтеся з <a href='/about'>правилами сайту</a> та <a href='/privacy'>політикою конфіденційності</a>";
"tour_section_2_title_1" = "Ваш профіль";
"tour_section_2_text_1_1" = "Після реєстрації на сайті, ви автоматично потрапите у <b>свій</b> профіль";
"tour_section_2_text_1_2" = "Ви зможете редагувати його де завгодно та в будь-який час, коли Ви самі цього бажаєте.";
"tour_section_2_text_1_3" = "<b>Порада:</b> Щоб ваш профіль мав гарний і поважний вигляд, ви можете його заповнити будь-якою інформацією або завантажити фотографію, яка підкреслить, наприклад, ваш глибокий внутрішній світ.";
"tour_section_2_bottom_text_1" = "Ви єдиний, хто вирішує, скільки інформації ваші друзі мають дізнатися про вас.";
"tour_section_2_title_2" = "Задайте свої налаштування своєї приватності";
"tour_section_2_text_2_1" = "Ви можете визначити, хто саме може мати доступ до певних типів інформації, розділам та можливостям зв'язатися з вами.";
"tour_section_2_text_2_2" = "Ви маєте змогу закрити доступ до своєї сторінки від пошукових систем і незареєстрованих користувачів.";
"tour_section_2_text_2_3" = "<b>Пам'ятайте:</b> у майбутньому налаштування приватності будуть розширюватися.";
"tour_section_2_title_3" = "Персональна адреса сторінки";
"tour_section_2_text_3_1" = "Після реєстрації сторінки, вам видається персональний ID типу: <b>/id12345</b>";
"tour_section_2_text_3_2" = "<b>Стандартний ID</b>, який був отриманий після реєстрації, <b>змінити не можна!</b>";
"tour_section_2_text_3_3" = "Однак, в налаштуваннях своєї сторінки ви зможете прив'язати свою персональну адресу і цю адресу <b>можна буде змінити</b> в будь-який час";
"tour_section_2_text_3_4" = "<b>Порада:</b> Можна займати будь-яку вільну адресу, довжина якої не менша за 5 символів.";
"tour_section_2_bottom_text_2" = "<i>Підтримується встановлення будь-якої короткої адреси з латинських маленьких літер; адреса може містити цифри (однак, не на початку), крапки та нижні підкреслення (не на початку або наприкінці)</i>";
"tour_section_2_title_4" = "Стіна";
"tour_section_3_title_1" = "Діліться своїми фотографіями";
"tour_section_3_text_1" = "Розділ &quot;Фотографії&quot; доступний у вашому профілі одразу з моменту реєстрації.";
"tour_section_3_text_2" = "Ви можете переглядати фотоальбоми користувачів та створювати свої власні фотоальбоми.";
"tour_section_3_text_3" = "Доступ до всіх ваших фотоальбомів для інших користувачів редагується в налаштуваннях приватності.";
"tour_section_3_bottom_text_1" = "Ви можете створювати необмежену кількість фотоальбомів з ваших подорожей або будь-яких подій, або просто зберігати фото кошенят";
"tour_section_4_title_1" = "Пошук";
"tour_section_4_text_1" = "Розділ &quot;Пошук&quot; має на меті шукати користувачів та групи.";
"tour_section_4_text_2" = "Даний розділ сайту з часом буде покращуватися та збільшуватися.";
"tour_section_4_text_3" = "Для початку пошуку, треба знати ім'я (чи прізвище); а якщо шукаєте групу, то потрібно знати її назву.";
"tour_section_4_title_2" = "Швидкий пошук";
"tour_section_4_text_4" = "Якщо Ви бажаєте будь-яким чином зберегти власний час, то поле пошуку доступна і в шапці сайту";
"tour_section_5_title_1" = "Завантажуйте та діліться відео зі своїми друзями!";
"tour_section_5_text_1" = "Ви можете завантажувати необмежену кількість відеозаписів і кліпів";
"tour_section_5_text_2" = "Розділ &quot;Відеозаписи&quot; регулюється налаштуваннями приватності";
"tour_section_5_bottom_text_1" = "Відео можна завантажувати, обходячи розділ &quot;Відеозаписи&quot; через звичайне прикріплення до нового допису на стіні:";
"tour_section_5_title_2" = "Імпорт відео з YouTube";
"tour_section_5_text_3" = "Окрім завантаження відео напряму, сайт підтримує і вбудовані відео через YouTube";
"tour_section_6_title_1" = "Аудіозаписи";
"tour_section_6_text_1" = "!!! АУДІОЗАПИСІВ НЕМАЄ, ЧЕКАЙТЕ ІНФОРМАЦІЇ ВІД ГЕН.ШТАБУ !!!";
"tour_section_7_title_1" = "Слідкуйте за тим, що пишуть ваші друзі";
"tour_section_7_text_1" = "Розділ &quot;Мої Новини&quot; поділяється на два типи: локальна стрічка та глобальна стрічка новин";
"tour_section_7_text_2" = "У локальній стрічці будуть показуватися новини тільки ваших друзів і груп";
"tour_section_7_bottom_text_1" = "Ніякої системи рекомендацій. <b>Свою стрічку новин формуєте тільки ви.</b>";
"tour_section_8_title_1" = "Слідкуйте за тим, які теми обговорюють на сайті";
"tour_section_8_text_1" = "У глобальній стрічці будуть показуватися дописи всіх користувачів сайту та груп";
"tour_section_8_text_2" = "Перегляд цього розділу може не рекомендуватися для чутливих і вразливих людей";
"tour_section_8_bottom_text_1" = "Дизайн глобальної стрічки за дизайном ніяк не відрізняється від локальної";
"tour_section_8_bottom_text_2" = "У стрічці є безліч типів контенту: починаючи від звичайних фото і відео, і закінчуючи анонімними постами й опитуваннями";
"tour_section_9_text_1" = "На сайті безліч груп, присвячені різним темам";
"tour_section_9_text_2" = "Ви можете приєднатися до будь-якої групи. А якщо не знайшли відповідну, то можна створити власну";
"tour_section_9_text_3" = "Кожна група може мати в собі свій розділ вікі-сторінок, фотоальбомів, блок посилань та обговорень";
"tour_section_9_title_2" = "Ви можете керувати своєю групою разом із другом";
"tour_section_9_text_2_1" = "Керування групою здійснюється в розділі &quot;Редагувати групу&quot; під аватаром спільноти";
"tour_section_9_text_2_2" = "Створіть команду адміністраторів зі звичайних учасників або тих, кому ви довіряєте";
"tour_section_9_text_2_3" = "Ви можете приховати потрібного Вам адміністратора, щоб він не показувався в межах вашої групи";
"tour_section_9_bottom_text_1" = "Розділ &quot;Мої Групи&quot; знаходиться в лівому меню сайту";
"tour_section_9_bottom_text_2" = "Приклад спільноти";
"tour_section_9_bottom_text_3" = "Групи часто являють собою реальні організації, члени яких хочуть залишатися на зв'язку зі своєю аудиторією";
"tour_section_10_title_1" = "Овва!";
"tour_section_10_text_1" = "Цей розділ поки що не написаний. Пропустіть його.";
"tour_section_11_title_1" = "Теми оформлення";
"tour_section_11_text_1" = "Після реєстрації, як стандартна тема, у вас буде класична тема оформлення";
"tour_section_11_text_2" = "Деяких нових користувачів може налякати чинна класична тема, яка виглядає дуже архаїчно";
"tour_section_11_text_3" = "<b>Але не біда:</b> Ви можете обрати іншу тему з каталогу, або створити свою, ознаомившись з <a href='https://docs.openvk.uk/'>документацією</a>";
"tour_section_11_bottom_text_1" = "Каталог тем доступний в розділі &quot;Мої Налаштування&quot; в вкладці &quot;Інтерфейс&quot; ";
"tour_section_11_wordart" = "<img src='https://openvk.uk/assets/packages/static/openvk/img/tour/wordart.png' width='65%'>";
"tour_section_12_title_1" = "Фон профілю та групи";
"tour_section_12_text_1" = "Ви можете встановити одну чи два зображення як фон вашої сторінки";
"tour_section_12_text_2" = "Вони відображатимуться по боках у тих, хто зайде на вашу сторінку";
"tour_section_12_text_3" = "<b>Порада:</b> перед встановленням фону, поекспериментуйте з розміткою: спробуйте віддзеркалити майбутню фонову картинку, або взагалі просто створіть гарний градієнт";
"tour_section_12_title_2" = "Аватари";
"tour_section_12_text_2_1" = "Ви можете задати варіант показу аватара користувача: стандартне, заокруглені та квадратні (1:1)";
"tour_section_12_text_2_2" = "Ці налаштування буде видно тільки вам";
"tour_section_12_title_3" = "Редагування правого меню";
"tour_section_12_text_3_1" = "За потреби, ви можете приховати непотрібні розділи сайту";
"tour_section_12_text_3_2" = "<b>Нагадування: </b>Розділи першої необхідності (Моя Сторінка; Мої Друзі; Мої Відповіді; Мої Налаштування) приховати не можна";
"tour_section_12_title_4" = "Вид постів";
"tour_section_12_text_4_1" = "Якщо набрид старий дизайн стіни, який був у колись популярному оригінальному ВКонтактє.сру, то ви завжди можете змінити вигляд постів на Мікроблог";
"tour_section_12_text_4_2" = "Вид постів можна змінювати між двома варіантами в будь-який час";
"tour_section_12_text_4_3" = "<b>Зверніть увагу</b>, що якщо обрано старий вид відображення постів, то останні коментарі довантажуватися не будуть";
"tour_section_12_bottom_text_1" = "Сторінка встановлення фону";
"tour_section_12_bottom_text_2" = "Приклади сторінок зі встановленим фоном";
"tour_section_12_bottom_text_3" = "За допомогою цієї можливості ви можете додати своєму профілю більше індивідуальності";
"tour_section_12_bottom_text_4" = "Старий вигляд постів";
"tour_section_12_bottom_text_5" = "Мікроблок";
"tour_section_13_title_1" = "Ваучер";
"tour_section_13_text_1" = "Ваучер в OpenVK це щось на кшталт промокоду на додавання будь-якої валюти (відсотки рейтингу, голосів тощо)";
"tour_section_13_text_2" = "Подібні купони створюються за будь-якими значущими подіями та святами. Слідкуйте за <a href='https://t.me/openvk'>Telegram каналом</a> OpenVK";
"tour_section_13_text_3" = "Після активації будь-якого ваучера, задана адміністраторами валюта буде перечислена в вашу користь";
"tour_section_13_text_4" = "<b>Пам'ятайте: </b>Усі ваучери мають обмежений термін активації";
"tour_section_13_bottom_text_1" = "Ваучери складаються з 24 цифр та літер";
"tour_section_13_bottom_text_2" = "Активація пройшла вдало (наприклад, нам зарахували 100 голосів)";
"tour_section_13_bottom_text_3" = "<b>Увага: </b>Після активації ваучера на вашу сторінку, той самий ваучер не можна буде активувати повторно";
"tour_section_14_title_1" = "Мобільна версія";
"tour_section_14_text_1" = "Наразі мобільної вебверсії сайту поки що немає, проте є мобільний додаток для Android";
"tour_section_14_text_2" = "OpenVK Legacy - це мобільний додаток OpenVK для пристроїв на базі Android із дизайном ВКонтакте 3.0.4 2013 року";
"tour_section_14_text_3" = "Мінімально підтримуваною версією є Android 2.1 Eclair, тобто апарати часів початку 2010-их стануть у пригоді.";
"tour_section_14_title_2" = "Де це можна завантажити?";
"tour_section_14_text_2_1" = "Релізні версії завантажуються через офіційний репозиторій F-Droid";
"tour_section_14_text_2_2" = "Якщо ви є бета-тестувальником програми, то нові версії програми викладаються в окремий канал оновлення";
"tour_section_14_text_2_3" = "<b>Важливо: </b>Додаток може мати різні помилки та недоліки, про помилки повідомляйте в <a href='/app'>офіційну групу додатка</a>";
"tour_section_14_bottom_text_1" = "Скріншоти застосунку";
"tour_section_14_bottom_text_2" = "На цьому екскурсія сайтом завершена. Якщо ви хочете спробувати наш мобільний застосунок, створити тут свою групу, покликати своїх друзів чи знайти нових, або взагалі просто якось розважитися, то це можна зробити просто зараз, пройшовши невелику <a href='/reg'>реєстрацію</a>";
"tour_section_14_bottom_text_3" = "На цьому екскурсія сайтом завершена.";

View file

@ -27,6 +27,7 @@ openvk:
requirePhone: false
forcePhoneVerification: false
forceEmailVerification: false
forceStrongPassword: false
enableSu: true
rateLimits:
actions: 5