├── .env.example ├── .gitignore ├── .jshintrc ├── .pylintrc ├── .travis.yml ├── Dockerfile ├── LICENSE ├── Makefile ├── Procfile ├── README.md ├── botbot ├── __init__.py ├── apps │ ├── __init__.py │ ├── accounts │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20150630_1459.py │ │ │ ├── 0003_auto_20151026_1950.py │ │ │ └── __init__.py │ │ └── models.py │ ├── bots │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20150630_1459.py │ │ │ ├── 0003_remove_channel_users.py │ │ │ ├── 0004_channel_status.py │ │ │ ├── 0005_move_to_status_choices.py │ │ │ ├── 0006_auto_20151030_1406.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── sitemaps.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── channel_url.py │ │ ├── tests.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── kudos │ │ ├── __init__.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── kudos.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── sandbox.py │ │ └── utils.py │ ├── logs │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── forms.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── redact.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── logs_tags.py │ │ ├── tests.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ ├── plugins │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── core │ │ │ ├── __init__.py │ │ │ ├── help.py │ │ │ └── logger.py │ │ ├── forms.py │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ └── run_plugins.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_auto_20140912_1656.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── plugin.py │ │ ├── runner.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── plugin_docs.py │ │ ├── tests.py │ │ └── utils.py │ ├── preview │ │ ├── __init__.py │ │ └── views.py │ └── sitemap │ │ ├── __init__.py │ │ └── urls.py ├── core │ ├── __init__.py │ ├── fields.py │ ├── middleware.py │ ├── models.py │ ├── paginator.py │ ├── templatetags │ │ ├── __init__.py │ │ └── verbatim.py │ └── tests.py ├── jinja2.py ├── less │ ├── animate-custom.less │ ├── banners.less │ ├── bootstrap │ │ ├── accordion.less │ │ ├── alerts.less │ │ ├── bootstrap.less │ │ ├── breadcrumbs.less │ │ ├── button-groups.less │ │ ├── buttons.less │ │ ├── carousel.less │ │ ├── close.less │ │ ├── code.less │ │ ├── component-animations.less │ │ ├── dropdowns.less │ │ ├── forms.less │ │ ├── grid.less │ │ ├── hero-unit.less │ │ ├── labels-badges.less │ │ ├── layouts.less │ │ ├── mixins.less │ │ ├── modals.less │ │ ├── navbar.less │ │ ├── navs.less │ │ ├── pager.less │ │ ├── pagination.less │ │ ├── popovers.less │ │ ├── progress-bars.less │ │ ├── reset.less │ │ ├── responsive-1200px-min.less │ │ ├── responsive-767px-max.less │ │ ├── responsive-768px-979px.less │ │ ├── responsive-navbar.less │ │ ├── responsive-utilities.less │ │ ├── responsive.less │ │ ├── scaffolding.less │ │ ├── sprites.less │ │ ├── tables.less │ │ ├── thumbnails.less │ │ ├── tooltip.less │ │ ├── type.less │ │ ├── utilities.less │ │ ├── variables.less │ │ └── wells.less │ ├── font-awesome │ │ ├── bootstrap.less │ │ ├── core.less │ │ ├── extras.less │ │ ├── font-awesome-ie7.less │ │ ├── font-awesome.less │ │ ├── icons.less │ │ ├── mixins.less │ │ ├── path.less │ │ └── variables.less │ ├── globals.less │ ├── management.less │ ├── nick-colors.less │ ├── responsive.less │ ├── screen.less │ └── timeline.less ├── settings │ ├── __init__.py │ ├── _asset_pipeline.py │ └── base.py ├── static │ ├── .gitignore │ ├── css │ │ └── screen.css │ ├── data │ │ └── world-110m2.json │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ └── fontawesome-webfont.woff │ ├── img │ │ ├── glyphicons-halflings-white.png │ │ ├── glyphicons-halflings.png │ │ ├── timeline-circle-large.svg │ │ ├── timeline-circle-selected-large.svg │ │ ├── timeline-circle-selected-small.svg │ │ ├── timeline-circle-small.svg │ │ └── timeline-more.svg │ ├── js │ │ ├── app │ │ │ ├── app.js │ │ │ ├── common.js │ │ │ ├── logs │ │ │ │ └── default.js │ │ │ └── manage │ │ │ │ ├── default.js │ │ │ │ └── models.js │ │ └── vendor │ │ │ ├── andlog.js │ │ │ ├── backbone.js │ │ │ ├── bootstrap.js │ │ │ ├── d3.v3.min.js │ │ │ ├── detect_timezone.js │ │ │ ├── handlebars.js │ │ │ ├── jquery-1.8.2.js │ │ │ ├── jquery-ui-1.9.1.custom.js │ │ │ ├── jquery.highlight.js │ │ │ ├── moment.js │ │ │ ├── topojson.v1.min.js │ │ │ ├── underscore.js │ │ │ └── waypoints.js │ └── robots.txt ├── templates │ ├── 404.html │ ├── 500.html │ ├── base.html │ ├── bootstrap_toolkit │ │ ├── button.html │ │ ├── field.html │ │ ├── field_checkbox.html │ │ ├── field_choices.html │ │ ├── field_default.html │ │ ├── field_errors.html │ │ ├── field_help.html │ │ ├── field_horizontal.html │ │ ├── field_inline.html │ │ ├── field_prepend_append.html │ │ ├── field_search.html │ │ ├── field_vertical.html │ │ ├── field_visible.html │ │ ├── form.html │ │ ├── formset.html │ │ ├── icon.html │ │ ├── messages.html │ │ ├── nav.html │ │ ├── non_field_error.html │ │ ├── non_field_errors.html │ │ ├── pagination.html │ │ ├── pills.html │ │ └── tabs.html │ ├── channel_list.html │ ├── home.html │ ├── includes │ │ ├── checkbox.html │ │ ├── field.html │ │ ├── footer.html │ │ ├── google-analytics.html │ │ └── header.html │ ├── launchpad │ │ ├── signup.html │ │ ├── success.html │ │ └── unsubscribe.html │ └── logs │ │ ├── help.html │ │ ├── kudos.html │ │ ├── log_display.html │ │ ├── logs.html │ │ └── logs.txt ├── urls │ ├── __init__.py │ ├── admin.py │ └── base.py └── wsgi.py ├── docs ├── Makefile ├── conf.py ├── developers.rst ├── getting_started.rst ├── images │ └── botbot-architecture.png ├── index.rst ├── install.rst ├── irc_resources.rst ├── managing_bots.rst ├── plugins.rst ├── production.rst └── troubleshooting.rst ├── manage.py ├── nginx.conf.example ├── requirements.txt └── setup.py /.env.example: -------------------------------------------------------------------------------- 1 | # Required 2 | SECRET_KEY=supersecretkeyhere 3 | WEB_PORT=8000 4 | EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend' 5 | GOPATH=$VIRTUAL_ENV 6 | WEB_SECRET_KEY=somerandomstring 7 | STORAGE_URL=postgres://user:pass@localhost:5432/botbot 8 | REDIS_PLUGIN_STORAGE_URL=redis://localhost:6379/0 9 | REDIS_PLUGIN_QUEUE_URL=redis://localhost:6379/1 10 | PUSH_STREAM_URL=http://localhost:8080/pub/?id={id} 11 | 12 | # Set encoding if the system hasn't done it properly 13 | LANG=en_US.UTF-8 14 | PYTHONIOENCODING=utf8 15 | 16 | # Optional 17 | # MEMCACHE_URL=127.0.0.1:11211 18 | # STATIC_ROOT=/var/www/botbot/static 19 | # MEDIA_ROOT=/var/www/botbot/uploads 20 | # DEBUG=True 21 | # SMTP_URL=smtp://user:pass@host:port 22 | # SMTP_TLS=True 23 | # ALLOWED_HOSTS=host1,host2 24 | # INCLUDE_DJANGO_ADMIN=False 25 | # EXCLUDE_NICKS=nick1,nick2 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | botbot.egg-info 3 | .pip-timestamp 4 | docs/_build 5 | *.pyc 6 | settings_debug.py 7 | .DS_Store 8 | node_modules/ 9 | venv/ 10 | .idea/ 11 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "jasmine", 4 | "spyOn", 5 | "it", 6 | "beforeEach", 7 | "describe", 8 | "expect", 9 | "$", 10 | "jQuery", 11 | "Backbone", 12 | "Handlebars", 13 | "EventSource", 14 | "moment", 15 | "jstz", 16 | "_", 17 | "_gaq", 18 | "mixpanel", 19 | "$$", 20 | "ohrl", 21 | "log", 22 | "console" 23 | ], 24 | 25 | "node" : true, 26 | "browser" : true, 27 | "boss" : false, 28 | "curly": false, 29 | "debug": false, 30 | "devel": false, 31 | "eqeqeq": true, 32 | "expr": true, 33 | "evil": false, 34 | "forin": false, 35 | "immed": true, 36 | "laxbreak": false, 37 | "newcap": true, 38 | "noarg": true, 39 | "noempty": true, 40 | "nonew": false, 41 | "nomen": false, 42 | "onevar": true, 43 | "plusplus": false, 44 | "regexp": false, 45 | "undef": true, 46 | "sub": true, 47 | "strict": false, 48 | "white": true 49 | } 50 | 51 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | 2 | [MASTER] 3 | 4 | # Add to the black list. It should be a base name, not a 5 | # path. You may set this option multiple times. 6 | ignore=conf 7 | ignore=migrations 8 | 9 | 10 | [MESSAGES CONTROL] 11 | 12 | # Disable pylint checkers which don't play well with Django 13 | # or which don't help us 14 | # 15 | #C0111 Missing docstring # We're grown ups. Maybe. 16 | # 17 | #E1002 Use super on an old style class # Pylint (incorrectly) thinks CBVs are old style classes 18 | #E1101 %s %r has no %r member # ForeignKey has lots of magic methods 19 | #E1103 %s %r has no %r member (but some types could not be inferred) 20 | # 21 | #R0201 Method could be a function # It's useful to organise methods into classes 22 | #R0901 Too many ancestors # CBVs 23 | #R0902 Too many instance attributes # BaseTestCase has lots of attributes 24 | #R0903 Too few public methods (%s/%s) # Meta, Admin, etc 25 | #R0904 Too many public methods (%s/%s) # All managers and CBVs 26 | #R0921 Abstract class not referenced # Abstract class in different file 27 | # 28 | #W0142 Used * or ** magic # Never used by accident 29 | #W0232 Class has no __init__ method # Django is different 30 | 31 | # Disable pylint checkers which don't help us 32 | # 33 | 34 | disable=C0111,E1002,E1101,E1103,R0201,R0901,R0903,R0904,R0902,R0921,W0142,W0232,R0801 35 | 36 | 37 | [BASIC] 38 | 39 | # Good variable names which should always be accepted, separated by a comma 40 | # Anytime you get an E1101 that you consider invalid, add the variable here. 41 | good-names=_,i,j,k,pk,qs,dt,urlpatterns,register,setUp,tearDown 42 | 43 | # Bad variable names which should always be refused, separated by a comma 44 | bad-names=foo,bar,baz,toto,tutu,tata 45 | 46 | # Increase max method name length, to include tests.py methods names 47 | # Regular expression which should only match correct method names 48 | method-rgx=[a-z_][a-z0-9_]{2,50}$ 49 | 50 | 51 | [TYPECHECK] 52 | 53 | # List of members which are set dynamically and missed by pylint inference 54 | generated-members=objects,DoesNotExist,id,pk,_default_manager,_meta,get_profile 55 | 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: precise 3 | python: 4 | - "2.7" 5 | install: 6 | - "pip install -e . -r requirements.txt" 7 | services: 8 | - postgresql 9 | - redis-server 10 | before_script: 11 | # Necessary variables 12 | - "export WEB_SECRET_KEY=somerandomstring" 13 | - "export STORAGE_URL=postgres://postgres@localhost:5432/botbot" 14 | - "export REDIS_PLUGIN_STORAGE_URL=redis://localhost:6379/0" 15 | - "export REDIS_PLUGIN_QUEUE_URL=redis://localhost:6379/1" 16 | - "psql -c 'create database botbot;' -U postgres" 17 | - "psql -c 'create extension hstore;' -U postgres botbot" 18 | - "psql -c 'create extension hstore;' -U postgres template1" 19 | - "echo 'STORAGE_URL=postgres://postgres@localhost:5432/botbot' >> .env" 20 | - "manage.py collectstatic --noinput" 21 | script: "manage.py test" 22 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:14.04 2 | MAINTAINER Yann Malet 3 | RUN locale-gen en_US.UTF-8 4 | ENV LANG en_US.UTF-8 5 | ENV LANGUAGE en_US:en 6 | ENV LC_ALL en_US.UTF-8 7 | RUN DEBIAN_FRONTEND=noninteractive apt-get update 8 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y python python-pip python-dev \ 9 | libmemcached-dev \ 10 | build-essential locales git-core \ 11 | libpq-dev libjpeg8-dev zlib1g-dev libfreetype6-dev liblcms2-dev 12 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y curl 13 | 14 | EXPOSE 8080 15 | ADD . /srv/botbot-web 16 | WORKDIR /srv/botbot-web 17 | 18 | RUN pip install -r requirements.txt -e . \ 19 | --src /srv/python-src/\ 20 | --timeout=120 21 | 22 | CMD manage.py runserver 0.0.0.0:8080 --settings=botbot.settings 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BotBot Logo is copyright Lincoln Loop, LLC. All rights reserved. 2 | 3 | Some JavaScript and CSS files are included and provide their own licenses. 4 | 5 | All original code is provided under the MIT License: 6 | 7 | The MIT License (MIT) 8 | 9 | Copyright (c) 2015 Lincoln Loop, LLC 10 | 11 | Permission is hereby granted, free of charge, to any person obtaining a copy 12 | of this software and associated documentation files (the "Software"), to deal 13 | in the Software without restriction, including without limitation the rights 14 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 | copies of the Software, and to permit persons to whom the Software is 16 | furnished to do so, subject to the following conditions: 17 | 18 | The above copyright notice and this permission notice shall be included in 19 | all copies or substantial portions of the Software. 20 | 21 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 | THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Developer Makefile 2 | 3 | ### Default Configuration. Override in a make.config file 4 | 5 | # the path to the lib folder in the venv 6 | LOCAL_LIB=$(VIRTUAL_ENV)/lib 7 | 8 | # where executables are stored 9 | LOCAL_BIN=$(VIRTUAL_ENV)/bin 10 | 11 | # where source files are stored 12 | LOCAL_SRC=$(VIRTUAL_ENV)/src 13 | 14 | # where misc files are stored 15 | LOCAL_VAR=$(VIRTUAL_ENV)/var 16 | 17 | NPM_BIN := $(or $(shell command -v npm),$(LOCAL_BIN)/npm) 18 | LESS_BIN := $(or $(shell command -v lessc),$(LOCAL_BIN)/lessc) 19 | JSHINT_BIN := $(or $(shell command -v jshint),$(LOCAL_BIN)/jshint) 20 | WATCHMEDO_BIN := $(or $(shell command -v watchmedo),$(LOCAL_BIN)/watchmedo) 21 | 22 | # allows a file make.config to override the above variables 23 | -include make.config 24 | 25 | ### PIP 26 | .pip-timestamp: requirements.txt 27 | pip install -r requirements.txt 28 | touch .pip-timestamp 29 | 30 | pip-install: .pip-timestamp 31 | 32 | $(LOCAL_BIN)/sphinx-build: 33 | pip install Sphinx 34 | 35 | ### GENERAL PYTHON COMMANDS 36 | clean-pyc: 37 | find . -name '*.pyc' -exec rm -f {} + 38 | find . -name '*.pyo' -exec rm -f {} + 39 | find . -name '*~' -exec rm -f {} + 40 | 41 | ### GO SUPPORT 42 | 43 | $(LOCAL_BIN)/botbot-bot: 44 | GOPATH=$(VIRTUAL_ENV) go get github.com/BotBotMe/botbot-bot 45 | 46 | test-bot: 47 | GOPATH=$(VIRTUAL_ENV) go test github.com/BotBotMe/botbot-bot 48 | 49 | ### LOCAL LESS SUPPORT 50 | $(NPM_BIN): 51 | @echo "Installing node.js..." 52 | cd $(VIRTUAL_ENV) && mkdir -p "src" 53 | cd $(LOCAL_LIB) && curl http://nodejs.org/dist/node-latest.tar.gz | tar xvz 54 | cd $(LOCAL_LIB)/node-v* && ./configure --prefix=$(VIRTUAL_ENV) && make install 55 | @echo "Installed npm" 56 | 57 | $(LESS_BIN): $(NPM_BIN) 58 | NPM_CONFIG_PREFIX=$(VIRTUAL_ENV) npm install "less@<1.4" -g 59 | 60 | less-install: $(LESS_BIN) 61 | 62 | less-compile: 63 | lessc botbot/less/screen.less > botbot/static/css/screen.css 64 | 65 | $(WATCHMEDO_BIN): 66 | # Install watchdog to run commands when files change 67 | pip install watchdog argcomplete 68 | 69 | less-watch: $(WATCHMEDO_BIN) 70 | watchmedo shell-command --patterns=*.less --recursive --command="make less-compile" botbot/less 71 | 72 | 73 | ### Local JSHint 74 | 75 | $(JSHINT_BIN): $(NPM_BIN) 76 | NPM_CONFIG_PREFIX=$(VIRTUAL_ENV) npm install jshint -g 77 | 78 | jshint-install: $(JSHINT_BIN) 79 | 80 | jshint: 81 | jshint botbot/static/js/app/ 82 | 83 | ### Local Settings 84 | 85 | .env: 86 | cp .env.example $@ 87 | 88 | local-settings: .env 89 | 90 | ### General Tasks 91 | dependencies: less-install pip-install local-settings $(LOCAL_BIN)/botbot-bot 92 | 93 | $(LOCAL_VAR)/GeoLite2-City.mmdb: 94 | curl http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz | gunzip -c > $@ 95 | 96 | geoip-db: $(LOCAL_VAR)/GeoLite2-City.mmdb 97 | 98 | run: dependencies 99 | honcho start 100 | 101 | docs: $(LOCAL_BIN)/sphinx-build 102 | cd docs && make html 103 | 104 | .PHONY: clean-pyc run pip-install less-install jshint-install dependencies local-settings docs geoip-db 105 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: manage.py runserver $WEB_PORT 2 | plugins: manage.py run_plugins 3 | bot: botbot-bot -v=2 -logtostderr=true 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ``` 2 | ____ __ ____ __ 3 | / __ )____ / /_/ __ )____ / /_ 4 | / __ / __ \/ __/ __ / __ \/ __/ 5 | / /_/ / /_/ / /_/ /_/ / /_/ / /__ 6 | /_____/\____/\__/_____/\____/\___/ 7 | 8 | ``` 9 | 10 | [![Build Status](https://api.travis-ci.org/BotBotMe/botbot-web.png)](https://travis-ci.org/BotBotMe/botbot-web) 11 | 12 | Botbot is collection of tools for running IRC bots. It has primarily been 13 | used with Freenode channels but works with other IRC networks or servers. 14 | 15 | [Documentation](http://botbot.readthedocs.org/en/latest/) 16 | -------------------------------------------------------------------------------- /botbot/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/__init__.py -------------------------------------------------------------------------------- /botbot/apps/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/__init__.py -------------------------------------------------------------------------------- /botbot/apps/accounts/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/accounts/__init__.py -------------------------------------------------------------------------------- /botbot/apps/accounts/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from . import models 5 | 6 | 7 | class CustomUserAdmin(UserAdmin): 8 | list_display = ( 9 | 'username', 'email', 'first_name', 'last_name', 'is_staff', 'date_joined') 10 | 11 | 12 | admin.site.register(models.User, CustomUserAdmin) 13 | -------------------------------------------------------------------------------- /botbot/apps/accounts/migrations/0002_auto_20150630_1459.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import django.core.validators 6 | import django.contrib.auth.models 7 | from django.conf import settings 8 | 9 | 10 | class Migration(migrations.Migration): 11 | 12 | dependencies = [ 13 | ('auth', '0006_require_contenttypes_0002'), 14 | ('bots', '0001_initial'), 15 | ('accounts', '0001_initial'), 16 | ] 17 | 18 | operations = [ 19 | migrations.AlterModelManagers( 20 | name='user', 21 | managers=[ 22 | ('objects', django.contrib.auth.models.UserManager()), 23 | ], 24 | ), 25 | migrations.AddField( 26 | model_name='membership', 27 | name='channel', 28 | field=models.ForeignKey(default=1, to='bots.Channel'), 29 | preserve_default=False, 30 | ), 31 | migrations.AddField( 32 | model_name='membership', 33 | name='user', 34 | field=models.ForeignKey(default=1, to=settings.AUTH_USER_MODEL), 35 | preserve_default=False, 36 | ), 37 | migrations.AddField( 38 | model_name='user', 39 | name='groups', 40 | field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Group', blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', verbose_name='groups'), 41 | ), 42 | migrations.AddField( 43 | model_name='user', 44 | name='user_permissions', 45 | field=models.ManyToManyField(related_query_name='user', related_name='user_set', to='auth.Permission', blank=True, help_text='Specific permissions for this user.', verbose_name='user permissions'), 46 | ), 47 | migrations.AlterField( 48 | model_name='user', 49 | name='email', 50 | field=models.EmailField(max_length=254, verbose_name='email address', blank=True), 51 | ), 52 | migrations.AlterField( 53 | model_name='user', 54 | name='last_login', 55 | field=models.DateTimeField(null=True, verbose_name='last login', blank=True), 56 | ), 57 | migrations.AlterField( 58 | model_name='user', 59 | name='username', 60 | field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, max_length=30, validators=[django.core.validators.RegexValidator('^[\\w.@+-]+$', 'Enter a valid username. This value may contain only letters, numbers and @/./+/-/_ characters.', 'invalid')], help_text='Required. 30 characters or fewer. Letters, digits and @/./+/-/_ only.', unique=True, verbose_name='username'), 61 | ), 62 | migrations.AlterUniqueTogether( 63 | name='membership', 64 | unique_together=set([('user', 'channel')]), 65 | ), 66 | ] 67 | -------------------------------------------------------------------------------- /botbot/apps/accounts/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/accounts/migrations/__init__.py -------------------------------------------------------------------------------- /botbot/apps/accounts/models.py: -------------------------------------------------------------------------------- 1 | import pytz 2 | from django.contrib.auth import models as auth_models 3 | from django.db import models 4 | 5 | TIMEZONE_CHOICES = [(tz, tz.replace('_', ' ')) for tz in pytz.common_timezones] 6 | 7 | 8 | class User(auth_models.AbstractUser): 9 | nick = models.CharField("Preferred nick", max_length=100, blank=True) 10 | timezone = models.CharField(max_length=50, choices=TIMEZONE_CHOICES, 11 | blank=True) 12 | 13 | class Meta: 14 | db_table = 'auth_user' 15 | -------------------------------------------------------------------------------- /botbot/apps/bots/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/bots/__init__.py -------------------------------------------------------------------------------- /botbot/apps/bots/admin.py: -------------------------------------------------------------------------------- 1 | """Django admin configuration for the bot objects. 2 | """ 3 | import redis 4 | from django import forms 5 | from django.conf import settings 6 | from django.contrib import admin 7 | from django.forms.models import BaseInlineFormSet 8 | 9 | from . import models 10 | 11 | 12 | class PluginFormset(BaseInlineFormSet): 13 | def __init__(self, *args, **kwargs): 14 | super(PluginFormset, self).__init__(*args, **kwargs) 15 | 16 | 17 | class ActivePluginInline(admin.StackedInline): 18 | model = models.Channel.plugins.through 19 | formset = PluginFormset 20 | 21 | def get_extra(self, request, obj=None, **kwargs): 22 | return 0 23 | 24 | 25 | 26 | class ChatBotAdmin(admin.ModelAdmin): 27 | exclude = ('connection', 'server_identifier') 28 | list_display = ('__unicode__', 'is_active', 'usage') 29 | list_editable = ('is_active',) 30 | list_filter = ('is_active',) 31 | readonly_fields = ('server_identifier',) 32 | 33 | # Disable bulk delete, because it doesn't call delete, so skips REFRESH 34 | actions = None 35 | 36 | def usage(self, obj): 37 | return "%d%%" % ( 38 | (obj.channel_set.filter(status=models.Channel.ACTIVE).count() / float(obj.max_channels)) * 100) 39 | 40 | 41 | def botbot_refresh(modeladmin, request, queryset): 42 | """ 43 | Ask daemon to reload configuration 44 | """ 45 | queue = redis.from_url(settings.REDIS_PLUGIN_QUEUE_URL) 46 | queue.lpush('bot', 'REFRESH') 47 | botbot_refresh.short_description = "Reload botbot-bot configuration" 48 | 49 | 50 | class ChannelForm(forms.ModelForm): 51 | class Meta: 52 | model = models.Channel 53 | exclude = [] 54 | 55 | def clean_private_slug(self): 56 | return self.cleaned_data['private_slug'] or None 57 | 58 | 59 | class ChannelAdmin(admin.ModelAdmin): 60 | form = ChannelForm 61 | list_display = ('name', 'chatbot', 'status', 'is_featured', 'created', 'updated') 62 | list_filter = ('status', 'is_featured', 'is_public', 'chatbot') 63 | prepopulated_fields = { 64 | 'slug': ('name',) 65 | } 66 | list_editable = ('chatbot','status',) 67 | readonly_fields = ('fingerprint', 'created', 'updated') 68 | search_fields = ('name', 'chatbot__server') 69 | inlines = [ActivePluginInline] 70 | actions = [botbot_refresh] 71 | 72 | 73 | class PublicChannelApproval(ChannelAdmin): 74 | def has_add_permission(self, request): 75 | return False 76 | 77 | def get_queryset(self, request): 78 | qs = super(PublicChannelApproval, self).get_queryset(request) 79 | return qs.filter(status=self.model.ACTIVE, is_public=True) 80 | 81 | 82 | class PublicChannels(models.Channel): 83 | class Meta: 84 | proxy = True 85 | verbose_name = "Pending Public Channel" 86 | 87 | admin.site.register(PublicChannels, PublicChannelApproval) 88 | 89 | admin.site.register(models.ChatBot, ChatBotAdmin) 90 | admin.site.register(models.Channel, ChannelAdmin) 91 | admin.site.register(models.UserCount) 92 | -------------------------------------------------------------------------------- /botbot/apps/bots/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import djorm_pgarray.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Channel', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), 18 | ('created', models.DateTimeField(help_text='', auto_now_add=True)), 19 | ('updated', models.DateTimeField(help_text='', auto_now=True)), 20 | ('name', models.CharField(max_length=250, help_text=b'IRC expects room name: #django')), 21 | ('slug', models.SlugField(help_text='')), 22 | ('private_slug', models.SlugField(unique=True, blank=True, null=True, help_text=b'Slug used for private rooms')), 23 | ('password', models.CharField(max_length=250, blank=True, null=True, help_text=b'Password (mode +k) if the channel requires one')), 24 | ('is_public', models.BooleanField(default=False, help_text='')), 25 | ('is_active', models.BooleanField(default=True, help_text='')), 26 | ('is_pending', models.BooleanField(default=False, help_text='')), 27 | ('is_featured', models.BooleanField(default=False, help_text='')), 28 | ('fingerprint', models.CharField(max_length=36, blank=True, null=True, help_text='')), 29 | ('public_kudos', models.BooleanField(default=True, help_text='')), 30 | ('notes', models.TextField(blank=True, help_text='')), 31 | ], 32 | options={ 33 | 'ordering': ('name',), 34 | }, 35 | bases=(models.Model,), 36 | ), 37 | migrations.CreateModel( 38 | name='ChatBot', 39 | fields=[ 40 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), 41 | ('is_active', models.BooleanField(default=False, help_text='')), 42 | ('server', models.CharField(max_length=100, help_text=b'Format: irc.example.net:6697')), 43 | ('server_password', models.CharField(max_length=100, blank=True, null=True, help_text=b'IRC server password - PASS command. Optional')), 44 | ('server_identifier', models.CharField(max_length=164, help_text='')), 45 | ('nick', models.CharField(max_length=64, help_text='')), 46 | ('password', models.CharField(max_length=100, blank=True, null=True, help_text=b'Password to identify with NickServ. Optional.')), 47 | ('real_name', models.CharField(max_length=250, help_text=b'Usually a URL with information about this bot.')), 48 | ('slug', models.CharField(max_length=50, db_index=True, help_text='')), 49 | ('max_channels', models.IntegerField(default=200, help_text='')), 50 | ], 51 | options={ 52 | }, 53 | bases=(models.Model,), 54 | ), 55 | migrations.CreateModel( 56 | name='UserCount', 57 | fields=[ 58 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), 59 | ('dt', models.DateField(help_text='')), 60 | ('counts', djorm_pgarray.fields.ArrayField(blank=True, null=True, default=None, help_text='')), 61 | ('channel', models.ForeignKey(help_text='', to='bots.Channel')), 62 | ], 63 | options={ 64 | }, 65 | bases=(models.Model,), 66 | ), 67 | migrations.AddField( 68 | model_name='channel', 69 | name='chatbot', 70 | field=models.ForeignKey(help_text='', to='bots.ChatBot'), 71 | preserve_default=True, 72 | ), 73 | ] 74 | -------------------------------------------------------------------------------- /botbot/apps/bots/migrations/0002_auto_20150630_1459.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.conf import settings 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('accounts', '0002_auto_20150630_1459'), 12 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 13 | ('plugins', '0002_auto_20140912_1656'), 14 | ('bots', '0001_initial'), 15 | ] 16 | 17 | operations = [ 18 | migrations.CreateModel( 19 | name='PersonalChannels', 20 | fields=[ 21 | ], 22 | options={ 23 | 'verbose_name': 'Pending Personal Channel', 24 | 'proxy': True, 25 | }, 26 | bases=('bots.channel',), 27 | ), 28 | migrations.CreateModel( 29 | name='PublicChannels', 30 | fields=[ 31 | ], 32 | options={ 33 | 'verbose_name': 'Pending Public Channel', 34 | 'proxy': True, 35 | }, 36 | bases=('bots.channel',), 37 | ), 38 | migrations.AddField( 39 | model_name='channel', 40 | name='plugins', 41 | field=models.ManyToManyField(to='plugins.Plugin', through='plugins.ActivePlugin'), 42 | ), 43 | migrations.AddField( 44 | model_name='channel', 45 | name='users', 46 | field=models.ManyToManyField(to=settings.AUTH_USER_MODEL, through='accounts.Membership'), 47 | ), 48 | migrations.AlterUniqueTogether( 49 | name='channel', 50 | unique_together=set([('slug', 'chatbot'), ('name', 'chatbot')]), 51 | ), 52 | ] 53 | -------------------------------------------------------------------------------- /botbot/apps/bots/migrations/0003_remove_channel_users.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('bots', '0002_auto_20150630_1459'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='channel', 16 | name='users', 17 | ), 18 | ] 19 | -------------------------------------------------------------------------------- /botbot/apps/bots/migrations/0004_channel_status.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('bots', '0003_remove_channel_users'), 11 | ] 12 | 13 | operations = [ 14 | migrations.AddField( 15 | model_name='channel', 16 | name='status', 17 | field=models.CharField(default=b'PENDING', max_length=20, choices=[(b'PENDING', b'Pending'), (b'ACTIVE', b'Active'), (b'ARCHIVED', b'Archived')]), 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /botbot/apps/bots/migrations/0005_move_to_status_choices.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | def move_bools_to_status(apps, schema_editor): 7 | Channel = apps.get_model("bots", "Channel") 8 | 9 | for channel in Channel.objects.all(): 10 | if channel.is_active: 11 | channel.status = "ACTIVE" 12 | else: 13 | channel.status = "PENDING" 14 | 15 | channel.save() 16 | 17 | 18 | 19 | class Migration(migrations.Migration): 20 | 21 | dependencies = [ 22 | ('bots', '0004_channel_status'), 23 | ] 24 | 25 | operations = [ 26 | migrations.RunPython(move_bools_to_status), 27 | 28 | ] 29 | -------------------------------------------------------------------------------- /botbot/apps/bots/migrations/0006_auto_20151030_1406.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('bots', '0005_move_to_status_choices'), 11 | ] 12 | 13 | operations = [ 14 | migrations.RemoveField( 15 | model_name='channel', 16 | name='is_active', 17 | ), 18 | migrations.RemoveField( 19 | model_name='channel', 20 | name='is_pending', 21 | ), 22 | ] 23 | -------------------------------------------------------------------------------- /botbot/apps/bots/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/bots/migrations/__init__.py -------------------------------------------------------------------------------- /botbot/apps/bots/sitemaps.py: -------------------------------------------------------------------------------- 1 | """ 2 | Channel sitemap 3 | """ 4 | from django.contrib.sitemaps import Sitemap 5 | from django.utils.timezone import now 6 | 7 | from .models import Channel 8 | 9 | class ChannelSitemap(Sitemap): 10 | 11 | priority = 0.5 12 | 13 | def items(self): 14 | return Channel.objects.public() 15 | 16 | -------------------------------------------------------------------------------- /botbot/apps/bots/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/bots/templatetags/__init__.py -------------------------------------------------------------------------------- /botbot/apps/bots/templatetags/channel_url.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.defaulttags import kwarg_re 3 | from django.utils.encoding import smart_str 4 | 5 | from .. import utils 6 | 7 | register = template.Library() 8 | 9 | 10 | class ChannelURLNode(template.Node): 11 | 12 | def __init__(self, channel, view_name, args, kwargs): 13 | self.channel = channel 14 | self.view_name = view_name 15 | self.args = args 16 | self.kwargs = kwargs 17 | 18 | def render(self, context): 19 | channel = self.channel.resolve(context) 20 | view_name = self.view_name.resolve(context) 21 | 22 | args = [arg.resolve(context) for arg in self.args] 23 | kwargs = dict([(smart_str(k, 'ascii'), v.resolve(context)) 24 | for k, v in self.kwargs.items()]) 25 | 26 | return utils.reverse_channel(channel, view_name, args=args, 27 | kwargs=kwargs, current_app=context.current_app) 28 | 29 | 30 | @register.tag 31 | def channel_url(parser, token): 32 | bits = token.split_contents() 33 | 34 | if len(bits) < 3: 35 | raise template.TemplateSyntaxError("'%s' takes at least one argument" 36 | " (channel and view name)" % bits[0]) 37 | 38 | channel = parser.compile_filter(bits[1]) 39 | viewname = parser.compile_filter(bits[2]) 40 | bits = bits[3:] 41 | 42 | args = [] 43 | kwargs = {} 44 | 45 | if len(bits): 46 | for bit in bits: 47 | match = kwarg_re.match(bit) 48 | if not match: 49 | raise template.TemplateSyntaxError("Malformed arguments to " 50 | "url tag") 51 | name, value = match.groups() 52 | if name: 53 | kwargs[name] = parser.compile_filter(value) 54 | else: 55 | args.append(parser.compile_filter(value)) 56 | 57 | return ChannelURLNode(channel, viewname, args, kwargs) 58 | -------------------------------------------------------------------------------- /botbot/apps/bots/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | import pytz 5 | from django.core import mail 6 | from django.core.urlresolvers import reverse 7 | from django.test import TestCase 8 | 9 | from botbot.apps.accounts import models as account_models 10 | from botbot.apps.bots.models import pretty_slug 11 | from botbot.apps.logs import models as logs_models 12 | from . import models, utils 13 | 14 | 15 | class BaseTestCase(TestCase): 16 | def setUp(self): 17 | self.member = account_models.User.objects.create_user( 18 | username="dupont éîïè", 19 | password="secret", 20 | email="dupont@botbot.local") 21 | self.member.is_superuser = True 22 | self.member.is_staff = True 23 | self.member.save() 24 | self.outsider = account_models.User.objects.create_user( 25 | username="Marie Thérèse", 26 | password="secret", 27 | email="m.therese@botbot.local") 28 | self.chatbot = models.ChatBot.objects.create( 29 | server='testserver', 30 | nick='botbot', 31 | is_active=True) 32 | self.public_channel = models.Channel.objects.create( 33 | chatbot=self.chatbot, 34 | name="#Test", 35 | slug="test", 36 | is_public=True, 37 | status=models.Channel.ACTIVE 38 | ) 39 | logs_models.Log.objects.create( 40 | channel=self.public_channel, 41 | command='PRIVMSG', 42 | timestamp=pytz.utc.localize(datetime.datetime.now())) 43 | self.private_channel = models.Channel.objects.create( 44 | chatbot=self.chatbot, 45 | name="#test-internal", 46 | is_public=False) 47 | 48 | 49 | class UrlTests(BaseTestCase): 50 | 51 | def assertFormError(self, response, form, field, error_str): 52 | """Override for Jinja2 templates""" 53 | self.assertIn(error_str, 54 | response.context_data[form].errors[field]) 55 | 56 | def test_help_channel(self): 57 | url = utils.reverse_channel(self.public_channel, "help_bot") 58 | response = self.client.get(url) 59 | self.assertEqual(response.status_code, 200) 60 | 61 | def test_log_current(self): 62 | url = utils.reverse_channel(self.public_channel, "log_current") 63 | response = self.client.get(url) 64 | self.assertEqual(response.status_code, 200) 65 | 66 | 67 | class PrettySlugTestCase(TestCase): 68 | def test_pretty_slug(self): 69 | original = { 70 | "chat.freenode.net": "freenode", 71 | "morgan.freenode.net": "freenode", 72 | "dickson.freenode.net": "freenode", 73 | "irc.oftc.net": "oftc", 74 | "irc.mozilla.org": "mozilla", 75 | "irc.coldfront.net": "coldfront", 76 | "irc.synirc.net": "synirc", 77 | } 78 | 79 | for server, slug in original.iteritems(): 80 | self.assertEqual(pretty_slug(server), slug) 81 | -------------------------------------------------------------------------------- /botbot/apps/bots/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from . import views 4 | 5 | urlpatterns = patterns('', 6 | url(r'^manage/$', views.ManageChannel.as_view(), name='manage_channel'), 7 | url(r'^delete/$', views.DeleteChannel.as_view(), name='delete_channel'), 8 | ) 9 | -------------------------------------------------------------------------------- /botbot/apps/bots/utils.py: -------------------------------------------------------------------------------- 1 | from django.core.urlresolvers import reverse 2 | 3 | 4 | def channel_url_kwargs(channel): 5 | kwargs = {} 6 | if channel.is_public: 7 | kwargs['bot_slug'] = channel.chatbot.slug 8 | kwargs['channel_slug'] = channel.slug 9 | else: 10 | kwargs['bot_slug'] = 'private' 11 | kwargs['channel_slug'] = channel.private_slug 12 | 13 | return kwargs 14 | 15 | 16 | def reverse_channel(channel, viewname, urlconf=None, args=None, kwargs=None, 17 | *reverse_args, **reverse_kwargs): 18 | """ 19 | Shortcut to make reversing a channel view easier. 20 | """ 21 | kwargs = kwargs or {} 22 | kwargs.update(channel_url_kwargs(channel)) 23 | return reverse(viewname, urlconf, args, kwargs, *reverse_args, 24 | **reverse_kwargs) 25 | -------------------------------------------------------------------------------- /botbot/apps/kudos/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/kudos/__init__.py -------------------------------------------------------------------------------- /botbot/apps/kudos/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/kudos/management/__init__.py -------------------------------------------------------------------------------- /botbot/apps/kudos/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/kudos/management/commands/__init__.py -------------------------------------------------------------------------------- /botbot/apps/kudos/management/commands/kudos.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | 3 | from django.core.management.base import BaseCommand, CommandError 4 | from django.db import transaction 5 | from django.db.models import Max 6 | from botbot.apps.bots.models import Channel 7 | 8 | from botbot.apps.kudos import utils, models 9 | 10 | 11 | class Command(BaseCommand): 12 | option_list = BaseCommand.option_list + ( 13 | make_option( 14 | '--force', '-f', action='store_true', 15 | help='Force full scan of kudos'), 16 | make_option( 17 | '--all', action='store_true', help='All channels'), 18 | ) 19 | 20 | @transaction.atomic 21 | def handle(self, *args, **options): 22 | qs = Channel.objects.all() 23 | if args: 24 | qs = qs.filter(name__in=args) 25 | if (not options.get('all') and not args) or not qs: 26 | raise CommandError('No channels to parse') 27 | verbosity = int(options['verbosity']) 28 | if verbosity > 1: 29 | parse_stdout = self.stdout 30 | else: 31 | parse_stdout = None 32 | for channel in qs: 33 | if verbosity: 34 | self.stdout.write('Processing {}...'.format(channel)) 35 | self.stdout.flush() 36 | qs = channel.log_set.all() 37 | if options['force']: 38 | self.stdout.write('(removing any kudos and doing full check)') 39 | self.stdout.flush() 40 | channel.kudos_set.all().delete() 41 | try: 42 | channel.kudostotal.delete() 43 | except models.KudosTotal.DoesNotExist: 44 | pass 45 | else: 46 | # Look up kudos for this channel, find max(recent), and limit 47 | # queryset to after that. 48 | recent = channel.kudos_set.aggregate(Max('recent')).values()[0] 49 | if recent: 50 | qs = qs.filter(timestamp__gt=recent) 51 | if verbosity: 52 | self.stdout.write('(found existing kudos, updating)') 53 | self.stdout.flush() 54 | else: 55 | if verbosity: 56 | self.stdout.write( 57 | '(no existing kudos found, full check)') 58 | self.stdout.flush() 59 | result = utils.parse_logs(qs, parse_stdout) 60 | if verbosity: 61 | self.stdout.write('Recording results...') 62 | self.stdout.flush() 63 | for person in result['kudos']: 64 | kudos, created = models.Kudos.objects.get_or_create( 65 | nick=person['nick'], 66 | channel=channel, 67 | defaults={ 68 | 'first': person['first'], 69 | 'recent': person['recent'], 70 | 'count': person['count'], 71 | }) 72 | if not created: 73 | # if kudos.recent > person['first']: 74 | # kudos.first = person['first'] 75 | # kudos.count = person['count'] 76 | # else: 77 | kudos.count += person['count'] 78 | kudos.recent = person['recent'] 79 | kudos.save() 80 | kudos_total, created = ( 81 | models.KudosTotal.objects.get_or_create( 82 | channel=channel, 83 | defaults={ 84 | 'kudos_given': result['kudos_given'], 85 | 'message_count': result['message_count'], 86 | })) 87 | if not created: 88 | kudos_total.kudos_given += result['kudos_given'] 89 | kudos_total.message_count += result['message_count'] 90 | kudos_total.save() 91 | if verbosity: 92 | self.stdout.write('Done!') 93 | -------------------------------------------------------------------------------- /botbot/apps/kudos/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | 6 | 7 | class Migration(migrations.Migration): 8 | 9 | dependencies = [ 10 | ('bots', '0001_initial'), 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Kudos', 16 | fields=[ 17 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), 18 | ('nick', models.CharField(max_length=255, help_text='')), 19 | ('count', models.PositiveIntegerField(help_text='')), 20 | ('first', models.DateTimeField(help_text='')), 21 | ('recent', models.DateTimeField(help_text='')), 22 | ('channel', models.ForeignKey(help_text='', to='bots.Channel')), 23 | ], 24 | options={ 25 | 'verbose_name_plural': 'kudos', 26 | }, 27 | bases=(models.Model,), 28 | ), 29 | migrations.CreateModel( 30 | name='KudosTotal', 31 | fields=[ 32 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), 33 | ('kudos_given', models.PositiveIntegerField(help_text='')), 34 | ('message_count', models.PositiveIntegerField(help_text='')), 35 | ('channel', models.OneToOneField(help_text='', to='bots.Channel')), 36 | ], 37 | options={ 38 | }, 39 | bases=(models.Model,), 40 | ), 41 | migrations.AlterUniqueTogether( 42 | name='kudos', 43 | unique_together=set([('nick', 'channel')]), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /botbot/apps/kudos/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/kudos/migrations/__init__.py -------------------------------------------------------------------------------- /botbot/apps/kudos/models.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import models 4 | from django.utils import timezone 5 | from django.utils.encoding import python_2_unicode_compatible 6 | 7 | 8 | class KudosManager(models.Manager): 9 | use_for_related_fields = True 10 | 11 | def ranks(self, debug=False): 12 | """ 13 | Return an ordered list of nicks for the current top ranks. 14 | 15 | The nicks are the first element in a list, the second being the nick's 16 | alltime score. If in debug mode, a dictionary of more information is 17 | also appended. 18 | """ 19 | kudos_set = self.all() 20 | current_scores = reversed(sorted((k.score, k) for k in kudos_set)) 21 | alltime_scores = reversed(sorted((k.count, k.nick) for k in kudos_set)) 22 | alltime_kudos = {} 23 | for i, (count, nick) in enumerate(alltime_scores): 24 | alltime_kudos[nick] = i+1 25 | ranks = [] 26 | for i, (score, k) in enumerate(current_scores): 27 | current = [ 28 | k.nick, 29 | alltime_kudos[k.nick], 30 | ] 31 | if debug: 32 | current.append({ 33 | 'current_rank': i+1, 34 | 'first': k.first.strftime('%d %b %Y'), 35 | 'recent': k.recent.strftime('%d %b %Y'), 36 | 'active_weight': k.active_weight(), 37 | 'kudos_per_day': k.kudos_per_day(), 38 | }) 39 | ranks.append(current) 40 | return ranks 41 | 42 | 43 | @python_2_unicode_compatible 44 | class Kudos(models.Model): 45 | """ 46 | Kudos given to a person (by being thanked by other people). 47 | 48 | Dates are kept of their very first kudos, and their most recent. 49 | """ 50 | nick = models.CharField(max_length=255) 51 | channel = models.ForeignKey('bots.Channel') 52 | count = models.PositiveIntegerField() 53 | first = models.DateTimeField() 54 | recent = models.DateTimeField() 55 | 56 | objects = KudosManager() 57 | 58 | class Meta: 59 | verbose_name_plural = 'kudos' 60 | unique_together = ('nick', 'channel') 61 | 62 | def __str__(self): 63 | return self.nick 64 | 65 | def save(self, *args, **kwargs): 66 | """ 67 | Always lowercase the nick, and set the first/recent dates if required. 68 | """ 69 | self.nick = self.nick.lower() 70 | now = timezone.now() 71 | if not self.first: 72 | self.first = now 73 | if not self.recent: 74 | self.recent = now 75 | return super(Kudos, self).save(*args, **kwargs) 76 | 77 | def active_weight(self, min_days=31, max_days=365, now=None): 78 | now = now or timezone.now() 79 | age = (now - self.recent).days 80 | if age > max_days: 81 | return 0 82 | return min_days / float(max(age, min_days)) 83 | 84 | def kudos_per_day(self, minimum=31): 85 | days = max((self.recent - self.first).days, minimum) 86 | return self.count / float(days) 87 | 88 | @property 89 | def score(self): 90 | return self.kudos_per_day() * self.active_weight() 91 | 92 | 93 | @python_2_unicode_compatible 94 | class KudosTotal(models.Model): 95 | channel = models.OneToOneField('bots.Channel') 96 | kudos_given = models.PositiveIntegerField() 97 | message_count = models.PositiveIntegerField() 98 | 99 | def __str__(self): 100 | return '{} kudos given to {}'.format(self.kudos_given, self.channel) 101 | 102 | @property 103 | def appreciation(self): 104 | if not self.message_count: 105 | return 0 106 | return self.kudos_given / float(self.message_count) 107 | -------------------------------------------------------------------------------- /botbot/apps/kudos/sandbox.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | os.environ['DJANGO_SETTINGS_MODULE'] = 'botbot.settings' 5 | 6 | from botbot.apps.bots.models import Channel 7 | from botbot.apps.kudos import utils 8 | 9 | 10 | django_channel = Channel.objects.filter(name='#django')[0] 11 | result = utils.parse_logs(django_channel.log_set.all(), stdout=sys.stderr) 12 | 13 | print('People thanked') 14 | for person in result['kudos']: 15 | print('{nick} ({count}, {first} - {recent})'.format(**person)) 16 | print( 17 | 'Thank you messages: {kudos_given} ({appreciation:.2%} of all ' 18 | 'messages)'.format( 19 | appreciation=result['kudos_given']/float(result['message_count'] or 1), 20 | **result)) 21 | print('People thanked: {}'.format(len(result['kudos']))) 22 | print('Unattributed: {unattributed}'.format(**result)) 23 | -------------------------------------------------------------------------------- /botbot/apps/kudos/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | import collections 5 | import re 6 | 7 | from django.core.management.base import OutputWrapper 8 | 9 | RE_KUDOS = re.compile( 10 | r'\b(?:' 11 | r'thanks|thank ?you|thx|thnx|thanx|ty|tysm|tyvm|cheers' 12 | r'|danke(:?sch.n)?' 13 | r'|gracias' 14 | r'|merci' 15 | r')\b') 16 | 17 | RE_DIRECTED = re.compile( 18 | ' *(?:({irc_nick_chars}+)[:,]|@({irc_nick_chars}+))'.format( 19 | irc_nick_chars=r'[-a-zA-Z0-9_\\\[\]{}^`|]')) 20 | 21 | 22 | def directed_message(message): 23 | match = RE_DIRECTED.match(message) 24 | if match: 25 | for g in match.groups(): 26 | if g: 27 | return g 28 | 29 | 30 | def _iterate_log(qs, block_size=100000): 31 | """ 32 | Split iteration of the queryset into blocks of 100,000 for increased 33 | performance. 34 | """ 35 | qs = qs.order_by('pk') 36 | last_pk = 0 37 | while last_pk is not None: 38 | block_qs = qs.filter(pk__gt=last_pk)[:block_size] 39 | last_pk = None 40 | for obj in block_qs.iterator(): 41 | yield obj 42 | last_pk = obj[0] 43 | 44 | 45 | def parse_logs(qs, stdout=None): 46 | """ 47 | Parse logs for kudos. 48 | """ 49 | names = collections.deque(maxlen=200) 50 | unattributed = 0 51 | count = 0 52 | kudos = {} 53 | kudos_count = 0 54 | kudos_first = {} 55 | kudos_recent = {} 56 | 57 | if stdout and not isinstance(stdout, OutputWrapper): 58 | stdout = OutputWrapper(stdout) 59 | 60 | def set_thanked(nick): 61 | timestamp = log[3] 62 | kudos[nick] = kudos.get(nick, 0) + 1 63 | kudos_first.setdefault(nick, timestamp) 64 | kudos_recent[nick] = timestamp 65 | 66 | qs = qs.order_by('pk').filter(command='PRIVMSG') 67 | qs = qs.values_list('pk', 'nick', 'text', 'timestamp') 68 | for log in _iterate_log(qs): 69 | log_nick = log[1].lower() 70 | log_text = log[2] 71 | count += 1 72 | directed = directed_message(log_text) 73 | if directed: 74 | directed = directed.lower() 75 | if directed == log_nick: 76 | # Can't thank yourself :P 77 | directed = None 78 | if RE_KUDOS.search(log_text): 79 | kudos_count += 1 80 | attributed = False 81 | if directed: 82 | for nick, _ in names: 83 | if nick == directed: 84 | set_thanked(nick) 85 | attributed = True 86 | break 87 | if not attributed: 88 | lower_text = log_text.lower() 89 | for recent in ( 90 | bits[0] for bits in names if bits[0] != log_nick): 91 | re_text = '(?:^| )@?{}(?:$|\W)'.format(re.escape(recent)) 92 | if re.search(re_text, lower_text): 93 | set_thanked(recent) 94 | attributed = True 95 | if not attributed: 96 | for nick, directed in names: 97 | if directed == log_nick: 98 | set_thanked(nick) 99 | attributed = True 100 | break 101 | if not attributed: 102 | unattributed += 1 103 | names.append((log_nick, directed)) 104 | if stdout and not count % 10000: 105 | stdout.write('.', ending='') 106 | stdout.flush() 107 | if stdout: 108 | stdout.write('') 109 | 110 | kudos_list = [] 111 | for c, nick in sorted((c, nick) for nick, c in kudos.items()): 112 | kudos_list.append({ 113 | 'nick': nick, 114 | 'count': c, 115 | 'first': kudos_first[nick], 116 | 'recent': kudos_recent[nick] 117 | }) 118 | return { 119 | 'kudos': kudos_list, 120 | 'message_count': count, 121 | 'kudos_given': kudos_count, 122 | 'unattributed': unattributed, 123 | } 124 | -------------------------------------------------------------------------------- /botbot/apps/logs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/logs/__init__.py -------------------------------------------------------------------------------- /botbot/apps/logs/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from . import models 4 | from botbot.core.paginator import PostgresLargeTablePaginator 5 | 6 | 7 | class CommandListFilter(admin.SimpleListFilter): 8 | title = "Command" 9 | parameter_name = "command" 10 | 11 | def lookups(self, request, model_admin): 12 | return ( 13 | ('ACTION', 'ACTION'), 14 | ('ERROR', 'ERROR'), 15 | ('JOIN', 'JOIN'), 16 | ('KICK', 'KICK'), 17 | ('MODE', 'MODE'), 18 | ('NICK', 'NICK'), 19 | ('NOTICE', 'NOTICE'), 20 | ('PART', 'PART'), 21 | ('PING', 'PING'), 22 | ('PRIVMSG', 'PRIVMSG'), 23 | ('QUIT', 'QUIT'), 24 | ('SHUTDOWN', 'SHUTDOWN'), 25 | ('TOPIC', 'TOPIC'), 26 | ('VERSION', 'VERSION'), 27 | ) 28 | 29 | def queryset(self, request, queryset): 30 | return queryset.filter(command=self.value()) 31 | 32 | 33 | class LogAdmin(admin.ModelAdmin): 34 | list_display = ['__unicode__', 'command', 'bot', 'timestamp'] 35 | list_filter = ['bot', CommandListFilter] 36 | paginator = PostgresLargeTablePaginator 37 | 38 | 39 | admin.site.register(models.Log, LogAdmin) 40 | -------------------------------------------------------------------------------- /botbot/apps/logs/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | 3 | 4 | class SearchForm(forms.Form): 5 | q = forms.CharField(required=False, label="search", 6 | widget=forms.TextInput(attrs={'placeholder': 'Search'})) 7 | -------------------------------------------------------------------------------- /botbot/apps/logs/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/logs/management/__init__.py -------------------------------------------------------------------------------- /botbot/apps/logs/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/logs/management/commands/__init__.py -------------------------------------------------------------------------------- /botbot/apps/logs/management/commands/redact.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | 3 | from django.core.management.base import BaseCommand 4 | 5 | from botbot.apps.logs import models 6 | 7 | def _redact_logs_for_nick(nick): 8 | redacted_count = models.Log.objects.filter(nick=nick).update( 9 | text=models.REDACTED_TEXT) 10 | return redacted_count 11 | 12 | 13 | class Command(BaseCommand): 14 | args = "" 15 | help = "Redact logs for the given nick" 16 | 17 | def handle(self, *args, **options): 18 | if len(args) != 1: 19 | self.stderr.write( 20 | "One argument (the nick to be redacted) is required.") 21 | nick = args[0] 22 | self.stdout.write("Redacting logs for '{0}'".format(nick)) 23 | count = _redact_logs_for_nick(nick) 24 | self.stdout.write("{0} log lines redacted".format(count)) 25 | -------------------------------------------------------------------------------- /botbot/apps/logs/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import djorm_pgfulltext.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('bots', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='Log', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), 19 | ('timestamp', models.DateTimeField(db_index=True, help_text='')), 20 | ('nick', models.CharField(max_length=255, help_text='')), 21 | ('text', models.TextField(help_text='')), 22 | ('action', models.BooleanField(default=False, help_text='')), 23 | ('command', models.CharField(max_length=50, blank=True, null=True, help_text='')), 24 | ('host', models.TextField(blank=True, null=True, help_text='')), 25 | ('raw', models.TextField(blank=True, null=True, help_text='')), 26 | ('room', models.CharField(max_length=100, blank=True, null=True, help_text='')), 27 | ('search_index', djorm_pgfulltext.fields.VectorField(null=True, db_index=True, default=b'', editable=False, serialize=False, help_text='')), 28 | ('bot', models.ForeignKey(null=True, help_text='', to='bots.ChatBot')), 29 | ('channel', models.ForeignKey(null=True, help_text='', to='bots.Channel')), 30 | ], 31 | options={ 32 | 'ordering': ('-timestamp',), 33 | }, 34 | bases=(models.Model,), 35 | ), 36 | migrations.AlterIndexTogether( 37 | name='log', 38 | index_together=set([('channel', 'timestamp')]), 39 | ), 40 | ] 41 | -------------------------------------------------------------------------------- /botbot/apps/logs/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/logs/migrations/__init__.py -------------------------------------------------------------------------------- /botbot/apps/logs/models.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | from djorm_pgfulltext.models import SearchManager 4 | from djorm_pgfulltext.fields import VectorField 5 | from django.db import models 6 | from django.conf import settings 7 | from django.template.loader import render_to_string 8 | from django.core.urlresolvers import reverse 9 | from botbot.apps.bots.utils import channel_url_kwargs 10 | 11 | 12 | from . import utils 13 | 14 | REDACTED_TEXT = '[redacted]' 15 | 16 | MSG_TMPL = { 17 | u"JOIN": u"{nick} joined the channel", 18 | u"NICK": u"{nick} is now known as {text}", 19 | u"QUIT": u"{nick} has quit", 20 | u"PART": u"{nick} has left the channel", 21 | u"ACTION": u"{nick} {text}", 22 | u"SHUTDOWN": u"-- BotBot disconnected, possible missing messages --", 23 | } 24 | 25 | 26 | class Log(models.Model): 27 | bot = models.ForeignKey('bots.ChatBot', null=True) 28 | channel = models.ForeignKey('bots.Channel', null=True) 29 | timestamp = models.DateTimeField(db_index=True) 30 | nick = models.CharField(max_length=255) 31 | text = models.TextField() 32 | action = models.BooleanField(default=False) 33 | 34 | command = models.CharField(max_length=50, null=True, blank=True) 35 | host = models.TextField(null=True, blank=True) 36 | raw = models.TextField(null=True, blank=True) 37 | 38 | # freenode chan name length limit is 50 chars, Campfire room ids are ints, 39 | # so 100 should be enough 40 | room = models.CharField(max_length=100, null=True, blank=True) 41 | 42 | search_index = VectorField() 43 | 44 | objects = SearchManager( 45 | fields=('text',), 46 | config='pg_catalog.english', # this is default 47 | search_field='search_index', # this is default 48 | auto_update_search_field=True 49 | ) 50 | 51 | class Meta: 52 | ordering = ('-timestamp',) 53 | index_together = [ 54 | ['channel', 'timestamp'], 55 | ] 56 | 57 | def get_absolute_url(self): 58 | kwargs = channel_url_kwargs(self.channel) 59 | kwargs['msg_pk'] = self.pk 60 | 61 | return reverse('log_message_permalink', kwargs=kwargs) 62 | 63 | def as_html(self): 64 | return render_to_string("logs/log_display.html", 65 | {'message_list': [self]}) 66 | def get_cleaned_host(self): 67 | if self.host: 68 | if '@' in self.host: 69 | return self.host.split('@')[1] 70 | else: 71 | return self.host 72 | 73 | 74 | def notify(self): 75 | """Send update to Nginx to be sent out via SSE""" 76 | utils.send_event_with_id( 77 | "log", 78 | self.as_html(), 79 | self.timestamp.isoformat(), 80 | self.get_cleaned_host(), 81 | channel=self.channel_id) 82 | 83 | def get_nick_color(self): 84 | return hash(self.nick) % 32 85 | 86 | def __unicode__(self): 87 | if self.command == u"PRIVMSG": 88 | text = u'' 89 | if self.nick: 90 | text += u'{0}: '.format(self.nick) 91 | text += self.text[:20] 92 | else: 93 | try: 94 | text = MSG_TMPL[self.command].format(nick=self.nick, text=self.text) 95 | except KeyError: 96 | text = u"{}: {}".format(self.command, self.text) 97 | 98 | return text 99 | 100 | def save(self, *args, **kwargs): 101 | is_new = False 102 | if not self.pk: 103 | is_new = True 104 | if self.nick in settings.EXCLUDE_NICKS: 105 | self.text = REDACTED_TEXT 106 | 107 | obj = super(Log, self).save(*args, **kwargs) 108 | if is_new: 109 | self.notify() 110 | return obj 111 | -------------------------------------------------------------------------------- /botbot/apps/logs/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/logs/templatetags/__init__.py -------------------------------------------------------------------------------- /botbot/apps/logs/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, url 2 | 3 | from . import views 4 | 5 | urlpatterns = patterns('', 6 | url(r'(?P\d{4})-(?P0[1-9]|1[0-2])-(?P0[1-9]|[1-2][0-9]|3[0-1])/$', 7 | views.DayLogViewer.as_view(), name="log_day"), 8 | url(r'(?P\d{4})-(?P0[1-9]|1[0-2])-(?P0[1-9]|[1-2][0-9]|3[0-1]).log$', 9 | views.DayLogViewer.as_view(format='text'), name="log_day_text"), 10 | url(r'^missed/(?P[\w\-\|]*)/$', views.MissedLogViewer.as_view(), 11 | name="log_missed"), 12 | url(r'^msg/(?P\d+)/$', views.SingleLogViewer.as_view(), 13 | name="log_message_permalink"), 14 | # url(r'^search/$', views.SearchLogViewer.as_view(), name='log_search'), 15 | url(r'^kudos.json$', views.Kudos.as_view(), name='kudos_json'), 16 | url(r'^kudos/$', views.ChannelKudos.as_view(), name='kudos'), 17 | url(r'^help/$', views.Help.as_view(), name='help_bot'), 18 | url(r'^stream/$', views.LogStream.as_view(), name='log_stream'), 19 | url(r'^$', views.DayLogViewer.as_view(), 20 | name="log_current"), 21 | ) 22 | -------------------------------------------------------------------------------- /botbot/apps/logs/utils.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | from django.conf import settings 5 | from django.core.cache import cache 6 | import requests 7 | import geoip2.database, geoip2.errors 8 | 9 | LOG = logging.getLogger(__name__) 10 | 11 | try: 12 | GEOIP = geoip2.database.Reader(settings.GEOIP_CITY_DB_PATH) 13 | except: 14 | LOG.warn('Could not open GEOIP database. Map stream is disabled. ' 15 | 'Try `make geoip-db` to download a copy.') 16 | GEOIP = None 17 | 18 | 19 | def ip_lookup(ip): 20 | cache_key = 'location:{}'.format(ip) 21 | coords = cache.get(cache_key) 22 | if not coords: 23 | try: 24 | location = GEOIP.city(ip).location 25 | coords = (location.latitude, location.longitude) 26 | except (geoip2.errors.AddressNotFoundError, ValueError): 27 | coords = (0, 0) 28 | cache.set(cache_key, coords) 29 | return coords 30 | 31 | 32 | def _send_event_with_id(event_name, data, event_id, ip, channel): 33 | """HTTP POST to Nginx which manages the Server-Sent Events""" 34 | requests.post(settings.PUSH_STREAM_URL.format(id=channel), 35 | headers={'Event-Id': event_id, 'Event-Type': event_name}, 36 | data=data.encode('utf-8')) 37 | if GEOIP: 38 | requests.post(settings.PUSH_STREAM_URL.format(id='glob'), 39 | headers={'Event-Id': event_id, 'Event-Type': 'loc'}, 40 | data=json.dumps(ip_lookup(ip))) 41 | 42 | if settings.PUSH_STREAM_URL: 43 | send_event_with_id = _send_event_with_id 44 | else: 45 | LOG.info('PUSH_STREAM_URL setting not defined. Realtime updates disabled.') 46 | send_event_with_id = lambda *a,**kw: None 47 | 48 | -------------------------------------------------------------------------------- /botbot/apps/plugins/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/plugins/__init__.py -------------------------------------------------------------------------------- /botbot/apps/plugins/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from . import models 3 | 4 | class ActivePluginAdmin(admin.ModelAdmin): 5 | list_display = ('__unicode__', 'configuration') 6 | list_filter = ('channel', 'plugin') 7 | list_editable = ('configuration',) 8 | 9 | admin.site.register(models.Plugin) 10 | admin.site.register(models.ActivePlugin, ActivePluginAdmin) 11 | -------------------------------------------------------------------------------- /botbot/apps/plugins/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/plugins/core/__init__.py -------------------------------------------------------------------------------- /botbot/apps/plugins/core/help.py: -------------------------------------------------------------------------------- 1 | # pylint: disable=W0212 2 | from botbot.apps.bots.utils import reverse_channel 3 | from botbot_plugins.base import BasePlugin 4 | from botbot_plugins.decorators import listens_to_mentions 5 | 6 | 7 | SITE = "https://botbot.me" 8 | 9 | 10 | class Plugin(BasePlugin): 11 | """ 12 | Shows available plugins and descriptions. 13 | 14 | Simply ask for help and I'll gladly tell you what I'm capable of: 15 | 16 | {{ nick }}: help 17 | 18 | For further details, you can ask me about a specific plugin: 19 | 20 | {{ nick }}: help images 21 | """ 22 | @listens_to_mentions(ur'^help$') 23 | def respond_to_help(self, line): 24 | plugins = [plgn.slug for plgn in line._channel.plugins.all()] 25 | help_url = get_help_url(line._channel) 26 | return u'Available plugins: {0} ({1})'.format(', '.join(plugins), 27 | help_url) 28 | 29 | @listens_to_mentions(ur'^help (?P.*)') 30 | def respond_to_plugin_help(self, line, command): 31 | """Returns first line of docstring and link to more""" 32 | slug = command.strip() 33 | try: 34 | plugin = line._channel.plugins.filter(slug=slug)[0] 35 | help_url = get_help_url(line._channel) 36 | response = [ 37 | plugin.user_docs.strip().split('\n')[0], 38 | 'More details: {0}#{1}'.format(help_url, command) 39 | ] 40 | return '\n'.join(response) 41 | except IndexError: 42 | return 'Sorry, that plugin is not available.' 43 | 44 | 45 | def get_help_url(channel): 46 | return SITE + reverse_channel(channel, "help_bot") 47 | -------------------------------------------------------------------------------- /botbot/apps/plugins/core/logger.py: -------------------------------------------------------------------------------- 1 | from botbot.apps.logs.models import Log 2 | from botbot.apps.plugins.utils import convert_nano_timestamp 3 | from botbot_plugins.base import BasePlugin 4 | import botbot_plugins.config as config 5 | 6 | class Config(config.BaseConfig): 7 | ignore_prefix = config.Field( 8 | default="!-", 9 | required=False, 10 | help_text="Don't log lines starting with this string" 11 | ) 12 | 13 | class Plugin(BasePlugin): 14 | """ 15 | Logs all activity. 16 | 17 | I keep extensive logs on all the activity in `{{ channel.name }}`. 18 | You can read and search them at {{ SITE }}{{ channel.get_absolute_url }}. 19 | """ 20 | config_class = Config 21 | 22 | def logit(self, line): 23 | """Log a message to the database""" 24 | # If the channel does not start with "#" that means the message 25 | # is part of a /query 26 | if line._channel_name.startswith("#"): 27 | ignore_prefix = self.config['ignore_prefix'] 28 | 29 | # Delete ACTION prefix created by /me 30 | text = line.text 31 | if text.startswith("ACTION "): 32 | text = text[7:] 33 | 34 | if not (ignore_prefix and text.startswith(ignore_prefix)): 35 | Log.objects.create( 36 | channel_id=line._channel.pk, 37 | timestamp=line._received, 38 | nick=line.user, 39 | text=line.full_text, 40 | room=line._channel, 41 | host=line._host, 42 | command=line._command, 43 | raw=line._raw) 44 | 45 | logit.route_rule = ('firehose', ur'(.*)') 46 | -------------------------------------------------------------------------------- /botbot/apps/plugins/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from . import models 3 | 4 | class PluginsForm(forms.Form): 5 | plugins = forms.ModelMultipleChoiceField(required=False, 6 | queryset=models.Plugin.objects.all(), 7 | widget=forms.CheckboxSelectMultiple()) 8 | 9 | def __init__(self, channel, *args, **kwargs): 10 | super(PluginsForm, self).__init__(*args, **kwargs) 11 | self.channel = channel 12 | self.fields['plugins'].initial = [p.pk for p in channel.plugins.all()] 13 | 14 | def save(self): 15 | self.channel.plugins.clear() 16 | for plugin in self.cleaned_data['plugins']: 17 | models.ActivePlugin.objects.create(plugin=plugin, 18 | channel=self.channel) 19 | -------------------------------------------------------------------------------- /botbot/apps/plugins/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/plugins/management/__init__.py -------------------------------------------------------------------------------- /botbot/apps/plugins/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/plugins/management/commands/__init__.py -------------------------------------------------------------------------------- /botbot/apps/plugins/management/commands/run_plugins.py: -------------------------------------------------------------------------------- 1 | from optparse import make_option 2 | 3 | from django.core.management.base import NoArgsCommand 4 | 5 | from botbot.apps.plugins import runner 6 | 7 | class Command(NoArgsCommand): 8 | 9 | help = ("Starts up all plugins in the botbot.apps.bots.plugins module") 10 | option_list = NoArgsCommand.option_list + ( 11 | make_option('--with-gevent', 12 | action='store_true', 13 | dest='with_gevent', 14 | default=False, 15 | help='Use gevent for concurrency'), 16 | ) 17 | def handle_noargs(self, **options): 18 | runner.start_plugins(use_gevent=options['with_gevent']) 19 | -------------------------------------------------------------------------------- /botbot/apps/plugins/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | import botbot.core.fields 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('bots', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.CreateModel( 16 | name='ActivePlugin', 17 | fields=[ 18 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), 19 | ('configuration', botbot.core.fields.JSONField(blank=True, default={}, help_text=b'User-specified attributes for this plugin {"username": "joe", "api-key": "foo"}')), 20 | ('channel', models.ForeignKey(help_text='', to='bots.Channel')), 21 | ], 22 | options={ 23 | }, 24 | bases=(models.Model,), 25 | ), 26 | migrations.CreateModel( 27 | name='Plugin', 28 | fields=[ 29 | ('id', models.AutoField(verbose_name='ID', primary_key=True, serialize=False, help_text='', auto_created=True)), 30 | ('name', models.CharField(max_length=100, help_text='')), 31 | ('slug', models.SlugField(help_text='')), 32 | ], 33 | options={ 34 | }, 35 | bases=(models.Model,), 36 | ), 37 | migrations.AddField( 38 | model_name='activeplugin', 39 | name='plugin', 40 | field=models.ForeignKey(help_text='', to='plugins.Plugin'), 41 | preserve_default=True, 42 | ), 43 | ] 44 | -------------------------------------------------------------------------------- /botbot/apps/plugins/migrations/0002_auto_20140912_1656.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import models, migrations 5 | from django.db.models.loading import get_model 6 | 7 | 8 | def initial_plugins(*args): 9 | Plugin = get_model('plugins', 'Plugin') 10 | 11 | intial = [ 12 | { 13 | "name": "Logger", 14 | "slug": "logger" 15 | }, 16 | { 17 | "name": "Ping", 18 | "slug": "ping" 19 | }, 20 | { 21 | "name": "Wolfram|Alpha", 22 | "slug": "wolfram" 23 | }, 24 | { 25 | "name": "Help", 26 | "slug": "help" 27 | }, 28 | { 29 | "name": "Images", 30 | "slug": "images" 31 | }, 32 | { 33 | "name": "Memory", 34 | "slug": "brain" 35 | }, 36 | { 37 | "name": "Last Seen", 38 | "slug": "last_seen" 39 | }, 40 | { 41 | "name": "GitHub", 42 | "slug": "github" 43 | }, 44 | { 45 | "name": "!m", 46 | "slug": "bangmotivate" 47 | } 48 | ] 49 | 50 | for i in intial: 51 | Plugin(**i).save() 52 | 53 | 54 | class Migration(migrations.Migration): 55 | dependencies = [ 56 | ('plugins', '0001_initial'), 57 | ] 58 | 59 | operations = [ 60 | migrations.RunPython(initial_plugins) 61 | ] 62 | -------------------------------------------------------------------------------- /botbot/apps/plugins/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/plugins/migrations/__init__.py -------------------------------------------------------------------------------- /botbot/apps/plugins/models.py: -------------------------------------------------------------------------------- 1 | from django.core.cache import cache 2 | from django.contrib.admindocs.utils import trim_docstring 3 | from django.db import models 4 | from django.utils.importlib import import_module 5 | 6 | from botbot.core.fields import JSONField 7 | 8 | 9 | class Plugin(models.Model): 10 | """A global plugin registered in botbot""" 11 | name = models.CharField(max_length=100) 12 | slug = models.SlugField() 13 | 14 | @property 15 | def user_docs(self): 16 | for mod_prefix in ('botbot_plugins.plugins.', 17 | 'botbot.apps.plugins.core.'): 18 | try: 19 | docs = import_module(mod_prefix + self.slug).Plugin.__doc__ 20 | return trim_docstring(docs) 21 | except (ImportError, AttributeError): 22 | continue 23 | return '' 24 | 25 | def __unicode__(self): 26 | return self.name 27 | 28 | 29 | class ActivePlugin(models.Model): 30 | """An active plugin for a ChatBot""" 31 | plugin = models.ForeignKey('plugins.Plugin') 32 | channel = models.ForeignKey('bots.Channel') 33 | configuration = JSONField( 34 | blank=True, default={}, 35 | help_text="User-specified attributes for this plugin " + 36 | '{"username": "joe", "api-key": "foo"}') 37 | 38 | def save(self, *args, **kwargs): 39 | obj = super(ActivePlugin, self).save(*args, **kwargs) 40 | # Let the plugin_runner auto-reload the new values 41 | cache.delete(self.channel.plugin_config_cache_key(self.plugin.slug)) 42 | cache.delete(self.channel.active_plugin_slugs_cache_key) 43 | return obj 44 | 45 | def __unicode__(self): 46 | return u'{0} for {1}'.format(self.plugin.name, self.channel.name) 47 | -------------------------------------------------------------------------------- /botbot/apps/plugins/plugin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from botbot_plugins.base import PrivateMessage 3 | 4 | LOG = logging.getLogger('botbot.plugin_runner') 5 | 6 | 7 | class RealPluginMixin(object): 8 | """ 9 | All the things that need to get added to botbot-plugins 10 | fake Plugin class to make it work with the bot and web. 11 | """ 12 | 13 | def __init__(self, slug, channel, chatbot_id, app): 14 | self.slug = slug 15 | self.channel_id = channel.pk 16 | self.chatbot_id = chatbot_id 17 | self.app = app 18 | self.channel_name = channel.name 19 | # Configuration variables as a dictionary, from database 20 | if self.config_class: 21 | self.prod_config = self.config_class().fields 22 | plugin_config = channel.plugin_config(self.slug) 23 | self.prod_config.update(plugin_config) 24 | 25 | def unique_key(self, key): 26 | """A unique key for the chatbot, channel, plugin, key combination""" 27 | return u'{0}:{1}:{2}:{3}'.format(self.chatbot_id, self.channel_id, 28 | self.slug, key.strip()) 29 | 30 | def store(self, key, value): 31 | """Saves a key,value to Redis""" 32 | ukey = self.unique_key(key) 33 | LOG.info('Storing: %s=%s', ukey, value) 34 | self.app.storage.set(ukey, value) 35 | 36 | def retrieve(self, key): 37 | """Retrieves the value for a key from Redis""" 38 | ukey = self.unique_key(key) 39 | value = self.app.storage.get(ukey) 40 | if value: 41 | value = unicode(value, 'utf-8') 42 | LOG.info('Retrieved: %s=%s', key, value) 43 | return value 44 | 45 | def delete(self, key): 46 | """ Delete the value from Redis""" 47 | ukey = self.unique_key(key) 48 | return self.app.storage.delete(ukey) == 1 49 | 50 | def greenlet_respond(self, grnlt): 51 | """Callback for gevent return values""" 52 | msg = grnlt.value 53 | self.respond(msg) 54 | 55 | def respond(self, msg): 56 | """Writes message back to the channel the line was received on""" 57 | # Internal method, not part of public API 58 | if msg: 59 | nick = self.channel_name 60 | if isinstance(msg, PrivateMessage): 61 | lines= msg.msg.split('\n') 62 | nick = msg.nick 63 | else: 64 | lines = msg.split('\n') 65 | for response_line in lines: 66 | LOG.info('Write to %s: %s', nick, response_line) 67 | response_cmd = u'WRITE {0} {1} {2}'.format(self.chatbot_id, 68 | nick, 69 | response_line) 70 | self.app.bot_bus.lpush('bot', response_cmd) 71 | -------------------------------------------------------------------------------- /botbot/apps/plugins/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/plugins/templatetags/__init__.py -------------------------------------------------------------------------------- /botbot/apps/plugins/templatetags/plugin_docs.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | from .. import utils 4 | 5 | register = template.Library() 6 | 7 | 8 | class PluginDocsNode(template.Node): 9 | 10 | def __init__(self, plugin, channel): 11 | self.plugin = plugin 12 | self.channel = channel 13 | 14 | def render(self, context): 15 | plugin = self.plugin.resolve(context) 16 | channel = self.channel.resolve(context) 17 | return utils.plugin_docs_as_html(plugin, channel) 18 | 19 | 20 | @register.tag 21 | def plugin_docs(parser, token): 22 | bits = token.split_contents() 23 | 24 | if len(bits) < 3: 25 | raise template.TemplateSyntaxError("'%s' takes two arguments" 26 | " (plugin and channel)" % bits[0]) 27 | 28 | plugin = parser.compile_filter(bits[1]) 29 | channel = parser.compile_filter(bits[2]) 30 | 31 | return PluginDocsNode(plugin, channel) -------------------------------------------------------------------------------- /botbot/apps/plugins/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | 4 | from django.utils.timezone import utc 5 | from django.test import TestCase 6 | from . import utils 7 | 8 | 9 | class UtilsTestCase(TestCase): 10 | def test_nano_timestamp(self): 11 | timestamp = '2014-01-27T16:35:53.123456789Z' 12 | py_date = utils.convert_nano_timestamp(timestamp) 13 | self.assertEqual(py_date, 14 | datetime.datetime(2014, 1, 27, 16, 35, 15 | 53, 123456, tzinfo=utc)) 16 | 17 | def test_short_nano_timestamp(self): 18 | timestamp = '2014-01-27T16:35:53.1234Z' 19 | py_date = utils.convert_nano_timestamp(timestamp) 20 | self.assertEqual(py_date, 21 | datetime.datetime(2014, 1, 27, 16, 35, 22 | 53, 123400, tzinfo=utc)) -------------------------------------------------------------------------------- /botbot/apps/plugins/utils.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | from functools import wraps 3 | 4 | from django.template import Template, Context 5 | from django.template.defaultfilters import urlize 6 | from django.utils.timezone import utc 7 | 8 | import markdown 9 | 10 | def plugin_docs_as_html(plugin, channel): 11 | tmpl = Template(plugin.user_docs) 12 | ctxt = Context({ 13 | 'nick': channel.chatbot.nick, 14 | 'channel': channel, 15 | 'SITE': 'https://botbot.me', 16 | }) 17 | return markdown.markdown(urlize(tmpl.render(ctxt))) 18 | 19 | def convert_nano_timestamp(nano_timestamp): 20 | """ 21 | Takes a time string created by the bot (in Go using nanoseconds) 22 | and makes it a Python datetime using microseconds 23 | """ 24 | # convert nanoseconds to microseconds 25 | # http://stackoverflow.com/a/10612166/116042 26 | rfc3339, nano_part = nano_timestamp.split('.') 27 | micro = nano_part[:-1] # strip trailing "Z" 28 | if len(nano_part) > 6: # trim to max size Python allows 29 | micro = micro[:6] 30 | rfc3339micro = ''.join([rfc3339, '.', micro, 'Z']) 31 | micro_timestamp = datetime.datetime.strptime( 32 | rfc3339micro, '%Y-%m-%dT%H:%M:%S.%fZ').replace(tzinfo=utc) 33 | return micro_timestamp 34 | 35 | 36 | def log_on_error(Log, method): 37 | @wraps(method) 38 | def wrap(*args, **kwargs): 39 | try: 40 | return method(*args, **kwargs) 41 | except Exception: 42 | Log.error("Plugin failed [%s]", method.__name__, exc_info=True) 43 | return wrap 44 | -------------------------------------------------------------------------------- /botbot/apps/preview/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/preview/__init__.py -------------------------------------------------------------------------------- /botbot/apps/preview/views.py: -------------------------------------------------------------------------------- 1 | from launchpad.views import Signup 2 | 3 | from botbot.apps.bots import models as bots_models 4 | 5 | 6 | class LandingPage(Signup): 7 | def get_context_data(self, **kwargs): 8 | kwargs.update({ 9 | 'featured_channels': bots_models.Channel.objects \ 10 | .filter(is_public=True, is_featured=True).active() \ 11 | .select_related('chatbot'), 12 | 'public_not_featured_channels': bots_models.Channel.objects \ 13 | .filter(is_public=True, is_featured=False).active() \ 14 | .select_related('chatbot') 15 | }) 16 | return kwargs 17 | -------------------------------------------------------------------------------- /botbot/apps/sitemap/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/apps/sitemap/__init__.py -------------------------------------------------------------------------------- /botbot/apps/sitemap/urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | Site map URLs 3 | """ 4 | from django.conf.urls import patterns, url 5 | from django.contrib import sitemaps 6 | from django.contrib.sitemaps.views import sitemap 7 | from django.core.urlresolvers import reverse 8 | from django.views.decorators.cache import cache_page 9 | 10 | from botbot.apps.bots.sitemaps import ChannelSitemap 11 | 12 | 13 | class StaticSitemap(sitemaps.Sitemap): 14 | priority = 0.5 15 | changefreq = 'monthly' 16 | 17 | def items(self): 18 | return ['terms', 'privacy', 'how-to', 'request_channel'] 19 | 20 | def location(self, item): 21 | return reverse(item) 22 | 23 | 24 | 25 | sitemaps = { 26 | 'channels': ChannelSitemap, 27 | 'static': StaticSitemap, 28 | } 29 | 30 | urlpatterns = patterns('', 31 | url(r'^$', cache_page(86400)(sitemap), {'sitemaps': sitemaps}, 32 | name='sitemap'), 33 | ) 34 | 35 | 36 | -------------------------------------------------------------------------------- /botbot/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/core/__init__.py -------------------------------------------------------------------------------- /botbot/core/fields.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.db import models 4 | from django.core.serializers.json import DjangoJSONEncoder 5 | 6 | from django.forms import fields 7 | from django.forms.util import ValidationError 8 | 9 | 10 | 11 | class JSONField(models.TextField): 12 | """JSONField is a generic textfield that neatly serializes/unserializes 13 | JSON objects seamlessly""" 14 | 15 | # Used so to_python() is called 16 | __metaclass__ = models.SubfieldBase 17 | 18 | def to_python(self, value): 19 | """Convert our string value to JSON after we load it from the DB""" 20 | if value == "": 21 | return None 22 | try: 23 | if isinstance(value, basestring): 24 | return json.loads(value) 25 | except ValueError: 26 | pass 27 | return value 28 | 29 | def get_db_prep_save(self, value, *args, **kwargs): 30 | if value == "": 31 | return None 32 | 33 | if value == "{}": 34 | value = {} 35 | 36 | if isinstance(value, dict) or isinstance(value, list): 37 | value = json.dumps(value, cls=DjangoJSONEncoder) 38 | return super(JSONField, self).get_db_prep_save(value, *args, **kwargs) 39 | 40 | def value_from_object(self, obj): 41 | value = super(JSONField, self).value_from_object(obj) 42 | if self.null and value is None: 43 | return None 44 | return json.dumps(value) 45 | 46 | try: 47 | from south.modelsinspector import add_introspection_rules 48 | add_introspection_rules([], ["^botbot\.core\.fields\.JSONField"]) 49 | except ImportError: 50 | pass 51 | -------------------------------------------------------------------------------- /botbot/core/middleware.py: -------------------------------------------------------------------------------- 1 | from django.utils import timezone 2 | 3 | class TimezoneMiddleware(object): 4 | def process_request(self, request): 5 | tz = request.session.get('django_timezone', "UTC") or "UTC" 6 | timezone.activate(tz) 7 | -------------------------------------------------------------------------------- /botbot/core/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | 4 | class TimeStampedModel(models.Model): 5 | created = models.DateTimeField(auto_now_add=True) 6 | updated = models.DateTimeField(auto_now=True) 7 | 8 | class Meta: 9 | abstract = True -------------------------------------------------------------------------------- /botbot/core/templatetags/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/core/templatetags/__init__.py -------------------------------------------------------------------------------- /botbot/core/templatetags/verbatim.py: -------------------------------------------------------------------------------- 1 | """ 2 | Templates use constructs like: 3 | 4 | {{if condition}} print something{{/if}} 5 | 6 | This, of course, completely screws up Django templates, 7 | because Django thinks {{ and }} mean something. 8 | 9 | Wrap {% verbatim %} and {% endverbatim %} around those 10 | blocks of jQuery templates and this will try its best 11 | to output the contents with no changes. 12 | """ 13 | 14 | from django import template 15 | 16 | register = template.Library() 17 | 18 | 19 | class VerbatimNode(template.Node): 20 | 21 | def __init__(self, text): 22 | self.text = text 23 | 24 | def render(self, context): 25 | return self.text 26 | 27 | 28 | @register.tag 29 | def verbatim(parser, token): 30 | text = [] 31 | while 1: 32 | token = parser.tokens.pop(0) 33 | if token.contents == 'endverbatim': 34 | break 35 | if token.token_type == template.TOKEN_VAR: 36 | text.append('{{') 37 | elif token.token_type == template.TOKEN_BLOCK: 38 | text.append('{%') 39 | text.append(token.contents) 40 | if token.token_type == template.TOKEN_VAR: 41 | text.append('}}') 42 | elif token.token_type == template.TOKEN_BLOCK: 43 | text.append('%}') 44 | return VerbatimNode(''.join(text)) 45 | -------------------------------------------------------------------------------- /botbot/core/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Testing InfinitePaginator 3 | """ 4 | from .paginator import InfinitePaginator 5 | from django.test import TestCase 6 | 7 | 8 | class TestInfinitePaginator(TestCase): 9 | 10 | def setUp(self): 11 | self.p = InfinitePaginator(range(20), 2, 12 | link_template='/bacon/page/%d') 13 | 14 | def test_validate_number(self): 15 | self.assertEqual(self.p.validate_number(2), 2) 16 | 17 | def test_orphans(self): 18 | self.assertEqual(self.p.orphans, 0) 19 | 20 | def test_page(self): 21 | p3 = self.p.page(3) 22 | self.assertEqual(str(p3), "") 23 | self.assertEqual(p3.end_index(), 6) 24 | self.assertEqual(p3.has_next(), True) 25 | self.assertEqual(p3.has_previous(), True) 26 | self.assertEqual(self.p.page(10).has_next(), False) 27 | self.assertEqual(self.p.page(1).has_previous(), False) 28 | self.assertEqual(p3.next_link(), '/bacon/page/4') 29 | self.assertEqual(p3.previous_link(), '/bacon/page/2') 30 | -------------------------------------------------------------------------------- /botbot/jinja2.py: -------------------------------------------------------------------------------- 1 | from __future__ import absolute_import # Python 2 only 2 | 3 | from django.contrib.staticfiles.storage import staticfiles_storage 4 | from django.core.urlresolvers import reverse 5 | from django.utils.timezone import now 6 | from django.utils.translation import ugettext 7 | 8 | from jinja2 import Environment 9 | 10 | from .apps.bots.utils import reverse_channel 11 | from .apps.logs.templatetags.logs_tags import bbme_urlizetrunc 12 | from .apps.plugins.utils import plugin_docs_as_html 13 | 14 | from allauth.account.utils import user_display 15 | from allauth.socialaccount import providers 16 | from bootstrap_toolkit.templatetags.bootstrap_toolkit import bootstrap_input_type 17 | 18 | 19 | def environment(**options): 20 | options['extensions'] = [ 21 | "jinja2.ext.autoescape", 22 | "jinja2.ext.with_", 23 | "jinja2.ext.i18n", 24 | 'pipeline.templatetags.ext.PipelineExtension', 25 | 'django_jinja.builtins.extensions.CacheExtension', 26 | ] 27 | env = Environment(**options) 28 | env.globals.update({ 29 | 'gettext': ugettext, 30 | # django 31 | 'static': staticfiles_storage.url, 32 | 'url': reverse, 33 | 'now': now, 34 | # bots 35 | 'channel_url': reverse_channel, 36 | # logs 37 | 'bbme_urlizetrunc': bbme_urlizetrunc, 38 | # plugins 39 | 'plugin_docs': plugin_docs_as_html, 40 | 41 | # allauth 42 | 'socialaccount_providers': providers.registry.get_list(), 43 | 'user_display': user_display, 44 | 45 | # bootstrap_toolkit 46 | 'bootstrap_input_type': bootstrap_input_type, 47 | }) 48 | return env 49 | -------------------------------------------------------------------------------- /botbot/less/banners.less: -------------------------------------------------------------------------------- 1 | .bbsponsor-area { 2 | position: fixed; 3 | right: 0px; 4 | top: 110px; 5 | width: 160px; 6 | height: 100%; 7 | background: #fff; 8 | .bbsponsor { 9 | overflow: hidden; 10 | &.vertical { 11 | width: 160px; 12 | height: 100%; 13 | } 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/accordion.less: -------------------------------------------------------------------------------- 1 | // ACCORDION 2 | // --------- 3 | 4 | 5 | // Parent container 6 | .accordion { 7 | margin-bottom: @baseLineHeight; 8 | } 9 | 10 | // Group == heading + body 11 | .accordion-group { 12 | margin-bottom: 2px; 13 | border: 1px solid #e5e5e5; 14 | .border-radius(4px); 15 | } 16 | .accordion-heading { 17 | border-bottom: 0; 18 | } 19 | .accordion-heading .accordion-toggle { 20 | display: block; 21 | padding: 8px 15px; 22 | } 23 | 24 | // General toggle styles 25 | .accordion-toggle { 26 | cursor: pointer; 27 | } 28 | 29 | // Inner needs the styles because you can't animate properly with any styles on the element 30 | .accordion-inner { 31 | padding: 9px 15px; 32 | border-top: 1px solid #e5e5e5; 33 | } 34 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/alerts.less: -------------------------------------------------------------------------------- 1 | // ALERT STYLES 2 | // ------------ 3 | 4 | // Base alert styles 5 | .alert { 6 | padding: 8px 35px 8px 14px; 7 | margin-bottom: @baseLineHeight; 8 | text-shadow: 0 1px 0 rgba(255,255,255,.5); 9 | background-color: @warningBackground; 10 | border: 1px solid @warningBorder; 11 | .border-radius(4px); 12 | color: @warningText; 13 | } 14 | .alert-heading { 15 | color: inherit; 16 | } 17 | 18 | // Adjust close link position 19 | .alert .close { 20 | position: relative; 21 | top: -2px; 22 | right: -21px; 23 | line-height: 18px; 24 | } 25 | 26 | // Alternate styles 27 | // ---------------- 28 | 29 | .alert-success { 30 | background-color: @successBackground; 31 | border-color: @successBorder; 32 | color: @successText; 33 | } 34 | .alert-danger, 35 | .alert-error { 36 | background-color: @errorBackground; 37 | border-color: @errorBorder; 38 | color: @errorText; 39 | } 40 | .alert-info { 41 | background-color: @infoBackground; 42 | border-color: @infoBorder; 43 | color: @infoText; 44 | } 45 | 46 | // Block alerts 47 | // ------------------------ 48 | .alert-block { 49 | padding-top: 14px; 50 | padding-bottom: 14px; 51 | } 52 | .alert-block > p, 53 | .alert-block > ul { 54 | margin-bottom: 0; 55 | } 56 | .alert-block p + p { 57 | margin-top: 5px; 58 | } 59 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/bootstrap.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v2.0.4 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | // CSS Reset 12 | @import "reset.less"; 13 | 14 | // Core variables and mixins 15 | @import "variables.less"; // Modify this for custom colors, font-sizes, etc 16 | @import "mixins.less"; 17 | 18 | // Grid system and page structure 19 | @import "scaffolding.less"; 20 | @import "grid.less"; 21 | @import "layouts.less"; 22 | 23 | // Base CSS 24 | @import "type.less"; 25 | @import "code.less"; 26 | @import "forms.less"; 27 | @import "tables.less"; 28 | 29 | // Components: common 30 | @import "../font-awesome/font-awesome.less"; 31 | //@import "dropdowns.less"; 32 | //@import "wells.less"; 33 | //@import "component-animations.less"; 34 | //@import "close.less"; 35 | 36 | // Components: Buttons & Alerts 37 | @import "buttons.less"; 38 | //@import "button-groups.less"; 39 | @import "alerts.less"; // Note: alerts share common CSS with buttons and thus have styles in buttons.less 40 | 41 | // Components: Nav 42 | @import "navs.less"; 43 | @import "navbar.less"; 44 | //@import "breadcrumbs.less"; 45 | //@import "pagination.less"; 46 | //@import "pager.less"; 47 | 48 | // Components: Popovers 49 | //@import "modals.less"; 50 | //@import "tooltip.less"; 51 | //@import "popovers.less"; 52 | 53 | // Components: Misc 54 | //@import "thumbnails.less"; 55 | @import "labels-badges.less"; 56 | //@import "progress-bars.less"; 57 | //@import "accordion.less"; 58 | //@import "carousel.less"; 59 | //@import "hero-unit.less"; 60 | 61 | // Utility classes 62 | @import "utilities.less"; // Has to be last to override when necessary 63 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/breadcrumbs.less: -------------------------------------------------------------------------------- 1 | // BREADCRUMBS 2 | // ----------- 3 | 4 | .breadcrumb { 5 | padding: 7px 14px; 6 | margin: 0 0 @baseLineHeight; 7 | list-style: none; 8 | #gradient > .vertical(@white, #f5f5f5); 9 | border: 1px solid #ddd; 10 | .border-radius(3px); 11 | .box-shadow(inset 0 1px 0 @white); 12 | li { 13 | display: inline-block; 14 | .ie7-inline-block(); 15 | text-shadow: 0 1px 0 @white; 16 | } 17 | .divider { 18 | padding: 0 5px; 19 | color: @grayLight; 20 | } 21 | .active a { 22 | color: @grayDark; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/carousel.less: -------------------------------------------------------------------------------- 1 | // CAROUSEL 2 | // -------- 3 | 4 | .carousel { 5 | position: relative; 6 | margin-bottom: @baseLineHeight; 7 | line-height: 1; 8 | } 9 | 10 | .carousel-inner { 11 | overflow: hidden; 12 | width: 100%; 13 | position: relative; 14 | } 15 | 16 | .carousel { 17 | 18 | .item { 19 | display: none; 20 | position: relative; 21 | .transition(.6s ease-in-out left); 22 | } 23 | 24 | // Account for jankitude on images 25 | .item > img { 26 | display: block; 27 | line-height: 1; 28 | } 29 | 30 | .active, 31 | .next, 32 | .prev { display: block; } 33 | 34 | .active { 35 | left: 0; 36 | } 37 | 38 | .next, 39 | .prev { 40 | position: absolute; 41 | top: 0; 42 | width: 100%; 43 | } 44 | 45 | .next { 46 | left: 100%; 47 | } 48 | .prev { 49 | left: -100%; 50 | } 51 | .next.left, 52 | .prev.right { 53 | left: 0; 54 | } 55 | 56 | .active.left { 57 | left: -100%; 58 | } 59 | .active.right { 60 | left: 100%; 61 | } 62 | 63 | } 64 | 65 | // Left/right controls for nav 66 | // --------------------------- 67 | 68 | .carousel-control { 69 | position: absolute; 70 | top: 40%; 71 | left: 15px; 72 | width: 40px; 73 | height: 40px; 74 | margin-top: -20px; 75 | font-size: 60px; 76 | font-weight: 100; 77 | line-height: 30px; 78 | color: @white; 79 | text-align: center; 80 | background: @grayDarker; 81 | border: 3px solid @white; 82 | .border-radius(23px); 83 | .opacity(50); 84 | 85 | // we can't have this transition here 86 | // because webkit cancels the carousel 87 | // animation if you trip this while 88 | // in the middle of another animation 89 | // ;_; 90 | // .transition(opacity .2s linear); 91 | 92 | // Reposition the right one 93 | &.right { 94 | left: auto; 95 | right: 15px; 96 | } 97 | 98 | // Hover state 99 | &:hover { 100 | color: @white; 101 | text-decoration: none; 102 | .opacity(90); 103 | } 104 | } 105 | 106 | // Caption for text below images 107 | // ----------------------------- 108 | 109 | .carousel-caption { 110 | position: absolute; 111 | left: 0; 112 | right: 0; 113 | bottom: 0; 114 | padding: 10px 15px 5px; 115 | background: @grayDark; 116 | background: rgba(0,0,0,.75); 117 | } 118 | .carousel-caption h4, 119 | .carousel-caption p { 120 | color: @white; 121 | } 122 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/close.less: -------------------------------------------------------------------------------- 1 | // CLOSE ICONS 2 | // ----------- 3 | 4 | .close { 5 | float: right; 6 | font-size: 20px; 7 | font-weight: bold; 8 | line-height: @baseLineHeight; 9 | color: @black; 10 | text-shadow: 0 1px 0 rgba(255,255,255,1); 11 | .opacity(20); 12 | &:hover { 13 | color: @black; 14 | text-decoration: none; 15 | cursor: pointer; 16 | .opacity(40); 17 | } 18 | } 19 | 20 | // Additional properties for button version 21 | // iOS requires the button element instead of an anchor tag. 22 | // If you want the anchor version, it requires `href="#"`. 23 | button.close { 24 | padding: 0; 25 | cursor: pointer; 26 | background: transparent; 27 | border: 0; 28 | -webkit-appearance: none; 29 | } -------------------------------------------------------------------------------- /botbot/less/bootstrap/code.less: -------------------------------------------------------------------------------- 1 | // Code.less 2 | // Code typography styles for the and
 elements
 3 | // --------------------------------------------------------
 4 | 
 5 | // Inline and block code styles
 6 | code,
 7 | pre {
 8 |   padding: 0 3px 2px;
 9 |   #font > #family > .monospace;
10 |   font-size: @baseFontSize - 1;
11 |   color: @grayDark;
12 |   .border-radius(3px);
13 | }
14 | 
15 | // Inline code
16 | code {
17 |   padding: 2px 4px;
18 |   color: #d14;
19 |   background-color: #f7f7f9;
20 |   border: 1px solid #e1e1e8;
21 | }
22 | 
23 | // Blocks of code
24 | pre {
25 |   display: block;
26 |   padding: (@baseLineHeight - 1) / 2;
27 |   margin: 0 0 @baseLineHeight / 2;
28 |   font-size: @baseFontSize * .925; // 13px to 12px
29 |   line-height: @baseLineHeight;
30 |   word-break: break-all;
31 |   word-wrap: break-word;
32 |   white-space: pre;
33 |   white-space: pre-wrap;
34 |   background-color: #f5f5f5;
35 |   border: 1px solid #ccc; // fallback for IE7-8
36 |   border: 1px solid rgba(0,0,0,.15);
37 |   .border-radius(4px);
38 | 
39 |   // Make prettyprint styles more spaced out for readability
40 |   &.prettyprint {
41 |     margin-bottom: @baseLineHeight;
42 |   }
43 | 
44 |   // Account for some code outputs that place code tags in pre tags
45 |   code {
46 |     padding: 0;
47 |     color: inherit;
48 |     background-color: transparent;
49 |     border: 0;
50 |   }
51 | }
52 | 
53 | // Enable scrollable blocks of code
54 | .pre-scrollable {
55 |   max-height: 340px;
56 |   overflow-y: scroll;
57 | }


--------------------------------------------------------------------------------
/botbot/less/bootstrap/component-animations.less:
--------------------------------------------------------------------------------
 1 | // COMPONENT ANIMATIONS
 2 | // --------------------
 3 | 
 4 | .fade {
 5 |   opacity: 0;
 6 |   .transition(opacity .15s linear);
 7 |   &.in {
 8 |     opacity: 1;
 9 |   }
10 | }
11 | 
12 | .collapse {
13 |   position: relative;
14 |   height: 0;
15 |   overflow: hidden;
16 |   .transition(height .35s ease);
17 |   &.in {
18 |     height: auto;
19 |   }
20 | }
21 | 


--------------------------------------------------------------------------------
/botbot/less/bootstrap/dropdowns.less:
--------------------------------------------------------------------------------
  1 | // DROPDOWN MENUS
  2 | // --------------
  3 | 
  4 | // Use the .menu class on any 
  • element within the topbar or ul.tabs and you'll get some superfancy dropdowns 5 | .dropup, 6 | .dropdown { 7 | position: relative; 8 | } 9 | .dropdown-toggle { 10 | // The caret makes the toggle a bit too tall in IE7 11 | *margin-bottom: -3px; 12 | } 13 | .dropdown-toggle:active, 14 | .open .dropdown-toggle { 15 | outline: 0; 16 | } 17 | 18 | // Dropdown arrow/caret 19 | // -------------------- 20 | .caret { 21 | display: inline-block; 22 | width: 0; 23 | height: 0; 24 | vertical-align: top; 25 | border-top: 4px solid @black; 26 | border-right: 4px solid transparent; 27 | border-left: 4px solid transparent; 28 | content: ""; 29 | .opacity(30); 30 | } 31 | 32 | // Place the caret 33 | .dropdown .caret { 34 | margin-top: 8px; 35 | margin-left: 2px; 36 | } 37 | .dropdown:hover .caret, 38 | .open .caret { 39 | .opacity(100); 40 | } 41 | 42 | // The dropdown menu (ul) 43 | // ---------------------- 44 | .dropdown-menu { 45 | position: absolute; 46 | top: 100%; 47 | left: 0; 48 | z-index: @zindexDropdown; 49 | display: none; // none by default, but block on "open" of the menu 50 | float: left; 51 | min-width: 160px; 52 | padding: 4px 0; 53 | margin: 1px 0 0; // override default ul 54 | list-style: none; 55 | background-color: @dropdownBackground; 56 | border: 1px solid #ccc; 57 | border: 1px solid rgba(0,0,0,.2); 58 | *border-right-width: 2px; 59 | *border-bottom-width: 2px; 60 | .border-radius(5px); 61 | .box-shadow(0 5px 10px rgba(0,0,0,.2)); 62 | -webkit-background-clip: padding-box; 63 | -moz-background-clip: padding; 64 | background-clip: padding-box; 65 | 66 | // Aligns the dropdown menu to right 67 | &.pull-right { 68 | right: 0; 69 | left: auto; 70 | } 71 | 72 | // Dividers (basically an hr) within the dropdown 73 | .divider { 74 | .nav-divider(@dropdownDividerTop, @dropdownDividerBottom); 75 | } 76 | 77 | // Links within the dropdown menu 78 | a { 79 | display: block; 80 | padding: 3px 15px; 81 | clear: both; 82 | font-weight: normal; 83 | line-height: @baseLineHeight; 84 | color: @dropdownLinkColor; 85 | white-space: nowrap; 86 | } 87 | } 88 | 89 | // Hover state 90 | // ----------- 91 | .dropdown-menu li > a:hover, 92 | .dropdown-menu .active > a, 93 | .dropdown-menu .active > a:hover { 94 | color: @dropdownLinkColorHover; 95 | text-decoration: none; 96 | background-color: @dropdownLinkBackgroundHover; 97 | } 98 | 99 | // Open state for the dropdown 100 | // --------------------------- 101 | .open { 102 | // IE7's z-index only goes to the nearest positioned ancestor, which would 103 | // make the menu appear below buttons that appeared later on the page 104 | *z-index: @zindexDropdown; 105 | 106 | & > .dropdown-menu { 107 | display: block; 108 | } 109 | } 110 | 111 | // Right aligned dropdowns 112 | // --------------------------- 113 | .pull-right > .dropdown-menu { 114 | right: 0; 115 | left: auto; 116 | } 117 | 118 | // Allow for dropdowns to go bottom up (aka, dropup-menu) 119 | // ------------------------------------------------------ 120 | // Just add .dropup after the standard .dropdown class and you're set, bro. 121 | // TODO: abstract this so that the navbar fixed styles are not placed here? 122 | .dropup, 123 | .navbar-fixed-bottom .dropdown { 124 | // Reverse the caret 125 | .caret { 126 | border-top: 0; 127 | border-bottom: 4px solid @black; 128 | content: "\2191"; 129 | } 130 | // Different positioning for bottom up menu 131 | .dropdown-menu { 132 | top: auto; 133 | bottom: 100%; 134 | margin-bottom: 1px; 135 | } 136 | } 137 | 138 | // Typeahead 139 | // --------- 140 | .typeahead { 141 | margin-top: 2px; // give it some space to breathe 142 | .border-radius(4px); 143 | } 144 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/grid.less: -------------------------------------------------------------------------------- 1 | // Fixed (940px) 2 | #grid > .core(@gridColumnWidth, @gridGutterWidth); 3 | 4 | // Fluid (940px) 5 | #grid > .fluid(@fluidGridColumnWidth, @fluidGridGutterWidth); -------------------------------------------------------------------------------- /botbot/less/bootstrap/hero-unit.less: -------------------------------------------------------------------------------- 1 | // HERO UNIT 2 | // --------- 3 | 4 | .hero-unit { 5 | padding: 60px; 6 | margin-bottom: 30px; 7 | background-color: @heroUnitBackground; 8 | .border-radius(6px); 9 | h1 { 10 | margin-bottom: 0; 11 | font-size: 60px; 12 | line-height: 1; 13 | color: @heroUnitHeadingColor; 14 | letter-spacing: -1px; 15 | } 16 | p { 17 | font-size: 18px; 18 | font-weight: 200; 19 | line-height: @baseLineHeight * 1.5; 20 | color: @heroUnitLeadColor; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/labels-badges.less: -------------------------------------------------------------------------------- 1 | // LABELS & BADGES 2 | // --------------- 3 | 4 | // Base classes 5 | .label, 6 | .badge { 7 | font-size: @baseFontSize * .846; 8 | font-weight: bold; 9 | line-height: 14px; // ensure proper line-height if floated 10 | color: @white; 11 | vertical-align: baseline; 12 | white-space: nowrap; 13 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 14 | background-color: @grayLight; 15 | } 16 | // Set unique padding and border-radii 17 | .label { 18 | padding: 1px 4px 2px; 19 | .border-radius(3px); 20 | } 21 | .badge { 22 | padding: 1px 9px 2px; 23 | .border-radius(9px); 24 | } 25 | 26 | // Hover state, but only for links 27 | a { 28 | &.label:hover, 29 | &.badge:hover { 30 | color: @white; 31 | text-decoration: none; 32 | cursor: pointer; 33 | } 34 | } 35 | 36 | // Colors 37 | // Only give background-color difference to links (and to simplify, we don't qualifty with `a` but [href] attribute) 38 | .label, 39 | .badge { 40 | // Important (red) 41 | &-important { background-color: @errorText; } 42 | &-important[href] { background-color: darken(@errorText, 10%); } 43 | // Warnings (orange) 44 | &-warning { background-color: @orange; } 45 | &-warning[href] { background-color: darken(@orange, 10%); } 46 | // Success (green) 47 | &-success { background-color: @successText; } 48 | &-success[href] { background-color: darken(@successText, 10%); } 49 | // Info (turquoise) 50 | &-info { background-color: @infoText; } 51 | &-info[href] { background-color: darken(@infoText, 10%); } 52 | // Inverse (black) 53 | &-inverse { background-color: @grayDark; } 54 | &-inverse[href] { background-color: darken(@grayDark, 10%); } 55 | } 56 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/layouts.less: -------------------------------------------------------------------------------- 1 | // 2 | // Layouts 3 | // Fixed-width and fluid (with sidebar) layouts 4 | // -------------------------------------------- 5 | 6 | 7 | // Container (centered, fixed-width layouts) 8 | .container { 9 | .container-fixed(); 10 | } 11 | 12 | // Fluid layouts (left aligned, with sidebar, min- & max-width content) 13 | .container-fluid { 14 | padding-right: @gridGutterWidth; 15 | padding-left: @gridGutterWidth; 16 | .clearfix(); 17 | } -------------------------------------------------------------------------------- /botbot/less/bootstrap/modals.less: -------------------------------------------------------------------------------- 1 | // MODALS 2 | // ------ 3 | 4 | // Recalculate z-index where appropriate 5 | .modal-open { 6 | .dropdown-menu { z-index: @zindexDropdown + @zindexModal; } 7 | .dropdown.open { *z-index: @zindexDropdown + @zindexModal; } 8 | .popover { z-index: @zindexPopover + @zindexModal; } 9 | .tooltip { z-index: @zindexTooltip + @zindexModal; } 10 | } 11 | 12 | // Background 13 | .modal-backdrop { 14 | position: fixed; 15 | top: 0; 16 | right: 0; 17 | bottom: 0; 18 | left: 0; 19 | z-index: @zindexModalBackdrop; 20 | background-color: @black; 21 | // Fade for backdrop 22 | &.fade { opacity: 0; } 23 | } 24 | 25 | .modal-backdrop, 26 | .modal-backdrop.fade.in { 27 | .opacity(80); 28 | } 29 | 30 | // Base modal 31 | .modal { 32 | position: fixed; 33 | top: 50%; 34 | left: 50%; 35 | z-index: @zindexModal; 36 | overflow: auto; 37 | width: 560px; 38 | margin: -250px 0 0 -280px; 39 | background-color: @white; 40 | border: 1px solid #999; 41 | border: 1px solid rgba(0,0,0,.3); 42 | *border: 1px solid #999; /* IE6-7 */ 43 | .border-radius(6px); 44 | .box-shadow(0 3px 7px rgba(0,0,0,0.3)); 45 | .background-clip(padding-box); 46 | &.fade { 47 | .transition(e('opacity .3s linear, top .3s ease-out')); 48 | top: -25%; 49 | } 50 | &.fade.in { top: 50%; } 51 | } 52 | .modal-header { 53 | padding: 9px 15px; 54 | border-bottom: 1px solid #eee; 55 | // Close icon 56 | .close { margin-top: 2px; } 57 | } 58 | 59 | // Body (where all modal content resides) 60 | .modal-body { 61 | overflow-y: auto; 62 | max-height: 400px; 63 | padding: 15px; 64 | } 65 | // Remove bottom margin if need be 66 | .modal-form { 67 | margin-bottom: 0; 68 | } 69 | 70 | // Footer (for actions) 71 | .modal-footer { 72 | padding: 14px 15px 15px; 73 | margin-bottom: 0; 74 | text-align: right; // right align buttons 75 | background-color: #f5f5f5; 76 | border-top: 1px solid #ddd; 77 | .border-radius(0 0 6px 6px); 78 | .box-shadow(inset 0 1px 0 @white); 79 | .clearfix(); // clear it in case folks use .pull-* classes on buttons 80 | 81 | // Properly space out buttons 82 | .btn + .btn { 83 | margin-left: 5px; 84 | margin-bottom: 0; // account for input[type="submit"] which gets the bottom margin like all other inputs 85 | } 86 | // but override that for button groups 87 | .btn-group .btn + .btn { 88 | margin-left: -1px; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/pager.less: -------------------------------------------------------------------------------- 1 | // PAGER 2 | // ----- 3 | 4 | .pager { 5 | margin-left: 0; 6 | margin-bottom: @baseLineHeight; 7 | list-style: none; 8 | text-align: center; 9 | .clearfix(); 10 | } 11 | .pager li { 12 | display: inline; 13 | } 14 | .pager a { 15 | display: inline-block; 16 | padding: 5px 14px; 17 | background-color: #fff; 18 | border: 1px solid #ddd; 19 | .border-radius(15px); 20 | } 21 | .pager a:hover { 22 | text-decoration: none; 23 | background-color: #f5f5f5; 24 | } 25 | .pager .next a { 26 | float: right; 27 | } 28 | .pager .previous a { 29 | float: left; 30 | } 31 | .pager .disabled a, 32 | .pager .disabled a:hover { 33 | color: @grayLight; 34 | background-color: #fff; 35 | cursor: default; 36 | } -------------------------------------------------------------------------------- /botbot/less/bootstrap/pagination.less: -------------------------------------------------------------------------------- 1 | // PAGINATION 2 | // ---------- 3 | 4 | .pagination { 5 | height: @baseLineHeight * 2; 6 | margin: @baseLineHeight 0; 7 | } 8 | .pagination ul { 9 | display: inline-block; 10 | .ie7-inline-block(); 11 | margin-left: 0; 12 | margin-bottom: 0; 13 | .border-radius(3px); 14 | .box-shadow(0 1px 2px rgba(0,0,0,.05)); 15 | } 16 | .pagination li { 17 | display: inline; 18 | } 19 | .pagination a { 20 | float: left; 21 | padding: 0 14px; 22 | line-height: (@baseLineHeight * 2) - 2; 23 | text-decoration: none; 24 | border: 1px solid #ddd; 25 | border-left-width: 0; 26 | } 27 | .pagination a:hover, 28 | .pagination .active a { 29 | background-color: #f5f5f5; 30 | } 31 | .pagination .active a { 32 | color: @grayLight; 33 | cursor: default; 34 | } 35 | .pagination .disabled span, 36 | .pagination .disabled a, 37 | .pagination .disabled a:hover { 38 | color: @grayLight; 39 | background-color: transparent; 40 | cursor: default; 41 | } 42 | .pagination li:first-child a { 43 | border-left-width: 1px; 44 | .border-radius(3px 0 0 3px); 45 | } 46 | .pagination li:last-child a { 47 | .border-radius(0 3px 3px 0); 48 | } 49 | 50 | // Centered 51 | .pagination-centered { 52 | text-align: center; 53 | } 54 | .pagination-right { 55 | text-align: right; 56 | } 57 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/popovers.less: -------------------------------------------------------------------------------- 1 | // POPOVERS 2 | // -------- 3 | 4 | .popover { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | z-index: @zindexPopover; 9 | display: none; 10 | padding: 5px; 11 | &.top { margin-top: -5px; } 12 | &.right { margin-left: 5px; } 13 | &.bottom { margin-top: 5px; } 14 | &.left { margin-left: -5px; } 15 | &.top .arrow { #popoverArrow > .top(); } 16 | &.right .arrow { #popoverArrow > .right(); } 17 | &.bottom .arrow { #popoverArrow > .bottom(); } 18 | &.left .arrow { #popoverArrow > .left(); } 19 | .arrow { 20 | position: absolute; 21 | width: 0; 22 | height: 0; 23 | } 24 | } 25 | .popover-inner { 26 | padding: 3px; 27 | width: 280px; 28 | overflow: hidden; 29 | background: @black; // has to be full background declaration for IE fallback 30 | background: rgba(0,0,0,.8); 31 | .border-radius(6px); 32 | .box-shadow(0 3px 7px rgba(0,0,0,0.3)); 33 | } 34 | .popover-title { 35 | padding: 9px 15px; 36 | line-height: 1; 37 | background-color: #f5f5f5; 38 | border-bottom:1px solid #eee; 39 | .border-radius(3px 3px 0 0); 40 | } 41 | .popover-content { 42 | padding: 14px; 43 | background-color: @white; 44 | .border-radius(0 0 3px 3px); 45 | .background-clip(padding-box); 46 | p, ul, ol { 47 | margin-bottom: 0; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/progress-bars.less: -------------------------------------------------------------------------------- 1 | // PROGRESS BARS 2 | // ------------- 3 | 4 | 5 | // ANIMATIONS 6 | // ---------- 7 | 8 | // Webkit 9 | @-webkit-keyframes progress-bar-stripes { 10 | from { background-position: 40px 0; } 11 | to { background-position: 0 0; } 12 | } 13 | 14 | // Firefox 15 | @-moz-keyframes progress-bar-stripes { 16 | from { background-position: 40px 0; } 17 | to { background-position: 0 0; } 18 | } 19 | 20 | // IE9 21 | @-ms-keyframes progress-bar-stripes { 22 | from { background-position: 40px 0; } 23 | to { background-position: 0 0; } 24 | } 25 | 26 | // Opera 27 | @-o-keyframes progress-bar-stripes { 28 | from { background-position: 0 0; } 29 | to { background-position: 40px 0; } 30 | } 31 | 32 | // Spec 33 | @keyframes progress-bar-stripes { 34 | from { background-position: 40px 0; } 35 | to { background-position: 0 0; } 36 | } 37 | 38 | 39 | 40 | // THE BARS 41 | // -------- 42 | 43 | // Outer container 44 | .progress { 45 | overflow: hidden; 46 | height: 18px; 47 | margin-bottom: 18px; 48 | #gradient > .vertical(#f5f5f5, #f9f9f9); 49 | .box-shadow(inset 0 1px 2px rgba(0,0,0,.1)); 50 | .border-radius(4px); 51 | } 52 | 53 | // Bar of progress 54 | .progress .bar { 55 | width: 0%; 56 | height: 18px; 57 | color: @white; 58 | font-size: 12px; 59 | text-align: center; 60 | text-shadow: 0 -1px 0 rgba(0,0,0,.25); 61 | #gradient > .vertical(#149bdf, #0480be); 62 | .box-shadow(inset 0 -1px 0 rgba(0,0,0,.15)); 63 | .box-sizing(border-box); 64 | .transition(width .6s ease); 65 | } 66 | 67 | // Striped bars 68 | .progress-striped .bar { 69 | #gradient > .striped(#149bdf); 70 | .background-size(40px 40px); 71 | } 72 | 73 | // Call animation for the active one 74 | .progress.active .bar { 75 | -webkit-animation: progress-bar-stripes 2s linear infinite; 76 | -moz-animation: progress-bar-stripes 2s linear infinite; 77 | -ms-animation: progress-bar-stripes 2s linear infinite; 78 | -o-animation: progress-bar-stripes 2s linear infinite; 79 | animation: progress-bar-stripes 2s linear infinite; 80 | } 81 | 82 | 83 | 84 | // COLORS 85 | // ------ 86 | 87 | // Danger (red) 88 | .progress-danger .bar { 89 | #gradient > .vertical(#ee5f5b, #c43c35); 90 | } 91 | .progress-danger.progress-striped .bar { 92 | #gradient > .striped(#ee5f5b); 93 | } 94 | 95 | // Success (green) 96 | .progress-success .bar { 97 | #gradient > .vertical(#62c462, #57a957); 98 | } 99 | .progress-success.progress-striped .bar { 100 | #gradient > .striped(#62c462); 101 | } 102 | 103 | // Info (teal) 104 | .progress-info .bar { 105 | #gradient > .vertical(#5bc0de, #339bb9); 106 | } 107 | .progress-info.progress-striped .bar { 108 | #gradient > .striped(#5bc0de); 109 | } 110 | 111 | // Warning (orange) 112 | .progress-warning .bar { 113 | #gradient > .vertical(lighten(@orange, 15%), @orange); 114 | } 115 | .progress-warning.progress-striped .bar { 116 | #gradient > .striped(lighten(@orange, 15%)); 117 | } 118 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/reset.less: -------------------------------------------------------------------------------- 1 | // Reset.less 2 | // Adapted from Normalize.css http://github.com/necolas/normalize.css 3 | // ------------------------------------------------------------------------ 4 | 5 | // Display in IE6-9 and FF3 6 | // ------------------------- 7 | 8 | article, 9 | aside, 10 | details, 11 | figcaption, 12 | figure, 13 | footer, 14 | header, 15 | hgroup, 16 | nav, 17 | section { 18 | display: block; 19 | } 20 | 21 | // Display block in IE6-9 and FF3 22 | // ------------------------- 23 | 24 | audio, 25 | canvas, 26 | video { 27 | display: inline-block; 28 | *display: inline; 29 | *zoom: 1; 30 | } 31 | 32 | // Prevents modern browsers from displaying 'audio' without controls 33 | // ------------------------- 34 | 35 | audio:not([controls]) { 36 | display: none; 37 | } 38 | 39 | // Base settings 40 | // ------------------------- 41 | 42 | html { 43 | font-size: 100%; 44 | -webkit-text-size-adjust: 100%; 45 | -ms-text-size-adjust: 100%; 46 | } 47 | // Focus states 48 | a:focus { 49 | .tab-focus(); 50 | } 51 | // Hover & Active 52 | a:hover, 53 | a:active { 54 | outline: 0; 55 | } 56 | 57 | // Prevents sub and sup affecting line-height in all browsers 58 | // ------------------------- 59 | 60 | sub, 61 | sup { 62 | position: relative; 63 | font-size: 75%; 64 | line-height: 0; 65 | vertical-align: baseline; 66 | } 67 | sup { 68 | top: -0.5em; 69 | } 70 | sub { 71 | bottom: -0.25em; 72 | } 73 | 74 | // Img border in a's and image quality 75 | // ------------------------- 76 | 77 | img { 78 | max-width: 100%; // Make images inherently responsive 79 | vertical-align: middle; 80 | border: 0; 81 | -ms-interpolation-mode: bicubic; 82 | } 83 | 84 | // Prevent max-width from affecting Google Maps 85 | #map_canvas img { 86 | max-width: none; 87 | } 88 | 89 | // Forms 90 | // ------------------------- 91 | 92 | // Font size in all browsers, margin changes, misc consistency 93 | button, 94 | input, 95 | select, 96 | textarea { 97 | margin: 0; 98 | font-size: 100%; 99 | vertical-align: middle; 100 | } 101 | button, 102 | input { 103 | *overflow: visible; // Inner spacing ie IE6/7 104 | line-height: normal; // FF3/4 have !important on line-height in UA stylesheet 105 | } 106 | button::-moz-focus-inner, 107 | input::-moz-focus-inner { // Inner padding and border oddities in FF3/4 108 | padding: 0; 109 | border: 0; 110 | } 111 | button, 112 | input[type="button"], 113 | input[type="reset"], 114 | input[type="submit"] { 115 | cursor: pointer; // Cursors on all buttons applied consistently 116 | -webkit-appearance: button; // Style clickable inputs in iOS 117 | } 118 | input[type="search"] { // Appearance in Safari/Chrome 119 | -webkit-box-sizing: content-box; 120 | -moz-box-sizing: content-box; 121 | box-sizing: content-box; 122 | -webkit-appearance: textfield; 123 | } 124 | input[type="search"]::-webkit-search-decoration, 125 | input[type="search"]::-webkit-search-cancel-button { 126 | -webkit-appearance: none; // Inner-padding issues in Chrome OSX, Safari 5 127 | } 128 | textarea { 129 | overflow: auto; // Remove vertical scrollbar in IE6-9 130 | vertical-align: top; // Readability and alignment cross-browser 131 | } 132 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/responsive-1200px-min.less: -------------------------------------------------------------------------------- 1 | // LARGE DESKTOP & UP 2 | // ------------------ 3 | 4 | @media (min-width: 1200px) { 5 | 6 | // Fixed grid 7 | #grid > .core(70px, 30px); 8 | 9 | // Fluid grid 10 | #grid > .fluid(5.982905983%, 2.564102564%); 11 | 12 | // Input grid 13 | #grid > .input(70px, 30px); 14 | 15 | // Thumbnails 16 | .thumbnails { 17 | margin-left: -30px; 18 | } 19 | .thumbnails > li { 20 | margin-left: 30px; 21 | } 22 | .row-fluid .thumbnails { 23 | margin-left: 0; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/responsive-767px-max.less: -------------------------------------------------------------------------------- 1 | // UP TO LANDSCAPE PHONE 2 | // --------------------- 3 | 4 | @media (max-width: 480px) { 5 | 6 | // Smooth out the collapsing/expanding nav 7 | .nav-collapse { 8 | -webkit-transform: translate3d(0, 0, 0); // activate the GPU 9 | } 10 | 11 | // Block level the page header small tag for readability 12 | .page-header h1 small { 13 | display: block; 14 | line-height: @baseLineHeight; 15 | } 16 | 17 | // Update checkboxes for iOS 18 | input[type="checkbox"], 19 | input[type="radio"] { 20 | border: 1px solid #ccc; 21 | } 22 | 23 | // Remove the horizontal form styles 24 | .form-horizontal .control-group > label { 25 | float: none; 26 | width: auto; 27 | padding-top: 0; 28 | text-align: left; 29 | } 30 | // Move over all input controls and content 31 | .form-horizontal .controls { 32 | margin-left: 0; 33 | } 34 | // Move the options list down to align with labels 35 | .form-horizontal .control-list { 36 | padding-top: 0; // has to be padding because margin collaspes 37 | } 38 | // Move over buttons in .form-actions to align with .controls 39 | .form-horizontal .form-actions { 40 | padding-left: 10px; 41 | padding-right: 10px; 42 | } 43 | 44 | // Modals 45 | .modal { 46 | position: absolute; 47 | top: 10px; 48 | left: 10px; 49 | right: 10px; 50 | width: auto; 51 | margin: 0; 52 | &.fade.in { top: auto; } 53 | } 54 | .modal-header .close { 55 | padding: 10px; 56 | margin: -10px; 57 | } 58 | 59 | // Carousel 60 | .carousel-caption { 61 | position: static; 62 | } 63 | 64 | } 65 | 66 | 67 | 68 | // LANDSCAPE PHONE TO SMALL DESKTOP & PORTRAIT TABLET 69 | // -------------------------------------------------- 70 | 71 | @media (max-width: 767px) { 72 | 73 | // Padding to set content in a bit 74 | body { 75 | padding-left: 20px; 76 | padding-right: 20px; 77 | } 78 | // Negative indent the now static "fixed" navbar 79 | .navbar-fixed-top, 80 | .navbar-fixed-bottom { 81 | margin-left: -20px; 82 | margin-right: -20px; 83 | } 84 | // Remove padding on container given explicit padding set on body 85 | .container-fluid { 86 | padding: 0; 87 | } 88 | 89 | // TYPOGRAPHY 90 | // ---------- 91 | // Reset horizontal dl 92 | .dl-horizontal { 93 | dt { 94 | float: none; 95 | clear: none; 96 | width: auto; 97 | text-align: left; 98 | } 99 | dd { 100 | margin-left: 0; 101 | } 102 | } 103 | 104 | // GRID & CONTAINERS 105 | // ----------------- 106 | // Remove width from containers 107 | .container { 108 | width: auto; 109 | } 110 | // Fluid rows 111 | .row-fluid { 112 | width: 100%; 113 | } 114 | // Undo negative margin on rows and thumbnails 115 | .row, 116 | .thumbnails { 117 | margin-left: 0; 118 | } 119 | // Make all grid-sized elements block level again 120 | [class*="span"], 121 | .row-fluid [class*="span"] { 122 | float: none; 123 | display: block; 124 | width: auto; 125 | margin-left: 0; 126 | } 127 | 128 | // FORM FIELDS 129 | // ----------- 130 | // Make span* classes full width 131 | .input-large, 132 | .input-xlarge, 133 | .input-xxlarge, 134 | input[class*="span"], 135 | select[class*="span"], 136 | textarea[class*="span"], 137 | .uneditable-input { 138 | .input-block-level(); 139 | } 140 | // But don't let it screw up prepend/append inputs 141 | .input-prepend input, 142 | .input-append input, 143 | .input-prepend input[class*="span"], 144 | .input-append input[class*="span"] { 145 | display: inline-block; // redeclare so they don't wrap to new lines 146 | width: auto; 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/responsive-768px-979px.less: -------------------------------------------------------------------------------- 1 | // PORTRAIT TABLET TO DEFAULT DESKTOP 2 | // ---------------------------------- 3 | 4 | @media (min-width: 768px) and (max-width: 979px) { 5 | 6 | // Fixed grid 7 | #grid > .core(42px, 20px); 8 | 9 | // Fluid grid 10 | #grid > .fluid(5.801104972%, 2.762430939%); 11 | 12 | // Input grid 13 | #grid > .input(42px, 20px); 14 | 15 | // No need to reset .thumbnails here since it's the same @gridGutterWidth 16 | 17 | } 18 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/responsive-navbar.less: -------------------------------------------------------------------------------- 1 | // TABLETS AND BELOW 2 | // ----------------- 3 | @media (max-width: 979px) { 4 | 5 | // UNFIX THE TOPBAR 6 | // ---------------- 7 | // Remove any padding from the body 8 | body { 9 | padding-top: 0; 10 | } 11 | // Unfix the navbar 12 | .navbar-fixed-top, 13 | .navbar-fixed-bottom { 14 | //position: static; 15 | } 16 | .navbar-fixed-top { 17 | margin-bottom: @baseLineHeight; 18 | } 19 | .navbar-fixed-bottom { 20 | margin-top: @baseLineHeight; 21 | } 22 | .navbar-fixed-top .navbar-inner, 23 | .navbar-fixed-bottom .navbar-inner { 24 | padding: 5px; 25 | } 26 | .navbar .container { 27 | width: auto; 28 | padding: 0; 29 | } 30 | // Account for brand name 31 | .navbar .brand { 32 | padding-left: 10px; 33 | padding-right: 10px; 34 | margin: 0 0 0 -5px; 35 | } 36 | 37 | // COLLAPSIBLE NAVBAR 38 | // ------------------ 39 | // Nav collapse clears brand 40 | .nav-collapse { 41 | clear: both; 42 | } 43 | // Block-level the nav 44 | .nav-collapse .nav { 45 | float: none; 46 | margin: 0 0 (@baseLineHeight / 2); 47 | } 48 | .nav-collapse .nav > li { 49 | float: none; 50 | } 51 | .nav-collapse .nav > li > a { 52 | margin-bottom: 2px; 53 | } 54 | .nav-collapse .nav > .divider-vertical { 55 | display: none; 56 | } 57 | .nav-collapse .nav .nav-header { 58 | color: @navbarText; 59 | text-shadow: none; 60 | } 61 | // Nav and dropdown links in navbar 62 | .nav-collapse .nav > li > a, 63 | .nav-collapse .dropdown-menu a { 64 | padding: 6px 15px; 65 | font-weight: bold; 66 | color: @navbarLinkColor; 67 | .border-radius(3px); 68 | } 69 | // Buttons 70 | .nav-collapse .btn { 71 | padding: 4px 10px 4px; 72 | font-weight: normal; 73 | .border-radius(4px); 74 | } 75 | .nav-collapse .dropdown-menu li + li a { 76 | margin-bottom: 2px; 77 | } 78 | .nav-collapse .nav > li > a:hover, 79 | .nav-collapse .dropdown-menu a:hover { 80 | background-color: @navbarBackground; 81 | } 82 | // Buttons in the navbar 83 | .nav-collapse.in .btn-group { 84 | margin-top: 5px; 85 | padding: 0; 86 | } 87 | // Dropdowns in the navbar 88 | .nav-collapse .dropdown-menu { 89 | position: static; 90 | top: auto; 91 | left: auto; 92 | float: none; 93 | display: block; 94 | max-width: none; 95 | margin: 0 15px; 96 | padding: 0; 97 | background-color: transparent; 98 | border: none; 99 | .border-radius(0); 100 | .box-shadow(none); 101 | } 102 | .nav-collapse .dropdown-menu:before, 103 | .nav-collapse .dropdown-menu:after { 104 | display: none; 105 | } 106 | .nav-collapse .dropdown-menu .divider { 107 | display: none; 108 | } 109 | // Forms in navbar 110 | .nav-collapse .navbar-form, 111 | .nav-collapse .navbar-search { 112 | float: none; 113 | padding: (@baseLineHeight / 2) 15px; 114 | margin: (@baseLineHeight / 2) 0; 115 | border-top: 1px solid @navbarBackground; 116 | border-bottom: 1px solid @navbarBackground; 117 | .box-shadow(~"inset 0 1px 0 rgba(255,255,255,.1), 0 1px 0 rgba(255,255,255,.1)"); 118 | } 119 | // Pull right (secondary) nav content 120 | .navbar .nav-collapse .nav.pull-right { 121 | float: none; 122 | margin-left: 0; 123 | } 124 | // Hide everything in the navbar save .brand and toggle button */ 125 | .nav-collapse, 126 | .nav-collapse.collapse { 127 | overflow: hidden; 128 | height: 0; 129 | } 130 | // Navbar button 131 | .navbar .btn-navbar { 132 | display: block; 133 | } 134 | 135 | // STATIC NAVBAR 136 | // ------------- 137 | .navbar-static .navbar-inner { 138 | padding-left: 10px; 139 | padding-right: 10px; 140 | } 141 | } 142 | 143 | 144 | // DEFAULT DESKTOP 145 | // --------------- 146 | 147 | // Required to make the collapsing navbar work on regular desktops 148 | @media (min-width: 980px) { 149 | .nav-collapse.collapse { 150 | height: auto !important; 151 | overflow: visible !important; 152 | } 153 | } -------------------------------------------------------------------------------- /botbot/less/bootstrap/responsive-utilities.less: -------------------------------------------------------------------------------- 1 | // RESPONSIVE CLASSES 2 | // ------------------ 3 | 4 | // Hide from screenreaders and browsers 5 | // Credit: HTML5 Boilerplate 6 | .hidden { 7 | display: none; 8 | visibility: hidden; 9 | } 10 | 11 | // Visibility utilities 12 | 13 | // For desktops 14 | .visible-phone { display: none !important; } 15 | .visible-tablet { display: none !important; } 16 | .visible-desktop { } // Don't set initially 17 | .hidden-phone { } 18 | .hidden-tablet { } 19 | .hidden-desktop { display: none !important; } 20 | 21 | // Phones only 22 | @media (max-width: 767px) { 23 | // Show 24 | .visible-phone { display: inherit !important; } // Use inherit to restore previous behavior 25 | // Hide 26 | .hidden-phone { display: none !important; } 27 | // Hide everything else 28 | .hidden-desktop { display: inherit !important; } 29 | .visible-desktop { display: none !important; } 30 | } 31 | 32 | // Tablets & small desktops only 33 | @media (min-width: 768px) and (max-width: 979px) { 34 | // Show 35 | .visible-tablet { display: inherit !important; } 36 | // Hide 37 | .hidden-tablet { display: none !important; } 38 | // Hide everything else 39 | .hidden-desktop { display: inherit !important; } 40 | .visible-desktop { display: none !important ; } 41 | } 42 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/responsive.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap Responsive v2.0.4 3 | * 4 | * Copyright 2012 Twitter, Inc 5 | * Licensed under the Apache License v2.0 6 | * http://www.apache.org/licenses/LICENSE-2.0 7 | * 8 | * Designed and built with all the love in the world @twitter by @mdo and @fat. 9 | */ 10 | 11 | 12 | // Responsive.less 13 | // For phone and tablet devices 14 | // ------------------------------------------------------------- 15 | 16 | 17 | // REPEAT VARIABLES & MIXINS 18 | // ------------------------- 19 | // Required since we compile the responsive stuff separately 20 | 21 | @import "variables.less"; // Modify this for custom colors, font-sizes, etc 22 | @import "mixins.less"; 23 | 24 | 25 | // RESPONSIVE CLASSES 26 | // ------------------ 27 | 28 | @import "responsive-utilities.less"; 29 | 30 | 31 | // MEDIA QUERIES 32 | // ------------------ 33 | 34 | // Phones to portrait tablets and narrow desktops 35 | @import "responsive-767px-max.less"; 36 | 37 | // Tablets to regular desktops 38 | @import "responsive-768px-979px.less"; 39 | 40 | // Large desktops 41 | @import "responsive-1200px-min.less"; 42 | 43 | 44 | // RESPONSIVE NAVBAR 45 | // ------------------ 46 | 47 | // From 979px and below, show a button to toggle navbar contents 48 | @import "responsive-navbar.less"; 49 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/scaffolding.less: -------------------------------------------------------------------------------- 1 | // Scaffolding 2 | // Basic and global styles for generating a grid system, structural layout, and page templates 3 | // ------------------------------------------------------------------------------------------- 4 | 5 | 6 | // Body reset 7 | // ---------- 8 | 9 | body { 10 | margin: 0; 11 | font-family: @baseFontFamily; 12 | font-size: @baseFontSize; 13 | line-height: @baseLineHeight; 14 | color: @textColor; 15 | background-color: @bodyBackground; 16 | } 17 | 18 | 19 | // Links 20 | // ----- 21 | 22 | a { 23 | color: @linkColor; 24 | text-decoration: none; 25 | } 26 | a:hover { 27 | color: @linkColorHover; 28 | text-decoration: underline; 29 | } 30 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/thumbnails.less: -------------------------------------------------------------------------------- 1 | // THUMBNAILS 2 | // ---------- 3 | // Note: `.thumbnails` and `.thumbnails > li` are overriden in responsive files 4 | 5 | // Make wrapper ul behave like the grid 6 | .thumbnails { 7 | margin-left: -@gridGutterWidth; 8 | list-style: none; 9 | .clearfix(); 10 | } 11 | // Fluid rows have no left margin 12 | .row-fluid .thumbnails { 13 | margin-left: 0; 14 | } 15 | 16 | // Float li to make thumbnails appear in a row 17 | .thumbnails > li { 18 | float: left; // Explicity set the float since we don't require .span* classes 19 | margin-bottom: @baseLineHeight; 20 | margin-left: @gridGutterWidth; 21 | } 22 | 23 | // The actual thumbnail (can be `a` or `div`) 24 | .thumbnail { 25 | display: block; 26 | padding: 4px; 27 | line-height: 1; 28 | border: 1px solid #ddd; 29 | .border-radius(4px); 30 | .box-shadow(0 1px 1px rgba(0,0,0,.075)); 31 | } 32 | // Add a hover state for linked versions only 33 | a.thumbnail:hover { 34 | border-color: @linkColor; 35 | .box-shadow(0 1px 4px rgba(0,105,214,.25)); 36 | } 37 | 38 | // Images and captions 39 | .thumbnail > img { 40 | display: block; 41 | max-width: 100%; 42 | margin-left: auto; 43 | margin-right: auto; 44 | } 45 | .thumbnail .caption { 46 | padding: 9px; 47 | } 48 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/tooltip.less: -------------------------------------------------------------------------------- 1 | // TOOLTIP 2 | // ------= 3 | 4 | .tooltip { 5 | position: absolute; 6 | z-index: @zindexTooltip; 7 | display: block; 8 | visibility: visible; 9 | padding: 5px; 10 | font-size: 11px; 11 | .opacity(0); 12 | &.in { .opacity(80); } 13 | &.top { margin-top: -2px; } 14 | &.right { margin-left: 2px; } 15 | &.bottom { margin-top: 2px; } 16 | &.left { margin-left: -2px; } 17 | &.top .tooltip-arrow { #popoverArrow > .top(); } 18 | &.left .tooltip-arrow { #popoverArrow > .left(); } 19 | &.bottom .tooltip-arrow { #popoverArrow > .bottom(); } 20 | &.right .tooltip-arrow { #popoverArrow > .right(); } 21 | } 22 | .tooltip-inner { 23 | max-width: 200px; 24 | padding: 3px 8px; 25 | color: @white; 26 | text-align: center; 27 | text-decoration: none; 28 | background-color: @black; 29 | .border-radius(4px); 30 | } 31 | .tooltip-arrow { 32 | position: absolute; 33 | width: 0; 34 | height: 0; 35 | } 36 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/utilities.less: -------------------------------------------------------------------------------- 1 | // UTILITY CLASSES 2 | // --------------- 3 | 4 | // Quick floats 5 | .pull-right { 6 | float: right; 7 | } 8 | .pull-left { 9 | float: left; 10 | } 11 | 12 | // Toggling content 13 | .hide { 14 | display: none; 15 | } 16 | .show { 17 | display: block; 18 | } 19 | 20 | // Visibility 21 | .invisible { 22 | visibility: hidden; 23 | } 24 | -------------------------------------------------------------------------------- /botbot/less/bootstrap/wells.less: -------------------------------------------------------------------------------- 1 | // WELLS 2 | // ----- 3 | 4 | .well { 5 | min-height: 20px; 6 | padding: 19px; 7 | margin-bottom: 20px; 8 | background-color: #f5f5f5; 9 | border: 1px solid #eee; 10 | border: 1px solid rgba(0,0,0,.05); 11 | .border-radius(4px); 12 | .box-shadow(inset 0 1px 1px rgba(0,0,0,.05)); 13 | blockquote { 14 | border-color: #ddd; 15 | border-color: rgba(0,0,0,.15); 16 | } 17 | } 18 | 19 | // Sizes 20 | .well-large { 21 | padding: 24px; 22 | .border-radius(6px); 23 | } 24 | .well-small { 25 | padding: 9px; 26 | .border-radius(3px); 27 | } 28 | -------------------------------------------------------------------------------- /botbot/less/font-awesome/bootstrap.less: -------------------------------------------------------------------------------- 1 | /* BOOTSTRAP SPECIFIC CLASSES 2 | * -------------------------- */ 3 | 4 | /* Bootstrap 2.0 sprites.less reset */ 5 | [class^="icon-"], 6 | [class*=" icon-"] { 7 | display: inline; 8 | width: auto; 9 | height: auto; 10 | line-height: normal; 11 | vertical-align: baseline; 12 | background-image: none; 13 | background-position: 0% 0%; 14 | background-repeat: repeat; 15 | margin-top: 0; 16 | } 17 | 18 | /* more sprites.less reset */ 19 | .icon-white, 20 | .nav-pills > .active > a > [class^="icon-"], 21 | .nav-pills > .active > a > [class*=" icon-"], 22 | .nav-list > .active > a > [class^="icon-"], 23 | .nav-list > .active > a > [class*=" icon-"], 24 | .navbar-inverse .nav > .active > a > [class^="icon-"], 25 | .navbar-inverse .nav > .active > a > [class*=" icon-"], 26 | .dropdown-menu > li > a:hover > [class^="icon-"], 27 | .dropdown-menu > li > a:hover > [class*=" icon-"], 28 | .dropdown-menu > .active > a > [class^="icon-"], 29 | .dropdown-menu > .active > a > [class*=" icon-"], 30 | .dropdown-submenu:hover > a > [class^="icon-"], 31 | .dropdown-submenu:hover > a > [class*=" icon-"] { 32 | background-image: none; 33 | } 34 | 35 | 36 | /* keeps Bootstrap styles with and without icons the same */ 37 | .btn, .nav { 38 | [class^="icon-"], 39 | [class*=" icon-"] { 40 | // display: inline; 41 | &.icon-large { line-height: .9em; } 42 | &.icon-spin { display: inline-block; } 43 | } 44 | } 45 | .nav-tabs, .nav-pills { 46 | [class^="icon-"], 47 | [class*=" icon-"] { 48 | &, &.icon-large { line-height: .9em; } 49 | } 50 | } 51 | .btn { 52 | [class^="icon-"], 53 | [class*=" icon-"] { 54 | &.pull-left, &.pull-right { 55 | &.icon-2x { margin-top: .18em; } 56 | } 57 | &.icon-spin.icon-large { line-height: .8em; } 58 | } 59 | } 60 | .btn.btn-small { 61 | [class^="icon-"], 62 | [class*=" icon-"] { 63 | &.pull-left, &.pull-right { 64 | &.icon-2x { margin-top: .25em; } 65 | } 66 | } 67 | } 68 | .btn.btn-large { 69 | [class^="icon-"], 70 | [class*=" icon-"] { 71 | margin-top: 0; // overrides bootstrap default 72 | &.pull-left, &.pull-right { 73 | &.icon-2x { margin-top: .05em; } 74 | } 75 | &.pull-left.icon-2x { margin-right: .2em; } 76 | &.pull-right.icon-2x { margin-left: .2em; } 77 | } 78 | } 79 | 80 | /* Fixes alignment in nav lists */ 81 | .nav-list [class^="icon-"], 82 | .nav-list [class*=" icon-"] { 83 | line-height: inherit; 84 | } 85 | -------------------------------------------------------------------------------- /botbot/less/font-awesome/core.less: -------------------------------------------------------------------------------- 1 | /* FONT AWESOME CORE 2 | * -------------------------- */ 3 | 4 | [class^="icon-"], 5 | [class*=" icon-"] { 6 | .icon-FontAwesome(); 7 | } 8 | 9 | [class^="icon-"]:before, 10 | [class*=" icon-"]:before { 11 | text-decoration: inherit; 12 | display: inline-block; 13 | speak: none; 14 | } 15 | 16 | /* makes the font 33% larger relative to the icon container */ 17 | .icon-large:before { 18 | vertical-align: -10%; 19 | font-size: 4/3em; 20 | } 21 | 22 | /* makes sure icons active on rollover in links */ 23 | a { 24 | [class^="icon-"], 25 | [class*=" icon-"] { 26 | display: inline; 27 | } 28 | } 29 | 30 | /* increased font size for icon-large */ 31 | [class^="icon-"], 32 | [class*=" icon-"] { 33 | &.icon-fixed-width { 34 | display: inline-block; 35 | width: 16/14em; 36 | text-align: right; 37 | padding-right: 4/14em; 38 | &.icon-large { 39 | width: 20/14em; 40 | } 41 | } 42 | } 43 | 44 | .icons-ul { 45 | margin-left: @icons-li-width; 46 | list-style-type: none; 47 | 48 | > li { position: relative; } 49 | 50 | .icon-li { 51 | position: absolute; 52 | left: -@icons-li-width; 53 | width: @icons-li-width; 54 | text-align: center; 55 | line-height: inherit; 56 | } 57 | } 58 | 59 | // allows usage of the hide class directly on font awesome icons 60 | [class^="icon-"], 61 | [class*=" icon-"] { 62 | &.hide { 63 | display: none; 64 | } 65 | } 66 | 67 | .icon-muted { color: @iconMuted; } 68 | .icon-light { color: @iconLight; } 69 | .icon-dark { color: @iconDark; } 70 | 71 | // Icon Borders 72 | // ------------------------- 73 | 74 | .icon-border { 75 | border: solid 1px @borderColor; 76 | padding: .2em .25em .15em; 77 | .border-radius(3px); 78 | } 79 | 80 | // Icon Sizes 81 | // ------------------------- 82 | 83 | .icon-2x { 84 | font-size: 2em; 85 | &.icon-border { 86 | border-width: 2px; 87 | .border-radius(4px); 88 | } 89 | } 90 | .icon-3x { 91 | font-size: 3em; 92 | &.icon-border { 93 | border-width: 3px; 94 | .border-radius(5px); 95 | } 96 | } 97 | .icon-4x { 98 | font-size: 4em; 99 | &.icon-border { 100 | border-width: 4px; 101 | .border-radius(6px); 102 | } 103 | } 104 | 105 | .icon-5x { 106 | font-size: 5em; 107 | &.icon-border { 108 | border-width: 5px; 109 | .border-radius(7px); 110 | } 111 | } 112 | 113 | 114 | // Floats & Margins 115 | // ------------------------- 116 | 117 | // Quick floats 118 | .pull-right { float: right; } 119 | .pull-left { float: left; } 120 | 121 | [class^="icon-"], 122 | [class*=" icon-"] { 123 | &.pull-left { 124 | margin-right: .3em; 125 | } 126 | &.pull-right { 127 | margin-left: .3em; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /botbot/less/font-awesome/extras.less: -------------------------------------------------------------------------------- 1 | /* EXTRAS 2 | * -------------------------- */ 3 | 4 | /* Stacked and layered icon */ 5 | .icon-stack(); 6 | 7 | /* Animated rotating icon */ 8 | .icon-spin { 9 | display: inline-block; 10 | -moz-animation: spin 2s infinite linear; 11 | -o-animation: spin 2s infinite linear; 12 | -webkit-animation: spin 2s infinite linear; 13 | animation: spin 2s infinite linear; 14 | } 15 | 16 | /* Prevent stack and spinners from being taken inline when inside a link */ 17 | a .icon-stack, 18 | a .icon-spin { 19 | display: inline-block; 20 | text-decoration: none; 21 | } 22 | 23 | @-moz-keyframes spin { 24 | 0% { -moz-transform: rotate(0deg); } 25 | 100% { -moz-transform: rotate(359deg); } 26 | } 27 | @-webkit-keyframes spin { 28 | 0% { -webkit-transform: rotate(0deg); } 29 | 100% { -webkit-transform: rotate(359deg); } 30 | } 31 | @-o-keyframes spin { 32 | 0% { -o-transform: rotate(0deg); } 33 | 100% { -o-transform: rotate(359deg); } 34 | } 35 | @-ms-keyframes spin { 36 | 0% { -ms-transform: rotate(0deg); } 37 | 100% { -ms-transform: rotate(359deg); } 38 | } 39 | @keyframes spin { 40 | 0% { transform: rotate(0deg); } 41 | 100% { transform: rotate(359deg); } 42 | } 43 | 44 | /* Icon rotations and mirroring */ 45 | .icon-rotate-90:before { 46 | -webkit-transform: rotate(90deg); 47 | -moz-transform: rotate(90deg); 48 | -ms-transform: rotate(90deg); 49 | -o-transform: rotate(90deg); 50 | transform: rotate(90deg); 51 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); 52 | } 53 | 54 | .icon-rotate-180:before { 55 | -webkit-transform: rotate(180deg); 56 | -moz-transform: rotate(180deg); 57 | -ms-transform: rotate(180deg); 58 | -o-transform: rotate(180deg); 59 | transform: rotate(180deg); 60 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); 61 | } 62 | 63 | .icon-rotate-270:before { 64 | -webkit-transform: rotate(270deg); 65 | -moz-transform: rotate(270deg); 66 | -ms-transform: rotate(270deg); 67 | -o-transform: rotate(270deg); 68 | transform: rotate(270deg); 69 | filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); 70 | } 71 | 72 | .icon-flip-horizontal:before { 73 | -webkit-transform: scale(-1, 1); 74 | -moz-transform: scale(-1, 1); 75 | -ms-transform: scale(-1, 1); 76 | -o-transform: scale(-1, 1); 77 | transform: scale(-1, 1); 78 | } 79 | 80 | .icon-flip-vertical:before { 81 | -webkit-transform: scale(1, -1); 82 | -moz-transform: scale(1, -1); 83 | -ms-transform: scale(1, -1); 84 | -o-transform: scale(1, -1); 85 | transform: scale(1, -1); 86 | } 87 | 88 | /* ensure rotation occurs inside anchor tags */ 89 | a { 90 | .icon-rotate-90, .icon-rotate-180, .icon-rotate-270, .icon-flip-horizontal, .icon-flip-vertical { 91 | &:before { display: inline-block; } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /botbot/less/font-awesome/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 3.2.1 3 | * the iconic font designed for Bootstrap 4 | * ------------------------------------------------------------------------------ 5 | * The full suite of pictographic icons, examples, and documentation can be 6 | * found at http://fontawesome.io. Stay up to date on Twitter at 7 | * http://twitter.com/fontawesome. 8 | * 9 | * License 10 | * ------------------------------------------------------------------------------ 11 | * - The Font Awesome font is licensed under SIL OFL 1.1 - 12 | * http://scripts.sil.org/OFL 13 | * - Font Awesome CSS, LESS, and SASS files are licensed under MIT License - 14 | * http://opensource.org/licenses/mit-license.html 15 | * - Font Awesome documentation licensed under CC BY 3.0 - 16 | * http://creativecommons.org/licenses/by/3.0/ 17 | * - Attribution is no longer required in Font Awesome 3.0, but much appreciated: 18 | * "Font Awesome by Dave Gandy - http://fontawesome.io" 19 | * 20 | * Author - Dave Gandy 21 | * ------------------------------------------------------------------------------ 22 | * Email: dave@fontawesome.io 23 | * Twitter: http://twitter.com/davegandy 24 | * Work: Lead Product Designer @ Kyruus - http://kyruus.com 25 | */ 26 | 27 | @import "variables.less"; 28 | @import "mixins.less"; 29 | @import "path.less"; 30 | @import "core.less"; 31 | @import "bootstrap.less"; 32 | @import "extras.less"; 33 | @import "icons.less"; 34 | -------------------------------------------------------------------------------- /botbot/less/font-awesome/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .icon(@icon) { 5 | .icon-FontAwesome(); 6 | content: @icon; 7 | } 8 | 9 | .icon-FontAwesome() { 10 | font-family: FontAwesome; 11 | font-weight: normal; 12 | font-style: normal; 13 | text-decoration: inherit; 14 | -webkit-font-smoothing: antialiased; 15 | *margin-right: .3em; // fixes ie7 issues 16 | } 17 | 18 | .border-radius(@radius) { 19 | -webkit-border-radius: @radius; 20 | -moz-border-radius: @radius; 21 | border-radius: @radius; 22 | } 23 | 24 | .icon-stack(@width: 2em, @height: 2em, @top-font-size: 1em, @base-font-size: 2em) { 25 | .icon-stack { 26 | position: relative; 27 | display: inline-block; 28 | width: @width; 29 | height: @height; 30 | line-height: @width; 31 | vertical-align: -35%; 32 | [class^="icon-"], 33 | [class*=" icon-"] { 34 | display: block; 35 | text-align: center; 36 | position: absolute; 37 | width: 100%; 38 | height: 100%; 39 | font-size: @top-font-size; 40 | line-height: inherit; 41 | *line-height: @height; 42 | } 43 | .icon-stack-base { 44 | font-size: @base-font-size; 45 | *line-height: @height / @base-font-size; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /botbot/less/font-awesome/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{FontAwesomePath}/fontawesome-webfont.eot?v=@{FontAwesomeVersion}'); 7 | src: url('@{FontAwesomePath}/fontawesome-webfont.eot?#iefix&v=@{FontAwesomeVersion}') format('embedded-opentype'), 8 | url('@{FontAwesomePath}/fontawesome-webfont.woff?v=@{FontAwesomeVersion}') format('woff'), 9 | url('@{FontAwesomePath}/fontawesome-webfont.ttf?v=@{FontAwesomeVersion}') format('truetype'), 10 | url('@{FontAwesomePath}/fontawesome-webfont.svg#fontawesomeregular?v=@{FontAwesomeVersion}') format('svg'); 11 | // src: url('@{FontAwesomePath}/FontAwesome.otf') format('opentype'); // used when developing fonts 12 | font-weight: normal; 13 | font-style: normal; 14 | } 15 | -------------------------------------------------------------------------------- /botbot/less/globals.less: -------------------------------------------------------------------------------- 1 | @sansFontFamily: "Droid sans", Helvetica, sans-serif; 2 | @serifFontFamily: "Droid Serif", Georgia, serif; 3 | @baseFontSize: 14px; 4 | @baseLineHeight: 22px; 5 | 6 | @backgroundColor: #f6f6f6; 7 | @textColor: #27221e; 8 | @highlight1: #ee632a; 9 | @highlight2: #613016; 10 | @lightColor: #bee1db; 11 | @partialLight: #A9C6C1; 12 | @darkContrast: #5B6266; 13 | @darkContrast2: #3A3E3F; 14 | 15 | @currentColor: @darkContrast; 16 | -------------------------------------------------------------------------------- /botbot/less/nick-colors.less: -------------------------------------------------------------------------------- 1 | .nick-color0 { color: #636aff;} 2 | .nick-color1 { color: #00BA00;} 3 | .nick-color2 { color: #C083F2;} 4 | .nick-color3 { color: #4DEDF0;} 5 | .nick-color4 { color: #E422B2;} 6 | .nick-color5 { color: #EAE87E;} 7 | .nick-color6 { color: #B8DFFF;} 8 | .nick-color7 { color: #75A1FB;} 9 | .nick-color8 { color: #97C74B;} 10 | .nick-color9 { color: #A5EAC4;} 11 | .nick-color10 {color: #FFA6A0; } 12 | .nick-color11 {color: #8E67E7; } 13 | .nick-color12 {color: #FFBB58; } 14 | .nick-color13 {color: #EDD7AD; } 15 | .nick-color14 {color: #FF1676; } 16 | .nick-color15 {color: #706616; } 17 | .nick-color16 {color: #46799c; } 18 | .nick-color17 {color: #80372e; } 19 | .nick-color18 {color: #8F478E; } 20 | .nick-color19 {color: #5b9e4c; } 21 | .nick-color20 {color: #13826c; } 22 | .nick-color21 {color: #b13637; } 23 | .nick-color22 {color: #e45d59; } 24 | .nick-color23 {color: #1b51ae; } 25 | .nick-color24 {color: #4855ac; } 26 | .nick-color25 {color: #7f1d86; } 27 | .nick-color26 {color: #73643f; } 28 | .nick-color27 {color: #0b9578; } 29 | .nick-color28 {color: #569c96; } 30 | .nick-color29 {color: #08465f; } 31 | .nick-color30 { color: #336699; } 32 | .nick-color31 { color: #458521; } -------------------------------------------------------------------------------- /botbot/less/timeline.less: -------------------------------------------------------------------------------- 1 | .timeline-navigation { 2 | overflow: hidden; 3 | 4 | ul { margin: 0; padding: 0; } 5 | 6 | 7 | li:before { 8 | content: ""; 9 | position: absolute; 10 | 11 | height: 100%; 12 | 13 | width: 2px; 14 | top: 0; 15 | left: 7px; 16 | 17 | background: @partialLight; 18 | 19 | display: block; 20 | z-index: -1; 21 | } 22 | 23 | li { 24 | list-style: none; 25 | position: relative; 26 | z-index: 10; 27 | } 28 | 29 | // The very first element should adjust its line. 30 | .older:before, 31 | .older + .year:before { 32 | height: 100%; 33 | top: 1em; 34 | } 35 | 36 | & > ul > li:last-of-type:before { 37 | height: 50%; 38 | bottom: 50%; 39 | } 40 | 41 | a { 42 | background: url(../img/timeline-circle-large.svg) no-repeat left center; 43 | background-size: 16px 16px; 44 | display: inline-block; 45 | vertical-align: middle; 46 | font-size: 15px; 47 | line-height: 2; 48 | padding: 2px 6px 2px 22px; 49 | text-shadow: 0 1px 0 #fff; 50 | } 51 | 52 | .active { 53 | a { 54 | background-image: url(../img/timeline-circle-selected-large.svg); 55 | font-weight: 600; 56 | 57 | &:hover { text-decoration: none; } 58 | } 59 | 60 | span { 61 | .border-radius(4px); 62 | 63 | background: @currentColor; 64 | color: #fff; 65 | padding: 2px 6px; 66 | text-shadow: none; 67 | } 68 | } 69 | 70 | .older a { 71 | background-image: url(../img/timeline-more.svg); 72 | } 73 | 74 | .year ul { display: none; } 75 | 76 | .month a { font-size: 13px; line-height: 24px; } 77 | 78 | .month a { 79 | background-image: url(../img/timeline-circle-small.svg); 80 | background-position: 2px center; 81 | background-size: 12px 12px; 82 | } 83 | 84 | .month.active a { 85 | background-image: url(../img/timeline-circle-selected-small.svg); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /botbot/settings/__init__.py: -------------------------------------------------------------------------------- 1 | from base import * 2 | -------------------------------------------------------------------------------- /botbot/settings/_asset_pipeline.py: -------------------------------------------------------------------------------- 1 | PIPELINE_CSS_COMPRESSOR = 'pipeline.compressors.yui.YUICompressor' 2 | PIPELINE_JS_COMPRESSOR = 'pipeline.compressors.yui.YUICompressor' 3 | 4 | PIPELINE_CSS = { 5 | 'screen': { 6 | 'source_filenames': ('css/screen.css',), 7 | 'output_filename': 'screen.css', 8 | 'extra_context': {'media': 'screen,projection'}, 9 | }, 10 | 'landing': { 11 | 'source_filenames': ('css/landing.css',), 12 | 'output_filename': 'landing.css', 13 | 'extra_context': {'media': 'screen,projection'}, 14 | }, 15 | 'howto': { 16 | 'source_filenames': ('css/howto.css',), 17 | 'output_filename': 'howto.css', 18 | 'extra_context': {'media': 'screen,projection'}, 19 | }, 20 | } 21 | 22 | PIPELINE_JS = { 23 | 'app': { 24 | 'source_filenames': ( 25 | 'js/vendor/andlog.js', 26 | 'js/vendor/jquery-1.8.2.js', 27 | 'js/vendor/bootstrap.js', 28 | 'js/vendor/moment.js', 29 | 'js/vendor/detect_timezone.js', 30 | 'js/vendor/jquery.highlight.js', 31 | 'js/vendor/waypoints.js', 32 | 'js/vendor/underscore.js', 33 | 'js/vendor/backbone.js', 34 | 'js/app/common.js', 35 | 'js/app/app.js', 36 | 'js/app/logs/default.js', 37 | ), 38 | 'output_filename': 'app.js', 39 | }, 40 | 'manage_channel': { 41 | 'source_filenames': ( 42 | 'js/vendor/handlebars.js', 43 | 'js/vendor/jquery-ui-1.9.1.custom.js', 44 | 'js/app/manage/default.js', 45 | 'js/app/manage/models.js', 46 | ), 47 | 'output_filename': 'channel.js', 48 | }, 49 | } 50 | -------------------------------------------------------------------------------- /botbot/static/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/static/.gitignore -------------------------------------------------------------------------------- /botbot/static/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/static/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /botbot/static/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/static/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /botbot/static/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/static/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /botbot/static/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/static/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /botbot/static/img/glyphicons-halflings-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/static/img/glyphicons-halflings-white.png -------------------------------------------------------------------------------- /botbot/static/img/glyphicons-halflings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/botbot/static/img/glyphicons-halflings.png -------------------------------------------------------------------------------- /botbot/static/img/timeline-circle-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ]> 6 | 10 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /botbot/static/img/timeline-circle-selected-large.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ]> 6 | 10 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /botbot/static/img/timeline-circle-selected-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | ]> 5 | 9 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /botbot/static/img/timeline-circle-small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | ]> 6 | 10 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /botbot/static/img/timeline-more.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | 11 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /botbot/static/js/app/app.js: -------------------------------------------------------------------------------- 1 | var $$ = $$ || {}; 2 | _.extend($$, Backbone.Events); 3 | $$.Models = $$.Models || {}; 4 | $$.Views = $$.Views || {}; 5 | $$.Collections = $$.Collections || {}; 6 | $$.Templates = $$.Templates || {}; 7 | -------------------------------------------------------------------------------- /botbot/static/js/app/common.js: -------------------------------------------------------------------------------- 1 | // usage: log('inside coolFunc',this,arguments); 2 | // http://paulirish.com/2009/log-a-lightweight-wrapper-for-consolelog/ 3 | window.log = function () { 4 | log.history = log.history || []; // store logs to an array for reference 5 | log.history.push(arguments); 6 | if (this.console) { 7 | console.log(Array.prototype.slice.call(arguments)); 8 | } 9 | }; 10 | 11 | // Prevent default hash jump in browsers 12 | if (location.hash) { 13 | window.scrollTo(0, 0); 14 | setTimeout(function() { 15 | window.scrollTo(0, 0); 16 | }, 0); 17 | } 18 | -------------------------------------------------------------------------------- /botbot/static/js/app/manage/default.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function () { 2 | 3 | $('form#chatbot input[name^="cb-irc"]') 4 | .parent().show() 5 | .siblings('.errorlist').show(); 6 | 7 | //Autocomplete 8 | $("#backbone-user-search").autocomplete({ 9 | source: $("#backbone-user-search").data('url'), 10 | minLength: 2, 11 | select: function (event, ui) { 12 | event.preventDefault(); 13 | var user = new $$.Models.User({ 14 | 'email': ui.item.label, 15 | 'id': ui.item.value 16 | }); 17 | if ($$.manager.user_view.collection.indexOf(user) === -1) { 18 | $$.manager.user_view.collection.add(user); 19 | } 20 | $('#backbone-user-search').val(''); 21 | }, 22 | focus: function (event, ui) { event.preventDefault(); } 23 | }); 24 | 25 | }); 26 | -------------------------------------------------------------------------------- /botbot/static/js/app/manage/models.js: -------------------------------------------------------------------------------- 1 | $$.Models.User = Backbone.Model.extend({}); 2 | 3 | $$.Collections.Users = Backbone.Collection.extend({ 4 | model: $$.Models.User 5 | }); 6 | 7 | $$.Views.UserListView = Backbone.View.extend({ 8 | template_source: "#user-list-template", 9 | input_source: "#user-input-template", 10 | 11 | events: { 12 | 'click .delete-user': 'deleteUser' 13 | }, 14 | 15 | initialize: function (options) { 16 | _.bindAll(this, "render", "deleteUser"); 17 | 18 | this.template = Handlebars.compile($(this.template_source).html()); 19 | this.input = Handlebars.compile($(this.input_source).html()); 20 | 21 | this.collection.on("add remove", this.render, this); 22 | this.render(); 23 | }, 24 | 25 | render: function () { 26 | // Load the compiled HTML into the Backbone "el" 27 | this.$('#backbone-user-list').html( 28 | this.template({'users': this.collection.toJSON()}) 29 | ); 30 | this.$('#input-el').html(this.input({'users': this.collection.toJSON()})); 31 | }, 32 | 33 | deleteUser: function (event) { 34 | event.preventDefault(); 35 | this.collection.remove($(event.target).data('id')); 36 | } 37 | 38 | }); 39 | 40 | 41 | $(document).ready(function () { 42 | 43 | $$.manager = (function () { 44 | 45 | var initial_users = new $$.Collections.Users($.parseJSON($('#initial-users').html())); 46 | 47 | return { 48 | user_view: new $$.Views.UserListView({ 49 | el: $("#backbone-users"), 50 | collection: initial_users 51 | }) 52 | }; 53 | 54 | }()); 55 | 56 | }); 57 | -------------------------------------------------------------------------------- /botbot/static/js/vendor/andlog.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * https://github.com/HenrikJoreteg/andlog 3 | */ 4 | (function (window) { 5 | var ls = window.localStorage, 6 | out = {}, 7 | inNode = typeof process !== 'undefined'; 8 | 9 | if (inNode) { 10 | module.exports = console; 11 | return; 12 | } 13 | 14 | if (ls && ls.debug && window.console) { 15 | out = window.console; 16 | } else { 17 | var methods = "assert,count,debug,dir,dirxml,error,exception,group,groupCollapsed,groupEnd,info,log,markTimeline,profile,profileEnd,time,timeEnd,trace,warn".split(","), 18 | l = methods.length, 19 | fn = function () {}; 20 | 21 | while (l--) { 22 | out[methods[l]] = fn; 23 | } 24 | } 25 | if (typeof exports !== 'undefined') { 26 | module.exports = out; 27 | } else { 28 | window.console = out; 29 | } 30 | })(this); -------------------------------------------------------------------------------- /botbot/static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: /*/msg/ 3 | Disallow: /eventsource/ 4 | Disallow: /*/*/search/ 5 | -------------------------------------------------------------------------------- /botbot/templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head_title %}Page Not Found {{ super() }}{% endblock %} 4 | 5 | {% block header %}{% endblock %} 6 | 7 | {% block body_class %}error-view{% endblock %} 8 | 9 | {% block content %} 10 |
    11 |
    12 |

    Page Not Found

    13 |

    Head to the homepage.

    14 |
    15 |
    16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /botbot/templates/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Internal Server Error 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
    20 |
    21 | 22 |
    23 |

    Internal Server Ever

    24 |
    25 |
    26 |
    27 | 28 | 29 | -------------------------------------------------------------------------------- /botbot/templates/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | {% block head_title %} | BotBot.me [o__o]{% endblock %} 10 | 11 | {# Defined in botbot.settings._asset_pipeline #} 12 | {% stylesheet "screen" %} 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | {% block extra_head %}{% endblock %} 23 | 24 | 25 | 26 | {% block header %} 27 | {% include "includes/header.html" %} 28 | {% endblock %} 29 | 30 | {% block content_outer %} 31 |
    32 | {% block content %}{% endblock %} 33 |
    34 | {% endblock %} 35 | 36 | {% block footer %} 37 | {% include "includes/footer.html" %} 38 | {% endblock %} 39 | 40 | {# Defined in botbot.settings._asset_pipeline #} 41 | {% javascript "app" %} 42 | 43 | {% block extra_js %}{% endblock extra_js %} 44 | 45 | {% include "includes/google-analytics.html" %} 46 | 47 | 48 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/button.html: -------------------------------------------------------------------------------- 1 | {% if icon_class %} {% endif %}{{ text }} -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field.html: -------------------------------------------------------------------------------- 1 | {% if field.is_hidden %} 2 | {{ field }} 3 | {% else %} 4 | {% include "bootstrap_toolkit/field_visible.html" %} 5 | {% endif %} 6 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_checkbox.html: -------------------------------------------------------------------------------- 1 | 5 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_choices.html: -------------------------------------------------------------------------------- 1 | {{ choices_start }} 2 | {% for choice_id, choice_label in field.field.choices %} 3 | 17 | {% if not loop.last and choices_separator > "" and loop.index is divisibleby(choices_group) %} 18 | {{ choices_separator }} 19 | {% endif %} 20 | {% endfor %} 21 | {{ choices_end }} 22 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_default.html: -------------------------------------------------------------------------------- 1 | {% if field.field.widget.bootstrap %} 2 | {% with bootstrap=field.field.widget.bootstrap %} 3 | {% if prepend %} 4 | {% if append %} 5 | {% include "bootstrap_toolkit/field_prepend_append.html" %} 6 | {% else %} 7 | {% with append=bootstrap.append %} 8 | {% include "bootstrap_toolkit/field_prepend_append.html" %} 9 | {% endwith %} 10 | {% endif %} 11 | {% else %} 12 | {% if append %} 13 | {% with prepend=bootstrap.prepend %} 14 | {% include "bootstrap_toolkit/field_prepend_append.html" %} 15 | {% endwith %} 16 | {% else %} 17 | {% with prepend=bootstrap.prepend, append=bootstrap.append %} 18 | {% include "bootstrap_toolkit/field_prepend_append.html" %} 19 | {% endwith %} 20 | {% endif %} 21 | {% endif %} 22 | {% endwith %} 23 | {% else %} 24 | {% include "bootstrap_toolkit/field_prepend_append.html" %} 25 | {% endif %} 26 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_errors.html: -------------------------------------------------------------------------------- 1 | {% if field.errors %} 2 | {% for error in field.errors %} 3 | {% if display == "inline" %} 4 | {{ error }} 5 | {% else %} 6 |

    {{ error }}

    7 | {% endif %} 8 | {% endfor %} 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_help.html: -------------------------------------------------------------------------------- 1 | {% if field.help_text %} 2 | {% autoescape false %} 3 | {% if display == "inline" %} 4 | {{ field.help_text }} 5 | {% else %} 6 |

    {{ field.help_text }}

    7 | {% endif %} 8 | {% endautoescape %} 9 | {% endif %} 10 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_horizontal.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if input_type != "checkbox" %} 3 | 4 | {% endif %} 5 |
    6 | {% if input_type == "checkbox" %} 7 | {% include "bootstrap_toolkit/field_checkbox.html" %} 8 | {% with display="inline" %} 9 | {% include "bootstrap_toolkit/field_errors.html" %} 10 | {% endwith %} 11 | {% elif input_type == "multicheckbox" %} 12 | {% with type="checkbox" %} 13 | {% include "bootstrap_toolkit/field_choices.html" %} 14 | {% endwith %} 15 | {% with display="block" %} 16 | {% include "bootstrap_toolkit/field_errors.html" %} 17 | {% endwith %} 18 | {% elif input_type == "radioset" %} 19 | {% with type="radio" %} 20 | {% include "bootstrap_toolkit/field_choices.html" %} 21 | {% endwith %} 22 | {% with display="block" %} 23 | {% include "bootstrap_toolkit/field_errors.html" %} 24 | {% endwith %} 25 | {% else %} 26 | {% include "bootstrap_toolkit/field_default.html" %} 27 | {% with display="inline" %} 28 | {% include "bootstrap_toolkit/field_errors.html" %} 29 | {% endwith %} 30 | {% endif %} 31 | {% with display="block" %} 32 | {% include "bootstrap_toolkit/field_help.html" %} 33 | {% endwith %} 34 |
    35 |
    36 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_inline.html: -------------------------------------------------------------------------------- 1 | 2 | {% if input_type == "checkbox" %} 3 | {% include "bootstrap_toolkit/field_checkbox.html" %} 4 | {% with display="inline" %} 5 | {% include "bootstrap_toolkit/field_help.html" %} 6 | {% endwith %} 7 | {% elif input_type == "multicheckbox" %} 8 | {% with type="checkbox" %} 9 | {% include "bootstrap_toolkit/field_choices.html" %} 10 | {% endwith %} 11 | {% with display="block" %} 12 | {% include "bootstrap_toolkit/field_help.html" %} 13 | {% endwith %} 14 | {% else %} 15 | {% if field.label %} 16 | 17 | {% endif %} 18 | {% if input_type == "radioset" %} 19 | {% with type="radio" %} 20 | {% include "bootstrap_toolkit/field_choices.html" %} 21 | {% endwith %} 22 | {% with display="block" %} 23 | {% include "bootstrap_toolkit/field_help.html" %} 24 | {% endwith %} 25 | {% else %} 26 | {% with display="inline" %} 27 | {% include "bootstrap_toolkit/field_default.html" %} 28 | {% include "bootstrap_toolkit/field_help.html" %} 29 | {% endwith %} 30 | {% endif %} 31 | {% endif %} 32 | {% with display="inline" %} 33 | {% include "bootstrap_toolkit/field_errors.html" %} 34 | {% endwith %} 35 | 36 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_prepend_append.html: -------------------------------------------------------------------------------- 1 | {% if prepend %} 2 | {% if append %} 3 |
    4 | {{ prepend }}{{ field }}{{ append }} 5 |
    6 | {% else %} 7 |
    8 | {{ prepend }}{{ field }} 9 |
    10 | {% endif %} 11 | {% else %} 12 | {% if append %} 13 |
    14 | {{ field }}{{ append }} 15 |
    16 | {% else %} 17 | {{ field }} 18 | {% endif %} 19 | {% endif %} 20 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_search.html: -------------------------------------------------------------------------------- 1 | {% include "bootstrap_toolkit/field_inline.html" %} 2 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_vertical.html: -------------------------------------------------------------------------------- 1 |
    2 | {% if input_type == "checkbox" %} 3 |
    4 | {% include "bootstrap_toolkit/field_checkbox.html" %} 5 | {% with display="inline" %} 6 | {% include "bootstrap_toolkit/field_help.html" %} 7 | {% endwith %} 8 | {% with display="block" %} 9 | {% include "bootstrap_toolkit/field_errors.html" %} 10 | {% endwith %} 11 |
    12 | {% elif input_type == "multicheckbox" %} 13 | 14 |
    15 | {% with type="checkbox" %} 16 | {% include "bootstrap_toolkit/field_choices.html" %} 17 | {% endwith %} 18 | {% with display="block" %} 19 | {% include "bootstrap_toolkit/field_help.html" %} 20 | {% include "bootstrap_toolkit/field_errors.html" %} 21 | {% endwith %} 22 | 23 |
    24 | {% elif input_type == "radioset" %} 25 | 26 |
    27 | {% with type="radio" %} 28 | {% include "bootstrap_toolkit/field_choices.html" %} 29 | {% endwith %} 30 | {% with display="block" %} 31 | {% include "bootstrap_toolkit/field_help.html" %} 32 | {% include "bootstrap_toolkit/field_errors.html" %} 33 | {% endwith %} 34 |
    35 | {% else %} 36 | 37 |
    38 | {% include "bootstrap_toolkit/field_default.html" %} 39 | {% with display="inline" %} 40 | {% include "bootstrap_toolkit/field_help.html" %} 41 | {% endwith %} 42 | {% with display="block" %} 43 | {% include "bootstrap_toolkit/field_errors.html" %} 44 | {% endwith %} 45 |
    46 | {% endif %} 47 |
    48 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/field_visible.html: -------------------------------------------------------------------------------- 1 | {% with input_type=bootstrap_input_type(field) %} 2 | {% if layout == "horizontal" %} 3 | {% include "bootstrap_toolkit/field_horizontal.html" %} 4 | {% elif layout == "inline" %} 5 | {% include "bootstrap_toolkit/field_inline.html" %} 6 | {% elif layout == "search" %} 7 | {% include "bootstrap_toolkit/field_search.html" %} 8 | {% else %} 9 | {% include "bootstrap_toolkit/field_vertical.html" %} 10 | {% endif %} 11 | {% endwith %} 12 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/form.html: -------------------------------------------------------------------------------- 1 | {% if form.non_field_errors() %} 2 | {% include "bootstrap_toolkit/non_field_errors.html" %} 3 | {% endif %} 4 | 5 | {% for field in form.hidden_fields() %} 6 | {% for error in field.errors %} 7 | {% include "bootstrap_toolkit/non_field_error.html" %} 8 | {% endfor %} 9 | {% endfor %} 10 | 11 | {% set exclude_fields = exclude and exclude.split(',')|map(lower) or [] %} 12 | {% for field in form %} 13 | {% if not field.name.lower() in exclude_fields %} 14 | {% if not fields or field.name.lower() in fields.split(',')|map(lower) %} 15 | {% include "bootstrap_toolkit/field.html" %} 16 | {% endif %} 17 | {% endif %} 18 | {% endfor %} 19 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/formset.html: -------------------------------------------------------------------------------- 1 | {% load bootstrap_toolkit %} 2 | 3 | {{ formset.management_form }} 4 | 5 | {% for form in formset %} 6 | {% with form=form, layout=layout|default("inline") %} 7 | {% include "bootstrap_toolkit/form.html" %} 8 | {% endwith %} 9 | {% endfor %} 10 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/icon.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/messages.html: -------------------------------------------------------------------------------- 1 | {% for message in messages %} 2 |
    3 | × 4 | {{ message }} 5 |
    6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/nav.html: -------------------------------------------------------------------------------- 1 | {%- for tab in tabs %} 2 | {{ tab.title }}
  • 3 | {% endfor %} 4 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/non_field_error.html: -------------------------------------------------------------------------------- 1 |

    {{ error }}

    -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/non_field_errors.html: -------------------------------------------------------------------------------- 1 |
    2 | {% for error in form.non_field_errors() %} 3 | {% include "bootstrap_toolkit/non_field_error.html" %} 4 | {% endfor %} 5 |
    6 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/pagination.html: -------------------------------------------------------------------------------- 1 | {% with bootstrap_pagination_url=bootstrap_pagination_url|default:"?" %} 2 | 3 |
    4 |
      5 | 6 |
    • 7 | « 8 |
    • 9 | 10 | {% if pages_back %} 11 |
    • 12 | 13 |
    • 14 | {% endif %} 15 | 16 | {% for p in pages_shown %} 17 | 18 | {{ p }} 19 | 20 | {% endfor %} 21 | 22 | {% if pages_forward %} 23 |
    • 24 | 25 |
    • 26 | {% endif %} 27 | 28 |
    • 29 | » 30 |
    • 31 | 32 |
    33 | 34 |
    35 | 36 | {% endwith %} 37 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/pills.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /botbot/templates/bootstrap_toolkit/tabs.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /botbot/templates/channel_list.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head_title %}Select a Channel {{ super() }}{% endblock %} 4 | 5 | {% block content %} 6 |

    Public Channels

    7 | {% if public_channels %} 8 |
      9 | {% for channel in public_channels %} 10 |
    • 11 | {{ channel }} 12 |
    • 13 | {% endfor %} 14 |
    15 | {% endif %} 16 | {% endblock content %} 17 | -------------------------------------------------------------------------------- /botbot/templates/home.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block content %} 4 |
    5 |

    Featured channels

    6 |
      7 | {% cache 600 "signup-featured-channels-list" %} 8 | {% for channel in featured_channels %} 9 |
    • {{ channel.name }}
    • 10 | {% endfor %} 11 | {% endcache %} 12 |
    13 |
    14 | 15 |
      16 | {% cache 600 "signup-public-channels" %} 17 | {% for channel in public_not_featured_channels %} 18 |
    • {{ channel.name }}
    • 19 | {% endfor %} 20 | {% endcache %} 21 |
    22 | {% endblock content %} 23 | -------------------------------------------------------------------------------- /botbot/templates/includes/checkbox.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 7 | 8 | {% for error in field.errors %} 9 |

    {{ error }}

    10 | {% endfor %} 11 | 12 |
    13 | {% with help_text=help_text or field.help_text %} 14 | {% if help_text %} 15 | {{ help_text }} 16 | {% endif %} 17 | {% endwith %} 18 |
    19 |
    20 | -------------------------------------------------------------------------------- /botbot/templates/includes/field.html: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 6 | 7 |
    8 | {{ field }} 9 | {% if help_text %} 10 | {{ help_text }} 11 | {% elif field.help_text %} 12 | {{ field.help_text }} 13 | {% endif %} 14 | {% for error in field.errors %} 15 |

    {{ error }}

    16 | {% endfor %} 17 |
    18 |
    19 | -------------------------------------------------------------------------------- /botbot/templates/includes/footer.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /botbot/templates/includes/google-analytics.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /botbot/templates/includes/header.html: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /botbot/templates/launchpad/signup.html: -------------------------------------------------------------------------------- 1 | {% include "home.html" %} 2 | -------------------------------------------------------------------------------- /botbot/templates/launchpad/success.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

    Right on, bro!

    7 | 8 | 9 | -------------------------------------------------------------------------------- /botbot/templates/launchpad/unsubscribe.html: -------------------------------------------------------------------------------- 1 | {% if member.is_subscribed %} 2 |
    {{ csrf_input }} 3 |

    Unsubscribe {{ member.email }}?

    4 |
    5 | {% else %} 6 |

    {{ member.email }} was unsubscribed

    7 | {% endif %} -------------------------------------------------------------------------------- /botbot/templates/logs/help.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | {% block head_title %}Documentation for {{ channel }} {{ super() }}{% endblock %} 3 | {% block body_class %}help-view{% endblock %} 4 | 5 | {% block content %} 6 | 7 |

    ← Return to {{ channel }} 8 | 9 |

    Help for {{ channel }}

    10 | 11 |
    12 | {% for plugin in channel.plugins.all() %} 13 |

    {{ plugin }}

    14 |
    {{ plugin_docs(plugin, channel)|safe }}
    15 | {% endfor %} 16 |
    17 | 18 | {% endblock content %} 19 | -------------------------------------------------------------------------------- /botbot/templates/logs/kudos.html: -------------------------------------------------------------------------------- 1 | {% extends "base.html" %} 2 | 3 | {% block head_title %}Documentation for {{ channel }} {{ super() }}{% endblock %} 4 | {% block body_class %}help-view{% endblock %} 5 | 6 | {% block content %} 7 | 8 |

    ← Return to {{ channel }} 9 | 10 |

    Kudos

    11 | 12 |
    13 | 14 |

    Kudos is a way of identifying helpful individuals in a channel.

    15 |

    By noting appreciation from participants, we can acknowledge the good work of those who devote so much time helping others be successful.

    16 | 17 | {% if search %} 18 |

    {{ search.nick }}

    19 | {% if search.details %} 20 |

    {{ search.nick }} is a {% if search.details.current_perc %}top {{ search.details.current_perc }}% {% endif %}helpful individual! 21 | {% else %} 22 |

    Botbot hasn't tracked any appreciation for this individual in {{ channel }} yet.

    23 | {% endif %} 24 | {% endif %} 25 | 26 | {# 27 |
    28 |

    29 |
    30 | #} 31 | 32 | {% if random_scoreboard %} 33 |

    Some{% if search %} other{% endif %} helpful people

    34 |

    Here's an sample of some of the helpful individuals in {{ channel }}:

    35 | 44 | {% endif %} 45 | 46 | {% if channel_messages %} 47 |

    Botbot has scanned {{ channel_messages }} lines of discussion in {{ channel }}, of which {{ channel_kudos_perc }} are messages of appreciation.

    48 | {% endif %} 49 | 50 |
    51 | 52 | {% endblock content %} 53 | -------------------------------------------------------------------------------- /botbot/templates/logs/log_display.html: -------------------------------------------------------------------------------- 1 | {%- for line in message_list %} 2 | {%- if line.timestamp.date() != last_date|default('') %} 3 | {%- if show_first_header or not loop.first %} 4 |

    {{ line.timestamp.strftime("%F %j%S, %Y") }}

    5 | {%- endif %} 6 | {%- endif %} 7 |
  • 8 | 9 | 10 | 11 | 12 | {%- if line.action %} 13 |
    {{ line.nick }} {{ bbme_urlizetrunc(line.text|e, 50) }}
    14 | {%- elif line.command == "PRIVMSG" %} 15 | {%- if line.nick %} 16 | {%- if line.nick != last_nick|default('') %} 17 |
    {{ line.nick }}
    18 | {%- endif %} 19 | {%- set last_nick = line.nick %} 20 |
    {{ bbme_urlizetrunc(line.text|e, 50) }}
    21 | {%- else %} 22 |
    {{ bbme_urlizetrunc(line.text|e, 50) }}
    23 | {%- endif %} 24 | {%- else %} 25 | {# JOIN, PART, QUIT, NICK, etc #} 26 |
    {{line}}
    27 | {%- endif %} 28 |
  • 29 | 30 | {%- set last_date = line.timestamp.date() %} 31 | {% endfor %} 32 | -------------------------------------------------------------------------------- /botbot/templates/logs/logs.txt: -------------------------------------------------------------------------------- 1 | {%- autoescape false %} 2 | {%- for line in message_list %} 3 | {%- if line.action -%} 4 | [{{ line.timestamp.strftime("%H:%M:%S") }}] * {{ line.nick }} {{ line.text }} 5 | {% elif line.command == "PRIVMSG" -%} 6 | [{{ line.timestamp.strftime("%H:%M:%S") }}] {% if line.nick %}<{{ line.nick }}>{% else %}*{% endif %} {{ line.text }} 7 | {% else -%} 8 | [{{ line.timestamp.strftime("%H:%M:%S") }}] * {{ line }} 9 | {% endif -%} 10 | {% endfor -%} 11 | {% endautoescape %} 12 | -------------------------------------------------------------------------------- /botbot/urls/__init__.py: -------------------------------------------------------------------------------- 1 | from .base import * 2 | -------------------------------------------------------------------------------- /botbot/urls/admin.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import patterns, include 2 | from django.contrib import admin 3 | 4 | admin.autodiscover() 5 | 6 | urlpatterns = patterns('', 7 | (r'^admin/doc/', include('django.contrib.admindocs.urls')), 8 | (r'^admin/', include(admin.site.urls)), 9 | ) 10 | -------------------------------------------------------------------------------- /botbot/urls/base.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.conf.urls import patterns, url, include 3 | 4 | from botbot.apps.bots.views import ChannelList 5 | from botbot.apps.preview.views import LandingPage 6 | 7 | channel_patterns = patterns('', 8 | url(r'', include('botbot.apps.logs.urls')), 9 | ) 10 | 11 | urlpatterns = patterns('', 12 | (r'^$', LandingPage.as_view()), 13 | url(r'^sitemap\.xml$', include('botbot.apps.sitemap.urls')), 14 | 15 | url(r'^(?P[\-\w\:\.]+(\@[\w]+)?)/(?P[\-\w\.]+)/', 16 | include(channel_patterns)), 17 | url(r'^(?P[\-\w\.]+)/$', ChannelList.as_view()) 18 | ) 19 | 20 | if settings.INCLUDE_DJANGO_ADMIN: 21 | from .admin import urlpatterns as admin_urlpatterns 22 | # Prepend the admin urls. 23 | urlpatterns = admin_urlpatterns + urlpatterns 24 | 25 | if settings.DEBUG: 26 | urlpatterns += patterns('django.shortcuts', 27 | url(r'^404/$', 'render', {'template_name': '404.html'}), 28 | url(r'^500/$', 'render', {'template_name': '500.html'}), 29 | ) 30 | -------------------------------------------------------------------------------- /botbot/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "botbot.settings") 4 | 5 | # This application object is used by any WSGI server configured to use this 6 | # file. This includes Django's development server, if the WSGI_APPLICATION 7 | # setting points here. 8 | from django.core.wsgi import get_wsgi_application 9 | application = get_wsgi_application() 10 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | 9 | # Internal variables. 10 | PAPEROPT_a4 = -D latex_paper_size=a4 11 | PAPEROPT_letter = -D latex_paper_size=letter 12 | ALLSPHINXOPTS = -d _build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 13 | 14 | .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest 15 | 16 | help: 17 | @echo "Please use \`make ' where is one of" 18 | @echo " html to make standalone HTML files" 19 | @echo " dirhtml to make HTML files named index.html in directories" 20 | @echo " pickle to make pickle files" 21 | @echo " json to make JSON files" 22 | @echo " htmlhelp to make HTML files and a HTML help project" 23 | @echo " qthelp to make HTML files and a qthelp project" 24 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 25 | @echo " changes to make an overview of all changed/added/deprecated items" 26 | @echo " linkcheck to check all external links for integrity" 27 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 28 | 29 | clean: 30 | -rm -rf _build/* 31 | 32 | html: 33 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) _build/html 34 | @echo 35 | @echo "Build finished. The HTML pages are in _build/html." 36 | 37 | dirhtml: 38 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) _build/dirhtml 39 | @echo 40 | @echo "Build finished. The HTML pages are in _build/dirhtml." 41 | 42 | pickle: 43 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) _build/pickle 44 | @echo 45 | @echo "Build finished; now you can process the pickle files." 46 | 47 | json: 48 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) _build/json 49 | @echo 50 | @echo "Build finished; now you can process the JSON files." 51 | 52 | htmlhelp: 53 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) _build/htmlhelp 54 | @echo 55 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 56 | ".hhp project file in _build/htmlhelp." 57 | 58 | qthelp: 59 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) _build/qthelp 60 | @echo 61 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 62 | ".qhcp project file in _build/qthelp, like this:" 63 | @echo "# qcollectiongenerator _build/qthelp/project.qhcp" 64 | @echo "To view the help file:" 65 | @echo "# assistant -collectionFile _build/qthelp/project.qhc" 66 | 67 | latex: 68 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) _build/latex 69 | @echo 70 | @echo "Build finished; the LaTeX files are in _build/latex." 71 | @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ 72 | "run these through (pdf)latex." 73 | 74 | changes: 75 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) _build/changes 76 | @echo 77 | @echo "The overview file is in _build/changes." 78 | 79 | linkcheck: 80 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) _build/linkcheck 81 | @echo 82 | @echo "Link check complete; look for any errors in the above output " \ 83 | "or in _build/linkcheck/output.txt." 84 | 85 | doctest: 86 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) _build/doctest 87 | @echo "Testing of doctests in the sources finished, look at the " \ 88 | "results in _build/doctest/output.txt." 89 | -------------------------------------------------------------------------------- /docs/images/botbot-architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BotBotMe/botbot-web/0ada6213b5f1d8bb0f71eb79aaf37704f4903564/docs/images/botbot-architecture.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Welcome to BotBot's documentation! 2 | ===================================== 3 | 4 | Contents: 5 | 6 | .. toctree:: 7 | :maxdepth: 2 8 | 9 | install 10 | getting_started 11 | managing_bots 12 | developers 13 | plugins 14 | irc_resources 15 | troubleshooting 16 | 17 | Indices and tables 18 | ================== 19 | 20 | * :ref:`genindex` 21 | * :ref:`modindex` 22 | * :ref:`search` 23 | 24 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | Installation 3 | ================== 4 | 5 | Pre-requisites 6 | --------------- 7 | 8 | Some of the suggested commands that follow may require root privileges on your system. 9 | 10 | Python 11 | ~~~~~~~ 12 | 13 | * Python 2.7 14 | 15 | Postgresql with hStore extension 16 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 17 | 18 | * **OS X**: 19 | 20 | * Homebrew: installed by default 21 | * Postgres.app: installed by default 22 | 23 | * **Ubuntu**: ``apt-get install postgresql-contrib-9.1 postgresql-server-dev-9.1 python-dev virtualenv`` 24 | 25 | Go 26 | ~~ 27 | 28 | Version 1.2 or higher required 29 | 30 | * **OS X**: ``brew install go`` 31 | * **Ubuntu**: ``apt-get install golang-go`` 32 | 33 | Redis 34 | ~~~~~ 35 | 36 | * **OS X**: ``brew install redis`` 37 | * **Ubuntu**: ``apt-get install redis-server`` 38 | 39 | Install 40 | -------- 41 | 42 | Run in a terminal: 43 | 44 | .. code-block:: bash 45 | 46 | virtualenv botbot && source botbot/bin/activate 47 | pip install -e git+https://github.com/BotBotMe/botbot-web.git#egg=botbot 48 | cd $VIRTUAL_ENV/src/botbot 49 | 50 | # This builds the project environment and will run for at least several minutes 51 | make dependencies 52 | 53 | # Adjust ``.env`` file if necessary. Defaults are chosen for local debug environments. 54 | # If your Postgres server requires a password, you'll need to override STORAGE_URL 55 | # The default database name is 'botbot' 56 | $EDITOR .env 57 | 58 | # Make the variables available to subprocesses 59 | export $(cat .env | grep -v ^# | xargs) 60 | 61 | createdb botbot 62 | echo "create extension hstore" | psql botbot 63 | manage.py migrate 64 | 65 | # You'll need a staff account for creating a bot and registering channels 66 | manage.py createsuperuser 67 | 68 | Redis needs to be running prior to starting the BotBot services. For example: 69 | 70 | .. code-block:: bash 71 | 72 | redis-server 73 | 74 | Then, to run all the services defined in ``Procfile``: 75 | 76 | .. code-block:: bash 77 | 78 | honcho start 79 | 80 | .. note:: `foreman `_ will also work if you have the gem or Heroku toolbelt installed. 81 | 82 | You should now be able to access the site at ``http://localhost:8000``. Log in with the username you created. 83 | 84 | See :doc:`getting_started` for instructions on configuring a bot. 85 | 86 | If you plan make code changes, please read through the :doc:`developers` doc. 87 | 88 | If you plan to run BotBot in a production environment please read the :doc:`production` doc. 89 | 90 | 91 | Running Tests 92 | -------------- 93 | 94 | The tests can currently be run with the following command: 95 | 96 | .. code-block:: bash 97 | 98 | manage.py test accounts bots logs plugins 99 | 100 | 101 | Building Documentation 102 | ---------------------- 103 | 104 | Documentation is available in ``docs`` and can be built into a number of 105 | formats using `Sphinx `_: 106 | 107 | .. code-block:: bash 108 | 109 | pip install Sphinx 110 | cd docs 111 | make html 112 | 113 | This creates the documentation in HTML format at ``docs/_build/html``. 114 | -------------------------------------------------------------------------------- /docs/irc_resources.rst: -------------------------------------------------------------------------------- 1 | IRC Resources 2 | ============== 3 | 4 | Internet Relay Chat Protocol 5 | 6 | Freenode 7 | -------- 8 | 9 | * `Registering Channels `_ 10 | * `FAQ `_ 11 | 12 | Being a Good Citizen 13 | ~~~~~~~~~~~~~~~~~~~~~ 14 | 15 | * `Channel Guidelines `_ 16 | * `Being a Catalyst `_ 17 | * `Policy `_ 18 | 19 | 20 | Nicks 21 | ~~~~~ 22 | 23 | It was surprisingly difficult to find a desciption of what constitutes a valid nick on Freenode. Asked the friendly folks on `#freenode `_ and had the answer a few minutes later. 24 | 25 | nickname = ( letter / special ) *8( letter / digit / special / "-" ) 26 | and special is "[", "]", "\", "`", "_", "^", "{", "|", "}" 27 | 28 | Nicks can be from 2 - 16 ASCII characters long. These characters are allowed: 29 | 30 | 31 | 32 | RFC 1459 - Internet Relay Chat Protocol http://www.ietf.org/rfc/rfc1459.txt 33 | -------------------------------------------------------------------------------- /docs/managing_bots.rst: -------------------------------------------------------------------------------- 1 | Managing Bots 2 | ============== 3 | 4 | Multiple Bots 5 | ------------- 6 | 7 | Bots can be connected to multiple networks. For example you can have a bot that connects to Freenode, and another that connects to Mozilla's IRC network. 8 | 9 | Multiple bots for the same network / server is not supported at this time. 10 | 11 | Public, Private, and Featured Channels 12 | --------------------------------------- 13 | 14 | These are primarily distinctions for the Django site and the display of logs. 15 | 16 | Public 17 | Logs for public channels will be available on a public URL like *http://example.com/freenode/django* 18 | 19 | Featured 20 | Featured channels are public channels. Used by `botbot.me `_ for highlighting some public channels. May be deprecated in the future. 21 | 22 | Private 23 | Logs for private channels are only availale to authenticated users of the site. They will have URLs that are not easy to guess. 24 | 25 | 26 | Freenode 27 | --------- 28 | 29 | Policy 30 | ~~~~~~ 31 | 32 | Before logging any public channels, take a couple simple steps to ensure no misunderstandings occur. 33 | 34 | 1. Have the consent of a channel operator. 35 | 2. Ask the operator to make it clear in the channel topic that it is being logged. 36 | 37 | `Freenode's channel guidelines `_ don't seem to address non-operator users who want to run bots. (see final bullet point) Our preference is to favor honesty and transparency. 38 | 39 | -------------------------------------------------------------------------------- /docs/plugins.rst: -------------------------------------------------------------------------------- 1 | Plugin API Documentation 2 | ========================= 3 | 4 | You can write your own Botbot plugin by extending the core plugin class and providing one or more message handlers. A 5 | message handler is a method on the plugin class that receives an object representing a user message that has been 6 | posted to the IRC channel the plugin is associated with. The existing plugins in ``botbotme_plugins/plugins`` serve as good examples to follow. **ping** and **brain** are good ones to start with due to their simplicity. 7 | 8 | Plugin Capabilities 9 | -------------------- 10 | 11 | Plugins provide three basic capabilities: 12 | 13 | 1. Parse messages and optionally respond with an output message. 14 | 2. Associate configuration variables. Useful if your plugin needs to connect to external services. 15 | 3. Store and retrieve key/value pairs. 16 | 17 | All plugins extend the BasePlugin class, providing them with the ability to utilize these capabilities. 18 | 19 | Parsing and responding to messages 20 | ----------------------------------- 21 | 22 | In the simplest case, a plugin will receive a message from an IRC channel and parse it based on a rule. When the parsed input 23 | matches a rule, the plugin may return a response. 24 | 25 | Additional methods should be defined on your ``Plugin`` class that will listen and optionally respond to incoming messages. They are registered with the app using one of the following decorators from ``botbotme_plugins.decorators``: 26 | 27 | * ``listens_to_mentions(regex)``: A method that should be called only when the bot's nick prefixes the message and that message matches the regex pattern. For example, ``[o__o]: What time is it in Napier, New Zealand?``. The nick will be stripped prior to regex matching. 28 | * ``listens_to_all(regex)``: A method that should be called on any line that matches the regex pattern. 29 | 30 | The method should accept a ``line`` object as its first argument and any named matches from the regex as keyword args. Any text returned by the method will be echoed back to the channel. 31 | 32 | The ``line`` object has the following attributes: 33 | 34 | * ``user``: The nick of the user who wrote the message 35 | * ``text``: The text of the message (stripped of nick if addressed to the bot) 36 | * ``full_text``: The text of the message 37 | 38 | Configuration Metadata 39 | ----------------------- 40 | 41 | Metadata can be associated with your plugin that can be referenced as needed in the message handlers. A common use case for 42 | this is storing authentication credentials and/or API endpoint locations for external services. The ``github`` plugin is an example that uses configuration for the ability to query a Github repository. 43 | 44 | To add configuration to your plugin, define a config class that inherits from ``config.BaseConfig``. Configuration values are 45 | declared by adding instances of ``config.Field`` as attributes of the class. 46 | 47 | Once your config class is defined, you associate it with the plugin via the ``config_class`` attribute:: 48 | 49 | class MyConfig(BaseConfig): 50 | unwarranted_comments = Field( 51 | required=False, 52 | help_text="Responds to every message with sarcastic comment", 53 | default=True) 54 | 55 | class Plugin(BasePlugin): 56 | config_class = MyConfig 57 | 58 | @listens_to_all 59 | def peanut_gallery(self, line): 60 | if self.config.unwarranted_comments: 61 | return "Good one!" 62 | 63 | 64 | Storage / Persisting Data 65 | -------------------------- 66 | 67 | BasePlugin provides a wrapper around the Redis API that Plugins should use for storage. Since multiple channels can be using the same plugin, keys need to unique per plugin instance. BasePlugin takes care of this for you. 68 | 69 | 70 | Testing Your Plugins 71 | --------------------- 72 | 73 | In order to simulate the plugin running in its normal environment, an app instance must be instantiated. See the current 74 | tests for examples. This may change with subsequent releases. 75 | -------------------------------------------------------------------------------- /docs/production.rst: -------------------------------------------------------------------------------- 1 | ****************************************** 2 | Serving BotBot In a Production Environment 3 | ****************************************** 4 | 5 | When you deploy botbot to production, we recommend that you do not use the Procfile. Instead, serve three pieces individually: 6 | 7 | * **botbot-web**: should be served as a wsgi application, from the ``wsgi.py`` file located at ``src/botbot/botbot/wsgi.py`` from `uwsgi `_, `gunicorn `_, `mod_wsgi `_, or any other wsgi server. 8 | * **botbot-plugins**: should be run as an application from botbot's manage.py file. Use `upstart `_, `systemd `_, `init `_, or whatever your system uses for managing long-running tasks. An example upstart script is provided below. 9 | * **botbot-bot**: should also be run as an application from your system's task management system. An example upstart script is provided below. 10 | 11 | Example upstart scripts 12 | ----------------------- 13 | 14 | ``botbot-plugins.conf``: 15 | 16 | .. code-block:: bash 17 | 18 | # BotBot Plugins 19 | # logs to /var/log/upstart/botbot_plugins.log 20 | 21 | description "BotBot Plugins" 22 | start on startup 23 | stop on shutdown 24 | 25 | respawn 26 | env LANG=en_US.UTF-8 27 | exec /srv/botbot/bin/manage.py run_plugins 28 | setuid www-data 29 | 30 | ``botbot-bot.conf``: 31 | 32 | .. code-block:: bash 33 | 34 | # BotBot-bot 35 | # logs to /var/log/upstart/botbot.log 36 | 37 | description "BotBot" 38 | start on startup 39 | stop on shutdown 40 | 41 | respawn 42 | env LANG=en_US.UTF-8 43 | env STORAGE_URL=postgres://yourdburl 44 | env REDIS_PLUGIN_QUEUE_URL=redis://localhost:6379/0 45 | 46 | exec /srv/botbot/bin/botbot-bot 47 | setuid www-data 48 | 49 | Running In A Subdirectory 50 | ------------------------- 51 | 52 | If you intend to run botbot in a subdirectory of your website, for example at ``http://example.com/botbot`` you'll need to add two options to your ``settings.py``: 53 | 54 | .. code-block:: python 55 | 56 | FORCE_SCRIPT_NAME = '/botbot' 57 | USE_X_FORWARDED_HOST = True 58 | 59 | -------------------------------------------------------------------------------- /docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | ================ 3 | 4 | Log messages 5 | ------------ 6 | 7 | **plugins.1 | DoesNotExist: Channel matching query does not exist.** 8 | 9 | If you edit or add channels this condition can occur. It is due to a bug where 10 | stale config data is in Redis. This bug will be resolved in a future release. 11 | 12 | .. warning: 13 | You can resolve this by flushing your Redis DB. **Not recommended for production environments. You will lose all plugin data** -------------------------------------------------------------------------------- /manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | 4 | if __name__ == "__main__": 5 | if (len(sys.argv) > 1 and 6 | 'run_plugins' in sys.argv and '--with-gevent' in sys.argv): 7 | # import gevent as soon as possible 8 | from gevent import monkey; monkey.patch_all() 9 | from psycogreen.gevent import patch_psycopg; patch_psycopg() 10 | 11 | import os 12 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "botbot.settings") 13 | 14 | from django.core.management import execute_from_command_line 15 | 16 | execute_from_command_line(sys.argv) 17 | -------------------------------------------------------------------------------- /nginx.conf.example: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | daemon off; 3 | error_log /dev/stdout debug; 4 | 5 | 6 | events { 7 | worker_connections 1024; 8 | } 9 | 10 | 11 | http { 12 | include /usr/local/etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | 15 | #access_log logs/access.log main; 16 | access_log /dev/stdout; 17 | 18 | sendfile on; 19 | keepalive_timeout 65; 20 | 21 | push_stream_shared_memory_size 32M; 22 | 23 | upstream django { 24 | server localhost:8000; 25 | } 26 | server { 27 | listen 127.0.0.1:8080; 28 | location /channels-stats { 29 | # activate channels statistics mode for this location 30 | push_stream_channels_statistics; 31 | 32 | # query string based channel id 33 | push_stream_channels_path $arg_id; 34 | allow 127.0.0.1; 35 | deny all; 36 | } 37 | 38 | location /pub { 39 | # activate publisher (admin) mode for this location 40 | push_stream_publisher admin; 41 | 42 | # query string based channel id 43 | push_stream_channels_path $arg_id; 44 | push_stream_store_messages on; 45 | allow 127.0.0.1; 46 | deny all; 47 | } 48 | 49 | location ~ /internal-channel-stream/(.*) { 50 | internal; 51 | # activate subscriber (streaming) mode for this location 52 | push_stream_subscriber eventsource; 53 | 54 | # positional channel path 55 | push_stream_channels_path $1; 56 | 57 | 58 | #push_stream_allowed_origins botbot.me; 59 | # ping frequency 60 | push_stream_ping_message_interval 30s; 61 | } 62 | 63 | try_files $uri @django; 64 | 65 | # Setup named location for Django requests and handle proxy details 66 | location @django { 67 | proxy_pass http://django; 68 | proxy_redirect off; 69 | proxy_set_header Host $host; 70 | proxy_set_header X-Real-IP $remote_addr; 71 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django==1.8.4 2 | 3 | pytz 4 | Jinja2==2.7.3 5 | django-jinja==1.4.1 6 | psycopg2==2.5.2 7 | dj-database-url==0.2.2 8 | # TODO (yml): Remove hstore when we squash the migration 9 | django-hstore==1.2.5 10 | # django-statsd-mozilla==0.3.12 11 | git+https://github.com/vbabiy/django-statsd.git@request-aggregation 12 | 13 | markdown==2.3.1 14 | redis==2.9.1 15 | django-pipeline==1.5.1 16 | # Requires java runtime 17 | yuicompressor==2.4.8 18 | honcho==0.5.0 19 | 20 | djorm-ext-pgfulltext==0.9.0 21 | djorm-ext-pgarray==0.9.0 22 | 23 | django-allauth==0.20.0 24 | requests==2.7.0 25 | oauthlib==0.7.2 26 | requests-oauthlib==0.5.0 27 | django-bootstrap-toolkit==2.15.0 28 | 29 | geoip2==2.1.0 30 | 31 | # Needs pinning 32 | -e git+https://github.com/lincolnloop/django-jsonit.git#egg=jsonit 33 | -e git+https://github.com/lincolnloop/django-launchpad.git@5047b2ec967c3ddad671cbcee7c02ce68edec2aa#egg=launchpad 34 | 35 | # For plugins 36 | -e git+https://github.com/BotBotMe/botbot-plugins.git#egg=botbot-plugins 37 | 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup, find_packages 3 | 4 | setup( 5 | name='botbot', 6 | version='1.0', 7 | description="", 8 | author="Lincoln Loop", 9 | author_email='info@lincolnloop.com', 10 | url='', 11 | packages=find_packages(), 12 | package_data={'botbot': ['static/*.*', 'templates/*.*']}, 13 | scripts=['manage.py'], 14 | ) 15 | --------------------------------------------------------------------------------