code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 * @link      http://www.gnu.org/software/social/
 */

defined('GNUSOCIAL') || die();

/**
 * ActivityPub's own Postman
 *
 * Standard workflow expects that we send an Explorer to find out destinataries'
 * inbox address. Then we send our postman to deliver whatever we want to send them.
 *
 * @category  Plugin
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
class Activitypub_postman
{
    private $actor;
    private $actor_uri;
    private $to = [];
    private $client;
    private $headers;

    /**
     * Create a postman to deliver something to someone
     *
     * @param Profile $from sender Profile
     * @param array $to receiver Profiles
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function __construct(Profile $from, array $to = [])
    {
        $this->actor = $from;
        $this->to = $to;

        $this->actor_uri = $this->actor->getUri();

        $this->client = new HTTPClient();
    }

    /**
     * Send something to remote instance
     *
     * @param string $data request body
     * @param string $inbox url of remote inbox
     * @param string $method request method
     * @return GNUsocial_HTTPResponse
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function send($data, $inbox, $method = 'POST')
    {
        common_debug('ActivityPub Postman: Delivering '.$data.' to '.$inbox);

        $headers = HttpSignature::sign($this->actor, $inbox, $data);

        common_debug('ActivityPub Postman: Delivery headers were: '.print_r($headers, true));

        $this->client->setBody($data);

        switch ($method) {
            case 'POST':
                $response = $this->client->post($inbox, $headers);
                break;
            case 'GET':
                $response = $this->client->get($inbox, $headers);
                break;
            default:
                throw new Exception("Unsupported request method for postman.");
        }

        common_debug('ActivityPub Postman: Delivery result with status code '.$response->getStatus().': '.$response->getBody());
        return $response;
    }

    /**
     * Send a follow notification to remote instance
     *
     * @return bool
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function follow()
    {
        $data = Activitypub_follow::follow_to_array($this->actor_uri, $this->to[0]->getUrl());
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody(), true);

        if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
            $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
            $pending_list->add();
            return true;
        } elseif (isset($res_body['error'])) {
            throw new Exception($res_body['error']);
        }

        throw new Exception("An unknown error occurred.");
    }

    /**
     * Send a Undo Follow notification to remote instance
     *
     * @return bool
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @throws Exception
     * @throws Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function undo_follow()
    {
        $data = Activitypub_undo::undo_to_array(
            Activitypub_follow::follow_to_array(
                $this->actor_uri,
                $this->to[0]->getUrl()
                    )
                );
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody(), true);

        if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
            Activitypub_profile::unsubscribeCacheUpdate($this->actor, $this->to[0]->local_profile());
            $pending_list = new Activitypub_pending_follow_requests($this->actor->getID(), $this->to[0]->getID());
            $pending_list->remove();
            return true;
        }
        if (isset($res_body['error'])) {
            throw new Exception($res_body['error']);
        }
        throw new Exception("An unknown error occurred.");
    }

    /**
     * Send a Accept Follow notification to remote instance
     *
     * @param string $id Follow activity id
     * @return bool
     * @throws HTTP_Request2_Exception
     * @throws Exception Description of HTTP Response error or generic error message.
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function accept_follow(string $id): bool
    {
        $data = Activitypub_accept::accept_to_array(
            Activitypub_follow::follow_to_array(
                $this->to[0]->getUrl(),
                $this->actor_uri,
                $id
                )
            );
        $res = $this->send(json_encode($data, JSON_UNESCAPED_SLASHES), $this->to[0]->get_inbox());
        $res_body = json_decode($res->getBody(), true);

        if ($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409) {
            $pending_list = new Activitypub_pending_follow_requests($this->to[0]->getID(), $this->actor->getID());
            $pending_list->remove();
            return true;
        }
        if (isset($res_body['error'])) {
            throw new Exception($res_body['error']);
        }
        throw new Exception("An unknown error occurred.");
    }

    /**
     * Send a Like notification to remote instances holding the notice
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function like($notice)
    {
        $data = Activitypub_like::like_to_array(
            $this->actor_uri,
            Activitypub_notice::getUrl($notice)
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox() as $inbox) {
            $res = $this->send($data, $inbox);

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the like activity!");
        }
    }

    /**
     * Send a Undo Like notification to remote instances holding the notice
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function undo_like($notice)
    {
        $data = Activitypub_undo::undo_to_array(
            Activitypub_like::like_to_array(
                $this->actor_uri,
                Activitypub_notice::getUrl($notice)
                         )
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox() as $inbox) {
            $res = $this->send($data, $inbox);

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the undo-like activity!");
        }
    }

    /**
     * Send a Create notification to remote instances
     *
     * @param Notice $notice
     * @throws EmptyPkeyValueException
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws ServerException
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function create_note($notice)
    {
        $data = Activitypub_create::create_to_array(
            $this->actor_uri,
            Activitypub_notice::notice_to_array($notice)
                );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox() as $inbox) {
            $res = $this->send($data, $inbox);

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
        }
    }

    /**
     * Send a Create direct-notification to remote instances
     *
     * @param Notice $message
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public function create_direct_note(Notice $message)
    {
        $data = Activitypub_create::create_to_array(
            $this->actor_uri,
            Activitypub_message::message_to_array($message),
            true
        );
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox(false) as $inbox) {
            $res = $this->send($data, $inbox);

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the create-note activity!");
        }
    }

    /**
     * Send a Announce notification to remote instances
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function announce($notice)
    {
        $data = json_encode(Activitypub_announce::announce_to_array($this->actor, $notice),
                            JSON_UNESCAPED_SLASHES);

        foreach ($this->to_inbox() as $inbox) {
            $res = $this->send($data, $inbox);

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the announce activity!");
        }
    }

    /**
     * Send a Delete notification to remote instances holding the notice
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    public function delete_note($notice)
    {
        $data = Activitypub_delete::delete_to_array(
            $notice->getProfile()->getUri(),
            Activitypub_notice::getUrl($notice)
                );
        $errors = [];
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);
        foreach ($this->to_inbox() as $inbox) {
            $res = $this->send($data, $inbox);
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }
        if (!empty($errors)) {
            throw new Exception(json_encode($errors));
        }
    }

    /**
     * Send a Delete notification to remote followers of some deleted profile
     *
     * @param Notice $notice
     * @throws HTTP_Request2_Exception
     * @throws InvalidUrlException
     * @throws Exception
     * @author Bruno Casteleiro <brunoccast@fc.up.pt>
     */
    public function delete_profile()
    {
        $data = Activitypub_delete::delete_to_array($this->actor_uri, $this->actor_uri);
        $data = json_encode($data, JSON_UNESCAPED_SLASHES);

        $errors = [];
        foreach ($this->to_inbox() as $inbox) {
            $res = $this->send($data, $inbox);

            // accummulate errors for later use, if needed
            if (!($res->getStatus() == 200 || $res->getStatus() == 202 || $res->getStatus() == 409)) {
                $res_body = json_decode($res->getBody(), true);
                $errors[] = isset($res_body['error']) ?
                          $res_body['error'] : "An unknown error occurred.";
            }
        }

        if (!empty($errors)) {
            common_log(LOG_ERR, sizeof($errors) . " instance/s failed to handle the delete_profile activity!");
        }
    }

    /**
     * Clean list of inboxes to deliver messages
     *
     * @param bool $actorFollowers whether to include the actor's follower collection
     * @return array To Inbox URLs
     * @throws Exception
     * @author Diogo Cordeiro <diogo@fc.up.pt>
     */
    private function to_inbox(bool $actorFollowers = true): array
    {
        if ($actorFollowers) {
            $discovery = new Activitypub_explorer();
            $followers = apActorFollowersAction::generate_followers($this->actor, 0, null);
            foreach ($followers as $sub) {
                try {
                    $this->to[]= Activitypub_profile::from_profile($discovery->lookup($sub)[0]);
                } catch (Exception $e) {
                    // Not an ActivityPub Remote Follower, let it go
                }
            }
            unset($discovery);
        }

        $to_inboxes = [];
        foreach ($this->to as $to_profile) {
            $i = $to_profile->get_inbox();
            // Prevent delivering to self
            if ($i == common_local_url('apInbox')) {
                continue;
            }
            $to_inboxes[] = $i;
        }

        return array_unique($to_inboxes);
    }
}


code:
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2020-08-04 01:05+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Transfer-Encoding: 8bit\n"

#. TRANS: Exception.
#: classes/Activitypub_profile.php:393
msgid "Not a valid WebFinger address (via cache)."
msgstr ""

#. TRANS: Exception.
#: classes/Activitypub_profile.php:414
msgid "Not a valid WebFinger address."
msgstr ""

#. TRANS: Exception. %s is a WebFinger address.
#: classes/Activitypub_profile.php:454
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr ""

#. TRANS: Plugin description.
#: ActivityPubPlugin.php:241
msgid ""
"Follow people across social networks that implement ActivityPub."
msgstr ""

#: ActivityPubPlugin.php:415
msgid "Remote Profile"
msgstr ""

#. TRANS: Title. %s is a domain name.
#: ActivityPubPlugin.php:920
#, php-format
msgid "Sent from %s via ActivityPub"
msgstr ""

repo_name: ksuudilla/gnu

license: mit


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
# Diogo Cordeiro <diogo@fc.up.pt>, 2019
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-12 01:23+0100\n"
"PO-Revision-Date: 2019-06-12 01:23+0100\n"
"Last-Translator: Diogo Cordeiro <diogo@fc.up.pt>\n"
"Language-Team: English (United Kingdom) (http://www.transifex.com/gnu-social/gnu-social/language/en_GB/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_GB\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: ActivityPubPlugin.php:212
msgid ""
"Follow people across social networks that implement ActivityPub."
msgstr ""
"Follow people across social networks that implement ActivityPub."

#: ActivityPubPlugin.php:254
msgid "Remote Profile"
msgstr "Remote Profile"

#. TRANS: Title. %s is a domain name.
#: ActivityPubPlugin.php:909
#, php-format
msgid "Sent from %s via ActivityPub"
msgstr "Sent from %s via ActivityPub"

#. TRANS: Exception.
#: classes/Activitypub_profile.php:355
msgid "Not a valid webfinger address (via cache)."
msgstr "Not a valid WebFinger address (via cache)."

#. TRANS: Exception.
#: classes/Activitypub_profile.php:376
msgid "Not a valid webfinger address."
msgstr "Not a valid WebFinger address."

#. TRANS: Exception. %s is a webfinger address.
#: classes/Activitypub_profile.php:416
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr "Could not find a valid profile for \"%s\"."


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
# Diogo Cordeiro <diogo@fc.up.pt>, 2019
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-12 01:23+0100\n"
"PO-Revision-Date: 2019-06-12 01:23+0100\n"
"Last-Translator: Diogo Cordeiro <diogo@fc.up.pt>\n"
"Language-Team: Portuguese (http://www.transifex.com/gnu-social/gnu-social/language/pt/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: ActivityPubPlugin.php:212
msgid ""
"Follow people across social networks that implement ActivityPub."
msgstr ""
"Segue pessoas em redes sociais que implementam ActivityPub."

#: ActivityPubPlugin.php:254
msgid "Remote Profile"
msgstr "Perfil Remoto"

#. TRANS: Title. %s is a domain name.
#: ActivityPubPlugin.php:909
#, php-format
msgid "Sent from %s via ActivityPub"
msgstr "Enviado por %s via ActivityPub"

#. TRANS: Exception.
#: classes/Activitypub_profile.php:355
msgid "Not a valid webfinger address (via cache)."
msgstr "Endereço WebFinger inválido (via cache)."

#. TRANS: Exception.
#: classes/Activitypub_profile.php:376
msgid "Not a valid webfinger address."
msgstr "Endereço WebFinger inválido."

#. TRANS: Exception. %s is a webfinger address.
#: classes/Activitypub_profile.php:416
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr "Não foi possível encontrar um perfil válido para \"%s\"."


code:
# Translation file for GNU social - the free software social networking platform
# Copyright (C) 2015 - 2019 Free Software Foundation, Inc http://www.fsf.org
# This file is under https://www.gnu.org/licenses/agpl v3 or later
#
# Translators:
# Diogo Cordeiro <diogo@fc.up.pt>, 2019
msgid ""
msgstr ""
"Project-Id-Version: GNU social\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2019-06-12 01:23+0100\n"
"PO-Revision-Date: 2019-06-12 02:24+0100\n"
"Last-Translator: Diogo Cordeiro <diogo@fc.up.pt>\n"
"Language-Team: Portuguese (Brazil) (http://www.transifex.com/gnu-social/gnu-social/language/pt_BR/)\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: pt_BR\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#. TRANS: Plugin description.
#: ActivityPubPlugin.php:212
msgid ""
"Follow people across social networks that implement ActivityPub."
msgstr ""
"Segue pessoas em redes sociais que implementam ActivityPub."

#: ActivityPubPlugin.php:254
msgid "Remote Profile"
msgstr "Perfil Remoto"

#. TRANS: Title. %s is a domain name.
#: ActivityPubPlugin.php:909
#, php-format
msgid "Sent from %s via ActivityPub"
msgstr "Enviado por %s via ActivityPub"

#. TRANS: Exception.
#: classes/Activitypub_profile.php:355
msgid "Not a valid webfinger address (via cache)."
msgstr "Endereço WebFinger inválido (via cache)."

#. TRANS: Exception.
#: classes/Activitypub_profile.php:376
msgid "Not a valid webfinger address."
msgstr "Endereço WebFinger inválido."

#. TRANS: Exception. %s is a webfinger address.
#: classes/Activitypub_profile.php:416
#, php-format
msgid "Could not find a valid profile for \"%s\"."
msgstr "Não foi possível encontrar um perfil válido para \"%s\"."


code:
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
         backupStaticAttributes="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         convertErrorsToExceptions="true"
         convertNoticesToExceptions="true"
         convertWarningsToExceptions="true"
         processIsolation="false"
         stopOnFailure="false">
    <testsuites>
        <testsuite name="Feature">
            <directory suffix="Test.php">./tests/Feature</directory>
        </testsuite>

        <testsuite name="Unit">
            <directory suffix="Test.php">./tests/Unit</directory>
        </testsuite>
    </testsuites>
    <filter>
        <whitelist processUncoveredFilesFromWhitelist="true">
            <directory suffix=".php">./app</directory>
        </whitelist>
    </filter>
</phpunit>


code:
#!/usr/bin/env php
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

define('INSTALLDIR', dirname(__DIR__, 3));
define('PUBLICDIR', INSTALLDIR . DIRECTORY_SEPARATOR . 'public');

$shortoptions = 'i:af';
$longoptions = ['id=', 'all', 'force'];

$helptext = <<<END_OF_HELP
fix_subsriptions.php [options]
For every ActivityPub subscription, re-send Follow activity.

    -i --id Local user id whose follows shall be re-sent
    -a --all update all

END_OF_HELP;

require_once INSTALLDIR.'/scripts/commandline.inc';

if (have_option('i', 'id')) {
    $id = get_option_value('i', 'id');
    $user = User::getByID($id);
    fix_subscriptions($user->getProfile());
    exit(0);
} elseif (!have_option('a', 'all')) {
    show_help();
    exit(1);
}

$user = new User();
$cnt = $user->find();
while ($user->fetch()) {
    fix_subscriptions($user->getProfile());
}
$user->free();
unset($user);

printfnq("Done.\n");

function fix_subscriptions(Profile $profile)
{
    // Collect every remote AP subscription
    $aprofiles = [];
    $subs = Subscription::getSubscribedIDs($profile->getID(), 0, null);
    $subs_aprofiles = Activitypub_profile::multiGet('profile_id', $subs);
    foreach ($subs_aprofiles->fetchAll() as $ap) {
        $aprofiles[$ap->getID()] = $ap;
    }
    unset($subs_aprofiles);
    // For each remote AP subscription, send a Follow activity
    foreach ($aprofiles as $sub) {
        try {
            $postman = new Activitypub_postman($profile, [$sub]);
            $postman->follow();
            printfnq(
                'Ensured subscription between ' . $profile->getBestName()
                . ' and ' . $sub->getUri() . "\n"
            );
        } catch (Exception $e) {
            // Let it go
            printfnq('Failed to ensure subscription between ' . $profile->getBestName()
                . ' and ' . $sub->getUri() . "\n"
            );
            printfnq('The reason was: ' . $e->getMessage() . "\n");
        }
    }
}


code:
#!/usr/bin/env php
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * ActivityPub implementation for GNU social
 *
 * @package   GNUsocial
 * @author    Diogo Cordeiro <diogo@fc.up.pt>
 * @copyright 2018-2019 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 * @link      http://www.gnu.org/software/social/
 */

define('INSTALLDIR', dirname(__DIR__, 3));
define('PUBLICDIR', INSTALLDIR . DIRECTORY_SEPARATOR . 'public');

$shortoptions = 'u:af';
$longoptions = ['uri=', 'all', 'force'];

$helptext = <<<END_OF_HELP
update_activitypub_profiles.php [options]
Refetch / update ActivityPub RSA keys, profile info and avatars. Useful if you
do something like accidentally delete your avatars directory when
you have no backup.

    -u --uri ActivityPub profile URI to update
    -a --all update all

END_OF_HELP;

require_once INSTALLDIR.'/scripts/commandline.inc';

if (!have_option('q', 'quiet')) {
    echo "ActivityPub Profiles updater will now start!\n";
    echo "Summoning Diogo Cordeiro, Richard Stallman and Chuck Norris to help us with this task!\n";
}

if (have_option('u', 'uri')) {
    $uri = get_option_value('u', 'uri');
    $user = Activitypub_profile::from_profile(Activitypub_explorer::get_profile_from_url($uri));
    try {
        $res = Activitypub_explorer::get_remote_user_activity($uri);
    } catch (Exception $e) {
        echo $e->getMessage()."\n";
        exit(1);
    }
    printfnq('Updated '.Activitypub_profile::update_profile($user, $res)->getBestName()."\n");
    exit(0);
} elseif (!have_option('a', 'all')) {
    show_help();
    exit(1);
}

$user = new Activitypub_profile();
$cnt = $user->find();
if (!empty($cnt)) {
    printfnq("Found {$cnt} ActivityPub profiles:\n");
} else {
    if (have_option('u', 'uri')) {
        printfnq("Couldn't find an existing ActivityPub profile with that URI.\n");
    } else {
        printfnq("Couldn't find any existing ActivityPub profiles.\n");
    }
    exit(0);
}
while ($user->fetch()) {
    try {
        $res = Activitypub_explorer::get_remote_user_activity($user->uri);
        $updated_profile = Activitypub_profile::update_profile($user, $res);
        printfnq('Updated '.$updated_profile->getBestName()."\n");
    } catch (NoProfileException $e) {
        printfnq('Deleted '.$user->uri."\n");
    } catch (Exception $e) {
        // let it go
    }
}
$user->free();
unset($user);


code:
<?php

// {{{ License

// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

// }}}

namespace Plugin\Directory\Controller;

use App\Core\DB\DB;
use Symfony\Component\HttpFoundation\Request;

class Directory
{
    public function people(Request $request)
    {
        return ['_template' => 'directory/people.html.twig', 'people' => DB::dql('select g from App\Entity\GSActor g order by g.nickname ASC')];
    }

    public function groups(Request $request)
    {
        return ['_template' => 'directory/group.html.twig', 'groups' => DB::dql('select g from App\Entity\Group g order by g.nickname ASC')];
    }
}


code:
<?php

// {{{ License

// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

// }}}

namespace Plugin\Directory;

use App\Core\Event;
use App\Core\Module;

class Directory extends Module
{
    public function onAddRoute($r)
    {
        $r->connect('people', '/people', [Controller\Directory::class, 'people']);
        $r->connect('groups', '/groups', [Controller\Directory::class, 'groups']);
        return Event::next;
    }
}


code:
<?php

// {{{ License

// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

// }}}

/**
 * Handle email notifications
 *
 * @package  GNUsocial
 * @category Plugin
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace Plugin\EmailNotifications;

use App\Core\Event;
use App\Core\Module;

class EmailNotifications extends Module
{
    public function onAddNotificationTransport(&$form_defs): bool
    {
        $form_defs['email'] = $form_defs['placeholder'];
        unset($form_defs['email']['post_on_status_change']);
        return Event::next;
    }
}


code:
<?php

// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
// }}}

namespace Plugin\Favourite;

use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Form;
use App\Core\Module;
use App\Entity\Favourite as Fave;
use App\Entity\Note;
use App\Util\Common;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;

class Favourite extends Module
{
    /**
     * HTML rendering event that adds the favourite form as a note
     * action, if a user is logged in
     */
    public function onAddNoteActions(Request $request, Note $note, array &$actions)
    {
        if (($user = Common::user()) == null) {
            return Event::next;
        }

        $opts   = ['note_id' => $note->getId(), 'gsactor_id' => $user->getId()];
        $is_set = DB::find('favourite', $opts) != null;
        $form   = Form::create([
            ['is_set',    HiddenType::class, ['data' => $is_set ? '1' : '0']],
            ['note_id',   HiddenType::class, ['data' => $note->getId()]],
            ['favourite', SubmitType::class, ['label' => ' ']],
        ]);

        // Form handler
        $ret = self::noteActionHandle($request, $form, $note, 'favourite', function ($note, $data) use ($opts) {
            $fave = DB::find('favourite', $opts);
            if (!$data['is_set'] && ($fave == null)) {
                DB::persist(Fave::create($opts));
                DB::flush();
            } else {
                DB::remove($fave);
                DB::flush();
            }
            return Event::stop;
        });

        if ($ret != null) {
            return $ret;
        }

        $actions[] = $form->createView();
        return Event::next;
    }
}


code:
<?php

// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
// }}}

namespace Plugin\Repeat;

use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Form;
use App\Core\Module;
use App\Entity\Note;
use App\Util\Common;
use App\Util\Exception\NotFoundException;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpFoundation\Request;

class Repeat extends Module
{
    /**
     * HTML rendering event that adds the repeat form as a note
     * action, if a user is logged in
     */
    public function onAddNoteActions(Request $request, Note $note, array &$actions)
    {
        if (($user = Common::user()) == null) {
            return Event::next;
        }

        $opts = ['gsactor_id' => $user->getId(), 'repeat_of' => $note->getId()];
        try {
            $is_set = DB::findOneBy('note', $opts) != null;
        } catch (NotFoundException $e) {
            // Not found
            $is_set = false;
        }
        $form = Form::create([
            ['is_set',  HiddenType::class, ['data' => $is_set ? '1' : '0']],
            ['note_id', HiddenType::class, ['data' => $note->getId()]],
            ['repeat',  SubmitType::class, ['label' => ' ']],
        ]);

        // Handle form
        $ret = self::noteActionHandle($request, $form, $note, 'repeat', function ($note, $data, $user) use ($opts) {
            $note = DB::findOneBy('note', $opts);
            if (!$data['is_set'] && $note == null) {
                DB::persist(Note::create([
                    'gsactor_id' => $user->getId(),
                    'repeat_of'  => $note->getId(),
                    'content'    => $note->getContent(),
                    'is_local'   => true,
                ]));
                DB::flush();
            } else {
                DB::remove($note);
                DB::flush();
            }
            return Event::stop;
        });

        if ($ret != null) {
            return $ret;
        }
        $actions[] = $form->createView();
        return Event::next;
    }
}


code:
<?php

// {{{ License

// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

// }}}

namespace Plugin\Reply;

use App\Core\DB\DB;
use App\Core\Event;
use App\Core\Form;
use function App\Core\I18n\_m;
use App\Core\Module;
use App\Entity\Note;
use App\Util\Common;
use App\Util\Exceptiion\InvalidFormException;
use App\Util\Exception\RedirectException;
use Component\Posting\Posting;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\HttpFoundation\Request;

class Reply extends Module
{
    public function onAddRoute($r)
    {
        $r->connect('note_reply', '/note/reply/{reply_to<\\d*>}', [self::class, 'replyController']);
    }

    /**
     * HTML rendering event that adds the reply form as a note action,
     * if a user is logged in
     */
    public function onAddNoteActions(Request $request, Note $note, array &$actions)
    {
        if (($user = Common::user()) == null) {
            return Event::next;
        }

        $form = Form::create([
            ['content',     HiddenType::class, ['label' => ' ', 'required' => false]],
            ['attachments', HiddenType::class, ['label' => ' ', 'required' => false]],
            ['note_id',     HiddenType::class, ['data' => $note->getId()]],
            ['reply',       SubmitType::class, ['label' => ' ']],
        ]);

        // Handle form
        $ret = self::noteActionHandle($request, $form, $note, 'reply', function ($note, $data) {
            if ($data['content'] !== null) {
                // JS submitted
                // TODO Implement in JS
                $actor_id = $user->getId();
                Posting::storeNote(
                    $actor_id,
                    $data['content'],
                    $data['attachments'],
                    $is_local = true,
                    $data['reply_to'],
                    $repeat_of = null
                );
            } else {
                // JS disabled, redirect
                throw new RedirectException('note_reply', ['reply_to' => $note->getId()]);
            }
        });

        if ($ret != null) {
            return $ret;
        }
        $actions[] = $form->createView();
        return Event::next;
    }

    /**
     * Controller for the note reply non-JS page
     */
    public function replyController(Request $request, string $reply_to)
    {
        $user     = Common::ensureLoggedIn();
        $actor_id = $user->getId();
        $note     = DB::find('note', ['id' => (int) $reply_to]);
        if ($note == null || !$note->isVisibleTo($user)) {
            throw new NoSuchNoteException();
        }

        $form = Form::create([
            ['content',     TextareaType::class, ['label' => ' ']],
            ['attachments', FileType::class,     ['label' => ' ', 'multiple' => true, 'required' => false]],
            ['reply',       SubmitType::class,   ['label' => _m('Submit')]],
        ]);

        $form->handleRequest($request);

        if ($form->isSubmitted()) {
            $data = $form->getData();
            if ($form->isValid()) {
                Posting::storeNote(
                    $actor_id,
                    $data['content'],
                    $data['attachments'],
                    $is_local = true,
                    $data['reply_to'],
                    $repeat_of = null
                );
            } else {
                throw new InvalidFormException();
            }
        }

        return [
            '_template' => 'note/reply.html.twig',
            'note'      => $note,
            'reply'     => $form->createView(),
        ];
    }
}


code:
<?php

// {{{ License

// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.

// }}}

/**
 * Handle xmpp notifications
 *
 * @package  GNUsocial
 * @category Plugin
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace Plugin\XMPPNotifications;

use App\Core\Event;
use App\Core\Module;

class XMPPNotifications extends Module
{
    public function onAddNotificationTransport(&$form_defs): bool
    {
        $form_defs['xmpp'] = $form_defs['placeholder'];
        return Event::next;
    }
}


code:
.content {
      display: flex;
      flex-wrap: wrap;
      margin-top: calc(4 * var(--main-size));
      justify-content: center;
      align-items: center;
      margin-left: calc(var(--nav-size) + var(--unit-size));
      margin-right: calc(var(--nav-size) + var(--unit-size));
      margin-bottom: var(--unit-size);
      border: solid 2px var(--accent-low);
      border-radius: var(--small-size);
}

.faq-nav {
      order: 1;
      display: flex;
      justify-content: space-evenly;
      flex-wrap: wrap;
      flex: 1;
      background-color: var(--bg1);
      padding: var(--medium-size);
      font-size: var(--medium-size);
      font-family: var(--head-font);
      border-radius: var(--small-size) var(--small-size) 0 0;
}

.faq-nav ul {
      display: flex;
      align-items: stretch;
      justify-content: space-evenly;
      width: 100%;
      margin-left: calc(2 * var(--side-margin));
      margin-right: calc(2 * var(--side-margin));
      padding: 0;
}

.faq-nav li {
      display: block;
      flex: 0 1 auto;
      list-style-type: none;
      font-weight: 700;
}

.active {
      color: #F6F6F6 !important;
      font-weight: 700;
}

.faq-nav a {
      color: #91B9D0;
}
.faq-nav a:hover {
      color: #F6F6F6;
      transition: all 0.8s ease;
}
.faq-nav a:focus {
      color: #F6F6F6;
      transition: all 0.8s ease;
}

.markd {
      order: 2;
      background-color: var(--bg2);
      font-size: var(--medium-size);
      padding: calc(3*var(--unit-size));
      padding-top: var(--unit-size);
      border-radius: 0 0 var(--small-size) var(--small-size);
}
.markd-single {
      border-radius: var(--small-size) !important;
}

.markd h2 {
      margin-top: var(--unit-size);
}

.markd p {
      margin-top: var(--unit-size);
      margin-left: calc(0.5 * var(--unit-size));
}

.markd ul {
      list-style-type: disc;
      margin-left: calc(2 * var(--unit-size));
      margin-top: var(--unit-size);
}


code:
.content {
      display: flex;
      flex-wrap: wrap;
      margin-top: calc(3 * var(--main-size));
      justify-content: center;
      align-items: center;
      margin-left: 0;
      margin-right: 0;
      height: 100%;
}

.faq-nav {
      order: 1;
      display: flex;
      justify-content: space-between;
      flex-wrap: wrap;
      width: 100%;
      background-color: #2A323B;
      padding: var(--medium-size);
      font-size: var(--medium-size);
      font-family: var(--head-font);
      box-shadow: 0px 0px 60px -20px rgba(0, 0, 0, 0.75);
}

.faq-nav ul {
      display: flex;
      align-items: stretch;
      justify-content: space-evenly;
      width: 100%;
      margin-left: var(--side-margin);
      margin-right: var(--side-margin);
      padding: 0;
}

.faq-nav li {
      display: block;
      flex: 0 1 auto;
      list-style-type: none;
      font-weight: 700;
      font-size: var(--medium-size);
}

.active {
      color: #F6F6F6 !important;
      font-weight: 700;
}

.faq-nav a {
      color: #91B9D0;
}
.faq-nav a:hover {
      color: #F6F6F6;
      transition: all 0.8s ease;
}
.faq-nav a:focus {
      color: #F6F6F6;
      transition: all 0.8s ease;
}

.markd {
      order: 2;
      background-color: #00000050;
      font-size: var(--small-size);
      padding: calc(3*var(--unit-size));
      padding-top: var(--unit-size);
      border-radius: var(--unit-size);
      margin-top: calc(2 * var(--unit-size));
      margin-right: var(--side-margin);
      margin-left: var(--side-margin);
      box-shadow: 0px 0px 60px -20px rgba(0, 0, 0, 0.75);
}

.markd h2 {
      margin-top: var(--unit-size);
}

.markd p {
      margin-top: var(--unit-size);
      margin-left: calc(0.5 * var(--unit-size));
}

.markd ul {
      list-style-type: disc;
      margin-left: calc(2 * var(--unit-size));
      margin-top: var(--unit-size);
}


code:
.content {
      display: flex;
      flex-wrap: wrap;
      margin-top: calc(3 * var(--main-size));
      justify-content: center;
      align-items: center;
      margin-left: 0;
      margin-right: 0;
      box-shadow: 0px 0px 60px -20px rgba(0, 0, 0, 0.75);
      height: 100%;
}

.faq-nav {
      order: 1;
      display: flex;
      justify-content: space-between;
      flex-wrap: wrap;
      width: 100%;
      background-color: #2A323B;
      padding: var(--medium-size);
      font-size: var(--medium-size);
      font-family: var(--head-font);
}

.faq-nav ul {
      display: flex;
      align-items: stretch;
      justify-content: space-evenly;
      width: 100%;
      margin-left: 0;
      margin-right: 0;
      padding: 0;
}

.faq-nav li {
      display: block;
      flex: 0 1 auto;
      list-style-type: none;
      font-weight: 700;
      font-size: var(--medium-size);
}

.active {
      color: #F6F6F6 !important;
      font-weight: 700;
}

.faq-nav a {
      color: #91B9D0;
}
.faq-nav a:hover {
      color: #F6F6F6;
      transition: all 0.8s ease;
}
.faq-nav a:focus {
      color: #F6F6F6;
      transition: all 0.8s ease;
}

.markd {
      order: 2;
      background-color: #00000050;
      font-size: var(--small-size);
      padding: calc(3*var(--unit-size));
      padding-top: var(--unit-size);
      border-radius: var(--unit-size);
      margin-top: calc(2 * var(--unit-size));
      margin-right: 1%;
      margin-left: 1%;
      box-shadow: 0px 0px 60px -20px rgba(0, 0, 0, 0.75);
}

.markd h2 {
      margin-top: var(--unit-size);
}

.markd p {
      margin-top: var(--unit-size);
      margin-left: calc(0.5 * var(--unit-size));
}

.markd ul {
      list-style-type: disc;
      margin-left: calc(2 * var(--unit-size));
      margin-top: var(--unit-size);
}


code:
.navbar {
      display: flex;
      flex-direction: column;
      width: var(--nav-size);
      height: calc(100vh - (3 * var(--unit-size)));
      position: fixed;
      background-color: var(--bg1);
      margin-top: calc(3 * var(--main-size));
      left: 0;
      top: 0;
      transition: 0.3s ease;
      border-right: solid 1px var(--accent-low);
}
/* left navigation */
.navbar .left-nav {
      flex: 1;
}
.navbar .left-nav > a {
      display: block;
      color: var(--accent);
      font-size: var(--medium-size);
      margin-bottom: var(--unit-size);
      margin-left: var(--unit-size);
}
.navbar .left-nav > a:nth-child(1) {
      margin-top: var(--unit-size);
}
.navbar .left-nav > a:last-child {
      margin-bottom: 0;
}
.navbar .left-nav > a:hover,
.navbar .left-nav > a:focus {
      color: var(--fg);
      transition: all 0.8s ease;
}
/* footer static pages links */
.navbar .footer {
      display: flex;
      flex-wrap: wrap;
      flex-direction: row;
      justify-content: space-evenly;
      font-size: var(--medium-size);
      padding: var(--unit-size);
      margin-bottom: var(--unit-size);
}
.navbar .footer a {
      margin-right: var(--unit-size);
      color: var(--accent);
      font-size: var(--medium-size);
}

/* checkbox trick remnants */
.icon-menu label {
      opacity: 0;
      cursor: pointer
}
#toggle {
      position: absolute;
      z-index: 2;
      cursor: pointer;
      opacity: 0;
}
#toggle:checked+.navbar {
      display: block;
      left: 0;
      top: 0;
      transition: 0.3s ease;
}

/* profile stats */
.profile {
      display: flex;
      flex-wrap: wrap;
      background-color: var(--bg2);
      padding: var(--unit-size);
      font-size: var(--small-size);
      margin-bottom: var(--unit-size);
      vertical-align: middle;
}

.icon-avatar {
      order: 1;
      height: calc(5 * var(--unit-size));
      calc(5 * var(--unit-size));
      margin-right: var(--small-size);
}

.info {
      order: 2;
}
.info #nick {
      order: 1;
      margin-bottom: var(--small-size);
}
.info .tags {
      order: 2;
      margin-bottom: var(--small-size);
}
.info .stats {
      order: 3;
}
.info .stats span:last-child {
      margin-left: var(--small-size);
}
.info .tags i {
      color: var(--accent);
}
.info .tags i:first-child {
      margin-left: 0 !important;
}
.info .tags i:last-child {
      margin-left: var(--small-size);
}


code:
.navbar {
      display: flex;
      flex-direction: column;
      width: var(--nav-size);
      height: calc(100vh - (3 * var(--unit-size)));
      position: fixed;
      background-color: var(--bg1);
      margin-top: calc(3 * var(--main-size));
      margin-left: -1px;
      border: solid 1px var(--accent-low);
}
.navbar {
      left: -100%;
      top: 0;
      transition: 0.3s ease;
}
/* left navigation */
.navbar .left-nav {
      flex: 1;
}
.navbar .left-nav > a {
      display: block;
      color: var(--accent);
      font-size: var(--medium-size);
      margin-bottom: var(--unit-size);
      margin-left: var(--unit-size);
}
.navbar .left-nav > a:nth-child(1) {
      margin-top: var(--unit-size);
}
.navbar .left-nav > a:last-child {
      margin-bottom: 0;
}
.navbar .left-nav > a:hover,
.navbar .left-nav > a:focus {
      color: var(--fg);
      transition: all 0.8s ease;
}
/* footer static pages links */
.navbar .footer {
      display: flex;
      flex-wrap: wrap;
      flex-direction: row;
      justify-content: space-evenly;
      font-size: var(--medium-size);
      padding: var(--unit-size);
      margin-bottom: var(--unit-size);
}
.navbar .footer a {
      margin-right: var(--unit-size);
      color: var(--accent);
      font-size: var(--medium-size);
}

/* checkbox trick */
.icon-menu label {
      display: block;
      cursor: pointer
}
#toggle {
      position: absolute;
      z-index: 2;
      cursor: pointer;
      opacity: 0;
}
#toggle:checked~.navbar {
      display: flex;
      left: 0;
      top: 0;
      transition: 0.3s ease;
}
input.larger {
      width: calc(1.5 * var(--main-size));
      height: calc(1.5 * var(--main-size));
}

/* profile stats */
.profile {
      display: flex;
      flex-wrap: wrap;
      background-color: var(--bg2);
      padding: var(--unit-size);
      font-size: var(--small-size);
      margin-bottom: var(--unit-size);
      vertical-align: middle;
}

.icon-avatar {
      order: 1;
      height: calc(5 * var(--unit-size));
calc(5 * var(--unit-size));
      margin-right: var(--small-size);
}

.info {
      order: 2;
}
.info #nick {
      order: 1;
      margin-bottom: var(--small-size);
}
.info .tags {
      order: 2;
      margin-bottom: var(--small-size);
}
.info .stats {
      order: 3;
}
.info .stats span:last-child {
      margin-left: var(--small-size);
}
.info .tags i {
      color: var(--accent);
}
.info .tags i:first-child {
      margin-left: 0 !important;
}
.info .tags i:last-child {
      margin-left: var(--small-size);
}


code:
.navbar {
      display: flex;
      flex-direction: column;
      width: var(--nav-size);
      height: calc(100vh - (3 * var(--unit-size)));
      position: fixed;
      background-color: var(--bg1);
      margin-top: calc(3 * var(--main-size));
      margin-left: -2px;
      border: solid 1px var(--accent-low);
}
.navbar {
      left: -100%;
      top: 0;
      transition: 0.3s ease;
}
/* left navigation */
.navbar .left-nav {
      flex: 1;
}
.navbar .left-nav > a {
      display: block;
      color: var(--accent);
      font-size: calc(1.2 * var(--medium-size));
      margin-bottom: var(--unit-size);
      margin-left: var(--unit-size);
}
.navbar .left-nav > a:nth-child(1) {
      margin-top: var(--unit-size);
}
.navbar .left-nav > a:last-child {
      margin-bottom: 0;
}
.navbar .left-nav > a:hover,
.navbar .left-nav > a:focus {
      color: var(--fg);
      transition: all 0.8s ease;
}
/* footer static pages links */
.navbar .footer {
      display: flex;
      flex-wrap: wrap;
      flex-direction: row;
      justify-content: space-evenly;
      font-size: var(--medium-size);
      padding: var(--unit-size);
      margin-bottom: var(--unit-size);
}
.navbar .footer a {
      margin-right: var(--unit-size);
      color: var(--accent);
      font-size: var(--medium-size);
}

/* checkbox trick */
.icon-menu label {
      display: block;
      cursor: pointer
}
#toggle {
      position: absolute;
      z-index: 2;
      cursor: pointer;
      opacity: 0;
}
#toggle:checked~.navbar {
      display: flex;
      left: 0;
      top: 0;
      transition: 0.3s ease;
}
input.larger {
      width: calc(1.5 * var(--main-size));
      height: calc(1.5 * var(--main-size));
}

/* profile stats */
.profile {
      display: flex;
      flex-wrap: wrap;
      background-color: var(--bg2);
      padding: var(--unit-size);
      font-size: var(--medium-size);
      margin-bottom: var(--unit-size);
      vertical-align: middle;
}

.icon-avatar {
      order: 1;
      height: calc(5 * var(--unit-size));
calc(5 * var(--unit-size));
      margin-right: var(--small-size);
}

.info {
      order: 2;
}
.info #nick {
      order: 1;
      margin-bottom: var(--small-size);
}
.info .tags {
      order: 2;
      margin-bottom: var(--small-size);
}
.info .stats {
      order: 3;
}
.info .stats span:last-child {
      margin-left: var(--small-size);
}
.info .tags i {
      color: var(--accent);
}
.info .tags i:first-child {
      margin-left: 0 !important;
}
.info .tags i:last-child {
      margin-left: var(--small-size);
}


code:
.content {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    align-items: center;
    margin-right: var(--small-size);
    margin-left: var(--small-size);
    margin-bottom: var(--unit-size);
}
.navbar .left-nav > a {
    font-size: var(--unit-size);
}
form {
    font-size: var(--medium-size);
    background-color: var(--bg2);
    padding: calc(2 * var(--unit-size));
    border-radius: var(--unit-size);
    border: solid 2px var(--accent-low);
}
label {
    font-family: 'Montserrat', sans-serif;
    font-weight: 700;
    width: 100%;
}
button {
    margin-top: var(--unit-size);
}
input[type=text],
input[type=password],
input[type=email]
{
    margin-top: calc(var(--unit-size) * 0.5);
    background-color: var(--bg1);
    color: var(--fg);
    border-style: none;
    padding: calc(var(--unit-size) * 0.5);
    border-radius: calc(var(--unit-size) * 0.5);
    font-size: var(--unit-size);
    margin-bottom: var(--unit-size);
    width: calc(100% - var(--unit-size));
}
.active {
    color: var(--fg) !important;
    font-weight: 700;
}
input {
    filter: none;
}
form input[type="checkbox"] {
    position: unset !important;
}
form button[type=submit] {
    background: var(--bg1);
    padding: 0.3em 1em;
    color: var(--fg);
    border-style: solid;
    border-color: var(--accent);
    border-radius: var(--unit-size);
    border-width: 2px;
    font-family: 'Montserrat', sans-serif;
    font-weight: 700;
}
form button[type=submit]:focus,
form button[type=submit]:hover {
    border-radius: var(--unit-size);
    box-shadow: 0 0px 8px var(--accent);
}


code:
.main {
      font-size: var(--medium-size);
      border: none !important;
}
.note {
      margin-bottom: 0 !important;
}
#save {
      display: flex;
}
#save div:nth-of-type(1) {
      padding: var(--unit-size);
}
#save div:nth-of-type(1) textarea {
      padding: var(--unit-size);
      border-radius: var(--unit-size);
      background-color: var(--bg1);
      font-size: var(--unit-size);
      width: 100%;
      box-sizing: border-box;
}
#save div:nth-of-type(2)


code:
/* link underlines tend to make hypertext less readable,
   because underlines obscure the shapes of the lower halves of words */
:link,:visited { text-decoration:none }

/* no list-markers by default, since lists are used more often for semantics */
ul,ol { list-style:none }

/* avoid browser default inconsistent heading font-sizes */
/* and pre/code too */
h1,h2,h3,h4,h5,h6,pre,code { font-size:1em; }

/* remove the inconsistent (among browsers) default ul,ol padding or margin  */
/* the default spacing on headings does not match nor align with
   normal interline spacing at all, so let's get rid of it. */
/* zero out the spacing around pre, form, body, html, p, blockquote as well */
/* form elements are oddly inconsistent, and not quite CSS emulatable. */
/*  nonetheless strip their margin and padding as well */
ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,body,html,p,blockquote,fieldset,input,textarea
{ margin:0; padding:0; border:none; }

/* whoever thought blue linked image borders were a good idea? */
a img,:link img,:visited img { border:none }

/* de-italicize address */
address { font-style:normal }

/* firefox focus outline */
:focus {outline:none;}
::-moz-focus-inner {border:0;}


code:
.content {
      display: flex;
      flex-wrap: wrap;
      margin-top: calc(4 * var(--main-size));
      justify-content: center;
      align-items: center;
      margin-left: var(--nav-size);
      margin-right: var(--nav-size);
      margin-bottom: var(--unit-size);
}

.main {
      width: 100%;
      margin-left: var(--unit-size);
      margin-right: var(--unit-size);
      box-shadow: var(--shadow);
      border-radius: var(--small-size);
      border: solid 2px var(--accent-low);
      background-color: var(--bg3);
}

.set-nav {
      order: 1;
      display: flex;
      justify-content: space-evenly;
      flex-wrap: wrap;
      flex: 1;
      background-color: var(--bg1);
      padding: var(--medium-size);
      font-size: var(--medium-size);
      font-family: var(--head-font);
      border-radius: var(--small-size) var(--small-size) 0 0;
}

.set-nav ul {
      display: flex;
      align-items: stretch;
      justify-content: space-evenly;
      width: 100%;
      margin-left: calc(2 * var(--side-margin));
      margin-right: calc(2 * var(--side-margin));
      padding: 0;
}

.set-nav li {
      display: block;
      flex: 0 1 auto;
      list-style-type: none;
      font-weight: 700;
}

.set-nav a {
      color: var(--accent);
}

.set-nav a:hover {
      color: var(--fg);
      transition: all 0.8s ease;
}

.active {
      color: var(--fg) !important;
      font-weight: 700;
}

.secundary {
      display: flex;
      flex-wrap: wrap;
      background-color: var(--bg3);
      margin: var(--unit-size);
      border: solid 2px var(--accent-low);
      border-radius: var(--small-size);
      box-shadow: var(--shadow);
}

.form {
      order: 3;
      padding: calc(2*var(--unit-size));
      height: 100%;
      width: 100%;
      border-radius: 0 0 var(--small-size) var(--small-size);
}
.form-single {
      order: 2;
      padding: calc(2*var(--unit-size));
      height: 100%;
      width: 100%;
      border-radius: var(--small-size);
}
.form label,
.form-single label {
      font-size: var(--medium-size);
}

/* FORMS ------------------------------*/
label {
      display: inline-block;
      font-family: 'Montserrat', sans-serif;
      font-weight: 700;
      width: 100%;
}

input[type=text] {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      background-color: var(--bg2);
      box-shadow: var(--shadow);
      border: solid 2px var(--accent-low);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}

.form input[type=checkbox] {
      float: left;
      margin-right: calc(var(--unit-size) * 0.5);
}

button[type=submit] {
      background: var(--bg1);
      padding: calc(var(--unit-size) * 0.8) calc(var(--unit-size) * 2);
      color: var(--fg);
      border-style: solid;
      border-color: var(--accent);
      border-radius: var(--unit-size);
      border-width: 2px;
      font-family: 'Montserrat', sans-serif;
      font-size: var(--small-size);
      font-weight: 700;
}

#save_bio {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      height: calc(10 * var(--unit-size));
      box-shadow: var(--shadow);
      background-color: var(--bg2);
      border: solid 2px var(--accent-low);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
      word-wrap: break-word;
      resize: vertical;
}

#save_phone_number {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      background-color: var(--bg2);
      box-shadow: var(--shadow);
      border: solid 2px var(--accent-low);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}

#save_language{
      margin-top: calc(var(--unit-size) * 0.5);
      background-color: var(--bg2);
      box-shadow: var(--shadow);
      border: solid 2px var(--accent-low);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}
select {
      -webkit-appearance: none;
      -moz-appearance: none;
      appearance: none;
      background-image: url("../../icons/drop.svg");
      background-size: var(--main-size);
      background-position: 95% 50%;
      background-repeat: no-repeat;
}

.help-text {
      font-size: var(--medium-size);
      font-style: italic;
      color: var(--fg);
      margin-bottom: var(--unit-size);
}

.form-single input[type=checkbox] {
      margin-left: var(--unit-size);
}

/* JS CROPPING */
#img-cropped {
      display: block;
      max-width: 100%;
}
#img-container {
      margin: var(--main-size) auto;
      max-width: 60%;
}
.cropper-view-box,
.cropper-face {
      border-radius: 50%;
}

/* NOTIFICATIONS TABS */
/* visual feedback */
ul input[type=radio] + label {
      color: var(--accent);
}
ul input[type=radio]:checked + label {
      color: var(--fg);
}
ul input[type=radio]:focus + label {
      color: var(--fg);
}

/* show/hide each tab */
ul input[type=radio]:not(:checked) + label + div {
      display: none;
}
ul input[type=radio]:checked + label + div {
      display: flex;
}

/* hide radio buttons */
input[type=radio] {
      position: absolute;
      z-index: 1;
      height: 1px;
      width: 1px;
      overflow: hidden;
      clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
      clip: rect(1px, 1px, 1px, 1px);
}

#form-tabs {
      order: 2;
      width: 100%;
      font-size: var(--medium-size);
}
#form-tabs ul {
      display: flex;
      flex-wrap: wrap;
      border-radius: 0 0 var(--unit-size) var(--unit-size);
      background-color: var(--bg1);
      justify-content: space-evenly;
      width: 100%;
}
#form-tabs ul label {
      order: 1;
      flex: 0;
}

#tabs {
      padding: var(--medium-size);
      text-transform: capitalize;
}

#form-tabs ul .form {
      order: 2;
      padding: calc(2*var(--unit-size));
      background-color: var(--bg3) !important;
      height: 100%;
      margin-left: 0;
      margin-right: 0;
      margin-top: 0;
      box-shadow: 0;
      border-radius: 0 0 var(--unit-size) var(--unit-size);
      font-size: var(--unit-size);
}
#form-tabs ul .form form{
      width: 100%;
}
#form-tabs ul .form form div label {
      margin-bottom: calc(var(--unit-size) * 0.5);
}

.secundary button[type=submit] {
      background: var(--bg1);
      padding: 0.6em 1.2em;
      color: var(--fg);
      border-style: solid;
      border-color: var(--accent);
      border-radius: var(--unit-size);
      border-width: 2px;
      font-family: 'Montserrat', sans-serif;
      font-weight: 700;
}
.secundary button[type=submit]:focus,
.secundary button[type=submit]:hover {
      border-radius: var(--unit-size);
      box-shadow: 0 0px 8px var(--accent);
}


code:
.content {
      display: flex;
      flex-wrap: wrap;
      margin-top: calc(4 * var(--main-size));
      justify-content: center;
      align-items: center;
      margin-left: 0;
      margin-right: 0;
      margin-bottom: var(--unit-size);
}

.main {
      width: 100%;
      margin-left: var(--unit-size);
      margin-right: var(--unit-size);
      box-shadow: var(--shadow);
      border-radius: var(--small-size);
      border: solid 2px var(--accent-low);
      background-color: var(--bg3);
}

.set-nav {
      order: 1;
      display: flex;
      justify-content: space-evenly;
      flex-wrap: wrap;
      flex: 1;
      background-color: var(--bg1);
      padding: var(--medium-size);
      font-family: var(--head-font);
      border-radius: var(--small-size) var(--small-size) 0 0;
}

.set-nav ul {
      display: flex;
      align-items: stretch;
      justify-content: space-around;
      width: 100%;
      margin-left: var(--side-margin);
      margin-right: var(--side-margin);
      padding: 0;
}

.set-nav li {
      display: block;
      flex: 0 1 auto;
      list-style-type: none;
      font-weight: 700;
      font-size: var(--medium-size);
}

.set-nav a {
      color: var(--accent);
}

.set-nav a:hover {
      color: var(--fg);
      transition: all 0.8s ease;
}

.active {
      color: var(--fg) !important;
      font-weight: 700;
}

.secundary {
      display: flex;
      flex-wrap: wrap;
      background-color: var(--bg3);
      margin: var(--unit-size);
      border: solid 2px var(--accent-low);
      border-radius: var(--small-size);
      box-shadow: var(--shadow);
}

.form {
      order: 3;
      padding: calc(2*var(--unit-size));
      height: 100%;
      width: 100%;
      box-shadow: var(--shadow);
      border-radius: 0 0 var(--small-size) var(--small-size);
}
.form-single {
      order: 2;
      padding: calc(2*var(--unit-size));
      height: 100%;
      width: 100%;
      border-radius: var(--small-size);
}
.form label,
.form-single label {
      font-size: var(--medium-size);
}

/* FORMS ------------------------------*/
.form label {
      display: inline-block;
      font-family: 'Montserrat', sans-serif;
      font-weight: 700;
      width: 100%;
}

.form input[type=text] {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      background-color: var(--bg2);
      box-shadow: var(--shadow);
      border: solid 2px var(--accent-low);
      padding: calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}

.form input[type=checkbox] {
      float: right;
}

#save_bio {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      height: calc(10 * var(--unit-size));
      box-shadow: var(--shadow);
      background-color: var(--bg2);
      border: solid 2px var(--accent-low);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
      word-wrap: break-word;
      resize: vertical;
}

#save_phone_number {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      background-color: var(--bg2);
      border: solid 2px var(--accent-low);
      box-shadow: var(--shadow);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}

#save_language{
      margin-top: calc(var(--unit-size) * 0.5);
      background-color: var(--bg2);
      border: solid 2px var(--accent-low);
      box-shadow: var(--shadow);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}
select {
      -webkit-appearance: none;
      -moz-appearance: none;
      appearance: none;
      background-image: url("../../icons/drop.svg");
      background-size: var(--main-size);
      background-position: 95% 50%;
      background-repeat: no-repeat;
}

.help-text {
      font-size: var(--medium-size);
      font-style: italic;
      color: var(--fg);
      margin-bottom: var(--unit-size);
}

.form-single input[type=checkbox] {
      margin-left: var(--unit-size);
}

/* JS Cropping */
#img-cropped {
      display: block;
      max-width: 100%;
}
#img-container {
      margin: var(--main-size) auto;
      max-width: 80%;
}
.cropper-view-box,
.cropper-face {
      border-radius: 50%;
}

/* NOTIFICATIONS TABS */
/* visual feedback */
ul input[type=radio] + label {
      color: var(--accent);
}
ul input[type=radio]:checked + label {
      color: var(--fg);
}
ul input[type=radio]:focus + label {
      color: var(--fg);
}

/* show/hide each tab */
ul input[type=radio]:not(:checked) + label + div {
      display: none;
}
ul input[type=radio]:checked + label + div {
      display: flex;
}

/* hide radio buttons */
input[type=radio] {
      position: absolute;
      z-index: 1;
      height: 1px;
      width: 1px;
      overflow: hidden;
      clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
      clip: rect(1px, 1px, 1px, 1px);
}

#form-tabs {
      order: 2;
      width: 100%;
      font-size: var(--medium-size);
}
#form-tabs ul {
      display: flex;
      flex-wrap: wrap;
      border-radius: 0 0 var(--unit-size) var(--unit-size);
      background-color: var(--bg1);
      justify-content: space-evenly;
      width: 100%;
}
#form-tabs ul label {
      order: 1;
      flex: 0;
}

#tabs {
      padding: var(--medium-size);
      text-transform: capitalize;
}

#form-tabs ul .form {
      order: 2;
      padding: calc(2*var(--unit-size));
      background-color: var(--bg3) !important;
      height: 100%;
      margin-left: 0;
      margin-right: 0;
      margin-top: 0;
      box-shadow: 0;
      border-radius: 0 0 var(--unit-size) var(--unit-size);
      font-size: var(--unit-size);
}
#form-tabs ul .form form{
      width: 100%;
}
#form-tabs ul .form form div label {
      margin-bottom: calc(var(--unit-size) * 0.5);
}

.secundary button[type=submit] {
      background: var(--bg1);
      padding: 0.6em 1.2em;
      color: var(--fg);
      border-style: solid;
      border-color: var(--accent);
      border-radius: var(--unit-size);
      border-width: 2px;
      font-family: 'Montserrat', sans-serif;
      font-weight: 700;
}
.secundary button[type=submit]:focus,
.secundary button[type=submit]:hover {
      border-radius: var(--unit-size);
      box-shadow: 0 0px 8px var(--accent);
}


code:
.content {
      display: flex;
      flex-wrap: wrap;
      margin-top: calc(3 * var(--main-size) + 2%);
      justify-content: center;
      align-items: center;
      margin-left: 0;
      margin-right: 0;
      margin-bottom: var(--unit-size);
}

.main {
      width: 100%;
      margin-left: 1%;
      margin-right: 1%;
      box-shadow: var(--shadow);
      border-radius: var(--small-size);
      border: solid 2px var(--accent-low);
      background-color: var(--bg3);
}

.set-nav {
      order: 1;
      display: flex;
      justify-content: space-between;
      flex-wrap: wrap;
      flex: 1;
      background-color: var(--bg1);
      padding: var(--medium-size);
      font-family: var(--head-font);
      border-radius: var(--small-size) var(--small-size) 0 0;
}

.set-nav ul {
      display: flex;
      align-items: stretch;
      justify-content: space-around;
      width: 100%;
      margin-left: var(--side-margin);
      margin-right: var(--side-margin);
      padding: 0;
}

.set-nav li {
      display: block;
      flex: 0 1 auto;
      list-style-type: none;
      font-weight: 700;
      font-size: var(--medium-size);
}

.set-nav a {
      color: var(--accent);
}

.set-nav a:hover {
      color: var(--fg);
      transition: all 0.8s ease;
}

.active {
      color: var(--fg) !important;
      font-weight: 700;
}

.secundary {
      display: flex;
      flex-wrap: wrap;
      border-radius: 0 0 var(--unit-size) var(--unit-size);
      background-color: var(--bg3);
      margin: 2%;
      border: solid 2px var(--accent-low);
      border-radius: var(--small-size);
      box-shadow: var(--shadow);
}

.form {
      order: 3;
      padding: var(--unit-size);
      height: 100%;
      width: 100%;
      box-shadow: var(--shadow);
      border-radius: 0 0 var(--small-size) var(--small-size);
}
.form-single {
      order: 2;
      padding: var(--unit-size);
      height: 100%;
      width: 100%;
      border-radius: var(--small-size);
}
.form label,
.form-single label {
      font-size: var(--medium-size);
}

/* FORMS ------------------------------*/
.form label,
.form-single label {
      display: inline-block;
      font-family: 'Montserrat', sans-serif;
      font-weight: 700;
      font-size: var(--medium-size);
      width: 100%;
}

.form input[type=text],
.form-single input[type=text] {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      box-shadow: var(--shadow);
      background-color: var(--bg2);
      border: solid 2px var(--accent-low);
      padding: calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}

.form input[type=checkbox] {
      float: right;
}

#save_bio {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      height: calc(10 * var(--unit-size));
      background-color: var(--bg2);
      border: solid 2px var(--accent-low);
      box-shadow: var(--shadow);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
      word-wrap: break-word;
      resize: vertical;
}

#save_phone_number {
      margin-top: calc(var(--unit-size) * 0.5);
      width: calc(100% - var(--unit-size));
      background-color: var(--bg2);
      border: solid 2px var(--accent-low);
      box-shadow: var(--shadow);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}

#save_language{
      margin-top: calc(var(--unit-size) * 0.5);
      background-color: var(--bg2);
      border: solid 2px var(--accent-low);
      box-shadow: var(--shadow);
      padding: calc(var(--unit-size) * 0.5) calc(var(--unit-size) * 0.5);
      color: var(--fg);
      border-radius: calc(var(--unit-size) * 0.5);
      font-size: var(--medium-size);
}
select {
      -webkit-appearance: none;
      -moz-appearance: none;
      appearance: none;
      background-image: url("../../icons/drop.svg");
      background-size: var(--main-size);
      background-position: 95% 50%;
      background-repeat: no-repeat;
}

.help-text {
      font-size: var(--medium-size);
      font-style: italic;
      color: var(--fg);
      margin-bottom: var(--unit-size);
}

.form-single input[type=checkbox] {
      margin-left: var(--unit-size);
}

/* JS Cropping */
#img-cropped {
      display: block;
      max-width: 100%;
}
#img-container {
      margin: var(--main-size) auto;
      max-width: 80%;
}
.cropper-view-box,
.cropper-face {
      border-radius: 50%;
}

/* NOTIFICATIONS TABS */
/* visual feedback */
ul input[type=radio] + label {
      color: var(--accent);
}
ul input[type=radio]:checked + label {
      color: var(--fg);
}
ul input[type=radio]:focus + label {
      color: var(--fg);
}

/* show/hide each tab */
ul input[type=radio]:not(:checked) + label + div {
      display: none;
}
ul input[type=radio]:checked + label + div {
      display: flex;
}

/* hide radio buttons */
input[type=radio] {
      position: absolute;
      z-index: 1;
      height: 1px;
      width: 1px;
      overflow: hidden;
      clip: rect(1px 1px 1px 1px); /* IE6, IE7 */
      clip: rect(1px, 1px, 1px, 1px);
}

#form-tabs {
      order: 2;
      width: 100%;
      font-size: var(--medium-size);
}
#form-tabs ul {
      display: flex;
      flex-wrap: wrap;
      border-radius: 0 0 var(--unit-size) var(--unit-size);
      background-color: var(--bg1);
      justify-content: space-evenly;
      width: 100%;
}
#form-tabs ul label {
      order: 1;
      flex: 0;
}

#tabs {
      padding: var(--medium-size);
      text-transform: capitalize;
}

#form-tabs ul .form {
      order: 2;
      padding: var(--unit-size);
      background-color: var(--bg3) !important;
      height: 100%;
      margin-left: 0;
      margin-right: 0;
      margin-top: 0;
      box-shadow: 0;
      border-radius: 0 0 var(--unit-size) var(--unit-size);
      font-size: var(--unit-size);
}
#form-tabs ul .form form{
      width: 100%;
}
#form-tabs ul .form form div label {
      margin-bottom: calc(var(--unit-size) * 0.5);
}


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   version="1.1"
   width="32"
   height="32"
   viewBox="0 0 32 32"
   id="svg6"
   sodipodi:docname="checkmark.svg"
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
  <metadata
     id="metadata12">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs
     id="defs10" />
  <sodipodi:namedview
     inkscape:document-rotation="0"
     pagecolor="#ffffff"
     bordercolor="#666666"
     borderopacity="1"
     objecttolerance="10"
     gridtolerance="10"
     guidetolerance="10"
     inkscape:pageopacity="0"
     inkscape:pageshadow="2"
     inkscape:window-width="2560"
     inkscape:window-height="1382"
     id="namedview8"
     showgrid="false"
     inkscape:zoom="29.875"
     inkscape:cx="16.066946"
     inkscape:cy="16"
     inkscape:window-x="0"
     inkscape:window-y="30"
     inkscape:window-maximized="1"
     inkscape:current-layer="svg6" />
  <title
     id="title2">checkmark</title>
  <path
     d="M27 4l-15 15-7-7-5 5 12 12 20-20z"
     id="path4"
     style="fill:#2e3440;fill-opacity:1" />
</svg>


code:
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   xmlns:dc="http://purl.org/dc/elements/1.1/"
   xmlns:cc="http://creativecommons.org/ns#"
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
   xmlns:svg="http://www.w3.org/2000/svg"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
   sodipodi:docname="drop.svg"
   id="svg6"
   viewBox="0 0 32 32"
   height="32"
   width="32"
   version="1.1">
  <metadata
     id="metadata12">
    <rdf:RDF>
      <cc:Work
         rdf:about="">
        <dc:format>image/svg+xml</dc:format>
        <dc:type
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
      </cc:Work>
    </rdf:RDF>
  </metadata>
  <defs
     id="defs10" />
  <sodipodi:namedview
     inkscape:current-layer="svg6"
     inkscape:window-maximized="1"
     inkscape:window-y="30"
     inkscape:window-x="0"
     inkscape:cy="16.019032"
     inkscape:cx="16.050301"
     inkscape:zoom="27.75"
     showgrid="false"
     id="namedview8"
     inkscape:window-height="1022"
     inkscape:window-width="1920"
     inkscape:pageshadow="2"
     inkscape:pageopacity="0"
     guidetolerance="10"
     gridtolerance="10"
     objecttolerance="10"
     borderopacity="1"
     bordercolor="#666666"
     pagecolor="#ffffff" />
  <title
     id="title2">drop</title>
  <path
     style="fill:#f6f6f6;fill-opacity:1"
     id="path4"
     d="M16 25.173c-0.823 0-10.695-17.1-10.284-17.813s20.157-0.713 20.568 0c0.411 0.713-9.461 17.813-10.284 17.813z" />
</svg>


code:
function getRoundedCanvas(sourceCanvas) {
    var canvas = document.createElement('canvas');
    var context = canvas.getContext('2d');
    var width = sourceCanvas.width;
    var height = sourceCanvas.height;

    canvas.width = width;
    canvas.height = height;
    context.imageSmoothingEnabled = true;
    context.drawImage(sourceCanvas, 0, 0, width, height);
    context.globalCompositeOperation = 'destination-in';
    context.beginPath();
    context.arc(width / 2, height / 2, Math.min(width, height) / 2, 0, 2 * Math.PI, true);
    context.fill();
    return canvas;
}

var input     = document.getElementById('form_avatar')
var container = document.getElementById('img-container')
var cropImage = document.getElementById('img-cropped')
var cropped   = document.getElementById('form_hidden')

input.addEventListener('change', function (e) {
    if (!input.files || !input.files[0]) return;
    var reader = new FileReader()
    reader.onload = function (e) {
        container.style = 'display: block'
        cropImage.setAttribute('src', e.target.result)

        function update() {
            var croppedCanvas = cropper.getCroppedCanvas()
            var roundedCanvas = getRoundedCanvas(croppedCanvas)
            cropped.value = roundedCanvas.toDataURL()
            input.files = undefined
        }
        var cropper = new Cropper(cropImage, {
            aspectRatio: 1,
            viewMode: 1,
            cropend: update,
            ready: update,
        })
    }
    reader.readAsDataURL(input.files[0])
})


code:
<?php

/*
 * This file is part of GNU social - https://www.gnu.org/software/social
 *
 * GNU social is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * GNU social is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * GNU social's true web entry point, bootstraps Symfony's configuration and instantiates our Kernel
 *
 * @package  GNUsocial
 * @category Framework
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */
use App\CacheKernel;
use App\Kernel;
use Symfony\Component\ErrorHandler\Debug;
use Symfony\Component\HttpFoundation\Request;

require dirname(__DIR__) . '/config/bootstrap.php';

if ($_SERVER['APP_DEBUG']) {
    umask(0000);

    Debug::enable();
}

if ($trustedProxies = $_SERVER['TRUSTED_PROXIES'] ?? $_ENV['TRUSTED_PROXIES'] ?? false) {
    Request::setTrustedProxies(\explode(',', $trustedProxies),
                               Request::HEADER_X_FORWARDED_ALL ^ Request::HEADER_X_FORWARDED_HOST);
}

if ($trustedHosts = $_SERVER['TRUSTED_HOSTS'] ?? $_ENV['TRUSTED_HOSTS'] ?? false) {
    Request::setTrustedHosts([$trustedHosts]);
}

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);

// Wrap the default Kernel with the CacheKernel one in 'prod' environment
if ('prod' === $kernel->getEnvironment() || isset($_ENV['SOCIAL_USE_CACHE_KERNEL'])) {
    $kernel = new CacheKernel($kernel);
}

$request  = Request::createFromGlobals();
$response = $kernel->handle($request);
$response->send();
$kernel->terminate($request, $response);


code:
parameters:
  remember_me_httponly: true
  remember_me_samesite: strict

  gnusocial:
    site:
      name: GNU social
      notice:
      theme: default
      logo:
      language: en
      detect_language: true
      email: ''
      use_email: false
      recovery_disclose: false
      timezone: UTC
      brought_by:
      brought_by_url:
      closed: false
      register_type: public
      nickname: foo
      ssl: always
      ssl_proxy: false
      duplicate_time_limit: 60
      text_limit: 1000
      x_static_delivery: false

    security:
      algorithm: bcrypt
      options:
        cost: 12

    db:
      mirror:

    cache:
      early_recompute: 0.95
      notice_max_count: 128
      adapters:
        default: redis://redis

    avatar:
      server:
      ssl:
      dir: "%kernel.project_dir%/file/avatar/"
      default: "/assets/default-avatar.svg"
      max_size_px: 300

    javascript:
      server:
      ssl:

    attachments:
      server:
      ssl:
      dir: "%kernel.project_dir%/file/attachments/"
      supported:
        application/vnd.oasis.opendocument.chart: odc
        application/vnd.oasis.opendocument.formula: odf
        application/vnd.oasis.opendocument.graphics: odg
        application/vnd.oasis.opendocument.graphics-template: otg
        application/vnd.oasis.opendocument.image: odi
        application/vnd.oasis.opendocument.presentation: odp
        application/vnd.oasis.opendocument.presentation-template: otp
        application/vnd.oasis.opendocument.spreadsheet: ods
        application/vnd.oasis.opendocument.spreadsheet-template: ots
        application/vnd.oasis.opendocument.text: odt
        application/vnd.oasis.opendocument.text-master: odm
        application/vnd.oasis.opendocument.text-template: ott
        application/vnd.oasis.opendocument.text-web: oth
        application/pdf: pdf
        application/zip: zip
        application/x-bzip2: bz2
        application/x-go-sgf: sgf
        application/xml: xml
        application/gpx+xml: gpx
        image/png: ".png"
        image/jpeg: ".jpeg"
        image/gif: ".gif"
        image/vnd.microsoft.icon: ".ico"
        image/svg+xml: svg
        audio/ogg: ogg
        audio/mpeg: mpg
        audio/x-speex: spx
        application/ogg: ogx
        text/plain: txt
        video/mpeg: mpeg
        video/mp4: mp4
        video/ogg: ogv
        video/quicktime: mov
        video/webm: webm
      file_quota: 2097152
      user_quota: 209715200
      monthly_quota: 20971520
      uploads: true
      show_html: true
      show_thumbs: true
      process_links: true
      ext_blacklist: []
      memory_limit: 1024M

    thumbnail:
      server:
      ssl:
      dir: "%kernel.project_dir%/file/thumbnails/"
      crop: false
      max_size_px: 1000
      width: 450
      height: 600
      upscale: false
      animated: false

    theme:
      server:
      ssl:
      dir: "%kernel.project_dir%/public/theme/"

    plugins:
      server:
      ssl:
      core: []
      default: []
      locale_path:

    license:
      type: cc
      owner:
      url: https://creativecommons.org/licenses/by/4.0/
      title: Creative Commons Attribution 4.0
      image: "/theme/licenses/cc_by_4.0.png"

    nickname:
      reserved:
        - doc
        - main
        - avatar
        - theme
        - settings
        - admin
      featured: []

    profile:
      bio_text_limit:
      allow_nick_change: false
      allow_private_stream: true
      backup: false
      restore: false
      delete: false
      move: false

    image:
      jpegquality: 85

    foaf:
      mbox_sha1sum: false

    public:
      local_only: false
      blacklist: []
      exclude_sources: []

    invite:
      enabled: true

    tag:
      dropoff: 864000
      cutoff: 7776000

    popular:
      dropoff: 864000
      cutoff: 7776000

    new_users:
      default_subscriptions:
      welcome_user:

    linkify:
      bare_domains: false
      ipv4: false
      ipv6: false

    group:
      max_aliases: 3
      description_limit:

    people_tag:
      max_tags: 100
      max_people: 500
      allow_tagging:
        local: true
        remote: true
      description_limit:

    search:
      type: fulltext

    html_filter:
      tags:
      - img
      - video
      - audio
      - script

    notice:
      content_limit:
      allow_private: false
      hide_banned: true

    message:
      content_limit:

    location:
      share: user

    robots_txt:
      crawl_delay: 0
      disallow:
      - main
      - settings
      - admin
      - search
      - message

    nofollow:
      subscribers: true
      members: true
      peopletag: true
      external: sometimes

    url_shortener:
      service: internal
      max_url_length: 100
      max_notice_length:

    http:
      ssl_ca_file: "/docker/certbot/files/live/"
      timeout: 60
      proxy_host:
      proxy_port:
      proxy_user:
      proxy_password:
      proxy_auth_scheme:

    discovery:
      CORS: false


code:
<?php

// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
// }}}

namespace App;

use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;

class CacheKernel extends HttpCache
{
    /**
     * Override default caching options
     */
    protected function getOptions(): array
    {
        return parent::getOptions();
    }
}


code:
<?php

// {{{ License
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// GNU social is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with GNU social.  If not, see <http://www.gnu.org/licenses/>.
// }}}

/**
 * Command to search for event by pattern
 *
 * @package  GNUsocial
 * @category Command
 *
 * @author    Hugo Sales <hugo@fc.up.pt>
 * @copyright 2020 Free Software Foundation, Inc http://www.fsf.org
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

namespace App\Command;

use Functional as F;
use ReflectionFunction;
use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class ListEventsCommand extends Command
{
    protected static $defaultName = 'app:events';
    private EventDispatcherInterface $dispatcher;

    public function __construct(EventDispatcherInterface $dispatcher)
    {
        parent::__construct();
        $this->dispatcher = $dispatcher;
    }

    protected function configure()
    {
        $this->setDefinition([new InputArgument('pattern', InputArgument::OPTIONAL, 'An event pattern to look for')])
             ->setDescription('Search for an event')
             ->setHelp(<<<'EOF'
The <info>%command.name%</info> command displays GNU social event listeners:

  <info>php %command.full_name%</info>

To get specific listeners for an event, specify its name:

  <info>php %command.full_name% kernel.request</info>
EOF
             );
    }

    /**
     * Execute the command, outputing a list of GNU social events
     */
    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $patterm   = $input->getArgument('pattern') ?? 'GNUsocial.*';
        $listeners = $this->dispatcher->getListeners();
        $listeners = F\select($listeners,
                              function ($_, $key, $__) use ($patterm) {
                                  return preg_match('/' . $patterm . '/', $key);
                              });

        ksort($listeners);
        foreach ($listeners as $event => $listener) {
            $event = explode('.', $event)[1];
            $output->writeln("<info>Event '<options=bold>{$event}</options=bold>':</info>");
            foreach ($listener as $c) {
                $r = new ReflectionFunction($c);
                $m = $r->getStaticVariables()['handler'];
                $output->writeln("\t" . get_class($m[0]) . '::' . $m[1]);
            }
            $output->writeln('');
        }

        if (!$input->hasArgument('pattern')) {
            $io                = new SymfonyStyle($input, $output);
            $options           = [];
            $helper            = new DescriptorHelper();
            $options['output'] = $io;
            $helper->describe($io, null, $options);
        }
        return 0;
    }
}