mirror of
https://github.com/pixelfed/pixelfed.git
synced 2024-11-26 08:13:16 +00:00
commit
8a1efb3ff0
302 changed files with 20809 additions and 873 deletions
|
@ -1,6 +1,7 @@
|
|||
storage
|
||||
data
|
||||
Dockerfile
|
||||
docker-compose*.yml
|
||||
.dockerignore
|
||||
.git
|
||||
.gitignore
|
||||
|
|
9
.editorconfig
Normal file
9
.editorconfig
Normal file
|
@ -0,0 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
32
.env.example
32
.env.example
|
@ -1,46 +1,50 @@
|
|||
APP_NAME=Laravel
|
||||
APP_NAME="PixelFed Test"
|
||||
APP_ENV=local
|
||||
APP_KEY=
|
||||
APP_DEBUG=true
|
||||
APP_URL=http://localhost
|
||||
|
||||
ADMIN_DOMAIN="localhost"
|
||||
APP_DOMAIN="localhost"
|
||||
|
||||
LOG_CHANNEL=stack
|
||||
|
||||
DB_CONNECTION=mysql
|
||||
DB_HOST=127.0.0.1
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=homestead
|
||||
DB_USERNAME=homestead
|
||||
DB_PASSWORD=secret
|
||||
DB_DATABASE=
|
||||
DB_USERNAME=
|
||||
DB_PASSWORD=
|
||||
|
||||
BROADCAST_DRIVER=log
|
||||
CACHE_DRIVER=file
|
||||
SESSION_DRIVER=file
|
||||
CACHE_DRIVER=redis
|
||||
SESSION_DRIVER=redis
|
||||
SESSION_LIFETIME=120
|
||||
QUEUE_DRIVER=sync
|
||||
QUEUE_DRIVER=redis
|
||||
|
||||
REDIS_HOST=127.0.0.1
|
||||
REDIS_PASSWORD=null
|
||||
REDIS_PORT=6379
|
||||
|
||||
MAIL_DRIVER=smtp
|
||||
MAIL_DRIVER=log
|
||||
MAIL_HOST=smtp.mailtrap.io
|
||||
MAIL_PORT=2525
|
||||
MAIL_USERNAME=null
|
||||
MAIL_PASSWORD=null
|
||||
MAIL_ENCRYPTION=null
|
||||
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_APP_KEY=
|
||||
PUSHER_APP_SECRET=
|
||||
PUSHER_APP_CLUSTER=mt1
|
||||
|
||||
SESSION_DOMAIN=".pixelfed.dev"
|
||||
SESSION_DOMAIN="${APP_DOMAIN}"
|
||||
SESSION_SECURE_COOKIE=true
|
||||
API_BASE="/api/1/"
|
||||
API_SEARCH="/api/search"
|
||||
|
||||
OPEN_REGISTRATION=true
|
||||
RECAPTCHA_ENABLED=false
|
||||
ENFORCE_EMAIL_VERIFICATION=true
|
||||
|
||||
MAX_PHOTO_SIZE=15000
|
||||
MAX_CAPTION_LENGTH=150
|
||||
MAX_ALBUM_LENGTH=4
|
||||
|
||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -12,3 +12,4 @@ Homestead.yaml
|
|||
npm-debug.log
|
||||
yarn-error.log
|
||||
.env
|
||||
.DS_Store
|
||||
|
|
38
Dockerfile
38
Dockerfile
|
@ -1,17 +1,31 @@
|
|||
FROM php:7.2-fpm-alpine
|
||||
FROM php:7.2.6-fpm-alpine
|
||||
|
||||
RUN apk add --no-cache git imagemagick \
|
||||
&& apk add --no-cache --virtual .build build-base autoconf imagemagick-dev libtool \
|
||||
&& docker-php-ext-install pdo_mysql \
|
||||
&& pecl install imagick \
|
||||
&& docker-php-ext-enable imagick \
|
||||
&& apk del --purge .build
|
||||
ARG COMPOSER_VERSION="1.6.5"
|
||||
ARG COMPOSER_CHECKSUM="67bebe9df9866a795078bb2cf21798d8b0214f2e0b2fd81f2e907a8ef0be3434"
|
||||
|
||||
RUN curl -sS https://getcomposer.org/installer | php \
|
||||
&& mv composer.phar /usr/local/bin/ \
|
||||
&& ln -s /usr/local/bin/composer.phar /usr/local/bin/composer
|
||||
RUN apk add --no-cache --virtual .build build-base autoconf imagemagick-dev libtool && \
|
||||
apk --no-cache add imagemagick git && \
|
||||
docker-php-ext-install pdo_mysql pcntl && \
|
||||
pecl install imagick && \
|
||||
docker-php-ext-enable imagick pcntl imagick && \
|
||||
curl -LsS https://getcomposer.org/download/${COMPOSER_VERSION}/composer.phar -o /tmp/composer.phar && \
|
||||
echo "${COMPOSER_CHECKSUM} /tmp/composer.phar" | sha256sum -c - && \
|
||||
install -m0755 -o root -g root /tmp/composer.phar /usr/bin/composer.phar && \
|
||||
ln -sf /usr/bin/composer.phar /usr/bin/composer && \
|
||||
rm /tmp/composer.phar && \
|
||||
apk --no-cache del --purge .build
|
||||
|
||||
COPY . /var/www/html/
|
||||
|
||||
WORKDIR /var/www/html
|
||||
COPY . .
|
||||
RUN composer install --prefer-source --no-interaction
|
||||
RUN install -d -m0755 -o www-data -g www-data \
|
||||
/var/www/html/storage \
|
||||
/var/www/html/storage/framework \
|
||||
/var/www/html/storage/logs \
|
||||
/var/www/html/storage/framework/sessions \
|
||||
/var/www/html/storage/framework/views \
|
||||
/var/www/html/storage/framework/cache && \
|
||||
composer install --prefer-source --no-interaction
|
||||
|
||||
VOLUME ["/var/www/html"]
|
||||
ENV PATH="~/.composer/vendor/bin:./vendor/bin:${PATH}"
|
||||
|
|
674
LICENSE
674
LICENSE
|
@ -1,21 +1,661 @@
|
|||
MIT License
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (c) 2018 Daniel Supernault
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
Preamble
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program 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.
|
||||
|
||||
This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
|
78
README.md
78
README.md
|
@ -1,4 +1,76 @@
|
|||
# PixelFed
|
||||
Federated Image Sharing
|
||||
# PixelFed: Federated Image Sharing
|
||||
|
||||
> This project is still in active development and not yet ready for use.
|
||||
PixelFed is a federated social image sharing platform, similar to instagram.
|
||||
Federation is done using the [ActivityPub](https://activitypub.rocks/) protocol,
|
||||
which is used by [Mastodon](http://joinmastodon.org/), [PeerTube](https://joinpeertube.org/en/),
|
||||
[Pleroma](https://pleroma.social/), and more. Through ActivityPub PixelFed can share
|
||||
and interact with these platforms, as well as other instances of PixelFed.
|
||||
|
||||
**_Please note this is alpha software, not recommended for production use,
|
||||
and federation is not supported yet._**
|
||||
|
||||
PixelFed is very early into the development stage. If you would like to have a
|
||||
permanent instance with minimal breakage, **do not use this software until
|
||||
there is a stable release**. The following setup instructions are intended for
|
||||
testing and development.
|
||||
|
||||
## Requirements
|
||||
- PHP >= 7.1.3 (7.2+ recommended for stable version)
|
||||
- MySQL, Postgres (MariaDB and sqlite are not supported yet)
|
||||
- Redis
|
||||
- Composer
|
||||
- GD or ImageMagick
|
||||
- OpenSSL PHP Extension
|
||||
- PDO PHP Extension
|
||||
- Mbstring PHP Extension
|
||||
- Tokenizer PHP Extension
|
||||
- XML PHP Extension
|
||||
- Ctype PHP Extension
|
||||
- JSON PHP Extension
|
||||
- JpegOptim
|
||||
- Optipng
|
||||
- Pngquant 2
|
||||
- SVGO
|
||||
- Gifsicle
|
||||
|
||||
## Installation
|
||||
|
||||
This guide assumes you have NGINX/Apache installed, along with the dependencies.
|
||||
Those will not be covered in these early docs.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/dansup/pixelfed.git
|
||||
cd pixelfed
|
||||
composer install
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
**Edit .env file with proper values**
|
||||
|
||||
```bash
|
||||
php artisan key:generate
|
||||
```
|
||||
|
||||
```bash
|
||||
php artisan storage:link
|
||||
php artisan migrate
|
||||
php artisan horizon
|
||||
php artisan serve --host=localhost --port=80
|
||||
```
|
||||
|
||||
Check your browser at http://localhost
|
||||
|
||||
## Communication
|
||||
|
||||
The ways you can communicate on the project are below. Before interacting, please
|
||||
read through the [Code Of Conduct](CODE_OF_CONDUCT.md).
|
||||
|
||||
* IRC: #pixelfed on irc.freenode.net ([#freenode_#pixelfed:matrix.org through
|
||||
Matrix](https://matrix.to/#/#freenode_#pixelfed:matrix.org)
|
||||
* Project on Mastodon: [@pixelfed@mastodon.social](https://mastodon.social/@pixelfed)
|
||||
* E-mail: [hello@pixelfed.org](mailto:hello@pixelfed.org)
|
||||
|
||||
## Support
|
||||
|
||||
The lead maintainer is on Patreon! You can become a Patron at
|
||||
https://www.patreon.com/dansup
|
|
@ -3,8 +3,16 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Avatar extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
}
|
||||
|
|
10
app/Bookmark.php
Normal file
10
app/Bookmark.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Bookmark extends Model
|
||||
{
|
||||
protected $fillable = ['profile_id', 'status_id'];
|
||||
}
|
47
app/Console/Commands/CatchUnoptimizedMedia.php
Normal file
47
app/Console/Commands/CatchUnoptimizedMedia.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use App\Media;
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
|
||||
class CatchUnoptimizedMedia extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'media:optimize';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Find and optimize media that has not yet been optimized.';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Media $media)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$medias = Media::whereNull('processed_at')->take(50)->get();
|
||||
foreach($medias as $media) {
|
||||
ImageOptimize::dispatch($media);
|
||||
}
|
||||
}
|
||||
}
|
60
app/Console/Commands/SeedFollows.php
Normal file
60
app/Console/Commands/SeedFollows.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\{Follower, Profile};
|
||||
use Illuminate\Console\Command;
|
||||
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||
|
||||
class SeedFollows extends Command
|
||||
{
|
||||
/**
|
||||
* The name and signature of the console command.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $signature = 'seed:follows';
|
||||
|
||||
/**
|
||||
* The console command description.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description = 'Seed follows for testing';
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$limit = 10000;
|
||||
|
||||
for ($i=0; $i < $limit; $i++) {
|
||||
try {
|
||||
$actor = Profile::inRandomOrder()->firstOrFail();
|
||||
$target = Profile::inRandomOrder()->firstOrFail();
|
||||
|
||||
$follow = new Follower;
|
||||
$follow->profile_id = $actor->id;
|
||||
$follow->following_id = $target->id;
|
||||
$follow->save();
|
||||
|
||||
FollowPipeline::dispatch($follow);
|
||||
} catch (Exception $e) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,8 +24,9 @@ class Kernel extends ConsoleKernel
|
|||
*/
|
||||
protected function schedule(Schedule $schedule)
|
||||
{
|
||||
// $schedule->command('inspire')
|
||||
// ->hourly();
|
||||
$schedule->command('media:optimize')
|
||||
->hourly();
|
||||
$schedule->command('horizon:snapshot')->everyFiveMinutes();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
15
app/EmailVerification.php
Normal file
15
app/EmailVerification.php
Normal file
|
@ -0,0 +1,15 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class EmailVerification extends Model
|
||||
{
|
||||
public function url()
|
||||
{
|
||||
$base = config('app.url');
|
||||
$path = '/i/confirm-email/' . $this->user_token . '/' . $this->random_token;
|
||||
return "{$base}{$path}";
|
||||
}
|
||||
}
|
|
@ -6,5 +6,32 @@ use Illuminate\Database\Eloquent\Model;
|
|||
|
||||
class Follower extends Model
|
||||
{
|
||||
//
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
}
|
||||
|
||||
public function target()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'following_id', 'id');
|
||||
}
|
||||
|
||||
public function toText()
|
||||
{
|
||||
$actorName = $this->actor->username;
|
||||
return "{$actorName} " . __('notification.startedFollowingYou');
|
||||
}
|
||||
|
||||
public function toHtml()
|
||||
{
|
||||
$actorName = $this->actor->username;
|
||||
$actorUrl = $this->actor->url();
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
|
||||
__('notification.startedFollowingYou');
|
||||
}
|
||||
}
|
||||
|
|
98
app/Http/Controllers/AccountController.php
Normal file
98
app/Http/Controllers/AccountController.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Carbon\Carbon;
|
||||
use App\Mail\ConfirmEmail;
|
||||
use Auth, DB, Cache, Mail, Redis;
|
||||
use App\{EmailVerification, Notification, Profile, User};
|
||||
|
||||
class AccountController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function notifications(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'page' => 'nullable|min:1|max:3'
|
||||
]);
|
||||
$profile = Auth::user()->profile;
|
||||
$timeago = Carbon::now()->subMonths(6);
|
||||
$notifications = Notification::whereProfileId($profile->id)
|
||||
->whereDate('created_at', '>', $timeago)
|
||||
->orderBy('id','desc')
|
||||
->take(30)
|
||||
->simplePaginate();
|
||||
|
||||
return view('account.activity', compact('profile', 'notifications'));
|
||||
}
|
||||
|
||||
public function verifyEmail(Request $request)
|
||||
{
|
||||
return view('account.verify_email');
|
||||
}
|
||||
|
||||
public function sendVerifyEmail(Request $request)
|
||||
{
|
||||
if(EmailVerification::whereUserId(Auth::id())->count() !== 0) {
|
||||
return redirect()->back()->with('status', 'A verification email has already been sent! Please check your email.');
|
||||
}
|
||||
|
||||
$user = User::whereNull('email_verified_at')->find(Auth::id());
|
||||
$utoken = hash('sha512', $user->id);
|
||||
$rtoken = str_random(40);
|
||||
|
||||
$verify = new EmailVerification;
|
||||
$verify->user_id = $user->id;
|
||||
$verify->email = $user->email;
|
||||
$verify->user_token = $utoken;
|
||||
$verify->random_token = $rtoken;
|
||||
$verify->save();
|
||||
|
||||
Mail::to($user->email)->send(new ConfirmEmail($verify));
|
||||
|
||||
return redirect()->back()->with('status', 'Email verification email sent!');
|
||||
}
|
||||
|
||||
public function confirmVerifyEmail(Request $request, $userToken, $randomToken)
|
||||
{
|
||||
$verify = EmailVerification::where(DB::raw('BINARY user_token'), $userToken)
|
||||
->where(DB::raw('BINARY random_token'), $randomToken)
|
||||
->firstOrFail();
|
||||
if(Auth::id() === $verify->user_id) {
|
||||
$user = User::find(Auth::id());
|
||||
$user->email_verified_at = Carbon::now();
|
||||
$user->save();
|
||||
return redirect('/timeline');
|
||||
}
|
||||
}
|
||||
|
||||
public function fetchNotifications($id)
|
||||
{
|
||||
$key = config('cache.prefix') . ":user.{$id}.notifications";
|
||||
$redis = Redis::connection();
|
||||
$notifications = $redis->lrange($key, 0, 30);
|
||||
if(empty($notifications)) {
|
||||
$notifications = Notification::whereProfileId($id)
|
||||
->orderBy('id','desc')->take(30)->get();
|
||||
} else {
|
||||
$notifications = $this->hydrateNotifications($notifications);
|
||||
}
|
||||
|
||||
return $notifications;
|
||||
}
|
||||
|
||||
public function hydrateNotifications($keys)
|
||||
{
|
||||
$prefix = 'notification.';
|
||||
$notifications = collect([]);
|
||||
foreach($keys as $key) {
|
||||
$notifications->push(Cache::get("{$prefix}{$key}"));
|
||||
}
|
||||
return $notifications;
|
||||
}
|
||||
}
|
|
@ -3,8 +3,42 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Comment, Like, Media, Profile, Status, User};
|
||||
|
||||
class AdminController extends Controller
|
||||
{
|
||||
//
|
||||
public function __construct()
|
||||
{
|
||||
return $this->middleware('admin');
|
||||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
return view('admin.home');
|
||||
}
|
||||
|
||||
public function users(Request $request)
|
||||
{
|
||||
$users = User::orderBy('id', 'desc')->paginate(10);
|
||||
return view('admin.users.home', compact('users'));
|
||||
}
|
||||
|
||||
|
||||
public function statuses(Request $request)
|
||||
{
|
||||
$statuses = Status::orderBy('id', 'desc')->paginate(10);
|
||||
return view('admin.statuses.home', compact('statuses'));
|
||||
}
|
||||
|
||||
public function showStatus(Request $request, $id)
|
||||
{
|
||||
$status = Status::findOrFail($id);
|
||||
return view('admin.statuses.show', compact('status'));
|
||||
}
|
||||
|
||||
public function media(Request $request)
|
||||
{
|
||||
$media = Status::whereHas('media')->orderby('id', 'desc')->paginate(12);
|
||||
return view('admin.media.home', compact('media'));
|
||||
}
|
||||
}
|
||||
|
|
32
app/Http/Controllers/ApiController.php
Normal file
32
app/Http/Controllers/ApiController.php
Normal file
|
@ -0,0 +1,32 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use App\Like;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ApiController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function hydrateLikes(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'min' => 'nullable|integer|min:1',
|
||||
'max' => 'nullable|integer',
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
|
||||
$likes = Like::whereProfileId($profile->id)
|
||||
->orderBy('id', 'desc')
|
||||
->take(1000)
|
||||
->pluck('status_id');
|
||||
|
||||
return response()->json($likes);
|
||||
}
|
||||
}
|
|
@ -36,4 +36,24 @@ class LoginController extends Controller
|
|||
{
|
||||
$this->middleware('guest')->except('logout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the user login request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return void
|
||||
*/
|
||||
public function validateLogin($request)
|
||||
{
|
||||
$rules = [
|
||||
$this->username() => 'required|string',
|
||||
'password' => 'required|string',
|
||||
];
|
||||
|
||||
if(config('pixelfed.recaptcha')) {
|
||||
$rules['g-recaptcha-response'] = 'required|recaptcha';
|
||||
}
|
||||
|
||||
$this->validate($request, $rules);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -52,12 +52,19 @@ class RegisterController extends Controller
|
|||
{
|
||||
$this->validateUsername($data['username']);
|
||||
|
||||
return Validator::make($data, [
|
||||
|
||||
$rules = [
|
||||
'name' => 'required|string|max:255',
|
||||
'username' => 'required|alpha_dash|min:2|max:15|unique:users',
|
||||
'email' => 'required|string|email|max:255|unique:users',
|
||||
'password' => 'required|string|min:6|confirmed',
|
||||
]);
|
||||
];
|
||||
|
||||
if(config('pixelfed.recaptcha')) {
|
||||
$rules['g-recaptcha-response'] = 'required|recaptcha';
|
||||
}
|
||||
|
||||
return Validator::make($data, $rules);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
10
app/Http/Controllers/AvatarController.php
Normal file
10
app/Http/Controllers/AvatarController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AvatarController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
38
app/Http/Controllers/BookmarkController.php
Normal file
38
app/Http/Controllers/BookmarkController.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use App\{Bookmark, Profile, Status};
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BookmarkController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
$bookmark = Bookmark::firstOrCreate(
|
||||
['status_id' => $status->id], ['profile_id' => $profile->id]
|
||||
);
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Bookmark saved!'];
|
||||
} else {
|
||||
$response = redirect()->back();
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
|
@ -3,40 +3,52 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth;
|
||||
use App\Jobs\CommentPipeline\CommentPipeline;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
use Auth, Hashids;
|
||||
use App\{Comment, Profile, Status};
|
||||
use Vinkla\Hashids\Facades\Hashids;
|
||||
|
||||
class CommentController extends Controller
|
||||
{
|
||||
|
||||
public function show(Request $request, $username, int $id, int $cid)
|
||||
{
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$status = Status::whereProfileId($user->id)->whereInReplyToId($id)->findOrFail($cid);
|
||||
return view('status.reply', compact('user', 'status'));
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
if(Auth::check() === false) { abort(403); }
|
||||
$this->validate($request, [
|
||||
'item' => 'required|alpha_num',
|
||||
'item' => 'required|integer',
|
||||
'comment' => 'required|string|max:500'
|
||||
]);
|
||||
|
||||
try {
|
||||
$statusId = Hashids::decode($request->item)[0];
|
||||
} catch (Exception $e) {
|
||||
abort(500);
|
||||
}
|
||||
$comment = $request->input('comment');
|
||||
$statusId = $request->item;
|
||||
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
$status = Status::findOrFail($statusId);
|
||||
|
||||
$comment = new Comment;
|
||||
$comment->profile_id = $profile->id;
|
||||
$comment->user_id = $user->id;
|
||||
$comment->status_id = $status->id;
|
||||
$comment->comment = e($request->comment);
|
||||
$comment->rendered = e($request->comment);
|
||||
$comment->is_remote = false;
|
||||
$comment->entities = null;
|
||||
$comment->save();
|
||||
$reply = new Status();
|
||||
$reply->profile_id = $profile->id;
|
||||
$reply->caption = e($comment);
|
||||
$reply->rendered = $comment;
|
||||
$reply->in_reply_to_id = $status->id;
|
||||
$reply->in_reply_to_profile_id = $status->profile_id;
|
||||
$reply->save();
|
||||
|
||||
return redirect($status->url());
|
||||
NewStatusPipeline::dispatch($reply, false);
|
||||
CommentPipeline::dispatch($status, $reply);
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Comment saved', 'username' => $profile->username, 'url' => $reply->url(), 'profile' => $profile->url(), 'comment' => $reply->caption];
|
||||
} else {
|
||||
$response = redirect($status->url());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
31
app/Http/Controllers/DiscoverController.php
Normal file
31
app/Http/Controllers/DiscoverController.php
Normal file
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Hashtag, Follower, Profile, Status, StatusHashtag};
|
||||
use Auth;
|
||||
|
||||
class DiscoverController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
||||
$people = Profile::inRandomOrder()->where('id', '!=', Auth::user()->profile->id)->whereNotIn('id', $following)->take(3)->get();
|
||||
$posts = Status::whereHas('media')->where('profile_id', '!=', Auth::user()->profile->id)->whereNotIn('profile_id', $following)->orderBy('created_at', 'desc')->take('21')->get();
|
||||
return view('discover.home', compact('people', 'posts'));
|
||||
}
|
||||
|
||||
public function showTags(Request $request, $hashtag)
|
||||
{
|
||||
$tag = Hashtag::whereSlug($hashtag)->firstOrFail();
|
||||
$posts = $tag->posts()->has('media')->orderBy('id','desc')->paginate(12);
|
||||
$count = $tag->posts()->has('media')->orderBy('id','desc')->count();
|
||||
return view('discover.tags.show', compact('tag', 'posts', 'count'));
|
||||
}
|
||||
}
|
138
app/Http/Controllers/FederationController.php
Normal file
138
app/Http/Controllers/FederationController.php
Normal file
|
@ -0,0 +1,138 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use App\Profile;
|
||||
use League\Fractal;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use App\Transformer\ActivityPub\{
|
||||
ProfileOutbox,
|
||||
ProfileTransformer
|
||||
};
|
||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowPipeline;
|
||||
|
||||
class FederationController extends Controller
|
||||
{
|
||||
public function authCheck()
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
abort(403);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
public function remoteFollow()
|
||||
{
|
||||
$this->authCheck();
|
||||
return view('federation.remotefollow');
|
||||
}
|
||||
|
||||
public function remoteFollowStore(Request $request)
|
||||
{
|
||||
$this->authCheck();
|
||||
$this->validate($request, [
|
||||
'url' => 'required|string'
|
||||
]);
|
||||
|
||||
if(config('pixelfed.remote_follow_enabled') !== true) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$follower = Auth::user()->profile;
|
||||
$url = $request->input('url');
|
||||
|
||||
RemoteFollowPipeline::dispatch($follower, $url);
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
public function nodeinfoWellKnown()
|
||||
{
|
||||
$res = [
|
||||
'links' => [
|
||||
[
|
||||
'href' => config('pixelfed.nodeinfo.url'),
|
||||
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0'
|
||||
]
|
||||
]
|
||||
];
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
public function nodeinfo()
|
||||
{
|
||||
$res = [
|
||||
'metadata' => [
|
||||
'nodeName' => config('app.name'),
|
||||
'software' => [
|
||||
'homepage' => 'https://pixelfed.org',
|
||||
'github' => 'https://github.com/pixelfed',
|
||||
'follow' => 'https://mastodon.social/@pixelfed'
|
||||
],
|
||||
/*
|
||||
TODO: Custom Features for Trending
|
||||
'customFeatures' => [
|
||||
'trending' => [
|
||||
'description' => 'Trending API for federated discovery',
|
||||
'api' => [
|
||||
'url' => null,
|
||||
'docs' => null
|
||||
],
|
||||
],
|
||||
],
|
||||
*/
|
||||
],
|
||||
'openRegistrations' => config('pixelfed.open_registration'),
|
||||
'protocols' => [
|
||||
'activitypub'
|
||||
],
|
||||
'services' => [
|
||||
'inbound' => [],
|
||||
'outbound' => []
|
||||
],
|
||||
'software' => [
|
||||
'name' => 'pixelfed',
|
||||
'version' => config('pixelfed.version')
|
||||
],
|
||||
'usage' => [
|
||||
'localPosts' => \App\Status::whereLocal(true)->count(),
|
||||
'users' => [
|
||||
'total' => \App\User::count()
|
||||
]
|
||||
],
|
||||
'version' => '2.0'
|
||||
];
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
|
||||
|
||||
public function webfinger(Request $request)
|
||||
{
|
||||
$this->validate($request, ['resource'=>'required']);
|
||||
$resource = $request->input('resource');
|
||||
$parsed = Nickname::normalizeProfileUrl($resource);
|
||||
$username = $parsed['username'];
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$webfinger = (new Webfinger($user))->generate();
|
||||
return response()->json($webfinger);
|
||||
}
|
||||
|
||||
public function userOutbox(Request $request, $username)
|
||||
{
|
||||
if(config('pixelfed.activitypub_enabled') == false) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$user = Profile::whereNull('remote_url')->whereUsername($username)->firstOrFail();
|
||||
$timeline = $user->statuses()->orderBy('created_at','desc')->paginate(10);
|
||||
$fractal = new Fractal\Manager();
|
||||
$resource = new Fractal\Resource\Item($user, new ProfileOutbox);
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
return response()->json($res['data']);
|
||||
}
|
||||
|
||||
}
|
|
@ -2,9 +2,41 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use App\{Follower, Profile};
|
||||
use Illuminate\Http\Request;
|
||||
use App\Jobs\FollowPipeline\FollowPipeline;
|
||||
|
||||
class FollowerController extends Controller
|
||||
{
|
||||
//
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer',
|
||||
]);
|
||||
|
||||
$user = Auth::user()->profile;
|
||||
$target = Profile::where('id', '!=', $user->id)->findOrFail($request->input('item'));
|
||||
|
||||
$isFollowing = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->count();
|
||||
|
||||
if($isFollowing == 0) {
|
||||
$follower = new Follower;
|
||||
$follower->profile_id = $user->id;
|
||||
$follower->following_id = $target->id;
|
||||
$follower->save();
|
||||
FollowPipeline::dispatch($follower);
|
||||
} else {
|
||||
$follower = Follower::whereProfileId($user->id)->whereFollowingId($target->id)->firstOrFail();
|
||||
$follower->delete();
|
||||
}
|
||||
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
||||
|
|
10
app/Http/Controllers/HashtagController.php
Normal file
10
app/Http/Controllers/HashtagController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class HashtagController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
10
app/Http/Controllers/ImportDataController.php
Normal file
10
app/Http/Controllers/ImportDataController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class ImportDataController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
|
@ -3,8 +3,48 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use Auth, Hashids;
|
||||
use App\{Like, Profile, Status, User};
|
||||
use App\Jobs\LikePipeline\LikePipeline;
|
||||
|
||||
class LikeController extends Controller
|
||||
{
|
||||
//
|
||||
public function __construct()
|
||||
{
|
||||
$this->middleware('auth');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'item' => 'required|integer',
|
||||
]);
|
||||
|
||||
$profile = Auth::user()->profile;
|
||||
$status = Status::withCount('likes')->findOrFail($request->input('item'));
|
||||
|
||||
$count = $status->likes_count;
|
||||
|
||||
if($status->likes()->whereProfileId($profile->id)->count() !== 0) {
|
||||
$like = Like::whereProfileId($profile->id)->whereStatusId($status->id)->firstOrFail();
|
||||
$like->delete();
|
||||
$count--;
|
||||
} else {
|
||||
$like = new Like;
|
||||
$like->profile_id = $profile->id;
|
||||
$like->status_id = $status->id;
|
||||
$like->save();
|
||||
$count++;
|
||||
}
|
||||
|
||||
LikePipeline::dispatch($like);
|
||||
|
||||
if($request->ajax()) {
|
||||
$response = ['code' => 200, 'msg' => 'Like saved', 'count' => $count];
|
||||
} else {
|
||||
$response = redirect($status->url());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
|
10
app/Http/Controllers/NotificationController.php
Normal file
10
app/Http/Controllers/NotificationController.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class NotificationController extends Controller
|
||||
{
|
||||
//
|
||||
}
|
|
@ -3,14 +3,97 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Profile};
|
||||
use Auth, Cache;
|
||||
use App\{Follower, Profile, User};
|
||||
use League\Fractal;
|
||||
use App\Util\Lexer\Nickname;
|
||||
use App\Util\Webfinger\Webfinger;
|
||||
use App\Transformer\ActivityPub\{
|
||||
ProfileOutbox,
|
||||
ProfileTransformer
|
||||
};
|
||||
|
||||
class ProfileController extends Controller
|
||||
{
|
||||
public function show(Request $request, $username)
|
||||
{
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$timeline = $user->statuses()->orderBy('id','desc')->paginate(10);
|
||||
return view('profile.show', compact('user', 'timeline'));
|
||||
|
||||
$mimes = [
|
||||
'application/activity+json',
|
||||
'application/ld+json',
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'
|
||||
];
|
||||
|
||||
if(in_array($request->header('accept'), $mimes) && config('pixelfed.activitypub_enabled')) {
|
||||
return $this->showActivityPub($request, $user);
|
||||
}
|
||||
|
||||
// TODO: refactor this mess
|
||||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
$timeline = $user->statuses()
|
||||
->whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->orderBy('id','desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(21);
|
||||
|
||||
return view('profile.show', compact('user', 'owner', 'is_following', 'is_admin', 'timeline'));
|
||||
}
|
||||
|
||||
public function showActivityPub(Request $request, $user)
|
||||
{
|
||||
$fractal = new Fractal\Manager();
|
||||
$resource = new Fractal\Resource\Item($user, new ProfileTransformer);
|
||||
$res = $fractal->createData($resource)->toArray();
|
||||
return response()->json($res['data']);
|
||||
}
|
||||
|
||||
public function showAtomFeed(Request $request, $user)
|
||||
{
|
||||
$profile = Profile::whereUsername($user)->firstOrFail();
|
||||
$items = $profile->statuses()->orderBy('created_at', 'desc')->take(10)->get();
|
||||
return response()->view('atom.user', compact('profile', 'items'))
|
||||
->header('Content-Type', 'application/atom+xml');
|
||||
}
|
||||
|
||||
public function followers(Request $request, $username)
|
||||
{
|
||||
$profile = Profile::whereUsername($username)->firstOrFail();
|
||||
// TODO: fix $profile/$user mismatch in profile & follower templates
|
||||
$user = $profile;
|
||||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$followers = $profile->followers()->orderBy('created_at','desc')->simplePaginate(12);
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.followers', compact('user', 'profile', 'followers', 'owner', 'is_following', 'is_admin'));
|
||||
}
|
||||
|
||||
public function following(Request $request, $username)
|
||||
{
|
||||
$profile = Profile::whereUsername($username)->firstOrFail();
|
||||
// TODO: fix $profile/$user mismatch in profile & follower templates
|
||||
$user = $profile;
|
||||
$owner = Auth::check() && Auth::id() === $user->user_id;
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$following = $profile->following()->orderBy('created_at','desc')->simplePaginate(12);
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.following', compact('user', 'profile', 'following', 'owner', 'is_following', 'is_admin'));
|
||||
}
|
||||
|
||||
public function savedBookmarks(Request $request, $username)
|
||||
{
|
||||
if(Auth::check() === false || $username !== Auth::user()->username) {
|
||||
abort(403);
|
||||
}
|
||||
$user = Auth::user()->profile;
|
||||
$owner = true;
|
||||
$following = false;
|
||||
$timeline = $user->bookmarks()->withCount(['likes','comments'])->orderBy('created_at','desc')->simplePaginate(10);
|
||||
$is_following = ($owner == false && Auth::check()) ? $user->followedBy(Auth::user()->profile) : false;
|
||||
$is_admin = is_null($user->domain) ? $user->user->is_admin : false;
|
||||
return view('profile.show', compact('user', 'owner', 'following', 'timeline', 'is_following', 'is_admin'));
|
||||
}
|
||||
}
|
||||
|
|
42
app/Http/Controllers/SearchController.php
Normal file
42
app/Http/Controllers/SearchController.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\{Hashtag, Profile};
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
class SearchController extends Controller
|
||||
{
|
||||
public function searchAPI(Request $request, $tag)
|
||||
{
|
||||
$res = Cache::remember('api:search:tag:' . $tag, 1440, function() use($tag) {
|
||||
$res = Hashtag::where('slug', 'like', '%'.$tag.'%')->get();
|
||||
$tags = $res->map(function($item, $key) {
|
||||
return [
|
||||
'count' => $item->posts()->count(),
|
||||
'url' => $item->url(),
|
||||
'type' => 'hashtag',
|
||||
'value' => $item->name,
|
||||
'tokens' => explode('-', $item->name),
|
||||
'name' => null
|
||||
];
|
||||
});
|
||||
$res = Profile::where('username', 'like', '%'.$tag.'%')->get();
|
||||
$profiles = $res->map(function($item, $key) {
|
||||
return [
|
||||
'count' => 0,
|
||||
'url' => $item->url(),
|
||||
'type' => 'profile',
|
||||
'value' => $item->username,
|
||||
'tokens' => [$item->username],
|
||||
'name' => $item->name
|
||||
];
|
||||
});
|
||||
$tags = $tags->push($profiles[0]);
|
||||
return $tags;
|
||||
});
|
||||
|
||||
return response()->json($res);
|
||||
}
|
||||
}
|
131
app/Http/Controllers/SettingsController.php
Normal file
131
app/Http/Controllers/SettingsController.php
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Profile, User};
|
||||
use Auth;
|
||||
|
||||
class SettingsController extends Controller
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
return $this->middleware('auth');
|
||||
}
|
||||
|
||||
public function home()
|
||||
{
|
||||
return view('settings.home');
|
||||
}
|
||||
|
||||
public function homeUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:30',
|
||||
'bio' => 'nullable|string|max:125'
|
||||
]);
|
||||
|
||||
$changes = false;
|
||||
$name = $request->input('name');
|
||||
$bio = $request->input('bio');
|
||||
$user = Auth::user();
|
||||
$profile = $user->profile;
|
||||
|
||||
if($profile->name != $name) {
|
||||
$changes = true;
|
||||
$user->name = $name;
|
||||
$profile->name = $name;
|
||||
}
|
||||
|
||||
if($profile->bio != $bio) {
|
||||
$changes = true;
|
||||
$profile->bio = $bio;
|
||||
}
|
||||
|
||||
if($changes === true) {
|
||||
$user->save();
|
||||
$profile->save();
|
||||
return redirect('/settings/home')->with('status', 'Profile successfully updated!');
|
||||
}
|
||||
|
||||
return redirect('/settings/home');
|
||||
}
|
||||
|
||||
public function password()
|
||||
{
|
||||
return view('settings.password');
|
||||
}
|
||||
|
||||
public function passwordUpdate(Request $request)
|
||||
{
|
||||
$this->validate($request, [
|
||||
'current' => 'required|string',
|
||||
'password' => 'required|string',
|
||||
'password_confirmation' => 'required|string',
|
||||
]);
|
||||
|
||||
$current = $request->input('current');
|
||||
$new = $request->input('password');
|
||||
$confirm = $request->input('password_confirmation');
|
||||
|
||||
$user = Auth::user();
|
||||
|
||||
if(password_verify($current, $user->password) && $new === $confirm) {
|
||||
$user->password = bcrypt($new);
|
||||
$user->save();
|
||||
|
||||
return redirect('/settings/home')->with('status', 'Password successfully updated!');
|
||||
}
|
||||
return redirect('/settings/home')->with('error', 'There was an error with your request!');
|
||||
}
|
||||
|
||||
public function email()
|
||||
{
|
||||
return view('settings.email');
|
||||
}
|
||||
|
||||
public function avatar()
|
||||
{
|
||||
return view('settings.avatar');
|
||||
}
|
||||
|
||||
public function notifications()
|
||||
{
|
||||
return view('settings.notifications');
|
||||
}
|
||||
|
||||
public function privacy()
|
||||
{
|
||||
return view('settings.privacy');
|
||||
}
|
||||
|
||||
public function security()
|
||||
{
|
||||
return view('settings.security');
|
||||
}
|
||||
|
||||
public function applications()
|
||||
{
|
||||
return view('settings.applications');
|
||||
}
|
||||
|
||||
public function dataExport()
|
||||
{
|
||||
return view('settings.dataexport');
|
||||
}
|
||||
|
||||
public function dataImport()
|
||||
{
|
||||
return view('settings.import.home');
|
||||
}
|
||||
|
||||
public function dataImportInstagram()
|
||||
{
|
||||
return view('settings.import.ig');
|
||||
}
|
||||
|
||||
public function developers()
|
||||
{
|
||||
return view('settings.developers');
|
||||
}
|
||||
}
|
18
app/Http/Controllers/SiteController.php
Normal file
18
app/Http/Controllers/SiteController.php
Normal file
|
@ -0,0 +1,18 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SiteController extends Controller
|
||||
{
|
||||
public function changeLocale(Request $request, $locale)
|
||||
{
|
||||
if(!App::isLocale($locale)) {
|
||||
return redirect()->back();
|
||||
}
|
||||
App::setLocale($locale);
|
||||
return redirect()->back();
|
||||
}
|
||||
}
|
|
@ -2,23 +2,24 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use Auth;
|
||||
use Auth, Cache;
|
||||
use App\Jobs\StatusPipeline\{NewStatusPipeline, StatusDelete};
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Media, Profile, Status, User};
|
||||
use Vinkla\Hashids\Facades\Hashids;
|
||||
|
||||
class StatusController extends Controller
|
||||
{
|
||||
public function show(Request $request, $username, $hashid)
|
||||
public function show(Request $request, $username, int $id)
|
||||
{
|
||||
$user = Profile::whereUsername($username)->firstOrFail();
|
||||
$id = Hashids::decode($hashid);
|
||||
if(empty($id)) {
|
||||
abort(404);
|
||||
} else {
|
||||
$id = $id[0];
|
||||
$status = Status::whereProfileId($user->id)
|
||||
->withCount(['likes', 'comments', 'media'])
|
||||
->findOrFail($id);
|
||||
if(!$status->media_path && $status->in_reply_to_id) {
|
||||
return redirect($status->url());
|
||||
}
|
||||
$status = Status::whereProfileId($user->id)->findOrFail($id);
|
||||
return view('status.show', compact('user', 'status'));
|
||||
}
|
||||
|
||||
|
@ -32,29 +33,73 @@ class StatusController extends Controller
|
|||
$user = Auth::user();
|
||||
|
||||
$this->validate($request, [
|
||||
'photo' => 'required|image|max:8000',
|
||||
'caption' => 'string|max:150'
|
||||
'photo.*' => 'required|mimes:jpeg,png,bmp,gif|max:' . config('pixelfed.max_photo_size'),
|
||||
'caption' => 'string|max:' . config('pixelfed.max_caption_length'),
|
||||
'cw' => 'nullable|string',
|
||||
'filter_class' => 'nullable|string',
|
||||
'filter_name' => 'nullable|string',
|
||||
]);
|
||||
$monthHash = hash('sha1',date('Y').date('m'));
|
||||
|
||||
if(count($request->file('photo')) > config('pixelfed.max_album_length')) {
|
||||
return redirect()->back()->with('error', 'Too many files, max limit per post: ' . config('pixelfed.max_album_length'));
|
||||
}
|
||||
|
||||
$cw = $request->filled('cw') && $request->cw == 'on' ? true : false;
|
||||
$monthHash = hash('sha1', date('Y') . date('m'));
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$path = $request->photo->store($storagePath);
|
||||
$profile = $user->profile;
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->caption = $request->caption;
|
||||
$status->caption = strip_tags($request->caption);
|
||||
$status->is_nsfw = $cw;
|
||||
|
||||
$status->save();
|
||||
|
||||
$photos = $request->file('photo');
|
||||
$order = 1;
|
||||
foreach ($photos as $k => $v) {
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
$path = $v->store($storagePath);
|
||||
$media = new Media;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $profile->id;
|
||||
$media->user_id = $user->id;
|
||||
$media->media_path = $path;
|
||||
$media->size = $request->file('photo')->getClientSize();
|
||||
$media->mime = $request->file('photo')->getClientMimeType();
|
||||
$media->size = $v->getClientSize();
|
||||
$media->mime = $v->getClientMimeType();
|
||||
$media->filter_class = $request->input('filter_class');
|
||||
$media->filter_name = $request->input('filter_name');
|
||||
$media->order = $order;
|
||||
$media->save();
|
||||
ImageOptimize::dispatch($media);
|
||||
$order++;
|
||||
}
|
||||
|
||||
NewStatusPipeline::dispatch($status);
|
||||
|
||||
// TODO: Send to subscribers
|
||||
|
||||
return redirect($status->url());
|
||||
}
|
||||
|
||||
public function delete(Request $request)
|
||||
{
|
||||
if(!Auth::check()) {
|
||||
abort(403);
|
||||
}
|
||||
|
||||
$this->validate($request, [
|
||||
'type' => 'required|string',
|
||||
'item' => 'required|integer|min:1'
|
||||
]);
|
||||
|
||||
$status = Status::findOrFail($request->input('item'));
|
||||
|
||||
if($status->profile_id === Auth::user()->profile->id || Auth::user()->is_admin == true) {
|
||||
StatusDelete::dispatch($status);
|
||||
}
|
||||
|
||||
return redirect(Auth::user()->url());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,8 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
use App\{Status, User};
|
||||
use Auth;
|
||||
use App\{Follower, Status, User};
|
||||
|
||||
class TimelineController extends Controller
|
||||
{
|
||||
|
@ -14,7 +15,27 @@ class TimelineController extends Controller
|
|||
|
||||
public function personal()
|
||||
{
|
||||
$timeline = Status::orderBy('id','desc')->paginate(10);
|
||||
// TODO: Use redis for timelines
|
||||
$following = Follower::whereProfileId(Auth::user()->profile->id)->pluck('following_id');
|
||||
$following->push(Auth::user()->profile->id);
|
||||
$timeline = Status::whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->whereIn('profile_id', $following)
|
||||
->orderBy('id','desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(10);
|
||||
return view('timeline.personal', compact('timeline'));
|
||||
}
|
||||
|
||||
public function local()
|
||||
{
|
||||
// TODO: Use redis for timelines
|
||||
$timeline = Status::whereHas('media')
|
||||
->whereNull('in_reply_to_id')
|
||||
->orderBy('id','desc')
|
||||
->withCount(['comments', 'likes'])
|
||||
->simplePaginate(10);
|
||||
return view('timeline.public', compact('timeline'));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -60,5 +60,6 @@ class Kernel extends HttpKernel
|
|||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'validemail' => \App\Http\Middleware\EmailVerificationCheck::class,
|
||||
];
|
||||
}
|
||||
|
|
28
app/Http/Middleware/EmailVerificationCheck.php
Normal file
28
app/Http/Middleware/EmailVerificationCheck.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use Auth, Closure;
|
||||
|
||||
class EmailVerificationCheck
|
||||
{
|
||||
/**
|
||||
* Handle an incoming request.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param \Closure $next
|
||||
* @return mixed
|
||||
*/
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
if($request->user() &&
|
||||
config('pixelfed.enforce_email_verification') &&
|
||||
is_null($request->user()->email_verified_at) &&
|
||||
!$request->is('i/verify-email') && !$request->is('login') &&
|
||||
!$request->is('i/confirm-email/*')
|
||||
) {
|
||||
return redirect('/i/verify-email');
|
||||
}
|
||||
return $next($request);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
namespace App\Jobs\AvatarPipeline;
|
||||
|
||||
use App\{Avatar, User};
|
||||
use App\{Avatar, Profile};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
|
@ -18,15 +18,15 @@ class CreateAvatar implements ShouldQueue
|
|||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $user;
|
||||
protected $profile;
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(User $user)
|
||||
public function __construct(Profile $profile)
|
||||
{
|
||||
$this->user = $user;
|
||||
$this->profile = $profile;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -36,19 +36,25 @@ class CreateAvatar implements ShouldQueue
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$username = $this->user->profile->username;
|
||||
$email = $this->user->email;
|
||||
$profile = $this->profile;
|
||||
$username = $profile->username;
|
||||
|
||||
$generator = new RingsGenerator();
|
||||
$generator->setBackgroundColor(Color::parseHex('#FFFFFF'));
|
||||
|
||||
$identicon = new Identicon(new HashPreprocessor('sha1'), $generator);
|
||||
$identicon = new Identicon(new HashPreprocessor('sha256'), $generator);
|
||||
|
||||
$hash = $username . str_random(12) . $email;
|
||||
$hash = $username . str_random(12);
|
||||
$icon = $identicon->getIcon($hash);
|
||||
|
||||
try {
|
||||
$prefix = $this->user->profile->id;
|
||||
|
||||
$baseDir = storage_path('app/public/avatars');
|
||||
if(!is_dir($baseDir)) {
|
||||
mkdir($baseDir);
|
||||
}
|
||||
|
||||
$prefix = $profile->id;
|
||||
$padded = str_pad($prefix, 12, 0, STR_PAD_LEFT);
|
||||
$parts = str_split($padded, 3);
|
||||
foreach($parts as $k => $part) {
|
||||
|
@ -92,7 +98,7 @@ class CreateAvatar implements ShouldQueue
|
|||
}
|
||||
|
||||
$avatar = new Avatar;
|
||||
$avatar->profile_id = $this->user->profile->id;
|
||||
$avatar->profile_id = $profile->id;
|
||||
$avatar->media_path = $path;
|
||||
$avatar->thumb_path = $path;
|
||||
$avatar->change_count = 0;
|
||||
|
|
73
app/Jobs/CommentPipeline/CommentPipeline.php
Normal file
73
app/Jobs/CommentPipeline/CommentPipeline.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\CommentPipeline;
|
||||
|
||||
use Cache, Log, Redis;
|
||||
use App\{Like, Notification, Status};
|
||||
use App\Util\Lexer\Hashtag as HashtagLexer;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class CommentPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $comment;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status, Status $comment)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->comment = $comment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$comment = $this->comment;
|
||||
|
||||
$target = $status->profile;
|
||||
$actor = $comment->profile;
|
||||
|
||||
if($actor->id === $target->id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'comment';
|
||||
$notification->message = $comment->replyToText();
|
||||
$notification->rendered = $comment->replyToHtml();
|
||||
$notification->item_id = $comment->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
|
||||
Cache::forever('notification.' . $notification->id, $notification);
|
||||
|
||||
$redis = Redis::connection();
|
||||
|
||||
$nkey = config('cache.prefix').':user.' . $target->id . '.notifications';
|
||||
$redis->lpush($nkey, $notification->id);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
64
app/Jobs/FollowPipeline/FollowPipeline.php
Normal file
64
app/Jobs/FollowPipeline/FollowPipeline.php
Normal file
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\FollowPipeline;
|
||||
|
||||
use Cache, Log, Redis;
|
||||
use App\{Like, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Jobs\FollowPipeline\FollowDiscover;
|
||||
|
||||
class FollowPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $follower;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($follower)
|
||||
{
|
||||
$this->follower = $follower;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$follower = $this->follower;
|
||||
$actor = $follower->actor;
|
||||
$target = $follower->target;
|
||||
|
||||
try {
|
||||
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $target->id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'follow';
|
||||
$notification->message = $follower->toText();
|
||||
$notification->rendered = $follower->toHtml();
|
||||
$notification->item_id = $target->id;
|
||||
$notification->item_type = "App\Profile";
|
||||
$notification->save();
|
||||
|
||||
Cache::forever('notification.' . $notification->id, $notification);
|
||||
|
||||
$redis = Redis::connection();
|
||||
|
||||
$nkey = config('cache.prefix').':user.' . $target->id . '.notifications';
|
||||
$redis->lpush($nkey, $notification->id);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -38,7 +38,10 @@ class ImageUpdate implements ShouldQueue
|
|||
$thumb = storage_path('app/'. $media->thumbnail_path);
|
||||
try {
|
||||
ImageOptimizer::optimize($thumb);
|
||||
if($media->mime !== 'image/gif')
|
||||
{
|
||||
ImageOptimizer::optimize($path);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
return;
|
||||
}
|
||||
|
|
73
app/Jobs/LikePipeline/LikePipeline.php
Normal file
73
app/Jobs/LikePipeline/LikePipeline.php
Normal file
|
@ -0,0 +1,73 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\LikePipeline;
|
||||
|
||||
use Cache, Log, Redis;
|
||||
use App\{Like, Notification};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class LikePipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $like;
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Like $like)
|
||||
{
|
||||
$this->like = $like;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$like = $this->like;
|
||||
|
||||
$status = $this->like->status;
|
||||
$actor = $this->like->actor;
|
||||
|
||||
$exists = Notification::whereProfileId($status->profile_id)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('like')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->count();
|
||||
|
||||
if($actor->id === $status->profile_id || $exists !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $status->profile_id;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'like';
|
||||
$notification->message = $like->toText();
|
||||
$notification->rendered = $like->toHtml();
|
||||
$notification->item_id = $status->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
|
||||
Cache::forever('notification.' . $notification->id, $notification);
|
||||
|
||||
$redis = Redis::connection();
|
||||
$key = config('cache.prefix').':user.' . $status->profile_id . '.notifications';
|
||||
$redis->lpush($key, $notification->id);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error($e);
|
||||
}
|
||||
}
|
||||
}
|
72
app/Jobs/MentionPipeline/MentionPipeline.php
Normal file
72
app/Jobs/MentionPipeline/MentionPipeline.php
Normal file
|
@ -0,0 +1,72 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\MentionPipeline;
|
||||
|
||||
use Cache, Log, Redis;
|
||||
use App\{Mention, Notification, Profile, Status};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class MentionPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $mention;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status, Mention $mention)
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->mention = $mention;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
|
||||
$status = $this->status;
|
||||
$mention = $this->mention;
|
||||
$actor = $this->status->profile;
|
||||
$target = $this->mention->profile_id;
|
||||
|
||||
$exists = Notification::whereProfileId($target)
|
||||
->whereActorId($actor->id)
|
||||
->whereAction('mention')
|
||||
->whereItemId($status->id)
|
||||
->whereItemType('App\Status')
|
||||
->count();
|
||||
|
||||
if($actor->id === $target || $exists !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
$notification = new Notification;
|
||||
$notification->profile_id = $target;
|
||||
$notification->actor_id = $actor->id;
|
||||
$notification->action = 'mention';
|
||||
$notification->message = $mention->toText();
|
||||
$notification->rendered = $mention->toHtml();
|
||||
$notification->item_id = $status->id;
|
||||
$notification->item_type = "App\Status";
|
||||
$notification->save();
|
||||
|
||||
} catch (Exception $e) {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
226
app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php
Normal file
226
app/Jobs/RemoteFollowPipeline/RemoteFollowImportRecent.php
Normal file
|
@ -0,0 +1,226 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\RemoteFollowPipeline;
|
||||
|
||||
use Zttp\Zttp;
|
||||
use Log, Storage;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\File;
|
||||
use App\{Media, Profile, Status};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Jobs\StatusPipeline\NewStatusPipeline;
|
||||
|
||||
class RemoteFollowImportRecent implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $actor;
|
||||
protected $profile;
|
||||
protected $outbox;
|
||||
protected $mediaCount;
|
||||
protected $cursor;
|
||||
protected $nextUrl;
|
||||
protected $supported;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($actorObject, $profile)
|
||||
{
|
||||
$this->actor = $actorObject;
|
||||
$this->profile = $profile;
|
||||
$this->cursor = 1;
|
||||
$this->mediaCount = 0;
|
||||
$this->supported = [
|
||||
'image/jpg',
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$outbox = $this->fetchOutbox();
|
||||
}
|
||||
|
||||
public function fetchOutbox($url = false)
|
||||
{
|
||||
Log::info(json_encode($url));
|
||||
$url = ($url == false) ? $this->actor['outbox'] : $url;
|
||||
|
||||
$response = Zttp::withHeaders([
|
||||
'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org'
|
||||
])->get($url);
|
||||
|
||||
$this->outbox = $response->json();
|
||||
$this->parseOutbox($this->outbox);
|
||||
}
|
||||
|
||||
public function parseOutbox($outbox)
|
||||
{
|
||||
$types = ['OrderedCollection', 'OrderedCollectionPage'];
|
||||
|
||||
if(isset($outbox['totalItems']) && $outbox['totalItems'] < 1) {
|
||||
// Skip remote fetch, not enough posts
|
||||
Log::info('not enough items');
|
||||
return;
|
||||
}
|
||||
|
||||
if(isset($outbox['type']) && in_array($outbox['type'], $types)) {
|
||||
Log::info('handle ordered collection');
|
||||
$this->handleOrderedCollection();
|
||||
}
|
||||
}
|
||||
|
||||
public function handleOrderedCollection()
|
||||
{
|
||||
$outbox = $this->outbox;
|
||||
|
||||
if(!isset($outbox['next']) && !isset($outbox['first']['next']) && $this->cursor !== 1) {
|
||||
$this->cursor = 40;
|
||||
$outbox['next'] = false;
|
||||
}
|
||||
|
||||
if($outbox['type'] == 'OrderedCollectionPage') {
|
||||
$this->nextUrl = $outbox['next'];
|
||||
}
|
||||
|
||||
if(isset($outbox['first']) && !is_array($outbox['first'])) {
|
||||
// Mastodon detected
|
||||
Log::info('Mastodon detected...');
|
||||
$this->nextUrl = $outbox['first'];
|
||||
return $this->fetchOutbox($this->nextUrl);
|
||||
} else {
|
||||
// Pleroma detected.
|
||||
$this->nextUrl = isset($outbox['next']) ? $outbox['next'] : (isset($outbox['first']['next']) ? $outbox['first']['next'] : $outbox['next']);
|
||||
Log::info('Checking ordered items...');
|
||||
$orderedItems = isset($outbox['orderedItems']) ? $outbox['orderedItems'] : $outbox['first']['orderedItems'];
|
||||
}
|
||||
|
||||
|
||||
foreach($orderedItems as $item) {
|
||||
Log::info('Parsing items...');
|
||||
$parsed = $this->parseObject($item);
|
||||
if($parsed !== 0) {
|
||||
Log::info('Found media!');
|
||||
$this->importActivity($item);
|
||||
}
|
||||
}
|
||||
|
||||
if($this->cursor < 40 && $this->mediaCount < 9) {
|
||||
$this->cursor++;
|
||||
$this->mediaCount++;
|
||||
$this->fetchOutbox($this->nextUrl);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function parseObject($parsed)
|
||||
{
|
||||
if($parsed['type'] !== 'Create') {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$activity = $parsed['object'];
|
||||
|
||||
if(isset($activity['attachment']) && !empty($activity['attachment'])) {
|
||||
return $this->detectSupportedMedia($activity['attachment']);
|
||||
}
|
||||
}
|
||||
|
||||
public function detectSupportedMedia($attachments)
|
||||
{
|
||||
$supported = $this->supported;
|
||||
$count = 0;
|
||||
|
||||
foreach($attachments as $media) {
|
||||
$mime = $media['mediaType'];
|
||||
$count = in_array($mime, $supported) ? ($count + 1) : $count;
|
||||
}
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
public function importActivity($activity)
|
||||
{
|
||||
$profile = $this->profile;
|
||||
$supported = $this->supported;
|
||||
$attachments = $activity['object']['attachment'];
|
||||
$caption = str_limit($activity['object']['content'], 125);
|
||||
|
||||
if(Status::whereUrl($activity['id'])->count() !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$status = new Status;
|
||||
$status->profile_id = $profile->id;
|
||||
$status->url = $activity['id'];
|
||||
$status->local = false;
|
||||
$status->caption = strip_tags($caption);
|
||||
$status->created_at = Carbon::parse($activity['published']);
|
||||
|
||||
$count = 0;
|
||||
|
||||
foreach($attachments as $media) {
|
||||
Log::info($media['mediaType'] . ' - ' . $media['url']);
|
||||
$url = $media['url'];
|
||||
$mime = $media['mediaType'];
|
||||
if(!in_array($mime, $supported)) {
|
||||
Log::info('Invalid media, skipping. ' . $mime);
|
||||
continue;
|
||||
}
|
||||
$count++;
|
||||
|
||||
if($count === 1) {
|
||||
$status->save();
|
||||
}
|
||||
$this->importMedia($url, $mime, $status);
|
||||
}
|
||||
Log::info(count($attachments) . ' media found...');
|
||||
|
||||
if($count !== 0) {
|
||||
NewStatusPipeline::dispatch($status, $status->media->first());
|
||||
}
|
||||
}
|
||||
|
||||
public function importMedia($url, $mime, $status)
|
||||
{
|
||||
$user = $this->profile;
|
||||
$monthHash = hash('sha1', date('Y') . date('m'));
|
||||
$userHash = hash('sha1', $user->id . (string) $user->created_at);
|
||||
$storagePath = "public/m/{$monthHash}/{$userHash}";
|
||||
try {
|
||||
$info = pathinfo($url);
|
||||
$img = file_get_contents($url);
|
||||
$file = '/tmp/' . str_random(12) . $info['basename'];
|
||||
file_put_contents($file, $img);
|
||||
$path = Storage::putFile($storagePath, new File($file), 'public');
|
||||
|
||||
$media = new Media;
|
||||
$media->status_id = $status->id;
|
||||
$media->profile_id = $status->profile_id;
|
||||
$media->user_id = null;
|
||||
$media->media_path = $path;
|
||||
$media->size = 0;
|
||||
$media->mime = $mime;
|
||||
$media->save();
|
||||
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
105
app/Jobs/RemoteFollowPipeline/RemoteFollowPipeline.php
Normal file
105
app/Jobs/RemoteFollowPipeline/RemoteFollowPipeline.php
Normal file
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\RemoteFollowPipeline;
|
||||
|
||||
use Zttp\Zttp;
|
||||
use App\{Profile};
|
||||
use GuzzleHttp\Client;
|
||||
use HttpSignatures\Context;
|
||||
use HttpSignatures\GuzzleHttpSignatures;
|
||||
use App\Jobs\RemoteFollowPipeline\RemoteFollowImportRecent;
|
||||
use App\Jobs\AvatarPipeline\CreateAvatar;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class RemoteFollowPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $url;
|
||||
protected $follower;
|
||||
protected $response;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct($follower, $url)
|
||||
{
|
||||
$this->follower = $follower;
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$follower = $this->follower;
|
||||
$url = $this->url;
|
||||
|
||||
if(Profile::whereRemoteUrl($url)->count() !== 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->discover($url);
|
||||
return true;
|
||||
}
|
||||
|
||||
public function discover($url)
|
||||
{
|
||||
$context = new Context([
|
||||
'keys' => ['examplekey' => 'secret-key-here'],
|
||||
'algorithm' => 'hmac-sha256',
|
||||
'headers' => ['(request-target)', 'date'],
|
||||
]);
|
||||
|
||||
$handlerStack = GuzzleHttpSignatures::defaultHandlerFromContext($context);
|
||||
$client = new Client(['handler' => $handlerStack]);
|
||||
$response = Zttp::withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelFedBot v0.1 - https://pixelfed.org'
|
||||
])->get($url);
|
||||
$this->response = $response->json();
|
||||
|
||||
$this->storeProfile();
|
||||
}
|
||||
|
||||
public function storeProfile()
|
||||
{
|
||||
$res = $this->response;
|
||||
$domain = parse_url($res['url'], PHP_URL_HOST);
|
||||
$username = $res['preferredUsername'];
|
||||
$remoteUsername = "@{$username}@{$domain}";
|
||||
|
||||
$profile = new Profile;
|
||||
$profile->user_id = null;
|
||||
$profile->domain = $domain;
|
||||
$profile->username = $remoteUsername;
|
||||
$profile->name = $res['name'];
|
||||
$profile->bio = str_limit($res['summary'], 125);
|
||||
$profile->sharedInbox = $res['endpoints']['sharedInbox'];
|
||||
$profile->remote_url = $res['url'];
|
||||
$profile->save();
|
||||
|
||||
RemoteFollowImportRecent::dispatch($this->response, $profile);
|
||||
CreateAvatar::dispatch($profile);
|
||||
}
|
||||
|
||||
public function sendActivity()
|
||||
{
|
||||
$res = $this->response;
|
||||
$url = $res['inbox'];
|
||||
|
||||
$activity = Zttp::withHeaders(['Content-Type' => 'application/activity+json'])->post($url, [
|
||||
'type' => 'Follow',
|
||||
'object' => $this->follower->url()
|
||||
]);
|
||||
}
|
||||
}
|
47
app/Jobs/StatusPipeline/NewStatusPipeline.php
Normal file
47
app/Jobs/StatusPipeline/NewStatusPipeline.php
Normal file
|
@ -0,0 +1,47 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use Cache, Redis;
|
||||
use App\{Media, Status};
|
||||
use App\Jobs\ImageOptimizePipeline\ImageOptimize;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class NewStatusPipeline implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
|
||||
StatusEntityLexer::dispatch($status);
|
||||
//StatusActivityPubDeliver::dispatch($status);
|
||||
|
||||
Cache::forever('post.' . $status->id, $status);
|
||||
|
||||
$redis = Redis::connection();
|
||||
$redis->lpush(config('cache.prefix').':user.' . $status->profile_id . '.posts', $status->id);
|
||||
}
|
||||
}
|
38
app/Jobs/StatusPipeline/StatusActivityPubDeliver.php
Normal file
38
app/Jobs/StatusPipeline/StatusActivityPubDeliver.php
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use App\{Media, Status};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class StatusActivityPubDeliver implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
|
||||
// todo: fanout on write
|
||||
}
|
||||
}
|
79
app/Jobs/StatusPipeline/StatusDelete.php
Normal file
79
app/Jobs/StatusPipeline/StatusDelete.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use App\{Media, Notification, StatusHashtag, Status};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
|
||||
class StatusDelete implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$this->unlinkRemoveMedia($status);
|
||||
}
|
||||
|
||||
public function unlinkRemoveMedia($status)
|
||||
{
|
||||
if($status->media()->count() == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach($status->media as $media) {
|
||||
$thumbnail = storage_path("app/{$media->thumbnail_path}");
|
||||
$photo = storage_path("app/{$media->media_path}");
|
||||
|
||||
try {
|
||||
if(is_file($thumbnail)) {
|
||||
unlink($thumbnail);
|
||||
}
|
||||
if(is_file($photo)) {
|
||||
unlink($photo);
|
||||
}
|
||||
$media->delete();
|
||||
} catch (Exception $e) {
|
||||
|
||||
}
|
||||
}
|
||||
$comments = Status::where('in_reply_to_id', $status->id)->get();
|
||||
foreach($comments as $comment) {
|
||||
$comment->in_reply_to_id = null;
|
||||
$comment->save();
|
||||
Notification::whereItemType('App\Status')
|
||||
->whereItemId($comment->id)
|
||||
->delete();
|
||||
}
|
||||
|
||||
$status->likes()->delete();
|
||||
Notification::whereItemType('App\Status')
|
||||
->whereItemId($status->id)
|
||||
->delete();
|
||||
StatusHashtag::whereStatusId($status->id)->delete();
|
||||
$status->delete();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
120
app/Jobs/StatusPipeline/StatusEntityLexer.php
Normal file
120
app/Jobs/StatusPipeline/StatusEntityLexer.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
namespace App\Jobs\StatusPipeline;
|
||||
|
||||
use Cache;
|
||||
use App\{
|
||||
Hashtag,
|
||||
Media,
|
||||
Mention,
|
||||
Profile,
|
||||
Status,
|
||||
StatusHashtag
|
||||
};
|
||||
use App\Util\Lexer\Hashtag as HashtagLexer;
|
||||
use App\Util\Lexer\{Autolink, Extractor};
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use App\Jobs\MentionPipeline\MentionPipeline;
|
||||
|
||||
class StatusEntityLexer implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
protected $status;
|
||||
protected $entities;
|
||||
protected $autolink;
|
||||
|
||||
/**
|
||||
* Create a new job instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(Status $status)
|
||||
{
|
||||
$this->status = $status;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the job.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$status = $this->status;
|
||||
$this->parseEntities();
|
||||
}
|
||||
|
||||
public function parseEntities()
|
||||
{
|
||||
$this->extractEntities();
|
||||
}
|
||||
|
||||
public function extractEntities()
|
||||
{
|
||||
$this->entities = Extractor::create()->extract($this->status->caption);
|
||||
$this->autolinkStatus();
|
||||
}
|
||||
|
||||
public function autolinkStatus()
|
||||
{
|
||||
$this->autolink = Autolink::create()->autolink($this->status->caption);
|
||||
$this->storeEntities();
|
||||
}
|
||||
|
||||
public function storeEntities()
|
||||
{
|
||||
$status = $this->status;
|
||||
$this->storeHashtags();
|
||||
$this->storeMentions();
|
||||
$status->rendered = $this->autolink;
|
||||
$status->entities = json_encode($this->entities);
|
||||
$status->save();
|
||||
}
|
||||
|
||||
public function storeHashtags()
|
||||
{
|
||||
$tags = array_unique($this->entities['hashtags']);
|
||||
$status = $this->status;
|
||||
|
||||
foreach($tags as $tag) {
|
||||
$slug = str_slug($tag);
|
||||
|
||||
$htag = Hashtag::firstOrCreate(
|
||||
['name' => $tag],
|
||||
['slug' => $slug]
|
||||
);
|
||||
|
||||
StatusHashtag::firstOrCreate(
|
||||
['status_id' => $status->id],
|
||||
['hashtag_id' => $htag->id]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function storeMentions()
|
||||
{
|
||||
$mentions = array_unique($this->entities['mentions']);
|
||||
$status = $this->status;
|
||||
|
||||
foreach($mentions as $mention) {
|
||||
$mentioned = Profile::whereUsername($mention)->first();
|
||||
|
||||
if(empty($mentioned) || !isset($mentioned->id)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$m = new Mention;
|
||||
$m->status_id = $status->id;
|
||||
$m->profile_id = $mentioned->id;
|
||||
$m->save();
|
||||
|
||||
MentionPipeline::dispatch($status, $m);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
26
app/Like.php
26
app/Like.php
|
@ -3,16 +3,40 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Like extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class);
|
||||
}
|
||||
|
||||
public function toText()
|
||||
{
|
||||
$actorName = $this->actor->username;
|
||||
return "{$actorName} " . __('notification.likedPhoto');
|
||||
}
|
||||
|
||||
public function toHtml()
|
||||
{
|
||||
$actorName = $this->actor->username;
|
||||
$actorUrl = $this->actor->url();
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
|
||||
__('notification.likedPhoto');
|
||||
}
|
||||
}
|
||||
|
|
34
app/Mail/ConfirmEmail.php
Normal file
34
app/Mail/ConfirmEmail.php
Normal file
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
namespace App\Mail;
|
||||
|
||||
use App\EmailVerification;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Mail\Mailable;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
|
||||
class ConfirmEmail extends Mailable
|
||||
{
|
||||
use Queueable, SerializesModels;
|
||||
|
||||
/**
|
||||
* Create a new message instance.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function __construct(EmailVerification $verify)
|
||||
{
|
||||
$this->verify = $verify;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the message.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
return $this->markdown('emails.confirm_email')->with(['verify'=>$this->verify]);
|
||||
}
|
||||
}
|
|
@ -2,11 +2,21 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Storage;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Media extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function url()
|
||||
{
|
||||
$path = $this->media_path;
|
||||
|
|
42
app/Mention.php
Normal file
42
app/Mention.php
Normal file
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Mention extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class, 'status_id', 'id');
|
||||
}
|
||||
|
||||
public function toText()
|
||||
{
|
||||
$actorName = $this->status->profile->username;
|
||||
return "{$actorName} " . __('notification.mentionedYou');
|
||||
}
|
||||
|
||||
public function toHtml()
|
||||
{
|
||||
$actorName = $this->status->profile->username;
|
||||
$actorUrl = $this->status->profile->url();
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
|
||||
__('notification.mentionedYou');
|
||||
}
|
||||
}
|
39
app/Notification.php
Normal file
39
app/Notification.php
Normal file
|
@ -0,0 +1,39 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Notification extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function actor()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'actor_id', 'id');
|
||||
}
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class, 'profile_id', 'id');
|
||||
}
|
||||
|
||||
public function item()
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function status()
|
||||
{
|
||||
return $this->belongsTo(Status::class, 'item_id', 'id');
|
||||
}
|
||||
|
||||
}
|
|
@ -34,7 +34,7 @@ class UserObserver
|
|||
$profile->public_key = $pki_public;
|
||||
$profile->save();
|
||||
|
||||
CreateAvatar::dispatch($user);
|
||||
CreateAvatar::dispatch($profile);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -2,20 +2,35 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Storage;
|
||||
use App\Util\Lexer\PrettyNumber;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Profile extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
protected $hidden = [
|
||||
'private_key',
|
||||
];
|
||||
|
||||
protected $visible = ['id', 'username', 'name'];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function url($suffix = '')
|
||||
{
|
||||
return url('/@' . $this->username . $suffix);
|
||||
return url($this->username . $suffix);
|
||||
}
|
||||
|
||||
public function permalink($suffix = '')
|
||||
|
@ -34,27 +49,63 @@ class Profile extends Model
|
|||
return $this->hasMany(Status::class);
|
||||
}
|
||||
|
||||
public function followingCount($short = false)
|
||||
{
|
||||
$count = $this->following()->count();
|
||||
if($short) {
|
||||
return PrettyNumber::convert($count);
|
||||
} else {
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
public function followerCount($short = false)
|
||||
{
|
||||
$count = $this->followers()->count();
|
||||
if($short) {
|
||||
return PrettyNumber::convert($count);
|
||||
} else {
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
|
||||
public function following()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
return $this->belongsToMany(
|
||||
Profile::class,
|
||||
Follower::class,
|
||||
'followers',
|
||||
'profile_id',
|
||||
'id',
|
||||
'id',
|
||||
'id'
|
||||
'following_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function followers()
|
||||
{
|
||||
return $this->hasManyThrough(
|
||||
return $this->belongsToMany(
|
||||
Profile::class,
|
||||
Follower::class,
|
||||
'followers',
|
||||
'following_id',
|
||||
'id',
|
||||
'id',
|
||||
'id'
|
||||
'profile_id'
|
||||
);
|
||||
}
|
||||
|
||||
public function follows($profile)
|
||||
{
|
||||
return Follower::whereProfileId($this->id)->whereFollowingId($profile->id)->count();
|
||||
}
|
||||
|
||||
public function followedBy($profile)
|
||||
{
|
||||
return Follower::whereProfileId($profile->id)->whereFollowingId($this->id)->count();
|
||||
}
|
||||
|
||||
public function bookmarks()
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Status::class,
|
||||
'bookmarks',
|
||||
'profile_id',
|
||||
'status_id'
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ use App\User;
|
|||
use Auth, Horizon;
|
||||
use App\Observers\UserObserver;
|
||||
use Illuminate\Support\Facades\Blade;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
|
@ -17,6 +18,8 @@ class AppServiceProvider extends ServiceProvider
|
|||
*/
|
||||
public function boot()
|
||||
{
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
User::observe(UserObserver::class);
|
||||
|
||||
Horizon::auth(function ($request) {
|
||||
|
@ -37,7 +40,6 @@ class AppServiceProvider extends ServiceProvider
|
|||
});
|
||||
|
||||
Blade::directive('prettySize', function($expression) {
|
||||
|
||||
$size = intval($expression);
|
||||
$precision = 0;
|
||||
$short = true;
|
||||
|
@ -48,6 +50,11 @@ class AppServiceProvider extends ServiceProvider
|
|||
$res = round($size, $precision).$units[$i];
|
||||
return "<?php echo '$res'; ?>";
|
||||
});
|
||||
|
||||
Blade::directive('maxFileSize', function() {
|
||||
$value = config('pixelfed.max_photo_size');
|
||||
return \App\Util\Lexer\PrettyNumber::size($value, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -2,12 +2,21 @@
|
|||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Storage;
|
||||
use Vinkla\Hashids\Facades\Hashids;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
|
||||
class Status extends Model
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
public function profile()
|
||||
{
|
||||
return $this->belongsTo(Profile::class);
|
||||
|
@ -25,6 +34,9 @@ class Status extends Model
|
|||
|
||||
public function thumb()
|
||||
{
|
||||
if($this->media->count() == 0 || $this->is_nsfw) {
|
||||
return "data:image/gif;base64,R0lGODlhAQABAIAAAMLCwgAAACH5BAAAAAAALAAAAAABAAEAAAICRAEAOw==";
|
||||
}
|
||||
return url(Storage::url($this->firstMedia()->thumbnail_path));
|
||||
}
|
||||
|
||||
|
@ -32,13 +44,25 @@ class Status extends Model
|
|||
{
|
||||
$id = $this->id;
|
||||
$username = $this->profile->username;
|
||||
return url(config('app.url') . "/p/@{$username}/{$id}");
|
||||
$path = config('app.url') . "/p/{$username}/{$id}";
|
||||
if(!is_null($this->in_reply_to_id)) {
|
||||
$pid = $this->in_reply_to_id;
|
||||
$path = config('app.url') . "/p/{$username}/{$pid}/c/{$id}";
|
||||
}
|
||||
return url($path);
|
||||
}
|
||||
|
||||
public function editUrl()
|
||||
{
|
||||
return $this->url() . '/edit';
|
||||
}
|
||||
|
||||
public function mediaUrl()
|
||||
{
|
||||
$path = $this->firstMedia()->media_path;
|
||||
$url = Storage::url($path);
|
||||
$media = $this->firstMedia();
|
||||
$path = $media->media_path;
|
||||
$hash = is_null($media->processed_at) ? md5('unprocessed') : md5($media->created_at);
|
||||
$url = Storage::url($path) . "?v={$hash}";
|
||||
return url($url);
|
||||
}
|
||||
|
||||
|
@ -96,4 +120,17 @@ class Status extends Model
|
|||
return $obj;
|
||||
}
|
||||
|
||||
public function replyToText()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
return "{$actorName} " . __('notification.commented');
|
||||
}
|
||||
|
||||
public function replyToHtml()
|
||||
{
|
||||
$actorName = $this->profile->username;
|
||||
$actorUrl = $this->profile->url();
|
||||
return "<a href='{$actorUrl}' class='profile-link'>{$actorName}</a> " .
|
||||
__('notification.commented');
|
||||
}
|
||||
}
|
||||
|
|
10
app/StatusHashtag.php
Normal file
10
app/StatusHashtag.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class StatusHashtag extends Model
|
||||
{
|
||||
protected $fillable = ['status_id', 'hashtag_id'];
|
||||
}
|
79
app/Transformer/ActivityPub/ProfileOutbox.php
Normal file
79
app/Transformer/ActivityPub/ProfileOutbox.php
Normal file
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Transformer\ActivityPub;
|
||||
|
||||
use App\Profile;
|
||||
use League\Fractal;
|
||||
|
||||
class ProfileOutbox extends Fractal\TransformerAbstract
|
||||
{
|
||||
|
||||
public function transform(Profile $profile)
|
||||
{
|
||||
$count = $profile->statuses()->count();
|
||||
$statuses = $profile->statuses()->has('media')->orderBy('id','desc')->take(20)->get()->map(function($i, $k) {
|
||||
$item = [
|
||||
'id' => $i->url(),
|
||||
// TODO: handle other types
|
||||
'type' => 'Create',
|
||||
'actor' => $i->profile->url(),
|
||||
'published' => $i->created_at->toISO8601String(),
|
||||
'to' => [
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc' => [
|
||||
$i->profile->permalink('/followers'),
|
||||
],
|
||||
'object' => [
|
||||
'id' => $i->url(),
|
||||
|
||||
// TODO: handle other types
|
||||
'type' => 'Note',
|
||||
|
||||
// XXX: CW Title
|
||||
'summary' => null,
|
||||
'content' => $i->rendered ?? $i->caption,
|
||||
'inReplyTo' => null,
|
||||
|
||||
// TODO: fix date format
|
||||
'published' => $i->created_at->toAtomString(),
|
||||
'url' => $i->url(),
|
||||
'attributedTo' => $i->profile->permalink(),
|
||||
'to' => [
|
||||
// TODO: handle proper scope
|
||||
'https://www.w3.org/ns/activitystreams#Public'
|
||||
],
|
||||
'cc' => [
|
||||
// TODO: add cc's
|
||||
//"{$notice->getProfile()->getUrl()}/subscribers",
|
||||
],
|
||||
'sensitive' => null,
|
||||
'atomUri' => $i->url(),
|
||||
'inReplyToAtomUri' => null,
|
||||
'conversation' => $i->url(),
|
||||
'attachment' => [
|
||||
|
||||
// TODO: support more than 1 attachment
|
||||
[
|
||||
'type' => 'Document',
|
||||
'mediaType' => $i->firstMedia()->mime,
|
||||
'url' => $i->firstMedia()->url(),
|
||||
'name' => null
|
||||
]
|
||||
],
|
||||
'tag' => []
|
||||
]
|
||||
];
|
||||
return $item;
|
||||
});
|
||||
|
||||
return [
|
||||
'@context' => 'https://www.w3.org/ns/activitystreams',
|
||||
'id' => $profile->permalink('/outbox'),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => $count,
|
||||
'orderedItems' => $statuses
|
||||
];
|
||||
}
|
||||
|
||||
}
|
12
app/User.php
12
app/User.php
|
@ -3,11 +3,19 @@
|
|||
namespace App;
|
||||
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use Notifiable;
|
||||
use Notifiable, SoftDeletes;
|
||||
|
||||
/**
|
||||
* The attributes that should be mutated to dates.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $dates = ['deleted_at'];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
@ -34,6 +42,6 @@ class User extends Authenticatable
|
|||
|
||||
public function url()
|
||||
{
|
||||
return url(config('app.url') . '/@' . $this->username);
|
||||
return url(config('app.url') . '/' . $this->username);
|
||||
}
|
||||
}
|
||||
|
|
49
app/Util/ActivityPub/DiscoverActor.php
Normal file
49
app/Util/ActivityPub/DiscoverActor.php
Normal file
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\ActivityPub;
|
||||
|
||||
use \Zttp\Zttp;
|
||||
|
||||
class DiscoverActor {
|
||||
|
||||
protected $url;
|
||||
protected $response;
|
||||
|
||||
public function __construct($url)
|
||||
{
|
||||
$this->url = $url;
|
||||
}
|
||||
|
||||
public function fetch()
|
||||
{
|
||||
$res = Zttp::withHeaders([
|
||||
'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||
'User-Agent' => 'PixelFedBot - https://pixelfed.org'
|
||||
])->get($this->url);
|
||||
$this->response = $res->body();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getResponse()
|
||||
{
|
||||
return json_decode($this->response, true);
|
||||
}
|
||||
|
||||
public function getJsonResponse()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function discover()
|
||||
{
|
||||
$this->fetch();
|
||||
$res = $this->getResponse();
|
||||
|
||||
if(empty($res) || !in_array('type', $res) || $res['type'] !== 'Person') {
|
||||
throw new \Exception('Invalid Actor Object');
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
}
|
88
app/Util/ActivityPub/Inbox.php
Normal file
88
app/Util/ActivityPub/Inbox.php
Normal file
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\ActivityPub;
|
||||
|
||||
use App\Jobs\AvatarPipeline\CreateAvatar;
|
||||
use App\{Follower, Like, Profile, Like, Status, User};
|
||||
|
||||
class Inbox {
|
||||
|
||||
protected $request;
|
||||
protected $profile;
|
||||
protected $payload;
|
||||
|
||||
public function __construct($request, Profile $profile, $payload)
|
||||
{
|
||||
$this->request = $request;
|
||||
$this->profile = $profile;
|
||||
$this->payload = $payload;
|
||||
}
|
||||
|
||||
public function handle()
|
||||
{
|
||||
$this->authenticatePayload();
|
||||
}
|
||||
|
||||
public function authenticatePayload()
|
||||
{
|
||||
// todo
|
||||
|
||||
$this->handleVerb();
|
||||
}
|
||||
|
||||
public function handleVerb()
|
||||
{
|
||||
$verb = $this->payload['type'];
|
||||
|
||||
switch ($verb) {
|
||||
case 'Create':
|
||||
$this->handleCreateActivity();
|
||||
break;
|
||||
|
||||
case 'Follow':
|
||||
$this->handleFollowActivity();
|
||||
break;
|
||||
|
||||
default:
|
||||
// TODO: decide how to handle invalid verbs.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function handleCreateActivity()
|
||||
{
|
||||
// todo
|
||||
}
|
||||
|
||||
public function handleFollowActivity()
|
||||
{
|
||||
$actor = $this->payload['object'];
|
||||
$target = $this->profile;
|
||||
|
||||
}
|
||||
|
||||
public function actorFirstOrCreate($actorUrl)
|
||||
{
|
||||
if(Profile::whereRemoteUrl($actorUrl)->count() !== 0) {
|
||||
return Profile::whereRemoteUrl($actorUrl)->firstOrFail();
|
||||
}
|
||||
|
||||
$res = (new DiscoverActor($url))->discover();
|
||||
|
||||
$domain = parse_url($res['url'], PHP_URL_HOST);
|
||||
$username = $res['preferredUsername'];
|
||||
$remoteUsername = "@{$username}@{$domain}";
|
||||
|
||||
$profile = new Profile;
|
||||
$profile->user_id = null;
|
||||
$profile->domain = $domain;
|
||||
$profile->username = $remoteUsername;
|
||||
$profile->name = $res['name'];
|
||||
$profile->bio = str_limit($res['summary'], 125);
|
||||
$profile->sharedInbox = $res['endpoints']['sharedInbox'];
|
||||
$profile->remote_url = $res['url'];
|
||||
$profile->save();
|
||||
|
||||
}
|
||||
|
||||
}
|
771
app/Util/Lexer/Autolink.php
Executable file
771
app/Util/Lexer/Autolink.php
Executable file
|
@ -0,0 +1,771 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Mike Cochrane, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Regex;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use App\Util\Lexer\StringUtils;
|
||||
|
||||
/**
|
||||
* Twitter Autolink Class
|
||||
*
|
||||
* Parses tweets and generates HTML anchor tags around URLs, usernames,
|
||||
* username/list pairs and hashtags.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Mike Cochrane, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
class Autolink extends Regex
|
||||
{
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_url = '';
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_user = 'u-url mention';
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked list URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_list = 'u-url list-slug';
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked hashtag URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_hash = 'u-url hashtag';
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked cashtag URLs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $class_cash = 'u-url cashtag';
|
||||
|
||||
/**
|
||||
* URL base for username links (the username without the @ will be appended).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url_base_user = null;
|
||||
|
||||
/**
|
||||
* URL base for list links (the username/list without the @ will be appended).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url_base_list = null;
|
||||
|
||||
/**
|
||||
* URL base for hashtag links (the hashtag without the # will be appended).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url_base_hash = null;
|
||||
|
||||
/**
|
||||
* URL base for cashtag links (the hashtag without the $ will be appended).
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url_base_cash = null;
|
||||
|
||||
/**
|
||||
* Whether to include the value 'nofollow' in the 'rel' attribute.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $nofollow = true;
|
||||
|
||||
/**
|
||||
* Whether to include the value 'noopener' in the 'rel' attribute.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $noopener = true;
|
||||
|
||||
/**
|
||||
* Whether to include the value 'external' in the 'rel' attribute.
|
||||
*
|
||||
* Often this is used to be matched on in JavaScript for dynamically adding
|
||||
* the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
|
||||
* been undeprecated and thus the 'target' attribute can be used. If this is
|
||||
* set to false then the 'target' attribute will be output.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $external = true;
|
||||
|
||||
/**
|
||||
* The scope to open the link in.
|
||||
*
|
||||
* Support for the 'target' attribute was deprecated in HTML 4.01 but has
|
||||
* since been reinstated in HTML 5. To output the 'target' attribute you
|
||||
* must disable the adding of the string 'external' to the 'rel' attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $target = '_blank';
|
||||
|
||||
/**
|
||||
* attribute for invisible span tag
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $invisibleTagAttrs = "style='position:absolute;left:-9999px;'";
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Extractor
|
||||
*/
|
||||
protected $extractor = null;
|
||||
|
||||
/**
|
||||
* Provides fluent method chaining.
|
||||
*
|
||||
* @param string $tweet The tweet to be converted.
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*
|
||||
* @see __construct()
|
||||
*
|
||||
* @return Autolink
|
||||
*/
|
||||
public static function create($tweet = null, $full_encode = false)
|
||||
{
|
||||
return new static($tweet, $full_encode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in a tweet to be parsed and converted to contain links.
|
||||
*
|
||||
* As the intent is to produce links and output the modified tweet to the
|
||||
* user, we take this opportunity to ensure that we escape user input.
|
||||
*
|
||||
* @see htmlspecialchars()
|
||||
*
|
||||
* @param string $tweet The tweet to be converted.
|
||||
* @param bool $escape Whether to escape the tweet (default: true).
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*/
|
||||
public function __construct($tweet = null, $escape = true, $full_encode = false)
|
||||
{
|
||||
if ($escape && !empty($tweet)) {
|
||||
if ($full_encode) {
|
||||
parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false));
|
||||
} else {
|
||||
parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false));
|
||||
}
|
||||
} else {
|
||||
parent::__construct($tweet);
|
||||
}
|
||||
$this->extractor = Extractor::create();
|
||||
$this->url_base_user = config('app.url') . '/';
|
||||
$this->url_base_list = config('app.url') . '/';
|
||||
$this->url_base_hash = config('app.url') . "/discover/tags/";
|
||||
$this->url_base_cash = config('app.url') . '/search?q=%24';
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked URLs.
|
||||
*
|
||||
* @return string CSS class for URL links.
|
||||
*/
|
||||
public function getURLClass()
|
||||
{
|
||||
return $this->class_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked URLs.
|
||||
*
|
||||
* @param string $v CSS class for URL links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setURLClass($v)
|
||||
{
|
||||
$this->class_url = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username URLs.
|
||||
*
|
||||
* @return string CSS class for username links.
|
||||
*/
|
||||
public function getUsernameClass()
|
||||
{
|
||||
return $this->class_user;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username URLs.
|
||||
*
|
||||
* @param string $v CSS class for username links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setUsernameClass($v)
|
||||
{
|
||||
$this->class_user = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username/list URLs.
|
||||
*
|
||||
* @return string CSS class for username/list links.
|
||||
*/
|
||||
public function getListClass()
|
||||
{
|
||||
return $this->class_list;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked username/list URLs.
|
||||
*
|
||||
* @param string $v CSS class for username/list links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setListClass($v)
|
||||
{
|
||||
$this->class_list = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked hashtag URLs.
|
||||
*
|
||||
* @return string CSS class for hashtag links.
|
||||
*/
|
||||
public function getHashtagClass()
|
||||
{
|
||||
return $this->class_hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked hashtag URLs.
|
||||
*
|
||||
* @param string $v CSS class for hashtag links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setHashtagClass($v)
|
||||
{
|
||||
$this->class_hash = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked cashtag URLs.
|
||||
*
|
||||
* @return string CSS class for cashtag links.
|
||||
*/
|
||||
public function getCashtagClass()
|
||||
{
|
||||
return $this->class_cash;
|
||||
}
|
||||
|
||||
/**
|
||||
* CSS class for auto-linked cashtag URLs.
|
||||
*
|
||||
* @param string $v CSS class for cashtag links.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setCashtagClass($v)
|
||||
{
|
||||
$this->class_cash = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to include the value 'nofollow' in the 'rel' attribute.
|
||||
*
|
||||
* @return bool Whether to add 'nofollow' to the 'rel' attribute.
|
||||
*/
|
||||
public function getNoFollow()
|
||||
{
|
||||
return $this->nofollow;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to include the value 'nofollow' in the 'rel' attribute.
|
||||
*
|
||||
* @param bool $v The value to add to the 'target' attribute.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setNoFollow($v)
|
||||
{
|
||||
$this->nofollow = $v;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to include the value 'external' in the 'rel' attribute.
|
||||
*
|
||||
* Often this is used to be matched on in JavaScript for dynamically adding
|
||||
* the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
|
||||
* been undeprecated and thus the 'target' attribute can be used. If this is
|
||||
* set to false then the 'target' attribute will be output.
|
||||
*
|
||||
* @return bool Whether to add 'external' to the 'rel' attribute.
|
||||
*/
|
||||
public function getExternal()
|
||||
{
|
||||
return $this->external;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether to include the value 'external' in the 'rel' attribute.
|
||||
*
|
||||
* Often this is used to be matched on in JavaScript for dynamically adding
|
||||
* the 'target' attribute which is deprecated in HTML 4.01. In HTML 5 it has
|
||||
* been undeprecated and thus the 'target' attribute can be used. If this is
|
||||
* set to false then the 'target' attribute will be output.
|
||||
*
|
||||
* @param bool $v The value to add to the 'target' attribute.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setExternal($v)
|
||||
{
|
||||
$this->external = $v;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* The scope to open the link in.
|
||||
*
|
||||
* Support for the 'target' attribute was deprecated in HTML 4.01 but has
|
||||
* since been reinstated in HTML 5. To output the 'target' attribute you
|
||||
* must disable the adding of the string 'external' to the 'rel' attribute.
|
||||
*
|
||||
* @return string The value to add to the 'target' attribute.
|
||||
*/
|
||||
public function getTarget()
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
/**
|
||||
* The scope to open the link in.
|
||||
*
|
||||
* Support for the 'target' attribute was deprecated in HTML 4.01 but has
|
||||
* since been reinstated in HTML 5. To output the 'target' attribute you
|
||||
* must disable the adding of the string 'external' to the 'rel' attribute.
|
||||
*
|
||||
* @param string $v The value to add to the 'target' attribute.
|
||||
*
|
||||
* @return Autolink Fluid method chaining.
|
||||
*/
|
||||
public function setTarget($v)
|
||||
{
|
||||
$this->target = trim($v);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Autolink with entities
|
||||
*
|
||||
* @param string $tweet
|
||||
* @param array $entities
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkEntities($tweet = null, $entities = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
$text = '';
|
||||
$beginIndex = 0;
|
||||
foreach ($entities as $entity) {
|
||||
if (isset($entity['screen_name'])) {
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex + 1);
|
||||
} else {
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, $entity['indices'][0] - $beginIndex);
|
||||
}
|
||||
|
||||
if (isset($entity['url'])) {
|
||||
$text .= $this->linkToUrl($entity);
|
||||
} elseif (isset($entity['hashtag'])) {
|
||||
$text .= $this->linkToHashtag($entity, $tweet);
|
||||
} elseif (isset($entity['screen_name'])) {
|
||||
$text .= $this->linkToMentionAndList($entity);
|
||||
} elseif (isset($entity['cashtag'])) {
|
||||
$text .= $this->linkToCashtag($entity, $tweet);
|
||||
}
|
||||
$beginIndex = $entity['indices'][1];
|
||||
}
|
||||
$text .= StringUtils::substr($tweet, $beginIndex, StringUtils::strlen($tweet));
|
||||
return $text;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link hashtags, URLs, usernames and lists, with JSON entities.
|
||||
*
|
||||
* @param string The tweet to be converted
|
||||
* @param mixed The entities info
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkWithJson($tweet = null, $json = null)
|
||||
{
|
||||
// concatenate entities
|
||||
$entities = array();
|
||||
if (is_object($json)) {
|
||||
$json = $this->object2array($json);
|
||||
}
|
||||
if (is_array($json)) {
|
||||
foreach ($json as $key => $vals) {
|
||||
$entities = array_merge($entities, $json[$key]);
|
||||
}
|
||||
}
|
||||
|
||||
// map JSON entity to twitter-text entity
|
||||
foreach ($entities as $idx => $entity) {
|
||||
if (!empty($entity['text'])) {
|
||||
$entities[$idx]['hashtag'] = $entity['text'];
|
||||
}
|
||||
}
|
||||
|
||||
$entities = $this->extractor->removeOverlappingEntities($entities);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* convert Object to Array
|
||||
*
|
||||
* @param mixed $obj
|
||||
* @return array
|
||||
*/
|
||||
protected function object2array($obj)
|
||||
{
|
||||
$array = (array) $obj;
|
||||
foreach ($array as $key => $var) {
|
||||
if (is_object($var) || is_array($var)) {
|
||||
$array[$key] = $this->object2array($var);
|
||||
}
|
||||
}
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link hashtags, URLs, usernames and lists.
|
||||
*
|
||||
* @param string The tweet to be converted
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLink($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractURLWithoutProtocol(false)->extractEntitiesWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link the @username and @username/list references in the provided text. Links to @username references will
|
||||
* have the usernameClass CSS classes added. Links to @username/list references will have the listClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkUsernamesAndLists($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractMentionsOrListsWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link #hashtag references in the provided Tweet text. The #hashtag links will have the hashtagClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkHashtags($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractHashtagsWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link URLs in the Tweet text provided.
|
||||
* <p/>
|
||||
* This only auto-links URLs with protocol.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkURLs($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractURLWithoutProtocol(false)->extractURLsWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link $cashtag references in the provided Tweet text. The $cashtag links will have the cashtagClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function autoLinkCashtags($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = $this->extractor->extractCashtagsWithIndices($tweet);
|
||||
return $this->autoLinkEntities($tweet, $entities);
|
||||
}
|
||||
|
||||
public function linkToUrl($entity)
|
||||
{
|
||||
if (!empty($this->class_url)) {
|
||||
$attributes['class'] = $this->class_url;
|
||||
}
|
||||
$attributes['href'] = $entity['url'];
|
||||
$linkText = $this->escapeHTML($entity['url']);
|
||||
|
||||
if (!empty($entity['display_url']) && !empty($entity['expanded_url'])) {
|
||||
// Goal: If a user copies and pastes a tweet containing t.co'ed link, the resulting paste
|
||||
// should contain the full original URL (expanded_url), not the display URL.
|
||||
//
|
||||
// Method: Whenever possible, we actually emit HTML that contains expanded_url, and use
|
||||
// font-size:0 to hide those parts that should not be displayed (because they are not part of display_url).
|
||||
// Elements with font-size:0 get copied even though they are not visible.
|
||||
// Note that display:none doesn't work here. Elements with display:none don't get copied.
|
||||
//
|
||||
// Additionally, we want to *display* ellipses, but we don't want them copied. To make this happen we
|
||||
// wrap the ellipses in a tco-ellipsis class and provide an onCopy handler that sets display:none on
|
||||
// everything with the tco-ellipsis class.
|
||||
//
|
||||
// As an example: The user tweets "hi http://longdomainname.com/foo"
|
||||
// This gets shortened to "hi http://t.co/xyzabc", with display_url = "…nname.com/foo"
|
||||
// This will get rendered as:
|
||||
// <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
|
||||
// …
|
||||
// <!-- There's a chance the onCopy event handler might not fire. In case that happens,
|
||||
// we include an here so that the … doesn't bump up against the URL and ruin it.
|
||||
// The is inside the tco-ellipsis span so that when the onCopy handler *does*
|
||||
// fire, it doesn't get copied. Otherwise the copied text would have two spaces in a row,
|
||||
// e.g. "hi http://longdomainname.com/foo".
|
||||
// <span style='font-size:0'> </span>
|
||||
// </span>
|
||||
// <span style='font-size:0'> <!-- This stuff should get copied but not displayed -->
|
||||
// http://longdomai
|
||||
// </span>
|
||||
// <span class='js-display-url'> <!-- This stuff should get displayed *and* copied -->
|
||||
// nname.com/foo
|
||||
// </span>
|
||||
// <span class='tco-ellipsis'> <!-- This stuff should get displayed but not copied -->
|
||||
// <span style='font-size:0'> </span>
|
||||
// …
|
||||
// </span>
|
||||
//
|
||||
// Exception: pic.socialhub.dev images, for which expandedUrl = "https://socialhub.dev/#!/username/status/1234/photo/1
|
||||
// For those URLs, display_url is not a substring of expanded_url, so we don't do anything special to render the elided parts.
|
||||
// For a pic.socialhub.dev URL, the only elided part will be the "https://", so this is fine.
|
||||
$displayURL = $entity['display_url'];
|
||||
$expandedURL = $entity['expanded_url'];
|
||||
$displayURLSansEllipses = preg_replace('/…/u', '', $displayURL);
|
||||
$diplayURLIndexInExpandedURL = mb_strpos($expandedURL, $displayURLSansEllipses);
|
||||
|
||||
if ($diplayURLIndexInExpandedURL !== false) {
|
||||
$beforeDisplayURL = mb_substr($expandedURL, 0, $diplayURLIndexInExpandedURL);
|
||||
$afterDisplayURL = mb_substr($expandedURL, $diplayURLIndexInExpandedURL + mb_strlen($displayURLSansEllipses));
|
||||
$precedingEllipsis = (preg_match('/\A…/u', $displayURL)) ? '…' : '';
|
||||
$followingEllipsis = (preg_match('/…\z/u', $displayURL)) ? '…' : '';
|
||||
|
||||
$invisibleSpan = "<span {$this->invisibleTagAttrs}>";
|
||||
|
||||
$linkText = "<span class='tco-ellipsis'>{$precedingEllipsis}{$invisibleSpan} </span></span>";
|
||||
$linkText .= "{$invisibleSpan}{$this->escapeHTML($beforeDisplayURL)}</span>";
|
||||
$linkText .= "<span class='js-display-url'>{$this->escapeHTML($displayURLSansEllipses)}</span>";
|
||||
$linkText .= "{$invisibleSpan}{$this->escapeHTML($afterDisplayURL)}</span>";
|
||||
$linkText .= "<span class='tco-ellipsis'>{$invisibleSpan} </span>{$followingEllipsis}</span>";
|
||||
} else {
|
||||
$linkText = $entity['display_url'];
|
||||
}
|
||||
$attributes['title'] = $entity['expanded_url'];
|
||||
} elseif (!empty($entity['display_url'])) {
|
||||
$linkText = $entity['display_url'];
|
||||
}
|
||||
|
||||
return $this->linkToText($entity, $linkText, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $entity
|
||||
* @param string $tweet
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function linkToHashtag($entity, $tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$this->target = false;
|
||||
$attributes = array();
|
||||
$class = array();
|
||||
$hash = StringUtils::substr($tweet, $entity['indices'][0], 1);
|
||||
$linkText = $hash . $entity['hashtag'];
|
||||
|
||||
$attributes['href'] = $this->url_base_hash . $entity['hashtag'] . '?src=hash';
|
||||
$attributes['title'] = '#' . $entity['hashtag'];
|
||||
if (!empty($this->class_hash)) {
|
||||
$class[] = $this->class_hash;
|
||||
}
|
||||
if (preg_match(self::$patterns['rtl_chars'], $linkText)) {
|
||||
$class[] = 'rtl';
|
||||
}
|
||||
if (!empty($class)) {
|
||||
$attributes['class'] = join(' ', $class);
|
||||
}
|
||||
|
||||
return $this->linkToText($entity, $linkText, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $entity
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function linkToMentionAndList($entity)
|
||||
{
|
||||
$attributes = array();
|
||||
|
||||
if (!empty($entity['list_slug'])) {
|
||||
# Replace the list and username
|
||||
$linkText = $entity['screen_name'] . $entity['list_slug'];
|
||||
$class = $this->class_list;
|
||||
$url = $this->url_base_list . $linkText;
|
||||
} else {
|
||||
# Replace the username
|
||||
$linkText = $entity['screen_name'];
|
||||
$class = $this->class_user;
|
||||
$url = $this->url_base_user . $linkText;
|
||||
}
|
||||
if (!empty($class)) {
|
||||
$attributes['class'] = $class;
|
||||
}
|
||||
$attributes['href'] = $url;
|
||||
|
||||
return $this->linkToText($entity, $linkText, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $entity
|
||||
* @param string $tweet
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function linkToCashtag($entity, $tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$attributes = array();
|
||||
$doller = StringUtils::substr($tweet, $entity['indices'][0], 1);
|
||||
$linkText = $doller . $entity['cashtag'];
|
||||
$attributes['href'] = $this->url_base_cash . $entity['cashtag'];
|
||||
$attributes['title'] = $linkText;
|
||||
if (!empty($this->class_cash)) {
|
||||
$attributes['class'] = $this->class_cash;
|
||||
}
|
||||
|
||||
return $this->linkToText($entity, $linkText, $attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param array $entity
|
||||
* @param string $text
|
||||
* @param array $attributes
|
||||
* @return string
|
||||
* @since 1.1.0
|
||||
*/
|
||||
public function linkToText(array $entity, $text, $attributes = array())
|
||||
{
|
||||
$rel = array();
|
||||
if ($this->external) {
|
||||
$rel[] = 'external';
|
||||
}
|
||||
if ($this->nofollow) {
|
||||
$rel[] = 'nofollow';
|
||||
}
|
||||
if ($this->noopener) {
|
||||
$rel[] = 'noopener';
|
||||
}
|
||||
if (!empty($rel)) {
|
||||
$attributes['rel'] = join(' ', $rel);
|
||||
}
|
||||
if ($this->target) {
|
||||
$attributes['target'] = $this->target;
|
||||
}
|
||||
$link = '<a';
|
||||
foreach ($attributes as $key => $val) {
|
||||
$link .= ' ' . $key . '="' . $this->escapeHTML($val) . '"';
|
||||
}
|
||||
$link .= '>' . $text . '</a>';
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* html escape
|
||||
*
|
||||
* @param string $text
|
||||
* @return string
|
||||
*/
|
||||
protected function escapeHTML($text)
|
||||
{
|
||||
return htmlspecialchars($text, ENT_QUOTES, 'UTF-8', false);
|
||||
}
|
||||
}
|
548
app/Util/Lexer/Extractor.php
Executable file
548
app/Util/Lexer/Extractor.php
Executable file
|
@ -0,0 +1,548 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Mike Cochrane, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Regex;
|
||||
use App\Util\Lexer\StringUtils;
|
||||
|
||||
/**
|
||||
* Twitter Extractor Class
|
||||
*
|
||||
* Parses tweets and extracts URLs, usernames, username/list pairs and
|
||||
* hashtags.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Mike Cochrane, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
class Extractor extends Regex
|
||||
{
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
protected $extractURLWithoutProtocol = true;
|
||||
|
||||
/**
|
||||
* Provides fluent method chaining.
|
||||
*
|
||||
* @param string $tweet The tweet to be converted.
|
||||
*
|
||||
* @see __construct()
|
||||
*
|
||||
* @return Extractor
|
||||
*/
|
||||
public static function create($tweet = null)
|
||||
{
|
||||
return new self($tweet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in a tweet to be parsed and extracts elements from it.
|
||||
*
|
||||
* Extracts various parts of a tweet including URLs, usernames, hashtags...
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
*/
|
||||
public function __construct($tweet = null)
|
||||
{
|
||||
parent::__construct($tweet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all parts of a tweet and returns an associative array containing
|
||||
* the extracted elements.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The elements in the tweet.
|
||||
*/
|
||||
public function extract($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
return array(
|
||||
'hashtags' => $this->extractHashtags($tweet),
|
||||
'urls' => $this->extractURLs($tweet),
|
||||
'mentions' => $this->extractMentionedUsernames($tweet),
|
||||
'replyto' => $this->extractRepliedUsernames($tweet),
|
||||
'hashtags_with_indices' => $this->extractHashtagsWithIndices($tweet),
|
||||
'urls_with_indices' => $this->extractURLsWithIndices($tweet),
|
||||
'mentions_with_indices' => $this->extractMentionedUsernamesWithIndices($tweet),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract URLs, @mentions, lists and #hashtag from a given text/tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array list of extracted entities
|
||||
*/
|
||||
public function extractEntitiesWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$entities = array();
|
||||
$entities = array_merge($entities, $this->extractURLsWithIndices($tweet));
|
||||
$entities = array_merge($entities, $this->extractHashtagsWithIndices($tweet, false));
|
||||
$entities = array_merge($entities, $this->extractMentionsOrListsWithIndices($tweet));
|
||||
$entities = array_merge($entities, $this->extractCashtagsWithIndices($tweet));
|
||||
$entities = $this->removeOverlappingEntities($entities);
|
||||
return $entities;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the hashtags from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The hashtag elements in the tweet.
|
||||
*/
|
||||
public function extractHashtags($tweet = null)
|
||||
{
|
||||
$hashtagsOnly = array();
|
||||
$hashtagsWithIndices = $this->extractHashtagsWithIndices($tweet);
|
||||
|
||||
foreach ($hashtagsWithIndices as $hashtagWithIndex) {
|
||||
$hashtagsOnly[] = $hashtagWithIndex['hashtag'];
|
||||
}
|
||||
return $hashtagsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the cashtags from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The cashtag elements in the tweet.
|
||||
*/
|
||||
public function extractCashtags($tweet = null)
|
||||
{
|
||||
$cashtagsOnly = array();
|
||||
$cashtagsWithIndices = $this->extractCashtagsWithIndices($tweet);
|
||||
|
||||
foreach ($cashtagsWithIndices as $cashtagWithIndex) {
|
||||
$cashtagsOnly[] = $cashtagWithIndex['cashtag'];
|
||||
}
|
||||
return $cashtagsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the URLs from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The URL elements in the tweet.
|
||||
*/
|
||||
public function extractURLs($tweet = null)
|
||||
{
|
||||
$urlsOnly = array();
|
||||
$urlsWithIndices = $this->extractURLsWithIndices($tweet);
|
||||
|
||||
foreach ($urlsWithIndices as $urlWithIndex) {
|
||||
$urlsOnly[] = $urlWithIndex['url'];
|
||||
}
|
||||
return $urlsOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the usernames from the tweet.
|
||||
*
|
||||
* A mention is an occurrence of a username anywhere in a tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The usernames elements in the tweet.
|
||||
*/
|
||||
public function extractMentionedScreennames($tweet = null)
|
||||
{
|
||||
$usernamesOnly = array();
|
||||
$mentionsWithIndices = $this->extractMentionsOrListsWithIndices($tweet);
|
||||
|
||||
foreach ($mentionsWithIndices as $mentionWithIndex) {
|
||||
$screen_name = mb_strtolower($mentionWithIndex['screen_name']);
|
||||
if (empty($screen_name) OR in_array($screen_name, $usernamesOnly)) {
|
||||
continue;
|
||||
}
|
||||
$usernamesOnly[] = $screen_name;
|
||||
}
|
||||
return $usernamesOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the usernames from the tweet.
|
||||
*
|
||||
* A mention is an occurrence of a username anywhere in a tweet.
|
||||
*
|
||||
* @return array The usernames elements in the tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function extractMentionedUsernames($tweet)
|
||||
{
|
||||
$this->tweet = $tweet;
|
||||
return $this->extractMentionedScreennames($tweet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the usernames replied to from the tweet.
|
||||
*
|
||||
* A reply is an occurrence of a username at the beginning of a tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The usernames replied to in a tweet.
|
||||
*/
|
||||
public function extractReplyScreenname($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$matched = preg_match(self::$patterns['valid_reply'], $tweet, $matches);
|
||||
# Check username ending in
|
||||
if ($matched && preg_match(self::$patterns['end_mention_match'], $matches[2])) {
|
||||
$matched = false;
|
||||
}
|
||||
return $matched ? $matches[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract all the usernames replied to from the tweet.
|
||||
*
|
||||
* A reply is an occurrence of a username at the beginning of a tweet.
|
||||
*
|
||||
* @return array The usernames replied to in a tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function extractRepliedUsernames()
|
||||
{
|
||||
return $this->extractReplyScreenname();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the hashtags and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @param boolean $checkUrlOverlap if true, check if extracted hashtags overlap URLs and remove overlapping ones
|
||||
* @return array The hashtag elements in the tweet.
|
||||
*/
|
||||
public function extractHashtagsWithIndices($tweet = null, $checkUrlOverlap = true)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
if (!preg_match('/[##]/iu', $tweet)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match_all(self::$patterns['valid_hashtag'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
$tags = array();
|
||||
|
||||
foreach ($matches as $match) {
|
||||
list($all, $before, $hash, $hashtag, $outer) = array_pad($match, 3, array('', 0));
|
||||
$start_position = $hash[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $hash[1])) : $hash[1];
|
||||
$end_position = $start_position + StringUtils::strlen($hash[0] . $hashtag[0]);
|
||||
|
||||
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tags[] = array(
|
||||
'hashtag' => $hashtag[0],
|
||||
'indices' => array($start_position, $end_position)
|
||||
);
|
||||
}
|
||||
|
||||
if (!$checkUrlOverlap) {
|
||||
return $tags;
|
||||
}
|
||||
|
||||
# check url overlap
|
||||
$urls = $this->extractURLsWithIndices($tweet);
|
||||
$entities = $this->removeOverlappingEntities(array_merge($tags, $urls));
|
||||
|
||||
$validTags = array();
|
||||
foreach ($entities as $entity) {
|
||||
if (empty($entity['hashtag'])) {
|
||||
continue;
|
||||
}
|
||||
$validTags[] = $entity;
|
||||
}
|
||||
|
||||
return $validTags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the cashtags and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The cashtag elements in the tweet.
|
||||
*/
|
||||
public function extractCashtagsWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
if (!preg_match('/\$/iu', $tweet)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match_all(self::$patterns['valid_cashtag'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
$tags = array();
|
||||
|
||||
foreach ($matches as $match) {
|
||||
list($all, $before, $dollar, $cash_text, $outer) = array_pad($match, 3, array('', 0));
|
||||
$start_position = $dollar[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $dollar[1])) : $dollar[1];
|
||||
$end_position = $start_position + StringUtils::strlen($dollar[0] . $cash_text[0]);
|
||||
|
||||
if (preg_match(self::$patterns['end_hashtag_match'], $outer[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$tags[] = array(
|
||||
'cashtag' => $cash_text[0],
|
||||
'indices' => array($start_position, $end_position)
|
||||
);
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the URLs and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The URLs elements in the tweet.
|
||||
*/
|
||||
public function extractURLsWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
$needle = $this->extractURLWithoutProtocol() ? '.' : ':';
|
||||
if (strpos($tweet, $needle) === false) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$urls = array();
|
||||
preg_match_all(self::$patterns['valid_url'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
|
||||
foreach ($matches as $match) {
|
||||
list($all, $before, $url, $protocol, $domain, $port, $path, $query) = array_pad($match, 8, array(''));
|
||||
$start_position = $url[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $url[1])) : $url[1];
|
||||
$end_position = $start_position + StringUtils::strlen($url[0]);
|
||||
|
||||
$all = $all[0];
|
||||
$before = $before[0];
|
||||
$url = $url[0];
|
||||
$protocol = $protocol[0];
|
||||
$domain = $domain[0];
|
||||
$port = $port[0];
|
||||
$path = $path[0];
|
||||
$query = $query[0];
|
||||
|
||||
// If protocol is missing and domain contains non-ASCII characters,
|
||||
// extract ASCII-only domains.
|
||||
if (empty($protocol)) {
|
||||
if (!$this->extractURLWithoutProtocol || preg_match(self::$patterns['invalid_url_without_protocol_preceding_chars'], $before)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$last_url = null;
|
||||
$ascii_end_position = 0;
|
||||
|
||||
if (preg_match(self::$patterns['valid_ascii_domain'], $domain, $asciiDomain)) {
|
||||
$asciiDomain[0] = preg_replace('/' . preg_quote($domain, '/') . '/u', $asciiDomain[0], $url);
|
||||
$ascii_start_position = StringUtils::strpos($domain, $asciiDomain[0], $ascii_end_position);
|
||||
$ascii_end_position = $ascii_start_position + StringUtils::strlen($asciiDomain[0]);
|
||||
$last_url = array(
|
||||
'url' => $asciiDomain[0],
|
||||
'indices' => array($start_position + $ascii_start_position, $start_position + $ascii_end_position),
|
||||
);
|
||||
if (!empty($path)
|
||||
|| preg_match(self::$patterns['valid_special_short_domain'], $asciiDomain[0])
|
||||
|| !preg_match(self::$patterns['invalid_short_domain'], $asciiDomain[0])) {
|
||||
$urls[] = $last_url;
|
||||
}
|
||||
}
|
||||
|
||||
// no ASCII-only domain found. Skip the entire URL
|
||||
if (empty($last_url)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// $last_url only contains domain. Need to add path and query if they exist.
|
||||
if (!empty($path)) {
|
||||
// last_url was not added. Add it to urls here.
|
||||
$last_url['url'] = preg_replace('/' . preg_quote($domain, '/') . '/u', $last_url['url'], $url);
|
||||
$last_url['indices'][1] = $end_position;
|
||||
}
|
||||
} else {
|
||||
// In the case of t.co URLs, don't allow additional path characters
|
||||
if (preg_match(self::$patterns['valid_tco_url'], $url, $tcoUrlMatches)) {
|
||||
$url = $tcoUrlMatches[0];
|
||||
$end_position = $start_position + StringUtils::strlen($url);
|
||||
}
|
||||
$urls[] = array(
|
||||
'url' => $url,
|
||||
'indices' => array($start_position, $end_position),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $urls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the usernames and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The username elements in the tweet.
|
||||
*/
|
||||
public function extractMentionedScreennamesWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
$usernamesOnly = array();
|
||||
$mentions = $this->extractMentionsOrListsWithIndices($tweet);
|
||||
foreach ($mentions as $mention) {
|
||||
if (isset($mention['list_slug'])) {
|
||||
unset($mention['list_slug']);
|
||||
}
|
||||
$usernamesOnly[] = $mention;
|
||||
}
|
||||
return $usernamesOnly;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the usernames and the indices they occur at from the tweet.
|
||||
*
|
||||
* @return array The username elements in the tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function extractMentionedUsernamesWithIndices()
|
||||
{
|
||||
return $this->extractMentionedScreennamesWithIndices();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the usernames and the indices they occur at from the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to extract.
|
||||
* @return array The username elements in the tweet.
|
||||
*/
|
||||
public function extractMentionsOrListsWithIndices($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
|
||||
if (!preg_match('/[@@]/iu', $tweet)) {
|
||||
return array();
|
||||
}
|
||||
|
||||
preg_match_all(self::$patterns['valid_mentions_or_lists'], $tweet, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
|
||||
$results = array();
|
||||
|
||||
foreach ($matches as $match) {
|
||||
list($all, $before, $at, $username, $list_slug, $outer) = array_pad($match, 6, array('', 0));
|
||||
$start_position = $at[1] > 0 ? StringUtils::strlen(substr($tweet, 0, $at[1])) : $at[1];
|
||||
$end_position = $start_position + StringUtils::strlen($at[0]) + StringUtils::strlen($username[0]);
|
||||
$entity = array(
|
||||
'screen_name' => $username[0],
|
||||
'list_slug' => $list_slug[0],
|
||||
'indices' => array($start_position, $end_position),
|
||||
);
|
||||
|
||||
if (preg_match(self::$patterns['end_mention_match'], $outer[0])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!empty($list_slug[0])) {
|
||||
$entity['indices'][1] = $end_position + StringUtils::strlen($list_slug[0]);
|
||||
}
|
||||
|
||||
$results[] = $entity;
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts all the usernames and the indices they occur at from the tweet.
|
||||
*
|
||||
* @return array The username elements in the tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function extractMentionedUsernamesOrListsWithIndices()
|
||||
{
|
||||
return $this->extractMentionsOrListsWithIndices();
|
||||
}
|
||||
|
||||
/**
|
||||
* setter/getter for extractURLWithoutProtocol
|
||||
*
|
||||
* @param boolean $flag
|
||||
* @return Extractor
|
||||
*/
|
||||
public function extractURLWithoutProtocol($flag = null)
|
||||
{
|
||||
if (is_null($flag)) {
|
||||
return $this->extractURLWithoutProtocol;
|
||||
}
|
||||
$this->extractURLWithoutProtocol = (bool) $flag;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove overlapping entities.
|
||||
* This returns a new array with no overlapping entities.
|
||||
*
|
||||
* @param array $entities
|
||||
* @return array
|
||||
*/
|
||||
public function removeOverlappingEntities($entities)
|
||||
{
|
||||
$result = array();
|
||||
usort($entities, array($this, 'sortEntites'));
|
||||
|
||||
$prev = null;
|
||||
foreach ($entities as $entity) {
|
||||
if (isset($prev) && $entity['indices'][0] < $prev['indices'][1]) {
|
||||
continue;
|
||||
}
|
||||
$prev = $entity;
|
||||
$result[] = $entity;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* sort by entity start index
|
||||
*
|
||||
* @param array $a
|
||||
* @param array $b
|
||||
* @return int
|
||||
*/
|
||||
protected function sortEntites($a, $b)
|
||||
{
|
||||
if ($a['indices'][0] == $b['indices'][0]) {
|
||||
return 0;
|
||||
}
|
||||
return ($a['indices'][0] < $b['indices'][0]) ? -1 : 1;
|
||||
}
|
||||
}
|
|
@ -7,7 +7,7 @@ class Hashtag {
|
|||
public static function getHashtags($status)
|
||||
{
|
||||
$hashtags = false;
|
||||
preg_match_all("/(#\w+)/u", $status, $matches);
|
||||
preg_match_all("/(?<!&)(#\w+)/u", $status, $matches);
|
||||
if ($matches) {
|
||||
$res = array_count_values($matches[0]);
|
||||
$hashtags = array_keys($res);
|
||||
|
|
202
app/Util/Lexer/HitHighlighter.php
Executable file
202
app/Util/Lexer/HitHighlighter.php
Executable file
|
@ -0,0 +1,202 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Regex;
|
||||
use App\Util\Lexer\StringUtils;
|
||||
|
||||
/**
|
||||
* Twitter HitHighlighter Class
|
||||
*
|
||||
* Performs "hit highlighting" on tweets that have been auto-linked already.
|
||||
* Useful with the results returned from the search API.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
class HitHighlighter extends Regex
|
||||
{
|
||||
|
||||
/**
|
||||
* The tag to surround hits with.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $tag = 'em';
|
||||
|
||||
/**
|
||||
* Provides fluent method chaining.
|
||||
*
|
||||
* @param string $tweet The tweet to be hit highlighted.
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*
|
||||
* @see __construct()
|
||||
*
|
||||
* @return HitHighlighter
|
||||
*/
|
||||
public static function create($tweet = null, $full_encode = false)
|
||||
{
|
||||
return new self($tweet, $full_encode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in a tweet to be parsed and hit highlighted.
|
||||
*
|
||||
* We take this opportunity to ensure that we escape user input.
|
||||
*
|
||||
* @see htmlspecialchars()
|
||||
*
|
||||
* @param string $tweet The tweet to be hit highlighted.
|
||||
* @param bool $escape Whether to escape the tweet (default: true).
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*/
|
||||
public function __construct($tweet = null, $escape = true, $full_encode = false)
|
||||
{
|
||||
if (!empty($tweet) && $escape) {
|
||||
if ($full_encode) {
|
||||
parent::__construct(htmlentities($tweet, ENT_QUOTES, 'UTF-8', false));
|
||||
} else {
|
||||
parent::__construct(htmlspecialchars($tweet, ENT_QUOTES, 'UTF-8', false));
|
||||
}
|
||||
} else {
|
||||
parent::__construct($tweet);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the highlighting tag to surround hits with. The default tag is 'em'.
|
||||
*
|
||||
* @return string The tag name.
|
||||
*/
|
||||
public function getTag()
|
||||
{
|
||||
return $this->tag;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the highlighting tag to surround hits with. The default tag is 'em'.
|
||||
*
|
||||
* @param string $v The tag name.
|
||||
*
|
||||
* @return HitHighlighter Fluid method chaining.
|
||||
*/
|
||||
public function setTag($v)
|
||||
{
|
||||
$this->tag = $v;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit highlights the tweet.
|
||||
*
|
||||
* @param string $tweet The tweet to be hit highlighted.
|
||||
* @param array $hits An array containing the start and end index pairs
|
||||
* for the highlighting.
|
||||
* @param bool $escape Whether to escape the tweet (default: true).
|
||||
* @param bool $full_encode Whether to encode all special characters.
|
||||
*
|
||||
* @return string The hit highlighted tweet.
|
||||
*/
|
||||
public function highlight($tweet = null, array $hits = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
if (empty($hits)) {
|
||||
return $tweet;
|
||||
}
|
||||
$highlightTweet = '';
|
||||
$tags = array('<' . $this->tag . '>', '</' . $this->tag . '>');
|
||||
# Check whether we can simply replace or whether we need to chunk...
|
||||
if (strpos($tweet, '<') === false) {
|
||||
$ti = 0; // tag increment (for added tags)
|
||||
$highlightTweet = $tweet;
|
||||
foreach ($hits as $hit) {
|
||||
$highlightTweet = StringUtils::substrReplace($highlightTweet, $tags[0], $hit[0] + $ti, 0);
|
||||
$ti += StringUtils::strlen($tags[0]);
|
||||
$highlightTweet = StringUtils::substrReplace($highlightTweet, $tags[1], $hit[1] + $ti, 0);
|
||||
$ti += StringUtils::strlen($tags[1]);
|
||||
}
|
||||
} else {
|
||||
$chunks = preg_split('/[<>]/iu', $tweet);
|
||||
$chunk = $chunks[0];
|
||||
$chunk_index = 0;
|
||||
$chunk_cursor = 0;
|
||||
$offset = 0;
|
||||
$start_in_chunk = false;
|
||||
# Flatten the multidimensional hits array:
|
||||
$hits_flat = array();
|
||||
foreach ($hits as $hit) {
|
||||
$hits_flat = array_merge($hits_flat, $hit);
|
||||
}
|
||||
# Loop over the hit indices:
|
||||
for ($index = 0; $index < count($hits_flat); $index++) {
|
||||
$hit = $hits_flat[$index];
|
||||
$tag = $tags[$index % 2];
|
||||
$placed = false;
|
||||
while ($chunk !== null && $hit >= ($i = $offset + StringUtils::strlen($chunk))) {
|
||||
$highlightTweet .= StringUtils::substr($chunk, $chunk_cursor);
|
||||
if ($start_in_chunk && $hit === $i) {
|
||||
$highlightTweet .= $tag;
|
||||
$placed = true;
|
||||
}
|
||||
if (isset($chunks[$chunk_index + 1])) {
|
||||
$highlightTweet .= '<' . $chunks[$chunk_index + 1] . '>';
|
||||
}
|
||||
$offset += StringUtils::strlen($chunk);
|
||||
$chunk_cursor = 0;
|
||||
$chunk_index += 2;
|
||||
$chunk = (isset($chunks[$chunk_index]) ? $chunks[$chunk_index] : null);
|
||||
$start_in_chunk = false;
|
||||
}
|
||||
if (!$placed && $chunk !== null) {
|
||||
$hit_spot = $hit - $offset;
|
||||
$highlightTweet .= StringUtils::substr($chunk, $chunk_cursor, $hit_spot - $chunk_cursor) . $tag;
|
||||
$chunk_cursor = $hit_spot;
|
||||
$start_in_chunk = ($index % 2 === 0);
|
||||
$placed = true;
|
||||
}
|
||||
# Ultimate fallback - hits that run off the end get a closing tag:
|
||||
if (!$placed) {
|
||||
$highlightTweet .= $tag;
|
||||
}
|
||||
}
|
||||
if ($chunk !== null) {
|
||||
if ($chunk_cursor < StringUtils::strlen($chunk)) {
|
||||
$highlightTweet .= StringUtils::substr($chunk, $chunk_cursor);
|
||||
}
|
||||
for ($index = $chunk_index + 1; $index < count($chunks); $index++) {
|
||||
$highlightTweet .= ($index % 2 === 0 ? $chunks[$index] : '<' . $chunks[$index] . '>');
|
||||
}
|
||||
}
|
||||
}
|
||||
return $highlightTweet;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hit highlights the tweet.
|
||||
*
|
||||
* @param array $hits An array containing the start and end index pairs
|
||||
* for the highlighting.
|
||||
*
|
||||
* @return string The hit highlighted tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function addHitHighlighting(array $hits)
|
||||
{
|
||||
return $this->highlight($this->tweet, $hits);
|
||||
}
|
||||
}
|
348
app/Util/Lexer/LooseAutolink.php
Executable file
348
app/Util/Lexer/LooseAutolink.php
Executable file
|
@ -0,0 +1,348 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @author Takashi Nojima
|
||||
* @copyright Copyright 2014 Mike Cochrane, Nick Pope, Takashi Nojima
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Autolink;
|
||||
|
||||
/**
|
||||
* Twitter LooseAutolink Class
|
||||
*
|
||||
* Parses tweets and generates HTML anchor tags around URLs, usernames,
|
||||
* username/list pairs and hashtags.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Mike Cochrane <mikec@mikenz.geek.nz>
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @author Takashi Nojima
|
||||
* @copyright Copyright 2014 Mike Cochrane, Nick Pope, Takashi Nojima
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
* @since 1.8.0
|
||||
* @deprecated since version 1.9.0
|
||||
*/
|
||||
class LooseAutolink extends Autolink
|
||||
{
|
||||
|
||||
/**
|
||||
* Auto-link hashtags, URLs, usernames and lists.
|
||||
*
|
||||
* @param string The tweet to be converted
|
||||
* @return string that auto-link HTML added
|
||||
* @deprecated since version 1.9.0
|
||||
*/
|
||||
public function autoLink($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinks();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link the @username and @username/list references in the provided text. Links to @username references will
|
||||
* have the usernameClass CSS classes added. Links to @username/list references will have the listClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
*/
|
||||
public function autoLinkUsernamesAndLists($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinksToUsernamesAndLists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link #hashtag references in the provided Tweet text. The #hashtag links will have the hashtagClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
*/
|
||||
public function autoLinkHashtags($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinksToHashtags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link URLs in the Tweet text provided.
|
||||
* <p/>
|
||||
* This only auto-links URLs with protocol.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
*/
|
||||
public function autoLinkURLs($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinksToURLs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-link $cashtag references in the provided Tweet text. The $cashtag links will have the cashtagClass CSS class
|
||||
* added.
|
||||
*
|
||||
* @return string that auto-link HTML added
|
||||
*/
|
||||
public function autoLinkCashtags($tweet = null)
|
||||
{
|
||||
if (!is_null($tweet)) {
|
||||
$this->tweet = $tweet;
|
||||
}
|
||||
return $this->addLinksToCashtags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to all elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet.
|
||||
* @deprecated since version 1.9.0
|
||||
*/
|
||||
public function addLinks()
|
||||
{
|
||||
$original = $this->tweet;
|
||||
$this->tweet = $this->addLinksToURLs();
|
||||
$this->tweet = $this->addLinksToHashtags();
|
||||
$this->tweet = $this->addLinksToCashtags();
|
||||
$this->tweet = $this->addLinksToUsernamesAndLists();
|
||||
$modified = $this->tweet;
|
||||
$this->tweet = $original;
|
||||
return $modified;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to hashtag elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet.
|
||||
*/
|
||||
public function addLinksToHashtags()
|
||||
{
|
||||
return preg_replace_callback(
|
||||
self::$patterns['valid_hashtag'],
|
||||
array($this, '_addLinksToHashtags'),
|
||||
$this->tweet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to cashtag elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet.
|
||||
*/
|
||||
public function addLinksToCashtags()
|
||||
{
|
||||
return preg_replace_callback(
|
||||
self::$patterns['valid_cashtag'],
|
||||
array($this, '_addLinksToCashtags'),
|
||||
$this->tweet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to URL elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet
|
||||
*/
|
||||
public function addLinksToURLs()
|
||||
{
|
||||
return preg_replace_callback(self::$patterns['valid_url'], array($this, '_addLinksToURLs'), $this->tweet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds links to username/list elements in the tweet.
|
||||
*
|
||||
* @return string The modified tweet.
|
||||
*/
|
||||
public function addLinksToUsernamesAndLists()
|
||||
{
|
||||
return preg_replace_callback(
|
||||
self::$patterns['valid_mentions_or_lists'],
|
||||
array($this, '_addLinksToUsernamesAndLists'),
|
||||
$this->tweet
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a tweet element in an HTML anchor tag using the provided URL.
|
||||
*
|
||||
* This is a helper function to perform the generation of the link.
|
||||
*
|
||||
* @param string $url The URL to use as the href.
|
||||
* @param string $class The CSS class(es) to apply (space separated).
|
||||
* @param string $element The tweet element to wrap.
|
||||
*
|
||||
* @return string The tweet element with a link applied.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
protected function wrap($url, $class, $element)
|
||||
{
|
||||
$link = '<a';
|
||||
if ($class) {
|
||||
$link .= ' class="' . $class . '"';
|
||||
}
|
||||
$link .= ' href="' . $url . '"';
|
||||
$rel = array();
|
||||
if ($this->external) {
|
||||
$rel[] = 'external';
|
||||
}
|
||||
if ($this->nofollow) {
|
||||
$rel[] = 'nofollow';
|
||||
}
|
||||
if (!empty($rel)) {
|
||||
$link .= ' rel="' . implode(' ', $rel) . '"';
|
||||
}
|
||||
if ($this->target) {
|
||||
$link .= ' target="' . $this->target . '"';
|
||||
}
|
||||
$link .= '>' . $element . '</a>';
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a tweet element in an HTML anchor tag using the provided URL.
|
||||
*
|
||||
* This is a helper function to perform the generation of the hashtag link.
|
||||
*
|
||||
* @param string $url The URL to use as the href.
|
||||
* @param string $class The CSS class(es) to apply (space separated).
|
||||
* @param string $element The tweet element to wrap.
|
||||
*
|
||||
* @return string The tweet element with a link applied.
|
||||
*/
|
||||
protected function wrapHash($url, $class, $element)
|
||||
{
|
||||
$title = preg_replace('/#/u', '#', $element);
|
||||
$link = '<a';
|
||||
$link .= ' href="' . $url . '"';
|
||||
$link .= ' title="' . $title . '"';
|
||||
if ($class) {
|
||||
$link .= ' class="' . $class . '"';
|
||||
}
|
||||
$rel = array();
|
||||
if ($this->external) {
|
||||
$rel[] = 'external';
|
||||
}
|
||||
if ($this->nofollow) {
|
||||
$rel[] = 'nofollow';
|
||||
}
|
||||
if (!empty($rel)) {
|
||||
$link .= ' rel="' . implode(' ', $rel) . '"';
|
||||
}
|
||||
if ($this->target) {
|
||||
$link .= ' target="' . $this->target . '"';
|
||||
}
|
||||
$link .= '>' . $element . '</a>';
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by the method that adds links to hashtags.
|
||||
*
|
||||
* @see addLinksToHashtags()
|
||||
* @param array $matches The regular expression matches.
|
||||
* @return string The link-wrapped hashtag.
|
||||
*/
|
||||
protected function _addLinksToHashtags($matches)
|
||||
{
|
||||
list($all, $before, $hash, $tag, $after) = array_pad($matches, 5, '');
|
||||
if (preg_match(self::$patterns['end_hashtag_match'], $after)
|
||||
|| (!preg_match('!\A["\']!', $before) && preg_match('!\A["\']!', $after)) || preg_match('!\A</!', $after)) {
|
||||
return $all;
|
||||
}
|
||||
$replacement = $before;
|
||||
$element = $hash . $tag;
|
||||
$url = $this->url_base_hash . $tag;
|
||||
$class_hash = $this->class_hash;
|
||||
if (preg_match(self::$patterns['rtl_chars'], $element)) {
|
||||
$class_hash .= ' rtl';
|
||||
}
|
||||
$replacement .= $this->wrapHash($url, $class_hash, $element);
|
||||
return $replacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by the method that adds links to cashtags.
|
||||
*
|
||||
* @see addLinksToCashtags()
|
||||
* @param array $matches The regular expression matches.
|
||||
* @return string The link-wrapped cashtag.
|
||||
*/
|
||||
protected function _addLinksToCashtags($matches)
|
||||
{
|
||||
list($all, $before, $cash, $tag, $after) = array_pad($matches, 5, '');
|
||||
if (preg_match(self::$patterns['end_cashtag_match'], $after)
|
||||
|| (!preg_match('!\A["\']!', $before) && preg_match('!\A["\']!', $after)) || preg_match('!\A</!', $after)) {
|
||||
return $all;
|
||||
}
|
||||
$replacement = $before;
|
||||
$element = $cash . $tag;
|
||||
$url = $this->url_base_cash . $tag;
|
||||
$replacement .= $this->wrapHash($url, $this->class_cash, $element);
|
||||
return $replacement;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by the method that adds links to URLs.
|
||||
*
|
||||
* @see addLinksToURLs()
|
||||
* @param array $matches The regular expression matches.
|
||||
* @return string The link-wrapped URL.
|
||||
*/
|
||||
protected function _addLinksToURLs($matches)
|
||||
{
|
||||
list($all, $before, $url, $protocol, $domain, $path, $query) = array_pad($matches, 7, '');
|
||||
$url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8', false);
|
||||
if (!$protocol) {
|
||||
return $all;
|
||||
}
|
||||
return $before . $this->wrap($url, $this->class_url, $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback used by the method that adds links to username/list pairs.
|
||||
*
|
||||
* @see addLinksToUsernamesAndLists()
|
||||
* @param array $matches The regular expression matches.
|
||||
* @return string The link-wrapped username/list pair.
|
||||
*/
|
||||
protected function _addLinksToUsernamesAndLists($matches)
|
||||
{
|
||||
list($all, $before, $at, $username, $slash_listname, $after) = array_pad($matches, 6, '');
|
||||
# If $after is not empty, there is an invalid character.
|
||||
if (!empty($slash_listname)) {
|
||||
# Replace the list and username
|
||||
$element = $username . $slash_listname;
|
||||
$class = $this->class_list;
|
||||
$url = $this->url_base_list . $element;
|
||||
} else {
|
||||
if (preg_match(self::$patterns['end_mention_match'], $after)) {
|
||||
return $all;
|
||||
}
|
||||
# Replace the username
|
||||
$element = $username;
|
||||
$class = $this->class_user;
|
||||
$url = $this->url_base_user . $element;
|
||||
}
|
||||
# XXX: Due to use of preg_replace_callback() for multiple replacements in a
|
||||
# single tweet and also as only the match is replaced and we have to
|
||||
# use a look-ahead for $after because there is no equivalent for the
|
||||
# $' (dollar apostrophe) global from Ruby, we MUST NOT append $after.
|
||||
return $before . $at . $this->wrap($url, $class, $element);
|
||||
}
|
||||
}
|
36
app/Util/Lexer/PrettyNumber.php
Normal file
36
app/Util/Lexer/PrettyNumber.php
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
class PrettyNumber {
|
||||
|
||||
public static function convert($expression)
|
||||
{
|
||||
$abbrevs = array(12 => "T", 9 => "B", 6 => "M", 3 => "K", 0 => "");
|
||||
foreach($abbrevs as $exponent => $abbrev) {
|
||||
if($expression >= pow(10, $exponent)) {
|
||||
$display_num = $expression / pow(10, $exponent);
|
||||
$num = number_format($display_num,0) . $abbrev;
|
||||
return $num;
|
||||
}
|
||||
}
|
||||
return $expression;
|
||||
}
|
||||
|
||||
public static function size($expression, $kb = false)
|
||||
{
|
||||
if($kb) {
|
||||
$expression = $expression * 1024;
|
||||
}
|
||||
$size = intval($expression);
|
||||
$precision = 0;
|
||||
$short = true;
|
||||
$units = $short ?
|
||||
['B','k','M','G','T','P','E','Z','Y'] :
|
||||
['B','kB','MB','GB','TB','PB','EB','ZB','YB'];
|
||||
for($i = 0; ($size / 1024) > 0.9; $i++, $size /= 1024) {}
|
||||
$res = round($size, $precision).$units[$i];
|
||||
return $res;
|
||||
}
|
||||
|
||||
}
|
337
app/Util/Lexer/Regex.php
Executable file
337
app/Util/Lexer/Regex.php
Executable file
File diff suppressed because one or more lines are too long
|
@ -55,8 +55,6 @@ class RestrictedNames {
|
|||
"js",
|
||||
"localdomain",
|
||||
"localhost",
|
||||
"login",
|
||||
"logout",
|
||||
"mail",
|
||||
"mailer-daemon",
|
||||
"mailerdaemon",
|
||||
|
@ -108,8 +106,6 @@ class RestrictedNames {
|
|||
"tutorial",
|
||||
"tutorials",
|
||||
"usenet",
|
||||
"user",
|
||||
"users",
|
||||
"uucp",
|
||||
"webmaster",
|
||||
"wpad",
|
||||
|
@ -126,13 +122,23 @@ class RestrictedNames {
|
|||
// Laravel Horizon
|
||||
"horizon",
|
||||
|
||||
// Reserved route
|
||||
// Reserved routes
|
||||
"account",
|
||||
"api",
|
||||
"auth",
|
||||
"i",
|
||||
|
||||
// Official accounts
|
||||
"pixelfed",
|
||||
"pixelfed-support",
|
||||
"pixelfed_support",
|
||||
"discover",
|
||||
"home",
|
||||
"login",
|
||||
"logout",
|
||||
"p",
|
||||
"password",
|
||||
"search",
|
||||
"settings",
|
||||
"site",
|
||||
"timeline",
|
||||
"user",
|
||||
"users",
|
||||
];
|
||||
|
||||
public static function get()
|
||||
|
|
104
app/Util/Lexer/StringUtils.php
Executable file
104
app/Util/Lexer/StringUtils.php
Executable file
|
@ -0,0 +1,104 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Takashi Nojima
|
||||
* @copyright Copyright 2014, Takashi Nojima
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
/**
|
||||
* String utility
|
||||
*
|
||||
* @author Takashi Nojima
|
||||
* @copyright Copyright 2014, Takashi Nojima
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter
|
||||
*/
|
||||
class StringUtils
|
||||
{
|
||||
|
||||
/**
|
||||
* alias of mb_substr
|
||||
*
|
||||
* @param string $str
|
||||
* @param integer $start
|
||||
* @param integer $length
|
||||
* @param string $encoding
|
||||
* @return string
|
||||
*/
|
||||
public static function substr($str, $start, $length = null, $encoding = 'UTF-8')
|
||||
{
|
||||
if (is_null($length)) {
|
||||
// for PHP <= 5.4.7
|
||||
$length = mb_strlen($str, $encoding);
|
||||
}
|
||||
return mb_substr($str, $start, $length, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* alias of mb_strlen
|
||||
*
|
||||
* @param string $str
|
||||
* @param string $encoding
|
||||
* @return integer
|
||||
*/
|
||||
public static function strlen($str, $encoding = 'UTF-8')
|
||||
{
|
||||
return mb_strlen($str, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* alias of mb_strpos
|
||||
*
|
||||
* @param string $haystack
|
||||
* @param string $needle
|
||||
* @param integer $offset
|
||||
* @param string $encoding
|
||||
* @return integer
|
||||
*/
|
||||
public static function strpos($haystack, $needle, $offset = 0, $encoding = 'UTF-8')
|
||||
{
|
||||
return mb_strpos($haystack, $needle, $offset, $encoding);
|
||||
}
|
||||
|
||||
/**
|
||||
* A multibyte-aware substring replacement function.
|
||||
*
|
||||
* @param string $string The string to modify.
|
||||
* @param string $replacement The replacement string.
|
||||
* @param int $start The start of the replacement.
|
||||
* @param int $length The number of characters to replace.
|
||||
* @param string $encoding The encoding of the string.
|
||||
*
|
||||
* @return string The modified string.
|
||||
*
|
||||
* @see http://www.php.net/manual/en/function.substr-replace.php#90146
|
||||
*/
|
||||
public static function substrReplace($string, $replacement, $start, $length = null, $encoding = 'UTF-8')
|
||||
{
|
||||
if (extension_loaded('mbstring') === true) {
|
||||
$string_length = static::strlen($string, $encoding);
|
||||
if ($start < 0) {
|
||||
$start = max(0, $string_length + $start);
|
||||
} elseif ($start > $string_length) {
|
||||
$start = $string_length;
|
||||
}
|
||||
if ($length < 0) {
|
||||
$length = max(0, $string_length - $start + $length);
|
||||
} elseif ((is_null($length) === true) || ($length > $string_length)) {
|
||||
$length = $string_length;
|
||||
}
|
||||
if (($start + $length) > $string_length) {
|
||||
$length = $string_length - $start;
|
||||
}
|
||||
|
||||
$suffixOffset = $start + $length;
|
||||
$suffixLength = $string_length - $start - $length;
|
||||
return static::substr($string, 0, $start, $encoding) . $replacement . static::substr($string, $suffixOffset, $suffixLength, $encoding);
|
||||
}
|
||||
return (is_null($length) === true) ? substr_replace($string, $replacement, $start) : substr_replace($string, $replacement, $start, $length);
|
||||
}
|
||||
}
|
388
app/Util/Lexer/Validator.php
Executable file
388
app/Util/Lexer/Validator.php
Executable file
|
@ -0,0 +1,388 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
|
||||
namespace App\Util\Lexer;
|
||||
|
||||
use App\Util\Lexer\Regex;
|
||||
use App\Util\Lexer\Extractor;
|
||||
use App\Util\Lexer\StringUtils;
|
||||
|
||||
/**
|
||||
* Twitter Validator Class
|
||||
*
|
||||
* Performs "validation" on tweets.
|
||||
*
|
||||
* Originally written by {@link http://github.com/mikenz Mike Cochrane}, this
|
||||
* is based on code by {@link http://github.com/mzsanford Matt Sanford} and
|
||||
* heavily modified by {@link http://github.com/ngnpope Nick Pope}.
|
||||
*
|
||||
* @author Nick Pope <nick@nickpope.me.uk>
|
||||
* @copyright Copyright © 2010, Nick Pope
|
||||
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License v2.0
|
||||
* @package Twitter.Text
|
||||
*/
|
||||
class Validator extends Regex
|
||||
{
|
||||
|
||||
/**
|
||||
* The maximum length of a tweet.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const MAX_LENGTH = 140;
|
||||
|
||||
/**
|
||||
* The length of a short URL beginning with http:
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $short_url_length = 23;
|
||||
|
||||
/**
|
||||
* The length of a short URL beginning with http:
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $short_url_length_https = 23;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var Extractor
|
||||
*/
|
||||
protected $extractor = null;
|
||||
|
||||
/**
|
||||
* Provides fluent method chaining.
|
||||
*
|
||||
* @param string $tweet The tweet to be validated.
|
||||
* @param mixed $config Setup short URL length from Twitter API /help/configuration response.
|
||||
*
|
||||
* @see __construct()
|
||||
*
|
||||
* @return Validator
|
||||
*/
|
||||
public static function create($tweet = null, $config = null)
|
||||
{
|
||||
return new self($tweet, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads in a tweet to be parsed and validates it.
|
||||
*
|
||||
* @param string $tweet The tweet to validate.
|
||||
*/
|
||||
public function __construct($tweet = null, $config = null)
|
||||
{
|
||||
parent::__construct($tweet);
|
||||
if (!empty($config)) {
|
||||
$this->setConfiguration($config);
|
||||
}
|
||||
$this->extractor = Extractor::create();
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup short URL length from Twitter API /help/configuration response
|
||||
*
|
||||
* @param mixed $config
|
||||
* @return Validator
|
||||
* @link https://dev.twitter.com/docs/api/1/get/help/configuration
|
||||
*/
|
||||
public function setConfiguration($config)
|
||||
{
|
||||
if (is_array($config)) {
|
||||
// setup from array
|
||||
if (isset($config['short_url_length'])) {
|
||||
$this->setShortUrlLength($config['short_url_length']);
|
||||
}
|
||||
if (isset($config['short_url_length_https'])) {
|
||||
$this->setShortUrlLengthHttps($config['short_url_length_https']);
|
||||
}
|
||||
} elseif (is_object($config)) {
|
||||
// setup from object
|
||||
if (isset($config->short_url_length)) {
|
||||
$this->setShortUrlLength($config->short_url_length);
|
||||
}
|
||||
if (isset($config->short_url_length_https)) {
|
||||
$this->setShortUrlLengthHttps($config->short_url_length_https);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the length of a short URL beginning with http:
|
||||
*
|
||||
* @param mixed $length
|
||||
* @return Validator
|
||||
*/
|
||||
public function setShortUrlLength($length)
|
||||
{
|
||||
$this->short_url_length = intval($length);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of a short URL beginning with http:
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getShortUrlLength()
|
||||
{
|
||||
return $this->short_url_length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the length of a short URL beginning with https:
|
||||
*
|
||||
* @param mixed $length
|
||||
* @return Validator
|
||||
*/
|
||||
public function setShortUrlLengthHttps($length)
|
||||
{
|
||||
$this->short_url_length_https = intval($length);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the length of a short URL beginning with https:
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getShortUrlLengthHttps()
|
||||
{
|
||||
return $this->short_url_length_https;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a tweet is valid.
|
||||
*
|
||||
* @param string $tweet The tweet to validate.
|
||||
* @return boolean Whether the tweet is valid.
|
||||
*/
|
||||
public function isValidTweetText($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$length = $this->getTweetLength($tweet);
|
||||
if (!$tweet || !$length) {
|
||||
return false;
|
||||
}
|
||||
if ($length > self::MAX_LENGTH) {
|
||||
return false;
|
||||
}
|
||||
if (preg_match(self::$patterns['invalid_characters'], $tweet)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a tweet is valid.
|
||||
*
|
||||
* @return boolean Whether the tweet is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateTweet()
|
||||
{
|
||||
return $this->isValidTweetText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a username is valid.
|
||||
*
|
||||
* @param string $username The username to validate.
|
||||
* @return boolean Whether the username is valid.
|
||||
*/
|
||||
public function isValidUsername($username = null)
|
||||
{
|
||||
if (is_null($username)) {
|
||||
$username = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($username);
|
||||
if (empty($username) || !$length) {
|
||||
return false;
|
||||
}
|
||||
$extracted = $this->extractor->extractMentionedScreennames($username);
|
||||
return count($extracted) === 1 && $extracted[0] === substr($username, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a username is valid.
|
||||
*
|
||||
* @return boolean Whether the username is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateUsername()
|
||||
{
|
||||
return $this->isValidUsername();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a list is valid.
|
||||
*
|
||||
* @param string $list The list name to validate.
|
||||
* @return boolean Whether the list is valid.
|
||||
*/
|
||||
public function isValidList($list = null)
|
||||
{
|
||||
if (is_null($list)) {
|
||||
$list = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($list);
|
||||
if (empty($list) || !$length) {
|
||||
return false;
|
||||
}
|
||||
preg_match(self::$patterns['valid_mentions_or_lists'], $list, $matches);
|
||||
$matches = array_pad($matches, 5, '');
|
||||
return isset($matches) && $matches[1] === '' && $matches[4] && !empty($matches[4]) && $matches[5] === '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a list is valid.
|
||||
*
|
||||
* @return boolean Whether the list is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateList()
|
||||
{
|
||||
return $this->isValidList();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a hashtag is valid.
|
||||
*
|
||||
* @param string $hashtag The hashtag to validate.
|
||||
* @return boolean Whether the hashtag is valid.
|
||||
*/
|
||||
public function isValidHashtag($hashtag = null)
|
||||
{
|
||||
if (is_null($hashtag)) {
|
||||
$hashtag = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($hashtag);
|
||||
if (empty($hashtag) || !$length) {
|
||||
return false;
|
||||
}
|
||||
$extracted = $this->extractor->extractHashtags($hashtag);
|
||||
return count($extracted) === 1 && $extracted[0] === substr($hashtag, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a hashtag is valid.
|
||||
*
|
||||
* @return boolean Whether the hashtag is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateHashtag()
|
||||
{
|
||||
return $this->isValidHashtag();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a URL is valid.
|
||||
*
|
||||
* @param string $url The url to validate.
|
||||
* @param boolean $unicode_domains Consider the domain to be unicode.
|
||||
* @param boolean $require_protocol Require a protocol for valid domain?
|
||||
*
|
||||
* @return boolean Whether the URL is valid.
|
||||
*/
|
||||
public function isValidURL($url = null, $unicode_domains = true, $require_protocol = true)
|
||||
{
|
||||
if (is_null($url)) {
|
||||
$url = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($url);
|
||||
if (empty($url) || !$length) {
|
||||
return false;
|
||||
}
|
||||
preg_match(self::$patterns['validate_url_unencoded'], $url, $matches);
|
||||
$match = array_shift($matches);
|
||||
if (!$matches || $match !== $url) {
|
||||
return false;
|
||||
}
|
||||
list($scheme, $authority, $path, $query, $fragment) = array_pad($matches, 5, '');
|
||||
# Check scheme, path, query, fragment:
|
||||
if (($require_protocol && !(
|
||||
self::isValidMatch($scheme, self::$patterns['validate_url_scheme']) && preg_match('/^https?$/i', $scheme))
|
||||
) || !self::isValidMatch($path, self::$patterns['validate_url_path']) || !self::isValidMatch($query, self::$patterns['validate_url_query'], true)
|
||||
|| !self::isValidMatch($fragment, self::$patterns['validate_url_fragment'], true)) {
|
||||
return false;
|
||||
}
|
||||
# Check authority:
|
||||
$authority_pattern = $unicode_domains ? 'validate_url_unicode_authority' : 'validate_url_authority';
|
||||
return self::isValidMatch($authority, self::$patterns[$authority_pattern]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a URL is valid.
|
||||
*
|
||||
* @param boolean $unicode_domains Consider the domain to be unicode.
|
||||
* @param boolean $require_protocol Require a protocol for valid domain?
|
||||
*
|
||||
* @return boolean Whether the URL is valid.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function validateURL($unicode_domains = true, $require_protocol = true)
|
||||
{
|
||||
return $this->isValidURL(null, $unicode_domains, $require_protocol);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the length of a tweet. Takes shortening of URLs into account.
|
||||
*
|
||||
* @param string $tweet The tweet to validate.
|
||||
* @return int the length of a tweet.
|
||||
*/
|
||||
public function getTweetLength($tweet = null)
|
||||
{
|
||||
if (is_null($tweet)) {
|
||||
$tweet = $this->tweet;
|
||||
}
|
||||
$length = StringUtils::strlen($tweet);
|
||||
$urls_with_indices = $this->extractor->extractURLsWithIndices($tweet);
|
||||
foreach ($urls_with_indices as $x) {
|
||||
$length += $x['indices'][0] - $x['indices'][1];
|
||||
$length += stripos($x['url'], 'https://') === 0 ? $this->short_url_length_https : $this->short_url_length;
|
||||
}
|
||||
return $length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the length of a tweet. Takes shortening of URLs into account.
|
||||
*
|
||||
* @return int the length of a tweet.
|
||||
* @deprecated since version 1.1.0
|
||||
*/
|
||||
public function getLength()
|
||||
{
|
||||
return $this->getTweetLength();
|
||||
}
|
||||
|
||||
/**
|
||||
* A helper function to check for a valid match. Used in URL validation.
|
||||
*
|
||||
* @param string $string The subject string to test.
|
||||
* @param string $pattern The pattern to match against.
|
||||
* @param boolean $optional Whether a match is compulsory or not.
|
||||
*
|
||||
* @return boolean Whether an exact match was found.
|
||||
*/
|
||||
protected static function isValidMatch($string, $pattern, $optional = false)
|
||||
{
|
||||
$found = preg_match($pattern, $string, $matches);
|
||||
if (!$optional) {
|
||||
return (($string || $string === '') && $found && $matches[0] === $string);
|
||||
} else {
|
||||
return !(($string || $string === '') && (!$found || $matches[0] !== $string));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -103,44 +103,31 @@ class Image {
|
|||
$ratio = $this->getAspectRatio($file, $thumbnail);
|
||||
$aspect = $ratio['dimensions'];
|
||||
$orientation = $ratio['orientation'];
|
||||
if($media->mime === 'image/gif' && !$thumbnail)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$img = Intervention::make($file);
|
||||
$img->fit($aspect['width'], $aspect['height'], function ($constraint) {
|
||||
$constraint->upsize();
|
||||
$img = Intervention::make($file)->orientate();
|
||||
$img->resize($aspect['width'], $aspect['height'], function ($constraint) {
|
||||
$constraint->aspectRatio();
|
||||
});
|
||||
$converted = $this->convertPngToJpeg($path, $thumbnail, $img->extension);
|
||||
$converted = $this->setBaseName($path, $thumbnail, $img->extension);
|
||||
$newPath = storage_path('app/'.$converted['path']);
|
||||
$is_png = false;
|
||||
|
||||
if($img->extension == 'png' || $converted['png'] == true) {
|
||||
\Log::info('PNG detected, ' . json_encode([$img, $converted]));
|
||||
$is_png = true;
|
||||
$newPath = str_replace('.png', '.jpeg', $newPath);
|
||||
$img->encode('jpg', 80)->save($newPath);
|
||||
if(!$thumbnail) {
|
||||
@unlink($file);
|
||||
}
|
||||
\Log::info('PNG SAVED, ' . json_encode([$img, $newPath]));
|
||||
} else {
|
||||
\Log::info('PNG not detected, ' . json_encode([$img, $converted]));
|
||||
$img->save($newPath, 75);
|
||||
}
|
||||
|
||||
if(!$thumbnail) {
|
||||
$media->orientation = $orientation;
|
||||
}
|
||||
|
||||
if($is_png == true) {
|
||||
if($thumbnail == false) {
|
||||
$media->media_path = $converted['path'];
|
||||
$media->mime = $img->mime;
|
||||
}
|
||||
}
|
||||
|
||||
if($thumbnail == true) {
|
||||
$media->thumbnail_path = $converted['path'];
|
||||
$media->thumbnail_url = url(Storage::url($converted['path']));
|
||||
} else {
|
||||
$media->media_path = $converted['path'];
|
||||
$media->mime = $img->mime;
|
||||
}
|
||||
|
||||
$media->save();
|
||||
|
@ -150,18 +137,14 @@ class Image {
|
|||
}
|
||||
}
|
||||
|
||||
public function convertPngToJpeg($basePath, $thumbnail = false, $extension)
|
||||
public function setBaseName($basePath, $thumbnail = false, $extension)
|
||||
{
|
||||
$png = false;
|
||||
$path = explode('.', $basePath);
|
||||
$name = ($thumbnail == true) ? $path[0] . '_thumb' : $path[0];
|
||||
$ext = last($path);
|
||||
$basePath = "{$name}.{$ext}";
|
||||
if($extension == 'png' || $ext == 'png') {
|
||||
$ext = 'jpeg';
|
||||
$basePath = "{$name}.{$ext}";
|
||||
$png = true;
|
||||
}
|
||||
|
||||
return ['path' => $basePath, 'png' => $png];
|
||||
}
|
||||
|
||||
|
|
10
app/WebSub.php
Normal file
10
app/WebSub.php
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class WebSub extends Model
|
||||
{
|
||||
//
|
||||
}
|
|
@ -8,14 +8,18 @@
|
|||
"php": "^7.1.3",
|
||||
"99designs/http-signatures-guzzlehttp": "^2.0",
|
||||
"bitverse/identicon": "^1.1",
|
||||
"doctrine/dbal": "^2.7",
|
||||
"fideloper/proxy": "^4.0",
|
||||
"greggilbert/recaptcha": "dev-master",
|
||||
"intervention/image": "^2.4",
|
||||
"kitetail/zttp": "^0.3.0",
|
||||
"laravel/framework": "5.6.*",
|
||||
"laravel/horizon": "^1.2",
|
||||
"laravel/tinker": "^1.0",
|
||||
"league/fractal": "^0.17.0",
|
||||
"phpseclib/phpseclib": "~2.0",
|
||||
"predis/predis": "^1.1",
|
||||
"spatie/laravel-backup": "^5.0.0",
|
||||
"spatie/laravel-image-optimizer": "^1.1",
|
||||
"spatie/laravel-partialcache": "^1.3"
|
||||
},
|
||||
|
|
1216
composer.lock
generated
1216
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -65,7 +65,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'timezone' => 'UTC',
|
||||
'timezone' => env('APP_TIMEZONE', 'UTC'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -78,7 +78,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'locale' => 'en',
|
||||
'locale' => env('APP_LOCALE', 'en'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -91,7 +91,7 @@ return [
|
|||
|
|
||||
*/
|
||||
|
||||
'fallback_locale' => 'en',
|
||||
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
@ -150,6 +150,7 @@ return [
|
|||
/*
|
||||
* Package Service Providers...
|
||||
*/
|
||||
Greggilbert\Recaptcha\RecaptchaServiceProvider::class,
|
||||
|
||||
/*
|
||||
* Application Service Providers...
|
||||
|
@ -209,6 +210,7 @@ return [
|
|||
'Validator' => Illuminate\Support\Facades\Validator::class,
|
||||
'View' => Illuminate\Support\Facades\View::class,
|
||||
|
||||
'Recaptcha' => Greggilbert\Recaptcha\Facades\Recaptcha::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
182
config/backup.php
Normal file
182
config/backup.php
Normal file
|
@ -0,0 +1,182 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
'backup' => [
|
||||
|
||||
/*
|
||||
* The name of this application. You can use this name to monitor
|
||||
* the backups.
|
||||
*/
|
||||
'name' => config('app.name'),
|
||||
|
||||
'source' => [
|
||||
|
||||
'files' => [
|
||||
|
||||
/*
|
||||
* The list of directories and files that will be included in the backup.
|
||||
*/
|
||||
'include' => [
|
||||
base_path(),
|
||||
],
|
||||
|
||||
/*
|
||||
* These directories and files will be excluded from the backup.
|
||||
*
|
||||
* Directories used by the backup process will automatically be excluded.
|
||||
*/
|
||||
'exclude' => [
|
||||
base_path('vendor'),
|
||||
base_path('node_modules'),
|
||||
],
|
||||
|
||||
/*
|
||||
* Determines if symlinks should be followed.
|
||||
*/
|
||||
'followLinks' => false,
|
||||
],
|
||||
|
||||
/*
|
||||
* The names of the connections to the databases that should be backed up
|
||||
* MySQL, PostgreSQL, SQLite and Mongo databases are supported.
|
||||
*/
|
||||
'databases' => [
|
||||
'mysql',
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* The database dump can be gzipped to decrease diskspace usage.
|
||||
*/
|
||||
'gzip_database_dump' => false,
|
||||
|
||||
'destination' => [
|
||||
|
||||
/*
|
||||
* The filename prefix used for the backup zip file.
|
||||
*/
|
||||
'filename_prefix' => '',
|
||||
|
||||
/*
|
||||
* The disk names on which the backups will be stored.
|
||||
*/
|
||||
'disks' => [
|
||||
'local',
|
||||
],
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* You can get notified when specific events occur. Out of the box you can use 'mail' and 'slack'.
|
||||
* For Slack you need to install guzzlehttp/guzzle.
|
||||
*
|
||||
* You can also use your own notification classes, just make sure the class is named after one of
|
||||
* the `Spatie\Backup\Events` classes.
|
||||
*/
|
||||
'notifications' => [
|
||||
|
||||
'notifications' => [
|
||||
\Spatie\Backup\Notifications\Notifications\BackupHasFailed::class => ['mail'],
|
||||
\Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFound::class => ['mail'],
|
||||
\Spatie\Backup\Notifications\Notifications\CleanupHasFailed::class => ['mail'],
|
||||
\Spatie\Backup\Notifications\Notifications\BackupWasSuccessful::class => ['mail'],
|
||||
\Spatie\Backup\Notifications\Notifications\HealthyBackupWasFound::class => ['mail'],
|
||||
\Spatie\Backup\Notifications\Notifications\CleanupWasSuccessful::class => ['mail'],
|
||||
],
|
||||
|
||||
/*
|
||||
* Here you can specify the notifiable to which the notifications should be sent. The default
|
||||
* notifiable will use the variables specified in this config file.
|
||||
*/
|
||||
'notifiable' => \Spatie\Backup\Notifications\Notifiable::class,
|
||||
|
||||
'mail' => [
|
||||
'to' => '',
|
||||
],
|
||||
|
||||
'slack' => [
|
||||
'webhook_url' => '',
|
||||
|
||||
/*
|
||||
* If this is set to null the default channel of the webhook will be used.
|
||||
*/
|
||||
'channel' => null,
|
||||
|
||||
'username' => null,
|
||||
|
||||
'icon' => null,
|
||||
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
* Here you can specify which backups should be monitored.
|
||||
* If a backup does not meet the specified requirements the
|
||||
* UnHealthyBackupWasFound event will be fired.
|
||||
*/
|
||||
'monitorBackups' => [
|
||||
[
|
||||
'name' => config('app.name'),
|
||||
'disks' => ['local'],
|
||||
'newestBackupsShouldNotBeOlderThanDays' => 1,
|
||||
'storageUsedMayNotBeHigherThanMegabytes' => 5000,
|
||||
],
|
||||
|
||||
/*
|
||||
[
|
||||
'name' => 'name of the second app',
|
||||
'disks' => ['local', 's3'],
|
||||
'newestBackupsShouldNotBeOlderThanDays' => 1,
|
||||
'storageUsedMayNotBeHigherThanMegabytes' => 5000,
|
||||
],
|
||||
*/
|
||||
],
|
||||
|
||||
'cleanup' => [
|
||||
/*
|
||||
* The strategy that will be used to cleanup old backups. The default strategy
|
||||
* will keep all backups for a certain amount of days. After that period only
|
||||
* a daily backup will be kept. After that period only weekly backups will
|
||||
* be kept and so on.
|
||||
*
|
||||
* No matter how you configure it the default strategy will never
|
||||
* delete the newest backup.
|
||||
*/
|
||||
'strategy' => \Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy::class,
|
||||
|
||||
'defaultStrategy' => [
|
||||
|
||||
/*
|
||||
* The number of days for which backups must be kept.
|
||||
*/
|
||||
'keepAllBackupsForDays' => 7,
|
||||
|
||||
/*
|
||||
* The number of days for which daily backups must be kept.
|
||||
*/
|
||||
'keepDailyBackupsForDays' => 16,
|
||||
|
||||
/*
|
||||
* The number of weeks for which one weekly backup must be kept.
|
||||
*/
|
||||
'keepWeeklyBackupsForWeeks' => 8,
|
||||
|
||||
/*
|
||||
* The number of months for which one monthly backup must be kept.
|
||||
*/
|
||||
'keepMonthlyBackupsForMonths' => 4,
|
||||
|
||||
/*
|
||||
* The number of years for which one yearly backup must be kept.
|
||||
*/
|
||||
'keepYearlyBackupsForYears' => 2,
|
||||
|
||||
/*
|
||||
* After cleaning up the backups remove the oldest backup until
|
||||
* this amount of megabytes has been reached.
|
||||
*/
|
||||
'deleteOldestBackupsWhenUsingMoreMegabytesThan' => 5000,
|
||||
],
|
||||
],
|
||||
];
|
|
@ -70,7 +70,15 @@ return [
|
|||
|
||||
'redis' => [
|
||||
'driver' => 'redis',
|
||||
'connection' => 'default',
|
||||
'client' => 'predis',
|
||||
|
||||
'default' => [
|
||||
'host' => env('REDIS_HOST', 'localhost'),
|
||||
'password' => env('REDIS_PASSWORD', null),
|
||||
'port' => env('REDIS_PORT', 6379),
|
||||
'database' => env('REDIS_DATABASE', 0),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
],
|
||||
|
|
201
config/debugbar.php
Normal file
201
config/debugbar.php
Normal file
|
@ -0,0 +1,201 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Debugbar Settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Debugbar is enabled by default, when debug is set to true in app.php.
|
||||
| You can override the value by setting enable to true or false instead of null.
|
||||
|
|
||||
| You can provide an array of URI's that must be ignored (eg. 'api/*')
|
||||
|
|
||||
*/
|
||||
|
||||
'enabled' => env('DEBUGBAR_ENABLED', false),
|
||||
'except' => [
|
||||
//
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Storage settings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| DebugBar stores data for session/ajax requests.
|
||||
| You can disable this, so the debugbar stores data in headers/session,
|
||||
| but this can cause problems with large data collectors.
|
||||
| By default, file storage (in the storage folder) is used. Redis and PDO
|
||||
| can also be used. For PDO, run the package migrations first.
|
||||
|
|
||||
*/
|
||||
'storage' => [
|
||||
'enabled' => true,
|
||||
'driver' => 'file', // redis, file, pdo, custom
|
||||
'path' => storage_path('debugbar'), // For file driver
|
||||
'connection' => null, // Leave null for default connection (Redis/PDO)
|
||||
'provider' => '' // Instance of StorageInterface for custom driver
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Vendors
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Vendor files are included by default, but can be set to false.
|
||||
| This can also be set to 'js' or 'css', to only include javascript or css vendor files.
|
||||
| Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
|
||||
| and for js: jquery and and highlight.js
|
||||
| So if you want syntax highlighting, set it to true.
|
||||
| jQuery is set to not conflict with existing jQuery scripts.
|
||||
|
|
||||
*/
|
||||
|
||||
'include_vendors' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Capture Ajax Requests
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
|
||||
| you can use this option to disable sending the data through the headers.
|
||||
|
|
||||
| Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
||||
*/
|
||||
|
||||
'capture_ajax' => true,
|
||||
'add_ajax_timing' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Custom Error Handler for Deprecated warnings
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When enabled, the Debugbar shows deprecated warnings for Symfony components
|
||||
| in the Messages tab.
|
||||
|
|
||||
*/
|
||||
'error_handler' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Clockwork integration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The Debugbar can emulate the Clockwork headers, so you can use the Chrome
|
||||
| Extension, without the server-side code. It uses Debugbar collectors instead.
|
||||
|
|
||||
*/
|
||||
'clockwork' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DataCollectors
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Enable/disable DataCollectors
|
||||
|
|
||||
*/
|
||||
|
||||
'collectors' => [
|
||||
'phpinfo' => true, // Php version
|
||||
'messages' => true, // Messages
|
||||
'time' => true, // Time Datalogger
|
||||
'memory' => true, // Memory usage
|
||||
'exceptions' => true, // Exception displayer
|
||||
'log' => true, // Logs from Monolog (merged in messages if enabled)
|
||||
'db' => true, // Show database (PDO) queries and bindings
|
||||
'views' => true, // Views with their data
|
||||
'route' => true, // Current route information
|
||||
'auth' => true, // Display Laravel authentication status
|
||||
'gate' => true, // Display Laravel Gate checks
|
||||
'session' => true, // Display session data
|
||||
'symfony_request' => true, // Only one can be enabled..
|
||||
'mail' => true, // Catch mail messages
|
||||
'laravel' => false, // Laravel version and environment
|
||||
'events' => false, // All events fired
|
||||
'default_request' => false, // Regular or special Symfony request logger
|
||||
'logs' => false, // Add the latest log messages
|
||||
'files' => false, // Show the included files
|
||||
'config' => false, // Display config settings
|
||||
'cache' => false, // Display cache events
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Extra options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Configure some DataCollectors
|
||||
|
|
||||
*/
|
||||
|
||||
'options' => [
|
||||
'auth' => [
|
||||
'show_name' => true, // Also show the users name/email in the debugbar
|
||||
],
|
||||
'db' => [
|
||||
'with_params' => true, // Render SQL with the parameters substituted
|
||||
'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
|
||||
'timeline' => false, // Add the queries to the timeline
|
||||
'explain' => [ // Show EXPLAIN output on queries
|
||||
'enabled' => false,
|
||||
'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
|
||||
],
|
||||
'hints' => true, // Show hints for common mistakes
|
||||
],
|
||||
'mail' => [
|
||||
'full_log' => false
|
||||
],
|
||||
'views' => [
|
||||
'data' => false, //Note: Can slow down the application, because the data can be quite large..
|
||||
],
|
||||
'route' => [
|
||||
'label' => true // show complete route on bar
|
||||
],
|
||||
'logs' => [
|
||||
'file' => null
|
||||
],
|
||||
'cache' => [
|
||||
'values' => true // collect cache values
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Inject Debugbar in Response
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Usually, the debugbar is added just before </body>, by listening to the
|
||||
| Response after the App is done. If you disable this, you have to add them
|
||||
| in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
||||
|
|
||||
*/
|
||||
|
||||
'inject' => true,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DebugBar route prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Sometimes you want to set route prefix to be used by DebugBar to load
|
||||
| its resources from. Usually the need comes from misconfigured web server or
|
||||
| from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
||||
|
|
||||
*/
|
||||
'route_prefix' => '_debugbar',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| DebugBar route domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| By default DebugBar route served from the same domain that request served.
|
||||
| To override default domain, specify it as a non-empty value.
|
||||
*/
|
||||
'route_domain' => null,
|
||||
];
|
|
@ -86,7 +86,7 @@ return [
|
|||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'simple',
|
||||
'processes' => 3,
|
||||
'processes' => 10,
|
||||
'tries' => 3,
|
||||
],
|
||||
],
|
||||
|
|
|
@ -2,19 +2,119 @@
|
|||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Domains
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Application domains used for routing
|
||||
|
|
||||
*/
|
||||
'domain' => [
|
||||
'admin' => env('ADMIN_DOMAIN'),
|
||||
'app' => env('APP_DOMAIN'),
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| PixelFed Version
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value is the version of your PixelFed instance.
|
||||
|
|
||||
*/
|
||||
'version' => '0.1.0',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| NodeInfo Route Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Do not change this value unless you know what you are doing.
|
||||
|
|
||||
*/
|
||||
'nodeinfo' => [
|
||||
'url' => config('app.url') . '/' . 'api/nodeinfo/2.0.json'
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| PHP/ImageMagic/GD Memory Limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This memory_limit value is only used for image processing. The
|
||||
| default memory_limit php.ini is used for the rest of the app.
|
||||
|
|
||||
*/
|
||||
'memory_limit' => '1024M',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Restricted Usernames
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Optional blacklist to prevent registering usernames that could
|
||||
| be confused for admin or system services.
|
||||
|
|
||||
*/
|
||||
'restricted_names' => [
|
||||
'reserved_routes' => true,
|
||||
'use_blacklist' => false
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Allow New Registrations
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Enable/disable new local account registrations.
|
||||
|
|
||||
*/
|
||||
'open_registration' => env('OPEN_REGISTRATION', true),
|
||||
'recaptcha' => env('RECAPTCHA_ENABLED', false),
|
||||
|
||||
'remote_follow_enabled' => env('REMOTE_FOLLOW', false),
|
||||
|
||||
'activitypub_enabled' => env('ACTIVITY_PUB', false),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Photo file size limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Update the max photo size, in KB.
|
||||
|
|
||||
*/
|
||||
'max_photo_size' => env('MAX_PHOTO_SIZE', 15000),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Caption limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Change the caption length limit for new local posts.
|
||||
|
|
||||
*/
|
||||
'max_caption_length' => env('MAX_CAPTION_LENGTH', 150),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Album size limit
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| The max number of photos allowed per post.
|
||||
|
|
||||
*/
|
||||
'max_album_length' => env('MAX_ALBUM_LENGTH', 4),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Email Verification
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Require email verification before a new user can do anything.
|
||||
|
|
||||
*/
|
||||
'enforce_email_verification' => env('ENFORCE_EMAIL_VERIFICATION', true),
|
||||
|
||||
];
|
66
config/recaptcha.php
Normal file
66
config/recaptcha.php
Normal file
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| API Keys
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set the public and private API keys as provided by reCAPTCHA.
|
||||
|
|
||||
| In version 2 of reCAPTCHA, public_key is the Site key,
|
||||
| and private_key is the Secret key.
|
||||
|
|
||||
*/
|
||||
'public_key' => env('RECAPTCHA_PUBLIC_KEY', ''),
|
||||
'private_key' => env('RECAPTCHA_PRIVATE_KEY', ''),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Template
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set a template to use if you don't want to use the standard one.
|
||||
|
|
||||
*/
|
||||
'template' => '',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Determine how to call out to get response; values are 'curl' or 'native'.
|
||||
| Only applies to v2.
|
||||
|
|
||||
*/
|
||||
'driver' => 'curl',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Options
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Various options for the driver
|
||||
|
|
||||
*/
|
||||
'options' => [
|
||||
|
||||
'curl_timeout' => 1,
|
||||
'curl_verify' => true,
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Version
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Set which version of ReCaptcha to use.
|
||||
|
|
||||
*/
|
||||
|
||||
'version' => 2,
|
||||
|
||||
];
|
|
@ -0,0 +1,37 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateNotificationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('notifications', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('profile_id')->unsigned()->index();
|
||||
$table->bigInteger('actor_id')->unsigned()->nullable();
|
||||
$table->string('action')->nullable();
|
||||
$table->string('message')->nullable();
|
||||
$table->string('rendered')->nullable();
|
||||
$table->timestamp('read_at')->nullable();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('notifications');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateBookmarksTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('bookmarks', function (Blueprint $table) {
|
||||
$table->increments('id');
|
||||
$table->bigInteger('status_id')->unsigned();
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->unique(['status_id', 'profile_id']);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('bookmarks');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class UpdateNotificationsTableAddPolymorphicRelationship extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('notifications', function (Blueprint $table) {
|
||||
$table->bigInteger('item_id')->unsigned()->nullable()->after('actor_id');
|
||||
$table->string('item_type')->nullable()->after('item_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateMentionsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('mentions', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('status_id')->unsigned();
|
||||
$table->bigInteger('profile_id')->unsigned();
|
||||
$table->boolean('local')->default(true);
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('mentions');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddFiltersToMediaTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('media', function (Blueprint $table) {
|
||||
$table->string('filter_name')->nullable()->after('orientation');
|
||||
$table->string('filter_class')->nullable()->after('filter_name');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('media', function (Blueprint $table) {
|
||||
$table->dropColumn('filter_name');
|
||||
$table->dropColumn('filter_class');
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class AddSoftDeletesToModels extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::table('avatars', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('likes', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('media', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('mentions', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('notifications', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('profiles', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('statuses', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
|
||||
Schema::table('users', function ($table) {
|
||||
$table->softDeletes();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateEmailVerificationsTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('email_verifications', function (Blueprint $table) {
|
||||
$table->bigIncrements('id');
|
||||
$table->bigInteger('user_id')->unsigned();
|
||||
$table->string('email')->nullable();
|
||||
$table->string('user_token')->index();
|
||||
$table->string('random_token')->index();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::dropIfExists('email_verifications');
|
||||
}
|
||||
}
|
|
@ -3,37 +3,62 @@ version: '3'
|
|||
services:
|
||||
nginx:
|
||||
image: nginx:alpine
|
||||
networks:
|
||||
- internal
|
||||
- external
|
||||
ports:
|
||||
- 3000:80
|
||||
volumes:
|
||||
- .:/var/www/html
|
||||
- "php-storage:/var/www/html"
|
||||
- ./contrib/nginx.conf:/etc/nginx/conf.d/default.conf
|
||||
depends_on:
|
||||
- php
|
||||
|
||||
php:
|
||||
build: .
|
||||
image: pixelfed
|
||||
volumes:
|
||||
- ./storage:/var/www/html/storage
|
||||
depends_on:
|
||||
- mysql
|
||||
- redis
|
||||
- "php-storage:/var/www/html"
|
||||
networks:
|
||||
- internal
|
||||
environment:
|
||||
- DB_HOST=mysql
|
||||
- DB_DATABASE=pixelfed
|
||||
- DB_USERNAME=${DB_USERNAME}
|
||||
- DB_PASSWORD=${DB_PASSWORD}
|
||||
- DB_USERNAME=${DB_USERNAME:-pixelfed}
|
||||
- DB_PASSWORD=${DB_PASSWORD:-pixelfed}
|
||||
- REDIS_HOST=redis
|
||||
- APP_KEY=${APP_KEY}
|
||||
env_file:
|
||||
- ./.env
|
||||
|
||||
mysql:
|
||||
image: mysql:5.7
|
||||
networks:
|
||||
- internal
|
||||
environment:
|
||||
- MYSQL_DATABASE=pixelfed
|
||||
- MYSQL_USER=${DB_USERNAME}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD}
|
||||
- MYSQL_USER=${DB_USERNAME:-pixelfed}
|
||||
- MYSQL_PASSWORD=${DB_PASSWORD:-pixelfed}
|
||||
- MYSQL_RANDOM_ROOT_PASSWORD="true"
|
||||
env_file:
|
||||
- ./.env
|
||||
volumes:
|
||||
- ./docker-volumes/mysql:/var/lib/mysql
|
||||
- "mysql-data:/var/lib/mysql"
|
||||
|
||||
redis:
|
||||
image: redis:alpine
|
||||
volumes:
|
||||
- ./docker-volumes/redis:/data
|
||||
...
|
||||
- "redis-data:/data"
|
||||
networks:
|
||||
- internal
|
||||
|
||||
volumes:
|
||||
redis-data:
|
||||
mysql-data:
|
||||
php-storage:
|
||||
|
||||
networks:
|
||||
internal:
|
||||
internal: true
|
||||
external:
|
||||
driver: bridge
|
||||
|
|
BIN
public/css/app.css
vendored
BIN
public/css/app.css
vendored
Binary file not shown.
BIN
public/fonts/fa-brands-400.eot
Normal file
BIN
public/fonts/fa-brands-400.eot
Normal file
Binary file not shown.
BIN
public/fonts/fa-brands-400.svg
Normal file
BIN
public/fonts/fa-brands-400.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 586 KiB |
BIN
public/fonts/fa-brands-400.ttf
Normal file
BIN
public/fonts/fa-brands-400.ttf
Normal file
Binary file not shown.
BIN
public/fonts/fa-brands-400.woff
Normal file
BIN
public/fonts/fa-brands-400.woff
Normal file
Binary file not shown.
BIN
public/fonts/fa-brands-400.woff2
Normal file
BIN
public/fonts/fa-brands-400.woff2
Normal file
Binary file not shown.
BIN
public/fonts/fa-regular-400.eot
Normal file
BIN
public/fonts/fa-regular-400.eot
Normal file
Binary file not shown.
BIN
public/fonts/fa-regular-400.svg
Normal file
BIN
public/fonts/fa-regular-400.svg
Normal file
Binary file not shown.
After Width: | Height: | Size: 102 KiB |
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue