Compare commits

..

1 Commits

Author SHA1 Message Date
Luke Faraone
2a67775c27 Disable use_socket manually for CUSTOMER13
This was causing tracebacks during message sending

(imported from commit 9dde67f07934fe7a5aef7036340eebb7c6f6a97a)
2014-02-13 22:53:47 -05:00
1036 changed files with 58644 additions and 24776 deletions

2
.gitattributes vendored
View File

@@ -15,6 +15,6 @@
/zproject/test_settings.py export-ignore
/zerver/fixtures export-ignore
/zerver/tests.py export-ignore
/frontend_tests export-ignore
/zerver/tests export-ignore
/node_modules export-ignore
/humbug export-ignore

11
.gitignore vendored
View File

@@ -7,7 +7,7 @@
/prod-static
/errors/*
*.sw[po]
*.DS_Store
.DS_Store
event_queues.pickle
stats/
zerver/fixtures/available-migrations
@@ -26,12 +26,3 @@ manage.log
.kateproject.d/
.kateproject
*.kate-swp
event_queues.json
.vagrant
/zproject/dev-secrets.conf
static/js/bundle.js
static/third/gemoji/
static/third/zxcvbn/
tools/emoji_dump/bitmaps/
tools/emoji_dump/*.ttx
node_modules

View File

@@ -1,22 +0,0 @@
before_install:
- nvm install 0.10
install:
- tools/travis/setup-$TEST_SUITE
cache:
- apt: false
env:
- TEST_SUITE=frontend
- TEST_SUITE=backend
- TEST_SUITE=production
- TEST_SUITE=py3k
language: python
python:
- "2.7"
# command to run tests
script:
- ./tools/travis/$TEST_SUITE
sudo: required
services:
- docker
addons:
postgresql: "9.3"

203
LICENSE
View File

@@ -1,202 +1,5 @@
Copyright <20> 2012-2013 Zulip, Inc.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
This software is licensed under the Zulip Enterprise License Agreement.
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Zulip can be reached at support@zulip.com.

View File

@@ -1,363 +0,0 @@
Installing the Zulip Development environment
============================================
You will need a machine with at least 2GB of RAM available (see
https://github.com/zulip/zulip/issues/32 for a plan for how to
dramatically reduce this requirement).
Start by cloning this repository: `git clone https://github.com/zulip/zulip.git`
Using Vagrant
-------------
This is the recommended approach for all platforms, and will install
the Zulip development environment inside a VM or container and works
on any platform that supports Vagrant.
The best performing way to run the Zulip development environment is
using an LXC container on a Linux host, but we support other platforms
such as Mac via Virtualbox (but everything will be 2-3x slower).
* If your host is Ubuntu 15.04 or newer, you can install and configure
the LXC Vagrant provider directly using apt:
```
sudo apt-get install vagrant lxc lxc-templates cgroup-lite redir
vagrant plugin install vagrant-lxc
```
* If your host is Ubuntu 14.04, you will need to [download a newer
version of Vagrant](https://www.vagrantup.com/downloads.html), and
then do the following:
```
sudo apt-get install lxc lxc-templates cgroup-lite redir
sudo dpkg -i vagrant*.deb # in directory where you downloaded vagrant
vagrant plugin install vagrant-lxc
```
* For other Linux hosts with a kernel above 3.12, [follow the Vagrant
LXC installation
instructions](https://github.com/fgrehm/vagrant-lxc) to get Vagrant
with LXC for your platform.
* If your host is OS X or older Linux, [download VirtualBox](https://www.virtualbox.org/wiki/Downloads),
[download Vagrant](https://www.vagrantup.com/downloads.html), and install them both.
* If you're on OS X and have VMWare, it should be possible to patch
Vagrantfile to use the VMWare vagrant provider which should perform
much better than Virtualbox. Patches to do this by default if
VMWare is available are welcome!
* On Windows: You can use Vagrant and Virtualbox/VMWare on Windows
with Cygwin, similar to the Mac setup. Be sure to create your git
clone using `git clone https://github.com/zulip/zulip.git -c
core.autocrlf=false` to avoid Windows line endings being added to
files (this causes weird errors).
Once that's done, simply change to your zulip directory and run
`vagrant up` in your terminal to install the development server. This
will take a long time on the first run because Vagrant needs to
download the Ubuntu Trusty base image, but later you can run `vagrant
destroy` and then `vagrant up` again to rebuild the environment and it
will be much faster.
Once that finishes, you can run the development server as follows:
```
vagrant ssh -- -L9991:localhost:9991
# Now inside the container
cd /srv/zulip
source /srv/zulip-venv/bin/activate
./tools/run-dev.py --interface=''
```
To get shell access to the virtual machine running the server to run
lint, management commands, etc., use `vagrant ssh`.
(A small note on tools/run-dev.py: the `--interface=''` option will make
the development server listen on all network interfaces. While this
is correct for the Vagrant guest sitting behind a NAT, you probably
don't want to use that option when using run-dev.py in other environments).
At this point you should [read about using the development environment](https://github.com/zulip/zulip/blob/master/README.dev.md#using-the-development-environment).
Using provision.py without Vagrant
----------------------------------
If you'd like to install a Zulip development environment on a server
that's already running Ubuntu 14.04 Trusty, you can do that by just
running:
```
sudo apt-get update
sudo apt-get install -y python-pbs
python /srv/zulip/provision.py
cd /srv/zulip
source /srv/zulip-venv/bin/activate
./tools/run-dev.py
```
Note that there is no supported uninstallation process without Vagrant
(with Vagrant, you can just do `vagrant destroy` to clean up the
development environment).
By hand
-------
If you really want to install everything by hand, the below
instructions should work.
Install the following non-Python dependencies:
* libffi-dev — needed for some Python extensions
* postgresql 9.1 or later — our database (also install development headers)
* memcached (and headers)
* rabbitmq-server
* libldap2-dev
* python-dev
* redis-server — rate limiting
* tsearch-extras — better text search
* libfreetype6-dev - needed before you pip install Pillow to properly generate emoji PNGs
### On Debian or Ubuntu systems:
```
sudo apt-get install libffi-dev memcached rabbitmq-server libldap2-dev python-dev redis-server postgresql-server-dev-all libmemcached-dev libfreetype6-dev
# If on 12.04 or wheezy:
sudo apt-get install postgresql-9.1
wget https://dl.dropboxusercontent.com/u/283158365/zuliposs/postgresql-9.1-tsearch-extras_0.1.2_amd64.deb
sudo dpkg -i postgresql-9.1-tsearch-extras_0.1.2_amd64.deb
# If on 14.04:
sudo apt-get install postgresql-9.3
wget https://dl.dropboxusercontent.com/u/283158365/zuliposs/postgresql-9.3-tsearch-extras_0.1.2_amd64.deb
sudo dpkg -i postgresql-9.3-tsearch-extras_0.1.2_amd64.deb
# If on 15.04 or jessie:
sudo apt-get install postgresql-9.4
wget https://dl.dropboxusercontent.com/u/283158365/zuliposs/postgresql-9.4-tsearch-extras_0.1_amd64.deb
sudo dpkg -i postgresql-9.4-tsearch-extras_0.1_amd64.deb
```
Now continue with the "All systems" instructions below.
### On Fedora 22 (experimental):
These instructions are experimental and may have bugs; patches welcome!
```
sudo dnf install libffi-devel memcached rabbitmq-server openldap-devel python-devel redis postgresql-server postgresql-devel postgresql libmemcached-devel freetype-devel
```
Now continue with the Common to Fedora/CentOS instructions below.
### On CentOS 7 Core (experimental):
These instructions are experimental and may have bugs; patches welcome!
```
# Add user zulip to the system (not necessary if you configured zulip as the administrator
# user during the install process of CentOS 7).
useradd zulip
# Create a password for zulip user
passwd zulip
# Allow zulip to sudo
visudo
# Add this line after line `root ALL=(ALL) ALL`
zulip ALL=(ALL) ALL
# Switch to zulip user
su zulip
# Enable EPEL 7 repo so we can install rabbitmq-server, redis and other dependencies
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
# Install dependencies
sudo yum install libffi-devel memcached rabbitmq-server openldap-devel python-devel redis postgresql-server \
postgresql-devel postgresql libmemcached-devel wget python-pip openssl-devel freetype-devel libjpeg-turbo-devel \
zlib-devel nodejs
# We need these packages to compile tsearch-extras
sudo yum groupinstall "Development Tools"
# clone Zulip's git repo and cd into it
cd && git clone https://github.com/zulip/zulip && cd zulip/
## NEEDS TESTING: The next few DB setup items may not be required at all.
# Initialize the postgres db
sudo postgresql-setup initdb
# Edit the postgres settings:
sudo vi /var/lib/pgsql/data/pg_hba.conf
# Change these lines:
host all all 127.0.0.1/32 ident
host all all ::1/128 ident
# to this:
host all all 127.0.0.1/32 md5
host all all ::1/128 md5
```
Now continue with the Common to Fedora/CentOS instructions below.
### Common to Fedora/CentOS instructions
```
# Build and install postgres tsearch-extras module
wget https://launchpad.net/~tabbott/+archive/ubuntu/zulip/+files/tsearch-extras_0.1.3.tar.gz
tar xvzf tsearch-extras_0.1.3.tar.gz
cd ts2
make
sudo make install
# Hack around missing dictionary files -- need to fix this to get
# the proper dictionaries from what in debian is the hunspell-en-us package.
sudo touch /usr/share/pgsql/tsearch_data/english.stop
sudo touch /usr/share/pgsql/tsearch_data/en_us.dict
sudo touch /usr/share/pgsql/tsearch_data/en_us.affix
# Edit the postgres settings:
sudo vi /var/lib/pgsql/data/pg_hba.conf
# Add this line before the first uncommented line to enable password auth:
host all all 127.0.0.1/32 md5
# Start the services
sudo systemctl start redis memcached rabbitmq-server postgresql
# Enable automatic service startup after the system startup
sudo systemctl enable redis rabbitmq-server memcached postgresql
```
Finally continue with the All Systems instructions below.
### All Systems:
```
pip install -r requirements.txt
./tools/download-zxcvbn
./tools/emoji_dump/build_emoji
./scripts/setup/generate_secrets.py -d
sudo cp ./puppet/zulip/files/postgresql/zulip_english.stop /usr/share/postgresql/9.3/tsearch_data/
./scripts/setup/configure-rabbitmq
./tools/postgres-init-dev-db
./tools/do-destroy-rebuild-database
./tools/postgres-init-test-db
./tools/do-destroy-rebuild-test-database
```
To start the development server:
```
./tools/run-dev.py
```
… and visit [http://localhost:9991/](http://localhost:9991/).
Using the Development Environment
=================================
Once the development environment is running, you can visit
<http://localhost:9991/> in your browser. By default, the development
server homepage just shows a list of the users that exist on the
server and you can login as any of them by just clicking on a user.
This setup saves time for the common case where you want to test
something other than the login process; to test the login process
you'll want to change AUTHENTICATION_BACKENDS in the not-PRODUCTION
case of `zproject/settings.py` from zproject.backends.DevAuthBackend
to use the auth method(s) you'd like to test.
While developing, it's helpful to watch the `run-dev.py` console
output, which will show any errors your Zulip development server
encounters.
When you make a change, here's a guide for what you need to do in
order to see your change take effect in Development:
* If you change Javascript or CSS, you'll just need to reload the
browser window to see changes take effect.
* If you change Python code used by the the main Django/Tornado server
processes, these services are run on top of Django's [manage.py
runserver](https://docs.djangoproject.com/en/1.8/ref/django-admin/#runserver-port-or-address-port),
which will automatically restart the Zulip Django and Tornado servers
whenever you save changes to Python code. You can watch this happen
in the `run-dev.py` console to make sure the backend has reloaded.
* The Python queue workers don't automatically restart when you save
changes (or when they stop running), so you will want to ctrl-C and
then restart `run-dev.py` manually if you are testing changes to the
queue workers or if a queue worker has crashed.
* If you change the database schema, you'll need to use the standard
Django migrations process to create and then run your migrations; see
the [new feature
tutorial](http://zulip.readthedocs.org/en/latest/new-feature-tutorial.html)
for an example. Additionally you should check out the [detailed
testing docs](http://zulip.readthedocs.org/en/latest/testing.html) for
how to run the tests properly after doing a migration.
(In production, everything runs under supervisord and thus will
restart if it crashes, and `upgrade-zulip` will take care of running
migrations and then cleanly restaring the server for you).
Running the test suite
======================
For more details, check out the [detailed testing
docs](http://zulip.readthedocs.org/en/latest/testing.html).
To run all the tests, do this:
```
./tools/test-all
```
For the Vagrant environment, you'll want to first enter the environment:
```
vagrant ssh
source /srv/zulip-venv/bin/activate
cd /srv/zulip
```
This runs the linter (`tools/lint-all`) plus all of our test suites;
they can all be run separately (just read `tools/test-all` to see
them). You can also run individual tests which can save you a lot of
time debugging a test failure, e.g.:
```
./tools/lint-all # Runs all the linters in parallel
./tools/test-backend zerver.test_bugdown.BugdownTest.test_inline_youtube
./tools/test-js-with-casper 10-navigation.js
./tools/test-js-with-node # Runs all node tests but is very fast
```
The above setup instructions include the first-time setup of test
databases, but you may need to rebuild the test database occasionally
if you're working on new database migrations. To do this, run:
```
./tools/postgres-init-test-db
./tools/do-destroy-rebuild-test-database
```
Possible testing issues
=======================
- When running the test suite, if you get an error like this:
```
sqlalchemy.exc.ProgrammingError: (ProgrammingError) function ts_match_locs_array(unknown, text, tsquery) does not exist
LINE 2: ...ECT message_id, flags, subject, rendered_content, ts_match_l...
^
```
… then you need to install tsearch-extras, described
above. Afterwards, re-run the `init*-db` and the
`do-destroy-rebuild*-database` scripts.
- When building the development environment using Vagrant and the LXC provider, if you encounter permissions errors, you may need to `chown -R 1000:$(whoami) /path/to/zulip` on the host before running `vagrant up` in order to ensure that the synced directory has the correct owner during provision. This issue will arise if you run `id username` on the host where `username` is the user running Vagrant and the output is anything but 1000.
This seems to be caused by Vagrant behavior; more information can be found here https://github.com/fgrehm/vagrant-lxc/wiki/FAQ#help-my-shared-folders-have-the-wrong-owner

149
README.md
View File

@@ -1,149 +0,0 @@
Zulip
=====
Zulip is a powerful, open source group chat application. Written in
Python and using the Django framework, Zulip supports both private
messaging and group chats via conversation streams.
Zulip also supports fast search, drag-and-drop file uploads, image
previews, group private messages, audible notifications,
missed-message emails, desktop apps, and much more.
Further information on the Zulip project and its features can be found
at https://www.zulip.org.
Installing the Zulip Development environment
============================================
The Zulip development environment is the recommened option for folks
interested in trying out Zulip. This is documented in
[README.dev.md](README.dev.md).
Running Zulip in production
===========================
Zulip in production only supports Ubuntu 14.04 right now, but work is
ongoing on adding support for additional platforms. The installation
process is documented in https://zulip.org/server.html and in more
detail in [README.prod.md](README.prod.md).
Contributing to Zulip
=====================
Zulip welcomes all forms of contributions! The page documents the
Zulip development process.
* **Pull requests**. Before a pull request can be merged, you need to to sign the [Dropbox
Contributor License Agreement](https://opensource.dropbox.com/cla/).
Also, please skim our [commit message style
guidelines](http://zulip.readthedocs.org/en/latest/code-style.html#commit-messages).
* **Testing**. The Zulip automated tests all run automatically when
you submit a pull request, but you can also run them all in your
development environment following the instructions in the [testing
section](https://github.com/zulip/zulip#running-the-test-suite) below.
* **Developer Documentation**. Zulip has a growing collection of
developer documentation on [Read The Docs](https://zulip.readthedocs.org/).
Recommended reading for new contributors includes the
[directory structure](http://zulip.readthedocs.org/en/latest/directory-structure.html) and
[new feature tutorial](http://zulip.readthedocs.org/en/latest/new-feature-tutorial.html).
* **Mailing list and bug tracker** Zulip has a [development discussion
mailing list](https://groups.google.com/forum/#!forum/zulip-devel) and
uses [GitHub issues](https://github.com/zulip/zulip/issues). Feel
free to send any questions or suggestions of areas where you'd love to
see more documentation to the list! Please report any security issues
you discover to support@zulip.com.
* **App codebases** This repository is for the Zulip server and web app; the
[desktop](https://github.com/zulip/zulip-desktop),
[Android](https://github.com/zulip/zulip-android), and
[iOS](https://github.com/zulip/zulip-ios) apps are separate
repositories.
How to get involved with contributing to Zulip
==============================================
First, subscribe to the Zulip [development discussion mailing list](https://groups.google.com/forum/#!forum/zulip-devel).
The Zulip project uses a system of labels in our [issue
tracker](https://github.com/zulip/zulip/issues) to make it easy to
find a project if you don't have your own project idea in mind or want
to get some experience with working on Zulip before embarking on a
larger project you have in mind:
* [Bite Size](https://github.com/zulip/zulip/labels/bite%20size):
Smaller projects that could be a great first contribution.
* [Integrations](https://github.com/zulip/zulip/labels/integrations).
Integrate Zulip with another piece of software and contribute it
back to the community! Writing an integration can be a great
started project. There's some brief documentation on the best way
to write integrations at https://github.com/zulip/zulip/issues/70.
* [Documentation](https://github.com/zulip/zulip/labels/documentation).
The Zulip project loves contributions of new documentation.
* [Help Wanted](https://github.com/zulip/zulip/labels/help%20wanted):
A broader list of projects that nobody is currently working on.
* [Platform support](https://github.com/zulip/zulip/labels/Platform%20support).
These are open issues about making it possible to install Zulip on a wider
range of platforms.
* [Bugs](https://github.com/zulip/zulip/labels/bug). Open bugs.
* [Feature requests](https://github.com/zulip/zulip/labels/enhancement).
Browsing this list can be a great way to find feature ideas to implement that
other Zulip users are excited about.
If you're excited about helping with an open issue, just post on the
conversation thread that you're working on it. You're encouraged to
ask questions on how to best implement or debug your changes -- the
Zulip maintainers are excited to answer questions to help you stay
unblocked and working efficiently.
We also welcome suggestions of features that you feel would be
valuable or changes that you feel would make Zulip a better open
source project, and are happy to support you in adding new features or
other user experience improvements to Zulip.
If you have a new feature you'd like to add, we recommend you start by
opening a GitHub issue about the feature idea explaining the problem
that you're hoping to solve and that you're excited to work on it. A
Zulip maintainer will usually reply within a day with feedback on the
idea, notes on any important issues or concerns, and and often tips on
how to implement or test it. Please feel free to ping the thread if
you don't hear a response from the maintainers -- we try to be very
responsive so this usually means we missed your message.
For significant changes to the visual design, user experience, data
model, or architecture, we highly recommend posting a mockup,
screenshot, or description of what you have in mind to zulip-devel@ to
get broad feedback before you spend too much time on implementation
details.
Finally, before implementing a larger feature, we highly recommend
looking at the new feature tutorial and coding style guidelines on
ReadTheDocs.
Feedback on how to make this development process more efficient, fun,
and friendly to new contributors is very welcome! Just shoot an email
to the Zulip Developers list with your thoughts.
License
=======
Copyright 2011-2015 Dropbox, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
The software includes some works released by third parties under other
free and open source licenses. Those works are redistributed under the
license terms under which the works were received. For more details,
see the ``THIRDPARTY`` file included with this distribution.

View File

@@ -1,510 +0,0 @@
Zulip in production
===================
This documents the process for installing Zulip in a production environment.
Note that if you just want to play around with Zulip and see what it
looks like, it is easier to install it in a development environment
following the instructions in README.dev, since then you don't need to
worry about setting up SSL certificates and an authentication mechanism.
Recommended requirements:
* Server running Ubuntu Trusty
* At least 2 CPUs for production use with 100+ users
* At least 4GB of RAM for production use with 100+ users. We **strongly
recommend against installing with less than 2GB of RAM**, as you will
likely experience OOM issues. In the future we expect Zulip's RAM
requirements to decrease to support smaller installations (see
https://github.com/zulip/zulip/issues/32).
* At least 10GB of free disk for production use (more may be required
if you intend to store uploaded files locally rather than in S3
and your team uses that feature extensively)
* Outgoing HTTP(S) access to the public Internet.
* SSL Certificate for the host you're putting this on
(e.g. zulip.example.com). If you just want to see what
Zulip looks like, we recommend installing the development
environment detailed in README.md as that is easier to setup.
* Email credentials Zulip can use to send outgoing emails to users
(e.g. email address confirmation emails during the signup process,
missed message notifications, password reminders if you're not using
SSO, etc.).
Installing Zulip in production
==============================
These instructions should be followed as root.
(1) Install the SSL certificates for your machine to
`/etc/ssl/private/zulip.key` and `/etc/ssl/certs/zulip.combined-chain.crt`.
If you don't know how to generate an SSL certificate, you, you can
do the following to generate a self-signed certificate:
```
apt-get install openssl
openssl genrsa -des3 -passout pass:x -out server.pass.key 4096
openssl rsa -passin pass:x -in server.pass.key -out zulip.key
rm server.pass.key
openssl req -new -key zulip.key -out server.csr
openssl x509 -req -days 365 -in server.csr -signkey zulip.key -out zulip.combined-chain.crt
rm server.csr
cp zulip.key /etc/ssl/private/zulip.key
cp zulip.combined-chain.crt /etc/ssl/certs/zulip.combined-chain.crt
```
You will eventually want to get a properly signed certificate (and
note that at present the Zulip desktop app doesn't support
self-signed certificates), but this will let you finish the
installation process.
(2) Download [the latest built server tarball](https://www.zulip.com/dist/releases/zulip-server-latest.tar.gz)
and unpack it to `/root/zulip`, e.g.
```
wget https://www.zulip.com/dist/releases/zulip-server-latest.tar.gz
tar -xf zulip-server-latest.tar.gz
mv zulip-server-1.3.6 /root/zulip
```
(3) Run
```
/root/zulip/scripts/setup/install
```
This may take a while to run, since it will install a large number of
packages via apt.
(4) Configure the Zulip server instance by filling in the settings in
`/etc/zulip/settings.py`. Be sure to fill in all the mandatory
settings, enable at least one authentication mechanism, and do the
configuration required for that authentication mechanism to work.
See the section on "Authentication" below for more detail on
configuring authentication mechanisms.
(5) Run
```
su zulip -c /home/zulip/deployments/current/scripts/setup/initialize-database
```
This will report an error if you did not fill in all the mandatory
settings from `/etc/zulip/settings.py`. Once this completes
successfully, the main installation process will be complete, and if
you are planning on using password authentication, you should be able
to visit the URL for your server and register for an account.
(6) Subscribe to [the Zulip announcements Google Group](https://groups.google.com/forum/#!forum/zulip-announce)
to get announcements about new releases, security issues, etc.
Authentication and logging into Zulip the first time
====================================================
(As you read and follow the instructions in this section, if you run
into trouble, check out the troubleshooting advice in the next major
section.)
Once you've finished installing Zulip, configuring your settings.py
file, and initializing the database, it's time to login to your new
installation. By default, initialize-database creates 1 realm that
you can join, the `ADMIN_DOMAIN` realm (defined in
`/etc/zulip/settings.py`).
The `ADMIN_DOMAIN` realm is by default configured with the following settings:
* `restricted_to_domain=True`: Only people with emails ending with @ADMIN_DOMAIN can join.
* `invite_required=False`: An invitation is not required to join the realm.
* `invite_by_admin_only=False`: You don't need to be an admin user to invite other users.
* `mandatory_topics=False`: Users are not required to specify a topic when sending messages.
If you would like to change these settings, you can do so using the
Django management python shell (as the zulip user):
```
cd /home/zulip/deployments/current
./manage.py shell
from zerver.models import *
r = get_realm(settings.ADMIN_DOMAIN)
r.restricted_to_domain=False # Now anyone anywhere can login
r.save() # save to the database
```
If you realize you set `ADMIN_DOMAIN` wrong, in addition to fixing the
value in settings.py, you will also want to do a similar manage.py
process to set `r.domain = "newexample.com"`. If you've already
changed `ADMIN_DOMAIN` in settings.py, you can use
`Realm.objects.all()` in the management shell to find the list of
realms and pass the domain of the realm that is not "zulip.com" to
`get_realm`.
Depending what authentication backend you're planning to use, you will
need to do some additional setup documented in the `settings.py` template:
* For Google authentication, you need to follow the configuration
instructions around `GOOGLE_OAUTH2_CLIENT_ID` and `GOOGLE_CLIENT_ID`.
* For Email authentication, you will need to follow the configuration
instructions around outgoing SMTP from Django.
You should be able to login now. If you get an error, check
`/var/log/zulip/errors.log` for a traceback, and consult the next
section for advice on how to debug. If you aren't able to figure it
out, email zulip-help@googlegroups.com with the traceback and we'll
try to help you out!
You will likely want to make your own user account an admin user,
which you can do via the following management command:
```
./manage.py knight username@example.com -f
```
Now that you are an administrator, you will have a special
"Administration" tab linked to from the upper-right gear menu in the
Zulip app that lets you deactivate other users, manage streams, change
the Realm settings you may have edited using manage.py shell above,
etc.
You can also use `manage.py knight` with the
`--permission=api_super_user` argument to create API super users,
which are needed to mirror messages to streams from other users for
the IRC and Jabber mirroring integrations (see
`bots/irc-mirror.py` and `bots/jabber_mirror.py` for some detail on these).
There are a large number of useful management commands under
`zerver/manangement/commands/`; you can also see them listed using
`./manage.py` with no arguments.
One such command worth highlighting because it's a valuable feature
with no UI in the Administration page is `./manage.py realm_filters`,
which allows you to configure certain patterns in messages to be
automatically linkified, e.g. whenever someone mentions "T1234" it
could be auto-linkified to ticket 1234 in your team's Trac instance.
Checking Zulip is healthy and debugging the services it depends on
==================================================================
You can check if the zulip application is running using:
```
supervisorctl status
```
And checking for errors in the Zulip errors logs under
`/var/log/zulip/`. That contains one log file for each service, plus
`errors.log` (has all errors), `server.log` (logs from the Django and
Tornado servers), and `workers.log` (combined logs from the queue
workers).
After you change configuration in `/etc/zulip/settings.py` or fix a
misconfiguration, you will often want to restart the Zulip application.
You can restart Zulip using:
```
supervisorctl restart all
```
Similarly, you can stop Zulip using:
```
supervisorctl stop all
```
The Zulip application uses several major services to store and cache
data, queue messages, and otherwise support the Zulip application:
* postgresql
* rabbitmq-server
* nginx
* redis
* memcached
If one of these services is not installed or functioning correctly,
Zulip will not work. Below we detail some common configuration
problems and how to resolve them:
* An AMQPConnectionError traceback or error running rabbitmqctl
usually means that RabbitMQ is not running; to fix this, try:
```
service rabbitmq-server restart
```
If RabbitMQ fails to start, the problem is often that you are using
a virtual machine with broken DNS configuration; you can often
correct this by configuring `/etc/hosts` properly.
* If your browser reports no webserver is running, that is likely
because nginx is not configured properly and thus failed to start.
nginx will fail to start if you configured SSL incorrectly or did
not provide SSL certificates. To fix this, configure them properly
and then run:
```
service nginx restart
```
If you run into additional problems, [please report
them](https://github.com/zulip/zulip/issues) so that we can update
these lists! The Zulip installation scripts logs its full output to
`/var/log/zulip/install.log`, so please include the context for any
tracebacks from that log.
Making your Zulip instance awesome
==================================
Once you've got Zulip setup, you'll likely want to configure it the
way you like. There are four big things to focus on:
(1) Integrations. We recommend setting up integrations for the major
tools that your team works with. For example, if you're a software
development team, you may want to start with integrations for your
version control, issue tracker, CI system, and monitoring tools.
Spend time configuring these integrations to be how you like them --
if an integration is spammy, you may want to change it to not send
messages that nobody cares about (E.g. for the zulip.com trac
integration, some teams find they only want notifications when new
tickets are opened, commented on, or closed, and not every time
someone edits the metadata).
If Zulip doesn't have an integration you want, you can add your own!
Most integrations are very easy to write, and even more complex
integrations usually take less than a day's work to build. We very
much appreciate contributions of new integrations; there is a brief
draft integration writing guide [here](https://github.com/zulip/zulip/issues/70).
It can often be valuable to integrate your own internal processes to
send notifications into Zulip; e.g. notifications of new customer
signups, new error reports, or daily reports on the team's key
metrics; this can often spawn discussions in response to the data.
(2) Streams and Topics. If it feels like a stream has too much
traffic about a topic only of interest to some of the subscribers,
consider adding or renaming streams until you feel like your team is
working productively.
Second, most users are not used to topics. It can require a bit of
time for everyone to get used to topics and start benefitting from
them, but usually once a team is using them well, everyone ends up
enthusiastic about how much topics make life easier. Some tips on
using topics:
* When replying to an existing conversation thread, just click on the
message, or navigate to it with the arrow keys and hit "r" or
"enter" to reply on the same topic
* When you start a new conversation topic, even if it's related to the
previous conversation, type a new topic in the compose box
* You can edit topics to fix a thread that's already been started,
which can be helpful when onboarding new batches of users to the platform.
Third, setting default streams for new users is a great way to get
new users involved in conversations before they've accustomed
themselves with joining streams on their own. You can use the
[`set_default_streams`](https://github.com/zulip/zulip/blob/master/zerver/management/commands/set_default_streams.py)
command to set default streams for users within a realm:
```
python manage.py set_default_streams --domain=example.com --streams=foo,bar,...
```
(3) Notification settings. Zulip gives you a great deal of control
over which messages trigger desktop notifications; you can configure
these extensively in the `/#settings` page (get there from the gear
menu). If you find the desktop notifications annoying, consider
changing the settings to only trigger desktop notifications when you
receive a PM or are @-mentioned.
(4) The mobile and desktop apps. Currently, the Zulip Desktop app
only supports talking to servers with a properly signed SSL
certificate, so you may find that you get a blank screen when you
connect to a Zulip server using a self-signed certificate.
The Zulip iOS and Android apps in their respective stores don't yet
support talking to non-zulip.com servers; the iOS app is waiting on
Apple's app store review, while the Android app is waiting on someone
to do the small project of adding a field to specify what Zulip server
to talk to.
These issues will likely all be addressed in the coming weeks; make
sure to join the zulip-announce@googlegroups.com list so that you can
receive the announcements when these become available.
(5) All the other features: Hotkeys, emoji, search filters,
@-mentions, etc. Zulip has lots of great features, make sure your
team knows they exist and how to use them effectively.
(6) Enjoy your Zulip installation! If you discover things that you
wish had been documented, please contribute documentation suggestions
either via a GitHub issue or pull request; we love even small
contributions, and we'd love to make the Zulip documentation cover
everything anyone might want to know about running Zulip in
production.
Maintaining and upgrading Zulip in production
=============================================
We recommend reading this entire section before doing your first
upgrade.
* To upgrade to a new version of the zulip server, download the
appropriate release tarball from
https://www.zulip.com/dist/releases/ to a path readable by the zulip
user (e.g. /home/zulip), and then run as root:
```
/home/zulip/deployments/current/scripts/upgrade-zulip zulip-server-VERSION.tar.gz
```
Be sure to download to a path readable by the Zulip user (see
https://github.com/zulip/zulip/issues/208 for details on this
issue) but then run the upgrade as root.
The upgrade process will shut down the service, run `apt-get
upgrade`, a puppet apply, and any database migrations, and then
bring the service back up. This will result in some brief downtime
for the service, which should be under 30 seconds unless there is an
expensive transition involved. Unless you have tested the upgrade
in advance, we recommend doing upgrades at off hours.
You can create your own release tarballs from a copy of zulip.git
repository using `tools/build-release-tarball`.
* **Warning**: If you have modified configuration files installed by
Zulip (e.g. the nginx configuration), the Zulip upgrade process will
overwrite your configuration when it does the `puppet apply`. You
can test whether this will happen assuming no upstream changes to
the configuration using `scripts/zulip-puppet-apply` (without the
`-f` option), which will do a test puppet run and output and changes
it would make. Using this list, you can save a copy of any files
that you've modified, do the upgrade, and then restore your
configuration. If you need to do this, please report the issue so
that we can make the Zulip puppet configuration flexible enough to
handle your setup.
* The Zulip upgrade script automatically logs output to
/var/log/zulip/upgrade.log; please use those logs to include output
that shows all errors in any bug reports.
* The Zulip upgrade process works by creating a new deployment under
/home/zulip/deployments/ containing a complete copy of the Zulip
server code, and then moving the symlinks at
`/home/zulip/deployments/current` and /root/zulip` as part of the
upgrade process. This means that if the new version isn't working,
you can quickly downgrade to the old version by using
`/home/zulip/deployments/<date>/scripts/restart-server` to return to
a previous version that you've deployed (the version is specified
via the path to the copy of `restart-server` you call).
* To update your settings, simply edit `/etc/zulip/settings.py` and then
run `/home/zulip/deployments/current/scripts/restart-server` to
restart the server
* You are responsible for running `apt-get upgrade` on your system on
a regular basis to ensure that it is up to date with the latest
security patches.
* To use the Zulip API with your Zulip server, you will need to use the
API endpoint of e.g. `https://zulip.example.com/api`. Our Python
API example scripts support this via the
`--site=https://zulip.example.com` argument. The API bindings
support it via putting `site=https://zulip.example.com` in your
.zuliprc.
Every Zulip integration supports this sort of argument (or e.g. a
`ZULIP_SITE` variable in a zuliprc file or the environment), but this
is not yet documented for some of the integrations (the included
integration documentation on `/integrations` will properly document
how to do this for most integrations). Pull requests welcome to
document this for those integrations that don't discuss this!
* Similarly, you will need to instruct your users to specify the URL
for your Zulip server when using the Zulip desktop and mobile apps.
* As a measure to mitigate the impact of potential memory leaks in one
of the Zulip daemons, the service automatically restarts itself
every Sunday early morning. See `/etc/cron.d/restart-zulip` for the
precise configuration.
Remote User SSO Authentication
==============================
Zulip supports integrating with a corporate Single-Sign-On solution.
There are a few ways to do it, but this section documents how to
configure Zulip to use an SSO solution that best supports Apache and
will set the `REMOTE_USER` variable:
(0) Check that `/etc/zulip/settings.py` has
`zproject.backends.ZulipRemoteUserBackend` as the only enabled value
in the `AUTHENTICATION_BACKENDS` list, and that `SSO_APPEND_DOMAIN` is
correct set depending on whether your SSO system uses email addresses
or just usernames in `REMOTE_USER`.
Make sure that you've restarted the Zulip server since making this
configuration change.
(1) Edit `/etc/zulip/zulip.conf` and change the `puppet_classes` line to read:
```
puppet_classes = zulip::voyager, zulip::apache_sso
```
(2) As root, run `/home/zulip/deployments/current/scripts/zulip-puppet-apply`
to install our SSO integration.
(3) To configure our SSO integration, edit
`/etc/apache2/sites-available/zulip-sso.example` and fill in the
configuration required for your SSO service to set `REMOTE_USER` and
place your completed configuration file at `/etc/apache2/sites-available/zulip-sso`
(4) Run `a2ensite zulip-sso` to enable the Apache integration site.
Now you should be able to visit `https://zulip.example.com/` and
login via the SSO solution.
### Troubleshooting Remote User SSO
This system is a little finicky to networking setup (e.g. common
issues have to do with /etc/hosts not mapping settings.EXTERNAL_HOST
to the Apache listening on 127.0.0.1/localhost, for example). It can
often help while debugging to temporarily change the Apache config in
/etc/apache2/sites-available/zulip-sso to listen on all interfaces
rather than just 127.0.0.1 as you debug this. It can also be helpful
to change /etc/nginx/zulip-include/app.d/external-sso.conf to
proxy_pass to a more explicit URL possibly not over HTTPS when
debugging. The following log files can be helpful when debugging this
setup:
* /var/log/zulip/{errors.log,server.log} (the usual places)
* /var/log/nginx/access.log (nginx access logs)
* /var/log/apache2/zulip_auth_access.log (you may want to change
LogLevel to "debug" in the apache config file to make this more
verbose)
Here's a summary of how the remote user SSO system works assuming
you're using HTTP basic auth; this summary should help with
understanding what's going on as you try to debug:
* Since you've configured /etc/zulip/settings.py to only define the
zproject.backends.ZulipRemoteUserBackend, zproject/settings.py
configures /accounts/login/sso as HOME_NOT_LOGGED_IN, which makes
`https://zulip.example.com/` aka the homepage for the main Zulip
Django app running behind nginx redirect to /accounts/login/sso if
you're not logged in.
* nginx proxies requests to /accounts/login/sso/ to an Apache instance
listening on localhost:8888 apache via the config in
/etc/nginx/zulip-include/app.d/external-sso.conf (using the upstream
localhost:8888 defined in /etc/nginx/zulip-include/upstreams).
* The Apache zulip-sso site which you've enabled listens on
localhost:8888 and presents the htpasswd dialogue; you provide
correct login information and the request reaches a second Zulip
Django app instance that is running behind Apache with with
REMOTE_USER set. That request is served by
`zerver.views.remote_user_sso`, which just checks the REMOTE_USER
variable and either logs in (sets a cookie) or registers the new
user (depending whether they have an account).
* After succeeding, that redirects the user back to / on port 443
(hosted by nginx); the main Zulip Django app sees the cookie and
proceeds to load the site homepage with them logged in (just as if
they'd logged in normally via username/password).
Again, most issues with this setup tend to be subtle issues with the
hostname/DNS side of the configuration. Suggestions for how to
improve this SSO setup documentation are very welcome!

View File

@@ -1,570 +0,0 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: Zulip
Upstream-Contact: Zulip Development Discussion <zulip-devel@googlegroups.com>
Source: https://zulip.org/
Comment:
Unless otherwise noted, the Zulip software is distributed under the Apache
License, Version 2.0. The software includes some works released by third
parties under other free and open source licenses. Those works are
redistributed under the license terms under which the works were received.
.
While Dropbox has sought to provide complete and accurate licensing
information for each FOSS package, Dropbox does not represent or warrant
that the licensing information provided herein is correct or error-free.
Recipients of the Zulip software should investigate the identified FOSS
packages to confirm the accuracy of the licensing information provided.
Recipients are also encouraged to notify Dropbox of any inaccurate
information or errors found in these notices.
Files: *
Copyright: 2011-2015 Dropbox, Inc.
License: Apache-2
Files: api/*
Copyright: 2012-2014 Dropbox, Inc
License: Expat
Files: api/integrations/perforce/git_p4.py
Copyright: 2007 Simon Hausmann <simon@lst.de>,
2007 Trolltech ASA
License: Expat
Comment: https://raw.github.com/git/git/34022ba/git-p4.py
Files: bots/jabber_mirror_backend.py
Copyright: 2013 Permabit, Inc., 2013-2014 Dropbox, Inc.
License: Expat
Files: confirmation/*
Copyright: 2008, Jarek Zgoda <jarek.zgoda@gmail.com>
License: BSD-3-Clause
Files: puppet/apt/*
Copyright: 2011, Evolving Web Inc.
License: Expat
Files: puppet/common/*
Copyright: 2007, David Schmitt
License: BSD-3-Clause
Comment: https://github.com/DavidS/puppet-common
Distribution includes a file `lib/puppet/parser/functions/ip_to_cron.rb` which
we removed due to unclear license
Files: puppet/stdlib/*
Copyright: 2011, Krzysztof Wilczynski
2011, Puppet Labs Inc
License: Apache-2.0
File: puppet/zulip_internal/files/mediawiki/Auth_remoteuser.php
Copyright: 2006 Otheus Shelling
2007 Rusty Burchfield
2009 James Kinsman
2010 Daniel Thomas
2010 Ian Ward Comfort
License: GPL-2.0
Comment: Not linked.
Files: puppet/zulip_internal/files/nagios_plugins/check_debian_packages
Copyright: 2005 Francesc Guasch
License: GPL-2.0
Comment: Not linked.
Files: puppet/zulip_internal/files/nagios_plugins/check_postgres.pl
Copyright: 2007-2015 Greg Sabino Mullane
License: BSD-2-Clause
Files: puppet/zulip_internal/files/nagios_plugins/check_website_response.sh
Copyright: 2011 Chris Freeman
License: GPL-2.0
Files: puppet/zulip_internal/files/trac/cgi-bin/
Copyright: 2003-2009 Edgewall Software
2003-2004 Jonas Borgström <jonas@edgewall.com>
License: BSD-3-Clause
Files: puppet/zulip_internal/files/pagerduty_nagios.pl
Copyright: 2011, PagerDuty, Inc.
License: BSD-3-Clause
Files: puppet/zulip_internal/files/zulip-ec2-configure-interfaces
Copyright: 2013, Dropbox, Inc.
License: Expat
Files: static/audio/zulip.*
Copyright: 2011 Vidsyn
License: CC-0-1.0
Files: static/styles/thirdparty-fonts.css
Copyright: 2012-2013 Dave Gandy
License: Expat
Files: static/third/fontawesome/*
Copyright: 2012-2013 Dave Gandy
License: SIL-OFL-1.1
Files: static/third/bootstrap/bootstrap-btn.css
Copyright: 2011-2014 Twitter, Inc
License: Expat
Files: static/third/bootstrap/css/bootstrap-responsive.css static/third/bootstrap/css/bootstrap.css
Copyright: 2012 Twitter, Inc
License: Apache-2.0
Comment: The software has been modified.
Files: static/third/bootstrap/js/bootstrap.js
Copyright: 2012 Twitter, Inc
License: Apache-2.0
Comment: The software has been modified.
Files: static/third/bootstrap-notify/*
Copyright: 2013 Nijiko Yonskai
2012 Goodybag, Inc.
2012 Twitter, Inc
License: Apache-2.0
Comment: The software has been modified.
Files: static/third/gemoji/images/emoji/unicode/* tools/emoji_dump/*.ttf
Copyright: Google, Inc.
License: Apache-2.0
Comment: These are actually Noto Emoji, not gemoji.
Files: static/third/html5-formdata/formdata.js
Copyright: 2010 François de Metz
License: Expat
Comment: See https://github.com/francois2metz/html5-formdata
Files: src/zulip/static/third/jquery/*
Copyright: 2011, John Resig
2011, The Dojo Foundation
License: Expat
Files: static/third/jquery-autosize/jquery.autosize.js
Copyright: 2013 Jack Moore
License: Expat
Files: static/third/jquery-caret/*
Copyright: 2010 C.F., Wong
License: Expat
Files: static/third/jquery-filedrop/jquery.filedrop.js
Copyright: Resopollution
License: Expat
Files: static/third/jquery-form/jquery.form.js
Copyright: M. Alsup
License: Expat or GPL-2.0
Files: static/third/jquery-idle/jquery.idle.js
Copyright: 2011-2013 Henrique Boaventura
License: Expat
Comment: The software has been modified.
Files: static/third/jquery-mousewheel/jquery.mousewheel.js
Copyright: 2011 Brandon Aaron
License: Expat
Files: static/third/jquery-perfect-scrollbar/*
Copyright: 2012 HyeonJe Jun
License: Expat
Files: static/third/jquery-throttle-debounce/*
Copyright: 2010 "Cowboy" Ben Alman
License: Expat or GPL
Files: static/third/jquery-validate/*
Copyright: 2006 - 2011 Jörn Zaefferer
License: Expat
Files: src/zulip/static/third/lazyload/*
Copyright: 2011 Ryan Grove
License: Expat
Files: static/third/marked/*
Copyright: 2011-2013, Christopher Jeffrey
License: Expat
Files: static/third/sockjs/sockjs-0.3.4.js
Copyright: 2011-2012 VMware, Inc.
2012 Douglas Crockford
License: Expat and public-domain
Files: static/third/sorttable/sorttable.js
Copyright: 2007 Stuart Langridge
License: X11
Files: static/third/sourcesans/*
Copyright: 2010, 2012, 2014 Adobe Systems Incorporated
License: SIL-OFL-1.1
Files: static/third/spectrum/*
Copyright: 2013 Brian Grinstead
License: Expat
Files: static/third/spin/spin.js
Copyright: 2011-2013 Felix Gnass
License: Expat
Files: static/third/underscore/underscore.js
Copyright: 2009-2013 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
License: Expat
Comment: https://github.com/jashkenas/underscore/blob/master/LICENSE
Files: static/third/winchan/*
Copyright: 2012 Lloyd Hilaiel
License: Expat
Comment: https://github.com/mozilla/winchan
Files: static/third/xdate/*
Copyright: 2010 C. F., Wong
License: Expat
Files: static/third/zocial/*
Copyright: Sam Collins
License: Expat
Files: tools/inject-messages/othello
Copyright: Shakespeare
License: public-domain
Files: tools/jslint/jslint.js
Copyright: 2002 Douglas Crockford
License: XXX-good-not-evil
Files: tools/python-proxy
Copyright: 2009 F.bio Domingues
License: Expat
Files: tools/review
Copyright: 2010 Ksplice, Inc.
License: Apache-2.0
Files: zerver/lib/bugdown/codehilite.py zerver/lib/bugdown/fenced_code.py
Copyright: 2006-2008 Waylan Limberg
License: BSD-3-Clause
Comment: https://pypi.python.org/pypi/Markdown
Files: zerver/lib/ccache.py
Copyright: 2013 David Benjamin and Alan Huang
License: Expat
Files: frontend_tests/casperjs/*
Copyright: 2011-2012 Nicolas Perriault
Joyent, Inc. and other Node contributors
License: Expat
Files: frontend_tests/casperjs/modules/vendors/*
Copyright: 2011, Jeremy Ashkenas
License: Expat
License: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
.
http://www.apache.org/licenses/LICENSE-2.0
.
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
.
On Debian systems, the full text of the Apache License version 2 can
be found in /usr/share/common-licenses/Apache-2.0.
License: BSD-2-clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice(s), this list of conditions and the following disclaimer
unmodified other than the allowable addition of one or more
copyright notices.
2. Redistributions in binary form must reproduce the above copyright
notice(s), this list of conditions and the following disclaimer in
the documentation and/or other materials provided with the
distribution.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) ``AS IS'' AND ANY
EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S) BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License: BSD-3-Clause
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
.
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
License: CC-0-1.0
Creative Commons CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION
ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE
USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND
DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT
OR THE INFORMATION OR WORKS PROVIDED HEREUNDER.
.
Statement of Purpose
.
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work
of authorship and/or a database (each, a "Work").
.
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without
fear of later claims of infringement build upon, modify, incorporate in
other works, reuse and redistribute as freely as possible in any form
whatsoever and for any purposes, including without limitation commercial
purposes. These owners may contribute to the Commons to promote the
ideal of a free culture and the further production of creative, cultural
and scientific works, or to gain reputation or greater distribution for
their Work in part through the use and efforts of others.
.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or
she is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under
its terms, with knowledge of his or her Copyright and Related Rights in
the Work and the meaning and intended legal effect of CC0 on those
rights.
.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
.
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
.
ii. moral rights retained by the original author(s) and/or performer(s);
.
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
.
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
.
v. rights protecting the extraction, dissemination, use and reuse of
data in a Work;
.
vi. database rights (such as those arising under Directive 96/9/EC of
the European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation thereof,
including any amended or successor version of such directive); and
.
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or
future medium and for any number of copies, and (iv) for any purpose
whatsoever, including without limitation commercial, advertising or
promotional purposes (the "Waiver"). Affirmer makes the Waiver for the
benefit of each member of the public at large and to the detriment of
Affirmer's heirs and successors, fully intending that such Waiver shall
not be subject to revocation, rescission, cancellation, termination, or
any other legal or equitable action to disrupt the quiet enjoyment of
the Work by the public as contemplated by Affirmer's express Statement
of Purpose.
.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non
exclusive, irrevocable and unconditional license to exercise Affirmer's
Copyright and Related Rights in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or
future medium and for any number of copies, and (iv) for any purpose
whatsoever, including without limitation commercial, advertising or
promotional purposes (the "License"). The License shall be deemed
effective as of the date CC0 was applied by Affirmer to the Work. Should
any part of the License for any reason be judged legally invalid or
ineffective under applicable law, such partial invalidity or
ineffectiveness shall not invalidate the remainder of the License, and
in such case Affirmer hereby affirms that he or she will not (i)
exercise any of his or her remaining Copyright and Related Rights in the
Work or (ii) assert any associated claims and causes of action with
respect to the Work, in either case contrary to Affirmer's express
Statement of Purpose.
.
4. Limitations and Disclaimers.
.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied, statutory
or otherwise, including without limitation warranties of title,
merchantability, fitness for a particular purpose, non infringement, or
the absence of latent or other defects, accuracy, or the present or
absence of errors, whether or not discoverable, all to the greatest
extent permissible under applicable law.
.
c. Affirmer disclaims responsibility for clearing rights of other
persons that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the Work.
.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
License: Expat
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.
License: GPL-2.0
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2, dated June, 1991.
.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
On Debian systems, the complete text of the GNU General Public License
can be found in /usr/share/common-licenses/GPL-2 file.
License: SIL-OFL-1.1
---------------------------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
---------------------------------------------------------------------------
.
PREAMBLE
.
The goals of the Open Font License (OFL) are to stimulate worldwide development
of collaborative font projects, to support the font creation efforts of academic
and linguistic communities, and to provide a free and open framework in which
fonts may be shared and improved in partnership with others.
.
The OFL allows the licensed fonts to be used, studied, modified and redistributed
freely as long as they are not sold by themselves. The fonts, including any
derivative works, can be bundled, embedded, redistributed and/or sold with any
software provided that any reserved names are not used by derivative works. The
fonts and derivatives, however, cannot be released under any other type of license.
The requirement for fonts to remain under this license does not apply to any
document created using the fonts or their derivatives.
.
DEFINITIONS
.
"Font Software" refers to the set of files released by the Copyright Holder(s) under
this license and clearly marked as such. This may include source files, build
scripts and documentation.
.
"Reserved Font Name" refers to any names specified as such after the copyright
statement(s).
.
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
.
"Modified Version" refers to any derivative made by adding to, deleting, or
substituting -- in part or in whole -- any of the components of the Original Version,
by changing formats or by porting the Font Software to a new environment.
.
"Author" refers to any designer, engineer, programmer, technical writer or other
person who contributed to the Font Software.
.
PERMISSION & CONDITIONS
.
Permission is hereby granted, free of charge, to any person obtaining a copy of the
Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell
modified and unmodified copies of the Font Software, subject to the following
conditions:
.
1) Neither the Font Software nor any of its individual components, in Original or
Modified Versions, may be sold by itself.
.
2) Original or Modified Versions of the Font Software may be bundled, redistributed
and/or sold with any software, provided that each copy contains the above copyright
notice and this license. These can be included either as stand-alone text files,
human-readable headers or in the appropriate machine-readable metadata fields within
text or binary files as long as those fields can be easily viewed by the user.
.
3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless
explicit written permission is granted by the corresponding Copyright Holder. This
restriction only applies to the primary font name as presented to the users.
.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall
not be used to promote, endorse or advertise any Modified Version, except to
acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with
their explicit written permission.
.
5) The Font Software, modified or unmodified, in part or in whole, must be distributed
entirely under this license, and must not be distributed under any other license. The
requirement for fonts to remain under this license does not apply to any document
created using the Font Software.
.
TERMINATION
.
This license becomes null and void if any of the above conditions are not met.
.
DISCLAIMER
.
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER
RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR
INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE.

34
Vagrantfile vendored
View File

@@ -1,34 +0,0 @@
# -*- mode: ruby -*-
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
# For LXC. VirtualBox hosts use a different box, described below.
config.vm.box = "fgrehm/trusty64-lxc"
# The Zulip development environment runs on 9991 on the guest.
config.vm.network "forwarded_port", guest: 9991, host: 9991, host_ip: "127.0.0.1"
config.vm.synced_folder ".", "/vagrant", disabled: true
config.vm.synced_folder ".", "/srv/zulip"
config.vm.provider "virtualbox" do |vb, override|
override.vm.box = "ubuntu/trusty64"
# 2GiB seemed reasonable here. The VM OOMs with only 1024MiB.
vb.memory = 2048
end
$provision_script = <<SCRIPT
set -x
set -e
sudo apt-get update
sudo apt-get install -y python-pbs
python /srv/zulip/provision.py
SCRIPT
config.vm.provision "shell",
# We want provision.py to be run with the permissions of the vagrant user.
privileged: false,
inline: $provision_script
end

View File

@@ -1,5 +1,4 @@
from __future__ import absolute_import
from __future__ import print_function
from django.core.management.base import BaseCommand

View File

@@ -1,5 +1,4 @@
from __future__ import absolute_import
from __future__ import print_function
import datetime
import pytz
@@ -20,6 +19,6 @@ class Command(BaseCommand):
date = datetime.datetime.now() - datetime.timedelta(days=1)
else:
date = datetime.datetime.strptime(options["date"], "%Y-%m-%d")
print("Activity data for", date)
print(activity_averages_during_day(date))
print("Please note that the total registered user count is a total for today")
print "Activity data for", date
print activity_averages_during_day(date)
print "Please note that the total registered user count is a total for today"

View File

@@ -1,5 +1,4 @@
from __future__ import absolute_import
from __future__ import print_function
from optparse import make_option
from django.core.management.base import BaseCommand
@@ -64,7 +63,7 @@ def compute_stats(log_level):
logging.info("Top %6s | %s%%" % (size, round(top_percents[size], 1)))
grand_total = sum(total_counts.values())
print(grand_total)
print grand_total
logging.info("%15s | %s" % ("Client", "Percentage"))
for client in total_counts.keys():
logging.info("%15s | %s%%" % (client, round(100. * total_counts[client] / grand_total, 1)))

View File

@@ -1,5 +1,4 @@
from __future__ import absolute_import
from __future__ import print_function
from zerver.lib.statistics import seconds_usage_between
@@ -17,7 +16,7 @@ def analyze_activity(options):
if options["realm"]:
user_profile_query = user_profile_query.filter(realm__domain=options["realm"])
print("Per-user online duration:\n")
print "Per-user online duration:\n"
total_duration = datetime.timedelta(0)
for user_profile in user_profile_query:
duration = seconds_usage_between(user_profile, day_start, day_end)
@@ -26,11 +25,11 @@ def analyze_activity(options):
continue
total_duration += duration
print("%-*s%s" % (37, user_profile.email, duration, ))
print "%-*s%s" % (37, user_profile.email, duration, )
print("\nTotal Duration: %s" % (total_duration,))
print("\nTotal Duration in minutes: %s" % (total_duration.total_seconds() / 60.,))
print("Total Duration amortized to a month: %s" % (total_duration.total_seconds() * 30. / 60.,))
print "\nTotal Duration: %s" % (total_duration,)
print "\nTotal Duration in minutes: %s" % (total_duration.total_seconds() / 60.,)
print "Total Duration amortized to a month: %s" % (total_duration.total_seconds() * 30. / 60.,)
class Command(BaseCommand):
help = """Report analytics of user activity on a per-user and realm basis.
@@ -43,7 +42,7 @@ It will correctly not count server-initiated reloads in the activity statistics.
The duration flag can be used to control how many days to show usage duration for
Usage: python2.7 manage.py analyze_user_activity [--realm=zulip.com] [--date=2013-09-10] [--duration=1]
Usage: python manage.py analyze_user_activity [--realm=zulip.com] [--date=2013-09-10] [--duration=1]
By default, if no date is selected 2013-09-10 is used. If no realm is provided, information
is shown for all realms"""

View File

@@ -1,5 +1,4 @@
from __future__ import absolute_import
from __future__ import print_function
from django.core.management.base import BaseCommand
from django.db.models import Count
@@ -14,13 +13,9 @@ class Command(BaseCommand):
Usage examples:
python2.7 manage.py client_activity
python2.7 manage.py client_activity zulip.com
python2.7 manage.py client_activity jesstess@zulip.com"""
def add_arguments(self, parser):
parser.add_argument('arg', metavar='<arg>', type=str, nargs='?', default=None,
help="realm or user to estimate client activity for")
python manage.py client_activity
python manage.py client_activity zulip.com
python manage.py client_activity jesstess@zulip.com"""
def compute_activity(self, user_activity_objects):
# Report data from the past week.
@@ -49,27 +44,26 @@ python2.7 manage.py client_activity jesstess@zulip.com"""
counts.sort()
for count in counts:
print("%25s %15d" % (count[1], count[0]))
print("Total:", total)
print "%25s %15d" % (count[1], count[0])
print "Total:", total
def handle(self, *args, **options):
if options['arg'] is None:
if len(args) == 0:
# Report global activity.
self.compute_activity(UserActivity.objects.all())
else:
arg = options['arg']
elif len(args) == 1:
try:
# Report activity for a user.
user_profile = get_user_profile_by_email(arg)
user_profile = get_user_profile_by_email(args[0])
self.compute_activity(UserActivity.objects.filter(
user_profile=user_profile))
except UserProfile.DoesNotExist:
try:
# Report activity for a realm.
realm = get_realm(arg)
realm = get_realm(args[0])
self.compute_activity(UserActivity.objects.filter(
user_profile__realm=realm))
except Realm.DoesNotExist:
print("Unknown user or domain %s" % (arg,))
print "Unknown user or domain %s" % (args[0],)
exit(1)

View File

@@ -1,5 +1,4 @@
from __future__ import absolute_import
from __future__ import print_function
import datetime
import pytz
@@ -7,7 +6,7 @@ import pytz
from django.core.management.base import BaseCommand
from django.db.models import Count
from zerver.models import UserProfile, Realm, Stream, Message, Recipient, UserActivity, \
Subscription, UserMessage, get_realm
Subscription, UserMessage
MOBILE_CLIENT_LIST = ["Android", "ios"]
HUMAN_CLIENT_LIST = MOBILE_CLIENT_LIST + ["website"]
@@ -17,10 +16,6 @@ human_messages = Message.objects.filter(sending_client__name__in=HUMAN_CLIENT_LI
class Command(BaseCommand):
help = "Generate statistics on realm activity."
def add_arguments(self, parser):
parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
help="realm to generate statistics for")
def active_users(self, realm):
# Has been active (on the website, for now) in the last 7 days.
activity_cutoff = datetime.datetime.now(tz=pytz.utc) - datetime.timedelta(days=7)
@@ -66,51 +61,52 @@ class Command(BaseCommand):
fraction = 0.0
else:
fraction = numerator / float(denominator)
print("%.2f%% of" % (fraction * 100,), text)
print "%.2f%% of" % (fraction * 100,), text
def handle(self, *args, **options):
if options['realms']:
if args:
try:
realms = [get_realm(domain) for domain in options['realms']]
except Realm.DoesNotExist as e:
print(e)
realms = [Realm.objects.get(domain=domain) for domain in args]
except Realm.DoesNotExist, e:
print e
exit(1)
else:
realms = Realm.objects.all()
for realm in realms:
print(realm.domain)
print realm.domain
user_profiles = UserProfile.objects.filter(realm=realm, is_active=True)
active_users = self.active_users(realm)
num_active = len(active_users)
print("%d active users (%d total)" % (num_active, len(user_profiles)))
print "%d active users (%d total)" % (num_active, len(user_profiles))
streams = Stream.objects.filter(realm=realm).extra(
tables=['zerver_subscription', 'zerver_recipient'],
where=['zerver_subscription.recipient_id = zerver_recipient.id',
'zerver_recipient.type = 2',
'zerver_recipient.type_id = zerver_stream.id',
'zerver_subscription.active = true']).annotate(count=Count("name"))
print("%d streams" % (streams.count(),))
print "%d streams" % (streams.count(),)
for days_ago in (1, 7, 30):
print("In last %d days, users sent:" % (days_ago,))
print "In last %d days, users sent:" % (days_ago,)
sender_quantities = [self.messages_sent_by(user, days_ago) for user in user_profiles]
for quantity in sorted(sender_quantities, reverse=True):
print(quantity, end=' ')
print("")
print quantity,
print ""
print("%d stream messages" % (self.stream_messages(realm, days_ago),))
print("%d one-on-one private messages" % (self.private_messages(realm, days_ago),))
print("%d messages sent via the API" % (self.api_messages(realm, days_ago),))
print("%d group private messages" % (self.group_private_messages(realm, days_ago),))
print "%d stream messages" % (self.stream_messages(realm, days_ago),)
print "%d one-on-one private messages" % (self.private_messages(realm, days_ago),)
print "%d messages sent via the API" % (self.api_messages(realm, days_ago),)
print "%d group private messages" % (self.group_private_messages(realm, days_ago),)
num_notifications_enabled = len([x for x in active_users if x.enable_desktop_notifications == True])
num_notifications_enabled = len(filter(lambda x: x.enable_desktop_notifications == True,
active_users))
self.report_percentage(num_notifications_enabled, num_active,
"active users have desktop notifications enabled")
num_enter_sends = len([x for x in active_users if x.enter_sends])
num_enter_sends = len(filter(lambda x: x.enter_sends, active_users))
self.report_percentage(num_enter_sends, num_active,
"active users have enter-sends")
@@ -124,8 +120,8 @@ class Command(BaseCommand):
starrers = UserMessage.objects.filter(user_profile__in=user_profiles,
flags=UserMessage.flags.starred).values(
"user_profile").annotate(count=Count("user_profile"))
print("%d users have starred %d messages" % (
len(starrers), sum([elt["count"] for elt in starrers])))
print "%d users have starred %d messages" % (
len(starrers), sum([elt["count"] for elt in starrers]))
active_user_subs = Subscription.objects.filter(
user_profile__in=user_profiles, active=True)
@@ -133,20 +129,20 @@ class Command(BaseCommand):
# Streams not in home view
non_home_view = active_user_subs.filter(in_home_view=False).values(
"user_profile").annotate(count=Count("user_profile"))
print("%d users have %d streams not in home view" % (
len(non_home_view), sum([elt["count"] for elt in non_home_view])))
print "%d users have %d streams not in home view" % (
len(non_home_view), sum([elt["count"] for elt in non_home_view]))
# Code block markup
markup_messages = human_messages.filter(
sender__realm=realm, content__contains="~~~").values(
"sender").annotate(count=Count("sender"))
print("%d users have used code block markup on %s messages" % (
len(markup_messages), sum([elt["count"] for elt in markup_messages])))
print "%d users have used code block markup on %s messages" % (
len(markup_messages), sum([elt["count"] for elt in markup_messages]))
# Notifications for stream messages
notifications = active_user_subs.filter(notifications=True).values(
"user_profile").annotate(count=Count("user_profile"))
print("%d users receive desktop notifications for %d streams" % (
len(notifications), sum([elt["count"] for elt in notifications])))
print "%d users receive desktop notifications for %d streams" % (
len(notifications), sum([elt["count"] for elt in notifications]))
print("")
print ""

View File

@@ -1,41 +1,36 @@
from __future__ import absolute_import
from __future__ import print_function
from django.core.management.base import BaseCommand
from django.db.models import Q
from zerver.models import Realm, Stream, Message, Subscription, Recipient, get_realm
from zerver.models import Realm, Stream, Message, Subscription, Recipient
class Command(BaseCommand):
help = "Generate statistics on the streams for a realm."
def add_arguments(self, parser):
parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
help="realm to generate statistics for")
def handle(self, *args, **options):
if options['realms']:
if args:
try:
realms = [get_realm(domain) for domain in options['realms']]
except Realm.DoesNotExist as e:
print(e)
realms = [Realm.objects.get(domain=domain) for domain in args]
except Realm.DoesNotExist, e:
print e
exit(1)
else:
realms = Realm.objects.all()
for realm in realms:
print(realm.domain)
print("------------")
print("%25s %15s %10s" % ("stream", "subscribers", "messages"))
print realm.domain
print "------------"
print "%25s %15s %10s" % ("stream", "subscribers", "messages")
streams = Stream.objects.filter(realm=realm).exclude(Q(name__istartswith="tutorial-"))
invite_only_count = 0
for stream in streams:
if stream.invite_only:
invite_only_count += 1
continue
print("%25s" % (stream.name,), end=' ')
print "%25s" % (stream.name,),
recipient = Recipient.objects.filter(type=Recipient.STREAM, type_id=stream.id)
print("%10d" % (len(Subscription.objects.filter(recipient=recipient, active=True)),), end=' ')
print "%10d" % (len(Subscription.objects.filter(recipient=recipient, active=True)),),
num_messages = len(Message.objects.filter(recipient=recipient))
print("%12d" % (num_messages,))
print("%d invite-only streams" % (invite_only_count,))
print("")
print "%12d" % (num_messages,)
print "%d invite-only streams" % (invite_only_count,)
print ""

View File

@@ -1,43 +1,37 @@
from __future__ import absolute_import
from __future__ import print_function
import datetime
import pytz
from django.core.management.base import BaseCommand
from zerver.models import UserProfile, Realm, Stream, Message, get_realm
from six.moves import range
from zerver.models import UserProfile, Realm, Stream, Message
class Command(BaseCommand):
help = "Generate statistics on user activity."
def add_arguments(self, parser):
parser.add_argument('realms', metavar='<realm>', type=str, nargs='*',
help="realm to generate statistics for")
def messages_sent_by(self, user, week):
start = datetime.datetime.now(tz=pytz.utc) - datetime.timedelta(days=(week + 1)*7)
end = datetime.datetime.now(tz=pytz.utc) - datetime.timedelta(days=week*7)
return Message.objects.filter(sender=user, pub_date__gt=start, pub_date__lte=end).count()
def handle(self, *args, **options):
if options['realms']:
if args:
try:
realms = [get_realm(domain) for domain in options['realms']]
except Realm.DoesNotExist as e:
print(e)
realms = [Realm.objects.get(domain=domain) for domain in args]
except Realm.DoesNotExist, e:
print e
exit(1)
else:
realms = Realm.objects.all()
for realm in realms:
print(realm.domain)
print realm.domain
user_profiles = UserProfile.objects.filter(realm=realm, is_active=True)
print("%d users" % (len(user_profiles),))
print("%d streams" % (len(Stream.objects.filter(realm=realm)),))
print "%d users" % (len(user_profiles),)
print "%d streams" % (len(Stream.objects.filter(realm=realm)),)
for user_profile in user_profiles:
print("%35s" % (user_profile.email,), end=' ')
print "%35s" % (user_profile.email,),
for week in range(10):
print("%5d" % (self.messages_sent_by(user_profile, week)), end=' ')
print("")
print "%5d" % (self.messages_sent_by(user_profile, week)),
print ""

View File

@@ -1,4 +1,3 @@
from __future__ import absolute_import
from django.db import connection
from django.template import RequestContext, loader
from django.utils.html import mark_safe
@@ -16,10 +15,6 @@ import itertools
import time
import re
import pytz
from six.moves import filter
from six.moves import map
from six.moves import range
from six.moves import zip
eastern_tz = pytz.timezone('US/Eastern')
def make_table(title, cols, rows, has_row_class=False):
@@ -27,7 +22,7 @@ def make_table(title, cols, rows, has_row_class=False):
if not has_row_class:
def fix_row(row):
return dict(cells=row, row_class=None)
rows = list(map(fix_row, rows))
rows = map(fix_row, rows)
data = dict(title=title, cols=cols, rows=rows)
@@ -42,7 +37,7 @@ def dictfetchall(cursor):
"Returns all rows from a cursor as a dict"
desc = cursor.description
return [
dict(list(zip([col[0] for col in desc], row)))
dict(zip([col[0] for col in desc], row))
for row in cursor.fetchall()
]
@@ -177,7 +172,9 @@ def realm_summary_table(realm_minutes):
GROUP BY realm_id
) at_risk_counts
ON at_risk_counts.realm_id = realm.id
WHERE EXISTS (
WHERE
realm.domain not in ('customer4.invalid', 'wdaher.com')
AND EXISTS (
SELECT *
FROM zerver_useractivity ua
JOIN zerver_userprofile up
@@ -229,9 +226,14 @@ def realm_summary_table(realm_minutes):
# Count active sites
def meets_goal(row):
# The wdaher.com realm doesn't count toward company goals for
# obvious reasons, and customer4.invalid is essentially a dup
# for users.customer4.invalid.
if row['domain'] in ['customer4.invalid', 'wdaher.com']:
return False
return row['active_user_count'] >= 5
num_active_sites = len(list(filter(meets_goal, rows)))
num_active_sites = len(filter(meets_goal, rows))
# create totals
total_active_user_count = 0
@@ -380,7 +382,7 @@ def ad_hoc_queries():
cursor = connection.cursor()
cursor.execute(query)
rows = cursor.fetchall()
rows = list(map(list, rows))
rows = map(list, rows)
cursor.close()
def fix_rows(i, fixup_func):
@@ -614,7 +616,7 @@ def raw_user_activity_table(records):
format_date_for_activity_reports(record.last_visit)
]
rows = list(map(row, records))
rows = map(row, records)
title = 'Raw Data'
return make_table(title, cols, rows)
@@ -821,7 +823,7 @@ def get_realm_activity(request, realm):
all_user_records = {}
try:
admins = get_realm(realm).get_admin_users()
admins = Realm.objects.get(domain=realm).get_admin_users()
except Realm.DoesNotExist:
return HttpResponseNotFound("Realm %s does not exist" % (realm,))

View File

@@ -30,25 +30,15 @@ file is as follows:
[api]
key=<api key from the web interface>
email=<your email address>
site=<your Zulip server's URI>
insecure=<true or false, true means do not verify the server certificate>
cert_bundle=<path to a file containing CA or server certificates to trust>
If omitted, these settings have the following defaults:
If you are using Zulip Enterprise, you should also add
site=https://api.zulip.com
insecure=false
cert_bundle=<the default CA bundle trusted by Python>
site=<your Zulip Enterprise server's URI>
Alternatively, you may explicitly use "--user" and "--api-key" in our
examples, which is especially useful if you are running several bots
which share a home directory.
The command line equivalents for other configuration options are:
--site=<your Zulip server's URI>
--insecure
--cert-bundle=<file>
which share a home directory. There is also a "--site" option for
setting the Zulip Enterprise server on the command line.
You can obtain your Zulip API key, create bots, and manage bots all
from your Zulip [settings page](https://zulip.com/#settings).
@@ -114,46 +104,3 @@ Alternatively, if you don't want to use your ~/.zuliprc file:
--api-key a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 \
hamlet@example.com cordelia@example.com -m \
"Conscience doth make cowards of us all."
#### Working with an untrusted server certificate
If your server has either a self-signed certificate, or a certificate signed
by a CA that you don't wish to globally trust then by default the API will
fail with an SSL verification error.
You can add `insecure=true` to your .zuliprc file.
[api]
site=https://zulip.example.com
insecure=true
This disables verification of the server certificate, so connections are
encrypted but unauthenticated. This is not secure, but may be good enough
for a development environment.
You can explicitly trust the server certificate using `cert_bundle=<filename>`
in your .zuliprc file.
[api]
site=https://zulip.example.com
cert_bundle=/home/bots/certs/zulip.example.com.crt
You can also explicitly trust a different set of Certificate Authorities from
the default bundle that is trusted by Python. For example to trust a company
internal CA.
[api]
site=https://zulip.example.com
cert_bundle=/home/bots/certs/example.com.ca-bundle
Save the server certificate (or the CA certificate) in its own file,
converting to PEM format first if necessary.
Verify that the certificate you have saved is the same as the one on the
server.
The `cert_bundle` option trusts the server / CA certificate only for
interaction with the zulip site, and is relatively secure.
Note that a certificate bundle is merely one or more certificates combined
into a single file.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# zulip-send -- Sends a message to the specified recipients.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright © 2012-2014 Zulip, Inc.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2014 Zulip, Inc.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.
@@ -29,7 +29,7 @@ usage = """list-subscriptions --user=<bot's email address> --api-key=<bot's api
Prints out a list of the user's subscriptions.
Example: list-subscriptions --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
Example: list-subscriptions --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
"""

View File

@@ -1,51 +0,0 @@
#!/usr/bin/env python2.7
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.
#
# 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.
import sys
import os
import optparse
usage = """print-events --user=<bot's email address> --api-key=<bot's api key> [options]
Prints out certain events received by the indicated bot or user matching the filter below.
Example: print-events --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
"""
sys.path.append(os.path.join(os.path.dirname(__file__), '..'))
import zulip
parser = optparse.OptionParser(usage=usage)
parser.add_option_group(zulip.generate_option_group(parser))
(options, args) = parser.parse_args()
client = zulip.init_from_options(options)
def print_event(event):
print event
# This is a blocking call, and will continuously poll for new events
# Note also the filter here is messages to the stream Denmark; if you
# don't specify event_types it'll print all events.
client.call_on_each_event(print_event, event_types=["message"], narrow=[["stream", "Denmark"]])

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.
@@ -29,7 +29,7 @@ usage = """print-messages --user=<bot's email address> --api-key=<bot's api key>
Prints out each message received by the indicated bot or user.
Example: print-messages --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
Example: print-messages --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
"""

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.
@@ -29,7 +29,7 @@ usage = """print-next-message --user=<bot's email address> --api-key=<bot's api
Prints out the next message received by the user.
Example: print-next-messages --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
Example: print-next-messages --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
"""

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.
@@ -29,8 +29,8 @@ usage = """subscribe --user=<bot's email address> --api-key=<bot's api key> [opt
Ensures the user is subscribed to the listed streams.
Examples: subscribe --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams=foo
subscribe --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams='foo bar'
Examples: subscribe --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams=foo
subscribe --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams='foo bar'
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
"""

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Copyright © 2012 Zulip, Inc.
@@ -29,8 +29,8 @@ usage = """unsubscribe --user=<bot's email address> --api-key=<bot's api key> [
Ensures the user is not subscribed to the listed streams.
Examples: unsubscribe --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams=foo
unsubscribe --user=username@example.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams='foo bar'
Examples: unsubscribe --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams=foo
unsubscribe --user=tabbott@zulip.com --api-key=a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5 --streams='foo bar'
You can omit --user and --api-key arguments if you have a properly set up ~/.zuliprc
"""

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright © 2014 Zulip, Inc.
@@ -38,7 +38,7 @@ ZULIP_STREAM_NAME = "asana"
### OPTIONAL CONFIGURATION ###
# Set to None for logging to stdout when testing, and to a file for
# logging in production.
# logging when deployed.
#LOG_FILE = "/var/tmp/zulip_asana.log"
LOG_FILE = None
@@ -49,7 +49,7 @@ RESUME_FILE = "/var/tmp/zulip_asana.state"
# When initially started, how many hours of messages to include.
ASANA_INITIAL_HISTORY_HOURS = 1
# Set this to your Zulip API server URI
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
ZULIP_SITE = "https://api.zulip.com"
# If properly installed, the Zulip API should be in your import

21
api/integrations/asana/zulip_asana_mirror Executable file → Normal file
View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Asana integration for Zulip
@@ -22,17 +22,11 @@
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# The "zulip_asana_mirror" script is run continuously, possibly on a work computer
# or preferably on a server.
#
# When restarted, it will attempt to pick up where it left off.
#
# python-dateutil is a dependency for this script.
import base64
from datetime import datetime, timedelta
import dateutil.parser
import dateutil.tz
import json
import logging
import os
@@ -40,15 +34,6 @@ import time
import urllib2
import sys
try:
import dateutil.parser
import dateutil.tz
except ImportError, e:
print >>sys.stderr, e
print >>sys.stderr, "Please install the python-dateutil package."
exit(1)
sys.path.insert(0, os.path.dirname(__file__))
import zulip_asana_config as config
VERSION = "0.9"

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright © 2014 Zulip, Inc.
@@ -41,7 +41,7 @@ ZULIP_STREAM_NAME = "basecamp"
## path, but if not, set a custom path below
ZULIP_API_PATH = None
# Set this to your Zulip API server URI
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
ZULIP_SITE = "https://api.zulip.com"
# If you wish to log to a file rather than stdout/stderr,

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Zulip mirror of Basecamp activity
@@ -49,7 +49,7 @@ client = zulip.Client(
site=config.ZULIP_SITE,
api_key=config.ZULIP_API_KEY,
client="ZulipBasecamp/" + VERSION)
user_agent = "Basecamp To Zulip Mirroring script (zulip-devel@googlegroups.com)"
user_agent = "Basecamp To Zulip Mirroring script (support@zulip.com)"
htmlParser = HTMLParser()
# find some form of JSON loader/dumper, with a preference order for speed.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright © 2014 Zulip, Inc.
@@ -50,7 +50,7 @@ ZULIP_TICKETS_STREAM_NAME = "tickets"
# path, but if not, set a custom path below
ZULIP_API_PATH = None
# Set this to your Zulip API server URI
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
ZULIP_SITE = "https://api.zulip.com"
# If you wish to log to a file rather than stdout/stderr,

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Zulip mirror of Codebase HQ activity
@@ -22,12 +22,12 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# The "zulip_codebase_mirror" script is run continuously, possibly on a work
# computer or preferably on a server.
# The "codebase-mirror.py" script is run continuously, possibly on a work computer
# or preferably on a server.
#
# When restarted, it will attempt to pick up where it left off.
#
# python-dateutil is a dependency for this script.
# You may need to install the python-requests library, as well as python-dateutil
import requests
import logging
@@ -37,13 +37,7 @@ import os
from datetime import datetime, timedelta
try:
import dateutil.parser
except ImportError, e:
print >>sys.stderr, e
print >>sys.stderr, "Please install the python-dateutil package."
exit(1)
import dateutil.parser
sys.path.insert(0, os.path.dirname(__file__))
import zulip_codebase_config as config
@@ -58,7 +52,7 @@ client = zulip.Client(
site=config.ZULIP_SITE,
api_key=config.ZULIP_API_KEY,
client="ZulipCodebase/" + VERSION)
user_agent = "Codebase To Zulip Mirroring script (zulip-devel@googlegroups.com)"
user_agent = "Codebase To Zulip Mirroring script (support@zulip.com)"
# find some form of JSON loader/dumper, with a preference order for speed.
json_implementations = ['ujson', 'cjson', 'simplejson', 'json']

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Zulip notification post-receive hook.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright © 2014 Zulip, Inc.
@@ -61,5 +61,5 @@ def format_commit_message(author, subject, commit_id):
## path, but if not, set a custom path below
ZULIP_API_PATH = None
# Set this to your Zulip server's API URI
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
ZULIP_SITE = "https://api.zulip.com"

View File

@@ -1,8 +1,8 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Zulip hook for Mercurial changeset pushes.
# Copyright © 2012-2014 Zulip, Inc.
# Copyright © 2012-2014 Zulip, Inc.
#
# 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,10 +25,8 @@
#
# This hook is called when changesets are pushed to the master repository (ie
# `hg push`). See https://zulip.com/integrations for installation instructions.
from __future__ import absolute_import
import zulip
from six.moves import range
VERSION = "0.9"

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import optparse
import zulip

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright © 2012-2014 Zulip, Inc.
@@ -33,7 +33,6 @@ For example:
1234 //depot/security/src/
'''
from __future__ import print_function
import os
import sys
@@ -60,12 +59,12 @@ try:
changelist = int(sys.argv[1])
changeroot = sys.argv[2]
except IndexError:
print("Wrong number of arguments.\n\n", end=' ', file=sys.stderr)
print(__doc__, file=sys.stderr)
print >> sys.stderr, "Wrong number of arguments.\n\n",
print >> sys.stderr, __doc__
sys.exit(-1)
except ValueError:
print("First argument must be an integer.\n\n", end=' ', file=sys.stderr)
print(__doc__, file=sys.stderr)
print >> sys.stderr, "First argument must be an integer.\n\n",
print >> sys.stderr, __doc__
sys.exit(-1)
metadata = git_p4.p4_describe(changelist)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright © 2014 Zulip, Inc.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# RSS integration for Zulip
@@ -136,7 +136,7 @@ def strip_tags(html):
def compute_entry_hash(entry):
entry_time = entry.get("published", entry.get("updated"))
entry_id = entry.get("id", entry.get("link"))
return hashlib.md5(entry_id + str(entry_time)).hexdigest()
return hashlib.md5(entry_id + entry_time).hexdigest()
def elide_subject(subject):
MAX_TOPIC_LENGTH = 60
@@ -184,7 +184,7 @@ for feed_url in feed_urls:
entry_hash = compute_entry_hash(entry)
# An entry has either been published or updated.
entry_time = entry.get("published_parsed", entry.get("updated_parsed"))
if entry_time is not None and (time.time() - calendar.timegm(entry_time)) > OLDNESS_THRESHOLD * 60 * 60 * 24:
if (time.time() - calendar.timegm(entry_time)) > OLDNESS_THRESHOLD * 60 * 60 * 24:
# As a safeguard against misbehaving feeds, don't try to process
# entries older than some threshold.
continue

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Zulip notification post-commit hook.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright © 2014 Zulip, Inc.
@@ -53,5 +53,5 @@ def commit_notice_destination(path, commit):
## path, but if not, set a custom path below
ZULIP_API_PATH = None
# Set this to your Zulip server's API URI
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
ZULIP_SITE = "https://api.zulip.com"

View File

@@ -107,8 +107,8 @@ class ZulipPlugin(Component):
field_changes = []
for key in old_values.keys():
if key == "description":
content += '- Changed %s from %s\n\nto %s' % (key, markdown_block(old_values.get(key)),
markdown_block(ticket.values.get(key)))
content += '- Changed %s from %s to %s' % (key, markdown_block(old_values.get(key)),
markdown_block(ticket.values.get(key)))
elif old_values.get(key) == "":
field_changes.append('%s: => **%s**' % (key, ticket.values.get(key)))
elif ticket.values.get(key) == "":

View File

@@ -47,5 +47,5 @@ TRAC_NOTIFY_FIELDS = ["description", "summary", "resolution", "comment", "owner"
## path, but if not, set a custom path below
ZULIP_API_PATH = None
# Set this to your Zulip API server URI
# If you're using Zulip Enterprise, set this to your Zulip Enterprise server
ZULIP_SITE = "https://api.zulip.com"

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Twitter integration for Zulip
@@ -126,9 +126,9 @@ except ConfigParser.NoOptionError:
user_id = options.twitter_id
client = zulip.Client(
email=options.zulip_email,
api_key=options.zulip_api_key,
site=options.zulip_site,
email=options.email,
api_key=options.api_key,
site=options.site,
client="ZulipTwitter/" + VERSION,
verbose=True)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Twitter search integration for Zulip

View File

@@ -1,7 +1,6 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from __future__ import print_function
import os
import sys
@@ -10,8 +9,8 @@ import itertools
def version():
version_py = os.path.join(os.path.dirname(__file__), "zulip", "__init__.py")
with open(version_py) as in_handle:
version_line = next(itertools.dropwhile(lambda x: not x.startswith("__version__"),
in_handle))
version_line = itertools.dropwhile(lambda x: not x.startswith("__version__"),
in_handle).next()
version = version_line.split('=')[-1].strip().replace('"', '')
return version
@@ -27,7 +26,7 @@ package_info = dict(
version=version(),
description='Bindings for the Zulip message API',
author='Zulip, Inc.',
author_email='zulip-devel@googlegroups.com',
author_email='support@zulip.com',
classifiers=[
'Development Status :: 3 - Alpha',
'Environment :: Web Environment',
@@ -61,13 +60,13 @@ except ImportError:
try:
import simplejson
except ImportError:
print("simplejson is not installed", file=sys.stderr)
print >>sys.stderr, "simplejson is not installed"
sys.exit(1)
try:
import requests
assert(LooseVersion(requests.__version__) >= LooseVersion('0.12.1'))
except (ImportError, AssertionError):
print("requests >=0.12.1 is not installed", file=sys.stderr)
print >>sys.stderr, "requests >=0.12.1 is not installed"
sys.exit(1)

View File

@@ -20,8 +20,6 @@
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from __future__ import print_function
from __future__ import absolute_import
import simplejson
import requests
import time
@@ -31,19 +29,14 @@ import sys
import os
import optparse
import platform
import urllib
import random
from distutils.version import LooseVersion
from six.moves.configparser import SafeConfigParser
from ConfigParser import SafeConfigParser
import logging
import six
__version__ = "0.2.4"
logger = logging.getLogger(__name__)
# Check that we have a recent enough version
# Older versions don't provide the 'json' attribute on responses.
assert(LooseVersion(requests.__version__) >= LooseVersion('0.12.1'))
@@ -52,117 +45,55 @@ requests_json_is_function = callable(requests.Response.json)
API_VERSTRING = "v1/"
class CountingBackoff(object):
def __init__(self, maximum_retries=10, timeout_success_equivalent=None):
self.number_of_retries = 0
self.maximum_retries = maximum_retries
self.timeout_success_equivalent = timeout_success_equivalent
self.last_attempt_time = 0
def keep_going(self):
self._check_success_timeout()
return self.number_of_retries < self.maximum_retries
def succeed(self):
self.number_of_retries = 0
self.last_attempt_time = time.time()
def fail(self):
self._check_success_timeout()
self.number_of_retries = min(self.number_of_retries + 1,
self.maximum_retries)
self.last_attempt_time = time.time()
def _check_success_timeout(self):
if (self.timeout_success_equivalent is not None
and self.last_attempt_time != 0
and time.time() - self.last_attempt_time > self.timeout_success_equivalent):
self.number_of_retries = 0
class RandomExponentialBackoff(CountingBackoff):
def fail(self):
super(RandomExponentialBackoff, self).fail()
# Exponential growth with ratio sqrt(2); compute random delay
# between x and 2x where x is growing exponentially
delay_scale = int(2 ** (self.number_of_retries / 2.0 - 1)) + 1
delay = delay_scale + random.randint(1, delay_scale)
message = "Sleeping for %ss [max %s] before retrying." % (delay, delay_scale * 2)
try:
logger.warning(message)
except NameError:
print(message)
time.sleep(delay)
def _default_client():
return "ZulipPython/" + __version__
def generate_option_group(parser, prefix=''):
group = optparse.OptionGroup(parser, 'Zulip API configuration')
group.add_option('--%ssite' % (prefix,),
dest="zulip_site",
help="Zulip server URI",
def generate_option_group(parser):
group = optparse.OptionGroup(parser, 'API configuration')
group.add_option('--site',
help="Zulip Enterprise server URI (if using Zulip Enterprise)",
default=None)
group.add_option('--%sapi-key' % (prefix,),
dest="zulip_api_key",
group.add_option('--api-key',
action='store')
group.add_option('--%suser' % (prefix,),
dest='zulip_email',
group.add_option('--user',
dest='email',
help='Email address of the calling bot or user.')
group.add_option('--%sconfig-file' % (prefix,),
group.add_option('--config-file',
action='store',
dest="zulip_config_file",
help='Location of an ini file containing the\nabove information. (default ~/.zuliprc)')
group.add_option('-v', '--verbose',
action='store_true',
help='Provide detailed output.')
group.add_option('--%sclient' % (prefix,),
group.add_option('--client',
action='store',
default=None,
dest="zulip_client",
help=optparse.SUPPRESS_HELP)
group.add_option('--insecure',
action='store_true',
dest='insecure',
help='''Do not verify the server certificate.
The https connection will not be secure.''')
group.add_option('--cert-bundle',
action='store',
dest='cert_bundle',
help='''Specify a file containing either the
server certificate, or a set of trusted
CA certificates. This will be used to
verify the server's identity. All
certificates should be PEM encoded.''')
return group
def init_from_options(options, client=None):
if options.zulip_client is not None:
client = options.zulip_client
if options.client is not None:
client = options.client
elif client is None:
client = _default_client()
return Client(email=options.zulip_email, api_key=options.zulip_api_key,
config_file=options.zulip_config_file, verbose=options.verbose,
site=options.zulip_site, client=client,
cert_bundle=options.cert_bundle, insecure=options.insecure)
def get_default_config_filename():
config_file = os.path.join(os.environ["HOME"], ".zuliprc")
if (not os.path.exists(config_file) and
os.path.exists(os.path.join(os.environ["HOME"], ".humbugrc"))):
raise RuntimeError("The Zulip API configuration file is now ~/.zuliprc; please run:\n\n mv ~/.humbugrc ~/.zuliprc\n")
return config_file
return Client(email=options.email, api_key=options.api_key,
config_file=options.config_file, verbose=options.verbose,
site=options.site, client=client)
class Client(object):
def __init__(self, email=None, api_key=None, config_file=None,
verbose=False, retry_on_errors=True,
site=None, client=None,
cert_bundle=None, insecure=None):
site=None, client=None):
if client is None:
client = _default_client()
if config_file is None:
config_file = get_default_config_filename()
if os.path.exists(config_file):
if None in (api_key, email):
if config_file is None:
config_file = os.path.join(os.environ["HOME"], ".zuliprc")
if (not os.path.exists(config_file) and
os.path.exists(os.path.join(os.environ["HOME"], ".humbugrc"))):
raise RuntimeError("The Zulip API configuration file is now ~/.zuliprc; please run:\n\n mv ~/.humbugrc ~/.zuliprc\n")
if not os.path.exists(config_file):
raise RuntimeError("api_key or email not specified and %s does not exist"
% (config_file,))
config = SafeConfigParser()
with file(config_file, 'r') as f:
config.readfp(f, config_file)
@@ -172,22 +103,6 @@ class Client(object):
email = config.get("api", "email")
if site is None and config.has_option("api", "site"):
site = config.get("api", "site")
if cert_bundle is None and config.has_option("api", "cert_bundle"):
cert_bundle = config.get("api", "cert_bundle")
if insecure is None and config.has_option("api", "insecure"):
# Be quite strict about what is accepted so that users don't
# disable security unintentionally.
insecure_setting = config.get("api", "insecure").lower()
if insecure_setting == "true":
insecure = True
elif insecure_setting == "false":
insecure = False
else:
raise RuntimeError("insecure is set to '%s', it must be 'true' or 'false' if it is used in %s"
% (insecure_setting, config_file))
elif None in (api_key, email):
raise RuntimeError("api_key or email not specified and %s does not exist"
% (config_file,))
self.api_key = api_key
self.email = email
@@ -207,27 +122,9 @@ class Client(object):
self.retry_on_errors = retry_on_errors
self.client_name = client
if insecure:
self.tls_verification=False
elif cert_bundle is not None:
if not os.path.isfile(cert_bundle):
raise RuntimeError("tls bundle '%s' does not exist"
%(cert_bundle,))
self.tls_verification=cert_bundle
else:
# Default behavior: verify against system CA certificates
self.tls_verification=True
def get_user_agent(self):
vendor = ''
vendor_version = ''
try:
vendor = platform.system()
vendor_version = platform.release()
except IOError:
# If the calling process is handling SIGCHLD, platform.system() can
# fail with an IOError. See http://bugs.python.org/issue9127
pass
vendor = platform.system()
vendor_version = platform.release()
if vendor == "Linux":
vendor, vendor_version, dummy = platform.linux_distribution()
@@ -246,7 +143,7 @@ class Client(object):
request = {}
for (key, val) in orig_request.iteritems():
if not (isinstance(val, str) or isinstance(val, six.text_type)):
if not (isinstance(val, str) or isinstance(val, unicode)):
request[key] = simplejson.dumps(val)
else:
request[key] = val
@@ -276,9 +173,9 @@ class Client(object):
def end_error_retry(succeeded):
if query_state["had_error_retry"] and self.verbose:
if succeeded:
print("Success!")
print "Success!"
else:
print("Failed!")
print "Failed!"
while True:
try:
@@ -292,7 +189,7 @@ class Client(object):
urlparse.urljoin(self.base_url, url),
auth=requests.auth.HTTPBasicAuth(self.email,
self.api_key),
verify=self.tls_verification, timeout=90,
verify=True, timeout=90,
headers={"User-agent": self.get_user_agent()},
**kwargs)
@@ -344,17 +241,13 @@ class Client(object):
@classmethod
def _register(cls, name, url=None, make_request=(lambda request={}: request),
method="POST", computed_url=None, **query_kwargs):
method="POST", **query_kwargs):
if url is None:
url = name
def call(self, *args, **kwargs):
request = make_request(*args, **kwargs)
if computed_url is not None:
req_url = computed_url(request)
else:
req_url = url
return self.do_api_query(request, API_VERSTRING + req_url, method=method, **query_kwargs)
call.__name__ = name
return self.do_api_query(request, API_VERSTRING + url, method=method, **query_kwargs)
call.func_name = name
setattr(cls, name, call)
def call_on_each_event(self, callback, event_types=None, narrow=[]):
@@ -367,7 +260,7 @@ class Client(object):
if 'error' in res.get('result'):
if self.verbose:
print("Server returned error:\n%s" % res['msg'])
print "Server returned error:\n%s" % res['msg']
time.sleep(1)
else:
return (res['queue_id'], res['last_event_id'])
@@ -381,13 +274,13 @@ class Client(object):
if 'error' in res.get('result'):
if res["result"] == "http-error":
if self.verbose:
print("HTTP error fetching events -- probably a server restart")
print "HTTP error fetching events -- probably a server restart"
elif res["result"] == "connection-error":
if self.verbose:
print("Connection error fetching events -- probably server is temporarily down?")
print "Connection error fetching events -- probably server is temporarily down?"
else:
if self.verbose:
print("Server returned error:\n%s" % res["msg"])
print "Server returned error:\n%s" % res["msg"]
if res["msg"].startswith("Bad event queue id:"):
# Our event queue went away, probably because
# we were asleep or the server restarted
@@ -459,7 +352,6 @@ Client._register('update_message', method='PATCH', url='messages', make_request=
Client._register('get_messages', method='GET', url='messages/latest', longpolling=True)
Client._register('get_events', url='events', method='GET', longpolling=True, make_request=(lambda **kwargs: kwargs))
Client._register('register', make_request=_mk_events)
Client._register('export', method='GET', url='export')
Client._register('deregister', url="events", method="DELETE", make_request=_mk_deregister)
Client._register('get_profile', method='GET', url='users/me')
Client._register('get_streams', method='GET', url='streams', make_request=_kwargs_to_dict)
@@ -467,8 +359,5 @@ Client._register('get_members', method='GET', url='users')
Client._register('list_subscriptions', method='GET', url='users/me/subscriptions')
Client._register('add_subscriptions', url='users/me/subscriptions', make_request=_mk_subs)
Client._register('remove_subscriptions', method='PATCH', url='users/me/subscriptions', make_request=_mk_rm_subs)
Client._register('get_subscribers', method='GET',
computed_url=lambda request: 'streams/%s/members' % (urllib.quote(request['stream'], safe=''),),
make_request=_kwargs_to_dict)
Client._register('render_message', method='GET', url='messages/render')
Client._register('create_user', method='POST', url='users')

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import xml.etree.ElementTree as ET
import subprocess

View File

@@ -1,202 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@@ -1,13 +0,0 @@
This directory contains images adapted from the Noto project. For more detail
about Noto, please refer to:
https://code.google.com/p/noto/
These images were generated from the git repository at
<https://android.googlesource.com/platform/external/noto-fonts> as of
90372d894b5d9c9f2a111315d2eb3b8de1979ee4
and
<https://android.googlesource.com/platform/frameworks/base> at
07912f876c8639f811b06831465c14c4a3b17663.

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
from __future__ import absolute_import
import os

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import sys
import logging
import os

View File

@@ -11,5 +11,5 @@ ZULIP_DIR=/home/zulip/deployments/current
STATE_DIR=/var/lib/nagios_state
STATE_FILE=$STATE_DIR/check-rabbitmq-consumers-$queue
"$ZULIP_DIR/bots/check-rabbitmq-consumers" "--queue=$queue" &> "${STATE_FILE}-tmp";
mv "${STATE_FILE}-tmp" "$STATE_FILE"
$ZULIP_DIR/bots/check-rabbitmq-consumers --queue=$queue &> ${STATE_FILE}-tmp;
mv ${STATE_FILE}-tmp $STATE_FILE

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import sys
import time
import optparse

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import sys
import time

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import sys
import re
@@ -6,14 +6,8 @@ import time
import os
import subprocess
WARN_THRESHOLD_DEFAULT = 10
WARN_THRESHOLD = {
'missedmessage_emails': 45,
}
CRIT_THRESHOLD_DEFAULT = 50
CRIT_THRESHOLD = {
'missedmessage_emails': 70,
}
WARN_THRESHOLD = 10
CRIT_THRESHOLD = 50
states = {
0: "OK",
@@ -39,10 +33,10 @@ for line in output.split("\n"):
queue = m.group(1)
count = int(m.group(2))
this_status = 0
if count > CRIT_THRESHOLD.get(queue, CRIT_THRESHOLD_DEFAULT):
if count > CRIT_THRESHOLD:
this_status = 2
warn_queues.append(queue)
elif count > WARN_THRESHOLD.get(queue, WARN_THRESHOLD_DEFAULT):
elif count > WARN_THRESHOLD:
this_status = max(status, 1)
warn_queues.append(queue)
@@ -56,7 +50,7 @@ now_struct = time.gmtime(now)
# While we are sending digest emails, at 11am each weekday, the mail queues can
# get backed up; don't alert on those.
if not set(warn_queues) - set(("missedmessage_emails", "digest_emails")) and \
now_struct.tm_hour == 15 and now_struct.tm_min < 25:
now_struct.tm_hour == 16 and now_struct.tm_min < 15:
status = 0
print("%s|%s|%s|processing digests, not alerting on elevated mail queues" % (
now, status, states[status]))

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import time
def nagios_from_file(results_file):

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import sys
import time
import datetime
@@ -47,7 +47,7 @@ parser.add_option_group(zulip.generate_option_group(parser))
(options, args) = parser.parse_args()
if not (options.zulip_email and options.calendar):
if not (options.user and options.calendar):
parser.error('You must specify --user and --calendar')
try:

View File

@@ -1,4 +1,4 @@
#! /usr/bin/env python2.7
#! /usr/bin/env python
#
# EXPERIMENTAL
# IRC <=> Zulip mirroring bot
@@ -6,18 +6,15 @@
# Setup: First, you need to install python-irc version 8.5.3
# (https://bitbucket.org/jaraco/irc)
from __future__ import print_function
import irc.bot
import irc.strings
from irc.client import ip_numstr_to_quad, ip_quad_to_numstr
import zulip
import optparse
IRC_DOMAIN = "irc.example.com"
def zulip_sender(sender_string):
nick = sender_string.split("!")[0]
return nick + "@" + IRC_DOMAIN
return nick + "@irc.zulip.com"
class IRCBot(irc.bot.SingleServerIRCBot):
def __init__(self, channel, nickname, server, port=6667):
@@ -50,33 +47,33 @@ class IRCBot(irc.bot.SingleServerIRCBot):
def on_privmsg(self, c, e):
content = e.arguments[0]
sender = zulip_sender(e.source)
if sender.endswith("_zulip@" + IRC_DOMAIN):
if sender.endswith("_zulip@irc.zulip.com"):
return
# Forward the PM to Zulip
print(zulip_client.send_message({
print zulip_client.send_message({
"sender": sender,
"type": "private",
"to": "username@example.com",
"to": "tabbott@zulip.com",
"content": content,
}))
})
def on_pubmsg(self, c, e):
content = e.arguments[0]
stream = e.target
sender = zulip_sender(e.source)
if sender.endswith("_zulip@" + IRC_DOMAIN):
if sender.endswith("_zulip@irc.zulip.com"):
return
# Forward the stream message to Zulip
print(zulip_client.send_message({
print zulip_client.send_message({
"forged": "yes",
"sender": sender,
"type": "stream",
"to": stream,
"subject": "IRC",
"content": content,
}))
})
def on_dccmsg(self, c, e):
c.privmsg("You said: " + e.arguments[0])
@@ -93,12 +90,12 @@ class IRCBot(irc.bot.SingleServerIRCBot):
return
self.dcc_connect(address, port)
usage = """python2.7 irc-mirror.py --server=IRC_SERVER --channel=<CHANNEL> --nick-prefix=<NICK> [optional args]
usage = """python irc-mirror.py --server=IRC_SERVER --channel=<CHANNEL> --nick-prefix=<NICK> [optional args]
Example:
python2.7 irc-mirror.py --irc-server=127.0.0.1 --channel='#test' --nick-prefix=username
--site=https://zulip.example.com --user=irc-bot@example.com
python irc-mirror.py --irc-server=127.0.0.1 --channel='#test' --nick-prefix=tabbott
--site=https://staging.zulip.com --user=irc-bot@zulip.com
--api-key=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Note that "_zulip" will be automatically appended to the IRC nick provided

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env python2.7
# Copyright (C) 2014 Zulip, Inc.
#
# 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.
from __future__ import print_function
import sys
import subprocess
import os
import traceback
import signal
from zulip import RandomExponentialBackoff
def die(signal, frame):
# We actually want to exit, so run os._exit (so as not to be caught and restarted)
os._exit(1)
signal.signal(signal.SIGINT, die)
args = [os.path.join(os.path.dirname(sys.argv[0]), "jabber_mirror_backend.py")]
args.extend(sys.argv[1:])
backoff = RandomExponentialBackoff(timeout_success_equivalent=300)
while backoff.keep_going():
print("Starting Jabber mirroring bot")
try:
ret = subprocess.call(args)
except:
traceback.print_exc()
else:
if ret == 2:
# Don't try again on initial configuration errors
sys.exit(ret)
backoff.fail()
print("")
print("")
print("ERROR: The Jabber mirroring bot is unable to continue mirroring Jabber.")
print("Please contact zulip-devel@googlegroups.com if you need assistance.")
print("")
sys.exit(1)

View File

@@ -1,445 +0,0 @@
#!/usr/bin/env python2.7
#
# Copyright (C) 2013 Permabit, Inc.
# Copyright (C) 2013--2014 Zulip, Inc.
#
# 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.
# The following is a table showing which kinds of messages are handled by the
# mirror in each mode:
#
# Message origin/type --> | Jabber | Zulip
# Mode/sender-, +-----+----+--------+----
# V | MUC | PM | stream | PM
# --------------+-------------+-----+----+--------+----
# | other sender| | x | |
# personal mode +-------------+-----+----+--------+----
# | self sender | | x | x | x
# ------------- +-------------+-----+----+--------+----
# | other sender| x | | |
# public mode +-------------+-----+----+--------+----
# | self sender | | | |
import logging
import threading
import optparse
from sleekxmpp import ClientXMPP, InvalidJID, JID
from sleekxmpp.exceptions import IqError, IqTimeout
from six.moves.configparser import SafeConfigParser
import os, sys, zulip, getpass
import re
__version__ = "1.1"
def room_to_stream(room):
return room + "/xmpp"
def stream_to_room(stream):
return stream.lower().rpartition("/xmpp")[0]
def jid_to_zulip(jid):
suffix = ''
if not jid.username.endswith("-bot"):
suffix = options.zulip_email_suffix
return "%s%s@%s" % (jid.username, suffix, options.zulip_domain)
def zulip_to_jid(email, jabber_domain):
jid = JID(email, domain=jabber_domain)
if (options.zulip_email_suffix
and options.zulip_email_suffix in jid.username
and not jid.username.endswith("-bot")):
jid.username = jid.username.rpartition(options.zulip_email_suffix)[0]
return jid
class JabberToZulipBot(ClientXMPP):
def __init__(self, jid, password, rooms):
if jid.resource:
self.nick = jid.resource
else:
self.nick = jid.username
jid.resource = "zulip"
ClientXMPP.__init__(self, jid, password)
self.rooms = set()
self.rooms_to_join = rooms
self.add_event_handler("session_start", self.session_start)
self.add_event_handler("message", self.message)
self.zulip = None
self.use_ipv6 = False
self.register_plugin('xep_0045') # Jabber chatrooms
self.register_plugin('xep_0199') # XMPP Ping
def set_zulip_client(self, client):
self.zulip = client
def session_start(self, event):
self.get_roster()
self.send_presence()
for room in self.rooms_to_join:
self.join_muc(room)
def join_muc(self, room):
if room in self.rooms:
return
logging.debug("Joining " + room)
self.rooms.add(room)
muc_jid = JID(local=room, domain=options.conference_domain)
xep0045 = self.plugin['xep_0045']
try:
xep0045.joinMUC(muc_jid, self.nick, wait=True)
except InvalidJID:
logging.error("Could not join room: " + str(muc_jid))
return
# Configure the room. Really, we should only do this if the room is
# newly created.
form = None
try:
form = xep0045.getRoomConfig(muc_jid)
except ValueError:
pass
if form:
xep0045.configureRoom(muc_jid, form)
else:
logging.error("Could not configure room: " + str(muc_jid))
def leave_muc(self, room):
if room not in self.rooms:
return
logging.debug("Leaving " + room)
self.rooms.remove(room)
muc_jid = JID(local=room, domain=options.conference_domain)
self.plugin['xep_0045'].leaveMUC(muc_jid, self.nick)
def message(self, msg):
try:
if msg["type"] == "groupchat":
return self.group(msg)
elif msg["type"] == "chat":
return self.private(msg)
else:
logging.warning("Got unexpected message type")
logging.warning(msg)
except Exception:
logging.exception("Error forwarding Jabber => Zulip")
def private(self, msg):
if options.mode == 'public' or msg['thread'] == u'\u1FFFE':
return
sender = jid_to_zulip(msg["from"])
recipient = jid_to_zulip(msg["to"])
zulip_message = dict(
sender = sender,
type = "private",
to = recipient,
content = msg["body"],
)
ret = self.zulip.client.send_message(zulip_message)
if ret.get("result") != "success":
logging.error(ret)
def group(self, msg):
if options.mode == 'personal' or msg["thread"] == u'\u1FFFE':
return
subject = msg["subject"]
if len(subject) == 0:
subject = "(no topic)"
stream = room_to_stream(msg['from'].local)
sender_nick = msg.get_mucnick()
if not sender_nick:
# Messages from the room itself have no nickname. We should not try
# to mirror these
return
jid = self.nickname_to_jid(msg.get_mucroom(), sender_nick)
sender = jid_to_zulip(jid)
zulip_message = dict(
forged = "yes",
sender = sender,
type = "stream",
subject = subject,
to = stream,
content = msg["body"],
)
ret = self.zulip.client.send_message(zulip_message)
if ret.get("result") != "success":
logging.error(ret)
def nickname_to_jid(self, room, nick):
jid = self.plugin['xep_0045'].getJidProperty(room, nick, "jid")
if (jid is None or jid == ''):
return JID(local=nick.replace(' ', ''), domain=self.boundjid.domain)
else:
return jid
class ZulipToJabberBot(object):
def __init__(self, zulip_client):
self.client = zulip_client
self.jabber = None
def set_jabber_client(self, client):
self.jabber = client
def process_event(self, event):
if event['type'] == 'message':
message = event["message"]
if message['sender_email'] != self.client.email:
return
try:
if message['type'] == 'stream':
self.stream_message(message)
elif message['type'] == 'private':
self.private_message(message)
except:
logging.exception("Exception forwarding Zulip => Jabber")
elif event['type'] == 'subscription':
self.process_subscription(event)
elif event['type'] == 'stream':
self.process_stream(event)
def stream_message(self, msg):
stream = msg['display_recipient']
if not stream.endswith("/xmpp"):
return
room = stream_to_room(stream)
jabber_recipient = JID(local=room, domain=options.conference_domain)
outgoing = self.jabber.make_message(
mto = jabber_recipient,
mbody = msg['content'],
mtype = 'groupchat')
outgoing['thread'] = u'\u1FFFE'
outgoing.send()
def private_message(self, msg):
for recipient in msg['display_recipient']:
if recipient["email"] == self.client.email:
continue
if not recipient["is_mirror_dummy"]:
continue
recip_email = recipient['email']
jabber_recipient = zulip_to_jid(recip_email, self.jabber.boundjid.domain)
outgoing = self.jabber.make_message(
mto = jabber_recipient,
mbody = msg['content'],
mtype = 'chat')
outgoing['thread'] = u'\u1FFFE'
outgoing.send()
def process_subscription(self, event):
if event['op'] == 'add':
streams = [s['name'].lower() for s in event['subscriptions']]
streams = [s for s in streams if s.endswith("/xmpp")]
for stream in streams:
self.jabber.join_muc(stream_to_room(stream))
if event['op'] == 'remove':
streams = [s['name'].lower() for s in event['subscriptions']]
streams = [s for s in streams if s.endswith("/xmpp")]
for stream in streams:
self.jabber.leave_muc(stream_to_room(stream))
def process_stream(self, event):
if event['op'] == 'occupy':
streams = [s['name'].lower() for s in event['streams']]
streams = [s for s in streams if s.endswith("/xmpp")]
for stream in streams:
self.jabber.join_muc(stream_to_room(stream))
if event['op'] == 'vacate':
streams = [s['name'].lower() for s in event['streams']]
streams = [s for s in streams if s.endswith("/xmpp")]
for stream in streams:
self.jabber.leave_muc(stream_to_room(stream))
def get_rooms(zulip):
def get_stream_infos(key, method):
ret = method()
if ret.get("result") != "success":
logging.error(ret)
sys.exit("Could not get initial list of Zulip %s" % (key,))
return ret[key]
if options.mode == 'public':
stream_infos = get_stream_infos("streams", zulip.client.get_streams)
else:
stream_infos = get_stream_infos("subscriptions", zulip.client.list_subscriptions)
rooms = []
for stream_info in stream_infos:
stream = stream_info['name']
if stream.endswith("/xmpp"):
rooms.append(stream_to_room(stream))
return rooms
def config_error(msg):
sys.stderr.write("%s\n" % (msg,))
sys.exit(2)
if __name__ == '__main__':
parser = optparse.OptionParser(epilog=
'''Most general and Jabber configuration options may also be specified in the
zulip configuration file under the jabber_mirror section (exceptions are noted
in their help sections). Keys have the same name as options with hyphens
replaced with underscores. Zulip configuration options go in the api section,
as normal.'''.replace("\n", " ")
)
parser.add_option('--mode',
default=None,
action='store',
help= \
'''Which mode to run in. Valid options are "personal" and "public". In
"personal" mode, the mirror uses an individual users' credentials and mirrors
all messages they send on Zulip to Jabber and all private Jabber messages to
Zulip. In "public" mode, the mirror uses the credentials for a dedicated mirror
user and mirrors messages sent to Jabber rooms to Zulip. Defaults to
"personal"'''.replace("\n", " "))
parser.add_option('--zulip-email-suffix',
default=None,
action='store',
help= \
'''Add the specified suffix to the local part of email addresses constructed
from JIDs and nicks before sending requests to the Zulip server, and remove the
suffix before sending requests to the Jabber server. For example, specifying
"+foo" will cause messages that are sent to the "bar" room by nickname "qux" to
be mirrored to the "bar/xmpp" stream in Zulip by user "qux+foo@example.com". This
option does not affect login credentials.'''.replace("\n", " "))
parser.add_option('-d', '--debug',
help='set logging to DEBUG. Can not be set via config file.',
action='store_const',
dest='log_level',
const=logging.DEBUG,
default=logging.INFO)
jabber_group = optparse.OptionGroup(parser, "Jabber configuration")
jabber_group.add_option('--jid',
default=None,
action='store',
help="Your Jabber JID. If a resource is specified, "
+ "it will be used as the nickname when joining MUCs. "
+ "Specifying the nickname is mostly useful if you want "
+ "to run the public mirror from a regular user instead of "
+ "from a dedicated account.")
jabber_group.add_option('--jabber-password',
default=None,
action='store',
help="Your Jabber password")
jabber_group.add_option('--conference-domain',
default=None,
action='store',
help="Your Jabber conference domain (E.g. conference.jabber.example.com). "
+ "If not specifed, \"conference.\" will be prepended to your JID's domain.")
jabber_group.add_option('--no-use-tls',
default=None,
action='store_true')
jabber_group.add_option('--jabber-server-address',
default=None,
action='store',
help="The hostname of your Jabber server. This is only needed if "
"your server is missing SRV records")
jabber_group.add_option('--jabber-server-port',
default='5222',
action='store',
help="The port of your Jabber server. This is only needed if "
"your server is missing SRV records")
parser.add_option_group(jabber_group)
parser.add_option_group(zulip.generate_option_group(parser, "zulip-"))
(options, args) = parser.parse_args()
logging.basicConfig(level=options.log_level,
format='%(levelname)-8s %(message)s')
if options.zulip_config_file is None:
config_file = zulip.get_default_config_filename()
else:
config_file = options.zulip_config_file
config = SafeConfigParser()
try:
with file(config_file, 'r') as f:
config.readfp(f, config_file)
except IOError:
pass
for option in ("jid", "jabber_password", "conference_domain", "mode", "zulip_email_suffix", "jabber_server_address", "jabber_server_port"):
if (getattr(options, option) is None
and config.has_option("jabber_mirror", option)):
setattr(options, option, config.get("jabber_mirror", option))
for option in ("no_use_tls",):
if getattr(options, option) is None:
if config.has_option("jabber_mirror", option):
setattr(options, option, config.getboolean("jabber_mirror", option))
else:
setattr(options, option, False)
if options.mode is None:
options.mode = "personal"
if options.zulip_email_suffix is None:
options.zulip_email_suffix = ''
if options.mode not in ('public', 'personal'):
config_error("Bad value for --mode: must be one of 'public' or 'personal'")
if None in (options.jid, options.jabber_password):
config_error("You must specify your Jabber JID and Jabber password either "
+ "in the Zulip configuration file or on the commandline")
zulip = ZulipToJabberBot(zulip.init_from_options(options, "JabberMirror/" + __version__))
# This won't work for open realms that don't have a consistent domain
options.zulip_domain = zulip.client.email.partition('@')[-1]
try:
jid = JID(options.jid)
except InvalidJID as e:
config_error("Bad JID: %s: %s" % (options.jid, e.message))
if options.conference_domain is None:
options.conference_domain = "conference.%s" % (jid.domain,)
xmpp = JabberToZulipBot(jid, options.jabber_password, get_rooms(zulip))
address = None
if options.jabber_server_address:
address = (options.jabber_server_address, options.jabber_server_port)
if not xmpp.connect(use_tls=not options.no_use_tls, address=address):
sys.exit("Unable to connect to Jabber server")
xmpp.set_zulip_client(zulip)
zulip.set_jabber_client(xmpp)
xmpp.process(block=False)
if options.mode == 'public':
event_types = ['stream']
else:
event_types = ['message', 'subscription']
try:
logging.info("Connecting to Zulip.")
zulip.client.call_on_each_event(zulip.process_event,
event_types=event_types)
except BaseException as e:
logging.exception("Exception in main loop")
xmpp.abort()
sys.exit(1)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/python
import subprocess
import os
import sys

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/python
import sys
import subprocess
import base64

View File

@@ -1,4 +1,3 @@
from __future__ import print_function
# This is hacky code to analyze data on our support stream. The main
# reusable bits are get_recent_messages and get_words.
@@ -32,7 +31,7 @@ def analyze_messages(msgs, word_count, email_count):
if False:
if ' ack' in msg['content']:
name = msg['sender_full_name'].split()[0]
print('ACK', name)
print 'ACK', name
m = re.search('ticket (Z....).*email: (\S+).*~~~(.*)', msg['content'], re.M | re.S)
if m:
ticket, email, req = m.groups()
@@ -41,9 +40,9 @@ def analyze_messages(msgs, word_count, email_count):
word_count[word] += 1
email_count[email] += 1
if False:
print()
print
for k, v in msg.items():
print('%-20s: %s' % (k, v))
print '%-20s: %s' % (k, v)
def generate_support_stats():
client = zulip.Client()
@@ -65,16 +64,16 @@ def generate_support_stats():
if True:
words = word_count.keys()
words = [w for w in words if word_count[w] >= 10]
words = [w for w in words if len(w) >= 5]
words = filter(lambda w: word_count[w] >= 10, words)
words = filter(lambda w: len(w) >= 5, words)
words = sorted(words, key=lambda w: word_count[w], reverse=True)
for word in words:
print(word, word_count[word])
print word, word_count[word]
if False:
emails = email_count.keys()
emails = sorted(emails, key=lambda w: email_count[w], reverse=True)
for email in emails:
print(email, email_count[email])
print email, email_count[email]
generate_support_stats()

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
import sys
import os
import logging

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# Copyright (C) 2012 Zulip, Inc.
#
# Permission is hereby granted, free of charge, to any person

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# Copyright (C) 2012 Zulip, Inc.
#
# Permission is hereby granted, free of charge, to any person
@@ -21,15 +21,14 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from __future__ import absolute_import
from __future__ import print_function
import sys
import subprocess
import os
import traceback
import signal
from .zephyr_mirror_backend import parse_args
from zephyr_mirror_backend import parse_args
from zephyr_mirror_backend import RandomExponentialBackoff
def die(signal, frame):
# We actually want to exit, so run os._exit (so as not to be caught and restarted)
@@ -39,9 +38,6 @@ signal.signal(signal.SIGINT, die)
(options, args) = parse_args()
sys.path[:0] = [os.path.join(options.root_path, 'api')]
from zulip import RandomExponentialBackoff
args = [os.path.join(options.root_path, "user_root", "zephyr_mirror_backend.py")]
args.extend(sys.argv[1:])
@@ -54,30 +50,30 @@ if options.forward_class_messages and not options.noshard:
if options.on_startup_command is not None:
subprocess.call([options.on_startup_command])
from zerver.lib.parallel import run_parallel
print("Starting parallel zephyr class mirroring bot")
print "Starting parallel zephyr class mirroring bot"
jobs = list("0123456789abcdef")
def run_job(shard):
subprocess.call(args + ["--shard=%s" % (shard,)])
return 0
for (status, job) in run_parallel(run_job, jobs, threads=16):
print("A mirroring shard died!")
print "A mirroring shard died!"
pass
sys.exit(0)
backoff = RandomExponentialBackoff(timeout_success_equivalent=300)
backoff = RandomExponentialBackoff()
while backoff.keep_going():
print("Starting zephyr mirroring bot")
print "Starting zephyr mirroring bot"
try:
subprocess.call(args)
except:
traceback.print_exc()
backoff.fail()
print("")
print("")
print("ERROR: The Zephyr mirroring bot is unable to continue mirroring Zephyrs.")
print("This is often caused by failing to maintain unexpired Kerberos tickets")
print("or AFS tokens. See https://zulip.com/zephyr for documentation on how to")
print("maintain unexpired Kerberos tickets and AFS tokens.")
print("")
print ""
print ""
print "ERROR: The Zephyr mirroring bot is unable to continue mirroring Zephyrs."
print "This is often caused by failing to maintain unexpired Kerberos tickets"
print "or AFS tokens. See https://zulip.com/zephyr for documentation on how to"
print "maintain unexpired Kerberos tickets and AFS tokens."
print ""
sys.exit(1)

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env python2.7
#!/usr/bin/env python
# Copyright (C) 2012 Zulip, Inc.
#
# Permission is hereby granted, free of charge, to any person
@@ -20,11 +20,8 @@
# 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.
from __future__ import absolute_import
import sys
from six.moves import map
from six.moves import range
try:
import simplejson
except ImportError:
@@ -40,12 +37,43 @@ import signal
import logging
import hashlib
import tempfile
import random
import select
class CountingBackoff(object):
def __init__(self, maximum_retries=10):
self.number_of_retries = 0
self.maximum_retries = maximum_retries
def keep_going(self):
return self.number_of_retries < self.maximum_retries
def succeed(self):
self.number_of_retries = 0
def fail(self):
self.number_of_retries = min(self.number_of_retries + 1,
self.maximum_retries)
class RandomExponentialBackoff(CountingBackoff):
def fail(self):
self.number_of_retries = min(self.number_of_retries + 1,
self.maximum_retries)
# Exponential growth with ratio sqrt(2); compute random delay
# between x and 2x where x is growing exponentially
delay_scale = int(2 ** (self.number_of_retries / 2.0 - 1)) + 1
delay = delay_scale + random.randint(1, delay_scale)
message = "Sleeping for %ss [max %s] before retrying." % (delay, delay_scale * 2)
try:
logger.warning(message)
except NameError:
print message
time.sleep(delay)
DEFAULT_SITE = "https://api.zulip.com"
class States(object):
Startup, ZulipToZephyr, ZephyrToZulip, ChildSending = list(range(4))
class States:
Startup, ZulipToZephyr, ZephyrToZulip, ChildSending = range(4)
CURRENT_STATE = States.Startup
def to_zulip_username(zephyr_username):
@@ -373,7 +401,7 @@ def process_notice(notice, log):
if is_personal:
if body.startswith("CC:"):
is_huddle = True
# Map "CC: user1 user2" => "user1@mit.edu, user2@mit.edu"
# Map "CC: sipbtest espuser" => "starnine@mit.edu,espuser@mit.edu"
huddle_recipients = [to_zulip_username(x.strip()) for x in
body.split("\n")[0][4:].split()]
if notice.sender not in huddle_recipients:
@@ -450,7 +478,7 @@ def quit_failed_initialization(message):
sys.exit(1)
def zephyr_init_autoretry():
backoff = zulip.RandomExponentialBackoff()
backoff = RandomExponentialBackoff()
while backoff.keep_going():
try:
# zephyr.init() tries to clear old subscriptions, and thus
@@ -465,7 +493,7 @@ def zephyr_init_autoretry():
quit_failed_initialization("Could not initialize Zephyr library, quitting!")
def zephyr_load_session_autoretry(session_path):
backoff = zulip.RandomExponentialBackoff()
backoff = RandomExponentialBackoff()
while backoff.keep_going():
try:
session = file(session_path, "r").read()
@@ -480,7 +508,7 @@ def zephyr_load_session_autoretry(session_path):
quit_failed_initialization("Could not load saved Zephyr session, quitting!")
def zephyr_subscribe_autoretry(sub):
backoff = zulip.RandomExponentialBackoff()
backoff = RandomExponentialBackoff()
while backoff.keep_going():
try:
zephyr.Subscriptions().add(sub)
@@ -601,9 +629,7 @@ Feedback button or at support@zulip.com."""
wrapped_content = "\n".join("\n".join(wrapper.wrap(line))
for line in message["content"].replace("@", "@@").split("\n"))
zwrite_args = ["zwrite", "-n", "-s", message["sender_full_name"],
"-F", "Zephyr error: See http://zephyr.1ts.org/wiki/df",
"-x", "UTF-8"]
zwrite_args = ["zwrite", "-n", "-s", message["sender_full_name"], "-F", "Zephyr error: See http://zephyr.1ts.org/wiki/df"]
# Hack to make ctl's fake username setup work :)
if message['type'] == "stream" and zulip_account_email == "ctl@mit.edu":
@@ -737,7 +763,7 @@ def maybe_forward_to_zephyr(message):
logger.exception("Error forwarding message:")
def zulip_to_zephyr(options):
# Sync messages from zulip to zephyr
# Sync messages from zephyr to zulip
logger.info("Starting syncing messages.")
while True:
try:

View File

@@ -1,5 +1,5 @@
[program:zmirror-USERNAME]
command=python /home/zulip/zulip/bots/zephyr_mirror_backend.py --root-path=/home/zulip/zulip --user=USERNAME --log-path=/home/zulip/logs/mirror-log-%(program_name)s --use-sessions --session-path=/home/zulip/zephyr_sessions/%(program_name)s --api-key-file=/home/zulip/api-keys/%(program_name)s --ignore-expired-tickets --nagios-path=/home/zulip/mirror_status/%(program_name)s --nagios-class=zulip-mirror-nagios
command=python /home/zulip/zulip/bots/zephyr_mirror_backend.py --root-path=/home/zulip/zulip/bots --user=USERNAME --log-path=/home/zulip/logs/mirror-log-%(program_name)s --use-sessions --session-path=/home/zulip/zephyr_sessions/%(program_name)s --api-key-file=/home/zulip/api-keys/%(program_name)s --ignore-expired-tickets --nagios-path=/home/zulip/mirror_status/%(program_name)s --nagios-class=zulip-mirror-nagios
priority=200 ; the relative start priority (default 999)
autostart=true ; start at supervisord start (default: true)
autorestart=true ; whether/when to restart (default: unexpected)

View File

@@ -39,4 +39,4 @@ def format_commit_message(author, subject, commit_id):
return '!avatar(%s) [%s](https://git.zulip.net/eng/zulip/commit/%s)\n' % (author, subject, commit_id)
ZULIP_API_PATH = "/home/zulip/zulip/api"
ZULIP_SITE = "https://zulip.com"
ZULIP_SITE = "https://staging.zulip.com"

View File

@@ -11,4 +11,4 @@ TRAC_BASE_TICKET_URL = "https://trac.zulip.net/ticket"
TRAC_NOTIFY_FIELDS = ["description", "summary", "resolution", "comment", "owner"]
ZULIP_API_PATH = "/home/zulip/zulip/api"
ZULIP_SITE = "https://zulip.com"
ZULIP_SITE = "https://staging.zulip.com"

View File

@@ -4,4 +4,4 @@
[api]
email = nagios-bot@zulip.com
key = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
site = https://zulip.com
site = https://staging.zulip.com

View File

@@ -1,31 +0,0 @@
# Change Log
All notable changes to this project will be documented in this file.
[Unreleased]
[1.3.9] - 2015-11-16
- Fixed buggy #! lines in upgrade scripts.
[1.3.8] - 2015-11-15
- Added options to the Python api for working with untrusted server certificates.
- Added a lot of documentation on the development environment and testing.
- Added partial support for translating the Zulip UI.
- Migrated installing Node dependencies to use npm.
- Fixed LDAP integration breaking autocomplete of @-mentions.
- Fixed admin panel reactivation/deactivation of bots.
- Fixed inaccurate documentation for downloading the desktop apps.
- Fixed various minor bugs in production installation process.
- Fixed security issue where recent history on private streams might
be visible to new users (to the Zulip team) who were invited with that
private stream as one of their initial streams
(https://github.com/zulip/zulip/issues/230).
- Major preliminary progress towards supporting Python 3.
[1.3.7] - 2015-10-19
- Turn off desktop and audible notifications for streams by default.
- Added support for the LDAP authentication integration creating new users.
- Added new endpoint to support Google auth on mobile.
- Fixed desktop notifications in modern Firefox.
- Fixed several installation issues for both production and development environments.
- Improved documentation for outgoing SMTP and the email mirror integration.

View File

@@ -1,29 +1,45 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(migrations.Migration):
class Migration(SchemaMigration):
dependencies = [
('contenttypes', '0001_initial'),
]
def forwards(self, orm):
# Adding model 'Confirmation'
db.create_table('confirmation_confirmation', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'])),
('object_id', self.gf('django.db.models.fields.PositiveIntegerField')()),
('date_sent', self.gf('django.db.models.fields.DateTimeField')()),
('confirmation_key', self.gf('django.db.models.fields.CharField')(max_length=40)),
))
db.send_create_signal('confirmation', ['Confirmation'])
operations = [
migrations.CreateModel(
name='Confirmation',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('object_id', models.PositiveIntegerField()),
('date_sent', models.DateTimeField(verbose_name='sent')),
('confirmation_key', models.CharField(max_length=40, verbose_name='activation key')),
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
],
options={
'verbose_name': 'confirmation email',
'verbose_name_plural': 'confirmation emails',
},
bases=(models.Model,),
),
]
def backwards(self, orm):
# Deleting model 'Confirmation'
db.delete_table('confirmation_confirmation')
models = {
'confirmation.confirmation': {
'Meta': {'object_name': 'Confirmation'},
'confirmation_key': ('django.db.models.fields.CharField', [], {'max_length': '40'}),
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
'date_sent': ('django.db.models.fields.DateTimeField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {})
},
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
}
}
complete_apps = ['confirmation']

View File

@@ -34,10 +34,9 @@ def generate_key():
return generate_random_token(40)
def generate_activation_url(key):
return u'%s%s%s' % (settings.EXTERNAL_URI_SCHEME,
settings.EXTERNAL_HOST,
reverse('confirmation.views.confirm',
kwargs={'confirmation_key': key}))
current_site = Site.objects.get_current()
return u'https://%s%s' % (current_site.domain,
reverse('confirmation.views.confirm', kwargs={'confirmation_key': key}))
class ConfirmationManager(models.Manager):
@@ -49,7 +48,7 @@ class ConfirmationManager(models.Manager):
except self.model.DoesNotExist:
return False
obj = confirmation.content_object
status_field = get_status_field(obj._meta.app_label, obj._meta.model_name)
status_field = get_status_field(obj._meta.app_label, obj._meta.module_name)
setattr(obj, status_field, getattr(settings, 'STATUS_ACTIVE', 1))
obj.save()
return obj
@@ -75,7 +74,7 @@ class ConfirmationManager(models.Manager):
if additional_context is not None:
context.update(additional_context)
templates = [
'confirmation/%s_confirmation_email_subject.txt' % obj._meta.model_name,
'confirmation/%s_confirmation_email_subject.txt' % obj._meta.module_name,
'confirmation/confirmation_email_subject.txt',
]
if subject_template_path:
@@ -84,7 +83,7 @@ class ConfirmationManager(models.Manager):
template = loader.select_template(templates)
subject = template.render(context).strip().replace(u'\n', u' ') # no newlines, please
templates = [
'confirmation/%s_confirmation_email_body.txt' % obj._meta.model_name,
'confirmation/%s_confirmation_email_body.txt' % obj._meta.module_name,
'confirmation/confirmation_email_body.txt',
]
if body_template_path:
@@ -105,7 +104,7 @@ class Confirmation(models.Model):
objects = ConfirmationManager()
class Meta(object):
class Meta:
verbose_name = _('confirmation email')
verbose_name_plural = _('confirmation emails')

View File

@@ -31,13 +31,13 @@ def confirm(request, confirmation_key):
'key': confirmation_key,
'full_name': request.GET.get("full_name", None),
'support_email': settings.ZULIP_ADMINISTRATOR,
'voyager': settings.VOYAGER
'enterprise': settings.ENTERPRISE
}
templates = [
'confirmation/confirm.html',
]
if obj:
# if we have an object, we can use specific template
templates.insert(0, 'confirmation/confirm_%s.html' % obj._meta.model_name)
templates.insert(0, 'confirmation/confirm_%s.html' % obj._meta.module_name)
return render_to_response(templates, ctx,
context_instance=RequestContext(request))

View File

@@ -2,8 +2,20 @@ from django.conf.urls import patterns, url
from django.views.generic import TemplateView, RedirectView
urlpatterns = patterns('',
# Job postings
url(r'^jobs/$', TemplateView.as_view(template_name='corporate/jobs/index.html')),
url(r'^jobs/lead-designer/$', TemplateView.as_view(template_name='corporate/jobs/lead-designer.html')),
# Zephyr/MIT
url(r'^zephyr/$', TemplateView.as_view(template_name='corporate/zephyr.html')),
url(r'^mit/$', TemplateView.as_view(template_name='corporate/mit.html')),
url(r'^zephyr-mirror/$', TemplateView.as_view(template_name='corporate/zephyr-mirror.html')),
# Marketing
url(r'^compare/$', TemplateView.as_view(template_name='corporate/compare.html')),
# signup form
url(r'^signup/$', TemplateView.as_view(template_name='corporate/signup.html'),
name='signup'),
# TODO: The beta signup view should probably be moved to corporate.
url(r'^signup/sign-me-up$', 'zerver.views.beta_signup_submission', name='beta-signup-submission'),
)

1
docs/.gitignore vendored
View File

@@ -1 +0,0 @@
_build

View File

@@ -1,192 +0,0 @@
# Makefile for Sphinx documentation
#
# You can set these variables from the command line.
SPHINXOPTS =
SPHINXBUILD = sphinx-build
PAPER =
BUILDDIR = _build
# User-friendly check for sphinx-build
ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1)
$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/)
endif
# Internal variables.
PAPEROPT_a4 = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
# the i18n builder cannot share the environment and doctrees with the others
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext
help:
@echo "Please use \`make <target>' where <target> is one of"
@echo " html to make standalone HTML files"
@echo " dirhtml to make HTML files named index.html in directories"
@echo " singlehtml to make a single large HTML file"
@echo " pickle to make pickle files"
@echo " json to make JSON files"
@echo " htmlhelp to make HTML files and a HTML help project"
@echo " qthelp to make HTML files and a qthelp project"
@echo " applehelp to make an Apple Help Book"
@echo " devhelp to make HTML files and a Devhelp project"
@echo " epub to make an epub"
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
@echo " latexpdf to make LaTeX files and run them through pdflatex"
@echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
@echo " text to make text files"
@echo " man to make manual pages"
@echo " texinfo to make Texinfo files"
@echo " info to make Texinfo files and run them through makeinfo"
@echo " gettext to make PO message catalogs"
@echo " changes to make an overview of all changed/added/deprecated items"
@echo " xml to make Docutils-native XML files"
@echo " pseudoxml to make pseudoxml-XML files for display purposes"
@echo " linkcheck to check all external links for integrity"
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
@echo " coverage to run coverage check of the documentation (if enabled)"
clean:
rm -rf $(BUILDDIR)/*
html:
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
dirhtml:
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
singlehtml:
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
@echo
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
pickle:
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
@echo
@echo "Build finished; now you can process the pickle files."
json:
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
@echo
@echo "Build finished; now you can process the JSON files."
htmlhelp:
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
@echo
@echo "Build finished; now you can run HTML Help Workshop with the" \
".hhp project file in $(BUILDDIR)/htmlhelp."
qthelp:
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
@echo
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/zulip-contributor-docs.qhcp"
@echo "To view the help file:"
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/zulip-contributor-docs.qhc"
applehelp:
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
@echo
@echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
@echo "N.B. You won't be able to view it unless you put it in" \
"~/Library/Documentation/Help or install it in your application" \
"bundle."
devhelp:
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
@echo
@echo "Build finished."
@echo "To view the help file:"
@echo "# mkdir -p $$HOME/.local/share/devhelp/zulip-contributor-docs"
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/zulip-contributor-docs"
@echo "# devhelp"
epub:
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
@echo
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
latex:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
@echo "Run \`make' in that directory to run these through (pdf)latex" \
"(use \`make latexpdf' here to do that automatically)."
latexpdf:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through pdflatex..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
latexpdfja:
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
@echo "Running LaTeX files through platex and dvipdfmx..."
$(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
text:
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
@echo
@echo "Build finished. The text files are in $(BUILDDIR)/text."
man:
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
@echo
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
texinfo:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
@echo "Run \`make' in that directory to run these through makeinfo" \
"(use \`make info' here to do that automatically)."
info:
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
@echo "Running Texinfo files through makeinfo..."
make -C $(BUILDDIR)/texinfo info
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
gettext:
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
@echo
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
changes:
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
@echo
@echo "The overview file is in $(BUILDDIR)/changes."
linkcheck:
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
@echo
@echo "Link check complete; look for any errors in the above output " \
"or in $(BUILDDIR)/linkcheck/output.txt."
doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
coverage:
$(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
@echo "Testing of coverage in the sources finished, look at the " \
"results in $(BUILDDIR)/coverage/python.txt."
xml:
$(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
@echo
@echo "Build finished. The XML files are in $(BUILDDIR)/xml."
pseudoxml:
$(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
@echo
@echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."

View File

@@ -1,23 +0,0 @@
These docs are written in rST, and are included on the zulip.org website
as well as on each development installation. Many of these docs
have been ported from the internal docs of Zulip Inc.,
and may need to be updated for use in the open source project.
To generate HTML docs locally from rST:
* `pip install sphinx`
* In this directory, `make html`. Output appears in a `_build/html` subdirectory.
To create rST from MediaWiki input:
* Use `pandoc -r mediawiki -w rst` on MediaWiki source.
* Use unescape.py to remove any leftover HTML entities (often inside <pre>
tags and the like).
We can use pandoc to translate mediawiki into reStructuredText, but some things need fixing up:
* Add page titles.
* Review pages for formatting (especially inline code chunks) and content.
* Fix wiki links?
* Add pages to the table of contents (`index.rst`).

View File

@@ -1,10 +0,0 @@
/* override table width restrictions */
.wy-table-responsive table td, .wy-table-responsive table th {
/* !important prevents the common CSS stylesheets from
overriding this as on RTD they are loaded after this stylesheet */
white-space: normal !important;
}
.wy-table-responsive {
overflow: visible !important;
}

View File

@@ -1,484 +0,0 @@
==========================
Code style and conventions
==========================
Be consistent!
==============
Look at the surrounding code, or a similar part of the project, and
try to do the same thing. If you think the other code has actively bad
style, fix it (in a separate commit).
When in doubt, send an email to zulip-devel@googlegroups.com with your
question.
Lint tools
==========
You can run them all at once with
::
./tools/lint-all
You can set this up as a local Git commit hook with
::
``tools/setup-git-repo``
The Vagrant setup process runs this for you.
``lint-all`` runs many lint checks in parallel, including
- Javascript (`JSLint <http://www.jslint.com/>`__)
``tools/jslint/check-all.js`` contains a pretty fine-grained set of
JSLint options, rule exceptions, and allowed global variables. If you
add a new global, you'll need to add it to the list.
- Python (`Pyflakes <http://pypi.python.org/pypi/pyflakes>`__)
- templates
- Puppet configuration
- custom checks (e.g. trailing whitespace and spaces-not-tabs)
Secrets
=======
Please don't put any passwords, secret access keys, etc. inline in the
code. Instead, use the ``get_secret`` function in
``zproject/settings.py`` to read secrets from ``/etc/zulip/secrets.conf``.
Dangerous constructs
====================
Misuse of database queries
--------------------------
Look out for Django code like this::
[Foo.objects.get(id=bar.x.id)
for bar in Bar.objects.filter(...)
if  bar.baz < 7]
This will make one database query for each ``Bar``, which is slow in
production (but not in local testing!). Instead of a list comprehension,
write a single query using Django's `QuerySet
API <https://docs.djangoproject.com/en/dev/ref/models/querysets/>`__.
If you can't rewrite it as a single query, that's a sign that something
is wrong with the database schema. So don't defer this optimization when
performing schema changes, or else you may later find that it's
impossible.
UserProfile.objects.get() / Client.objects.get / etc.
-----------------------------------------------------
In our Django code, never do direct
``UserProfile.objects.get(email=foo)`` database queries. Instead always
use ``get_user_profile_by_{email,id}``. There are 3 reasons for this:
#. It's guaranteed to correctly do a case-inexact lookup
#. It fetches the user object from memcached, which is faster
#. It always fetches a UserProfile object which has been queried using
.selected\_related(), and thus will perform well when one later
accesses related models like the Realm.
Similarly we have ``get_client`` and ``get_stream`` functions to fetch
those commonly accessed objects via memcached.
Using Django model objects as keys in sets/dicts
------------------------------------------------
Don't use Django model objects as keys in sets/dictionaries -- you will
get unexpected behavior when dealing with objects obtained from
different database queries:
For example,
``UserProfile.objects.only("id").get(id=17) in set([UserProfile.objects.get(id=17)])``
is False
You should work with the IDs instead.
user\_profile.save()
--------------------
You should always pass the update\_fields keyword argument to .save()
when modifying an existing Django model object. By default, .save() will
overwrite every value in the column, which results in lots of race
conditions where unrelated changes made by one thread can be
accidentally overwritten by another thread that fetched its UserProfile
object before the first thread wrote out its change.
Using raw saves to update important model objects
-------------------------------------------------
In most cases, we already have a function in zephyr/lib/actions.py with
a name like do\_activate\_user that will correctly handle lookups,
caching, and notifying running browsers via the event system about your
change. So please check whether such a function exists before writing
new code to modify a model object, since your new code has a good chance
of getting at least one of these things wrong.
``x.attr('zid')`` vs. ``rows.id(x)``
------------------------------------
Our message row DOM elements have a custom attribute ``zid`` which
contains the numerical message ID. **Don't access this directly as**
``x.attr('zid')`` ! The result will be a string and comparisons (e.g.
with ``<=``) will give the wrong result, occasionally, just enough to
make a bug that's impossible to track down.
You should instead use the ``id`` function from the ``rows`` module, as
in ``rows.id(x)``. This returns a number. Even in cases where you do
want a string, use the ``id`` function, as it will simplify future code
changes. In most contexts in JavaScript where a string is needed, you
can pass a number without any explicit conversion.
Javascript var
--------------
Always declare Javascript variables using ``var``::
var x = ...;
In a function, ``var`` is necessary or else ``x`` will be a global
variable. For variables declared at global scope, this has no effect,
but we do it for consistency.
Javascript has function scope only, not block scope. This means that a
``var`` declaration inside a ``for`` or ``if`` acts the same as a
``var`` declaration at the beginning of the surrounding ``function``. To
avoid confusion, declare all variables at the top of a function.
Javascript ``for (i in myArray)``
---------------------------------
Don't use it:
`[1] <http://stackoverflow.com/questions/500504/javascript-for-in-with-arrays>`__,
`[2] <http://google-styleguide.googlecode.com/svn/trunk/javascriptguide.xml#for-in_loop>`__,
`[3] <http://www.jslint.com/lint.html#forin>`__
jQuery global state
-------------------
Don't mess with jQuery global state once the app has loaded. Code like
this is very dangerous::
$.ajaxSetup({ async: false });
$.get(...);
$.ajaxSetup({ async: true });
jQuery and the browser are free to run other code while the request is
pending, which could perform other Ajax requests with the altered
settings.
Instead, switch to the more general |ajax|_ function, which can take options
like ``async``.
.. |ajax| replace:: ``$.ajax``
.. _ajax: http://api.jquery.com/jQuery.ajax
State and logs files
--------------------
Do not write state and logs files inside the current working directory
in the production environment. This will not how you expect, because the
current working directory for the app changes every time we do a deploy.
Instead, hardcode a path in settings.py -- see SERVER\_LOG\_PATH in
settings.py for an example.
JS array/object manipulation
============================
For generic functions that operate on arrays or JavaScript objects, you
should generally use `Underscore <http://underscorejs.org/>`__. We used
to use jQuery's utility functions, but the Underscore equivalents are
more consistent, better-behaved and offer more choices.
A quick conversion table::
   $.each  _.each (parameters to the callback reversed)
   $.inArray  _.indexOf (parameters reversed)
   $.grep  _.filter
   $.map  _.map
   $.extend  _.extend
There's a subtle difference in the case of ``_.extend``; it will replace
attributes with undefined, whereas jQuery won't::
   $.extend({foo: 2}, {foo: undefined});  // yields {foo: 2}, BUT...
   _.extend({foo: 2}, {foo: undefined});  // yields {foo: undefined}!
Also, ``_.each`` does not let you break out of the iteration early by
returning false, the way jQuery's version does. If you're doing this,
you probably want ``_.find``, ``_.every``, or ``_.any``, rather than
'each'.
Some Underscore functions have multiple names. You should always use the
canonical name (given in large print in the Underscore documentation),
with the exception of ``_.any``, which we prefer over the less clear
'some'.
More arbitrary style things
===========================
General
-------
Indentation is four space characters for Python, JS, CSS, and shell
scripts. Indentation is two space characters for HTML templates.
We never use tabs anywhere in source code we write, but we have some
third-party files which contain tabs.
Keep third-party static files under the directory
``zephyr/static/third/``, with one subdirectory per third-party project.
We don't have an absolute hard limit on line length, but we should avoid
extremely long lines. A general guideline is: refactor stuff to get it
under 85 characters, unless that makes the code a lot uglier, in which
case it's fine to go up to 120 or so.
Whitespace guidelines:
- Put one space (or more for alignment) around binary arithmetic and
equality operators.
- Put one space around each part of the ternary operator.
- Put one space between keywords like ``if`` and ``while`` and their
associated open paren.
- Put one space between the closing paren for ``if`` and ``while``-like
constructs and the opening curly brace. Put the curly brace on the
same line unless doing otherwise improves readability.
- Put no space before or after the open paren for function calls and no
space before the close paren for function calls.
- For the comma operator and colon operator in languages where it is
used for inline dictionaries, put no space before it and at least one
space after. Only use more than one space for alignment.
Javascript
----------
Don't use ``==`` and ``!=`` because these operators perform type
coercions, which can mask bugs. Always use ``===`` and ``!==``.
End every statement with a semicolon.
``if`` statements with no braces are allowed, if the body is simple and
its extent is abundantly clear from context and formatting.
Anonymous functions should have spaces before and after the argument
list::
var x = function (foo, bar) { // ...
When calling a function with an anonymous function as an argument, use
this style::
$.get('foo', function (data) {
    var x = ...;
    // ...
});
The inner function body is indented one level from the outer function
call. The closing brace for the inner function and the closing
parenthesis for the outer call are together on the same line. This style
isn't necessarily appropriate for calls with multiple anonymous
functions or other arguments following them.
Use
::
$(function () { ...
rather than
::
$(document).ready(function () { ...
and combine adjacent on-ready functions, if they are logically related.
The best way to build complicated DOM elements is a Mustache template
like ``zephyr/static/templates/message.handlebars``. For simpler things
you can use jQuery DOM building APIs like so::
var new_tr = $('<tr />').attr('id', zephyr.id);
Passing a HTML string to jQuery is fine for simple hardcoded things::
foo.append('<p id="selected">foo</p>');
but avoid programmatically building complicated strings.
We used to favor attaching behaviors in templates like so::
<p onclick="select_zephyr({{id}})">
but there are some reasons to prefer attaching events using jQuery code:
- Potential huge performance gains by using delegated events where
possible
- When calling a function from an ``onclick`` attribute, ``this`` is
not bound to the element like you might think
- jQuery does event normalization
Either way, avoid complicated JavaScript code inside HTML attributes;
call a helper function instead.
HTML / CSS
----------
Don't use the ``style=`` attribute. Instead, define logical classes and
put your styles in ``zulip.css``.
Don't use the tag name in a selector unless you have to. In other words,
use ``.foo`` instead of ``span.foo``. We shouldn't have to care if the
tag type changes in the future.
Don't use inline event handlers (``onclick=``, etc. attributes).
Instead, attach a jQuery event handler
(``$('#foo').on('click', function () {...})``) when the DOM is ready
(inside a ``$(function () {...})`` block).
Use this format when you have the same block applying to multiple CSS
styles (separate lines for each selector)::
selector1,
selector2 {
};
Python
------
- Scripts should start with ``#!/usr/bin/env python2.7`` and not
``#!/usr/bin/env python2.7``. See commit ``437d4aee`` for an explanation of
why. Don't put such a line on a Python file unless it's meaningful to
run it as a script. (Some libraries can also be run as scripts, e.g.
to run a test suite.)
- The first import in a file should be
``from __future__ import absolute_import``, per `PEP
328 <http://docs.python.org/2/whatsnew/2.5.html#pep-328-absolute-and-relative-imports>`__
- Put all imports together at the top of the file, absent a compelling
reason to do otherwise.
- Unpacking sequences doesn't require list brackets::
[x, y] = xs    # unnecessary
x, y = xs      # better
- For string formatting, use ``x % (y,)`` rather than ``x % y``, to
avoid ambiguity if ``y`` happens to be a tuple.
- When selecting by id, don't use ``foo.pk`` when you mean ``foo.id``.
E.g.
::
recipient = Recipient(type_id=huddle.pk, type=Recipient.HUDDLE)
should be written as
::
recipient = Recipient(type_id=huddle.id, type=Recipient.HUDDLE)
in case we ever change the primary keys.
Version Control
===============
Commit Discipline
-----------------
We follow the Git project's own commit discipline practice of "Each
commit is a minimal coherent idea".
Coherency requirements for any commit:
- It should pass tests (so test updates needed by a change should be in
the same commit as the original change, not a separate "fix the tests
that were broken by the last commit" commit).
- It should be safe to deploy individually, or comment in detail in the
commit message as to why it isn't (maybe with a [manual] tag). So
implementing a new API endpoint in one commit and then adding the
security checks in a future commit should be avoided -- the security
checks should be there from the beginning.
- Error handling should generally be included along with the code that
might trigger the error.
- TODO comments should be in the commit that introduces the
issue or functionality with further work required.
When you should be minimal:
- Significant refactorings should be done in a separate commit from
functional changes.
- Moving code from one file to another should be done in a separate
commits from functional changes or even refactoring within a file.
- 2 different refactorings should be done in different commits.
- 2 different features should be done in different commits.
- If you find yourself writing a commit message that reads like a list
of somewhat dissimilar things that you did, you probably should have
just done 2 commits.
When not to be overly minimal:
- For completely new features, you don't necessarily need to split out
new commits for each little subfeature of the new feature. E.g. if
you're writing a new tool from scratch, it's fine to have the initial
tool have plenty of options/features without doing separate commits
for each one. That said, reviewing a 2000-line giant blob of new
code isn't fun, so please be thoughtful about submitting things in
reviewable units.
- Don't bother to split back end commits from front end commits, even
though the backend can often be coherent on its own.
Other considerations:
- Overly fine commits are easily squashed, but not vice versa, so err
toward small commits, and the code reviewer can advise on squashing.
It can take some practice to get used to writing your commits this
way. For example, often you'll start adding a feature, and discover
you need to a refactoring partway through writing the feature. When
that happens, we recommend stashing your partial feature, do the
refactoring, commit it, and then finish implementing your feature.
Commit Messages
---------------
- The first line of commit messages should be written in the imperative
and be kept relatively short while concisely explaining what the
commit does. For example:
Bad::
bugfix
gather_subscriptions was broken
Good::
Prevent gather_subscriptions from throwing an exception when given bad input.
- Please use a complete sentence, ending with a period.
- The rest of the commit message should be written in full prose and
explain why and how the change was made. If the commit makes
performance improvements, you should generally include some rough
benchmarks showing that it actually improves the performance.
- In your commit message, you should describe any manual testing you
did in addition to running the automated tests, and any aspects of
the commit that you think are questionable and you'd like special
attention applied to.
Tests
-----
All significant new features should come with tests.
Third party code
----------------
When adding new third-party packages to our codebase, please include
"[third]" at the beginning of the commit message. You don't necessarily
need to do this when patching third-party code that's already in tree.

View File

@@ -1,295 +0,0 @@
# -*- coding: utf-8 -*-
#
# zulip-contributor-docs documentation build configuration file, created by
# sphinx-quickstart on Mon Aug 17 16:24:04 2015.
#
# 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 os
import shlex
# 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 = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
# The encoding of source files.
#source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
# General information about the project.
project = u'Zulip'
copyright = u'2015, The Zulip Team'
author = u'The Zulip Team'
# 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.
#
# The short X.Y version.
version = '0.1'
# The full version, including alpha/beta/rc tags.
release = '0.1'
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
#
# This is also used if you do content translation via gettext catalogs.
# Usually you set "language" from the command line for these cases.
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 = 'sphinx'
# 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
# If true, `todo` and `todoList` produce output, else they produce nothing.
todo_include_todos = False
# -- Options for HTML output ----------------------------------------------
# The theme to use for HTML and HTML Help pages. See the documentation for
# a list of builtin themes.
# Read The Docs can't import sphinx_rtd_theme, so don't import it there.
on_rtd = os.environ.get('READTHEDOCS', None) == 'True'
if not on_rtd:
import sphinx_rtd_theme
html_theme = 'sphinx_rtd_theme'
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# 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 = None
# 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 = {}
# If false, no module index is generated.
#html_domain_indices = True
# If false, no index is generated.
#html_use_index = True
# 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, "Created using Sphinx" is shown in the HTML footer. Default is True.
#html_show_sphinx = True
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
#html_show_copyright = 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
# Language to be used for generating the HTML full-text search index.
# Sphinx supports the following languages:
# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja'
# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr'
#html_search_language = 'en'
# A dictionary with options for the search language support, empty by default.
# Now only 'ja' uses this config value
#html_search_options = {'type': 'default'}
# The name of a javascript file (relative to the configuration directory) that
# implements a search results scorer. If empty, the default will be used.
#html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'zulip-contributor-docsdoc'
def setup(app):
# overrides for wide tables in RTD theme
app.add_stylesheet('theme_overrides.css') # path relative to _static
# -- 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': '',
# Latex figure (float) alignment
#'figure_align': 'htbp',
}
# 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 = [
(master_doc, 'zulip-contributor-docs.tex', u'Zulip Documentation',
u'The Zulip Team', '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 = [
(master_doc, 'zulip-contributor-docs', u'Zulip Documentation',
[author], 1)
]
# 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 = [
(master_doc, 'zulip-contributor-docs', u'Zulip Documentation',
author, 'zulip-contributor-docs', 'Documentation for contributing to Zulip.',
'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

View File

@@ -1,95 +0,0 @@
===================
Directory structure
===================
This page documents the Zulip directory structure and how to decide where to
put a file.
Scripts
=======
+--------------------+-----------------------------------------------------------------------------------+
| ``scripts/`` | Scripts that production deployments might run manually (e.g. ``restart-server``) |
+--------------------+-----------------------------------------------------------------------------------+
| ``bin/`` | Scripts that are needed on production deployments but humans should never run |
+--------------------+-----------------------------------------------------------------------------------+
| ``scripts/setup/`` | Tools that production deployments will only run once, during installation |
+--------------------+-----------------------------------------------------------------------------------+
| ``tools/`` | Development tools |
+--------------------+-----------------------------------------------------------------------------------+
Bots
====
+------------------------+----------------------------------------------------------------------+
| ``api/integrations`` | Bots distributed as part of the Zulip API bundle. |
+------------------------+----------------------------------------------------------------------+
| ``bots/`` | Previously Zulip internal bots. These usually need a bit of work. |
+------------------------+----------------------------------------------------------------------+
Management commands
===================
+-------------------------------------+------------------------------------------------------------------------------------------------------------------------------------+
| ``zerver/management/commands/`` | Management commands one might run at a production deployment site (e.g. scripts to change a value or deactivate a user properly) |
+-------------------------------------+------------------------------------------------------------------------------------------------------------------------------------+
Views
=====
+--------------------------------+-----------------------------------------+
| ``zerver/tornadoviews.py`` | Tornado views |
+--------------------------------+-----------------------------------------+
| ``zerver/views/webhooks.py`` | Webhook views |
+--------------------------------+-----------------------------------------+
| ``zerver/views/messages.py`` | message-related views |
+--------------------------------+-----------------------------------------+
| ``zerver/views/__init__.py`` | other Django views |
+--------------------------------+-----------------------------------------+
Static assets
=============
+---------------+---------------------------------------------------------------------------------------------------------------+
| ``assets/`` | For assets not to be served to the web (e.g. the system to generate our favicons) |
+---------------+---------------------------------------------------------------------------------------------------------------+
| ``static/`` | For things we do want to both serve to the web and distribute to production deployments (e.g. the webpages) |
+---------------+---------------------------------------------------------------------------------------------------------------+
Puppet
======
+--------------------+----------------------------------------------------------------------------------+
| ``puppet/zulip`` | For configuration for production deployments |
+--------------------+----------------------------------------------------------------------------------+
Templates
=========
+--------------------------+--------------------------------------------------------+
| ``templates/zerver`` | For templates related to zerver views |
+--------------------------+--------------------------------------------------------+
| ``static/templates`` | Handlebars templates for the frontend |
+--------------------------+--------------------------------------------------------+
Tests
=====
+------------------------+-----------------------------------+
| ``zerver/test*.py`` | Backend tests |
+------------------------+-----------------------------------+
| ``frontend_tests/node`` | Node Frontend unit tests |
+------------------------+-----------------------------------+
| ``frontend_tests/tests`` | Casper frontend tests |
+------------------------+-----------------------------------+
Documentation
=============
+-------------+-----------------------------------------------+
| ``docs/`` | Source for this documentation |
+-------------+-----------------------------------------------+
You can consult the repository's .gitattributes file to see exactly
which components are excluded from production releases (release
tarballs are generated using tools/build-release-tarball).

View File

@@ -1,9 +0,0 @@
#!/usr/bin/env python2.7
# Remove HTML entity escaping left over from MediaWiki->rST conversion.
import html
import sys
for line in sys.stdin:
print(html.unescape(line), end='')

View File

@@ -1,25 +0,0 @@
.. zulip documentation master file, created by
sphinx-quickstart on Mon Aug 17 16:24:04 2015.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.
Welcome to Zulip documentation!
===============================
Contents:
.. toctree::
:maxdepth: 2
new-feature-tutorial
code-style
directory-structure
testing
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View File

@@ -1,216 +0,0 @@
====================
New Feature Tutorial
====================
The changes needed to add a new feature will vary, of course, but this document
provides a general outline of what you may need to do, as well as an example of
the specific steps needed to add a new feature: adding a new option to the
application that is dynamically synced through the data system in real-time to
all browsers the user may have open.
General Process
===============
Adding a field to the database
------------------------------
**Update the model:** The server accesses the underlying database in `zerver/
models.py`. Add a new field in the appropriate class.
**Create and run the migration:** To create and apply a migration, run: ::
./manage.py makemigrations
./manage.py migrate
**Test your changes:** Once you've run the migration, restart memcached on your
development server (``/etc/init.d/memcached restart``) and then restart
``run-dev.py`` to avoid interacting with cached objects.
Backend changes
---------------
**Database interaction:** Add any necessary code for updating and interacting
with the database in ``zerver/lib/actions.py``. It should update the database and
send an event announcing the change.
**Application state:** Modify the ``fetch_initial_state_data`` and ``apply_events``
functions in ``zerver/lib/actions.py`` to update the state based on the event you
just created.
**Backend implementation:** Make any other modifications to the backend required for
your change.
**Testing:** At the very least, add a test of your event data flowing through
the system in ``test_events.py``.
Frontend changes
----------------
**JavaScript:** Zulip's JavaScript is located in the directory ``static/js/``.
The exact files you may need to change depend on your feature. If you've added a
new event that is sent to clients, be sure to add a handler for it to
``static/js/server_events.js``.
**CSS:** The primary CSS file is ``static/styles/zulip.css``. If your new
feature requires UI changes, you may need to add additional CSS to this file.
**Templates:** The initial page structure is rendered via Django templates
located in ``template/server``. For JavaScript, Zulip uses Handlebars templates located in
``static/templates``. Templates are precompiled as part of the build/deploy
process.
**Testing:** There are two types of frontend tests: node-based unit tests and
blackbox end-to-end tests. The blackbox tests are run in a headless browser
using Casper.js and are located in ``zerver/tests/frontend/tests/``. The unit
tests use Node's ``assert`` module are located in ``zerver/tests/frontend/node/``.
For more information on writing and running tests see the :doc:`testing
documentation <testing>`.
Example Feature
===============
This example describes the process of adding a new setting to Zulip:
a flag that restricts inviting new users to admins only (the default behavior
is that any user can invite other users). It is based on an actual Zulip feature,
and you can review `the original commit in the Zulip git repo <https://github.com/zulip/zulip/commit/5b7f3466baee565b8e5099bcbd3e1ccdbdb0a408>`_.
(Note that Zulip has since been upgraded from Django 1.6 to 1.8, so the migration
format has changed.)
First, update the database and model to store the new setting. Add a
new boolean field, ``realm_invite_by_admins_only``, to the Realm model in
``zerver/models.py``.
Then create a Django migration that adds a new field, ``invite_by_admins_only``,
to the ``zerver_realm`` table.
In ``zerver/lib/actions.py``, create a new function named
``do_set_realm_invite_by_admins_only``. This function will update the database
and trigger an event to notify clients when this setting changes. In this case
there was an exisiting ``realm|update`` event type which was used for setting
similar flags on the Realm model, so it was possible to add a new property to
that event rather than creating a new one. The property name matches the
database field to make it easy to understand what it indicates.
The second argument to ``send_event`` is the list of users whose browser
sessions should be notified. Depending on the setting, this can be a single user
(if the setting is a personal one, like time display format), only members in a
particular stream or all active users in a realm. ::
# zerver/lib/actions.py
def do_set_realm_invite_by_admins_only(realm, invite_by_admins_only):
realm.invite_by_admins_only = invite_by_admins_only
realm.save(update_fields=['invite_by_admins_only'])
event = dict(
type="realm",
op="update",
property='invite_by_admins_only',
value=invite_by_admins_only,
)
send_event(event, active_user_ids(realm))
return {}
You then need to add code that will handle the event and update the application
state. In ``zerver/lib/actions.py`` update the ``fetch_initial_state`` and
``apply_events`` functions. ::
def fetch_initial_state_data(user_profile, event_types, queue_id):
# ...
state['realm_invite_by_admins_only'] = user_profile.realm.invite_by_admins_only`
In this case you don't need to change ``apply_events`` because there is already
code that will correctly handle the realm update event type: ::
def apply_events(state, events, user_profile):
for event in events:
# ...
elif event['type'] == 'realm':
field = 'realm_' + event['property']
state[field] = event['value']
You then need to add a view for clients to access that will call the newly-added
``actions.py`` code to update the database. This example feature adds a new
parameter that should be sent to clients when the application loads and be
accessible via JavaScript, and there is already a view that does this for
related flags: ``update_realm``. So in this case, we can add out code to the
exisiting view instead of creating a new one. ::
# zerver/views/__init__.py
def home(request):
# ...
page_params = dict(
# ...
realm_invite_by_admins_only = register_ret['realm_invite_by_admins_only'],
# ...
)
Since this feature also adds a checkbox to the admin page, and adds a new
property the Realm model that can be modified from there, you also need to make
changes to the ``update_realm`` function in the same file: ::
# zerver/views/__init__.py
def update_realm(request, user_profile,
name=REQ(validator=check_string, default=None),
restricted_to_domain=REQ(validator=check_bool, default=None),
invite_by_admins_only=REQ(validator=check_bool,default=None)):
# ...
if invite_by_admins_only is not None and
realm.invite_by_admins_only != invite_by_admins_only:
do_set_realm_invite_by_admins_only(realm, invite_by_admins_only)
data['invite_by_admins_only'] = invite_by_admins_only
Then make the required front end changes: in this case a checkbox needs to be
added to the admin page (and its value added to the data sent back to server
when a realm is updated) and the change event needs to be handled on the client.
To add the checkbox to the admin page, modify the relevant template,
``static/templates/admin_tab.handlebars`` (omitted here since it is relatively
straightforward). Then add code to handle changes to the new form control in
``static/js/admin.js``. ::
var url = "/json/realm";
var new_invite_by_admins_only =
$("#id_realm_invite_by_admins_only").prop("checked");
data[invite_by_admins_only] = JSON.stringify(new_invite_by_admins_only);
channel.patch({
url: url,
data: data,
success: function (data) {
# ...
if (data.invite_by_admins_only) {
ui.report_success("New users must be invited by an admin!", invite_by_admins_only_status);
} else {
ui.report_success("Any user may now invite new users!", invite_by_admins_only_status);
}
# ...
}
});
Finally, update ``server_events.js`` to handle related events coming from the
server. ::
# static/js/server_events.js
function get_events_success(events) {
# ...
var dispatch_event = function dispatch_event(event) {
switch (event.type) {
# ...
case 'realm':
if (event.op === 'update' && event.property === 'invite_by_admins_only') {
page_params.realm_invite_by_admins_only = event.value;
}
}
}
Any code needed to update the UI should be placed in ``dispatch_event`` callback
(rather than the ``channel.patch``) function. This ensures the appropriate code
will run even if the changes are made in another browser window. In this example
most of the changes are on the backend, so no UI updates are required.

View File

@@ -1 +0,0 @@
# Empty requirements.txt to avoid readthedocs installing all our dependencies.

View File

@@ -1,323 +0,0 @@
=======
Testing
=======
Running tests
=============
To run everything, just use ``./tools/test-all``. This runs lint checks,
web frontend / whole-system blackbox tests, and backend Django tests.
If you want to run individual parts, see the various commands inside
that script.
Schema and initial data changes
-------------------------------
If you change the database schema or change the initial test data, you
have have to regenerate the pristine test database by running
``tools/do-destroy-rebuild-test-database``.
Wiping the test databases
-------------------------
You should first try running: ``tools/do-destroy-rebuild-test-database``
If that fails you should try to do:
::
sudo -u postgres psql
> DROP DATABASE zulip_test;
> DROP DATABASE zulip_test_template;
and then run ``tools/do-destroy-rebuild-test-database``
Recreating the postgres cluster
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. warning::
**This is irreversible, so do it with care, and never do this anywhere
in production.**
If your postgres cluster (collection of databases) gets totally trashed
permissions-wise, and you can't otherwise repair it, you can recreate
it. On Ubuntu:
::
sudo pg_dropcluster --stop 9.1 main
sudo pg_createcluster --locale=en_US.utf8 --start 9.1 main
Backend Django tests
--------------------
These live in ``zerver/tests.py`` and ``zerver/test_*.py``. Run them
with ``tools/test-backend``.
Web frontend black-box casperjs tests
-------------------------------------
These live in ``frontend_tests/casper_tests/``. This is a "black box"
test; we load the frontend in a real (headless) browser, from a real dev
server, and simulate UI interactions like sending messages, narrowing,
etc.
Since this is interacting with a real dev server, it can catch backend
bugs as well.
You can run this with ``./tools/test-js-with-casper`` or as
``./tools/test-js-with-casper 05-settings.js`` to run a single test
file from ``frontend_tests/casper_tests/``.
Writing Casper tests
~~~~~~~~~~~~~~~~~~~~
Probably the easiest way to learn how to write Casper tests is to
study some of the existing test files. There are a few tips that can
be useful for writing Casper tests in addition to the debugging notes
below:
- Run just the file containing your new tests as described above to
have a fast debugging cycle.
- With frontend tests in general, it's very important to write your
code to wait for the right events. Before essentially every action
you take on the page, you'll want to use ``waitForSelector``,
``waitUntilVisible``, or a similar function to make sure the page or
elemant is ready before you interact with it. For instance, if you
want to click a button that you can select via ``#btn-submit``, and
then check that it causes ``success-elt`` to appear, you'll want to
write something like:
::
casper.waitForSelector("#btn-submit", function () {
casper.click('#btn-submit')
casper.test.assertExists("#success-elt");
});
This will ensure that the element is present before the interaction
is attempted. The various wait functions supported in Casper are
documented in the Casper here:
http://docs.casperjs.org/en/latest/modules/casper.html#waitforselector
and the various assert statements available are documented here:
http://docs.casperjs.org/en/latest/modules/tester.html#the-tester-prototype
- Casper uses CSS3 selectors; you can often save time by testing and
debugigng your selectors on the relevant page of the Zulip
development app in the Chrome javascript console by using
e.g. ``$$("#settings-dropdown")``.
- The test suite uses a smaller set of default user accounts and other
data initialized in the database than the development environment;
to see what differs check out the section related to
``options["test_suite"]`` in
``zilencer/management/commands/populate_db.py``.
- Casper effectively runs your test file in two phases -- first it
runs the code in the test file, which for most test files will just
collect a series of steps (each being a ``casper.then`` or
``casper.wait...`` call). Then, usually at the end of the test
file, you'll have a ``casper.run`` call which actually runs that
series of steps. This means that if you If you write code in your
test file outside a ``casper.then`` or ``casper.wait...`` method, it
will actually run before all the Casper test steps that are declared
in the file, which can lead to confusing failures where the new code
you write in between two ``casper.then`` blocks actually runs before
either of them. See this for more details about how Casper works:
http://docs.casperjs.org/en/latest/faq.html#how-does-then-and-the-step-stack-work
Debugging Casper.JS
~~~~~~~~~~~~~~~~~~~
Casper.js (via PhantomJS) has support for remote debugging. However, it
is not perfect. Here are some steps for using it and gotchas you might
want to know.
To turn on remote debugging, pass ``--remote-debug`` to the
``./frontend_tests/tests/run`` script. This will run the tests with
port ``7777`` open for remote debugging. You can now connect to
``localhost:7777`` in a Webkit browser. Somewhat recent versions of
Chrome or Safari might be required.
- When connecting to the remote debugger, you will see a list of pages,
probably 2. One page called ``about:blank`` is the headless page in
which the CasperJS test itself is actually running in. This is where
your test code is.
- The other page, probably ``localhost:9981``, is the Zulip page that
the test is testing---that is, the page running our app that our test
is exercising.
Since the tests are now running, you can open the ``about:blank`` page,
switch to the Scripts tab, and open the running ``0x-foo.js`` test. If
you set a breakpoint and it is hit, the inspector will pause and you can
do your normal JS debugging. You can also put breakpoints in the Zulip
webpage itself if you wish to inspect the state of the Zulip frontend.
If you need to use print debugging in casper, you can do using
``casper.log``; see http://docs.casperjs.org/en/latest/logging.html
for details.
An additional debugging technique is to enable verbose mode in the
Casper tests; you can do this by adding to the top of the relevant
test file the following:
::
var casper = require('casper').create({
verbose: true,
logLevel: "debug"
});
This can sometimes give insight into exactly what's happening.
Web frontend unit tests
-----------------------
As an alternative to the black-box whole-app testing, you can unit test
individual JavaScript files that use the module pattern. For example, to
test the ``foobar.js`` file, you would first add the following to the
bottom of ``foobar.js``:
::
if (typeof module !== 'undefined') {
module.exports = foobar;
}
This makes ``foobar.js`` follow the CommonJS module pattern, so it can
be required in Node.js, which runs our tests.
Now create ``frontend_tests/node_tests/foobar.js``. At the top, require
the `Node.js assert module <http://nodejs.org/api/assert.html>`__, and
the module you're testing, like so:
::
var assert = require('assert');
var foobar = require('js/foobar.js');
(If the module you're testing depends on other modules, or modifies
global state, you need to also read `the next section`__.)
__ handling-dependencies_
Define and call some tests using the `assert
module <http://nodejs.org/api/assert.html>`__. Note that for "equal"
asserts, the *actual* value comes first, the *expected* value second.
::
(function test_somefeature() {
assert.strictEqual(foobar.somefeature('baz'), 'quux');
assert.throws(foobar.somefeature('Invalid Input'));
}());
The test runner (index.js) automatically runs all .js files in the
frontend_tests/node directory.
.. _handling-dependencies:
Handling dependencies in unit tests
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The following scheme helps avoid tests leaking globals between each
other.
First, if you can avoid globals, do it, and the code that is directly
under test can simply be handled like this:
::
var search = require('js/search_suggestion.js');
For deeper dependencies, you want to categorize each module as follows:
- Exercise the module's real code for deeper, more realistic testing?
- Stub out the module's interface for more control, speed, and
isolation?
- Do some combination of the above?
For all the modules where you want to run actual code, add a statement
like the following to the top of your test file:
::
add_dependencies({
_: 'third/underscore/underscore.js',
util: 'js/util.js',
Dict: 'js/dict.js',
Handlebars: 'handlebars',
Filter: 'js/filter.js',
typeahead_helper: 'js/typeahead_helper.js',
stream_data: 'js/stream_data.js',
narrow: 'js/narrow.js'
});
For modules that you want to completely stub out, please use a pattern
like this:
::
set_global('page_params', {
email: 'bob@zulip.com'
});
// then maybe further down
global.page_params.email = 'alice@zulip.com';
Finally, there's the hybrid situation, where you want to borrow some of
a module's real functionality but stub out other pieces. Obviously, this
is a pretty strong smell that the other module might be lacking in
cohesion, but that code might be outside your jurisdiction. The pattern
here is this:
::
// Use real versions of parse/unparse
var narrow = require('js/narrow.js');
set_global('narrow', {
parse: narrow.parse,
unparse: narrow.unparse
});
// But later, I want to stub the stream without having to call super-expensive
// real code like narrow.activate().
global.narrow.stream = function () {
return 'office';
};
Coverage reports
~~~~~~~~~~~~~~~~
You can automatically generate coverage reports for the JavaScript unit
tests. To do so, install istanbul:
::
sudo npm install -g istanbul
And run test-js-with-node with the 'cover' parameter:
::
tools/test-js-with-node cover
Then open ``coverage/lcov-report/js/index.html`` in your browser.
Modules we don't test *at all* aren't listed in the report, so this
tends to overstate how good our overall coverage is, but it's accurate
for individual files. You can also click a filename to see the specific
statements and branches not tested. 100% branch coverage isn't
necessarily possible, but getting to at least 80% branch coverage is a
good goal.
Manual testing (local app + web browser)
========================================
Setting up the manual testing database
--------------------------------------
::
./tools/do-destroy-rebuild-database
Will populate your local database with all the usual accounts plus some
test messages involving Shakespeare characters.

View File

@@ -1,2 +0,0 @@
/server.log
/test_credentials.js

View File

@@ -1,46 +0,0 @@
var common = require('../casper_lib/common.js').common;
common.start_and_log_in();
casper.then(function () {
casper.test.info('Subscriptions page');
casper.click('a[href^="#subscriptions"]');
casper.test.assertUrlMatch(/^http:\/\/[^\/]+\/#subscriptions/, 'URL suggests we are on subscriptions page');
casper.test.assertExists('#subscriptions.tab-pane.active', 'Subscriptions page is active');
// subscriptions need to load; if they have *any* subs,
// the word "Unsubscribe" will appear
});
casper.waitForText('Subscribed', function () {
casper.test.assertTextExists('Subscribed', 'Initial subscriptions loaded');
casper.fill('form#add_new_subscription', {stream_name: 'Waseemio'});
casper.click('form#add_new_subscription input.btn');
});
casper.waitForText('Waseemio', function () {
casper.test.assertTextExists('Create stream Waseemio', 'Modal for specifying new stream users');
casper.click('form#stream_creation_form button.btn.btn-primary');
});
casper.waitFor(function () {
return casper.evaluate(function () {
return $('.subscription_name').is(':contains("Waseemio")');
});
});
casper.then(function () {
casper.test.assertSelectorHasText('.subscription_name', 'Waseemio', 'Subscribing to a stream');
casper.fill('form#add_new_subscription', {stream_name: 'WASeemio'});
casper.click('form#add_new_subscription input.btn');
});
casper.waitForText('Already subscribed', function () {
casper.test.assertTextExists('Already subscribed', "Can't subscribe twice to a stream");
casper.fill('form#add_new_subscription', {stream_name: ' '});
casper.click('form#add_new_subscription input.btn');
});
casper.waitForText('Error adding subscription', function () {
casper.test.assertTextExists('Error adding subscription', "Can't subscribe to an empty stream name");
});
common.then_log_out();
casper.run(function () {
casper.test.done();
});

View File

@@ -1,77 +0,0 @@
var common = require('../casper_lib/common.js').common;
var test_credentials = require('../casper_lib/test_credentials.js').test_credentials;
common.start_and_log_in();
casper.then(function () {
casper.test.info('Administration page');
casper.click('a[href^="#administration"]');
casper.test.assertUrlMatch(/^http:\/\/[^\/]+\/#administration/, 'URL suggests we are on administration page');
casper.test.assertExists('#administration.tab-pane.active', 'Administration page is active');
});
// Test user deactivation and reactivation
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]', function () {
casper.test.assertSelectorHasText('.user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
casper.click('.user_row[id="user_cordelia@zulip.com"] .deactivate');
casper.test.assertTextExists('Deactivate cordelia@zulip.com', 'Deactivate modal has right user');
casper.test.assertTextExists('Deactivate now', 'Deactivate now button available');
casper.click('#do_deactivate_user_button');
});
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"].deactivated_user', function () {
casper.test.assertSelectorHasText('.user_row[id="user_cordelia@zulip.com"]', 'Reactivate');
casper.click('.user_row[id="user_cordelia@zulip.com"] .reactivate');
});
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]:not(.deactivated_user)', function () {
casper.test.assertSelectorHasText('.user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
});
// Test Deactivated users section of admin page
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]', function () {
casper.test.assertSelectorHasText('.user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
casper.click('.user_row[id="user_cordelia@zulip.com"] .deactivate');
casper.test.assertTextExists('Deactivate cordelia@zulip.com', 'Deactivate modal has right user');
casper.test.assertTextExists('Deactivate now', 'Deactivate now button available');
casper.click('#do_deactivate_user_button');
});
casper.then(function () {
// Leave the page and return
casper.click('#settings-dropdown');
casper.click('a[href^="#subscriptions"]');
casper.click('#settings-dropdown');
casper.click('a[href^="#administration"]');
casper.waitForSelector('.user_row[id="user_cordelia@zulip.com"]', function () {
casper.test.assertSelectorHasText('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"]', 'Reactivate');
casper.click('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"] .reactivate');
});
casper.waitForSelector('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"]:not(.deactivated_user)', function () {
casper.test.assertSelectorHasText('#admin_deactivated_users_table .user_row[id="user_cordelia@zulip.com"]', 'Deactivate');
});
});
// Test bot deactivation and reactivation
casper.waitForSelector('.user_row[id="user_new-user-bot@zulip.com"]', function () {
casper.test.assertSelectorHasText('.user_row[id="user_new-user-bot@zulip.com"]', 'Deactivate');
casper.click('.user_row[id="user_new-user-bot@zulip.com"] .deactivate');
});
casper.waitForSelector('.user_row[id="user_new-user-bot@zulip.com"].deactivated_user', function () {
casper.test.assertSelectorHasText('.user_row[id="user_new-user-bot@zulip.com"]', 'Reactivate');
casper.click('.user_row[id="user_new-user-bot@zulip.com"] .reactivate');
});
casper.waitForSelector('.user_row[id="user_new-user-bot@zulip.com"]:not(.deactivated_user)', function () {
casper.test.assertSelectorHasText('.user_row[id="user_new-user-bot@zulip.com"]', 'Deactivate');
});
// TODO: Test stream deletion
common.then_log_out();
casper.run(function () {
casper.test.done();
});

View File

@@ -1,108 +0,0 @@
var _ = global._;
set_global('$', function (f) {
if (f) {
return f();
}
return {trigger: function () {}};
});
set_global('document', null);
var page_params = {
bot_list: [{email: 'bot0@zulip.com', full_name: 'Bot 0'}],
is_admin: false,
email: 'owner@zulip.com'
};
set_global('page_params', page_params);
var patched_underscore = _.clone(_);
patched_underscore.debounce = function (f) { return(f); };
global.patch_builtin('_', patched_underscore);
var bot_data = require('js/bot_data.js');
// Our startup logic should have added Bot 0 from page_params.
assert.equal(bot_data.get('bot0@zulip.com').full_name, 'Bot 0');
(function () {
var test_bot = {
email: 'bot1@zulip.com',
avatar_url: '',
default_all_public_streams: '',
default_events_register_stream: '',
default_sending_stream: '',
full_name: 'Bot 1',
extra: 'Not in data'
};
(function test_add() {
bot_data.add(test_bot);
var bot = bot_data.get('bot1@zulip.com');
assert.equal('Bot 1', bot.full_name);
assert.equal(undefined, bot.extra);
}());
(function test_update() {
var bot;
bot_data.add(test_bot);
bot = bot_data.get('bot1@zulip.com');
assert.equal('Bot 1', bot.full_name);
bot_data.update('bot1@zulip.com', {full_name: 'New Bot 1'});
bot = bot_data.get('bot1@zulip.com');
assert.equal('New Bot 1', bot.full_name);
}());
(function test_remove() {
var bot;
bot_data.add(test_bot);
bot = bot_data.get('bot1@zulip.com');
assert.equal('Bot 1', bot.full_name);
bot_data.remove('bot1@zulip.com');
bot = bot_data.get('bot1@zulip.com');
assert.equal(undefined, bot);
}());
(function test_owner_can_admin() {
var bot;
bot_data.add(_.extend({owner: 'owner@zulip.com'}, test_bot));
bot = bot_data.get('bot1@zulip.com');
assert(bot.can_admin);
bot_data.add(_.extend({owner: 'notowner@zulip.com'}, test_bot));
bot = bot_data.get('bot1@zulip.com');
assert.equal(false, bot.can_admin);
}());
(function test_admin_can_admin() {
var bot;
page_params.is_admin = true;
bot_data.add(test_bot);
bot = bot_data.get('bot1@zulip.com');
assert(bot.can_admin);
page_params.is_admin = false;
}());
(function test_get_editable() {
var can_admin;
bot_data.add(_.extend({}, test_bot, {owner: 'owner@zulip.com'}));
bot_data.add(_.extend({}, test_bot, {email: 'bot2@zulip.com'}));
can_admin = _.pluck(bot_data.get_editable(), 'email');
assert.deepEqual(['bot1@zulip.com'], can_admin);
}());
}());

View File

@@ -1,20 +0,0 @@
var hashchange = require('js/hashchange.js');
(function test_basics() {
var operators;
var hash;
operators = [
{operator: 'stream', operand: 'devel'},
{operator: 'topic', operand: 'algol'}
];
hash = hashchange.operators_to_hash(operators);
assert.equal(hash, '#narrow/stream/devel/topic/algol');
operators = [
{operator: 'stream', operand: 'devel'},
{operator: 'topic', operand: 'visual c++', negated: true}
];
hash = hashchange.operators_to_hash(operators);
assert.equal(hash, '#narrow/stream/devel/-topic/visual.20c.2B.2B');
}());

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