Compare commits

..

7 Commits

Author SHA1 Message Date
Martin Zimmermann 95e8bcf256 Back to development: 0.2.4
11 years ago
Martin Zimmermann d9ff690057 Preparing release 0.2.3
11 years ago
Martin Zimmermann c5af859286 detect Isso API endpoint when using *.dev.js script
11 years ago
Martin Zimmermann 6924d480d5 include uncompressed JS files for debugging purposes
11 years ago
Martin Zimmermann 9721b781b9 Back to development: 0.2.3
11 years ago
Martin Zimmermann 2fbc3623e8 Preparing release 0.2.2
11 years ago
Martin Zimmermann 26544b69be fix empty thread title due premature HTTP connection closing
11 years ago

@ -1,47 +0,0 @@
---
kind: pipeline
name: default
platform:
os: linux
arch: amd64
steps:
- name: publish
pull: default
image: plugins/docker:18.09
settings:
registry: https://registry.nixaid.com
repo: "registry.nixaid.com/${DRONE_REPO_NAMESPACE}/${DRONE_REPO_NAME}"
tags:
- latest
username:
from_secret: docker_username
password:
from_secret: docker_password
# storage_path: /drone/docker
# storage_driver: aufs
# ipv6: false
# debug: true
when:
branch:
- master
event:
- push
- tag
- name: notify
pull: default
image: drillster/drone-email:latest
settings:
from: "Drone CI <noreply@nixaid.com>"
host: mx.nixaid.com
port: 587
subject: "NIXAID Drone Pipeline {{#success build.status}}SUCCESS{{else}}FAILURE{{/success}} Notification"
when:
event:
- push
- tag
status:
- success
- failure

@ -1,85 +0,0 @@
# workspace:
# base: /workspace
# path: src/git.nixaid.com/arno/myapp/
#
# branches:
# - master
pipeline:
restore_cache:
image: drillster/drone-volume-cache:latest
restore: true
mount:
- /drone/docker
# Set the ``DRONE_VOLUME=/tmp/drone-cache:/cache`` drone-server variable,
# so you can benefit from the caching.
# Otherwise you will have to make this repository trusted in Drone and use
# the volumes as follows.
# volumes:
# - /tmp/drone-cache:/cache
# drone repo add arno/isso
# drone secret add/update --name docker_username --value arno --event push --event tag --event deployment arno/isso
# drone secret add/update --name docker_password --value "$(pass show vps/registry.nixaid.com | head -1)" --event push --event tag --event deployment arno/isso
publish:
image: plugins/docker:17.12
# repo: andrey01/${DRONE_REPO_NAME}
registry: registry.nixaid.com
repo: registry.nixaid.com/arno/${DRONE_REPO_NAME}
tags:
- latest
# - ${DRONE_COMMIT_SHA:0:7}
# group: docker
# dockerfile: Dockerfile
secrets: [docker_username, docker_password]
# Since we restore the docker image cache to /drone/docker
storage_path: /drone/docker
use_cache: true
when:
event: [push, tag]
branch: master
rebuild_cache:
image: drillster/drone-volume-cache:latest
rebuild: true
mount:
- /drone/docker
# Set the ``DRONE_VOLUME=/tmp/drone-cache:/cache`` drone-server variable,
# so you can benefit from the caching.
# Otherwise you will have to make this repository trusted in Drone and use
# the volumes as follows.
# volumes:
# - /tmp/drone-cache:/cache
# # ca_cert comes from /srv/data/registry/certs/ca.crt
# claircheck:
# # image: jmccann/drone-clair:1
# image: andrey01/drone-clair
# url: http://clair:6060
# secrets: [ docker_username, docker_password ]
# # ignore errors for now. This will work only in drone 0.9 https://github.com/drone/drone-runtime/commit/3e8bd99f60f4032226523320cd2b2321f9525159
# err_ignore: true
# scan_image: registry.nixaid.com/arno/${DRONE_REPO_NAME}:latest
# ca_cert: |
# -----BEGIN CERTIFICATE-----
# MIIBOjCB4KADAgECAgkAzhpbLWXa4H0wCgYIKoZIzj0EAwIwEDEOMAwGA1UEAwwF
# bXktQ0EwHhcNMTgwNzA5MjIzMTAzWhcNMjgwNzA2MjIzMTAzWjAQMQ4wDAYDVQQD
# DAVteS1DQTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABFIE8bTfQ76U5qG/Xgjw
# BbQU0oRJLYlRxBIWF9MTNSJr2LoaoyrU8jrcWQGRrfKPoVuwUJWp2tp5SJy0AHH7
# 4fijIzAhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgKkMAoGCCqGSM49
# BAMCA0kAMEYCIQCYbTbxRD2yX4LzGjh84fKPWPQM9ps8RE2nfwZjqdRUGgIhAOHb
# USigh6FzqEPk2jiaV3t1wNtChRWRfupTKG6CD345
# -----END CERTIFICATE-----
notify:
image: drillster/drone-email:latest
from: Drone CI <noreply@nixaid.com>
subject: NIXAID Drone Pipeline {{#success build.status}}SUCCESS{{else}}FAILURE{{/success}} Notification
host: mx.nixaid.com
port: 587
# username: arno
# secrets: [ email_username, email_password ]
# recipients: [ andrey.arapov@nixaid.com ]
when:
status: [success, failure] # changed
event: [push, tag]

104
.gitignore vendored

@ -3,18 +3,13 @@
# For a project mostly in C, the following would be a good set of
# exclude patterns (uncomment them if you want to use them):
# *.[oa]
*~
# *~
*.pyc
.Python
.sass-cache
.vagrant
/bin
/include
/lib
/lib64
/man
/share
/isso.egg-info/
/isso/js/components
@ -22,100 +17,3 @@
/isso/js/embed.dev.js
/isso/js/count.min.js
/isso/js/count.dev.js
/docs/_build
/docs/_static/css/site.css
/pip-selfcheck.json
# github/gitignore
Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*,cover
.hypothesis/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# IPython Notebook
.ipynb_checkpoints
# pyenv
.python-version
# celery beat schedule file
celerybeat-schedule
# dotenv
.env
# virtualenv
.venv/
venv/
ENV/
# Spyder project settings
.spyderproject
# Rope project settings
.ropeproject

@ -1,25 +0,0 @@
language: python
matrix:
include:
- python: 3.5
env: TOX_ENV=py35
- python: 3.6
env: TOX_ENV=py36
- python: 3.7
dist: xenial
env: TOX_ENV=py37
- python: 3.8
dist: xenial
env: TOX_ENV=py38
install:
- pip install -U pip
- pip install flake8 tox
script:
- tox -e $TOX_ENV
- make flakes
notifications:
irc:
channels:
- "chat.freenode.net#isso"
on_success: change
on_failure: always

@ -1,6 +0,0 @@
[main]
host = https://www.transifex.com
[isso.js]
file_filter = .tx/js/<lang>
source_lang = en
type = KEYVALUEJSON

@ -1,563 +1,22 @@
Changelog for Isso
==================
0.12.3 (UNRELEASED)
-------------------
- New "flags" option in the [markdown] section to customize Misaka's Markdown
HTML rendering. By default, no flags are set.
[markup]
flags = skip-html, escape, hard-wrap
Check docs/configuration/server.rst for more details.
0.12.2 (2019-01-21)
-------------------
- Revert use of labels instead of placeholders, since it breaks
mail notifications. #524
0.12.1 (2019-01-19)
-------------------
- Revert fix for duplicate slashes, as it prevents isso from
starting in some cases. #523
0.12.0 (2019-01-18)
-------------------
- Fix compatibility with new XML API.
- Don't enable admin interface with default password by default. #491
- Add support and documentation for "generic" imports.
- Remove potential duplicate slashes in URLs from
email links. #420
- Add data-isso-reply-notifications to attributes in configuration.
- Use default IP in imports if none is found. Fixes imports of some comments.
- embed: fix feed link creation on older browsers.
- Properly handle to field in mail notifications when using uWSGI spooler
- css: fix vertical alignment of notification checkbox
0.11.1 (2018-11-03)
-------------------
- Include pre-built minified JavaScript and CSS.
0.11.0 (2018-11-03)
-------------------
Bugs & features:
- Fix link in moderation mails if isso is setup on a sub-url (e.g. domain.tld/comments/)
- Add reply notifications
- Add admin interface
- Add links highlighting in comments
- Add apidoc
- Add rc.d script for FreeBSD
- 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
Translations:
- Fix Chinese translation & typo in CJK
- Add Danish translation
- Add Hungarian translation
- Add Persian translation
- Improvement on german translation
0.10.6 (2016-09-22)
-------------------
- fix missing configuration field
0.10.5 (2016-09-20)
-------------------
- add support for different vote levels, #260
List of vote levels used to customize comment appearance based on score.
Provide a comma-separated values (eg. `"0,5,10,25,100"`) or a JSON array (eg.
`"[-5,5,15]"`).
For example, the value `"-5,5"` will cause each `isso-comment` to be given
one of these 3 classes:
- `isso-vote-level-0` for scores lower than `-5`
- `isso-vote-level-1` for scores between `-5` and `4`
- `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).
- add new post preview API endpoint, #254
- add an option for mandatory author, #257
- clients can now use `data-title` to get the HTML title for a new page, #252
- add finish translation and other minor bugfixes
0.10.4 (2016-04-12)
-------------------
- fix wrapper attribute when using data-isso-require-mail="true", #238
- fix reponse for OPTIONS response on Python 3, #242
0.10.3 (2016-02-24)
-------------------
- follow redirects, #193
0.10.2 (2016-02-21)
-------------------
- fix getAttribute return value
0.10.1 (2016-02-06)
-------------------
- fix empty author, email and website values when writing a comment
0.10 (2016-02-06)
-----------------
- add new configuration section for hash handling.
[hash]
salt = Eech7co8Ohloopo9Ol6baimi
algorithm = pbkdf2
You can customize the salt, choose different hash functions and tweak the
parameters for PBKDF2.
- Python 3.4+ validate TLS connections against the system's CA. Previously no
validation was in place, see PEP-446__ for details.
- add `fenced_code` and `no_intra_emphasis` to default configuration.
Fenced code allows to write code without indentation using `~~~` delimiters
(optionally with language identifier).
Intra emphasis would compile `foo_bar_baz` to foo<em>bar</em>baz. This
behavior is very confusing for users not knowing the Markdown spec in detail.
- new configuration to require an email when submitting comments, #199. Set
[guard]
require-email = true
and use `data-isso-require-email="true"` to enable this feature. Disabled by
default.
- new Bulgarian translation by sahwar, new Swedish translation by Gustav
Näslund #143, new Vietnamese translation by Đinh Xuân Sâm, new Croatian
translation by streger, new Czech translation by Jan Chren
- fix SMTP setup without credentials, #174
- version pin Misaka to 1.x, html5lib to 0.9999999
.. __: https://www.python.org/dev/peps/pep-0466/
0.9.10 (2015-04-11)
-------------------
- fix regression in SMTP authentication, #174
0.9.9 (2015-03-04)
0.2.4 (unreleased)
------------------
- several Python 3.x related bugfixes
- don't lose comment form if the server rejected the POST request, #144
- Nothing changed yet.
- add localStorage fallback if QUOTA_EXCEEDED_ERR is thrown (e.g. Safari
private browsing)
- add '--empty-id' flag to Disqus import, because Disqus' export sucks
- (re)gain compatibility with Werkzeug 0.8 and really old html5lib versions
available in Debian Squeeze, #170 & #168
- add User-Agent when Isso requests the URL, an alternate way to #151 (add
'X-Isso' when requesting).
0.9.8 (2014-10-08)
------------------
- add compatibility with configparser==3.5.0b1, #128
0.9.7 (2014-09-25)
0.2.3 (2013-10-31)
------------------
- fix SMTP authentication using CRAM-MD5 (incorrect usage of
`smtplib`), #126
- Nothing changed yet.
0.9.6 (2014-08-18)
0.2.2 (2013-10-31)
------------------
- remember name, email and website in localStorage, #119
- add option to hide voting feature, #115
data-isso-vote="true|false"
- remove email field from JSON responses
This is a quite serious issue. For the identicon, an expensive hash is used
to avoid the leakage of personal information like a real email address. A
`git blame` reveals, the email has been unintenionally exposed since the very
first release of Isso :-/
The testsuite now contains a dedicated test to prevent this error in the
future.
0.9.5 (2014-08-10)
------------------
- prevent no-break space (&nbsp;) insertion to enable manual line breaks using
two trailing spaces (as per Markdown convention), #112
- limit request size to 256 kb, #107
Previously unlimited or limited by proxy server). 256 kb is a rough
approximation of the next database schema with comments limited to 65535
characters and additional fields.
- add support for logging to file, #103
[general]
log-file =
- show timestamp when hovering <time>, #104
- fix a regression when editing comments with multiple paragraphs introduced
in 0.9.3 which would HTML escape manually inserted linebreaks.
0.9.4 (2014-07-09)
------------------
- fixed a regression when using Isso and Gevent
0.9.3 (2014-07-09)
------------------
- remove scrollIntoView while expanding further comments if a fragment is used
(e.g. #isso-thread brought you back to the top, unexpectedly)
- implement a custom Markdown renderer to support multi-line code listings. The
extension "fenced_code" is now enabled by default and generates HTML
compatible with Highlight.js__.
- escape HTML entities when editing a comment with raw HTML
- fix CSS for input
- remove isso.css from binary distribution to avoid confusion (it's still there
from the very first release, but modifications do not work)
.. __: http://highlightjs.org/
0.9 (2014-05-29)
----------------
- comment pagination by Srijan Choudhary, #15
Isso can now limit the amount of comments shown by default and add link to
show more. By default, all top-level comments are shown but only 5 nested
comments (per reply). You can override the settings:
isso-data-max-comments-top="N"
isso-data-max-comments-nested="N"
Where N is a number from 0 to infinity ("inf"). If you limit the amount of
shown top level comments, the overall comment count may be incorrect and a
known issue.
You can also configure the amount of comments shown per click (5 by default):
isso-data-reveal-on-click="N"
This feature also required a change in the comment structure. Previously, all
comments are stored tree-like but shown linearly. To ease the implementation
of pagination, the comment tree is now limited to a maximum depth of one.
Jeff Atwood explains, why `discussions are flat by design`__.
.. __: http://blog.codinghorror.com/web-discussions-flat-by-design/
When you upgrade, Isso will automatically normalize the tree and some
information gets lost. All new replies to a comment are now automatically a
direct child of the top-level comment.
- style improvements by William Dorffer, #39, #84 #90 and #91
Isso now longer uses a fat SCSS library, but plain CSS instead. The design is
now responsive and no longer sets global CSS rules.
- experimental WordPress import, #75
Isso should be able to import WXR 1.0-1.2 exports. The import code is based
on two WXR dumps I found (and created) and may not work for you. Please
report any failure.
- avatar changes, #49
You can now configure the client to not show avatars:
data-isso-avatar="false"
Also there is no longer an avatar shown next to the comment box. This is due
to the new CSS and removes two runtime dependencies.
- you may now set a full From header, #87
[smtp]
from = Foo Bar <spam@local>
- SMTP (all caps) is now recognized for notifications, #95
- Isso now ships a small demo site at /demo, #44
- a few bugfixes: Disqus import now anonymizes IP addresses, uWSGI spooling for
Python 3, HTTP-Referer fallback for HTTP-Origin
- remove Django's PBKDF2 implementation in favour of the PBKDF2 function
available in werkzeug 0.9 or higher. If you're still using werkzeug 0.8, Isso
imports passlib__ as fallback (if available).
This release also features a new templating engine Jade__ which replaces
Markup.js__. Jade can compile directly to JavaScript with a tiny runtime module
on the client. Along with the removal of sha1.js and pbkdf2.js and a few build
optimizations, the JS client now weighs only 40kb (12kb gzipped) 52kb resp.
17kb before.
.. __: https://pypi.python.org/pypi/passlib
.. __: http://jade-lang.com/
.. __: https://github.com/adammark/Markup.js
0.8 (2014-03-28)
----------------
- replace ``<textarea>`` with ``<div contentedtiable="true">`` to remove the
sluggish auto-resize on input feature. If you use a custom CSS, replace
``textarea`` with ``.textarea`` and also set ``white-space: pre``.
- remove superscript extension from Markdown defaults as it may lead to
unexpected behavior for certain smileys such as "^^". To enable the extension,
add
[markup]
options = superscript
allowed-elements = sup
to your configuration.
- comment count requests are now bundled into a single POST request, but the old
API is still there (deprecated though).
- store *session-key* in database (once generated on database creation). That
means links to activate, edit or delete comments are now always valid even
when you restart Isso.
Currently statically set session keys in ``[general]`` are automatically
migrated into the database on startup and you will get a notice that you can
remove this option.
- fix undefined timestamp when client time differs for more than 1 second.
The human-readable "time ago" deltas have been refined to match `Moment.js`_
behavior.
- avatar colors and background can now be customized:
* ``data-isso-avatar-bg="#f0f0f0"`` sets the background color
* ``data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."`` sets possible
avatar colors (up to 8 colors are possible).
- new [markup] section to customize Misaka's Markdown generation (strikethrough,
superscript and autolink enabled by default). Furthermore, you can now allow
certain HTML elemenets and attributes in the generated output, e.g. to enable
images, set
[markup]
allowed-elements = img
allowed-attributes = src
Check docs/configuration/server.rst for more details.
- replace requirejs-domready with a (self-made) HTML5 idiom, #51
.. _Moment.js: http://momentjs.com/docs/#/displaying/fromnow/
0.7 (2014-01-29)
----------------
- fix malicious HTML injection (due to wrong API usage). All unknown/unsafe
HTML tags are now removed from the output (`html5lib` 0.99(9) or later) or
properly escaped (older `html5lib` versions).
See 36d702c and 3a1f92b for more details.
- remove kriskowal/q JS library (promises implementation) in favour of a
self-made 50 LoC implementation to ease packaging (for Debian), #51
- add documentation to display a comment counter, #56 and #57
- SMTP notifications now support STARTTLS and use this transport security
by default, #48 and #58. This also changes the configuration option from
`ssl = [yes|no]` to `security = [none|starttls|ssl]`.
- translation can now be made (and updated) with Transifex_. If you want to
take ownership for a language, contact me on IRC.
- fix french pluralform
- the (by default random) session-key is now shown on application startup
to make different keys per startup more visible
- use `threading.lock` by default for systems without semaphore support
.. _Transifex: https://www.transifex.com/projects/p/isso/
0.6 (2013-12-16)
----------------
Major improvements:
- override thread discovery with data-isso-id="...", #27
To use the same thread for different URLs, you can now add a custom
``data-isso-id="my-id"`` attribute which is used to identify and retrieve
comments (defaults to current URL aka `window.location.pathname`).
- `isso.dispatch` now dispatches multiple websites (= configurations) based on
URL prefixes
- fix a cross-site request forgery vulnerability for comment creation, voting,
editing and deletion, #40
- show modal dialog to confirm comment deletion and activation, #36
- new, comprehensive documentation based on reST + Sphinx:
http://posativ.org/docs (or docs/ in the repository). Also includes an
annotated `example.conf`, #43
- new italian and russian translations
Minor improvements:
- move `isso:application` to `isso.run:application` to avoid uneccessary
initialization in some cases (change module if you use uWSGI or Gunicorn)
- add Date header to email notifications, #42
- check for blank text in new comment, #41
- work around IE10's HTML5 abilities for custom data-attributes
- add support for Gunicorn (and other pre-forking WSGI servers)
0.5 (2013-11-17)
----------------
Major improvements:
- `listen` option replaces `host` and `port` to support UNIX domain sockets, #25
Instead of `host = localhost` and `port = 8080`, use
`listen = http://localhost:8080`. To listen on a UNIX domain socket, replace
`http://` with `unix://`, e.g. `unix:///tmp/isso.sock`.
- new option `notify` (in the general section) is used to choose (one or more)
notification backends (currently only SMTP is available, though). Isso will
no longer automatically use SMTP for notifications if the initial connection
succeeds.
- new options to control the client integration
* ``data-isso-css="false"`` prevents the client from appending the CSS to the
document. Enabled by default.
* ``data-isso-lang="de"`` overrides the useragent's preferred language (de, en
and fr are currently supported).
* ``data-isso-reply-to-self="true"`` should be set, when you allow reply to
own comments (see server configuration for details).
- add support for `gevent <http://www.gevent.org/>`_, a coroutine-based Python
networking library that uses greenlets (lightweight threads). Recommended
WSGI server when not running with uWSGI (unfortunately stable gevent is not
yet able to listen on a UNIX domain socket).
- fix a serious issue with the voters bloomfilter. During an Isso run, the
ip addresses from all commenters accumulated into the voters bloomfilter
for new comments. Thus, previous commenters could no longer vote other
comments. This fixes the rare occurences of #5.
In addition to this fix, the current voters bloomfilter will be re-initialized
if you are using Isso 0.4 or below (this is not necessary, but on the
other hand, the current bloomfilter for each comment is sort-of useless).
- french translation (thanks to @sploinga), #38
- support for multiple sites, part of #34
Minor improvements:
- `ipaddr` is now used as `ipaddress` fallback for Python 2.6 and 2.7, #32
- changed URL to activate and delete comments to `/id/<N:int>/activate` etc.
- import command uses `<link>` tag instead of `<id>` to extract the relative
URL path, #37
- import command now uses `isDeleted` to mark comments as deleted (and
eventually remove stale comments). This seems to affect only a few comments
from a previous WordPress import into Disqus.
- import command lists orphaned comments after import.
- import command now has a ``--dry-run`` option to do no actual operation on
the database.
0.4 (2013-11-05)
----------------
- Isso now handles cross-domain requests and cookies, fixes #24
- Isso for Python 2.x now supports werkzeug>=0.8
- limit email length to 254 to avoid Hash-DDoS
- override Isso API location with ``data-isso="..."`` in the script tag
- override HTML title parsing with a custom ``data-title="..."`` attribute
in ``<div id="isso-thread"></div>``
0.3 (2013-11-01)
----------------
- improve initial comment loading performance in the client
- cache slow REST requests, see #18
- add a SQLite trigger that detects and removes stale threads (= threads,
with all comments being removed) from the database when a comment is
removed.
- PyPi releases now include an uncompressed version of the JavaScript
files -- `embed.dev.js` and `count.dev.js` -- to track down errors.
- use uWSGI's internal locking instead of a self-made shared memory lock
- Nothing changed yet.
0.2 (2013-10-29)

@ -0,0 +1,89 @@
Contributing
============
I appreciate any help and love pull requests. Here are some things
you need to respect:
* no hard-wired external services
* no support for ancient browsers (e.g. IE6-9)
Reporting Issues
----------------
### Disqus import fails
If `isso import /path/to/disqus.xml` fails, please do *NOT* attach the raw
dump file to GH:Issues. Please anonymize all IP addresses inside the `<ipAddress>`
tag first, as second step, replace all email addresses with a generic one (
email tag).
### embed.min.js-related issues
In case of a JavaScript traceback, please setup Isso in development mode
described below. Otherwise it is very hard to "guess" the reason.
### isso-related issues
Copy and paste traceback into a ticket and provide some details of your usage.
Development
-----------
If you want to hack on Isso or track down issues, there's an alternate
way to set up Isso. It requires a lot more dependencies and effort.
Requirements:
- Python 2.6, 2.7 or 3.3
- Ruby 1.8 or higher
- Node.js, [NPM](https://npmjs.org/) and [Bower](http://bower.io/)
On Debian/Ubuntu install the following packages
~> sudo aptitude install python-setuptools python-dev npm ruby
~> ln -s /usr/bin/nodejs /usr/bin/node
Get the repository:
~> git clone https://github.com/posativ/isso.git
~> cd isso/
Install `virtualenv` and create a new environment for Isso (recommended):
~> pip install virtualenv
~> virtualenv .
~> source ./bin/activate
Install Isso dependencies:
~> python setup.py develop
~> isso run
Compile SCSS to CSS:
~> gem install sass
~> scss --watch isso/css/isso.scss
Install JS components:
~> cd isso/js
~> bower install almond q requirejs requirejs-domready requirejs-text
Integration
-----------
```html
<script src="/isso/js/config.js"></script>
<script data-main="/isso/js/embed" src="/isso/js/components/requirejs/require.js"></script>
```
Optimization
------------
~> npm install -g requirejs uglifyjs
~> cd isso/js
~> r.js -o build.embed.js
~> r.js -o build.count.js

@ -1 +0,0 @@
docs/contribute.rst

@ -1,121 +0,0 @@
# Contributions to Isso
## Creator & Maintainer
* Martin Zimmermann <info@posativ.org>
## Co-Maintainers
* Benoît Latinier @blatinier <benoit@latinier.fr>
* Jelmer Vernooij <jelmer@jelmer.uk>
## Contributors
In chronological order:
* Florian Baumann <flo@noqqe.de>
* Documentation fixes in the early days
* Federico Ceratto <federico.ceratto@gmail.com>
* Support for ipaddr
* Added a sample configuration file
* Sploinga <xavier@sploing.be>
* French translation
* Laurent Arnoud <laurent@spkdev.net>
* Make Isso not accept blank comments
* Chimo <chimo@chromic.org>
* STARTTLS for SMTP notifications
* Jocelyn Delande <jocelyn at delalande dot fr>
* Documentation for comment counter and API
* Default SMTP security to STARTTLS
* Srijan Choudhary <srijan4@gmail.com>
* Correct nginx examples in documentation
* Added comment pagination feature
* Provide a demo site
* William Dorffer <schoewilliam@gmail.com>
* Convert SCSS to plain CSS
* Refresh style, make it responsive
* Baptiste Darthenay
* Esperanto translation
* Matías Ducasa <https://github.com/matiasducasa>
* Spanish translation
* Daniel Gräber <https://github.com/albohlabs>
* Added ansible for provisioning
* Nick Hu <https://github.com/NickHu>
* Added configuration to require email addresses (no validation)
* Fix Vagrantfile
* Benoît Latinier @blatinier <benoit@latinier.fr>
* Fix thread discovery
* Added mandatory author
* Added admin interface
* Ivan Pantic <ivanpantic82@gmail.com>
* 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>
* Fixing likes counter of replies not being displayed
* Adding contrib/dump_comments.py
* Adding a [server] proxy-fix-enable-x-prefix configuration option
* Using .access_route instead of .remote_addr to take into account HTTP_X_FORWARDED_FOR header
* 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
* Facundo Batista <facundo@taniquetil.com.ar>
* Added a generic way to migrate from a json file
* @benjhess
* Optionnal gravatar support
* Steffen Prince @sprin
* Upgrade to Misaka 2
* Rocka <i@rocka.me>
* Implementation and documentation about async comment loading
* Pelle Nilsson @pellenilsson
* Reply notifications
* Craig P Hicks @craigphicks
* Fix sub urls configurations on admin interface
* Chris Warrick @Kwpolska
* Update Polish translation
* Redirect to comment after moderation
* [Your name or handle] <[email or website]>
* [Brief summary of your changes]

@ -1,34 +0,0 @@
# First, compile JS stuff
FROM node:dubnium-buster
WORKDIR /src/
COPY . .
RUN npm install -g requirejs uglify-js jade bower \
&& make init js
# Second, create virtualenv
FROM python:3.8-buster
WORKDIR /src/
COPY --from=0 /src .
RUN python3 -m venv /isso \
&& . /isso/bin/activate \
&& pip3 install --no-cache-dir --upgrade pip \
&& pip3 install --no-cache-dir gunicorn cffi flask \
&& python setup.py install \
&& rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Third, create final repository
FROM python:3.8-slim-buster
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", "--worker-tmp-dir", "/dev/shm"]
# 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

@ -1,19 +1,7 @@
include man/man1/isso.1
include man/man5/isso.conf.5
include share/isso.conf
include isso/js/embed.min.js
include isso/js/embed.dev.js
include isso/js/count.min.js
include isso/js/count.dev.js
include isso/js/admin.js
include isso/defaults.ini
include isso/templates/admin.html
include isso/templates/disabled.html
include isso/templates/login.html
include isso/css/admin.css
include isso/css/isso.css
include isso/img/isso.svg
include isso/static/post.html

@ -1,75 +1,10 @@
# INSTALLATION: pip install sphinx && npm install --global node-sass
all: css js
ISSO_JS_SRC := $(shell find isso/js/app -type f) \
$(shell ls isso/js/*.js | grep -vE "(min|dev)") \
isso/js/lib/requirejs-jade/jade.js
ISSO_JS_DST := isso/js/embed.min.js isso/js/embed.dev.js \
isso/js/count.min.js isso/js/count.dev.js
ISSO_CSS := isso/css/isso.css
ISSO_PY_SRC := $(shell git ls-files | grep -E "^isso/.+.py$$")
DOCS_RST_SRC := $(shell find docs/ -type f -name '*.rst') \
$(wildcard docs/_isso/*) \
docs/index.html docs/conf.py docs/docutils.conf \
share/isso.conf
DOCS_CSS_SRC := docs/_static/css/site.scss
DOCS_CSS_DEP := $(shell find docs/_static/css/neat -type f) \
$(shell find docs/_static/css/bourbon -type f)
DOCS_CSS_DST := docs/_static/css/site.css
DOCS_MAN_DST := man/man1/isso.1 man/man5/isso.conf.5
DOCS_HTML_DST := docs/_build/html
RJS = r.js
SASS = node-sass
all: man js site
init:
(cd isso/js; bower --allow-root install almond requirejs requirejs-text jade)
flakes:
flake8 . --count --max-line-length=127 --show-source --statistics
isso/js/%.min.js: $(ISSO_JS_SRC) $(ISSO_CSS)
$(RJS) -o isso/js/build.$*.js out=$@
isso/js/%.dev.js: $(ISSO_JS_SRC) $(ISSO_CSS)
$(RJS) -o isso/js/build.$*.js optimize="none" out=$@
js: $(ISSO_JS_DST)
man: $(DOCS_RST_SRC)
sphinx-build -b man docs/ man/
mkdir -p man/man1/ man/man5
mv man/isso.1 man/man1/isso.1
mv man/isso.conf.5 man/man5/isso.conf.5
${DOCS_CSS_DST}: $(DOCS_CSS_SRC) $(DOCS_CSS_DEP)
$(SASS) --no-cache $(DOCS_CSS_SRC) $@
${DOCS_HTML_DST}: $(DOCS_RST_SRC) $(DOCS_CSS_DST)
sphinx-build -b dirhtml docs/ $@
site: $(DOCS_HTML_DST)
coverage: $(ISSO_PY_SRC)
nosetests --with-doctest --with-coverage --cover-package=isso --cover-html isso/
test: $($ISSO_PY_SRC)
python3 setup.py nosetests
clean:
rm -f $(DOCS_MAN_DST) $(DOCS_CSS_DST) $(ISSO_JS_DST)
rm -rf $(DOCS_HTML_DST)
.PHONY: clean site man init js coverage test
css:
scss --no-cache isso/css/isso.scss isso/css/isso.css
js:
r.js -o isso/js/build.embed.js
r.js -o isso/js/build.embed.js optimize="none" out="isso/js/embed.dev.js"
r.js -o isso/js/build.count.js
r.js -o isso/js/build.count.js optimize="none" out="isso/js/count.dev.js"

@ -1,12 +1,147 @@
[![Build Status](https://drone.nixaid.com/api/badges/arno/isso/status.svg)](https://drone.nixaid.com/arno/isso)
Isso Ich schrei sonst
=======================
Isso a commenting server similar to Disqus
============================================
You love static blog generators (especially [Acrylamid][1] *cough*) and the
only option to interact with the community is [Disqus][2]. There's nothing
wrong with it, but if you care about the privacy of your audience you are
better off with a comment system that is under your control. This is, where
Isso comes into play.
Isso *Ich schrei sonst* is a lightweight commenting server written in
Python and JavaScript. It aims to be a drop-in replacement for
[Disqus](http://disqus.com).
[1]: https://github.com/posativ/acrylamid
[2]: https://disqus.com/
![Isso in Action](http://posativ.org/~tmp/isso-sample.png)
**[Try Yourself!](http://posativ.org/isso/)**
See [posativ.org/isso](http://posativ.org/isso/) for more details.
Isso is not stable (pretty far from that state) and the database format may
change without any backwards compatibility. Just saying.
Features
--------
* [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) comments written in Markdown
* SQLite backend, Disqus import
* client-side JS (currently 61kb minified, 21kb gzipped)
* I18N, available in german and english (also fallback)
Roadmap
-------
- Ping/TrackBack™ support
- simple admin interface (needs contributor)
- spam filtering
Installation
------------
- Python 2.6, 2.7 or 3.3
- easy_install or pip
Install Isso (and its dependencies) with:
~> pip install isso
Before you start, you may want to import comments from
[Disqus.com](https://disqus.com/):
~> isso import ~/Downloads/user-2013-09-02T11_39_22.971478-all.xml
[100%] 53 threads, 192 comments
You start the server via (try to visit [http://localhost:8080/static/post.html]()).
~> isso run
If that is working, you may want to [edit the configuration](https://github.com/posativ/isso/blob/master/docs/CONFIGURATION.rst).
Webserver Configuration
-----------------------
This part is not fun, I know. I have prepared two possible setups for nginx,
using Isso on the same domain as the blog, and on a different domain. Each
setup has its own benefits.
### Isso on a Sub URI
Let's assume you want Isso on `/isso`, use the following nginx snippet:
```nginx
server {
listen [::]:80;
listen [::]:443 ssl;
server_name example.tld;
root /var/www/example.tld;
location /isso {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Script-Name /isso;
proxy_pass http://localhost:8080;
}
}
```
```html
<script src="http://example.tld/isso/js/embed.min.js"></script>
<div id="isso-thread"></div>
```
### Isso on a Dedicated Domain
Now, assuming you have your isso instance running on a different URL, such as
`http://example.tld:8080`, but your blog runs on `http://example.tld`:
~> cat example.cfg
[general]
host = http://example.tld/
~> isso -c example.cfg run
2013-10-30 09:32:48,369 WARNING: unable to connect to SMTP server
2013-10-30 09:32:48,408 WARNING: connected to HTTP server
2013-10-30 09:32:48,409 INFO: * Running on http://localhost:8080/
Make sure, Isso can connect to server that hosts your blog, otherwise you are
not able to post comments.
Integrate Isso with:
```html
<script src="http://example.tld:8080/js/embed.min.js"></script>
<div id="isso-thread"></div>
```
Also, put the plain isso server behind a real web server or [use uWSGI][3].
[3]: https://github.com/posativ/isso/blob/master/docs/uWSGI.md
Website Integration
-------------------
To enable comments, add a `<div id="isso-thread"></div>` or `<section id="isso-thread" ...`
below your post.
To add comment count links to your index page, include `count.min.js` at the
very bottom of your document. All links followed by `#isso-thread`, are
updated with the current comment count.
This functionality is already included when you embed `embed.min.js`, do
*not* mix `embed.min.js` and `count.min.js` in a single document.
Other Documents
---------------
- [Configuration](https://github.com/posativ/isso/blob/master/docs/CONFIGURATION.rst)
- [API overview](https://github.com/posativ/isso/raw/master/docs/API.md)
- [uWSGI usage](https://github.com/posativ/isso/blob/master/docs/uWSGI.md)
Alternatives
------------
- [talkatv](https://github.com/talkatv/talkatv) Python
- [Juvia](https://github.com/phusion/juvia) Ruby on Rails
- [Tildehash.com](http://www.tildehash.com/?article=why-im-reinventing-disqus) PHP
- [SO: Unobtrusive, self-hosted comments](http://stackoverflow.com/q/2053217)

65
Vagrantfile vendored

@ -1,65 +0,0 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# This is the Vagrant config file for setting up an environment for Isso development.
# It requires:
#
# * Vagrant (https://vagrantup.com)
# * A VM engine, like VirtualBox (https://virtualbox.org)
# * The Vagrant-Hostmanager plugin (https://github.com/smdahlen/vagrant-hostmanager)
# * Ansible (https://www.ansible.com)
#
# With them installed, cd into this directory and do 'vagrant up'. It's possible Vagrant will
# ask for your root password so it can update your /etc/hosts file. Once it's happily churning out
# console output, go get a coffee :)
#
# The resulting VM should be accessible at http://isso-dev.local/ so you can try the demo page out.
# Edit files in your checkout as usual. If you need to look at log files and stuff, 'vagrant ssh'
# to get into the VM. Useful info about it:
#
# * Running Ubuntu 14.04
# * Isso is running on uWSGI
# * Actual webserver is Nginx to talk to uWSGI over a unix socket
# * uWSGI log file is /var/log/uwsgi/apps/isso.log
# * Isso DB file is /var/isso/comments.db
# * Isso log file is /var/log/isso.log
#
# When the VM is getting rebooted vagrant mounts the shared folder after uWSGI is getting startet. To fix this issue for
# the moment you need to 'vagrant ssh' into the VM and execute 'sudo service uwsgi restart'.
#
# For debugging with _pudb_ stop uWSGI service and start it manually
# 'sudo uwsgi --ini /etc/uwsgi/apps-available/isso.ini'.
#
# Enjoy!
Vagrant.configure(2) do |config|
# The most common configuration options are documented and commented below.
# For a complete reference, please see the online documentation at
# https://docs.vagrantup.com.
config.vm.box = "ubuntu/trusty32"
config.vm.hostname = 'isso-dev.local'
config.vm.network "private_network", type: "dhcp"
config.hostmanager.enabled = true
config.hostmanager.manage_host = true
config.hostmanager.ignore_private_ip = false
config.hostmanager.include_offline = true
config.hostmanager.ip_resolver = proc do |machine|
result = ""
machine.communicate.execute("ifconfig eth1") do |type, data|
result << data if type == :stdout
end
(ip = /inet addr:(\d+\.\d+\.\d+\.\d)/.match(result)) && ip[1]
end
config.vm.provision "ansible" do |ansible|
ansible.playbook = "ansible/site.yml"
ansible.limit = "all"
ansible.verbose = "v"
end
config.vm.post_up_message = "Browse to http://isso-dev.local/demo/index.html to start."
end

@ -1,39 +0,0 @@
user root;
worker_processes 4;
worker_rlimit_nofile 8192;
error_log /var/log/nginx/error.log warn;
pid /run/nginx.pid;
events {
worker_connections 2014;
multi_accept on;
use epoll;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
log_format timed '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'$request_time $upstream_response_time $upstream_addr '
' $upstream_status $upstream_cache_status $pipe';
access_log /var/log/nginx/access.log timed;
sendfile on;
tcp_nopush on;
keepalive_timeout 30;
gzip on;
include /etc/nginx/conf.d/*.conf;
include /etc/nginx/sites-enabled/*;
}

@ -1,13 +0,0 @@
server {
client_max_body_size 20M;
listen 80 default_server;
server_name isso-dev.local;
root /vagrant/isso/demo;
location / {
# uwsgi_pass unix:///run/uwsgi/app/isso/socket;
uwsgi_pass 127.0.0.1:8080;
include uwsgi_params;
}
}

@ -1,24 +0,0 @@
[uwsgi]
plugins = python
chdir = /vagrant
uid = root
gid = root
socket = :8080
master = true
processes = 4
cache2 = name=hash,items=10240,blocksize=32
spooler = /var/isso/spool
module = isso.run
env = ISSO_SETTINGS=/vagrant/share/isso-dev.conf
env = PYTHON_EGG_CACHE=/tmp
# uncomment for debugging
# daemonize = /var/log/uwsgi/uwsgi.log
py-autoreload = 1
# prevent uWSGI from remapping stdin to /dev/null
honour-stdin = true

@ -1,85 +0,0 @@
---
- name: Provision development server
hosts: all
sudo: true
tasks:
- name: Apt | Add nodesource keys
apt_key: url=https://deb.nodesource.com/gpgkey/nodesource.gpg.key state=present
- name: Apt | Add nodesource sources list deb
apt_repository: repo='deb https://deb.nodesource.com/node {{ ansible_distribution_release }} main' state=present
- name: Apt | Add nodesource sources list deb src
apt_repository: repo='deb-src https://deb.nodesource.com/node {{ ansible_distribution_release }} main' state=present
- name: Apt | Install packages
apt: pkg={{ item }} state=latest update_cache=true
with_items:
- build-essential
- curl
- htop
- vim
- git
- python-dev
- python-software-properties
- python-setuptools
- python-pip
- nginx
- uwsgi
- uwsgi-plugin-python
- supervisor
- sqlite3
- nodejs
- libffi-dev
- name: NPM | Install packages
npm: name={{ item }} global=yes
with_items:
- bower
- requirejs
- uglify-js
- jade
- name: Python | Install egg
shell: cd /vagrant; python setup.py develop
- name: Make
shell: cd /vagrant; {{ item }}
with_items:
- make init
- make js
- name: Spool | Create directory
file: path=/var/isso/spool state=directory mode=0777
- name: uwsgi | Deploy configuration
copy: src=files/uwsgi.ini dest=/etc/uwsgi/apps-available/isso.ini
- name: uwsgi | Enable app
file: src=/etc/uwsgi/apps-available/isso.ini dest=/etc/uwsgi/apps-enabled/isso.ini state=link
- name: uwsgi | Restart service daemon
service: name=uwsgi state=restarted enabled=yes
- name: uwsgi | Chmod logfile
file: path=/var/log/uwsgi/uwsgi.log state=touch mode="a+r"
- name: nginx | Deploy nginx.conf
copy: src=files/nginx.conf dest=/etc/nginx/nginx.conf
- name: nginx | Delete default vhost
action: file path=/etc/nginx/sites-enabled/default state=absent
- name: nginx | Deploy vhost config
copy: src=files/nginx.vhost.conf dest=/etc/nginx/sites-available/isso.conf
- name: nginx | Enable vhost
file: src=/etc/nginx/sites-available/isso.conf dest=/etc/nginx/sites-enabled/000-isso state=link
- name: nginx | Chmod logfile
file: path=/var/log/nginx mode="a+rx" state=directory recurse=true
- name: nginx | Restart service daemon
service: name=nginx state=restarted enabled=yes

@ -1,11 +0,0 @@
{
"name": "isso",
"description": "a Disqus alternative",
"title": "isso API",
"order": ["Thread", "Comment"],
"template": {
"withCompare": false
}
}

@ -1,13 +0,0 @@
#!/usr/bin/env fish
set -g TRANSIFEX ".tx/js/"
set -g JS "isso/js/app/i18n/%s.js"
# fetch latest translations to .tx/<ressource>/<lang>
tx pull -a
for lang in (ls $TRANSIFEX)
printf "define(" > (printf $JS $lang)
cat .tx/js/$lang >> (printf $JS $lang)
printf ");\n" >> (printf $JS $lang)
end

@ -1,34 +0,0 @@
#!/usr/bin/env fish
set TRANSIFEX "https://www.transifex.com/api/2"
if [ (count $argv) -ne 1 ]
echo "tx-push FILE"
exit 2
end
if [ ! -f ~/.transifexrc ]
echo "no ~/.transifexrc found"
exit 1
end
set user (cat ~/.transifexrc | grep -E "^username" | awk -F " ?= ?" '{ print $2 }')
set pass (cat ~/.transifexrc | grep -E "^password" | awk -F " ?= ?" '{ print $2 }')
set lang (echo $argv | cut -d / -f 5 | cut -d . -f 1)
set trans (mktemp -t tx.XXX)
if [ $lang = "en" ]
set url "$TRANSIFEX/project/isso/resource/js/content/"
else
set url "$TRANSIFEX/project/isso/resource/js/translation/$lang/"
end
printf '{"content":' > $trans
cat $argv \
| sed "s,^define(,,g;\$ s,);,,g" \
| python -c 'import json,sys; print json.dumps(sys.stdin.read())' \
>> $trans
printf '}' >> $trans
curl -L -u $user:$pass -XPUT $url -H "Content-Type: application/json" -d @$trans

@ -1,128 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
#
# The MIT License (MIT)
#
# Copyright (c) 2020 Lucas Cimon.
#
# 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:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the 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.
"""Dump isso comments as text
The script can be run like this:
contrib/dump_comments.py .../path/to/isso.db --sort-by-last-reply
To get a list of all available options:
contrib/dump_comments.py --help
By installing the optional colorama dependency, you'll get a colored output.
An example of output can be found at https://github.com/posativ/isso/issues/634
"""
import argparse
import sqlite3
from collections import defaultdict, namedtuple
from datetime import date
from textwrap import indent
class ColorFallback():
__getattr__ = lambda self, name: '' # noqa: E731
try:
from colorama import Fore, Style, init
init() # needed for Windows
except ImportError: # fallback so that the imported classes always exist
Fore = Style = ColorFallback()
Comment = namedtuple('Comment', ('uri', 'id', 'parent', 'created', 'text', 'author', 'email', 'website', 'likes', 'dislikes', 'replies'))
INDENT = ' '
QUERY = 'SELECT uri, comments.id, parent, created, text, author, email, website, likes, dislikes FROM comments INNER JOIN threads on comments.tid = threads.id'
def main():
args = parse_args()
if not args.colors:
global Fore, Style
Fore = Style = ColorFallback()
db = sqlite3.connect(args.db_path)
comments_per_uri = defaultdict(list)
for result in db.execute(QUERY).fetchall():
comment = Comment(*result, replies=[])
comments_per_uri[comment.uri].append(comment)
root_comments_per_sort_date = {}
for comments in comments_per_uri.values():
comments_per_id = {comment.id: comment for comment in comments}
root_comments, sort_date = [], None
for comment in comments:
if comment.parent: # == this is a "reply" comment
comments_per_id[comment.parent].replies.append(comment)
if args.sort_by_last_reply and (sort_date is None or comment.created > sort_date):
sort_date = comment.created
else:
root_comments.append(comment)
if sort_date is None or comment.created > sort_date:
sort_date = comment.created
root_comments_per_sort_date[sort_date] = root_comments
for _, root_comments in sorted(root_comments_per_sort_date.items(), key=lambda pair: pair[0]):
print(Fore.MAGENTA + args.url_prefix + root_comments[0].uri + Fore.RESET)
for comment in root_comments:
print_comment(INDENT, comment)
for comment in comment.replies:
print_comment(INDENT * 2, comment)
print()
def print_comment(prefix, comment):
author = comment.author or 'Anonymous'
email = comment.email or ''
website = comment.website or ''
when = date.fromtimestamp(comment.created)
popularity = ''
if comment.likes:
popularity = '+{.likes}'.format(comment)
if comment.dislikes:
if popularity:
popularity += '/'
popularity = '-{.dislikes}'.format(comment)
print(prefix + '{Style.BRIGHT}{author}{Style.RESET_ALL} {Style.DIM}- {email} {website}{Style.RESET_ALL} {when} {Style.DIM}{popularity}{Style.RESET_ALL}'.format(Style=Style, **locals()))
print(indent(comment.text, prefix))
def parse_args():
parser = argparse.ArgumentParser(description='Dump all Isso comments in chronological order, grouped by replies',
formatter_class=ArgparseHelpFormatter)
parser.add_argument('db_path', help='File path to Isso Sqlite DB')
parser.add_argument('--sort-by-last-reply', action='store_true', help='By default comments are sorted by "parent" comment date, this sort comments based on the last replies')
parser.add_argument('--url-prefix', default='', help='Optional domain name to prefix to pages URLs')
parser.add_argument('--no-colors', action='store_false', dest='colors', default=True, help='Disabled colored output')
return parser.parse_args()
class ArgparseHelpFormatter(argparse.RawTextHelpFormatter, argparse.ArgumentDefaultsHelpFormatter):
pass
if __name__ == '__main__':
main()

@ -1,123 +0,0 @@
#!/usr/bin/env python
# -*- encoding: utf-8 -*-
"""Comment importer from Blogger
This python script can convert comments posted to a Blogger-powered blog to a
JSON file with can then be imported into Isso (by following the procedure
explained in docs/docs/extras/advanced-migration.rst.
The script can be run like this:
python import_blogger.py -p 'http://myblog.com/' blogger.xml out.json
where `blogger.xml` is a dump of the blog produced by the Blogger platform, and
the URL following the `-p` option is a prefix that will be applied to all post
URLs: the original host will be stripped and the path will be appended to the
string you specify here (this can be useful in the case that your blog moved to
a different domain, subdomain, or just into a new directory).
The `out.json` file is the file which will be generated by this tool, and which
can then be fed into isso:
isso -c /path/to/isso.cfg import -t generic out.json
"""
from __future__ import unicode_literals
import json
import feedparser
import time
from urllib.parse import urlparse
class Post:
def __init__(self, url):
self.url = url
self.title = None
self.comments = []
def add_comment(self, comment):
comment['id'] = len(self.comments) + 1
self.comments.append(comment)
def encode_post(post):
ret = {}
ret['id'] = post.url
ret['title'] = post.title
ret['comments'] = post.comments
return ret
class ImportBlogger:
TYPE_COMMENT = 'http://schemas.google.com/blogger/2008/kind#comment'
TYPE_POST = 'http://schemas.google.com/blogger/2008/kind#post'
def __init__(self, filename_in, filename_out, prefix):
self.channel = feedparser.parse(filename_in)
self.filename_out = filename_out
self.prefix = prefix
def run(self):
self.posts = {}
for item in self.channel.entries:
terms = [tag.term for tag in item.tags]
if not terms:
continue
if terms[0] == self.TYPE_COMMENT:
post = self.ensure_post(item)
post.add_comment(self.process_comment(item))
elif terms[0] == self.TYPE_POST:
self.process_post(item)
data = [encode_post(p) for p in self.posts.values() if p.comments]
with open(self.filename_out, 'w') as fp:
json.dump(data, fp, indent=2)
def process_post(self, item):
pid = self.post_id(item)
if pid in self.posts:
post = self.posts[pid]
else:
post = Post(pid)
self.posts[pid] = post
post.title = item.title
def ensure_post(self, item):
pid = self.post_id(item)
post = self.posts.get(pid, None)
if not post:
post = Post(pid)
self.posts[pid] = post
return post
def process_comment(self, item):
comment = {}
comment['author'] = item.author_detail.name
comment['email'] = item.author_detail.email
comment['website'] = item.author_detail.get('href', '')
t = time.strftime('%Y-%m-%d %H:%M:%S', item.published_parsed)
comment['created'] = t
comment['text'] = item.content[0].value
comment['remote_addr'] = '127.0.0.1'
return comment
def post_id(self, item):
u = urlparse(item.link)
return self.prefix + u.path
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(
description='Convert comments from blogger.com')
parser.add_argument('input', help='input file')
parser.add_argument('output', help='output file')
parser.add_argument('-p', dest='prefix',
help='prefix to be added to paths (ID)',
type=str, default='')
args = parser.parse_args()
importer = ImportBlogger(args.input, args.output, args.prefix)
importer.run()

@ -0,0 +1,100 @@
Isso API
========
The Isso API uses HTTP and JSON as primary communication protocol.
## JSON format
When querying the API you either get an error, an object or list of objects
representing the comment. Here's a example JSON returned from Isso:
```json
{
"text": "Hello, World!",
"author": "Bernd",
"website": null,
"votes": 0,
"mode": 1,
"id": 1,
"parent": null,
"hash": "68b329da9893e34099c7d8ad5cb9c940",
"created": 1379001637.50,
"modified": null
}
```
text
: required, comment as HTML
author
: author's name, may be `null`
website
: author's website, may be `null`
votes
: sum of up- and downvotes, defaults to zero.
mode
: * 1, accepted comment
* 2, comment in moderation queue
* 4, comment deleted, but is referenced
id
: unique comment number per thread
parent
: answer to a parent id, may be `null`
hash
: user identification, used to generate identicons
created
: time in seconds sinde epoch
modified
: last modification time in seconds, may be `null`
## List comments
List all visible comments for a thread. Does not include deleted and
comments currently in moderation queue.
GET /?uri=path
You must encode `path`, e.g. to retrieve comments for `/hello-world/`:
GET /?uri=%2Fhello-world%2F
To disable automatic Markdown-to-HTML conversion, pass `plain=1` to the
query URL:
GET /?uri=...&plain=1
As response, you either get 200, 400, or 404, which are pretty self-explanatory.
GET /
400 BAD REQUEST
GET /?uri=%2Fhello-world%2F
404 NOT FOUND
GET /?uri=%2Fcomment-me%2F
[{comment 1}, {comment 2}, ...]
## Create comments
...
## Delete comments
...
## Up- and downvote comments
...

@ -0,0 +1,188 @@
Isso Configuration
==================
The Isso configuration file is an `INI-style`__ textfile. Below is an example for
a basic Isso configuration. Each section has its own documentation.
.. code-block:: ini
[general]
dbpath = /var/lib/isso/comments.db
host = https://example.tld/
[server]
port = 1234
You can point Isso to your configuration file either witg a command-line parameter
or using an environment variable:
.. code-block:: sh
~> isso -c path/to/isso.cfg
~> env ISSO_SETTINGS=path/to/isso.cfg isso
__ https://en.wikipedia.org/wiki/INI_file
General
-------
In this section, you configure most comment-related options such as database path,
session key and hostname. Here are the default values for this section:
.. code-block:: ini
[general]
dbpath = /tmp/isso.db
host = http://localhost:8080/
max-age = 15m
session-key = ... # python: binascii.b2a_hex(os.urandom(24))
dbpath
file location to the SQLite3 database, highly recommended to change this
location to a non-temporary location!
host
URL to your website. When you start Isso, it will probe your website with
a simple ``GET /`` request to see if it can reach the webserver. If this
fails, Isso may not be able check if a web page exists, thus fails to
accept new comments.
You can supply more than one host:
.. code-block:: ini
[general]
host =
http://localhost/
https://localhost/
This is useful, when your website is available on HTTP and HTTPS.
session-key
private session key to validate client cookies. If you restart the
application several times per hour for whatever reason, use a fixed
key.
max-age
time range that allows users to edit/remove their own comments. See
:ref:`Appendum: Timedelta <appendum-timedelta>` for valid values.
Moderation
----------
Enable moderation queue and handling of comments still in moderation queue
.. code-block:: ini
[moderation]
enabled = false
purge-after = 30d
enabled
enable comment moderation queue. This option only affects new comments.
Comments in modertion queue are not visible to other users until you
activate them.
purge-after
remove unprocessed comments in moderation queue after given time.
Server
------
HTTP server configuration, does **not** apply to uWSGI.
.. code-block:: ini
[server]
host = localhost
port = 8080
reload = off
host
listen on specified interface
port
application port
reload
reload application, when the source code has changed. Useful for
development (don't forget to use a fixed `session-key`).
SMTP
----
Isso can notify you on new comments via SMTP. In the email notification, you
also can moderate comments. If the server connection fails during startup, a
null mailer is used.
.. code-block:: ini
[smtp]
username =
password =
host = localhost
port = 465
ssl = on
to =
from =
username
self-explanatory, optional
password
self-explanatory (yes, plain text, create a dedicated account for
notifications), optional.
host
SMTP server
port
SMTP port
ssl
use SSL to connect to the server. Python probably does not validate the
certificate. Needs research, though. But you should use a dedicated
email account anyways.
to
recipient address, e.g. your email address
from
sender address, e.g. isso@example.tld
Guard
-----
Enable basic spam protection features, e.g. rate-limit per IP address (``/24``
for IPv4, ``/48`` for IPv6).
.. code-block:: ini
[guard]
enabled = true
ratelimit = 2
enabled
enable guard, recommended in production. Not useful for debugging
purposes.
ratelimit: N
limit to N new comments per minute.
Appendum
---------
.. _appendum-timedelta:
Timedelta
A human-friendly representation of a time range: `1m` equals to 60
seconds. This works for years (y), weeks (w), days (d) and seconds (s),
e.g. `30s` equals 30 to seconds.
You can add different types: `1m30s` equals to 90 seconds, `3h45m12s`
equals to 3 hours, 45 minutes and 12 seconds (12512 seconds).

@ -1,124 +0,0 @@
<!DOCTYPE html>
{%- set url_root = pathto('', 1) %}
{# XXX necessary? #}
{%- if url_root == '#' %}{% set url_root = '' %}{% endif %}
{%- macro script() %}
<script type="text/javascript">
var DOCUMENTATION_OPTIONS = {
URL_ROOT: '{{ url_root }}',
VERSION: '{{ release|e }}',
COLLAPSE_INDEX: false,
FILE_SUFFIX: '{{ '' if no_search_suffix else file_suffix }}',
HAS_SOURCE: {{ has_source|lower }},
SOURCELINK_SUFFIX: '{{ sourcelink_suffix }}'
};
</script>
{%- for scriptfile in script_files %}
<script type="text/javascript" src="{{ pathto(scriptfile, 1) }}"></script>
{%- endfor %}
{%- endmacro %}
{%- macro css() %}
<!--link rel="stylesheet" href="{{ pathto('_static/' + style, 1) }}" type="text/css" /-->
<link rel="stylesheet" href="{{ pathto('_static/pygments.css', 1) }}" type="text/css" />
<link rel="stylesheet" href="{{ pathto('_static/css/site.css', 1) }}"/>
{%- for cssfile in css_files %}
<link rel="stylesheet" href="{{ pathto(cssfile, 1) }}" type="text/css" />
{%- endfor %}
{%- endmacro %}
<html>
<head>
<meta charset="utf-8">
{{ metatags }}
{%- block htmltitle %}
<title>{{ title|striptags|e }}</title>
{%- endblock %}
{{ css() }}
{%- if not embedded %}
{{ script() }}
{%- if use_opensearch %}
<link rel="search" type="application/opensearchdescription+xml"
title="{% trans docstitle=docstitle|e %}Search within {{ docstitle }}{% endtrans %}"
href="{{ pathto('_static/opensearch.xml', 1) }}"/>
{%- endif %}
{%- if favicon %}
<link rel="shortcut icon" href="{{ pathto('_static/' + favicon, 1) }}"/>
{%- endif %}
{%- endif %}
{%- block linktags %}
{%- if hasdoc('about') %}
<link rel="author" title="{{ _('About these documents') }}" href="{{ pathto('about') }}" />
{%- endif %}
{%- if hasdoc('genindex') %}
<link rel="index" title="{{ _('Index') }}" href="{{ pathto('genindex') }}" />
{%- endif %}
{%- if hasdoc('search') %}
<link rel="search" title="{{ _('Search') }}" href="{{ pathto('search') }}" />
{%- endif %}
{%- if hasdoc('copyright') %}
<link rel="copyright" title="{{ _('Copyright') }}" href="{{ pathto('copyright') }}" />
{%- endif %}
<link rel="top" title="{{ docstitle|e }}" href="{{ pathto('index') }}" />
{%- if parents %}
<link rel="up" title="{{ parents[-1].title|striptags|e }}" href="{{ parents[-1].link|e }}" />
{%- endif %}
{%- if next %}
<link rel="next" title="{{ next.title|striptags|e }}" href="{{ next.link|e }}" />
{%- endif %}
{%- if prev %}
<link rel="prev" title="{{ prev.title|striptags|e }}" href="{{ prev.link|e }}" />
{%- endif %}
{%- endblock %}
{%- block extrahead %} {% endblock %}
</head>
<body>
<div class="wrapper">
<div class="header">
<header>
<img class="logo" src="{{ pathto('_static/isso.svg', 1) }}" alt="Wynaut by @veekun"/>
<div class="title">
<a href="{{ pathto(".") }}">
<h1>Isso</h1>
<h2>a commenting server similar to Disqus</h2>
</a>
</div>
</header>
<nav>
<ul>
<li><a href="{{ pathto('faq') }}">FAQ</a></li>
<li><a href="{{ pathto('contribute') }}">Contribute</a></li>
<li><a href="{{ pathto('docs') }}">Documentation</a></li>
</ul>
</nav>
</div>
<div class="outer">
{% block header %} {% endblock %}
</div>
<main>
{% block body %} {% endblock %}
</main>
<div class="push"></div>
</div>
<div class="outer footer">
{%- block footer %}
<footer>
&copy; Copyright {{ copyright }}.
{%- if last_updated %}
Last updated on {{ last_updated }}.
{%- endif %}
Made with <a href="http://sphinx-doc.org/">Sphinx</a>.
</footer>
</div>
{%- endblock %}
</body>
</html>

@ -1,16 +0,0 @@
{%- extends "layout.html" %}
{% block header %}
<header>
<h1>{{- title -}}</h1>
</header>
{% endblock %}
{% block body %}
<div class="sidebar">
{% if pagename.startswith("docs/") %}
{% include "sidebar-docs.html" %}
{% endif %}
</div>
<div class="docs">
{{ body }}
</div>
{% endblock %}

@ -1,11 +0,0 @@
from docutils import nodes
from sphinx.writers.html import HTMLTranslator
class IssoTranslator(HTMLTranslator):
def visit_title(self, node):
if self.section_level == 1:
raise nodes.SkipNode
HTMLTranslator.visit_title(self, node)

@ -1,54 +0,0 @@
{% extends "layout.html" %}
{% block header %}
<header>
<h1>Search</h1>
</header>
{% endblock %}
{% set title = _('Search') %}
{% set script_files = script_files + ['_static/searchtools.js'] %}
{% block extrahead %}
<script type="text/javascript">
jQuery(function() { Search.loadIndex("{{ pathto('searchindex.js', 1) }}"); });
</script>
{# this is used when loading the search index using $.ajax fails,
such as on Chrome for documents on localhost #}
<script type="text/javascript" id="searchindexloader"></script>
{{ super() }}
{% endblock %}
{% block body %}
<div id="fallback" class="admonition warning">
<script type="text/javascript">$('#fallback').hide();</script>
<p>
{% trans %}Please activate JavaScript to enable the search
functionality.{% endtrans %}
</p>
</div>
<p>
{% trans %}From here you can search these documents. Enter your search
words into the box below and click "search". Note that the search
function will automatically search for all of the words. Pages
containing fewer words won't appear in the result list.{% endtrans %}
</p>
<form action="" method="get">
<input type="text" name="q" value="" />
<input type="submit" value="{{ _('search') }}" />
<span id="search-progress" style="padding-left: 10px"></span>
</form>
{% if search_performed %}
<h2>{{ _('Search Results') }}</h2>
{% if not search_results %}
<p>{{ _("Your search did not match any documents. Please make sure that all words are spelled correctly and that you've selected enough categories.") }}</p>
{% endif %}
{% endif %}
<div id="search-results">
{% if search_results %}
<ul>
{% for href, caption, context in search_results %}
<li><a href="{{ pathto(item.href) }}">{{ caption }}</a>
<div class="context">{{ context|e }}</div>
</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endblock %}

@ -1,12 +0,0 @@
{%- if pagename != "search" %}
<div id="searchbox" style="display: none">
<h2>{{ _('Quick search') }}</h2>
<form class="search" action="{{ pathto('search') }}" method="get">
<input type="text" name="q" />
<input type="submit" value="{{ _('Go') }}" />
<input type="hidden" name="check_keywords" value="yes" />
<input type="hidden" name="area" value="default" />
</form>
</div>
<script type="text/javascript">$('#searchbox').show(0);</script>
{%- endif %}

@ -1,32 +0,0 @@
{% macro doc(path, title) %}
{%- if pagename == path -%}
<li class="active">
{% else %}
<li>
{%- endif -%}
<a href="{{ pathto(path) }}">{{ title }}</a></li>
{% endmacro %}
<strong>Getting Started</strong>
<ul>
{{ doc("docs/install", "Installation") }}
{{ doc("docs/quickstart", "Quickstart") }}
{{ doc("docs/troubleshooting", "Troubleshooting") }}
</ul>
<strong>Advanced Setup</strong>
<ul>
{{ doc("docs/setup/sub-uri", "Sub URI") }}
{{ doc("docs/setup/multiple-sites", "Multiple Sites") }}
</ul>
<strong>Configuration</strong>
<ul>
{{ doc("docs/configuration/server", "Server") }}
{{ doc("docs/configuration/client", "Client") }}
</ul>
<strong>Extras</strong>
<ul>
{{ doc("docs/extras/deployment", "Deployment") }}
{{ doc("docs/extras/advanced-integration", "Advanced Integration") }}
{{ doc("docs/extras/api", "API") }}
{{ doc("docs/extras/contribs", "Community tools") }}
</ul>

@ -1,3 +0,0 @@
[theme]
inherit = basic
stylesheet = css/site.css

@ -1,383 +0,0 @@
@import "bourbon/bourbon";
@import "neat/neat";
$blue: #00aac2;
$text: #5c5c5c;
* {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
}
h1, h2, h3, h4, h5, h6 {
font-weight: normal;
font-style: normal;
}
body {
font: 14px "Helvetica Neue", Helvetica, Arial, sans-serif;
color: black;
}
.wrapper {
min-height: 100%;
height: auto !important;
height: 100%;
margin: 0 auto -2em;
}
.push, .footer {
height: 2em;
a, a:visited {
color: $blue;
text-decoration: none;
}
}
.header {
@include outer-container;
padding-top: 1em;
padding-bottom: 1em;
a, a:visited {
color: #4d4c4c;
text-decoration: none;
}
header {
@include span-columns(1 of 2);
font-weight: normal;
.logo {
max-height: 60px;
padding-right: 12px;
float: left;
}
h1 {
font-size: 1.55em;
margin-bottom: 0.3em;
}
h2 {
font-size: 1.05em;
}
}
nav {
@include span-columns(1 of 2);
ul {
padding-top: 1.55em + 0.3em;
li {
display: block;
float: right;
margin-left: 2em;
font-weight: 300;
text-transform: uppercase;
color: #444;
letter-spacing: 0.05em;
}
}
}
}
.outer {
background-color: rgb(238, 238, 238);
box-shadow: inset 0 0 0.5em #c0c0c0;
header {
@include outer-container;
padding: 1em 0;
h1 {
color: #555;
font: 300 35px Lato, "Helvetica Neue", Helvetica, Arial, sans-serif;
text-shadow: 1px 0 #c0c0c0;
letter-spacing: 0.05em;
}
}
footer {
@include outer-container;
}
.index {
@include outer-container;
padding: 1em;
figure {
@include span-columns(2 of 5);
text-align: center;
max-width: 300px;
img {
max-width: 100%;
max-height: 100%;
}
}
ul {
@include span-columns(3 of 5);
margin-top: 1em;
li {
list-style-type: none;
margin-bottom: 2em;
strong {
font-weight: normal;
font-size: 18px;
color: black;
}
p {
color: $text;
}
p + p {
margin-top: 0.5em;
}
}
}
}
}
.footer {
text-align: right;
padding: 0.5em 0;
}
main {
@include outer-container;
margin-top: 1.6em;
margin-bottom: 2em;
line-height: 1.6em;
h2:before {
content: "» ";
}
.links {
@include span-columns(1 of 3);
h2 {
margin-bottom: 1em;
margin-top: 1em;
}
> h2:first-child {
margin-top: 0;
}
p + p {
margin-top: 1em;
}
p + p:last-child {
margin-top: 0;
}
li {
list-style-type: none;
}
.search {
input {
padding: 0.1em 0.4em;
}
}
}
.demo {
@include span-columns(2 of 3);
h2 {
margin-bottom: 1em;
}
h4 {
margin-bottom: 0.5em;
}
blockquote {
margin-top: 10px;
margin-bottom: 10px;
padding-left: 15px;
border-left: 3px solid #ccc;
}
pre {
background: #eee;
border: 1px solid #ddd;
padding: 10px 15px;
color: #4d4d4c;
overflow: auto;
}
}
.sidebar {
@include span-columns(1 of 5);
display: block;
strong {
color: $blue;
font-weight: bold;
text-transform: uppercase;
}
ul {
margin-top: 0.5em;
margin-bottom: 1em;
}
ul:last-child {
margin-bottom: 0;
}
li {
border-left: solid 2px #d3d3d3;
margin-left: 0.25em;
padding-left: 0.75em;
padding-top: 0.25em;
padding-bottom: 0.25em;
font-weight: 300;
list-style-type: none;
a {
color: $text;
}
}
.active {
border-left-color: $blue;
a {
color: $blue;
font-weight: bold;
}
}
}
.docs {
@include span-columns(4 of 5);
font-size: 15px;
color: $text;
h2, h3, h4 {
margin-top: 1em;
margin-bottom: 1em;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 18px;
}
h4 {
font-size: 14px;
}
pre, code {
color: rgb(77, 77, 76);
font-size: 12px;
font-family: Monaco, Menlo, Consolas, monospace;
}
.highlight {
margin: 1.2em 0;
padding: 10px 15px;
background-color: rgb(238, 238, 238);
color: rgb(77, 77, 76);
border: 1px solid rgb(221, 221, 221);
border-radius: 4px;
overflow: auto;
}
.headerlink {
visibility: hidden;
}
p + p {
margin-top: 1em;
}
p + p:last-child {
margin-bottom: 0;
}
blockquote {
margin: 1em;
text-align: center;
font-size:125%;
font-style:italic;
}
blockquote:before {
content: "» ";
}
blockquote:after {
content: " «";
}
ul {
margin: 1em 0;
li {
list-style-type: square;
margin-left: 2em;
margin-right: 2em;
}
}
}
a {
text-decoration: none;
color: $blue;
}
dt {
font-weight: bold;
margin: 0.4em 0;
}
dd {
margin-left: 1.2em;
}
dl {
margin-bottom: 0.4em;
}
.admonition {
p + p {
margin-top: 0.25em;
}
p:not(:first-child) {
margin-left: 2em;
}
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 11 KiB

@ -1,278 +0,0 @@
# -*- coding: utf-8 -*-
#
# Isso documentation build configuration file, created by
# sphinx-quickstart on Thu Nov 21 11:28:01 2013.
#
# This file is execfile()d with the current directory set to its
# containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.
import sys
import io
import re
import pkg_resources
from os.path import join, dirname
sys.path.insert(0, join(dirname(__file__), "_isso/"))
try:
dist = pkg_resources.get_distribution("isso")
except pkg_resources.DistributionNotFound:
dist = type("I'm a Version", (object, ), {})
with io.open(join(dirname(__file__), "../setup.py")) as fp:
for line in fp:
m = re.match("\\s*version='([^']+)'\\s*", line)
if m:
dist.version = m.group(1)
break
else:
dist.version = ""
# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#sys.path.insert(0, os.path.abspath('.'))
# -- General configuration ------------------------------------------------
# If your documentation needs a minimal Sphinx version, state it here.
#needs_sphinx = '1.0'
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinx.ext.todo',
]
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix of source filenames.
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'docs/index'
# General information about the project.
project = u'Isso'
copyright = u'2016, Martin Zimmermann'
# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
from distutils.version import LooseVersion
#
# The full version, including alpha/beta/rc tags.
release = dist.version
# The short X.Y version.
version = "{0}.{1}".format(*LooseVersion(dist.version).version)
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#language = None
# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
#today = ''
# Else, today_fmt is used as the format for a strftime call.
#today_fmt = '%B %d, %Y'
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
# The reST default role (used for this markup: `text`) to use for all
# documents.
#default_role = None
# If true, '()' will be appended to :func: etc. cross-reference text.
#add_function_parentheses = True
# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
#add_module_names = True
# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
#show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'trac'
# A list of ignored prefixes for module index sorting.
#modindex_common_prefix = []
# If true, keep warnings as "system message" paragraphs in the built documents.
#keep_warnings = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
html_theme = '_isso'
html_translator_class = "remove_heading.IssoTranslator"
# Theme options are theme-specific and customize the look and feel of a theme
# further. For a list of options available for each theme, see the
# documentation.
#html_theme_options = {}
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = ["."]
# The name for this set of Sphinx documents. If None, it defaults to
# "<project> v<release> documentation".
#html_title = None
# A shorter title for the navigation bar. Default is the same as html_title.
#html_short_title = None
# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
#html_logo = None
# The name of an image file (within the static path) to use as favicon of the
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
html_favicon = "favicon.ico"
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
# directly to the root of the documentation.
#html_extra_path = []
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
#html_last_updated_fmt = '%b %d, %Y'
# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
#html_use_smartypants = True
# Custom sidebar templates, maps document names to template names.
#html_sidebars = {}
# Additional templates that should be rendered to pages, maps page names to
# template names.
html_additional_pages = {"index": "index.html"}
# If false, no module index is generated.
html_domain_indices = False
# If false, no index is generated.
html_use_index = False
# If true, the index is split into individual pages for each letter.
#html_split_index = False
# If true, links to the reST sources are added to the pages.
#html_show_sourcelink = True
# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it. The value of this option must be the
# base URL from which the finished HTML is served.
#html_use_opensearch = ''
# This is the file name suffix for HTML files (e.g. ".xhtml").
#html_file_suffix = None
# Output file base name for HTML help builder.
htmlhelp_basename = 'Issodoc'
# -- Options for LaTeX output ---------------------------------------------
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
#'papersize': 'letterpaper',
# The font size ('10pt', '11pt' or '12pt').
#'pointsize': '10pt',
# Additional stuff for the LaTeX preamble.
#'preamble': '',
}
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title,
# author, documentclass [howto, manual, or own class]).
latex_documents = [
('index', 'Isso.tex', u'Isso Documentation',
u'Martin Zimmermann', 'manual'),
]
# The name of an image file (relative to this directory) to place at the top of
# the title page.
#latex_logo = None
# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
#latex_use_parts = False
# If true, show page references after internal links.
#latex_show_pagerefs = False
# If true, show URL addresses after external links.
#latex_show_urls = False
# Documents to append as an appendix to all manuals.
#latex_appendices = []
# If false, no module index is generated.
#latex_domain_indices = True
# -- Options for manual page output ---------------------------------------
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
('docs/man/index', 'isso', u'a Disqus alternative',
[u'Martin Zimmermann'], 1),
('docs/configuration/server', 'isso.conf', u'server configuration',
[u'Martin Zimmermann'], 5)
]
# If true, show URL addresses after external links.
#man_show_urls = False
# -- Options for Texinfo output -------------------------------------------
# Grouping the document tree into Texinfo files. List of tuples
# (source start file, target name, title, author,
# dir menu entry, description, category)
texinfo_documents = [
('index', 'Isso', u'Isso Documentation',
u'Martin Zimmermann', 'Isso', 'a commenting server similar to Disqus',
'Miscellaneous'),
]
# Documents to append as an appendix to all manuals.
#texinfo_appendices = []
# If false, no module index is generated.
#texinfo_domain_indices = True
# How to display URL addresses: 'footnote', 'no', or 'inline'.
#texinfo_show_urls = 'footnote'
# If true, do not generate a @detailmenu in the "Top" node's menu.
#texinfo_no_detailmenu = False

@ -1,61 +0,0 @@
Contribute
==========
Write some code.
----------------
I appreciate any help and love pull requests. Here are some things you
need to respect:
* no hard-wired external services (e.g. Gravatar, Akismet)
* no support for ancient browsers (e.g. IE6-9)
If you create a feature request, start a new branch. If you fix an
issue, start off the *master* branch and pull request against the
master. I'll cherry-pick/backport the fix onto the current legacy
(maintenance) release.
Report issues
-------------
- **Disqus import fails** if ``isso import /path/to/disqus.xml`` fails,
please do *NOT* attach the raw dump file to GH:Issues. Please anonymize all
IP addresses inside the ``<ipAddress>`` tag first, as second step, replace
all ``<email>`` addresses with a generic one.
- **embed.min.js-related issues** if you get a cryptic JavaScript error in
the console, embed ``embed.dev.js`` instead of ``embed.min.js`` and create an
issue with ±10 lines of code around the error.
- **Isso-related issues** Copy and paste traceback into a ticket and provide
some details of your usage.
Translations
------------
Isso supports multiple languages and it is fairly easy to add new translations.
You can either use the `english translation file`__ or use Transifex_. Contact
me on IRC (@posativ) if you want to be the main contributor for a language.
You may notice some "weird" newlines in translations -- that's the separator
for pluralforms_ in the templating engine.
.. __: https://github.com/posativ/isso/blob/master/isso/js/app/i18n/en.js
.. _Transifex: https://www.transifex.com/projects/p/isso/
.. _pluralforms: http://docs.translatehouse.org/projects/localization-guide/en/latest/l10n/pluralforms.html?id=l10n/pluralforms
Where I need help.
------------------
Isso is my first projects which involves JavaScript. If you are more
experienced in JS, please take a look at the code (that does not mean, the
Python code is perfect ;-). A few feature requests and issues, where I
definitely need help:
* `Admin Web Interface <https://github.com/posativ/isso/issues/10>`_
administration via email is cumbersome with a high amount of comments. A
administration web interface should include the ability to:
- delete or activate comments matching a filter (e.g. name, email, ip address)
- close threads and remove threads completely

@ -1,158 +0,0 @@
Client Configuration
====================
You can configure the client (the JS part) with custom data attributes,
preferably in the script tag which embeds the JS:
.. code-block:: html
<script data-isso="/prefix/"
data-isso-id="thread-id"
data-isso-css="true"
data-isso-lang="ru"
data-isso-reply-to-self="false"
data-isso-require-author="false"
data-isso-require-email="false"
data-isso-reply-notifications="false"
data-isso-max-comments-top="10"
data-isso-max-comments-nested="5"
data-isso-reveal-on-click="5"
data-isso-avatar="true"
data-isso-avatar-bg="#f0f0f0"
data-isso-avatar-fg="#9abf88 #5698c4 #e279a3 #9163b6 ..."
data-isso-vote="true"
data-isso-vote-levels=""
data-isso-feed="false"
src="/prefix/js/embed.js"></script>
Furthermore you can override the automatic title detection inside
the embed tag, as well as the thread ID, e.g.:
.. code-block:: html
<section id="isso-thread" data-title="Foo!" data-isso-id="/path/to/resource"></section>
Additionally, you can override any translation string for any language by adding
a ``data-isso-`` attribute that is equal to the translation key (found `here`__) with
``-text-[lang]`` appended to it. So, for example, if you want to override the
english translation of the ``postbox-notification`` message, you could add:
``data-isso-postbox-notification-text-en="Select to be notified of replies to your comment"``
.. __: https://github.com/posativ/isso/blob/master/isso/js/app/i18n/en.js
data-isso
---------
Isso usually detects the REST API automatically, but when you serve the JS
script on a different location, this may fail. Use `data-isso` to
override the API location:
.. code-block:: html
<script data-isso="/isso" src="/path/to/embed.min.js"></script>
data-isso-css
-------------
Set to `false` prevents Isso from automatically appending the stylesheet.
Defaults to `true`.
.. code-block:: html
<script src="..." data-isso-css="false"></script>
data-isso-lang
--------------
Override useragent's preferred language. Isso has been translated in over 12
languages. The language is configured by its `ISO 639-1
<https://en.wikipedia.org/wiki/ISO_639-1>`_ (two letter) code.
You find a list of all supported languages on `GitHub
<https://github.com/posativ/isso/tree/master/isso/js/app/i18n>`_.
data-isso-reply-to-self
-----------------------
Set to `true` when spam guard is configured with `reply-to-self = true`.
data-isso-require-author
------------------------
Set to `true` when spam guard is configured with `require-author = true`.
data-isso-require-email
-----------------------
Set to `true` when spam guard is configured with `require-email = true`.
data-isso-reply-notifications
-----------------------------
Set to `true` when reply notifications is configured with `reply-notifications = true`.
data-isso-max-comments-top and data-isso-max-comments-nested
------------------------------------------------------------
Number of top level (or nested) comments to show by default. If some
comments are not shown, an "X Hidden" link is shown.
Set to `"inf"` to show all, or `"0"` to hide all.
data-isso-reveal-on-click
-------------------------
Number of comments to reveal on clicking the "X Hidden" link.
data-isso-avatar
----------------
Enable or disable avatar generation.
data-isso-avatar-bg
-------------------
Set avatar background color. Any valid CSS color will do.
data-isso-avatar-fg
-------------------
Set avatar foreground color. Up to 8 colors are possible. The default color
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
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
--------------
Enable or disable voting feature on the client side.
data-isso-vote-levels
---------------------
List of vote levels used to customize comment appearance based on score.
Provide a comma-separated values (eg. `"0,5,10,25,100"`) or a JSON array (eg. `"[-5,5,15]"`).
For example, the value `"-5,5"` will cause each `isso-comment` to be given one of these 3 classes:
- `isso-vote-level-0` for scores lower than `-5`
- `isso-vote-level-1` for scores between `-5` and `4`
- `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)
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.

@ -1,429 +0,0 @@
Server Configuration
====================
The Isso configuration file is an `INI-style`__ textfile. It reads integers,
booleans, strings and lists. Here's the default isso configuration:
`isso.conf <https://github.com/posativ/isso/blob/master/share/isso.conf>`. A
basic configuration from scratch looks like this:
.. code-block:: ini
[general]
dbpath = /var/lib/isso/comments.db
host = https://example.tld/
[server]
listen = http://localhost:1234/
To use your configuration file with Isso, append ``-c /path/to/cfg`` to the
executable or run Isso with an environment variable:
.. code-block:: sh
~> isso -c path/to/isso.cfg
~> env ISSO_SETTINGS=path/to/isso.cfg isso
__ https://en.wikipedia.org/wiki/INI_file
Sections covered in this document:
.. contents::
:local:
General
-------
In this section, you configure most comment-related options such as database path,
session key and hostname. Here are the default values for this section:
.. code-block:: ini
[general]
dbpath = /tmp/isso.db
name =
host =
max-age = 15m
notify = stdout
log-file =
dbpath
file location to the SQLite3 database, highly recommended to change this
location to a non-temporary location!
name
required to dispatch :ref:`multiple websites <configure-multiple-sites>`,
not used otherwise.
host
Your website(s). If Isso is unable to connect to at least one site, you'll
get a warning during startup and comments are most likely non-functional.
You'll need at least one host/website to run Isso. This is due to security
reasons: Isso uses CORS_ to embed comments and to restrict comments only to
your website, you have to "whitelist" your website(s).
I recommend the first value to be a non-SSL website that is used as fallback
if Firefox users (and only those) supress their HTTP referer completely.
.. code-block:: ini
[general]
host =
http://example.tld/
https://example.tld/
max-age
time range that allows users to edit/remove their own comments. See
:ref:`Appendum: Timedelta <appendum-timedelta>` for valid values.
notify
Select notification backend(s) for new comments, separated by comma.
Available backends:
stdout
Log to standard output. Default, if none selected. Note, this
functionality is broken since a few releases.
smtp
Send notifications via SMTP on new comments with activation (if
moderated) and deletion links.
reply-notifications
Allow users to request E-mail notifications for replies to their post.
It is highly recommended to also turn on moderation when enabling this
setting, as Isso can otherwise be easily exploited for sending spam.
Do not forget to configure the client accordingly.
log-file
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"
latest-enabled
If True it will enable the ``/latest`` endpoint. Optional, defaults
to False.
.. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS
Moderation
----------
Enable moderation queue and handling of comments still in moderation queue
.. code-block:: ini
[moderation]
enabled = false
approve-if-email-previously-approved = false
purge-after = 30d
enabled
enable comment moderation queue. This option only affects new comments.
Comments in moderation queue are not visible to other users until you
activate them.
approve-if-email-previously-approved
automatically approve comments by an email address if that address has
had a comment approved within the last 6 months. No ownership verification
is done on the entered email address. This means that if someone is able
to guess correctly the email address used by a previously approved author,
they will be able to have their new comment auto-approved.
purge-after
remove unprocessed comments in moderation queue after given time.
Server
------
HTTP server configuration.
.. code-block:: ini
[server]
listen = http://localhost:8080
reload = off
profile = off
listen
interface to listen on. Isso supports TCP/IP and unix domain sockets:
.. code-block:: ini
; UNIX domain socket
listen = unix:///tmp/isso.sock
; TCP/IP
listen = http://localhost:1234/
When ``gevent`` is available, it is automatically used for `http://`
Currently, gevent can not handle http requests on unix domain socket
(see `#295 <https://github.com/surfly/gevent/issues/295>`_ and
`#299 <https://github.com/surfly/gevent/issues/299>`_ for details).
Does not apply for `uWSGI`.
public-endpoint
public URL that Isso is accessed from by end users. Should always be
a http:// or https:// absolute address. If left blank, automatic
detection is attempted. Normally only needs to be specified if
different than the `listen` setting.
reload
reload application, when the source code has changed. Useful for
development. Only works with the internal webserver.
profile
show 10 most time consuming function in Isso after each request. Do
not use in production.
trusted-proxies
an optional list of reverse proxies IPs behind which you have deployed
your Isso web service (e.g. `127.0.0.1`).
This allow for proper remote address resolution based on a
`X-Forwarded-For` HTTP header, which is important for the mechanism
forbiding several comment votes coming from the same subnet.
.. _configure-smtp:
SMTP
----
Isso can notify you on new comments via SMTP. In the email notification, you
also can moderate (=activate or delete) comments. Don't forget to configure
``notify = smtp`` in the general section.
.. code-block:: ini
[smtp]
username =
password =
host = localhost
port = 587
security = starttls
to =
from =
timeout = 10
username
self-explanatory, optional
password
self-explanatory (yes, plain text, create a dedicated account for
notifications), optional.
host
SMTP server
port
SMTP port
security
use a secure connection to the server, possible values: *none*, *starttls*
or *ssl*. Note, that there is no easy way for Python 2.7 and 3.3 to
implement certification validation and thus the connection is vulnerable to
Man-in-the-Middle attacks. You should definitely use a dedicated SMTP
account for Isso in that case.
to
recipient address, e.g. your email address
from
sender address, e.g. `"Foo Bar" <isso@example.tld>`
timeout
specify a timeout in seconds for blocking operations like the
connection attempt.
Guard
-----
Enable basic spam protection features, e.g. rate-limit per IP address (``/24``
for IPv4, ``/48`` for IPv6).
.. code-block:: ini
[guard]
enabled = true
ratelimit = 2
direct-reply = 3
reply-to-self = false
require-author = false
require-email = false
enabled
enable guard, recommended in production. Not useful for debugging
purposes.
ratelimit
limit to N new comments per minute.
direct-reply
how many comments directly to the thread (prevent a simple
`while true; do curl ...; done`.
reply-to-self
allow commenters to reply to their own comments when they could still edit
the comment. After the editing timeframe is gone, commenters can reply to
their own comments anyways.
Do not forget to configure the `client <client>`_ accordingly
require-author
force commenters to enter a value into the author field. No validation is
performed on the provided value.
Do not forget to configure the `client <client>`_ accordingly.
require-email
force commenters to enter a value into the email field. No validation is
performed on the provided value.
Do not forget to configure the `client <client>`_ accordingly.
Markup
------
Customize markup and sanitized HTML. Currently, only Markdown (via Misaka) is
supported, but new languages are relatively easy to add.
.. code-block:: ini
[markup]
options = strikethrough, superscript, autolink
flags = skip-html, escape, hard-wrap
allowed-elements =
allowed-attributes =
options
`Misaka-specific Markdown extensions <https://misaka.61924.nl/#api>`_, all
extension flags can be used there, separated by comma, either by their name
or as `EXT_`_.
flags
`Misaka-specific HTML rendering flags
<https://misaka.61924.nl/#html-render-flags>`_, all html rendering flags
can be used here, separated by comma, either by their name or as `HTML_`_.
Per Misaka's defaults, no flags are set.
allowed-elements
Additional HTML tags to allow in the generated output, comma-separated. By
default, only *a*, *blockquote*, *br*, *code*, *del*, *em*, *h1*, *h2*,
*h3*, *h4*, *h5*, *h6*, *hr*, *ins*, *li*, *ol*, *p*, *pre*, *strong*,
*table*, *tbody*, *td*, *th*, *thead* and *ul* are allowed.
allowed-attributes
Additional HTML attributes (independent from elements) to allow in the
generated output, comma-separated. By default, only *align* and *href* are
allowed.
To allow images in comments, you just need to add ``allowed-elements = img`` and
``allowed-attributes = src``.
Hash
----
Customize used hash functions to hide the actual email addresses from
commenters but still be able to generate an identicon.
.. code-block:: ini
[hash]
salt = Eech7co8Ohloopo9Ol6baimi
algorithm = pbkdf2
salt
A salt is used to protect against rainbow tables. Isso does not make use of
pepper (yet). The default value has been in use since the release of Isso
and generates the same identicons for same addresses across installations.
algorithm
Hash algorithm to use -- either from Python's `hashlib` or PBKDF2 (a
computational expensive hash function).
The actual identifier for PBKDF2 is `pbkdf2:1000:6:sha1`, which means 1000
iterations, 6 bytes to generate and SHA1 as pseudo-random family used for
key strengthening.
Arguments have to be in that order, but can be reduced to `pbkdf2:4096`
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
Admin
-----
Isso has an optional web administration interface that can be used to moderate
comments. The interface is available under ``/admin`` on your isso URL.
.. code-block:: ini
[admin]
enabled = true
password = secret
enabled
whether to enable the admin interface
password
the plain text password to use for logging into the administration interface
Appendum
--------
.. _appendum-timedelta:
Timedelta
A human-friendly representation of a time range: `1m` equals to 60
seconds. This works for years (y), weeks (w), days (d) and seconds (s),
e.g. `30s` equals 30 to seconds.
You can add different types: `1m30s` equals to 90 seconds, `3h45m12s`
equals to 3 hours, 45 minutes and 12 seconds (12512 seconds).
Environment variables
---------------------
.. _environment-variables:
Isso also support configuration through some environment variables:
ISSO_CORS_ORIGIN
By default, `isso` will use the `Host` or else the `Referrer` HTTP header
of the request to defines a CORS `Access-Control-Allow-Origin` HTTP header
in the response.
This environent variable allows you to define a broader fixed value,
in order for example to share a single Isso instance among serveral of your
subdomains : `ISSO_CORS_ORIGIN=*.example.test`

@ -1,56 +0,0 @@
Advanced integration
====================
Comment counter
---------------
If you want to display a comment counter for a given thread, simply
put a link to that comments thread anchor:
.. code-block:: html
<a href="/my-uri.html#isso-thread">Comments</a>
The *isso js client* willl replace the content of this tag with a human readable
counter like *"5 comments"*.
Alternatively, if guessing from `href` is not relevant, you could use a
`data-isso-id` attribute on the `<a>` to indicate which thread to count for.
Now, either include `count.min.js` if you want to show only the comment count
(e.g. on an index page) or `embed.min.js` for the full comment client (see
:doc:`../quickstart`); do not mix both.
You can have as many comments counters as you want in a page, and they will be
merged into a single `GET` request.
Asynchronous comments loading
-----------------------------
Isso will automatically fetch comments after `DOMContentLoaded` event. However
in the case where your website is creating content dynamically (eg. via ajax),
you need to re-fetch comment thread manually. Here is how you can re-fetch the
comment thread:
.. code-block:: js
window.Isso.fetchComments()
It will delete all comments under the thread but not the PostBox, fetch
comments with `data-isso-id` attribute of the element `section#isso-thread` (if
that attribute does not exist, fallback to `window.location.pathname`), then
fill comments into the thread. In other words, you should change `data-isso-id`
attribute of the element `section#isso-thread` (or modify the pathname with
`location.pushState`) before you can get new comments. And the thread element
itself should *NOT* be touched or removed.
If you removed the `section#isso-thread` element, just create another element
with same TagName and ID in which you wish comments to be placed, then call the
`init` method of `Isso`:
.. code-block:: js
window.Isso.init()
Then Isso will initialize the comment section and fetch comments, as if the page
was loaded.

@ -1,70 +0,0 @@
Advanced Migration
==================
In quickstart we saw you can import comments from Disqus or WordPress. But there
are a many other comments system and you could be using one of them.
Isso provides a way to import such comments, however it's up to you to to:
- dump comments
- fit the data to the following JSON format::
A list of threads, each item being a dict with the following data:
- id: a text representing the unique thread id (note: by default isso
associates this ID to the article URL, but it can be changed on
client side with "data-isso-id" - see :doc:`client configuration <../configuration/client>` )
- title: the title of the thread
- comments: the list of comments
Each item in that list of comments is a dict with the following data:
- id: an integer with the unique id of the comment inside the thread
(it can be repeated among different threads); this will be used to
order the comment inside the thread
- author: the author's name
- email: the author's email
- website: the author's website
- remote_addr: the author's IP
- created: a timestamp, in the format "%Y-%m-%d %H:%M:%S"
Example:
.. code-block:: json
[
{
"id": "/blog/article1",
"title": "First article!",
"comments": [
{
"author": "James",
"created": "2018-11-28 17:24:23",
"email": "email@mail.com",
"id": "1",
"remote_addr": "127.0.0.1",
"text": "Great article!",
"website": "http://fefzfzef.frzr"
},
{
"author": "Harold",
"created": "2018-11-28 17:58:03",
"email": "email2@mail.com",
"id": "2",
"remote_addr": "",
"text": "I hated it...",
"website": ""
}
]
}
]
Keep in mind that isso expects to have an array, so keep the opening and ending square bracket even if you have only one article thread!
Next you can import you json dump:
.. code-block:: sh
~> isso -c /path/to/isso.cfg import -t generic comment-dump.json
[100%] 53 threads, 192 comments

@ -1,213 +0,0 @@
API
====
The Isso API uses HTTP and JSON as primary communication protocol.
.. contents::
:local:
JSON format
-----------
When querying the API you either get a regular HTTP error, an object or list of
objects representing the comment. Here's an example JSON returned from
Isso:
.. code-block:: json
{
"id": 1,
"parent": null,
"text": "<p>Hello, World!</p>\n",
"mode": 1,
"hash": "4505c1eeda98",
"author": null,
"website": null,
"created": 1387321261.572392,
"modified": null,
"likes": 3,
"dislikes": 0
}
id :
comment id (unique per website).
parent :
parent id reference, may be ``null``.
text :
required, comment written in Markdown.
mode :
* 1 accepted
* 2 in moderation queue
* 4 deleted, but referenced.
hash :
user identication, used to generate identicons. PBKDF2 from email or IP
address (fallback).
author :
author's name, may be ``null``.
website :
author's website, may be ``null``.
likes :
upvote count, defaults to 0.
dislikes :
downvote count, defaults to 0.
created :
time in seconds since UNIX time.
modified :
last modification since UNIX time, may be ``null``.
List comments
-------------
List all publicly visible comments for thread `uri`:
.. code-block:: text
GET /?uri=%2Fhello-world%2F
uri :
URI to fetch comments for, required.
plain :
pass plain=1 to get the raw comment text, defaults to 0.
Get the latest N comments for all threads:
.. code-block:: text
GET /latest?limit=N
The N parameter limits how many of the latest comments to retrieve; it's
mandatory, and must be an integer greater than 0.
This endpoint needs to be enabled in the configuration (see the
``latest-enabled`` option in the ``general`` section).
Create comment
--------------
To create a new comment, you need to issue a POST request to ``/new`` and add
the current URI (so the server can check if the location actually exists).
.. code-block:: bash
$ curl -vX POST http://isso/new?uri=%2F -d '{"text": "Hello, World!"}' -H "Content-Type: application/json"
< HTTP/1.1 201 CREATED
< Set-Cookie: 1=...; Expires=Wed, 18-Dec-2013 12:57:20 GMT; Max-Age=900; Path=/
{
"author": null,
"created": 1387370540.733824,
"dislikes": 0,
"email": null,
"hash": "6dcdbfb4f00d",
"id": 1,
"likes": 0,
"mode": 1,
"modified": null,
"parent": null,
"text": "<p>Hello, World!</p>\n",
"website": null
}
The payload must be valid JSON. To prevent CSRF attacks, you must set the
`Content-Type` to `application/json` or omit the header completely.
The server issues a cookie per new comment which acts as authentication token
to modify or delete your own comment. The token is cryptographically signed
and expires automatically after 900 seconds by default.
The following keys can be used to POST a new comment, all other fields are
dropped or replaced with values from the server:
text : String
Actual comment, at least three characters long, required.
author : String
Comment author, optional.
website : String
Commenter's website (currently no field available in the client JS though),
optional.
email : String
Commenter's email address (can be any arbitrary string though) used to
generate the identicon. Limited to 254 characters (RFC specification),
optional.
parent : Integer
Reference to parent comment, optional.
Edit comment
------------
When your authentication token is not yet expired, you can issue a PUT request
to update `text`, `author` and `website`. After an update, you get an updated
authentication token and the comment as JSON:
.. code-block:: bash
$ curl -X PUT http://isso/id/1 -d "..." -H "Content-Type: application/json"
Delete comment
--------------
You can delete your own comments when your authentication token (= cookie) is
not yet expired:
.. code-block:: bash
$ curl -X DELETE http://isso/id/1 -H "Content-Type: application/json"
null
Returns either `null` or a comment with an empty text value when the comment
is still referenced by other comments.
Up- and downvote comments
-------------------------
...
Get comment count
-----------------
Counts all publicly visible comments for thread `uri`:
.. code-block:: text
GET /count?uri=%2Fhello-world%2F
2
uri :
URI to count comments for, required.
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.

@ -1,20 +0,0 @@
Community tools
===============
Utility scripts
---------------
Some utility scripts have been developed by isso users.
They are stored in the `GitHub contrib/ directory
<https://github.com/posativ/isso/tree/master/contrib>`_ :
* `dump_comments.py` : dump isso comments as text, optionally with color
* `import_blogger.py` : comment importer from Blogger
Related projects
----------------
* `wonderfall/isso Docker image <https://github.com/Wonderfall/docker-isso>`
* `a grav plugin to integrate isso comments <https://github.com/Sommerregen/grav-plugin-jscomments>`
* `a Pelican theme supporting isso comments <https://github.com/Lucas-C/pelican-mg>`

@ -1,273 +0,0 @@
Deployment
----------
Isso ships with a built-in web server, which is useful for the initial setup
and may be used in production for low-traffic sites (up to 20 requests per
second). Running a "real" WSGI server supports nice things such as UNIX domain
sockets, daemonization and solid HTTP handler. WSGI servers are more stable, secure
and web-scale than the built-in web server.
* gevent_, coroutine-based network library
* uWSGI_, full-featured uWSGI server
* gunicorn_, Python WSGI HTTP Server for UNIX
* mod_wsgi_, Apache interface to WSGI
* mod_fastcgi_, Apache interface to FastCGI
* uberspace.de, `try this guide (in german) <http://blog.posativ.org/2014/isso-und-uberspace-de/>`_
* Openshift, Isso has a one click installer
`gevent <http://www.gevent.org/>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Probably the easiest deployment method. Install with PIP (requires libevent):
.. code-block:: sh
$ pip install gevent
Then, just use the ``isso`` executable as usual. Gevent monkey-patches Python's
standard library to work with greenlets.
To execute Isso, just use the commandline interface:
.. code-block:: sh
$ isso -c my.cfg run
Unfortunately, gevent 0.13.2 does not support UNIX domain sockets (see `#295
<https://github.com/surfly/gevent/issues/295>`_ and `#299
<https://github.com/surfly/gevent/issues/299>`_ for details).
`uWSGI <http://uwsgi-docs.readthedocs.org/en/latest/>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Isso has special support for uWSGI, namely fast IPC caching, job spooling and
delayed jobs. It is the author's choice, but not the only one. You need
uWSGI 1.9 or higher, fortunately you can install it from PyPi:
.. code-block:: sh
~> apt-get install build-essential python-dev
~> pip install uwsgi
For convenience, I recommend a INI-style configuration (you can also
supply everything as command-line arguments):
.. code-block:: ini
[uwsgi]
http = :8080
master = true
; set to `nproc`
processes = 4
cache2 = name=hash,items=1024,blocksize=32
; you may change this
spooler = /tmp/isso/mail
module = isso.run
; uncomment if you use a virtual environment
; virtualenv = /path/to/isso
env = ISSO_SETTINGS=/path/to/isso.cfg
Then, create the spooling directory and start Isso via uWSGI:
.. code-block:: sh
~> mkdir /tmp/isso/mail
~> uwsgi /path/to/uwsgi.ini
`gunicorn <http://gunicorn.org>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX with a pre-fork
worker ported from Ruby's Unicorn project. Install gunicorn_ via PIP:
.. code-block:: sh
$ pip install gunicorn
To execute Isso, use a command similar to:
.. code-block:: sh
$ export ISSO_SETTINGS="/path/to/isso.cfg"
$ gunicorn -b localhost:8080 -w 4 --preload isso.run
`mod_wsgi <https://code.google.com/p/modwsgi/>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
First, create a startup script, called `isso.wsgi`. If Isso is in your system module
search path, then the script is quite simple. This script is included in the
isso distribution as `run.py`:
.. code-block:: python
from __future__ import unicode_literals
import os
from isso import make_app
from isso import dist, config
application = make_app(
config.load(
os.path.join(dist.location, dist.project_name, "defaults.ini"),
"/path/to/isso.cfg"),
multiprocessing=True)
If you have installed Isso in a virtual environment, then you will have to add the path
of the virtualenv to the site-specific paths of Python:
.. code-block:: python
from __future__ import unicode_literals
import site
site.addsitedir("/path/to/isso_virtualenv")
import os
from isso import make_app
from isso import dist, config
application = make_app(
config.load(
os.path.join(dist.location, dist.project_name, "defaults.ini"),
"/path/to/isso.cfg",
multiprocessing=True)
Using the aforementioned script will load system modules when available and modules
from the virtualenv otherwise. Should you want the opposite behavior, where modules from
the virtualenv have priority over system modules, the following script does the trick:
.. code-block:: python
from __future__ import unicode_literals
import os
import site
import sys
# Remember original sys.path.
prev_sys_path = list(sys.path)
# Add the new site-packages directory.
site.addsitedir("/path/to/isso_virtualenv")
# Reorder sys.path so new directories at the front.
new_sys_path = []
for item in list(sys.path):
if item not in prev_sys_path:
new_sys_path.append(item)
sys.path.remove(item)
sys.path[:0] = new_sys_path
from isso import make_app
from isso import dist, config
application = make_app(
config.load(
os.path.join(dist.location, dist.project_name, "defaults.ini"),
"/path/to/isso.cfg",
multiprocessing=True)
The last two scripts are based on those given by
`mod_wsgi documentation <https://code.google.com/p/modwsgi/wiki/VirtualEnvironments>`_.
The Apache configuration will then be similar to the following:
.. code-block:: apache
<VirtualHost *>
ServerName example.org
WSGIDaemonProcess isso user=www-data group=www-data threads=5
WSGIScriptAlias /mounted_isso_path /path/to/isso.wsgi
</VirtualHost>
You will need to adjust the user and group according to your Apache installation and
security policy. Be aware that the directory containing the comments database must
be writable by the user or group running the WSGI daemon process: having a writable
database only is not enough, since SQLite will need to create a lock file in the same
directory.
`mod_fastcgi <http://www.fastcgi.com/mod_fastcgi/docs/mod_fastcgi.html>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
You can use this method if your hosting provider doesn't allow you to have long
running processes. If FastCGI has not yet been configured in your server,
please follow these steps:
.. note:: This information may be incorrect, if you have more knowledge on how
to deploy Python via `mod_fastcgi`, consider extending/correcting this section.
For more information, see `Flask: Configuring Apache
<http://flask.pocoo.org/docs/deploying/fastcgi/#configuring-apache>`_.
.. code-block:: apache
LoadModule fastcgi_module /usr/lib64/httpd/modules/mod_fastcgi.so
FastCgiServer /var/www/html/yourapplication/app.fcgi -idle-timeout 300 -processes 5
<VirtualHost *>
ServerName example.org
AddHandler fastcgi-script fcgi
ScriptAlias / /var/www/isso.fcgi
<Location />
SetHandler fastcgi-script
</Location>
</VirtualHost>
Next, to run isso as a FastCGI script you'll need to install ``flup`` with
PIP:
.. code-block:: sh
$ pip install flup
Finally, copy'n'paste to `/var/www/isso.fcgi` (or whatever location you prefer):
.. code-block:: python
#!/usr/bin/env python
#: uncomment if you're using a virtualenv
# import sys
# sys.path.insert(0, '<your_local_path>/lib/python2.7/site-packages')
from isso import make_app, dist, config
import os
from flup.server.fcgi import WSGIServer
application = make_app(config.load(
os.path.join(dist.location, dist.project_name, "defaults.ini"),
"/path/to/isso.cfg"))
WSGIServer(application).run()
`Openshift <http://openshift.com>`__
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
With `Isso Openshift Deployment Kit`_, Isso can be installed on Open
Shift with just one click. Make sure you already have installed ``rhc``
(`instructions`_) and completed the setup.
1. Run the following, you will get an Open Shift instance installed with
Isso:
::
rhc create-app appname python-2.7 --from-code https://github.com/avinassh/isso-openshift.git
2. Above step also clones Git repository of your Open Shift instance, in
current directory. Make changes to the configuration file and push
back to Openshift, it will be redeployed with new settings.
3. Visit ``http://<yourappname>-<openshift-namespace>.com/info`` to
verify Isso is deployed properly and is working.
.. _Isso Openshift Deployment Kit: https://github.com/avinassh/isso-openshift
.. _instructions: https://developers.openshift.com/en/managing-client-tools.html

@ -1,59 +0,0 @@
Overview
========
Welcome to Isso's documentation. This documentation will help you get started
fast. If you run into any problems when using Isso, you can find the answer in
troubleshooting guide or you can ask me on IRC or GitHub.
Documentation overview:
.. toctree::
:maxdepth: 1
install
quickstart
troubleshooting
What's Isso?
------------
Isso is a lightweight commenting server similar to Disqus. It allows anonymous
comments, maintains identity and is simple to administrate. It uses JavaScript
and cross-origin ressource sharing for easy integration into (static) websites.
No, I meant "Isso"
------------------
Isso is an informal, german abbreviation for "Ich schrei sonst!", which can
roughly be translated to "I'm yelling otherwise". It usually ends the
discussion without any further arguments.
In germany, Isso `is also pokémon N° 360`__.
.. __: http://bulbapedia.bulbagarden.net/wiki/Wynaut_(Pok%C3%A9mon)
What's wrong with Disqus?
-------------------------
No anonymous comments (IP address, email and name recorded), hosted in the USA,
third-party. Just like IntenseDebate, livefrye etc. When you embed Disqus, they
can do anything with your readers (and probably mine Bitcoins, see the loading
times).
Isso is not the first open-source commenting server:
* `talkatv <https://github.com/talkatv/talkatv>`_, written in Python.
Unfortunately, talkatv's (read "talkative") development stalled. Neither
anonymous nor threaded comments.
* `Juvia <https://github.com/phusion/juvia>`_, written in Ruby (on Rails).
No threaded comments, nice administration webinterface, but... yeah... Ruby.
* `Tildehash.com <http://www.tildehash.com/?article=why-im-reinventing-disqus>`_,
written in PHP__ (compare to Python__). Did I forgot something?
* `Unobtrusive, self-hosted comments <http://stackoverflow.com/q/2053217>`_,
discussion on StackOverflow.
.. __: http://www.cvedetails.com/vendor/74/PHP.html
.. __: http://www.cvedetails.com/vendor/10210/Python.html

@ -1,274 +0,0 @@
Installation
============
Isso is a web application written in Python. If pip and virtualenv mean anything
to you, continue with :ref:`install-from-pypi`. If you are running
Debian/Ubuntu, Gentoo, Archlinux or Fedora, you can use
:ref:`prebuilt-package`. If not, read the next section carefully.
.. contents::
:local:
:depth: 1
.. _install-interludium:
Interludium: Python is not PHP
------------------------------
If you think hosting a web application written in Python is as easy as one
written in PHP, you are wrong. Unlike for PHP, many Linux distribution use
Python for internal tools. Your package manager already ships several python
libraries, but most likely not all required by Isso (or in an up-to-date
version looking at you, Debian!).
That's why most Python developers use the `Python Package Index`_ to get their
dependencies. The most important rule to follow is to never install *anything* from PyPi
as root. Not because of malicious software, but because you *will* break your
system.
``easy_install`` is one tool to mess up your system. Another package manager is
``pip``. If you ever searched for an issue with Python/pip and Stackoverflow is
suggesting you ``easy_install pip`` or ``pip install --upgrade pip`` (as root
of course!), you are doing it wrong. `Why you should not use Python's
easy_install carelessly on Debian`_ is worth the read.
Fortunately, Python has a way to install packages (both as root and as user)
without interfering with your globally installed packages: `virtualenv`. Use
this *always* if you are installing software unavailable in your favourite
package manager.
.. code-block:: sh
# for Debian/Ubuntu
~> sudo apt-get install python-setuptools python-virtualenv python-dev
# Fedora/Red Hat
~> sudo yum install python-setuptools python-virtualenv python-devel
The next steps should be done as regular user, not as root (although possible
but not recommended):
.. code-block:: sh
~> virtualenv /opt/isso
~> source /opt/isso/bin/activate
After calling `source`, you can now install packages from PyPi locally into this
virtual environment. If you don't like Isso anymore, you just `rm -rf` the
folder. Inside this virtual environment, you may also execute the example
commands from above to upgrade your Python Package Manager (although it barely
makes sense), it is completely independent from your global system.
To use Isso installed in a virtual environment outside of the virtual
environment, you just need to add */opt/isso/bin* to your :envvar:`PATH` or
execute */opt/isso/bin/isso* directly. It will launch Isso from within the
virtual environment.
With a virtualenv active, you may now continue to :ref:`install-from-pypi`!
Of course you may not need a virtualenv when you are running dedicated virtual
machines or a shared host (e.g. Uberspace.de).
.. _Python Package Index: https://pypi.python.org/pypi
.. _Why you should not use Python's easy_install carelessly on Debian:
https://workaround.org/easy-install-debian
.. _install-from-pypi:
Install from PyPi
-----------------
Requirements
^^^^^^^^^^^^
- Python 2.7 or 3.4+ (+ devel headers)
- SQLite 3.3.8 or later
- a working C compiler
For Debian/Ubuntu just `copy and paste
<http://thejh.net/misc/website-terminal-copy-paste>`_ to your terminal:
.. code-block:: sh
~> sudo apt-get install python-dev sqlite3 build-essential
Similar for Fedora (and derivates):
.. code-block:: sh
~> sudo yum install python-devel sqlite
~> sudo yum groupinstall “Development Tools”
Installation
^^^^^^^^^^^^
Install Isso with `pip <http://www.pip-installer.org/en/latest/>`_:
.. code-block:: sh
~> pip install isso
`Don't have pip? <https://twitter.com/gardaud/status/357638468572151808>`_
.. code-block:: sh
~> easy_install isso # cross your fingers
For easier execution, you can symlink the executable to a location in your
:envvar:`PATH`.
.. code-block:: sh
~> ln -s /opt/isso/bin/isso /usr/local/bin/isso
Upgrade
^^^^^^^
To upgrade Isso, activate your virtual environment again, and run
.. code-block:: sh
~> source /opt/isso/bin/activate # optional
~> pip install --upgrade isso
.. _prebuilt-package:
Prebuilt Packages
-----------------
* Debian (since Buster): https://packages.debian.org/search?keywords=isso
* Gentoo: http://eroen.eu/cgit/cgit.cgi/eroen-overlay/tree/www-apps/isso?h=isso
not yet available in Portage, but you can use the ebuild to build Isso.
* Arch Linux: https://aur.archlinux.org/packages/isso/
install with `yaourt isso`.
* Fedora: https://copr.fedoraproject.org/coprs/jujens/isso/ — copr
repository. Built from Pypi, includes a systemctl unit script.
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
-------------------
If you want to hack on Isso or track down issues, there's an alternate
way to set up Isso. It requires a lot more dependencies and effort:
- Python 2.7 or 3.4+ (+ devel headers)
- Virtualenv
- SQLite 3.3.8 or later
- a working C compiler
- Node.js, `NPM <https://npmjs.org/>`__ and `Bower <http://bower.io/>`__
Get a fresh copy of Isso:
.. code-block:: sh
~> git clone https://github.com/posativ/isso.git
~> cd isso/
To create a virtual environment (recommended), run:
.. code-block:: sh
~> virtualenv .
~> source ./bin/activate
Install Isso and its dependencies:
.. code-block:: sh
~> python setup.py develop # or `install`
~> isso run
Install JavaScript modules:
.. code-block:: sh
~> make init
Integration without optimization:
.. code-block:: html
<script src="/js/config.js"></script>
<script data-main="/js/embed" src="/js/components/requirejs/require.js"></script>
Optimization:
.. code-block:: sh
~> npm install -g requirejs uglify-js jade
~> make js
.. _init-scripts:
Init scripts
------------
Init scripts to run Isso as a service (check your distribution's documentation
for your init-system; e.g. Debian uses SysVinit, Fedora uses systemd) if you
don't use FastCGi or uWSGI:
- systemd (Isso + Gunicorn): https://github.com/jgraichen/debian-isso/blob/master/debian/isso.service
- SysVinit (Isso + Gunicorn): https://github.com/jgraichen/debian-isso/blob/master/debian/isso.init
- OpenBSD: https://gist.github.com/noqqe/7397719
- FreeBSD: https://gist.github.com/ckoepp/52f6f0262de04cee1b88ef4a441e276d
- Supervisor: https://github.com/posativ/isso/issues/47
If you're writing your own init script, you can utilize ``start-stop-daemon``
to run Isso in the background (Isso runs in the foreground usually). Below you
will find a very basic SysVinit script which you can use for inspiration:
.. code-block:: sh
#!/bin/sh
### BEGIN INIT INFO
# Provides: isso
# Required-Start: $local_fs $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: lightweight Disqus alternative
### END INIT INFO
EXEC=/opt/isso/bin/isso
EXEC_OPTS="-c /etc/isso.cfg run"
RUNAS=isso
PIDFILE=/var/run/isso.pid
start() {
echo 'Starting service…' >&2
start-stop-daemon --start --user "$RUNAS" --background --make-pidfile --pidfile $PIDFILE \
--exec $EXEC -- $EXEC_OPTS
}
stop() {
echo 'Stopping service…' >&2
start-stop-daemon --stop --user "$RUNAS" --pidfile $PIDFILE --exec $EXEC
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
*)
echo "Usage: $0 {start|stop|restart}"
esac

@ -1,38 +0,0 @@
Isso
====
What's Isso?
------------
Isso is a lightweight commenting server similar to Disqus. It allows anonymous
comments, maintains identity and is simple to administrate. It uses JavaScript
and cross-origin resource sharing for easy integration into static websites.
No, I meant "Isso"
------------------
Isso is an informal, german abbreviation for "Ich schrei sonst!", which can
roughly be translated to "I'm yelling otherwise". It usually ends the
discussion without any further arguments.
In germany, Isso `is also pokémon N° 360`__.
.. __: http://bulbapedia.bulbagarden.net/wiki/Wynaut_(Pok%C3%A9mon)
What's wrong with Disqus?
-------------------------
No anonymous comments (IP address, email and name recorded), hosted in the USA,
third-party. Just like IntenseDebate, livefrye etc. When you embed Disqus, they
can do anything with your readers (and probably mine Bitcoins, see the loading
times).
Setup
-----
.. toctree::
:maxdepth: 1
../quickstart
../troubleshooting

@ -1,154 +0,0 @@
Quickstart
==========
Assuming you have successfully :doc:`installed <install>` Isso, here's
a quickstart guide that covers the most common setup. Sections covered:
.. contents::
:local:
:depth: 1
Configuration
-------------
You must provide a custom configuration to set `dbpath` (your database
location) and `host` (a list of websites for CORS_). All other options have
sane defaults.
.. code-block:: ini
[general]
; database location, check permissions, automatically created if it
does not exist
dbpath = /var/lib/isso/comments.db
; your website or blog (not the location of Isso!)
host = http://example.tld/
; you can add multiple hosts for local development
; or SSL connections. There is no wildcard to allow
; any domain.
host =
http://localhost:1234/
http://example.tld/
https://example.tld/
Note, that multiple, *different* websites are **not** supported in a single
configuration. To serve comments for different websites, refer to
:ref:`Multiple Sites <configure-multiple-sites>`.
The moderation is done with signed URLs sent by email or logged to stdout.
By default, comments are accepted and immediately shown to other users. To
enable moderation queue, add:
.. code-block:: ini
[moderation]
enabled = true
To moderate comments, either use the activation or deletion URL in the logs or
:ref:`use SMTP <configure-smtp>` to get notified of new comments, including the
URLs for activation and deletion:
.. code-block:: ini
[general]
notify = smtp
[smtp]
; SMTP settings
For more options, see :doc:`server <configuration/server>` and :doc:`client
<configuration/client>` configuration.
Migration
---------
Isso provides a tool for importing comments from Disqus_ or WordPress_.
You can also import comments from any other comment system, but this topic is more
complex and is covered in :doc:`advanced migration <extras/advanced-migration>`.
To export your comments from Disqus, log into Disqus, go to your website, click
on *Discussions* and select the *Export* tab. You'll receive an email with your
comments. Unfortunately, Disqus does not export up- and downvotes.
To export comments from your previous WordPress installation, go to *Tools*,
export your data. It has been reported that WordPress may generate broken XML.
Try to repair the file using ``xmllint`` before you continue with the import.
Now import the XML dump:
.. code-block:: sh
~> isso -c /path/to/isso.cfg import -t [disqus|wordpress] disqus-or-wordpress.xml
[100%] 53 threads, 192 comments
.. _Disqus: https://disqus.com/
.. _WordPress: https://wordpress.org/
Running Isso
------------
To run Isso, simply execute:
.. code-block:: sh
$ isso -c /path/to/isso.cfg run
2013-11-25 15:31:34,773 INFO: connected to HTTP server
Next, we configure Nginx_ to proxy Isso. Do not run Isso on a public interface!
A popular but often error-prone (because of CORS_) setup to host Isso uses a
dedicated domain such as ``comments.example.tld``.
Assuming both, your website and Isso are on the same server, the nginx
configuration looks like this:
.. code-block:: nginx
server {
listen [::]:80 default ipv6only=off;
server_name example.tld;
root ...;
}
server {
listen [::]:80;
server_name comments.example.tld;
location / {
proxy_pass http://localhost:8080;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
Integration
-----------
Now, you embed Isso to your website:
.. code-block:: html
<script data-isso="//comments.example.tld/"
src="//comments.example.tld/js/embed.min.js"></script>
<section id="isso-thread"></section>
Note, that `data-isso` is optional, but when a website includes a script using
``async`` it is no longer possible to determine the script's external URL.
That's it. When you open your website, you should see a commenting form. Leave
a comment to see if the setup works. If not, see :doc:`troubleshooting`.
Going Further
-------------
There are several server and client configuration options not covered in this
quickstart, check out :doc:`configuration/server` and
:doc:`configuration/client` for more information. For further website
integration, see :doc:`extras/advanced-integration`.
To launch Isso automatically, check the :ref:`init-scripts` section from the
installation guide. A different approach to deploy a web application is
written here: :doc:`Deployment of Isso <extras/deployment>`.
.. _Nginx: http://nginx.org/
.. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS

@ -1,62 +0,0 @@
.. _configure-multiple-sites:
Multiple Sites
--------------
Isso is designed to serve comments for a single website and therefore stores
comments for a relative URL. This is done to support HTTP, HTTPS and even domain transfers
without manual intervention. You can chain Isso to support multiple
websites on different domains.
The following example uses `gunicorn <http://gunicorn.org/>`_ as WSGI server (
you can use uWSGI as well). Let's say you maintain two websites, like
foo.example and other.bar:
.. code-block:: sh
$ cat /etc/isso.d/foo.example.cfg
[general]
name = foo
host = http://foo.example/
dbpath = /var/lib/isso/foo.example.db
$ cat /etc/isso.d/other.bar.cfg
[general]
name = bar
host = http://other.bar/
dbpath = /var/lib/isso/other.bar.db
Then you run Isso with gunicorn (separate multiple configuration files by
semicolon):
.. code-block:: sh
$ export ISSO_SETTINGS="/etc/isso.d/foo.example.cfg;/etc/isso.d/other.bar.cfg"
$ gunicorn isso.dispatch -b localhost:8080
In your webserver configuration, proxy Isso as usual:
.. code-block:: nginx
server {
listen [::]:80;
server_name comments.example;
location / {
proxy_pass http://localhost:8080;
}
}
When you now visit http://comments.example/, you will see your different Isso
configuration separated by /`name`.
.. code-block:: text
$ curl http://comments.example/
/foo
/bar
Just embed the JavaScript including the new relative path, e.g.
*http://comments.example/foo/js/embed.min.js*. Make sure, you don't mix the
URLs on both sites as it will most likely cause CORS-related errors.

@ -1,29 +0,0 @@
Sub-URI
=======
You can run Isso on the same domain as your website, which circumvents issues
originating from CORS_. Also, privacy-protecting browser addons such as
`Request Policy`_ wont block comments.
.. code-block:: nginx
server {
listen [::]:80;
listen [::]:443 ssl;
server_name example.tld;
root /var/www/example.tld;
location /isso {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Script-Name /isso;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://localhost:8080;
}
}
Now, the website integration is just as described in :doc:`../quickstart` but
with a different location.
.. _CORS: https://developer.mozilla.org/en/docs/HTTP/Access_control_CORS
.. _Request Policy: https://www.requestpolicy.com/

@ -1,39 +0,0 @@
Troubleshooting
===============
For uberspace users
-------------------
Some uberspace users experienced problems with isso and they solved their
issues by adding `DirectoryIndex disabled` as the first line in the `.htaccess`
file for the domain the isso server is running on.
pkg_ressources.DistributionNotFound
-----------------------------------
This is usually caused by messing up the system's Python with newer packages
from PyPi (e.g. by executing `easy_install --upgrade pip` as root) and is not
related to Isso at all.
Install Isso in a virtual environment as described in
:ref:`install-interludium`. Alternatively, you can use `pip install --user`
to install Isso into the user's home.
UnicodeDecodeError: 'ascii' codec can't decode byte 0xff
--------------------------------------------------------
Likely an issue with your environment, check you set your preferred file
encoding either in :envvar:`LANG`, :envvar:`LANGUAGE`, :envvar:`LC_ALL` or
:envvar:`LC_CTYPE`:
.. code-block:: text
$ env LANG=C.UTF-8 isso [-h] [--version] ...
If none of the mentioned variables are set, the interaction with Isso will
likely fail (unable to print non-ascii characters to stdout/err, unable to
parse configuration file with non-ascii characters and so forth).
The web console shows 404 Not Found responses
---------------------------------------------
That's fine. Isso returns "404 Not Found" to indicate "No comments".

@ -1,2 +0,0 @@
[html4css1 writer]
initial_header_level: 2

@ -1,17 +0,0 @@
Frequently asked question
=========================
Why SQLite3?
------------
Although partially answered on the index page, here is a more complete answer: If
you manage massive amounts of comments, Isso is a really bad choice. Isso is
designed to be simple and easy to setup, it is not optimized for high-traffic
websites (use a `dedicated Disqus`_ instance then).
Comments are not big data.
For example, if you have 209 threads and 778 comments in total this only needs 620 kilobytes
of memory. This is an excellent use case for SQLite.
.. _dedicated Disqus:

@ -1,72 +0,0 @@
{%- extends "layout.html" %}
{% set title = "Isso a commenting server similar to Disqus" %}
{% block extrahead %}
<script data-isso="//posativ.org/isso/api/"
src="//posativ.org/isso/api/js/embed.min.js"></script>
{% endblock %}
{% block header %}
<div class="index">
<figure class="duty-call">
<a href="http://xkcd.com/386/">
<img src="{{ pathto('_static/duty_calls.png', 1) }}" alt="XKCD Duty Calls"/>
</a>
<figcaption>by Randall Munroe, CC BY-NC 2.5</figcaption>
</figure>
<ul>
<li>
<p><strong>Comments written in Markdown</strong></p>
<p>Users can edit or delete own comments (within 15 minutes by
default).</p>
<p>Comments in moderation queue are not publicly visible before
activation.</p>
</li>
<li>
<p><strong>SQLite backend</strong></p>
<p>Because comments are not Big Data.</p>
</li>
<li>
<p><strong>Disqus &amp; WordPress Import</strong></p>
<p>You can migrate your Disqus/WordPress comments without any hassle.</p>
</li>
<li>
<p><strong>Configurable JS client</strong></p>
<p>Embed a single JS file, 40kb (12kb gzipped) and you are
done.</p>
<p>Supports Firefox, Safari, Chrome and IE10.</p>
</li>
</ul>
</div>
{% endblock %}
{% block body %}
<div class="links">
<h2>Links</h2>
<ul>
<li><a href="https://pypi.python.org/pypi/isso/">Isso @ PyPi</a></li>
<li><a href="https://github.com/posativ/isso/">Isso @ GitHub</a></li>
</ul>
<h2>Help</h2>
<p>
Join <a href="http://webchat.freenode.net/?channels=isso">
<code>#isso</code></a> on <a href="http://freenode.net/">Freenode</a>
or open an issue on <a href="https://github.com/posativ/isso/issues">GitHub</a>.
</p>
<p>
Or join the <a href="http://librelist.com/browser/isso/">mailing list</a>,
just send an email to <a href="mailto:isso@librelist.com">isso@librelist.com</a>.
</p>
<h2>Contribute</h2>
<p>
<a href="{{ pathto('contribute') }}">Write Code</a>,
<a href="https://www.transifex.com/projects/p/isso/">Translate</a> or
<a href="https://flattr.com/thing/2059355/posativisso-on-GitHub">Flattr
<img src="{{ pathto('_static/flattr.png', 1) }}" alt="Flattr icon"/></a>.
</p>
{% include "searchbox.html" %}
</div>
<div class="demo">
<h2>Demo</h2>
<section id="isso-thread"></section>
</div>
{% endblock %}

@ -1,4 +0,0 @@
News
====
To be written.

@ -1,10 +0,0 @@
Releasing steps
===============
* Run ``python3 setup.py nosetests``, ``python2 setup.py nosetests``
* Update version number in ``setup.py`` and ``CHANGES.rst``
* ``git commit -m "Preparing ${VERSION}" setup.py CHANGES.rst``
* ``git tag -as ${VERSION}``
* ``make init all``
* ``python3 setup.py sdist``
* ``twine upload --sign dist/isso-${VERSION}.tar.gz``

@ -0,0 +1,8 @@
[uwsgi]
http = :8080
master = true
processes = 4
spooler = %v/mail
module = isso
virtualenv = .
env = ISSO_SETTINGS=%v/sample.cfg

@ -3,7 +3,7 @@
#
# The MIT License (MIT)
#
# Copyright (c) 2012-2014 Martin Zimmermann.
# Copyright (c) 2012-2013 Martin Zimmermann.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@ -25,56 +25,43 @@
#
# Isso a lightweight Disqus alternative
from __future__ import print_function, unicode_literals
from __future__ import print_function
import pkg_resources
dist = pkg_resources.get_distribution("isso")
# check if exectuable is `isso` and gevent is available
import sys
if sys.argv[0].startswith("isso"):
try:
import gevent.monkey
gevent.monkey.patch_all()
except ImportError:
pass
import os
import errno
import socket
import logging
import tempfile
from os.path import dirname, join
from argparse import ArgumentParser
from functools import partial, reduce
import pkg_resources
werkzeug = pkg_resources.get_distribution("werkzeug")
try:
import httplib
except ImportError:
import http.client as httplib
import misaka
from itsdangerous import URLSafeTimedSerializer
from werkzeug.routing import Map
from werkzeug.exceptions import HTTPException, InternalServerError
from werkzeug.routing import Map, Rule
from werkzeug.wrappers import Response, Request
from werkzeug.exceptions import HTTPException, NotFound, MethodNotAllowed
from werkzeug.middleware.shared_data import SharedDataMiddleware
from werkzeug.local import Local, LocalManager
from werkzeug.wsgi import SharedDataMiddleware
from werkzeug.serving import run_simple
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.middleware.profiler import ProfilerMiddleware
local = Local()
local_manager = LocalManager([local])
from werkzeug.contrib.fixers import ProxyFix
from isso import config, db, migrate, wsgi, ext, views
from isso.core import ThreadedMixin, ProcessMixin, uWSGIMixin
from isso.wsgi import origin, urlsplit
from isso.utils import http, JSONRequest, html, hash
from isso.views import comments
from jinja2 import Environment, FileSystemLoader
from isso.ext.notifications import Stdout, SMTP
from isso import db, migrate, views, wsgi
from isso.core import ThreadedMixin, uWSGIMixin, Config
from isso.utils import parse, http
from isso.views import comment, admin
logging.getLogger('werkzeug').setLevel(logging.WARN)
logging.getLogger('werkzeug').setLevel(logging.ERROR)
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s: %(message)s")
@ -82,140 +69,116 @@ logging.basicConfig(
logger = logging.getLogger("isso")
class ProxyFixCustom(ProxyFix):
def __init__(self, app):
# This is needed for werkzeug.wsgi.get_current_url called in isso/views/comments.py
# to work properly when isso is hosted under a sub-path
# cf. https://werkzeug.palletsprojects.com/en/1.0.x/middleware/proxy_fix/
super().__init__(app, x_prefix=1)
rules = Map([
Rule('/new', methods=['POST'], endpoint=views.comment.new),
Rule('/id/<int:id>', methods=['GET', 'PUT', 'DELETE'], endpoint=views.comment.single),
Rule('/id/<int:id>/like', methods=['POST'], endpoint=views.comment.like),
Rule('/id/<int:id>/dislike', methods=['POST'], endpoint=views.comment.dislike),
Rule('/', methods=['GET'], endpoint=views.comment.fetch),
Rule('/count', methods=['GET'], endpoint=views.comment.count),
Rule('/delete/<string:auth>', endpoint=views.comment.delete),
Rule('/activate/<string:auth>', endpoint=views.comment.activate),
Rule('/admin/', endpoint=views.admin.index),
Rule('/check-ip', endpoint=views.comment.checkip)
])
class Isso(object):
salt = b"Eech7co8Ohloopo9Ol6baimi"
def __init__(self, conf):
self.conf = conf
self.db = db.SQLite3(conf.get('general', 'dbpath'), conf)
self.signer = URLSafeTimedSerializer(
self.db.preferences.get("session-key"))
self.markup = html.Markup(conf.section('markup'))
self.hasher = hash.new(conf.section("hash"))
self.signer = URLSafeTimedSerializer(conf.get('general', 'session-key'))
self.j2env = Environment(loader=FileSystemLoader(join(dirname(__file__), 'templates/')))
super(Isso, self).__init__(conf)
subscribers = []
smtp_backend = False
for backend in conf.getlist("general", "notify"):
if backend == "stdout":
subscribers.append(Stdout(None))
elif backend in ("smtp", "SMTP"):
smtp_backend = True
else:
logger.warn("unknown notification backend '%s'", backend)
if smtp_backend or conf.getboolean("general", "reply-notifications"):
subscribers.append(SMTP(self))
self.signal = ext.Signal(*subscribers)
self.urls = Map()
views.Info(self)
comments.API(self, self.hasher)
def render(self, text):
return self.markup.render(text)
def sign(self, obj):
return self.signer.dumps(obj)
def unsign(self, obj, max_age=None):
return self.signer.loads(obj, max_age=max_age or self.conf.getint('general', 'max-age'))
def dispatch(self, request):
local.request = request
def markdown(self, text):
return misaka.html(text, extensions=misaka.EXT_STRIKETHROUGH \
| misaka.EXT_SUPERSCRIPT | misaka.EXT_AUTOLINK \
| misaka.HTML_SKIP_HTML | misaka.HTML_SKIP_IMAGES | misaka.HTML_SAFELINK)
local.host = wsgi.host(request.environ)
local.origin = origin(self.conf.getiter(
"general", "host"))(request.environ)
adapter = self.urls.bind_to_environ(request.environ)
def render(self, tt, **ctx):
tt = self.j2env.get_template(tt)
return tt.render(**ctx)
def dispatch(self, request, start_response):
adapter = rules.bind_to_environ(request.environ)
try:
handler, values = adapter.match()
return handler(self, request.environ, request, **values)
except NotFound:
return Response('Not Found', 404)
except MethodNotAllowed:
return Response("Yup.", 200)
except HTTPException as e:
return e
else:
try:
response = handler(request.environ, request, **values)
except HTTPException as e:
return e
except Exception:
logger.exception("%s %s", request.method,
request.environ["PATH_INFO"])
return InternalServerError()
else:
return response
def wsgi_app(self, environ, start_response):
response = self.dispatch(JSONRequest(environ))
response = self.dispatch(Request(environ), start_response)
# add CORS header
if hasattr(response, 'headers') and 'HTTP_ORIGN' in environ:
for host in self.conf.getiter('general', 'host'):
if environ["HTTP_ORIGIN"] == host.rstrip("/"):
origin = host.rstrip("/")
break
else:
origin = host.rstrip("/")
hdrs = response.headers
hdrs["Access-Control-Allow-Origin"] = origin
hdrs["Access-Control-Allow-Headers"] = "Origin, Content-Type"
hdrs["Access-Control-Allow-Credentials"] = "true"
hdrs["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
return response(environ, start_response)
def __call__(self, environ, start_response):
return self.wsgi_app(environ, start_response)
def make_app(conf=None, threading=True, multiprocessing=False, uwsgi=False):
if not any((threading, multiprocessing, uwsgi)):
raise RuntimeError("either set threading, multiprocessing or uwsgi")
def make_app(conf=None):
if threading:
try:
import uwsgi
except ImportError:
class App(Isso, ThreadedMixin):
pass
elif multiprocessing:
class App(Isso, ProcessMixin):
pass
else:
class App(Isso, uWSGIMixin):
pass
isso = App(conf)
# check HTTP server connection
for host in conf.getiter("general", "host"):
with http.curl('HEAD', host, '/', 5) as resp:
if resp is not None:
logger.info("connected to %s", host)
logger.info("connected to HTTP server")
break
else:
logger.warn("unable to connect to your website, Isso will probably not "
"work correctly. Please make sure, Isso can reach your "
"website via HTTP(S).")
wrapper = [local_manager.make_middleware]
if isso.conf.getboolean("server", "profile"):
wrapper.append(partial(ProfilerMiddleware,
sort_by=("cumulative", ), restrictions=("isso/(?!lib)", 10)))
logger.warn("unable to connect to HTTP server")
wrapper.append(partial(SharedDataMiddleware, exports={
app = ProxyFix(wsgi.SubURI(SharedDataMiddleware(isso.wsgi_app, {
'/static': join(dirname(__file__), 'static/'),
'/js': join(dirname(__file__), 'js/'),
'/css': join(dirname(__file__), 'css/'),
'/img': join(dirname(__file__), 'img/'),
'/demo': join(dirname(__file__), 'demo/')
}))
wrapper.append(partial(wsgi.CORSMiddleware,
origin=origin(isso.conf.getiter("general", "host")),
allowed=("Origin", "Referer", "Content-Type"),
exposed=("X-Set-Cookie", "Date")))
wrapper.extend([wsgi.SubURI, ProxyFixCustom])
'/css': join(dirname(__file__), 'css/')
})))
if werkzeug.version.startswith("0.8"):
wrapper.append(wsgi.LegacyWerkzeugMiddleware)
return reduce(lambda x, f: f(x), wrapper, isso)
return app
def main():
@ -223,67 +186,29 @@ def main():
parser = ArgumentParser(description="a blog comment hosting service")
subparser = parser.add_subparsers(help="commands", dest="command")
parser.add_argument('--version', action='version',
version='%(prog)s ' + dist.version)
parser.add_argument('--version', action='version', version='%(prog)s ' + dist.version)
parser.add_argument("-c", dest="conf", default="/etc/isso.conf",
metavar="/etc/isso.conf", help="set configuration file")
metavar="/etc/isso.conf", help="set configuration file")
imprt = subparser.add_parser('import', help="import Disqus XML export")
imprt.add_argument("dump", metavar="FILE")
imprt.add_argument("-n", "--dry-run", dest="dryrun", action="store_true",
help="perform a trial run with no changes made")
imprt.add_argument("-t", "--type", dest="type", default=None,
choices=["disqus", "wordpress", "generic"], help="export type")
imprt.add_argument("--empty-id", dest="empty_id", action="store_true",
help="workaround for weird Disqus XML exports, #135")
# run Isso as stand-alone server
subparser.add_parser("run", help="run server")
serve = subparser.add_parser("run", help="run server")
args = parser.parse_args()
conf = config.load(
join(dist.location, dist.project_name, "defaults.ini"), args.conf)
conf = Config.load(args.conf)
if args.command == "import":
conf.set("guard", "enabled", "off")
if args.dryrun:
xxx = tempfile.NamedTemporaryFile()
dbpath = xxx.name
else:
dbpath = conf.get("general", "dbpath")
mydb = db.SQLite3(dbpath, conf)
migrate.dispatch(args.type, mydb, args.dump, args.empty_id)
migrate.disqus(db.SQLite3(conf.get('general', 'dbpath'), conf), args.dump)
sys.exit(0)
if conf.get("general", "log-file"):
handler = logging.FileHandler(conf.get("general", "log-file"))
logger.addHandler(handler)
logging.getLogger("werkzeug").addHandler(handler)
run_simple(conf.get('server', 'host'), conf.getint('server', 'port'), make_app(conf),
threaded=True, use_reloader=conf.getboolean('server', 'reload'))
logger.propagate = False
logging.getLogger("werkzeug").propagate = False
if not any(conf.getiter("general", "host")):
logger.error("No website(s) configured, Isso won't work.")
sys.exit(1)
if conf.get("server", "listen").startswith("http://"):
host, port, _ = urlsplit(conf.get("server", "listen"))
try:
from gevent.pywsgi import WSGIServer
WSGIServer((host, port), make_app(conf)).serve_forever()
except ImportError:
run_simple(host, port, make_app(conf), threaded=True,
use_reloader=conf.getboolean('server', 'reload'))
else:
sock = conf.get("server", "listen").partition("unix://")[2]
try:
os.unlink(sock)
except OSError as ex:
if ex.errno != errno.ENOENT:
raise
wsgi.SocketHTTPServer(sock, make_app(conf)).serve_forever()
try:
import uwsgi
except ImportError:
pass
else:
application = make_app(Config.load(os.environ.get('ISSO_SETTINGS')))

@ -1,26 +1,22 @@
# -*- encoding: utf-8 -*-
try:
text_type = unicode # Python 2
string_types = (str, unicode)
PY2K = True
except NameError: # Python 3
PY2K = False
import sys
PY2K = sys.version_info[0] == 2
if not PY2K:
map, zip, filter = map, zip, filter
text_type = str
string_types = (str, )
if not PY2K:
buffer = memoryview
filter, map, zip = filter, map, zip
def iteritems(dikt):
return iter(dikt.items()) # noqa: E731
from functools import reduce
else:
buffer = buffer
from itertools import ifilter, imap, izip
filter, map, zip = ifilter, imap, izip
def iteritems(dikt):
return dikt.iteritems() # noqa: E731
reduce = reduce
from itertools import imap, izip, ifilter
map, zip, filter = imap, izip, ifilter
text_type = unicode
string_types = (str, unicode)
buffer = buffer

@ -1,152 +0,0 @@
# -*- encoding: utf-8 -*-
from __future__ import unicode_literals
import re
import logging
import datetime
from email.utils import parseaddr, formataddr
try:
from backports.configparser import ConfigParser
except ImportError:
from configparser import ConfigParser
from isso.compat import text_type as str
logger = logging.getLogger("isso")
def timedelta(string):
"""
Parse :param string: into :class:`datetime.timedelta`, you can use any
(logical) combination of Nw, Nd, Nh and Nm, e.g. `1h30m` for 1 hour, 30
minutes or `3w` for 3 weeks.
Raises a ValueError if the input is invalid/unparseable.
>>> print(timedelta("3w"))
21 days, 0:00:00
>>> print(timedelta("3w 12h 57m"))
21 days, 12:57:00
>>> print(timedelta("1h30m37s"))
1:30:37
>>> print(timedelta("1asdf3w"))
Traceback (most recent call last):
...
ValueError: invalid human-readable timedelta
"""
keys = ["weeks", "days", "hours", "minutes", "seconds"]
regex = "".join(["((?P<%s>\\d+)%s ?)?" % (k, k[0]) for k in keys])
kwargs = {}
for k, v in re.match(regex, string).groupdict(default="0").items():
kwargs[k] = int(v)
rv = datetime.timedelta(**kwargs)
if rv == datetime.timedelta():
raise ValueError("invalid human-readable timedelta")
return datetime.timedelta(**kwargs)
class Section(object):
"""A wrapper around :class:`IssoParser` that returns a partial configuration
section object.
>>> conf = new({"foo": {"bar": "spam"}})
>>> section = conf.section("foo")
>>> conf.get("foo", "bar") == section.get("bar")
True
"""
def __init__(self, conf, section):
self.conf = conf
self.section = section
def get(self, key):
return self.conf.get(self.section, key)
def getint(self, key):
return self.conf.getint(self.section, key)
def getlist(self, key):
return self.conf.getlist(self.section, key)
def getiter(self, key):
return self.conf.getiter(self.section, key)
def getboolean(self, key):
return self.conf.getboolean(self.section, key)
class IssoParser(ConfigParser):
"""Parse INI-style configuration with some modifications for Isso.
* parse human-readable timedelta such as "15m" as "15 minutes"
* handle indented lines as "lists"
"""
def getint(self, section, key):
try:
delta = timedelta(self.get(section, key))
except ValueError:
return super(IssoParser, self).getint(section, key)
else:
try:
return int(delta.total_seconds())
except AttributeError:
return int(delta.total_seconds())
def getlist(self, section, key):
return list(map(str.strip, self.get(section, key).split(',')))
def getiter(self, section, key):
for item in map(str.strip, self.get(section, key).split('\n')):
if item:
yield item
def section(self, section):
return Section(self, section)
def new(options=None):
cp = IssoParser(allow_no_value=True)
if options:
cp.read_dict(options)
return cp
def load(default, user=None):
# return set of (section, option)
def setify(cp):
return set((section, option) for section in cp.sections()
for option in cp.options(section))
parser = new()
parser.read(default)
a = setify(parser)
if user:
parser.read(user)
for item in setify(parser).difference(a):
logger.warn("no such option: [%s] %s", *item)
if item in (("server", "host"), ("server", "port")):
logger.warn("use `listen = http://$host:$port` instead")
if item == ("smtp", "ssl"):
logger.warn("use `security = none | starttls | ssl` instead")
if item == ("general", "session-key"):
logger.info("Your `session-key` has been stored in the "
"database itself, this option is now unused")
if not parseaddr(parser.get("smtp", "from"))[0]:
parser.set("smtp", "from",
formataddr(("Ich schrei sonst!", parser.get("smtp", "from"))))
return parser

@ -2,10 +2,17 @@
from __future__ import print_function
import io
import os
import time
import logging
import binascii
import threading
import multiprocessing
import logging
import socket
import smtplib
from configparser import ConfigParser
try:
import uwsgi
@ -19,35 +26,121 @@ if PY2K:
else:
import _thread as thread
from flask_caching.backends.null import NullCache
from flask_caching.backends.simple import SimpleCache
from isso import notify
from isso.utils import parse
from isso.compat import text_type as str
logger = logging.getLogger("isso")
class Cache:
"""Wrapper around werkzeug's cache class, to make it compatible to
uWSGI's cache framework.
class IssoParser(ConfigParser):
"""
Extended :class:`ConfigParser` to parse human-readable timedeltas
into seconds and handles multiple values per key.
>>> import io
>>> parser = IssoParser(allow_no_value=True)
>>> parser.read_file(io.StringIO(u'''
... [foo]
... bar = 1h
... baz = 12
... bla =
... spam
... ham
... asd = fgh
... '''))
>>> parser.getint("foo", "bar")
3600
>>> parser.getint("foo", "baz")
12
>>> list(parser.getiter("foo", "bla")) # doctest: +IGNORE_UNICODE
['spam', 'ham']
>>> list(parser.getiter("foo", "asd")) # doctest: +IGNORE_UNICODE
['fgh']
"""
def __init__(self, cache):
self.cache = cache
@classmethod
def _total_seconds(cls, td):
return (td.microseconds + (td.seconds + td.days * 24 * 3600) * 10**6) / 10**6
def getint(self, section, key):
try:
delta = parse.timedelta(self.get(section, key))
except ValueError:
return super(IssoParser, self).getint(section, key)
else:
try:
return int(delta.total_seconds())
except AttributeError:
return int(IssoParser._total_seconds(delta))
def getiter(self, section, key):
for item in map(str.strip, self.get(section, key).split('\n')):
if item:
yield item
class Config:
default = [
"[general]",
"dbpath = /tmp/isso.db", "session-key = %r" % binascii.b2a_hex(os.urandom(24)),
"host = http://localhost:8080/", "max-age = 15m",
"[moderation]",
"enabled = false",
"purge-after = 30d",
"[server]",
"host = localhost", "port = 8080", "reload = off",
"[SMTP]",
"username = ", "password = ",
"host = localhost", "port = 465", "ssl = on",
"to = ", "from = ",
"[guard]",
"enabled = true",
"ratelimit = 2"
""
]
@classmethod
def load(cls, configfile):
# return set of (section, option)
setify = lambda cp: set((section, option) for section in cp.sections()
for option in cp.options(section))
rv = IssoParser(allow_no_value=True)
rv.read_file(io.StringIO(u'\n'.join(Config.default)))
a = setify(rv)
def get(self, cache, key):
return self.cache.get(key)
if configfile:
rv.read(configfile)
def set(self, cache, key, value):
return self.cache.set(key, value)
diff = setify(rv).difference(a)
def delete(self, cache, key):
return self.cache.delete(key)
if diff:
for item in diff:
logger.warn("no such option: [%s] %s", *item)
return rv
def SMTP(conf):
try:
mailer = notify.SMTPMailer(conf)
logger.info("connected to SMTP server")
except (socket.error, smtplib.SMTPException):
logger.warn("unable to connect to SMTP server")
mailer = notify.NullMailer()
return mailer
class Mixin(object):
def __init__(self, conf):
self.lock = threading.Lock()
self.cache = Cache(NullCache())
def notify(self, subject, body, retries=5):
pass
@ -73,7 +166,19 @@ class ThreadedMixin(Mixin):
if conf.getboolean("moderation", "enabled"):
self.purge(conf.getint("moderation", "purge-after"))
self.cache = Cache(SimpleCache(threshold=1024, default_timeout=3600))
self.mailer = SMTP(conf)
@threaded
def notify(self, subject, body, retries=5):
for x in range(retries):
try:
self.mailer.sendmail(subject, body)
except Exception:
time.sleep(60)
else:
break
@threaded
def purge(self, delta):
@ -83,51 +188,39 @@ class ThreadedMixin(Mixin):
time.sleep(delta)
class ProcessMixin(ThreadedMixin):
class uWSGIMixin(Mixin):
def __init__(self, conf):
super(ProcessMixin, self).__init__(conf)
self.lock = multiprocessing.Lock()
class uWSGICache(object):
"""Uses uWSGI Caching Framework. INI configuration:
.. code-block:: ini
cache2 = name=hash,items=1024,blocksize=32
"""
@classmethod
def get(self, cache, key):
return uwsgi.cache_get(key, cache)
@classmethod
def set(self, cache, key, value):
uwsgi.cache_set(key, value, 3600, cache)
@classmethod
def delete(self, cache, key):
uwsgi.cache_del(key, cache)
super(uWSGIMixin, self).__init__(conf)
class Lock():
class uWSGIMixin(Mixin):
def __enter__(self):
uwsgi.lock()
def __init__(self, conf):
def __exit__(self, exc_type, exc_val, exc_tb):
uwsgi.unlock()
super(uWSGIMixin, self).__init__(conf)
def spooler(args):
try:
self.mailer.sendmail(args["subject"].decode('utf-8'), args["body"].decode('utf-8'))
except smtplib.SMTPConnectError:
return uwsgi.SPOOL_RETRY
else:
return uwsgi.SPOOL_OK
self.lock = multiprocessing.Lock()
self.cache = uWSGICache
self.lock = Lock()
self.mailer = SMTP(conf)
uwsgi.spooler = spooler
timedelta = conf.getint("moderation", "purge-after")
def purge(signum):
return self.db.comments.purge(timedelta)
purge = lambda signum: self.db.comments.purge(timedelta)
uwsgi.register_signal(1, "", purge)
uwsgi.add_timer(1, timedelta)
# run purge once
purge(1)
def notify(self, subject, body, retries=5):
uwsgi.spool({"subject": subject.encode('utf-8'), "body": body.encode('utf-8')})

@ -0,0 +1,90 @@
# -*- encoding: utf-8 -*-
#
# Copyright (c) Django Software Foundation and individual contributors.
# All rights reserved.
import hmac
import struct
import base64
import hashlib
import binascii
import operator
from functools import reduce
def _bin_to_long(x):
"""
Convert a binary string into a long integer
This is a clever optimization for fast xor vector math
"""
return int(binascii.hexlify(x), 16)
def _long_to_bin(x, hex_format_string):
"""
Convert a long integer into a binary string.
hex_format_string is like "%020x" for padding 10 characters.
"""
return binascii.unhexlify((hex_format_string % x).encode('ascii'))
def _fast_hmac(key, msg, digest):
"""
A trimmed down version of Python's HMAC implementation.
This function operates on bytes.
"""
dig1, dig2 = digest(), digest()
if len(key) > dig1.block_size:
key = digest(key).digest()
key += b'\x00' * (dig1.block_size - len(key))
dig1.update(key.translate(hmac.trans_36))
dig1.update(msg)
dig2.update(key.translate(hmac.trans_5C))
dig2.update(dig1.digest())
return dig2
def _pbkdf2(password, salt, iterations, dklen=0, digest=None):
"""
Implements PBKDF2 as defined in RFC 2898, section 5.2
HMAC+SHA256 is used as the default pseudo random function.
Right now 10,000 iterations is the recommended default which takes
100ms on a 2.2Ghz Core 2 Duo. This is probably the bare minimum
for security given 1000 iterations was recommended in 2001. This
code is very well optimized for CPython and is only four times
slower than openssl's implementation.
"""
assert iterations > 0
if not digest:
digest = hashlib.sha1
password = b'' + password
salt = b'' + salt
hlen = digest().digest_size
if not dklen:
dklen = hlen
if dklen > (2 ** 32 - 1) * hlen:
raise OverflowError('dklen too big')
l = -(-dklen // hlen)
r = dklen - (l - 1) * hlen
hex_format_string = "%%0%ix" % (hlen * 2)
def F(i):
def U():
u = salt + struct.pack(b'>I', i)
for j in range(int(iterations)):
u = _fast_hmac(password, u, digest).digest()
yield _bin_to_long(u)
return _long_to_bin(reduce(operator.xor, U()), hex_format_string)
T = [F(x) for x in range(1, l + 1)]
return b''.join(T[:-1]) + T[-1][:r]
pbkdf2 = lambda text, salt, iterations, dklen: base64.b16encode(
_pbkdf2(text.encode('utf-8'), salt, iterations, dklen)).lower()

@ -1,134 +0,0 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
h1, h2, h3, h4, h5, h6 {
font-style: normal;
font-weight: normal;
}
input {
text-align: center;
}
.header::before, .header::after {
content: " ";
display: table;
}
.header::after {
clear: both;
}
.header::before, .header::after {
content: " ";
display: table;
}
.header {
margin-left: auto;
margin-right: auto;
max-width: 68em;
padding-bottom: 1em;
padding-top: 1em;
}
.header header {
display: block;
float: left;
font-weight: normal;
margin-right: 16.0363%;
width: 41.9818%;
}
.header header .logo {
float: left;
max-height: 60px;
padding-right: 12px;
}
.header header h1 {
font-size: 1.55em;
margin-bottom: 0.3em;
}
.header header h2 {
font-size: 1.05em;
}
.header a, .header a:visited {
color: #4d4c4c;
text-decoration: none;
}
.outer {
background-color: #eeeeee;
box-shadow: 0 0 0.5em #c0c0c0 inset;
}
.outer .filters::before, .outer .filters::after {
content: " ";
display: table;
}
.outer .filters::after {
clear: both;
}
.outer .filters::before, .outer .filters::after {
content: " ";
display: table;
}
.outer .filters {
margin-left: auto;
margin-right: auto;
max-width: 68em;
padding: 1em;
}
a {
text-decoration: none;
color: #4d4c4c;
}
.label {
background-color: #ddd;
border: 1px solid #ccc;
border-radius: 2px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
cursor: pointer;
line-height: 1.4em;
outline: 0 none;
padding: calc(0.6em - 1px);
}
.active {
box-shadow: 2px 2px 2px rgba(0, 0, 0, 0.6) inset;
}
.label-valid {
background-color: #cfc;
border-color: #cfc;
}
.label-pending {
background-color: #ffc;
border-color: #ffc;
}
.mode {
float: left;
}
.pagination {
float: right;
}
.note .label {
margin: 9px;
padding: 3px;
}
#login {
margin-top: 40px;
text-align: center;
width: 100%;
}
.isso-comment-footer a {
cursor: pointer;
}
.thread-title {
margin-left: 3em;
}
.group {
float: left;
margin-left: 2em;
}
.editable {
border: 1px solid #aaa;
border-radius: 5px;
margin: 10px;
padding: 5px;
}
.hidden {
display: none;
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save