Merge remote-tracking branch 'posativ/master' into reply_notification
Conflicts: isso/css/isso.css isso/ext/notifications.py isso/js/app/text/postbox.jade isso/views/comments.py
This commit is contained in:
commit
f6f61c547a
13
CHANGES.rst
13
CHANGES.rst
@ -4,7 +4,8 @@ Changelog for Isso
|
|||||||
0.10.7 (unreleased)
|
0.10.7 (unreleased)
|
||||||
-------------------
|
-------------------
|
||||||
|
|
||||||
- Fix Chinese translation
|
- Fix Chinese translation & typo in CJK
|
||||||
|
- Fix link in moderation mails if isso is setup on a sub-url (e.g. domain.tld/comments/)
|
||||||
- Add Danish translation
|
- Add Danish translation
|
||||||
- Add Hungarian translation
|
- Add Hungarian translation
|
||||||
- Add Persian translation
|
- Add Persian translation
|
||||||
@ -12,7 +13,15 @@ Changelog for Isso
|
|||||||
- Add links highlighting in comments
|
- Add links highlighting in comments
|
||||||
- Add apidoc
|
- Add apidoc
|
||||||
- Add rc.d script for FreeBSD
|
- Add rc.d script for FreeBSD
|
||||||
- Some tests/travis/documentation improvements and fixes
|
- Add the possibility to set CORS Origin through ISSO_CORS_ORIGIN environ variable
|
||||||
|
- Add preview button
|
||||||
|
- Add Atom feed at /feed?uri={thread-id}
|
||||||
|
- Add optionnal gravatar support
|
||||||
|
- Add nofollow noopener on links inside comments
|
||||||
|
- Add Dockerfile
|
||||||
|
- Upgraded to Misaka 2
|
||||||
|
- Some tests/travis/documentation improvements and fixes + pep8
|
||||||
|
- Improvement on german translation
|
||||||
|
|
||||||
|
|
||||||
0.10.6 (2016-09-22)
|
0.10.6 (2016-09-22)
|
||||||
|
@ -51,12 +51,46 @@ In chronological order:
|
|||||||
* Added configuration to require email addresses (no validation)
|
* Added configuration to require email addresses (no validation)
|
||||||
* Fix Vagrantfile
|
* Fix Vagrantfile
|
||||||
|
|
||||||
* Benoît Latinier <benoit@latinier.fr>
|
* Benoît Latinier @blatinier <benoit@latinier.fr>
|
||||||
* Fix thread discovery
|
* Fix thread discovery
|
||||||
* Added mandatory author
|
* Added mandatory author
|
||||||
|
* Added admin interface
|
||||||
|
|
||||||
* Ivan Pantic <ivanpantic82@gmail.com>
|
* Ivan Pantic <ivanpantic82@gmail.com>
|
||||||
* Added vote levels
|
* Added vote levels
|
||||||
|
|
||||||
|
* Martin Schenck @schemar
|
||||||
|
* Improvement in the german translation
|
||||||
|
|
||||||
|
* @cclauss
|
||||||
|
* Pep8 and drop of legacy supports (old python & debian version tested in travis)
|
||||||
|
* Make travis use pyflakes
|
||||||
|
|
||||||
|
* Lucas Cimon @Lucas-C
|
||||||
|
* Added the possibility to define CORS origins through ISSO_CORS_ORIGIN environment variable
|
||||||
|
* Fix a bug with <a> in <svg>
|
||||||
|
|
||||||
|
* Yuchen Pei @ycpei
|
||||||
|
* Fix link in moderation emails when isso is installed in a sub URL
|
||||||
|
|
||||||
|
* @Rocket1184
|
||||||
|
* Fix typo in CJK translations
|
||||||
|
|
||||||
|
* @vincentbernat
|
||||||
|
* Added documentation about data-isso-id attribute (overriding the standard isso-thread-id)
|
||||||
|
* Added multi-staged Dockerfile
|
||||||
|
* Added atom feed
|
||||||
|
* Added a nofollow/noopener on links inside comments to protect against bots
|
||||||
|
* Added a preview using the existing preview endpoint
|
||||||
|
|
||||||
|
* @p-vitt & @M4a1x
|
||||||
|
* Documentation on troubleshooting for uberspace users
|
||||||
|
|
||||||
|
* @benjhess
|
||||||
|
* Optionnal gravatar support
|
||||||
|
|
||||||
|
* Steffen Prince @sprin
|
||||||
|
* Upgrade to Misaka 2
|
||||||
|
|
||||||
* [Your name or handle] <[email or website]>
|
* [Your name or handle] <[email or website]>
|
||||||
* [Brief summary of your changes]
|
* [Brief summary of your changes]
|
||||||
|
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# First, compile JS stuff
|
||||||
|
FROM node
|
||||||
|
WORKDIR /src/
|
||||||
|
COPY . .
|
||||||
|
RUN npm install -g requirejs uglify-js jade bower
|
||||||
|
RUN make init js
|
||||||
|
|
||||||
|
# Second, create virtualenv
|
||||||
|
FROM python:3-stretch
|
||||||
|
WORKDIR /src/
|
||||||
|
COPY --from=0 /src .
|
||||||
|
RUN apt-get -qqy update && apt-get -qqy install python3-dev sqlite3
|
||||||
|
RUN python3 -m venv /isso \
|
||||||
|
&& . /isso/bin/activate \
|
||||||
|
&& python setup.py install \
|
||||||
|
&& pip install gunicorn
|
||||||
|
|
||||||
|
# Third, create final repository
|
||||||
|
FROM python:3-slim-stretch
|
||||||
|
WORKDIR /isso/
|
||||||
|
COPY --from=1 /isso .
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
VOLUME /db /config
|
||||||
|
EXPOSE 8080
|
||||||
|
ENV ISSO_SETTINGS /config/isso.cfg
|
||||||
|
CMD ["/isso/bin/gunicorn", "-b", "0.0.0.0:8080", "-w", "4", "--preload", "isso.run"]
|
||||||
|
|
||||||
|
# Example of use:
|
||||||
|
#
|
||||||
|
# docker build -t isso .
|
||||||
|
# docker run -it --rm -v /opt/isso:/config -v /opt/isso:/db -v $PWD:$PWD isso /isso/bin/isso -c \$ISSO_SETTINGS import disqus.xml
|
||||||
|
# docker run -d --rm --name isso -p 8080:8080 -v /opt/isso:/config -v /opt/isso:/db isso
|
@ -59,5 +59,3 @@ definitely need help:
|
|||||||
- delete or activate comments matching a filter (e.g. name, email, ip address)
|
- delete or activate comments matching a filter (e.g. name, email, ip address)
|
||||||
|
|
||||||
- close threads and remove threads completely
|
- close threads and remove threads completely
|
||||||
|
|
||||||
- edit comments
|
|
||||||
|
@ -7,6 +7,7 @@ preferably in the script tag which embeds the JS:
|
|||||||
.. code-block:: html
|
.. code-block:: html
|
||||||
|
|
||||||
<script data-isso="/prefix/"
|
<script data-isso="/prefix/"
|
||||||
|
data-isso-id="thread-id"
|
||||||
data-isso-css="true"
|
data-isso-css="true"
|
||||||
data-isso-lang="ru"
|
data-isso-lang="ru"
|
||||||
data-isso-reply-to-self="false"
|
data-isso-reply-to-self="false"
|
||||||
@ -19,15 +20,16 @@ preferably in the script tag which embeds the JS:
|
|||||||
data-isso-avatar-bg="#f0f0f0"
|
data-isso-avatar-bg="#f0f0f0"
|
||||||
data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
|
data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
|
||||||
data-isso-vote="true"
|
data-isso-vote="true"
|
||||||
data-vote-levels=""
|
data-isso-vote-levels=""
|
||||||
|
data-isso-feed="false"
|
||||||
src="/prefix/js/embed.js"></script>
|
src="/prefix/js/embed.js"></script>
|
||||||
|
|
||||||
Furthermore you can override the automatic title detection inside
|
Furthermore you can override the automatic title detection inside
|
||||||
the embed tag, e.g.:
|
the embed tag, as well as the thread ID, e.g.:
|
||||||
|
|
||||||
.. code-block:: html
|
.. code-block:: html
|
||||||
|
|
||||||
<section id="isso-thread" data-title="Foo!"></section>
|
<section id="isso-thread" data-title="Foo!" data-isso-id="/path/to/resource"></section>
|
||||||
|
|
||||||
data-isso
|
data-isso
|
||||||
---------
|
---------
|
||||||
@ -106,6 +108,14 @@ scheme is based in `this color palette <http://colrd.com/palette/19308/>`_.
|
|||||||
Multiple colors must be separated by space. If you use less than eight colors
|
Multiple colors must be separated by space. If you use less than eight colors
|
||||||
and not a multiple of 2, the color distribution is not even.
|
and not a multiple of 2, the color distribution is not even.
|
||||||
|
|
||||||
|
data-isso-gravatar
|
||||||
|
------------------
|
||||||
|
|
||||||
|
Uses gravatar images instead of generating svg images. You have to set
|
||||||
|
"data-isso-avatar" to **false** when you want to use this. Otherwise
|
||||||
|
both the gravatar and avatar svg image will show up. Please also set
|
||||||
|
option "gravatar" to **true** in the server configuration...
|
||||||
|
|
||||||
data-isso-vote
|
data-isso-vote
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
@ -124,3 +134,10 @@ For example, the value `"-5,5"` will cause each `isso-comment` to be given one o
|
|||||||
- `isso-vote-level-2` for scores of `5` and greater
|
- `isso-vote-level-2` for scores of `5` and greater
|
||||||
|
|
||||||
These classes can then be used to customize the appearance of comments (eg. put a star on popular comments)
|
These classes can then be used to customize the appearance of comments (eg. put a star on popular comments)
|
||||||
|
|
||||||
|
data-isso-feed
|
||||||
|
--------------
|
||||||
|
|
||||||
|
Enable or disable the addition of a link to the feed for the comment
|
||||||
|
thread. The link will only be valid if the appropriate setting, in
|
||||||
|
``[rss]`` section, is also enabled server-side.
|
||||||
|
@ -91,6 +91,18 @@ notify
|
|||||||
log-file
|
log-file
|
||||||
Log console messages to file instead of standard out.
|
Log console messages to file instead of standard out.
|
||||||
|
|
||||||
|
gravatar
|
||||||
|
When set to ``true`` this will add the property "gravatar_image"
|
||||||
|
containing the link to a gravatar image to every comment. If a comment
|
||||||
|
does not contain an email address, gravatar will render a random icon.
|
||||||
|
This is only true when using the default value for "gravatar-url"
|
||||||
|
which contains the query string param ``d=identicon`` ...
|
||||||
|
|
||||||
|
gravatar-url
|
||||||
|
Url for gravatar images. The "{}" is where the email hash will be placed.
|
||||||
|
Defaults to "https://www.gravatar.com/avatar/{}?d=identicon"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS
|
.. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS
|
||||||
|
|
||||||
@ -108,7 +120,7 @@ Enable moderation queue and handling of comments still in moderation queue
|
|||||||
|
|
||||||
enabled
|
enabled
|
||||||
enable comment moderation queue. This option only affects new comments.
|
enable comment moderation queue. This option only affects new comments.
|
||||||
Comments in modertion queue are not visible to other users until you
|
Comments in moderation queue are not visible to other users until you
|
||||||
activate them.
|
activate them.
|
||||||
|
|
||||||
purge-after
|
purge-after
|
||||||
@ -308,6 +320,27 @@ algorithm
|
|||||||
Arguments have to be in that order, but can be reduced to `pbkdf2:4096`
|
Arguments have to be in that order, but can be reduced to `pbkdf2:4096`
|
||||||
for example to override the iterations only.
|
for example to override the iterations only.
|
||||||
|
|
||||||
|
.. _configure-rss:
|
||||||
|
|
||||||
|
RSS
|
||||||
|
---
|
||||||
|
|
||||||
|
Isso can provide an Atom feed for each comment thread. Users can use
|
||||||
|
them to subscribe to comments and be notified of changes. Atom feeds
|
||||||
|
are enabled as soon as there is a base URL defined in this section.
|
||||||
|
|
||||||
|
.. code-block:: ini
|
||||||
|
|
||||||
|
[rss]
|
||||||
|
base =
|
||||||
|
limit = 100
|
||||||
|
|
||||||
|
base
|
||||||
|
base URL to use to build complete URI to pages (by appending the URI from Isso)
|
||||||
|
|
||||||
|
limit
|
||||||
|
number of most recent comments to return for a thread
|
||||||
|
|
||||||
Appendum
|
Appendum
|
||||||
--------
|
--------
|
||||||
|
|
||||||
|
@ -185,3 +185,16 @@ uri :
|
|||||||
|
|
||||||
returns an integer
|
returns an integer
|
||||||
|
|
||||||
|
Get Atom feed
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Get an Atom feed of comments for thread `uri`:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
GET /feed?uri=%2Fhello-world%2F
|
||||||
|
|
||||||
|
uri :
|
||||||
|
URI to get comments for, required.
|
||||||
|
|
||||||
|
Returns an XML document as the Atom feed.
|
||||||
|
@ -39,10 +39,10 @@ package manager.
|
|||||||
.. code-block:: sh
|
.. code-block:: sh
|
||||||
|
|
||||||
# for Debian/Ubuntu
|
# for Debian/Ubuntu
|
||||||
~> sudo apt-get install python-setuptools python-virtualenv
|
~> sudo apt-get install python-setuptools python-virtualenv python-dev
|
||||||
|
|
||||||
# Fedora/Red Hat
|
# Fedora/Red Hat
|
||||||
~> sudo yum install python-setuptools python-virtualenv
|
~> sudo yum install python-setuptools python-virtualenv python-devel
|
||||||
|
|
||||||
The next steps should be done as regular user, not as root (although possible
|
The next steps should be done as regular user, not as root (although possible
|
||||||
but not recommended):
|
but not recommended):
|
||||||
@ -149,7 +149,18 @@ Prebuilt Packages
|
|||||||
* Fedora: https://copr.fedoraproject.org/coprs/jujens/isso/ — copr
|
* Fedora: https://copr.fedoraproject.org/coprs/jujens/isso/ — copr
|
||||||
repository. Built from Pypi, includes a systemctl unit script.
|
repository. Built from Pypi, includes a systemctl unit script.
|
||||||
|
|
||||||
* Docker Image: https://registry.hub.docker.com/u/bl4n/isso/
|
Build a Docker image
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
You can get a Docker image by running ``docker build . -t
|
||||||
|
isso``. Assuming you have your configuration in ``/opt/isso``, you can
|
||||||
|
use the following command to spawn the Docker container:
|
||||||
|
|
||||||
|
.. code-block:: sh
|
||||||
|
|
||||||
|
~> docker run -d --rm --name isso -p 127.0.0.1:8080:8080 -v /opt/isso:/config -v /opt/isso:/db isso
|
||||||
|
|
||||||
|
Then, you can use a reverse proxy to expose port 8080.
|
||||||
|
|
||||||
Install from Source
|
Install from Source
|
||||||
-------------------
|
-------------------
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
Troubleshooting
|
Troubleshooting
|
||||||
===============
|
===============
|
||||||
|
|
||||||
|
For uberspace users
|
||||||
|
-------------------
|
||||||
|
Some uberspace users experienced problems with isso and they solved their
|
||||||
|
issue by adding `DirectoryIndex disabled` as the first line in the `.htaccess`
|
||||||
|
file for the domain the isso server is running on.
|
||||||
|
|
||||||
pkg_ressources.DistributionNotFound
|
pkg_ressources.DistributionNotFound
|
||||||
-----------------------------------
|
-----------------------------------
|
||||||
|
|
||||||
|
@ -15,6 +15,14 @@
|
|||||||
color: #555;
|
color: #555;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
#isso-thread > .isso-feedlink {
|
||||||
|
float: right;
|
||||||
|
padding-left: 1em;
|
||||||
|
}
|
||||||
|
#isso-thread > .isso-feedlink > a {
|
||||||
|
font-size: 0.8em;
|
||||||
|
vertical-align: bottom;
|
||||||
|
}
|
||||||
#isso-thread .textarea {
|
#isso-thread .textarea {
|
||||||
min-height: 58px;
|
min-height: 58px;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
@ -23,31 +31,26 @@
|
|||||||
color: #757575;
|
color: #757575;
|
||||||
}
|
}
|
||||||
|
|
||||||
.isso-comment {
|
#isso-root .isso-comment {
|
||||||
max-width: 68em;
|
max-width: 68em;
|
||||||
padding-top: 0.95em;
|
padding-top: 0.95em;
|
||||||
margin: 0.95em auto;
|
margin: 0.95em auto;
|
||||||
}
|
}
|
||||||
.isso-comment:not(:first-of-type),
|
#isso-root .preview .isso-comment {
|
||||||
|
padding-top: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#isso-root .isso-comment:not(:first-of-type),
|
||||||
.isso-follow-up .isso-comment {
|
.isso-follow-up .isso-comment {
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
.isso-comment > div.avatar,
|
.isso-comment > div.avatar {
|
||||||
.isso-postbox > .avatar {
|
|
||||||
display: block;
|
display: block;
|
||||||
float: left;
|
float: left;
|
||||||
width: 7%;
|
width: 7%;
|
||||||
margin: 3px 15px 0 0;
|
margin: 3px 15px 0 0;
|
||||||
}
|
}
|
||||||
.isso-postbox > .avatar {
|
.isso-comment > div.avatar > svg {
|
||||||
float: left;
|
|
||||||
margin: 5px 10px 0 5px;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
.isso-comment > div.avatar > svg,
|
|
||||||
.isso-postbox > .avatar > svg {
|
|
||||||
max-width: 48px;
|
max-width: 48px;
|
||||||
max-height: 48px;
|
max-height: 48px;
|
||||||
border: 1px solid rgba(0, 0, 0, 0.2);
|
border: 1px solid rgba(0, 0, 0, 0.2);
|
||||||
@ -90,7 +93,8 @@
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
.isso-comment > div.text-wrapper > .textarea-wrapper .textarea {
|
.isso-comment > div.text-wrapper > .textarea-wrapper .textarea,
|
||||||
|
.isso-comment > div.text-wrapper > .textarea-wrapper .preview {
|
||||||
margin-top: 0.2em;
|
margin-top: 0.2em;
|
||||||
}
|
}
|
||||||
.isso-comment > div.text-wrapper > div.text p {
|
.isso-comment > div.text-wrapper > div.text p {
|
||||||
@ -108,7 +112,8 @@
|
|||||||
font-size: 130%;
|
font-size: 130%;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
.isso-comment > div.text-wrapper > div.textarea-wrapper .textarea {
|
.isso-comment > div.text-wrapper > div.textarea-wrapper .textarea,
|
||||||
|
.isso-comment > div.text-wrapper > div.textarea-wrapper .preview {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid #f0f0f0;
|
border: 1px solid #f0f0f0;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
@ -119,10 +124,12 @@
|
|||||||
color: gray !important;
|
color: gray !important;
|
||||||
clear: left;
|
clear: left;
|
||||||
}
|
}
|
||||||
|
.isso-feedlink,
|
||||||
.isso-comment > div.text-wrapper > .isso-comment-footer a {
|
.isso-comment > div.text-wrapper > .isso-comment-footer a {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
.isso-feedlink:hover,
|
||||||
.isso-comment > div.text-wrapper > .isso-comment-footer a:hover {
|
.isso-comment > div.text-wrapper > .isso-comment-footer a:hover {
|
||||||
color: #111111 !important;
|
color: #111111 !important;
|
||||||
text-shadow: #aaaaaa 0 0 1px !important;
|
text-shadow: #aaaaaa 0 0 1px !important;
|
||||||
@ -152,6 +159,7 @@
|
|||||||
.isso-postbox {
|
.isso-postbox {
|
||||||
max-width: 68em;
|
max-width: 68em;
|
||||||
margin: 0 auto 2em;
|
margin: 0 auto 2em;
|
||||||
|
clear: right;
|
||||||
}
|
}
|
||||||
.isso-postbox > .form-wrapper {
|
.isso-postbox > .form-wrapper {
|
||||||
display: block;
|
display: block;
|
||||||
@ -161,7 +169,8 @@
|
|||||||
.isso-postbox > .form-wrapper > .auth-section .post-action {
|
.isso-postbox > .form-wrapper > .auth-section .post-action {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.isso-postbox > .form-wrapper .textarea {
|
.isso-postbox > .form-wrapper .textarea,
|
||||||
|
.isso-postbox > .form-wrapper .preview {
|
||||||
margin: 0 0 .3em;
|
margin: 0 0 .3em;
|
||||||
padding: .4em .8em;
|
padding: .4em .8em;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
@ -191,7 +200,7 @@
|
|||||||
.isso-postbox > .form-wrapper > .auth-section .post-action {
|
.isso-postbox > .form-wrapper > .auth-section .post-action {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
float: right;
|
float: right;
|
||||||
margin: 0;
|
margin: 0 0 0 5px;
|
||||||
}
|
}
|
||||||
.isso-postbox > .form-wrapper > .auth-section .post-action > input {
|
.isso-postbox > .form-wrapper > .auth-section .post-action > input {
|
||||||
padding: calc(.3em - 1px);
|
padding: calc(.3em - 1px);
|
||||||
@ -209,6 +218,27 @@
|
|||||||
.isso-postbox > .form-wrapper > .auth-section .post-action > input:active {
|
.isso-postbox > .form-wrapper > .auth-section .post-action > input:active {
|
||||||
background-color: #BBB;
|
background-color: #BBB;
|
||||||
}
|
}
|
||||||
|
.isso-postbox > .form-wrapper .preview,
|
||||||
|
.isso-postbox > .form-wrapper input[name="edit"],
|
||||||
|
.isso-postbox.preview-mode > .form-wrapper input[name="preview"],
|
||||||
|
.isso-postbox.preview-mode > .form-wrapper .textarea {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.isso-postbox.preview-mode > .form-wrapper .preview {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.isso-postbox.preview-mode > .form-wrapper input[name="edit"] {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
.isso-postbox > .form-wrapper .preview {
|
||||||
|
background-color: #f8f8f8;
|
||||||
|
background: repeating-linear-gradient(
|
||||||
|
-45deg,
|
||||||
|
#f8f8f8,
|
||||||
|
#f8f8f8 10px,
|
||||||
|
#fff 10px,
|
||||||
|
#fff 20px
|
||||||
|
);
|
||||||
.isso-postbox > .form-wrapper > .notification-section {
|
.isso-postbox > .form-wrapper > .notification-section {
|
||||||
display: none;
|
display: none;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
@ -222,9 +252,4 @@
|
|||||||
.isso-postbox > .form-wrapper > .auth-section .input-wrapper input {
|
.isso-postbox > .form-wrapper > .auth-section .input-wrapper input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.isso-postbox > .form-wrapper > .auth-section .post-action {
|
|
||||||
display: block;
|
|
||||||
float: none;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -173,7 +173,8 @@ class Comments:
|
|||||||
for item in rv:
|
for item in rv:
|
||||||
yield dict(zip(fields_comments + fields_threads, item))
|
yield dict(zip(fields_comments + fields_threads, item))
|
||||||
|
|
||||||
def fetch(self, uri, mode=5, after=0, parent='any', order_by='id', limit=None):
|
def fetch(self, uri, mode=5, after=0, parent='any',
|
||||||
|
order_by='id', asc=1, limit=None):
|
||||||
"""
|
"""
|
||||||
Return comments for :param:`uri` with :param:`mode`.
|
Return comments for :param:`uri` with :param:`mode`.
|
||||||
"""
|
"""
|
||||||
@ -195,7 +196,8 @@ class Comments:
|
|||||||
order_by = 'id'
|
order_by = 'id'
|
||||||
sql.append('ORDER BY ')
|
sql.append('ORDER BY ')
|
||||||
sql.append(order_by)
|
sql.append(order_by)
|
||||||
sql.append(' ASC')
|
if not asc:
|
||||||
|
sql.append(' DESC')
|
||||||
|
|
||||||
if limit:
|
if limit:
|
||||||
sql.append('LIMIT ?')
|
sql.append('LIMIT ?')
|
||||||
|
@ -70,6 +70,12 @@ class SMTP(object):
|
|||||||
|
|
||||||
self.isso = isso
|
self.isso = isso
|
||||||
self.conf = isso.conf.section("smtp")
|
self.conf = isso.conf.section("smtp")
|
||||||
|
gh = isso.conf.get("general", "host")
|
||||||
|
if type(gh) == str:
|
||||||
|
self.general_host = gh
|
||||||
|
#if gh is not a string then gh is a list
|
||||||
|
else:
|
||||||
|
self.general_host = gh[0]
|
||||||
|
|
||||||
# test SMTP connectivity
|
# test SMTP connectivity
|
||||||
try:
|
try:
|
||||||
@ -118,7 +124,7 @@ class SMTP(object):
|
|||||||
rv.write("---\n")
|
rv.write("---\n")
|
||||||
|
|
||||||
if admin:
|
if admin:
|
||||||
uri = local("host") + "/id/%i" % comment["id"]
|
uri = self.general_host + "/id/%i" % comment["id"]
|
||||||
key = self.isso.sign(comment["id"])
|
key = self.isso.sign(comment["id"])
|
||||||
|
|
||||||
rv.write("Delete comment: %s\n" % (uri + "/delete/" + key))
|
rv.write("Delete comment: %s\n" % (uri + "/delete/" + key))
|
||||||
@ -127,7 +133,7 @@ class SMTP(object):
|
|||||||
rv.write("Activate comment: %s\n" % (uri + "/activate/" + key))
|
rv.write("Activate comment: %s\n" % (uri + "/activate/" + key))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
uri = local("host") + "/id/%i" % comment_parent["id"]
|
uri = self.general_host + "/id/%i" % comment_parent["id"]
|
||||||
key = self.isso.sign(('unsubscribe', comment_parent["id"]))
|
key = self.isso.sign(('unsubscribe', comment_parent["id"]))
|
||||||
|
|
||||||
rv.write("Unsubscribe from this conversation: %s\n" % (uri + "/unsubscribe/" + key))
|
rv.write("Unsubscribe from this conversation: %s\n" % (uri + "/unsubscribe/" + key))
|
||||||
|
@ -191,6 +191,24 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) {
|
|||||||
return deferred.promise;
|
return deferred.promise;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var feed = function(tid) {
|
||||||
|
return endpoint + "/feed?" + qs({uri: tid || location});
|
||||||
|
};
|
||||||
|
|
||||||
|
var preview = function(text) {
|
||||||
|
var deferred = Q.defer();
|
||||||
|
curl("POST", endpoint + "/preview", JSON.stringify({text: text}),
|
||||||
|
function(rv) {
|
||||||
|
if (rv.status === 200) {
|
||||||
|
deferred.resolve(JSON.parse(rv.body).text);
|
||||||
|
} else {
|
||||||
|
deferred.reject(rv.body);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return deferred.promise;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
endpoint: endpoint,
|
endpoint: endpoint,
|
||||||
salt: salt,
|
salt: salt,
|
||||||
@ -202,6 +220,8 @@ define(["app/lib/promise", "app/globals"], function(Q, globals) {
|
|||||||
fetch: fetch,
|
fetch: fetch,
|
||||||
count: count,
|
count: count,
|
||||||
like: like,
|
like: like,
|
||||||
dislike: dislike
|
dislike: dislike,
|
||||||
|
feed: feed,
|
||||||
|
preview: preview
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -10,12 +10,14 @@ define(function() {
|
|||||||
"max-comments-top": "inf",
|
"max-comments-top": "inf",
|
||||||
"max-comments-nested": 5,
|
"max-comments-nested": 5,
|
||||||
"reveal-on-click": 5,
|
"reveal-on-click": 5,
|
||||||
|
"gravatar": false,
|
||||||
"avatar": true,
|
"avatar": true,
|
||||||
"avatar-bg": "#f0f0f0",
|
"avatar-bg": "#f0f0f0",
|
||||||
"avatar-fg": ["#9abf88", "#5698c4", "#e279a3", "#9163b6",
|
"avatar-fg": ["#9abf88", "#5698c4", "#e279a3", "#9163b6",
|
||||||
"#be5168", "#f19670", "#e4bf80", "#447c69"].join(" "),
|
"#be5168", "#f19670", "#e4bf80", "#447c69"].join(" "),
|
||||||
"vote": true,
|
"vote": true,
|
||||||
"vote-levels": null
|
"vote-levels": null,
|
||||||
|
"feed": false
|
||||||
};
|
};
|
||||||
|
|
||||||
var js = document.getElementsByTagName("script");
|
var js = document.getElementsByTagName("script");
|
||||||
|
@ -4,7 +4,7 @@ define(["app/api", "app/dom", "app/i18n"], function(api, $, i18n) {
|
|||||||
var objs = {};
|
var objs = {};
|
||||||
|
|
||||||
$.each("a", function(el) {
|
$.each("a", function(el) {
|
||||||
if (! el.href.match(/#isso-thread$/)) {
|
if (! el.href.match || ! el.href.match(/#isso-thread$/)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Име/псевдоним (незадължително)",
|
"postbox-author": "Име/псевдоним (незадължително)",
|
||||||
"postbox-email": "Ел. поща (незадължително)",
|
"postbox-email": "Ел. поща (незадължително)",
|
||||||
"postbox-website": "Уебсайт (незадължително)",
|
"postbox-website": "Уебсайт (незадължително)",
|
||||||
|
"postbox-preview": "преглед",
|
||||||
|
"postbox-edit": "Редактиране",
|
||||||
"postbox-submit": "Публикуване",
|
"postbox-submit": "Публикуване",
|
||||||
"num-comments": "1 коментар\n{{ n }} коментара",
|
"num-comments": "1 коментар\n{{ n }} коментара",
|
||||||
"no-comments": "Все още няма коментари",
|
"no-comments": "Все още няма коментари",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Jméno (nepovinné)",
|
"postbox-author": "Jméno (nepovinné)",
|
||||||
"postbox-email": "E-mail (nepovinný)",
|
"postbox-email": "E-mail (nepovinný)",
|
||||||
"postbox-website": "Web (nepovinný)",
|
"postbox-website": "Web (nepovinný)",
|
||||||
|
"postbox-preview": "Náhled",
|
||||||
|
"postbox-edit": "Upravit",
|
||||||
"postbox-submit": "Publikovat",
|
"postbox-submit": "Publikovat",
|
||||||
"num-comments": "Jeden komentář\n{{ n }} Komentářů",
|
"num-comments": "Jeden komentář\n{{ n }} Komentářů",
|
||||||
"no-comments": "Zatím bez komentářů",
|
"no-comments": "Zatím bez komentářů",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Name (optional)",
|
"postbox-author": "Name (optional)",
|
||||||
"postbox-email": "E-mail (optional)",
|
"postbox-email": "E-mail (optional)",
|
||||||
"postbox-website": "Website (optional)",
|
"postbox-website": "Website (optional)",
|
||||||
|
"postbox-preview": "Eksempel",
|
||||||
|
"postbox-edit": "Rediger",
|
||||||
"postbox-submit": "Submit",
|
"postbox-submit": "Submit",
|
||||||
|
|
||||||
"num-comments": "One Comment\n{{ n }} Comments",
|
"num-comments": "One Comment\n{{ n }} Comments",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Name (optional)",
|
"postbox-author": "Name (optional)",
|
||||||
"postbox-email": "Email (optional)",
|
"postbox-email": "Email (optional)",
|
||||||
"postbox-website": "Website (optional)",
|
"postbox-website": "Website (optional)",
|
||||||
|
"postbox-preview": "Vorschau",
|
||||||
|
"postbox-edit": "Bearbeiten",
|
||||||
"postbox-submit": "Abschicken",
|
"postbox-submit": "Abschicken",
|
||||||
"num-comments": "1 Kommentar\n{{ n }} Kommentare",
|
"num-comments": "1 Kommentar\n{{ n }} Kommentare",
|
||||||
"no-comments": "Bisher keine Kommentare",
|
"no-comments": "Bisher keine Kommentare",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Όνομα (προαιρετικό)",
|
"postbox-author": "Όνομα (προαιρετικό)",
|
||||||
"postbox-email": "E-mail (προαιρετικό)",
|
"postbox-email": "E-mail (προαιρετικό)",
|
||||||
"postbox-website": "Ιστοσελίδα (προαιρετικό)",
|
"postbox-website": "Ιστοσελίδα (προαιρετικό)",
|
||||||
|
"postbox-preview": "Πρεμιέρα",
|
||||||
|
"postbox-edit": "Επεξεργασία",
|
||||||
"postbox-submit": "Υποβολή",
|
"postbox-submit": "Υποβολή",
|
||||||
"num-comments": "Ένα σχόλιο\n{{ n }} σχόλια",
|
"num-comments": "Ένα σχόλιο\n{{ n }} σχόλια",
|
||||||
"no-comments": "Δεν υπάρχουν σχόλια",
|
"no-comments": "Δεν υπάρχουν σχόλια",
|
||||||
|
@ -3,11 +3,14 @@ define({
|
|||||||
"postbox-author": "Name (optional)",
|
"postbox-author": "Name (optional)",
|
||||||
"postbox-email": "E-mail (optional)",
|
"postbox-email": "E-mail (optional)",
|
||||||
"postbox-website": "Website (optional)",
|
"postbox-website": "Website (optional)",
|
||||||
|
"postbox-preview": "Preview",
|
||||||
|
"postbox-edit": "Edit",
|
||||||
"postbox-submit": "Submit",
|
"postbox-submit": "Submit",
|
||||||
"postbox-notification": "Subscribe to email notification of replies",
|
"postbox-notification": "Subscribe to email notification of replies",
|
||||||
|
|
||||||
"num-comments": "One Comment\n{{ n }} Comments",
|
"num-comments": "One Comment\n{{ n }} Comments",
|
||||||
"no-comments": "No Comments Yet",
|
"no-comments": "No Comments Yet",
|
||||||
|
"atom-feed": "Atom feed",
|
||||||
|
|
||||||
"comment-reply": "Reply",
|
"comment-reply": "Reply",
|
||||||
"comment-edit": "Edit",
|
"comment-edit": "Edit",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Nomo (malnepra)",
|
"postbox-author": "Nomo (malnepra)",
|
||||||
"postbox-email": "Retadreso (malnepra)",
|
"postbox-email": "Retadreso (malnepra)",
|
||||||
"postbox-website": "Retejo (malnepra)",
|
"postbox-website": "Retejo (malnepra)",
|
||||||
|
"postbox-preview": "Antaŭrigardo",
|
||||||
|
"postbox-edit": "Redaktu",
|
||||||
"postbox-submit": "Sendu",
|
"postbox-submit": "Sendu",
|
||||||
"num-comments": "{{ n }} komento\n{{ n }} komentoj",
|
"num-comments": "{{ n }} komento\n{{ n }} komentoj",
|
||||||
"no-comments": "Neniu komento ankoraŭ",
|
"no-comments": "Neniu komento ankoraŭ",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Nombre (opcional)",
|
"postbox-author": "Nombre (opcional)",
|
||||||
"postbox-email": "E-mail (opcional)",
|
"postbox-email": "E-mail (opcional)",
|
||||||
"postbox-website": "Sitio web (opcional)",
|
"postbox-website": "Sitio web (opcional)",
|
||||||
|
"postbox-preview": "Avance",
|
||||||
|
"postbox-edit": "Editar",
|
||||||
"postbox-submit": "Enviar",
|
"postbox-submit": "Enviar",
|
||||||
"num-comments": "Un Comentario\n{{ n }} Comentarios",
|
"num-comments": "Un Comentario\n{{ n }} Comentarios",
|
||||||
"no-comments": "Sin Comentarios Todavía",
|
"no-comments": "Sin Comentarios Todavía",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "اسم (اختیاری)",
|
"postbox-author": "اسم (اختیاری)",
|
||||||
"postbox-email": "ایمیل (اختیاری)",
|
"postbox-email": "ایمیل (اختیاری)",
|
||||||
"postbox-website": "سایت (اختیاری)",
|
"postbox-website": "سایت (اختیاری)",
|
||||||
|
"postbox-preview": "پیشنمایش",
|
||||||
|
"postbox-edit": "ویرایش",
|
||||||
"postbox-submit": "ارسال",
|
"postbox-submit": "ارسال",
|
||||||
|
|
||||||
"num-comments": "یک نظر\n{{ n }} نظر",
|
"num-comments": "یک نظر\n{{ n }} نظر",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Nimi (valinnainen)",
|
"postbox-author": "Nimi (valinnainen)",
|
||||||
"postbox-email": "Sähköposti (valinnainen)",
|
"postbox-email": "Sähköposti (valinnainen)",
|
||||||
"postbox-website": "Web-sivu (valinnainen)",
|
"postbox-website": "Web-sivu (valinnainen)",
|
||||||
|
"postbox-preview": "Esikatselu",
|
||||||
|
"postbox-edit": "Muokkaa",
|
||||||
"postbox-submit": "Lähetä",
|
"postbox-submit": "Lähetä",
|
||||||
|
|
||||||
"num-comments": "Yksi kommentti\n{{ n }} kommenttia",
|
"num-comments": "Yksi kommentti\n{{ n }} kommenttia",
|
||||||
|
@ -3,10 +3,13 @@ define({
|
|||||||
"postbox-author": "Nom (optionnel)",
|
"postbox-author": "Nom (optionnel)",
|
||||||
"postbox-email": "Courriel (optionnel)",
|
"postbox-email": "Courriel (optionnel)",
|
||||||
"postbox-website": "Site web (optionnel)",
|
"postbox-website": "Site web (optionnel)",
|
||||||
|
"postbox-preview": "Aperçu",
|
||||||
|
"postbox-edit": "Éditer",
|
||||||
"postbox-submit": "Soumettre",
|
"postbox-submit": "Soumettre",
|
||||||
"postbox-notification": "S'abonner aux notifications de réponses",
|
"postbox-notification": "S'abonner aux notifications de réponses",
|
||||||
"num-comments": "{{ n }} commentaire\n{{ n }} commentaires",
|
"num-comments": "{{ n }} commentaire\n{{ n }} commentaires",
|
||||||
"no-comments": "Aucun commentaire pour l'instant",
|
"no-comments": "Aucun commentaire pour l'instant",
|
||||||
|
"atom-feed": "Flux Atom",
|
||||||
"comment-reply": "Répondre",
|
"comment-reply": "Répondre",
|
||||||
"comment-edit": "Éditer",
|
"comment-edit": "Éditer",
|
||||||
"comment-save": "Enregistrer",
|
"comment-save": "Enregistrer",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Ime (neobavezno)",
|
"postbox-author": "Ime (neobavezno)",
|
||||||
"postbox-email": "E-mail (neobavezno)",
|
"postbox-email": "E-mail (neobavezno)",
|
||||||
"postbox-website": "Web stranica (neobavezno)",
|
"postbox-website": "Web stranica (neobavezno)",
|
||||||
|
"postbox-preview": "Pregled",
|
||||||
|
"postbox-edit": "Uredi",
|
||||||
"postbox-submit": "Pošalji",
|
"postbox-submit": "Pošalji",
|
||||||
"num-comments": "Jedan komentar\n{{ n }} komentara",
|
"num-comments": "Jedan komentar\n{{ n }} komentara",
|
||||||
"no-comments": "Još nema komentara",
|
"no-comments": "Još nema komentara",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Név (nem kötelező)",
|
"postbox-author": "Név (nem kötelező)",
|
||||||
"postbox-email": "Email (nem kötelező)",
|
"postbox-email": "Email (nem kötelező)",
|
||||||
"postbox-website": "Website (nem kötelező)",
|
"postbox-website": "Website (nem kötelező)",
|
||||||
|
"postbox-preview": "Előnézet",
|
||||||
|
"postbox-edit": "Szerekesztés",
|
||||||
"postbox-submit": "Elküld",
|
"postbox-submit": "Elküld",
|
||||||
"num-comments": "Egy hozzászólás\n{{ n }} hozzászólás",
|
"num-comments": "Egy hozzászólás\n{{ n }} hozzászólás",
|
||||||
"no-comments": "Eddig nincs hozzászólás",
|
"no-comments": "Eddig nincs hozzászólás",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Nome (opzionale)",
|
"postbox-author": "Nome (opzionale)",
|
||||||
"postbox-email": "E-mail (opzionale)",
|
"postbox-email": "E-mail (opzionale)",
|
||||||
"postbox-website": "Sito web (opzionale)",
|
"postbox-website": "Sito web (opzionale)",
|
||||||
|
"postbox-preview": "Anteprima",
|
||||||
|
"postbox-edit": "Modifica",
|
||||||
"postbox-submit": "Invia",
|
"postbox-submit": "Invia",
|
||||||
"num-comments": "Un Commento\n{{ n }} Commenti",
|
"num-comments": "Un Commento\n{{ n }} Commenti",
|
||||||
"no-comments": "Ancora Nessun Commento",
|
"no-comments": "Ancora Nessun Commento",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Naam (optioneel)",
|
"postbox-author": "Naam (optioneel)",
|
||||||
"postbox-email": "E-mail (optioneel)",
|
"postbox-email": "E-mail (optioneel)",
|
||||||
"postbox-website": "Website (optioneel)",
|
"postbox-website": "Website (optioneel)",
|
||||||
|
"postbox-preview": "Voorbeeld",
|
||||||
|
"postbox-edit": "Bewerken",
|
||||||
"postbox-submit": "Versturen",
|
"postbox-submit": "Versturen",
|
||||||
"num-comments": "Één reactie\n{{ n }} reacties",
|
"num-comments": "Één reactie\n{{ n }} reacties",
|
||||||
"no-comments": "Nog geen reacties",
|
"no-comments": "Nog geen reacties",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Imię/nick (opcjonalnie)",
|
"postbox-author": "Imię/nick (opcjonalnie)",
|
||||||
"postbox-email": "E-mail (opcjonalnie)",
|
"postbox-email": "E-mail (opcjonalnie)",
|
||||||
"postbox-website": "Strona (opcjonalnie)",
|
"postbox-website": "Strona (opcjonalnie)",
|
||||||
|
"postbox-preview": "Visualizar",
|
||||||
|
"postbox-edit": "Edytuj",
|
||||||
"postbox-submit": "Wyślij",
|
"postbox-submit": "Wyślij",
|
||||||
"num-comments": "Jeden komentarz\n{{ n }} komentarzy",
|
"num-comments": "Jeden komentarz\n{{ n }} komentarzy",
|
||||||
"no-comments": "Jeszcze nie ma komentarzy",
|
"no-comments": "Jeszcze nie ma komentarzy",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Имя (необязательно)",
|
"postbox-author": "Имя (необязательно)",
|
||||||
"postbox-email": "Email (необязательно)",
|
"postbox-email": "Email (необязательно)",
|
||||||
"postbox-website": "Сайт (необязательно)",
|
"postbox-website": "Сайт (необязательно)",
|
||||||
|
"postbox-preview": "анонс",
|
||||||
|
"postbox-edit": "Правка",
|
||||||
"postbox-submit": "Отправить",
|
"postbox-submit": "Отправить",
|
||||||
"num-comments": "{{ n }} комментарий\n{{ n }} комментария\n{{ n }} комментариев",
|
"num-comments": "{{ n }} комментарий\n{{ n }} комментария\n{{ n }} комментариев",
|
||||||
"no-comments": "Пока нет комментариев",
|
"no-comments": "Пока нет комментариев",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Namn (frivilligt)",
|
"postbox-author": "Namn (frivilligt)",
|
||||||
"postbox-email": "E-mail (frivilligt)",
|
"postbox-email": "E-mail (frivilligt)",
|
||||||
"postbox-website": "Hemsida (frivilligt)",
|
"postbox-website": "Hemsida (frivilligt)",
|
||||||
|
"postbox-preview": "Förhandsvisning",
|
||||||
|
"postbox-edit": "Redigera",
|
||||||
"postbox-submit": "Skicka",
|
"postbox-submit": "Skicka",
|
||||||
"num-comments": "En kommentar\n{{ n }} kommentarer",
|
"num-comments": "En kommentar\n{{ n }} kommentarer",
|
||||||
"no-comments": "Inga kommentarer än",
|
"no-comments": "Inga kommentarer än",
|
||||||
|
@ -3,6 +3,8 @@ define({
|
|||||||
"postbox-author": "Tên (tùy chọn)",
|
"postbox-author": "Tên (tùy chọn)",
|
||||||
"postbox-email": "E-mail (tùy chọn)",
|
"postbox-email": "E-mail (tùy chọn)",
|
||||||
"postbox-website": "Website (tùy chọn)",
|
"postbox-website": "Website (tùy chọn)",
|
||||||
|
"postbox-preview": "Xem trước",
|
||||||
|
"postbox-edit": "Sửa",
|
||||||
"postbox-submit": "Gửi",
|
"postbox-submit": "Gửi",
|
||||||
|
|
||||||
"num-comments": "Một bình luận\n{{ n }} bình luận",
|
"num-comments": "Một bình luận\n{{ n }} bình luận",
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
define({
|
define({
|
||||||
"postbox-text": "在此输入评论 (最少3个字符)",
|
"postbox-text": "在此输入评论 (最少 3 个字符)",
|
||||||
"postbox-author": "名字 (可选)",
|
"postbox-author": "名字 (可选)",
|
||||||
"postbox-email": "E-mail (可选)",
|
"postbox-email": "E-mail (可选)",
|
||||||
"postbox-website": "网站 (可选)",
|
"postbox-website": "网站 (可选)",
|
||||||
|
"postbox-preview": "预习",
|
||||||
|
"postbox-edit": "编辑",
|
||||||
"postbox-submit": "提交",
|
"postbox-submit": "提交",
|
||||||
|
|
||||||
"num-comments": "1条评论\n{{ n }}条评论",
|
"num-comments": "1 条评论\n{{ n }} 条评论",
|
||||||
"no-comments": "还没有评论",
|
"no-comments": "还没有评论",
|
||||||
|
|
||||||
"comment-reply": "回复",
|
"comment-reply": "回复",
|
||||||
@ -21,10 +23,10 @@ define({
|
|||||||
"comment-hidden": "{{ n }} 条评论已隐藏",
|
"comment-hidden": "{{ n }} 条评论已隐藏",
|
||||||
|
|
||||||
"date-now": "刚刚",
|
"date-now": "刚刚",
|
||||||
"date-minute": "1分钟前\n{{ n }}分钟前",
|
"date-minute": "1 分钟前\n{{ n }} 分钟前",
|
||||||
"date-hour": "1小时前\n{{ n }}小时前",
|
"date-hour": "1 小时前\n{{ n }} 小时前",
|
||||||
"date-day": "昨天\n{{ n }}天前",
|
"date-day": "昨天\n{{ n }} 天前",
|
||||||
"date-week": "上周\n{{ n }}周前",
|
"date-week": "上周\n{{ n }} 周前",
|
||||||
"date-month": "上个月\n{{ n }}个月前",
|
"date-month": "上个月\n{{ n }} 个月前",
|
||||||
"date-year": "去年\n{{ n }}年前"
|
"date-year": "去年\n{{ n }} 年前"
|
||||||
});
|
});
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
define({
|
define({
|
||||||
"postbox-text": "在此輸入留言(至少3個字元)",
|
"postbox-text": "在此輸入留言(至少 3 個字元)",
|
||||||
"postbox-author": "名稱 (非必填)",
|
"postbox-author": "名稱 (非必填)",
|
||||||
"postbox-email": "電子信箱 (非必填)",
|
"postbox-email": "電子信箱 (非必填)",
|
||||||
"postbox-website": "個人網站 (非必填)",
|
"postbox-website": "個人網站 (非必填)",
|
||||||
|
"postbox-preview": "預習",
|
||||||
|
"postbox-edit": "編輯",
|
||||||
"postbox-submit": "送出",
|
"postbox-submit": "送出",
|
||||||
|
|
||||||
"num-comments": "1則留言\n{{ n }}則留言",
|
"num-comments": "1 則留言\n{{ n }} 則留言",
|
||||||
"no-comments": "尚無留言",
|
"no-comments": "尚無留言",
|
||||||
|
|
||||||
"comment-reply": "回覆",
|
"comment-reply": "回覆",
|
||||||
@ -18,13 +20,13 @@ define({
|
|||||||
"comment-deleted": "留言已刪",
|
"comment-deleted": "留言已刪",
|
||||||
"comment-queued": "留言待審",
|
"comment-queued": "留言待審",
|
||||||
"comment-anonymous": "匿名",
|
"comment-anonymous": "匿名",
|
||||||
"comment-hidden": "{{ n }}則隱藏留言",
|
"comment-hidden": "{{ n }} 則隱藏留言",
|
||||||
|
|
||||||
"date-now": "剛剛",
|
"date-now": "剛剛",
|
||||||
"date-minute": "1分鐘前\n{{ n }}分鐘前",
|
"date-minute": "1 分鐘前\n{{ n }} 分鐘前",
|
||||||
"date-hour": "1小時前\n{{ n }}小時前",
|
"date-hour": "1 小時前\n{{ n }} 小時前",
|
||||||
"date-day": "昨天\n{{ n }}天前",
|
"date-day": "昨天\n{{ n }} 天前",
|
||||||
"date-week": "上週\n{{ n }}週前",
|
"date-week": "上週\n{{ n }} 週前",
|
||||||
"date-month": "上個月\n{{ n }}個月前",
|
"date-month": "上個月\n{{ n }} 個月前",
|
||||||
"date-year": "去年\n{{ n }}年前"
|
"date-year": "去年\n{{ n }} 年前"
|
||||||
});
|
});
|
||||||
|
@ -11,7 +11,8 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
|||||||
el = $.htmlify(jade.render("postbox", {
|
el = $.htmlify(jade.render("postbox", {
|
||||||
"author": JSON.parse(localStorage.getItem("author")),
|
"author": JSON.parse(localStorage.getItem("author")),
|
||||||
"email": JSON.parse(localStorage.getItem("email")),
|
"email": JSON.parse(localStorage.getItem("email")),
|
||||||
"website": JSON.parse(localStorage.getItem("website"))
|
"website": JSON.parse(localStorage.getItem("website")),
|
||||||
|
"preview": ''
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// callback on success (e.g. to toggle the reply button)
|
// callback on success (e.g. to toggle the reply button)
|
||||||
@ -62,9 +63,27 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
|||||||
$("[name='author']", el).placeholder.replace(/ \(.*\)/, "");
|
$("[name='author']", el).placeholder.replace(/ \(.*\)/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// preview function
|
||||||
|
$("[name='preview']", el).on("click", function() {
|
||||||
|
api.preview(utils.text($(".textarea", el).innerHTML)).then(
|
||||||
|
function(html) {
|
||||||
|
$(".preview .text", el).innerHTML = html;
|
||||||
|
el.classList.add('preview-mode');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// edit function
|
||||||
|
var edit = function() {
|
||||||
|
$(".preview .text", el).innerHTML = '';
|
||||||
|
el.classList.remove('preview-mode');
|
||||||
|
};
|
||||||
|
$("[name='edit']", el).on("click", edit);
|
||||||
|
$(".preview", el).on("click", edit);
|
||||||
|
|
||||||
// submit form, initialize optional fields with `null` and reset form.
|
// submit form, initialize optional fields with `null` and reset form.
|
||||||
// If replied to a comment, remove form completely.
|
// If replied to a comment, remove form completely.
|
||||||
$("[type=submit]", el).on("click", function() {
|
$("[type=submit]", el).on("click", function() {
|
||||||
|
edit();
|
||||||
if (! el.validate()) {
|
if (! el.validate()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -240,7 +259,7 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
|||||||
$("a.edit", footer).toggle("click",
|
$("a.edit", footer).toggle("click",
|
||||||
function(toggler) {
|
function(toggler) {
|
||||||
var edit = $("a.edit", footer);
|
var edit = $("a.edit", footer);
|
||||||
var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null;
|
var avatar = config["avatar"] || config["gravatar"] ? $(".avatar", el, false)[0] : null;
|
||||||
|
|
||||||
edit.textContent = i18n.translate("comment-save");
|
edit.textContent = i18n.translate("comment-save");
|
||||||
edit.insertAfter($.new("a.cancel", i18n.translate("comment-cancel"))).on("click", function() {
|
edit.insertAfter($.new("a.cancel", i18n.translate("comment-cancel"))).on("click", function() {
|
||||||
@ -268,7 +287,7 @@ define(["app/dom", "app/utils", "app/config", "app/api", "app/jade", "app/i18n",
|
|||||||
},
|
},
|
||||||
function(toggler) {
|
function(toggler) {
|
||||||
var textarea = $(".textarea", text);
|
var textarea = $(".textarea", text);
|
||||||
var avatar = config["avatar"] ? $(".avatar", el, false)[0] : null;
|
var avatar = config["avatar"] || config["gravatar"] ? $(".avatar", el, false)[0] : null;
|
||||||
|
|
||||||
if (! toggler.canceled && textarea !== null) {
|
if (! toggler.canceled && textarea !== null) {
|
||||||
if (utils.text(textarea.innerHTML).length < 3) {
|
if (utils.text(textarea.innerHTML).length < 3) {
|
||||||
|
@ -7,6 +7,9 @@ define(["libjs-jade-runtime", "app/utils", "jade!app/text/postbox", "jade!app/te
|
|||||||
var load = function(name, js) {
|
var load = function(name, js) {
|
||||||
templates[name] = (function(jade) {
|
templates[name] = (function(jade) {
|
||||||
var fn;
|
var fn;
|
||||||
|
if (js.compiled) {
|
||||||
|
return js(jade);
|
||||||
|
}
|
||||||
eval("fn = " + js);
|
eval("fn = " + js);
|
||||||
return fn;
|
return fn;
|
||||||
})(runtime);
|
})(runtime);
|
||||||
|
@ -1,4 +1,7 @@
|
|||||||
div(class='isso-comment' id='isso-#{comment.id}')
|
div(class='isso-comment' id='isso-#{comment.id}')
|
||||||
|
if conf.gravatar
|
||||||
|
div(class='avatar')
|
||||||
|
img(src='#{comment.gravatar_image}')
|
||||||
if conf.avatar
|
if conf.avatar
|
||||||
div(class='avatar')
|
div(class='avatar')
|
||||||
svg(data-hash='#{comment.hash}')
|
svg(data-hash='#{comment.hash}')
|
||||||
|
@ -3,6 +3,10 @@ div(class='isso-postbox')
|
|||||||
div(class='textarea-wrapper')
|
div(class='textarea-wrapper')
|
||||||
div(class='textarea placeholder' contenteditable='true')
|
div(class='textarea placeholder' contenteditable='true')
|
||||||
= i18n('postbox-text')
|
= i18n('postbox-text')
|
||||||
|
div(class='preview')
|
||||||
|
div(class='isso-comment')
|
||||||
|
div(class='text-wrapper')
|
||||||
|
div(class='text')
|
||||||
section(class='auth-section')
|
section(class='auth-section')
|
||||||
p(class='input-wrapper')
|
p(class='input-wrapper')
|
||||||
input(type='text' name='author' placeholder=i18n('postbox-author')
|
input(type='text' name='author' placeholder=i18n('postbox-author')
|
||||||
@ -15,6 +19,12 @@ div(class='isso-postbox')
|
|||||||
value=website != null ? '#{website}' : '')
|
value=website != null ? '#{website}' : '')
|
||||||
p(class='post-action')
|
p(class='post-action')
|
||||||
input(type='submit' value=i18n('postbox-submit'))
|
input(type='submit' value=i18n('postbox-submit'))
|
||||||
|
p(class='post-action')
|
||||||
|
input(type='button' name='preview'
|
||||||
|
value=i18n('postbox-preview'))
|
||||||
|
p(class='post-action')
|
||||||
|
input(type='button' name='edit'
|
||||||
|
value=i18n('postbox-edit'))
|
||||||
section(class='notification-section')
|
section(class='notification-section')
|
||||||
label
|
label
|
||||||
input(type='checkbox' name='notification')
|
input(type='checkbox' name='notification')
|
||||||
|
@ -27,6 +27,13 @@ require(["app/lib/ready", "app/config", "app/i18n", "app/api", "app/isso", "app/
|
|||||||
return console.log("abort, #isso-thread is missing");
|
return console.log("abort, #isso-thread is missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config["feed"]) {
|
||||||
|
var feedLink = $.new('a', i18n.translate('atom-feed'));
|
||||||
|
var feedLinkWrapper = $.new('span.isso-feedlink');
|
||||||
|
feedLink.href = api.feed($("#isso-thread").getAttribute("data-isso-id"));
|
||||||
|
feedLinkWrapper.append(feedLink);
|
||||||
|
$("#isso-thread").append(feedLinkWrapper);
|
||||||
|
}
|
||||||
$("#isso-thread").append($.new('h4'));
|
$("#isso-thread").append($.new('h4'));
|
||||||
$("#isso-thread").append(new isso.Postbox(null));
|
$("#isso-thread").append(new isso.Postbox(null));
|
||||||
$("#isso-thread").append('<div id="isso-root"></div>');
|
$("#isso-thread").append('<div id="isso-root"></div>');
|
||||||
|
@ -49,8 +49,12 @@ define(function() {
|
|||||||
write: function(plugin, name, write) {
|
write: function(plugin, name, write) {
|
||||||
if (builds.hasOwnProperty(name)) {
|
if (builds.hasOwnProperty(name)) {
|
||||||
write("define('" + plugin + "!" + name +"', function () {" +
|
write("define('" + plugin + "!" + name +"', function () {" +
|
||||||
" var fn = " + builds[name] + ";" +
|
" var wfn = function (jade) {" +
|
||||||
" return fn;" +
|
" var fn = " + builds[name] + ";" +
|
||||||
|
" return fn;" +
|
||||||
|
" };" +
|
||||||
|
"wfn.compiled = true;" +
|
||||||
|
"return wfn;" +
|
||||||
"});\n");
|
"});\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -55,7 +55,7 @@ class Progress(object):
|
|||||||
|
|
||||||
if time() - self.last > 0.2:
|
if time() - self.last > 0.2:
|
||||||
sys.stdout.write("\r{0}".format(" " * cols))
|
sys.stdout.write("\r{0}".format(" " * cols))
|
||||||
sys.stdout.write("\r[{0:.0%}] {1}".format(i/self.end, message))
|
sys.stdout.write("\r[{0:.0%}] {1}".format(i / self.end, message))
|
||||||
sys.stdout.flush()
|
sys.stdout.flush()
|
||||||
self.last = time()
|
self.last = time()
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ from __future__ import unicode_literals
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ class TestComments(unittest.TestCase):
|
|||||||
conf.set("general", "dbpath", self.path)
|
conf.set("general", "dbpath", self.path)
|
||||||
conf.set("guard", "enabled", "off")
|
conf.set("guard", "enabled", "off")
|
||||||
conf.set("hash", "algorithm", "none")
|
conf.set("hash", "algorithm", "none")
|
||||||
|
self.conf = conf
|
||||||
|
|
||||||
class App(Isso, core.Mixin):
|
class App(Isso, core.Mixin):
|
||||||
pass
|
pass
|
||||||
@ -132,13 +134,13 @@ class TestComments(unittest.TestCase):
|
|||||||
self.assertFalse(verify({"text": text}))
|
self.assertFalse(verify({"text": text}))
|
||||||
|
|
||||||
# email/website length
|
# email/website length
|
||||||
self.assertTrue(verify({"text": "...", "email": "*"*254}))
|
self.assertTrue(verify({"text": "...", "email": "*" * 254}))
|
||||||
self.assertTrue(
|
self.assertTrue(
|
||||||
verify({"text": "...", "website": "google.de/" + "a"*128}))
|
verify({"text": "...", "website": "google.de/" + "a" * 128}))
|
||||||
|
|
||||||
self.assertFalse(verify({"text": "...", "email": "*"*1024}))
|
self.assertFalse(verify({"text": "...", "email": "*" * 1024}))
|
||||||
self.assertFalse(
|
self.assertFalse(
|
||||||
verify({"text": "...", "website": "google.de/" + "*"*1024}))
|
verify({"text": "...", "website": "google.de/" + "*" * 1024}))
|
||||||
|
|
||||||
# valid website url
|
# valid website url
|
||||||
self.assertTrue(comments.isurl("example.tld"))
|
self.assertTrue(comments.isurl("example.tld"))
|
||||||
@ -320,10 +322,41 @@ class TestComments(unittest.TestCase):
|
|||||||
rv = loads(rv.data)
|
rv = loads(rv.data)
|
||||||
|
|
||||||
for key in comments.API.FIELDS:
|
for key in comments.API.FIELDS:
|
||||||
rv.pop(key)
|
if key in rv:
|
||||||
|
rv.pop(key)
|
||||||
|
|
||||||
self.assertListEqual(list(rv.keys()), [])
|
self.assertListEqual(list(rv.keys()), [])
|
||||||
|
|
||||||
|
def testNoFeed(self):
|
||||||
|
rv = self.get('/feed?uri=%2Fpath%2Fnothing')
|
||||||
|
self.assertEqual(rv.status_code, 404)
|
||||||
|
|
||||||
|
def testFeedEmpty(self):
|
||||||
|
self.conf.set("rss", "base", "https://example.org")
|
||||||
|
|
||||||
|
rv = self.get('/feed?uri=%2Fpath%2Fnothing')
|
||||||
|
self.assertEqual(rv.status_code, 200)
|
||||||
|
self.assertEqual(rv.headers['ETag'], '"empty"')
|
||||||
|
data = rv.data.decode('utf-8')
|
||||||
|
self.assertEqual(data, """<?xml version=\'1.0\' encoding=\'utf-8\'?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"><updated>1970-01-01T01:00:00Z</updated><id>tag:example.org,2018:/isso/thread/path/nothing</id><title>Comments for example.org/path/nothing</title></feed>""")
|
||||||
|
|
||||||
|
def testFeed(self):
|
||||||
|
self.conf.set("rss", "base", "https://example.org")
|
||||||
|
|
||||||
|
self.post('/new?uri=%2Fpath%2F', data=json.dumps({'text': 'First'}))
|
||||||
|
self.post('/new?uri=%2Fpath%2F',
|
||||||
|
data=json.dumps({'text': '*Second*', 'parent': 1}))
|
||||||
|
|
||||||
|
rv = self.get('/feed?uri=%2Fpath%2F')
|
||||||
|
self.assertEqual(rv.status_code, 200)
|
||||||
|
self.assertEqual(rv.headers['ETag'], '"1-2"')
|
||||||
|
data = rv.data.decode('utf-8')
|
||||||
|
data = re.sub('[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+Z',
|
||||||
|
'2018-04-01T10:00:00Z', data)
|
||||||
|
self.assertEqual(data, """<?xml version=\'1.0\' encoding=\'utf-8\'?>
|
||||||
|
<feed xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0"><updated>2018-04-01T10:00:00Z</updated><id>tag:example.org,2018:/isso/thread/path/</id><title>Comments for example.org/path/</title><entry><id>tag:example.org,2018:/isso/1/2</id><title>Comment #2</title><updated>2018-04-01T10:00:00Z</updated><author><name /></author><link href="https://example.org/path/#isso-2" /><content type="html"><p><em>Second</em></p></content><thr:in-reply-to href="https://example.org/path/#isso-1" ref="tag:example.org,2018:/isso/1/1" /></entry><entry><id>tag:example.org,2018:/isso/1/1</id><title>Comment #1</title><updated>2018-04-01T10:00:00Z</updated><author><name /></author><link href="https://example.org/path/#isso-1" /><content type="html"><p>First</p></content></entry></feed>""")
|
||||||
|
|
||||||
def testCounts(self):
|
def testCounts(self):
|
||||||
|
|
||||||
self.assertEqual(self.get('/count?uri=%2Fpath%2F').status_code, 404)
|
self.assertEqual(self.get('/count?uri=%2Fpath%2F').status_code, 404)
|
||||||
|
@ -29,7 +29,7 @@ class TestHTML(unittest.TestCase):
|
|||||||
self.assertEqual(convert(input), expected)
|
self.assertEqual(convert(input), expected)
|
||||||
|
|
||||||
def test_github_flavoured_markdown(self):
|
def test_github_flavoured_markdown(self):
|
||||||
convert = html.Markdown(extensions=("fenced_code", ))
|
convert = html.Markdown(extensions=("fenced-code", ))
|
||||||
|
|
||||||
# without lang
|
# without lang
|
||||||
_in = textwrap.dedent("""\
|
_in = textwrap.dedent("""\
|
||||||
@ -65,13 +65,17 @@ class TestHTML(unittest.TestCase):
|
|||||||
examples = [
|
examples = [
|
||||||
('Look: <img src="..." />', 'Look: '),
|
('Look: <img src="..." />', 'Look: '),
|
||||||
('<a href="http://example.org/">Ha</a>',
|
('<a href="http://example.org/">Ha</a>',
|
||||||
'<a href="http://example.org/">Ha</a>'),
|
['<a href="http://example.org/" rel="nofollow noopener">Ha</a>',
|
||||||
|
'<a rel="nofollow noopener" href="http://example.org/">Ha</a>']),
|
||||||
('<a href="sms:+1234567890">Ha</a>', '<a>Ha</a>'),
|
('<a href="sms:+1234567890">Ha</a>', '<a>Ha</a>'),
|
||||||
('<p style="visibility: hidden;">Test</p>', '<p>Test</p>'),
|
('<p style="visibility: hidden;">Test</p>', '<p>Test</p>'),
|
||||||
('<script>alert("Onoe")</script>', 'alert("Onoe")')]
|
('<script>alert("Onoe")</script>', 'alert("Onoe")')]
|
||||||
|
|
||||||
for (input, expected) in examples:
|
for (input, expected) in examples:
|
||||||
self.assertEqual(html.sanitize(sanitizer, input), expected)
|
if isinstance(expected, list):
|
||||||
|
self.assertIn(html.sanitize(sanitizer, input), expected)
|
||||||
|
else:
|
||||||
|
self.assertEqual(html.sanitize(sanitizer, input), expected)
|
||||||
|
|
||||||
@unittest.skipIf(html.HTML5LIB_VERSION <= html.HTML5LIB_SIMPLETREE, "backport")
|
@unittest.skipIf(html.HTML5LIB_VERSION <= html.HTML5LIB_SIMPLETREE, "backport")
|
||||||
def test_sanitizer_extensions(self):
|
def test_sanitizer_extensions(self):
|
||||||
@ -92,5 +96,6 @@ class TestHTML(unittest.TestCase):
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
renderer = html.Markup(conf.section("markup")).render
|
renderer = html.Markup(conf.section("markup")).render
|
||||||
self.assertEqual(renderer("http://example.org/ and sms:+1234567890"),
|
self.assertIn(renderer("http://example.org/ and sms:+1234567890"),
|
||||||
'<p><a href="http://example.org/">http://example.org/</a> and sms:+1234567890</p>')
|
['<p><a href="http://example.org/" rel="nofollow noopener">http://example.org/</a> and sms:+1234567890</p>',
|
||||||
|
'<p><a rel="nofollow noopener" href="http://example.org/">http://example.org/</a> and sms:+1234567890</p>'])
|
||||||
|
@ -36,7 +36,7 @@ def anonymize(remote_addr):
|
|||||||
ipv6 = ipaddress.IPv6Address(remote_addr)
|
ipv6 = ipaddress.IPv6Address(remote_addr)
|
||||||
if ipv6.ipv4_mapped is not None:
|
if ipv6.ipv4_mapped is not None:
|
||||||
return anonymize(ipv6.ipv4_mapped)
|
return anonymize(ipv6.ipv4_mapped)
|
||||||
return u'' + ipv6.exploded.rsplit(':', 5)[0] + ':' + ':'.join(['0000']*5)
|
return u'' + ipv6.exploded.rsplit(':', 5)[0] + ':' + ':'.join(['0000'] * 5)
|
||||||
except ipaddress.AddressValueError:
|
except ipaddress.AddressValueError:
|
||||||
return u'0.0.0.0'
|
return u'0.0.0.0'
|
||||||
|
|
||||||
@ -89,11 +89,11 @@ class Bloomfilter:
|
|||||||
|
|
||||||
def add(self, key):
|
def add(self, key):
|
||||||
for i in self.get_probes(key):
|
for i in self.get_probes(key):
|
||||||
self.array[i//8] |= 2 ** (i % 8)
|
self.array[i // 8] |= 2 ** (i % 8)
|
||||||
self.elements += 1
|
self.elements += 1
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
return all(self.array[i//8] & (2 ** (i % 8)) for i in self.get_probes(key))
|
return all(self.array[i // 8] & (2 ** (i % 8)) for i in self.get_probes(key))
|
||||||
|
|
||||||
def __len__(self):
|
def __len__(self):
|
||||||
return self.elements
|
return self.elements
|
||||||
@ -132,3 +132,10 @@ class JSONResponse(Response):
|
|||||||
kwargs["content_type"] = "application/json"
|
kwargs["content_type"] = "application/json"
|
||||||
super(JSONResponse, self).__init__(
|
super(JSONResponse, self).__init__(
|
||||||
json.dumps(obj).encode("utf-8"), *args, **kwargs)
|
json.dumps(obj).encode("utf-8"), *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class XMLResponse(Response):
|
||||||
|
def __init__(self, obj, *args, **kwargs):
|
||||||
|
kwargs["content_type"] = "text/xml"
|
||||||
|
super(XMLResponse, self).__init__(
|
||||||
|
obj, *args, **kwargs)
|
||||||
|
@ -110,3 +110,4 @@ def new(conf):
|
|||||||
|
|
||||||
|
|
||||||
sha1 = Hash(func="sha1").uhash
|
sha1 = Hash(func="sha1").uhash
|
||||||
|
md5 = Hash(func="md5").uhash
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
import operator
|
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
||||||
from distutils.version import LooseVersion as Version
|
from distutils.version import LooseVersion as Version
|
||||||
@ -10,8 +9,6 @@ from distutils.version import LooseVersion as Version
|
|||||||
HTML5LIB_VERSION = Version(pkg_resources.get_distribution("html5lib").version)
|
HTML5LIB_VERSION = Version(pkg_resources.get_distribution("html5lib").version)
|
||||||
HTML5LIB_SIMPLETREE = Version("0.95")
|
HTML5LIB_SIMPLETREE = Version("0.95")
|
||||||
|
|
||||||
from isso.compat import reduce
|
|
||||||
|
|
||||||
import html5lib
|
import html5lib
|
||||||
from html5lib.sanitizer import HTMLSanitizer
|
from html5lib.sanitizer import HTMLSanitizer
|
||||||
from html5lib.serializer import HTMLSerializer
|
from html5lib.serializer import HTMLSerializer
|
||||||
@ -23,7 +20,8 @@ def Sanitizer(elements, attributes):
|
|||||||
|
|
||||||
class Inner(HTMLSanitizer):
|
class Inner(HTMLSanitizer):
|
||||||
|
|
||||||
# attributes found in Sundown's HTML serializer [1] except for <img> tag,
|
# attributes found in Sundown's HTML serializer [1]
|
||||||
|
# except for <img> tag,
|
||||||
# because images are not generated anyways.
|
# because images are not generated anyways.
|
||||||
#
|
#
|
||||||
# [1] https://github.com/vmg/sundown/blob/master/html/html.c
|
# [1] https://github.com/vmg/sundown/blob/master/html/html.c
|
||||||
@ -50,6 +48,11 @@ def sanitize(tokenizer, document):
|
|||||||
|
|
||||||
if HTML5LIB_VERSION > HTML5LIB_SIMPLETREE:
|
if HTML5LIB_VERSION > HTML5LIB_SIMPLETREE:
|
||||||
builder = "etree"
|
builder = "etree"
|
||||||
|
|
||||||
|
for link in domtree.findall(".//{http://www.w3.org/1999/xhtml}a"):
|
||||||
|
if link.get('href', None):
|
||||||
|
link.set("rel", "nofollow noopener")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
builder = "simpletree"
|
builder = "simpletree"
|
||||||
|
|
||||||
@ -60,14 +63,14 @@ def sanitize(tokenizer, document):
|
|||||||
return serializer.render(stream)
|
return serializer.render(stream)
|
||||||
|
|
||||||
|
|
||||||
def Markdown(extensions=("strikethrough", "superscript", "autolink")):
|
def Markdown(extensions=("strikethrough", "superscript", "autolink",
|
||||||
|
"fenced-code")):
|
||||||
|
|
||||||
flags = reduce(operator.xor, map(
|
renderer = Unofficial()
|
||||||
lambda ext: getattr(misaka, 'EXT_' + ext.upper()), extensions), 0)
|
md = misaka.Markdown(renderer, extensions=extensions)
|
||||||
md = misaka.Markdown(Unofficial(), extensions=flags)
|
|
||||||
|
|
||||||
def inner(text):
|
def inner(text):
|
||||||
rv = md.render(text).rstrip("\n")
|
rv = md(text).rstrip("\n")
|
||||||
if rv.startswith("<p>") or rv.endswith("</p>"):
|
if rv.startswith("<p>") or rv.endswith("</p>"):
|
||||||
return rv
|
return rv
|
||||||
return "<p>" + rv + "</p>"
|
return "<p>" + rv + "</p>"
|
||||||
@ -83,7 +86,7 @@ class Unofficial(misaka.HtmlRenderer):
|
|||||||
to <code class="$lang">, compatible with Highlight.js.
|
to <code class="$lang">, compatible with Highlight.js.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def block_code(self, text, lang):
|
def blockcode(self, text, lang):
|
||||||
lang = ' class="{0}"'.format(lang) if lang else ''
|
lang = ' class="{0}"'.format(lang) if lang else ''
|
||||||
return "<pre><code{1}>{0}</code></pre>\n".format(text, lang)
|
return "<pre><code{1}>{0}</code></pre>\n".format(text, lang)
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import functools
|
|||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from itsdangerous import SignatureExpired, BadSignature
|
from itsdangerous import SignatureExpired, BadSignature
|
||||||
|
from xml.etree import ElementTree as ET
|
||||||
|
|
||||||
from werkzeug.http import dump_cookie
|
from werkzeug.http import dump_cookie
|
||||||
from werkzeug.wsgi import get_current_url
|
from werkzeug.wsgi import get_current_url
|
||||||
@ -20,10 +21,21 @@ from werkzeug.exceptions import BadRequest, Forbidden, NotFound
|
|||||||
from isso.compat import text_type as str
|
from isso.compat import text_type as str
|
||||||
|
|
||||||
from isso import utils, local
|
from isso import utils, local
|
||||||
from isso.utils import (http, parse, JSONResponse as JSON,
|
from isso.utils import (http, parse,
|
||||||
|
JSONResponse as JSON, XMLResponse as XML,
|
||||||
render_template)
|
render_template)
|
||||||
from isso.views import requires
|
from isso.views import requires
|
||||||
from isso.utils.hash import sha1
|
from isso.utils.hash import sha1
|
||||||
|
from isso.utils.hash import md5
|
||||||
|
|
||||||
|
try:
|
||||||
|
from urlparse import urlparse
|
||||||
|
except ImportError:
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
try:
|
||||||
|
from StringIO import StringIO
|
||||||
|
except ImportError:
|
||||||
|
from io import BytesIO as StringIO
|
||||||
|
|
||||||
# from Django appearently, looks good to me *duck*
|
# from Django appearently, looks good to me *duck*
|
||||||
__url_re = re.compile(
|
__url_re = re.compile(
|
||||||
@ -81,7 +93,7 @@ def xhr(func):
|
|||||||
class API(object):
|
class API(object):
|
||||||
|
|
||||||
FIELDS = set(['id', 'parent', 'text', 'author', 'website',
|
FIELDS = set(['id', 'parent', 'text', 'author', 'website',
|
||||||
'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'notification'])
|
'mode', 'created', 'modified', 'likes', 'dislikes', 'hash', 'gravatar_image', 'notification'])
|
||||||
|
|
||||||
# comment fields, that can be submitted
|
# comment fields, that can be submitted
|
||||||
ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title', 'notification'])
|
ACCEPT = set(['text', 'author', 'website', 'email', 'parent', 'title', 'notification'])
|
||||||
@ -91,6 +103,7 @@ class API(object):
|
|||||||
('new', ('POST', '/new')),
|
('new', ('POST', '/new')),
|
||||||
('count', ('GET', '/count')),
|
('count', ('GET', '/count')),
|
||||||
('counts', ('POST', '/count')),
|
('counts', ('POST', '/count')),
|
||||||
|
('feed', ('GET', '/feed')),
|
||||||
('view', ('GET', '/id/<int:id>')),
|
('view', ('GET', '/id/<int:id>')),
|
||||||
('edit', ('PUT', '/id/<int:id>')),
|
('edit', ('PUT', '/id/<int:id>')),
|
||||||
('delete', ('DELETE', '/id/<int:id>')),
|
('delete', ('DELETE', '/id/<int:id>')),
|
||||||
@ -292,6 +305,8 @@ class API(object):
|
|||||||
self.cache.set(
|
self.cache.set(
|
||||||
'hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash'])
|
'hash', (rv['email'] or rv['remote_addr']).encode('utf-8'), rv['hash'])
|
||||||
|
|
||||||
|
rv = self._add_gravatar_image(rv)
|
||||||
|
|
||||||
for key in set(rv.keys()) - API.FIELDS:
|
for key in set(rv.keys()) - API.FIELDS:
|
||||||
rv.pop(key)
|
rv.pop(key)
|
||||||
|
|
||||||
@ -779,6 +794,18 @@ class API(object):
|
|||||||
|
|
||||||
return JSON(rv, 200)
|
return JSON(rv, 200)
|
||||||
|
|
||||||
|
def _add_gravatar_image(self, item):
|
||||||
|
if not self.conf.getboolean('gravatar'):
|
||||||
|
return item
|
||||||
|
|
||||||
|
email = item['email'] or ""
|
||||||
|
email_md5_hash = md5(email)
|
||||||
|
|
||||||
|
gravatar_url = self.conf.get('gravatar-url')
|
||||||
|
item['gravatar_image'] = gravatar_url.format(email_md5_hash)
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
def _process_fetched_list(self, fetched_list, plain=False):
|
def _process_fetched_list(self, fetched_list, plain=False):
|
||||||
for item in fetched_list:
|
for item in fetched_list:
|
||||||
|
|
||||||
@ -791,6 +818,8 @@ class API(object):
|
|||||||
|
|
||||||
item['hash'] = val
|
item['hash'] = val
|
||||||
|
|
||||||
|
item = self._add_gravatar_image(item)
|
||||||
|
|
||||||
for key in set(item.keys()) - API.FIELDS:
|
for key in set(item.keys()) - API.FIELDS:
|
||||||
item.pop(key)
|
item.pop(key)
|
||||||
|
|
||||||
@ -885,7 +914,6 @@ class API(object):
|
|||||||
@apiSuccessExample Counts of 5 threads:
|
@apiSuccessExample Counts of 5 threads:
|
||||||
[2, 18, 4, 0, 3]
|
[2, 18, 4, 0, 3]
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def counts(self, environ, request):
|
def counts(self, environ, request):
|
||||||
|
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
@ -895,6 +923,125 @@ class API(object):
|
|||||||
|
|
||||||
return JSON(self.comments.count(*data), 200)
|
return JSON(self.comments.count(*data), 200)
|
||||||
|
|
||||||
|
"""
|
||||||
|
@api {get} /feed Atom feed for comments
|
||||||
|
@apiGroup Thread
|
||||||
|
@apiDescription
|
||||||
|
Provide an Atom feed for the given thread.
|
||||||
|
"""
|
||||||
|
@requires(str, 'uri')
|
||||||
|
def feed(self, environ, request, uri):
|
||||||
|
conf = self.isso.conf.section("rss")
|
||||||
|
if not conf.get('base'):
|
||||||
|
raise NotFound
|
||||||
|
|
||||||
|
args = {
|
||||||
|
'uri': uri,
|
||||||
|
'order_by': 'id',
|
||||||
|
'asc': 0,
|
||||||
|
'limit': conf.getint('limit')
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
args['limit'] = max(int(request.args.get('limit')), args['limit'])
|
||||||
|
except TypeError:
|
||||||
|
pass
|
||||||
|
except ValueError:
|
||||||
|
return BadRequest("limit should be integer")
|
||||||
|
comments = self.comments.fetch(**args)
|
||||||
|
base = conf.get('base')
|
||||||
|
hostname = urlparse(base).netloc
|
||||||
|
|
||||||
|
# Let's build an Atom feed.
|
||||||
|
# RFC 4287: https://tools.ietf.org/html/rfc4287
|
||||||
|
# RFC 4685: https://tools.ietf.org/html/rfc4685 (threading extensions)
|
||||||
|
# For IDs: http://web.archive.org/web/20110514113830/http://diveintomark.org/archives/2004/05/28/howto-atom-id
|
||||||
|
feed = ET.Element('feed', {
|
||||||
|
'xmlns': 'http://www.w3.org/2005/Atom',
|
||||||
|
'xmlns:thr': 'http://purl.org/syndication/thread/1.0'
|
||||||
|
})
|
||||||
|
|
||||||
|
# For feed ID, we would use thread ID, but we may not have
|
||||||
|
# one. Therefore, we use the URI. We don't have a year
|
||||||
|
# either...
|
||||||
|
id = ET.SubElement(feed, 'id')
|
||||||
|
id.text = 'tag:{hostname},2018:/isso/thread{uri}'.format(
|
||||||
|
hostname=hostname, uri=uri)
|
||||||
|
|
||||||
|
# For title, we don't have much either. Be pretty generic.
|
||||||
|
title = ET.SubElement(feed, 'title')
|
||||||
|
title.text = 'Comments for {hostname}{uri}'.format(
|
||||||
|
hostname=hostname, uri=uri)
|
||||||
|
|
||||||
|
comment0 = None
|
||||||
|
|
||||||
|
for comment in comments:
|
||||||
|
if comment0 is None:
|
||||||
|
comment0 = comment
|
||||||
|
|
||||||
|
entry = ET.SubElement(feed, 'entry')
|
||||||
|
# We don't use a real date in ID either to help with
|
||||||
|
# threading.
|
||||||
|
id = ET.SubElement(entry, 'id')
|
||||||
|
id.text = 'tag:{hostname},2018:/isso/{tid}/{id}'.format(
|
||||||
|
hostname=hostname,
|
||||||
|
tid=comment['tid'],
|
||||||
|
id=comment['id'])
|
||||||
|
title = ET.SubElement(entry, 'title')
|
||||||
|
title.text = 'Comment #{}'.format(comment['id'])
|
||||||
|
updated = ET.SubElement(entry, 'updated')
|
||||||
|
updated.text = '{}Z'.format(datetime.fromtimestamp(
|
||||||
|
comment['modified'] or comment['created']).isoformat())
|
||||||
|
author = ET.SubElement(entry, 'author')
|
||||||
|
name = ET.SubElement(author, 'name')
|
||||||
|
name.text = comment['author']
|
||||||
|
ET.SubElement(entry, 'link', {
|
||||||
|
'href': '{base}{uri}#isso-{id}'.format(
|
||||||
|
base=base,
|
||||||
|
uri=uri, id=comment['id'])
|
||||||
|
})
|
||||||
|
content = ET.SubElement(entry, 'content', {
|
||||||
|
'type': 'html',
|
||||||
|
})
|
||||||
|
content.text = self.isso.render(comment['text'])
|
||||||
|
|
||||||
|
if comment['parent']:
|
||||||
|
ET.SubElement(entry, 'thr:in-reply-to', {
|
||||||
|
'ref': 'tag:{hostname},2018:/isso/{tid}/{id}'.format(
|
||||||
|
hostname=hostname,
|
||||||
|
tid=comment['tid'],
|
||||||
|
id=comment['parent']),
|
||||||
|
'href': '{base}{uri}#isso-{id}'.format(
|
||||||
|
base=base,
|
||||||
|
uri=uri, id=comment['parent'])
|
||||||
|
})
|
||||||
|
|
||||||
|
# Updated is mandatory. If we have comments, we use the date
|
||||||
|
# of last modification of the first one (which is the last
|
||||||
|
# one). Otherwise, we use a fixed date.
|
||||||
|
updated = ET.Element('updated')
|
||||||
|
if comment0 is None:
|
||||||
|
updated.text = '1970-01-01T01:00:00Z'
|
||||||
|
else:
|
||||||
|
updated.text = datetime.fromtimestamp(
|
||||||
|
comment0['modified'] or comment0['created']).isoformat()
|
||||||
|
updated.text += 'Z'
|
||||||
|
feed.insert(0, updated)
|
||||||
|
|
||||||
|
output = StringIO()
|
||||||
|
ET.ElementTree(feed).write(output,
|
||||||
|
encoding='utf-8',
|
||||||
|
xml_declaration=True)
|
||||||
|
response = XML(output.getvalue(), 200)
|
||||||
|
|
||||||
|
# Add an etag/last-modified value for caching purpose
|
||||||
|
if comment0 is None:
|
||||||
|
response.set_etag('empty')
|
||||||
|
response.last_modified = 0
|
||||||
|
else:
|
||||||
|
response.set_etag('{tid}-{id}'.format(**comment0))
|
||||||
|
response.last_modified = comment0['modified'] or comment0['created']
|
||||||
|
return response.make_conditional(request)
|
||||||
|
|
||||||
def preview(self, environment, request):
|
def preview(self, environment, request):
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
|
|
||||||
@ -904,14 +1051,19 @@ class API(object):
|
|||||||
return JSON({'text': self.isso.render(data["text"])}, 200)
|
return JSON({'text': self.isso.render(data["text"])}, 200)
|
||||||
|
|
||||||
def demo(self, env, req):
|
def demo(self, env, req):
|
||||||
return redirect(get_current_url(env) + '/index.html')
|
return redirect(
|
||||||
|
get_current_url(env, strip_querystring=True) + '/index.html'
|
||||||
|
)
|
||||||
|
|
||||||
def login(self, env, req):
|
def login(self, env, req):
|
||||||
data = req.form
|
data = req.form
|
||||||
password = self.isso.conf.get("general", "admin_password")
|
password = self.isso.conf.get("general", "admin_password")
|
||||||
if data['password'] and data['password'] == password:
|
if data['password'] and data['password'] == password:
|
||||||
response = redirect(get_current_url(
|
response = redirect(re.sub(
|
||||||
env, host_only=True) + '/admin')
|
r'/login$',
|
||||||
|
'/admin',
|
||||||
|
get_current_url(env, strip_querystring=True)
|
||||||
|
))
|
||||||
cookie = functools.partial(dump_cookie,
|
cookie = functools.partial(dump_cookie,
|
||||||
value=self.isso.sign({"logged": True}),
|
value=self.isso.sign({"logged": True}),
|
||||||
expires=datetime.now() + timedelta(1))
|
expires=datetime.now() + timedelta(1))
|
||||||
|
@ -30,7 +30,7 @@ def host(environ): # pragma: no cover
|
|||||||
of http://www.python.org/dev/peps/pep-0333/#url-reconstruction
|
of http://www.python.org/dev/peps/pep-0333/#url-reconstruction
|
||||||
"""
|
"""
|
||||||
|
|
||||||
url = environ['wsgi.url_scheme']+'://'
|
url = environ['wsgi.url_scheme'] + '://'
|
||||||
|
|
||||||
if environ.get('HTTP_HOST'):
|
if environ.get('HTTP_HOST'):
|
||||||
url += environ['HTTP_HOST']
|
url += environ['HTTP_HOST']
|
||||||
@ -84,6 +84,8 @@ def origin(hosts):
|
|||||||
hosts = [urlsplit(h) for h in hosts]
|
hosts = [urlsplit(h) for h in hosts]
|
||||||
|
|
||||||
def func(environ):
|
def func(environ):
|
||||||
|
if 'ISSO_CORS_ORIGIN' in environ:
|
||||||
|
return environ['ISSO_CORS_ORIGIN']
|
||||||
|
|
||||||
if not hosts:
|
if not hosts:
|
||||||
return "http://invalid.local"
|
return "http://invalid.local"
|
||||||
|
5
setup.py
5
setup.py
@ -5,8 +5,8 @@ import sys
|
|||||||
|
|
||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
requires = ['html5lib==0.9999999', 'itsdangerous', 'Jinja2',
|
requires = ['itsdangerous', 'Jinja2', 'misaka>=2.0,<3.0', 'html5lib<0.9999999',
|
||||||
'misaka>=1.0,<2.0', 'werkzeug>=0.9']
|
'werkzeug>=0.9']
|
||||||
|
|
||||||
if sys.version_info < (2, 7):
|
if sys.version_info < (2, 7):
|
||||||
raise SystemExit("Python 2 versions < 2.7 are not supported.")
|
raise SystemExit("Python 2 versions < 2.7 are not supported.")
|
||||||
@ -39,6 +39,7 @@ setup(
|
|||||||
extras_require={
|
extras_require={
|
||||||
':python_version=="2.7"': ['ipaddr>=2.1', 'configparser']
|
':python_version=="2.7"': ['ipaddr>=2.1', 'configparser']
|
||||||
},
|
},
|
||||||
|
setup_requires=["cffi>=1.3.0"],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts':
|
'console_scripts':
|
||||||
['isso = isso:main'],
|
['isso = isso:main'],
|
||||||
|
@ -46,6 +46,14 @@ notify = stdout
|
|||||||
# Log console messages to file instead of standard output.
|
# Log console messages to file instead of standard output.
|
||||||
log-file =
|
log-file =
|
||||||
|
|
||||||
|
# adds property "gravatar_image" to json response when true
|
||||||
|
# will automatically build md5 hash by email and use "gravatar_url" to build
|
||||||
|
# the url to the gravatar image
|
||||||
|
gravatar = false
|
||||||
|
|
||||||
|
# default url for gravatar. {} is where the hash will be placed
|
||||||
|
gravatar-url = https://www.gravatar.com/avatar/{}?d=identicon
|
||||||
|
|
||||||
# Admin access password
|
# Admin access password
|
||||||
admin_password = please_choose_a_strong_password
|
admin_password = please_choose_a_strong_password
|
||||||
|
|
||||||
@ -180,3 +188,15 @@ salt = Eech7co8Ohloopo9Ol6baimi
|
|||||||
# strengthening. Arguments have to be in that order, but can be reduced to
|
# strengthening. Arguments have to be in that order, but can be reduced to
|
||||||
# pbkdf2:4096 for example to override the iterations only.
|
# pbkdf2:4096 for example to override the iterations only.
|
||||||
algorithm = pbkdf2
|
algorithm = pbkdf2
|
||||||
|
|
||||||
|
|
||||||
|
[rss]
|
||||||
|
# Provide an Atom feed for each comment thread for users to subscribe to.
|
||||||
|
|
||||||
|
# The base URL of pages is needed to build the Atom feed. By appending
|
||||||
|
# the URI, we should get the complete URL to use to access the page
|
||||||
|
# with the comments. When empty, Atom feeds are disabled.
|
||||||
|
base =
|
||||||
|
|
||||||
|
# Limit the number of elements to return for each thread.
|
||||||
|
limit = 100
|
||||||
|
Loading…
Reference in New Issue
Block a user