├── API
├── API
│ ├── __init__.py
│ ├── wsgi.py
│ ├── urls.py
│ └── settings.py
├── chat
│ ├── __init__.py
│ ├── migrations
│ │ ├── __init__.py
│ │ ├── 0004_auto_20150905_1700.py
│ │ ├── 0002_auto_20150731_1920.py
│ │ ├── 0005_auto_20160511_1921.py
│ │ ├── 0003_auto_20150902_1854.py
│ │ └── 0001_squashed_0002_auto_20150707_1647.py
│ ├── admin.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── channel
│ │ │ ├── __init__.py
│ │ │ ├── test_model.py
│ │ │ ├── test_post_view.py
│ │ │ └── test_get_view.py
│ │ ├── test_url.py
│ │ ├── message
│ │ │ ├── __init__.py
│ │ │ ├── common.py
│ │ │ ├── test_delete_view.py
│ │ │ ├── test_model.py
│ │ │ ├── test_patch_view.py
│ │ │ ├── test_post_view.py
│ │ │ └── test_get_view.py
│ │ ├── common.py
│ │ └── test_privileged.py
│ ├── utils.py
│ ├── urls.py
│ ├── models.py
│ ├── forms.py
│ └── views.py
├── requirements.txt
└── manage.py
├── client
├── main.js
├── .babelrc
├── .eslintrc
├── static
│ └── sounds
│ │ └── message_sound.mp3
├── config.jsx
├── index.html
├── message
│ ├── image.jsx
│ ├── content.jsx
│ ├── text.jsx
│ ├── view.jsx
│ ├── __tests__
│ │ └── view-test.js
│ ├── form.jsx
│ └── history.jsx
├── avatar.jsx
├── locales
│ ├── en.json
│ └── el.json
├── css
│ ├── settings.css
│ └── index.css
├── select.jsx
├── topbar.jsx
├── analytics.js
├── webpack.config.js
├── package.json
├── userlist.jsx
├── app.jsx
├── settings.jsx
├── login.jsx
└── ting.jsx
├── docker
├── front
│ ├── run.sh
│ ├── build.sh
│ ├── nginx.conf
│ ├── supervisord.conf
│ └── Dockerfile
├── realtime
│ ├── run.sh
│ ├── build.sh
│ └── Dockerfile
├── api
│ ├── create-default-channels.py
│ ├── Dockerfile
│ └── run.sh
└── config
│ └── common.json
├── .dockerignore
├── .htaccess
├── etc
├── spec
│ ├── chat.jpg
│ └── login.jpg
├── mockups
│ ├── topbar-mockup
│ │ ├── avatar.png
│ │ ├── topbar.html
│ │ ├── css
│ │ │ ├── customstyle.css
│ │ │ └── bootstrap-theme.min.css
│ │ └── style.css
│ ├── login-mockups
│ │ ├── js
│ │ │ └── .DS_Store
│ │ ├── css
│ │ │ ├── customstyle.css
│ │ │ └── bootstrap-theme.min.css
│ │ ├── simple_login.html
│ │ ├── password_login.html
│ │ └── style.css
│ ├── settings-mockup
│ │ ├── avatar.png
│ │ ├── css
│ │ │ ├── customstyle.css
│ │ │ └── bootstrap-theme.min.css
│ │ ├── style.css
│ │ └── settings.html
│ ├── ting-channels_files
│ │ ├── 0e6be8383b0653c0a36730077cdcfb7f
│ │ ├── 3863e7fc8ee24a35326b0a813572bd00
│ │ ├── 8ae1e5b3c4d1df49c17ebefcc25f9dc2
│ │ └── dd6541aa30c336b429c768d4f8e7df2b
│ ├── index.css
│ └── ting-channels.html
├── credits.txt
└── config
│ └── systemd
│ ├── ting_api.service
│ └── ting_realtime.service
├── .gitignore
├── common.yml
├── production.yml
├── .travis.yml
├── config
└── common.json
├── realtime
├── package.json
├── tests
│ └── server.js
└── server.js
├── development.yml
├── .eslintrc
├── LICENSE
├── README.md
└── Makefile
/API/API/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/API/chat/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/API/chat/migrations/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/main.js:
--------------------------------------------------------------------------------
1 | require('./app.jsx');
2 |
--------------------------------------------------------------------------------
/docker/front/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | supervisord
3 |
--------------------------------------------------------------------------------
/docker/realtime/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | nodemon server.js
3 |
--------------------------------------------------------------------------------
/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | bower_components
3 | client/dist
4 |
--------------------------------------------------------------------------------
/.htaccess:
--------------------------------------------------------------------------------
1 | Header always unset Content-Security-Policy-Report-Only
2 |
--------------------------------------------------------------------------------
/etc/spec/chat.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/spec/chat.jpg
--------------------------------------------------------------------------------
/etc/spec/login.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/spec/login.jpg
--------------------------------------------------------------------------------
/API/chat/admin.py:
--------------------------------------------------------------------------------
1 | from django.contrib import admin
2 |
3 | # Register your models here.
4 |
--------------------------------------------------------------------------------
/docker/realtime/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export JOBS=MAX
3 | pushd /usr/src/app
4 | npm install
5 | popd
6 |
--------------------------------------------------------------------------------
/client/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "plugins": ["transform-class-properties"],
3 | "presets": ["env", "react"]
4 | }
5 |
--------------------------------------------------------------------------------
/client/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "rules": {
4 | "strict": 0
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/etc/mockups/topbar-mockup/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/mockups/topbar-mockup/avatar.png
--------------------------------------------------------------------------------
/client/static/sounds/message_sound.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/client/static/sounds/message_sound.mp3
--------------------------------------------------------------------------------
/etc/mockups/login-mockups/js/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/mockups/login-mockups/js/.DS_Store
--------------------------------------------------------------------------------
/etc/mockups/settings-mockup/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/mockups/settings-mockup/avatar.png
--------------------------------------------------------------------------------
/API/chat/tests/__init__.py:
--------------------------------------------------------------------------------
1 | from chat.tests.message import *
2 | from chat.tests.channel import *
3 | from chat.tests.test_url import *
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | *.pyc
3 | *.swp
4 | *.log
5 | *.DS_Store
6 | config/local.json
7 | nohup.out
8 | client/dist/
9 | API/venv/
10 |
--------------------------------------------------------------------------------
/client/config.jsx:
--------------------------------------------------------------------------------
1 | var config = {
2 | websocket: {
3 | secure: false
4 | },
5 | port: 8080
6 | };
7 |
8 | module.exports = config;
9 |
--------------------------------------------------------------------------------
/API/requirements.txt:
--------------------------------------------------------------------------------
1 | Django==1.8.3
2 | MySQL-python==1.2.5
3 | argparse==1.2.1
4 | django-dynamic-fixture==1.8.5
5 | pytz==2015.4
6 | six==1.9.0
7 | wsgiref==0.1.2
8 |
--------------------------------------------------------------------------------
/docker/api/create-default-channels.py:
--------------------------------------------------------------------------------
1 | from chat.models import Channel
2 | Channel.objects.get_or_create(name='ting')
3 | Channel.objects.get_or_create(name='dev')
4 |
--------------------------------------------------------------------------------
/docker/front/build.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | export JOBS=MAX
3 | pushd /usr/src/app
4 | export PATH=$(npm bin):$PATH
5 | npm install
6 | bower --allow-root install
7 | popd
8 |
--------------------------------------------------------------------------------
/etc/mockups/ting-channels_files/0e6be8383b0653c0a36730077cdcfb7f:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/mockups/ting-channels_files/0e6be8383b0653c0a36730077cdcfb7f
--------------------------------------------------------------------------------
/etc/mockups/ting-channels_files/3863e7fc8ee24a35326b0a813572bd00:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/mockups/ting-channels_files/3863e7fc8ee24a35326b0a813572bd00
--------------------------------------------------------------------------------
/etc/mockups/ting-channels_files/8ae1e5b3c4d1df49c17ebefcc25f9dc2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/mockups/ting-channels_files/8ae1e5b3c4d1df49c17ebefcc25f9dc2
--------------------------------------------------------------------------------
/etc/mockups/ting-channels_files/dd6541aa30c336b429c768d4f8e7df2b:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dionyziz/ting/HEAD/etc/mockups/ting-channels_files/dd6541aa30c336b429c768d4f8e7df2b
--------------------------------------------------------------------------------
/API/chat/tests/channel/__init__.py:
--------------------------------------------------------------------------------
1 | from chat.tests.channel.test_get_view import *
2 | from chat.tests.channel.test_post_view import *
3 | from chat.tests.channel.test_model import *
4 |
--------------------------------------------------------------------------------
/etc/credits.txt:
--------------------------------------------------------------------------------
1 | Creator: Adam_N
2 | Link: https://www.freesound.org/people/Adam_N/
3 |
4 | Song's Title: Water_drop_9
5 | Song's Link: https://www.freesound.org/people/Adam_N/sounds/166325/
6 |
--------------------------------------------------------------------------------
/API/chat/tests/test_url.py:
--------------------------------------------------------------------------------
1 | from chat.tests.common import *
2 |
3 | class URLTests(TestCase):
4 | def test_urls(self):
5 | self.assertEqual(
6 | reverse('chat:message', args=('channel', 'foo',)),
7 | '/messages/channel/foo/'
8 | )
9 |
--------------------------------------------------------------------------------
/API/chat/tests/message/__init__.py:
--------------------------------------------------------------------------------
1 | from chat.tests.message.test_post_view import *
2 | from chat.tests.message.test_get_view import *
3 | from chat.tests.message.test_patch_view import *
4 | from chat.tests.message.test_delete_view import *
5 | from chat.tests.message.test_model import *
6 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ting
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/API/manage.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | import os
3 | import sys
4 |
5 | if __name__ == "__main__":
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "API.settings")
7 |
8 | from django.core.management import execute_from_command_line
9 |
10 | execute_from_command_line(sys.argv)
11 |
--------------------------------------------------------------------------------
/client/message/image.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react');
2 |
3 | class Image extends React.Component {
4 | render() {
5 | var message_content = this.props.message_content;
6 |
7 | return (
8 |
9 |

10 |
11 | );
12 | }
13 | }
14 |
15 | module.exports = Image;
16 |
--------------------------------------------------------------------------------
/docker/front/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 |
4 | location /api/ {
5 | proxy_pass http://api:8000/;
6 | }
7 |
8 | location / {
9 | root /usr/src/app/;
10 | index index.html index.htm index.php;
11 | if (-f $request_filename) {
12 | break;
13 | }
14 |
15 | rewrite ^/(.*)$ /index.html?r=$1;
16 | try_files $uri $uri/ =404;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/common.yml:
--------------------------------------------------------------------------------
1 | db:
2 | image: mysql
3 | environment:
4 | MYSQL_ROOT_PASSWORD: ting
5 | MYSQL_DATABASE: ting
6 |
7 | api:
8 | build: .
9 | dockerfile: docker/api/Dockerfile
10 |
11 | realtime:
12 | build: .
13 | dockerfile: docker/realtime/Dockerfile
14 | ports:
15 | - "8080:8080"
16 |
17 | front:
18 | build: .
19 | dockerfile: docker/front/Dockerfile
20 | ports:
21 | - "80:80"
22 |
--------------------------------------------------------------------------------
/API/API/wsgi.py:
--------------------------------------------------------------------------------
1 | """
2 | WSGI config for API project.
3 |
4 | It exposes the WSGI callable as a module-level variable named ``application``.
5 |
6 | For more information on this file, see
7 | https://docs.djangoproject.com/en/1.8/howto/deployment/wsgi/
8 | """
9 |
10 | import os
11 |
12 | from django.core.wsgi import get_wsgi_application
13 |
14 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "API.settings")
15 |
16 | application = get_wsgi_application()
17 |
--------------------------------------------------------------------------------
/API/chat/tests/channel/test_model.py:
--------------------------------------------------------------------------------
1 | from chat.tests.common import *
2 |
3 | class ChannelModelTests(ChatTests):
4 | def test_channel_create(self):
5 | """
6 | A channel must be saved in the database.
7 | """
8 | channels = Channel.objects.filter(pk=self.channel.id)
9 | self.assertTrue(channels.exists())
10 | self.assertEqual(channels.count(), 1)
11 | self.assertEqual(channels[0].name, self.channel.name)
12 |
13 |
--------------------------------------------------------------------------------
/production.yml:
--------------------------------------------------------------------------------
1 | db:
2 | extends:
3 | file: common.yml
4 | service: db
5 |
6 | api:
7 | extends:
8 | file: common.yml
9 | service: api
10 | links:
11 | - db
12 |
13 | realtime:
14 | extends:
15 | file: common.yml
16 | service: realtime
17 | links:
18 | - front
19 |
20 | front:
21 | extends:
22 | file: common.yml
23 | service: front
24 | links:
25 | - api
26 |
--------------------------------------------------------------------------------
/docker/api/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM django:python2
2 |
3 | RUN mkdir -p /usr/src/{app,config,runtime}
4 | WORKDIR /usr/src/app
5 |
6 | COPY API/requirements.txt /usr/src/app/
7 | RUN pip install --no-cache-dir -r requirements.txt
8 |
9 | COPY docker/config/common.json /usr/src/config/
10 | COPY API/ /usr/src/app/
11 |
12 | COPY docker/api/create-default-channels.py /usr/src/runtime/
13 | COPY docker/api/run.sh /usr/src/runtime/
14 |
15 | EXPOSE 8000
16 | CMD ["/usr/src/runtime/run.sh"]
17 |
--------------------------------------------------------------------------------
/docker/front/supervisord.conf:
--------------------------------------------------------------------------------
1 | [supervisord]
2 | nodaemon=true
3 |
4 | [program:gulp]
5 | command=/usr/src/app/node_modules/.bin/gulp watchify
6 | directory=/usr/src/app
7 | stdout_logfile=/dev/stdout
8 | stdout_logfile_maxbytes=0
9 | stderr_logfile=/dev/stderr
10 | stderr_logfile_maxbytes=0
11 |
12 | [program:nginx]
13 | command=/usr/sbin/nginx -g "daemon off;"
14 | stdout_logfile=/dev/stdout
15 | stdout_logfile_maxbytes=0
16 | stderr_logfile=/dev/stderr
17 | stderr_logfile_maxbytes=0
18 |
--------------------------------------------------------------------------------
/docker/config/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "password": "apassword",
3 | "node": {
4 | "hostname": "front",
5 | "port": 8080
6 | },
7 | "django": {
8 | "port": 8000,
9 | "secret_key": "the_big_secret",
10 | "allowed_hosts": [
11 | ],
12 | "database": {
13 | "name": "ting",
14 | "user": "root",
15 | "password": "ting",
16 | "host": "db"
17 | },
18 | "debug": 1
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/docker/realtime/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node
2 |
3 | RUN mkdir -p /usr/src/{app,config,runtime}
4 | WORKDIR /usr/src/app
5 |
6 | RUN JOBS=MAX npm install -g nodemon
7 |
8 | COPY docker/realtime/build.sh /usr/src/runtime/
9 | COPY docker/realtime/run.sh /usr/src/runtime/
10 |
11 | COPY realtime/package.json /usr/src/app/
12 | RUN /usr/src/runtime/build.sh
13 |
14 | EXPOSE 8080
15 |
16 | COPY docker/config/common.json /usr/src/config/
17 | COPY realtime/ /usr/src/app/
18 | CMD ["/usr/src/runtime/run.sh"]
19 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js: "node"
3 | sudo: true
4 | before_script:
5 | - cd realtime; npm install; cd ..
6 | - cd client; npm install; bower install; cd ..
7 | script:
8 | - cd realtime && npm test && cd ..
9 | - cd client && npm test && cd ..
10 | - cd client && find . -regex ".*\.jsx?"|grep -v node_modules/|grep -v bower_components/|grep -v dist/|xargs eslint && cd ..
11 | - cd API && sudo pip install --no-cache-dir -r requirements.txt && python manage.py test chat && cd ..
12 |
--------------------------------------------------------------------------------
/API/chat/migrations/0004_auto_20150905_1700.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 | ('chat', '0003_auto_20150902_1854'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='message',
16 | name='datetime_start',
17 | field=models.DateTimeField(default=None),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/API/chat/migrations/0002_auto_20150731_1920.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 | ('chat', '0001_squashed_0002_auto_20150707_1647'),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name='channel',
16 | name='name',
17 | field=models.CharField(unique=True, max_length=20),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/config/common.json:
--------------------------------------------------------------------------------
1 | {
2 | "password": "apassword",
3 | "node": {
4 | "hostname": "ting.gr",
5 | "port": 8080
6 | },
7 | "django": {
8 | "port": 8000,
9 | "secret_key": "the_big_secret",
10 | "allowed_hosts": [
11 | "ting.gr",
12 | "www.ting.gr"
13 | ],
14 | "database": {
15 | "name": "ting",
16 | "user": "ting",
17 | "password": "ting",
18 | "host": ""
19 | },
20 | "debug": 0
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/docker/api/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | LOOP_LIMIT=60
3 | for (( i=0 ; ; i++ )); do
4 | if [ ${i} -eq ${LOOP_LIMIT} ]; then
5 | echo "=> Could not connect to the db container. Shutting down."
6 | exit 1
7 | fi
8 | echo "=> Waiting for the db container to start up, trying ${i}/${LOOP_LIMIT}..."
9 | sleep 1
10 | mysql -hdb -uroot -pting -e "status" > /dev/null 2>&1 && break
11 | done
12 |
13 | python manage.py migrate
14 | python manage.py shell < /usr/src/runtime/create-default-channels.py
15 | python manage.py runserver 0.0.0.0:8000
16 |
--------------------------------------------------------------------------------
/etc/config/systemd/ting_api.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Ting API Service
3 | Documentation=https://github.com/dionyziz/ting
4 | After=network.target
5 |
6 | [Service]
7 | WorkingDirectory=/var/www/ting.gr/html/API
8 | ExecStart=/var/www/ting.gr/html/API/venv/bin/python manage.py runserver
9 | Restart=always
10 | # Restart service after 10 seconds if node service crashes
11 | RestartSec=10
12 | KillSignal=SIGQUIT
13 | # Output to syslog
14 | StandardOutput=syslog
15 | StandardError=syslog
16 | SyslogIdentifier=ting_api
17 |
18 | [Install]
19 | WantedBy=multi-user.target
20 |
--------------------------------------------------------------------------------
/API/chat/utils.py:
--------------------------------------------------------------------------------
1 | import pytz
2 | import datetime
3 | import time
4 |
5 |
6 | def datetime_to_timestamp(datetime):
7 | """
8 | Takes a datetime and returns
9 | a unix epoch ms.
10 | """
11 | time_without_ms = time.mktime(datetime.timetuple()) * 1000
12 | ms = int(datetime.microsecond / 1000)
13 |
14 | return time_without_ms + ms
15 |
16 | def timestamp_to_datetime(timestamp):
17 | """
18 | Takes a timestamp and returns
19 | a datetime.
20 | """
21 | return datetime.datetime.fromtimestamp(timestamp / 1000.0).replace(tzinfo=pytz.UTC)
22 |
--------------------------------------------------------------------------------
/realtime/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat",
3 | "version": "0.1.0",
4 | "description": "",
5 | "main": "index.js",
6 | "author": "The Ting Team",
7 | "license": "MIT",
8 | "scripts": {
9 | "test": "forever start server.js && jasmine-node tests/ --matchall"
10 | },
11 | "dependencies": {
12 | "lodash": "^4.17.4",
13 | "request": "^2.81.0",
14 | "socket.io": "^2.0.3",
15 | "winston": "^2.3.1"
16 | },
17 | "devDependencies": {
18 | "forever": "^0.15.3",
19 | "jasmine-node": "^1.14.5",
20 | "socket.io-client": "^2.0.3"
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/etc/config/systemd/ting_realtime.service:
--------------------------------------------------------------------------------
1 | [Unit]
2 | Description=Ting Realtime Service
3 | Documentation=https://github.com/dionyziz/ting
4 | After=network.target
5 |
6 | [Service]
7 | WorkingDirectory=/var/www/ting.gr/html/realtime
8 | ExecStart=/usr/bin/node /var/www/ting.gr/html/realtime/server.js
9 | Restart=always
10 | # Restart service after 10 seconds if node service crashes
11 | RestartSec=10
12 | # Output to syslog
13 | StandardOutput=syslog
14 | StandardError=syslog
15 | SyslogIdentifier=ting_realtime
16 | Environment=NODE_ENV=production
17 |
18 | [Install]
19 | WantedBy=multi-user.target
20 |
--------------------------------------------------------------------------------
/API/chat/urls.py:
--------------------------------------------------------------------------------
1 | from django.conf.urls import url
2 | from django.views.decorators.csrf import csrf_exempt
3 |
4 | from . import views
5 |
6 | urlpatterns = [
7 | url(
8 | r'^messages/(?P[0-9]+)/$',
9 | csrf_exempt(views.MessageView.as_view()),
10 | name='message'
11 | ),
12 | url(
13 | r'^messages/(?P[a-z]+)/(?P[a-zA-Z0-9_.-]+)/$',
14 | csrf_exempt(views.MessageView.as_view()),
15 | name='message'
16 | ),
17 | url(
18 | r'^channels/',
19 | views.ChannelView.as_view(),
20 | name='channel'
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/docker/front/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM gtklocker/nginx-nodejs-base:v0.0.2
2 |
3 | RUN mkdir -p /usr/src/{app,runtime}
4 | WORKDIR /usr/src/app
5 |
6 | ENV PATH /usr/src/app/node_modules/.bin:$PATH
7 | COPY docker/front/build.sh /usr/src/runtime/
8 | COPY docker/front/run.sh /usr/src/runtime/
9 |
10 | COPY client/package.json /usr/src/app/
11 | COPY client/bower.json /usr/src/app/
12 | RUN /usr/src/runtime/build.sh
13 |
14 | COPY docker/front/nginx.conf /etc/nginx/conf.d/default.conf
15 | COPY docker/front/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
16 | COPY client/ /usr/src/app/
17 | CMD ["/usr/src/runtime/run.sh"]
18 |
--------------------------------------------------------------------------------
/client/avatar.jsx:
--------------------------------------------------------------------------------
1 | var React = require('react');
2 |
3 | class Avatar extends React.Component {
4 | _getAvatar = (username) => {
5 | return 'https://avatars.githubusercontent.com/' + username.toLowerCase();
6 | };
7 |
8 | render() {
9 | if (this.props.username == null) {
10 | return null;
11 | }
12 |
13 | const src = this._getAvatar(this.props.username);
14 |
15 | return (
16 |
19 | );
20 | }
21 | }
22 |
23 | module.exports = Avatar;
24 |
--------------------------------------------------------------------------------
/API/chat/tests/message/common.py:
--------------------------------------------------------------------------------
1 | from chat.tests.common import *
2 |
3 | from chat.models import Message
4 | from chat.utils import datetime_to_timestamp, timestamp_to_datetime
5 |
6 | def create_message(message_content, timestamp, username, channel, message_type):
7 | """
8 | Creates a message with the given text, datetime,
9 | username, channel and with typing set to True.
10 | """
11 | return Message.objects.create(
12 | message_content=message_content,
13 | datetime_start=timestamp_to_datetime(timestamp),
14 | username=username,
15 | typing=True,
16 | channel=channel,
17 | message_type=message_type
18 | )
19 |
20 |
--------------------------------------------------------------------------------
/API/chat/tests/channel/test_post_view.py:
--------------------------------------------------------------------------------
1 | from chat.tests.common import *
2 |
3 | class ChannelViewPOSTTests(ChatTests):
4 | def test_create_valid_channel(self):
5 | """
6 | When a channel is created the view should
7 | respond with a 204(No Content) code and save the channel
8 | in the database.
9 | """
10 | response = self.client.post(
11 | reverse('chat:channel'),
12 | {'name': 'New_Channel'}
13 | )
14 |
15 | self.assertTrue(Channel.objects.filter(name='New_Channel').exists())
16 | self.assertEqual(Channel.objects.filter(name='New_Channel').count(), 1)
17 | self.assertEqual(response.status_code, 204)
18 |
19 |
--------------------------------------------------------------------------------
/API/chat/migrations/0005_auto_20160511_1921.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 | ('chat', '0004_auto_20150905_1700'),
11 | ]
12 |
13 | operations = [
14 | migrations.RenameField(
15 | model_name='message',
16 | old_name='text',
17 | new_name='message_content',
18 | ),
19 | migrations.AddField(
20 | model_name='message',
21 | name='message_type',
22 | field=models.CharField(default=b'text', max_length=10, choices=[(b'text', b'text'), (b'image', b'image')]),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/client/message/content.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Text = require('./text.jsx'),
3 | Image = require('./image.jsx');
4 |
5 | class MessageContent extends React.Component {
6 | render() {
7 | var message_content = this.props.message_content;
8 | var messageType = this.props.messageType;
9 | var message_class = null;
10 |
11 | switch (messageType) {
12 | case 'text':
13 | message_class = ;
14 | break;
15 |
16 | case 'image':
17 | message_class = ;
18 | break;
19 | }
20 |
21 | return message_class;
22 | }
23 | }
24 |
25 | module.exports = MessageContent;
26 |
--------------------------------------------------------------------------------
/development.yml:
--------------------------------------------------------------------------------
1 | db:
2 | extends:
3 | file: common.yml
4 | service: db
5 |
6 | api:
7 | extends:
8 | file: common.yml
9 | service: api
10 | command: sh -c "/usr/src/runtime/run.sh"
11 | volumes:
12 | - ./API:/usr/src/app
13 | links:
14 | - db
15 |
16 | realtime:
17 | extends:
18 | file: common.yml
19 | service: realtime
20 | command: sh -c "/usr/src/runtime/build.sh && /usr/src/runtime/run.sh"
21 | volumes:
22 | - ./realtime:/usr/src/app
23 | links:
24 | - front
25 |
26 | front:
27 | extends:
28 | file: common.yml
29 | service: front
30 | command: sh -c "/usr/src/runtime/build.sh && /usr/src/runtime/run.sh"
31 | volumes:
32 | - ./client:/usr/src/app
33 | links:
34 | - api
35 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "rules": {
3 | "indent": [
4 | 1,
5 | 4
6 | ],
7 | "no-unused-vars": [
8 | 1
9 | ],
10 | "quotes": [
11 | 2,
12 | "single"
13 | ],
14 | "linebreak-style": [
15 | 2,
16 | "unix"
17 | ],
18 | "semi": [
19 | 2,
20 | "always"
21 | ]
22 | },
23 | "env": {
24 | "browser": true,
25 | "commonjs": true,
26 | "es6": true,
27 | "jquery": true,
28 | "jest": true
29 | },
30 | "parserOptions": {
31 | "ecmaFeatures": {
32 | "jsx": true
33 | }
34 | },
35 | "extends": "eslint:recommended",
36 | "plugins": [
37 | "react",
38 | "require-path-exists"
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/API/chat/migrations/0003_auto_20150902_1854.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 | ('chat', '0002_auto_20150731_1920'),
11 | ]
12 |
13 | operations = [
14 | migrations.RenameField(
15 | model_name='message',
16 | old_name='datetime',
17 | new_name='datetime_start'
18 | ),
19 | migrations.AddField(
20 | model_name='message',
21 | name='datetime_sent',
22 | field=models.DateTimeField(default=None, null=True),
23 | ),
24 | migrations.AddField(
25 | model_name='message',
26 | name='typing',
27 | field=models.BooleanField(default=False),
28 | ),
29 | ]
30 |
--------------------------------------------------------------------------------
/API/API/urls.py:
--------------------------------------------------------------------------------
1 | """API URL Configuration
2 |
3 | The `urlpatterns` list routes URLs to views. For more information please see:
4 | https://docs.djangoproject.com/en/1.8/topics/http/urls/
5 | Examples:
6 | Function views
7 | 1. Add an import: from my_app import views
8 | 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home')
9 | Class-based views
10 | 1. Add an import: from other_app.views import Home
11 | 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home')
12 | Including another URLconf
13 | 1. Add an import: from blog import urls as blog_urls
14 | 2. Add a URL to urlpatterns: url(r'^blog/', include(blog_urls))
15 | """
16 | from django.conf.urls import include, url
17 | from django.contrib import admin
18 |
19 | urlpatterns = [
20 | url(r'', include('chat.urls', namespace='chat')),
21 | url(r'^admin/', include(admin.site.urls)),
22 | ]
23 |
--------------------------------------------------------------------------------
/client/message/text.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | escape = require('escape-html'),
3 | emoticons = require('emoticons'),
4 | autolinks = require('autolinks');
5 |
6 | class Text extends React.Component {
7 | _formatMessage = (message) => {
8 | var html = escape(message);
9 |
10 | html = emoticons.replace(html);
11 |
12 | return {
13 | __html: autolinks(html, (title, url) => {
14 | return `
17 | ${title}
18 | `;
19 | })
20 | };
21 | };
22 |
23 | render() {
24 | var message_content = this.props.message_content;
25 |
26 | return (
27 |
28 |
29 | );
30 | }
31 | }
32 |
33 | module.exports = Text;
34 |
--------------------------------------------------------------------------------
/client/locales/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "usernameSet": {
3 | "placeholder": "Choose a username",
4 | "submit": "Join",
5 | "errors": {
6 | "empty": "Choose a username.",
7 | "length": "Your username must be less than 20 letters.",
8 | "chars": "Your username must include only letters or numbers.",
9 | "taken": "This username is taken."
10 | }
11 | },
12 | "messageInput": {
13 | "placeholder": "Type a message..."
14 | },
15 | "topbarSet": {
16 | "settings": "Settings",
17 | "logout": "Log out"
18 | },
19 | "settingsSet": {
20 | "changePic": "Change my picture",
21 | "password": "Password",
22 | "email": "E-mail",
23 | "birthDate": "Birth Date",
24 | "sex": "Sex",
25 | "region": "Region",
26 | "save": "Save"
27 | },
28 | "gender": {
29 | "boy": "Boy",
30 | "girl": "Girl",
31 | "undefined": "-"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/locales/el.json:
--------------------------------------------------------------------------------
1 | {
2 | "usernameSet": {
3 | "placeholder": "Γράψε ένα ψευδώνυμο",
4 | "submit": "Μπες",
5 | "errors": {
6 | "empty": "Γράψε ένα ψευδώνυμο.",
7 | "length": "Το ψευδώνυμο πρέπει να είναι έως 20 γράμματα.",
8 | "chars": "Το ψευδώνυμο πρέπει να περιλαμβάνει μόνο γράμματα ή αριθμούς.",
9 | "taken": "Το ψευδώνυμο το έχει άλλος."
10 | }
11 | },
12 | "messageInput": {
13 | "placeholder": "Γράψε ένα μήνυμα..."
14 | },
15 | "topbarSet": {
16 | "settings": "Ρυθμίσεις",
17 | "logout": "Έξοδος"
18 | },
19 | "settingsSet": {
20 | "changePic": "Αλλαγή της εικόνας μου",
21 | "password": "Κωδικός",
22 | "email": "E-mail",
23 | "birthDate": "Ημερομηνία Γέννησης",
24 | "sex": "Φύλο",
25 | "region": "Περιοχή",
26 | "save": "Αποθήκευση"
27 | },
28 | "gender": {
29 | "boy": "Aγόρι",
30 | "girl": "Κορίτσι",
31 | "undefined": "-"
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/client/message/view.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Avatar = require('../avatar.jsx'),
3 | MessageContent = require('./content.jsx');
4 |
5 | class Message extends React.Component {
6 | render() {
7 | var className, message_content = this.props.message_content;
8 |
9 | if (this.props.own) {
10 | className = 'self';
11 | }
12 | else {
13 | className = 'other';
14 | }
15 |
16 | if (this.props.typing) {
17 | className += ' typing';
18 | }
19 |
20 | if (this.props.own && this.props.typing) {
21 | message_content = '...';
22 | }
23 |
24 | return (
25 |
26 |
27 | {this.props.username}
28 |
30 |
31 | );
32 | }
33 | }
34 |
35 | module.exports = Message;
36 |
--------------------------------------------------------------------------------
/API/chat/migrations/0001_squashed_0002_auto_20150707_1647.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 | ]
11 |
12 | operations = [
13 | migrations.CreateModel(
14 | name='Channel',
15 | fields=[
16 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
17 | ('name', models.CharField(max_length=20)),
18 | ],
19 | ),
20 | migrations.CreateModel(
21 | name='Message',
22 | fields=[
23 | ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
24 | ('text', models.TextField(max_length=2000)),
25 | ('datetime', models.DateTimeField()),
26 | ('channel', models.ForeignKey(to='chat.Channel')),
27 | ('username', models.CharField(max_length=20)),
28 | ],
29 | ),
30 | ]
31 |
--------------------------------------------------------------------------------
/client/css/settings.css:
--------------------------------------------------------------------------------
1 | #settings {
2 | width: 400px;
3 | margin: 40px auto 0 auto;
4 | }
5 | #settings #icon-username {
6 | display: flex;
7 | flex-direction: row;
8 | justify-content: center;
9 | }
10 | #icon-username h2 {
11 | margin-left: 12px;
12 | }
13 | #settings .icon {
14 | width: 100px;
15 | height: 150px;
16 | border: 1px solid black;
17 | overflow: hidden;
18 | }
19 | .icon img {
20 | height: 100px;
21 | width: 100px;
22 | }
23 | .icon span {
24 | padding: 4px 10px;
25 | border-top: 1px solid black;
26 | cursor: pointer;
27 | display: block;
28 | }
29 | #settings #form {
30 | margin-top: 50px;
31 | }
32 | #form .button {
33 | text-decoration: none;
34 | }
35 | .button button {
36 | color: white;
37 | background-color: #0084ff;
38 | border: 1px solid #0084ff;
39 | padding: 10px 0;
40 | width: 120px;
41 | display: block;
42 | margin: 40px auto 0 auto;
43 | }
44 | .no-gutters {
45 | margin-left: 0;
46 | margin-right: 0;
47 | }
48 | .no-gutters [class*='col-']:not(:first-child) {
49 | padding-right: 5px;
50 | padding-left: 0;
51 | }
52 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2015 Ting team
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/client/select.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | _ = require('lodash');
3 |
4 | class Select extends React.Component {
5 | render() {
6 | var elements = null,
7 | options = null;
8 |
9 | if (this.props.start != null) {
10 | if (this.props.start < this.props.end) {
11 | elements = _.range(this.props.start, this.props.end + 1);
12 | }
13 | else {
14 | elements = _.rangeRight(this.props.end, this.props.start + 1);
15 | }
16 | }
17 | else {
18 | elements = this.props.container;
19 | }
20 |
21 | if (elements != null) {
22 | options = elements.map(function(elem, ind) {
23 | return ;
24 | });
25 | }
26 |
27 | var classProp = 'form-control ' + this.props.classProp,
28 | idProp = this.props.idProp;
29 |
30 | return (
31 |
34 | );
35 | }
36 | }
37 |
38 | module.exports = Select;
39 |
--------------------------------------------------------------------------------
/API/chat/tests/channel/test_get_view.py:
--------------------------------------------------------------------------------
1 | from chat.tests.common import *
2 |
3 | class ChannelViewGETTests(ChatTests):
4 | def test_request_valid_channel(self):
5 | """
6 | When a channel with a name that exists in
7 | the database is requested, the view should return
8 | a JSON object containing the name of the channel
9 | and a 200(OK) status code.
10 | """
11 | response = self.client.get(
12 | reverse('chat:channel'),
13 | {'name': self.channel.name}
14 | )
15 | returned_channel = json.loads(response.content)
16 |
17 | self.assertEqual(response.status_code, 200)
18 | self.assertEqual(returned_channel['name'], self.channel.name)
19 |
20 | def test_request_channel_that_does_not_exist(self):
21 | """
22 | When a channel that does not exist is requested
23 | the view should return a 404(Not Found) status code.
24 | """
25 | response = self.client.get(
26 | reverse('chat:channel'),
27 | {'name': 'invalid_channel'}
28 | )
29 |
30 | self.assertEqual(response.status_code, 404)
31 |
32 |
--------------------------------------------------------------------------------
/API/chat/models.py:
--------------------------------------------------------------------------------
1 | from django.db import models
2 |
3 |
4 | class Channel(models.Model):
5 | def __str__(self):
6 | return self.name
7 |
8 | name = models.CharField(max_length=20, unique=True)
9 |
10 |
11 | class Message(models.Model):
12 | def __str__(self):
13 | return self.message_content
14 |
15 | def to_dict(self):
16 | serializable_fields = ('message_content', 'datetime_start', 'datetime_sent', 'username')
17 | return {key: getattr(self, key) for key in serializable_fields}
18 |
19 | TEXT = 'text'
20 | IMAGE = 'image'
21 |
22 | MESSAGE_TYPE = (
23 | (TEXT, 'text'),
24 | (IMAGE, 'image'),
25 | )
26 |
27 | message_content = models.TextField(max_length=2000)
28 | datetime_start = models.DateTimeField(default=None)
29 | datetime_sent = models.DateTimeField(default=None, null=True)
30 | typing = models.BooleanField(default=False)
31 | username = models.CharField(max_length=20)
32 | channel = models.ForeignKey(Channel)
33 | message_type = models.CharField(max_length=10,
34 | choices=MESSAGE_TYPE,
35 | default=TEXT)
36 |
--------------------------------------------------------------------------------
/client/topbar.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Avatar = require('./avatar.jsx'),
3 | Link = require('react-router-dom').Link,
4 | i18n = require('i18next');
5 |
6 | class TopBar extends React.Component {
7 | state = {
8 | username: null
9 | };
10 |
11 | onLogin = (username) => {
12 | this.setState({username});
13 | };
14 |
15 | onReload = (e) => {
16 | e.preventDefault();
17 | window.location.reload();
18 | };
19 |
20 | render() {
21 | return (
22 |
23 |
26 |
27 | - {i18n.t('topbarSet.settings')}
28 | - {i18n.t('topbarSet.logout')}
29 |
30 |
31 | );
32 | }
33 | }
34 |
35 | module.exports = TopBar;
36 |
--------------------------------------------------------------------------------
/client/analytics.js:
--------------------------------------------------------------------------------
1 | /* global ga: false */
2 |
3 | var Analytics = {
4 | firstMessage: true,
5 | init() {
6 | /* eslint-disable */
7 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
8 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
9 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
10 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
11 | /* eslint-enable */
12 |
13 | ga('create', 'UA-64452066-1', 'auto');
14 | ga('send', 'pageview');
15 | },
16 | onMessageSubmit(message) {
17 | if (this.firstMessage) {
18 | ga('send', 'event', {
19 | eventCategory: 'chat',
20 | eventAction: 'chat_form_submit',
21 | eventLabel: 'send',
22 | eventValue: 1
23 | });
24 | this.firstMessage = false;
25 | }
26 | },
27 | onLoginIntention(username) {
28 | ga('send', 'event', {
29 | eventCategory: 'join',
30 | eventAction: 'username_set',
31 | eventLabel: 'submit',
32 | eventValue: 1
33 | });
34 | }
35 | };
36 |
37 | module.exports = Analytics;
38 |
--------------------------------------------------------------------------------
/client/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | const webpack = require('webpack'),
3 | path = require('path');
4 |
5 | module.exports = {
6 | entry: './main.js',
7 | output: {
8 | filename: 'main.js',
9 | path: path.resolve(__dirname, 'dist')
10 | },
11 | devtool: 'source-map',
12 | module: {
13 | rules: [
14 | {
15 | test: /\.jsx?$/,
16 | exclude: /node_modules/,
17 | use: {
18 | loader: 'babel-loader'
19 | }
20 | },
21 | {
22 | test: /\.css$/,
23 | use: [
24 | 'style-loader',
25 | 'css-loader'
26 | ]
27 | },
28 | {
29 | test: /\.(woff|woff2|eot|ttf|otf)$/,
30 | use: [
31 | 'file-loader'
32 | ]
33 | },
34 | {
35 | test: /\.(png|svg|jpg|gif)$/,
36 | use: [
37 | 'file-loader'
38 | ]
39 | }
40 | ]
41 | },
42 | plugins: [
43 | // bootstrap@3 needs this
44 | new webpack.ProvidePlugin({
45 | jQuery: 'jquery'
46 | })
47 | ]
48 | };
49 |
--------------------------------------------------------------------------------
/API/chat/tests/common.py:
--------------------------------------------------------------------------------
1 | import time
2 | import json
3 | import datetime
4 | import urllib
5 |
6 | from django.test import TestCase, Client
7 | from django_dynamic_fixture import G
8 | from django.core.urlresolvers import reverse
9 | from django.utils.dateformat import format
10 | from chat.utils import datetime_to_timestamp, timestamp_to_datetime
11 | from django.conf import settings
12 |
13 | from chat.models import Message, Channel
14 |
15 | class ChatClient(Client):
16 | def delete(self, url, qstring, *args, **kwargs):
17 | return Client().delete(
18 | url,
19 | qstring,
20 | content_type='application/x-www-form-urlencoded',
21 | *args,
22 | **kwargs
23 | )
24 |
25 | def patch(self, url, qstring, *args, **kwargs):
26 | return Client().patch(
27 | url,
28 | qstring,
29 | content_type='application/x-www-form-urlencoded',
30 | *args,
31 | **kwargs
32 | )
33 |
34 |
35 | class ChatTests(TestCase):
36 | def setUp(self):
37 | super(ChatTests, self).setUp()
38 | self.channel = G(Channel, name='Channel')
39 |
40 | def privileged_operation(self, endpoint, data, method):
41 | return getattr(self.client, method)(
42 | endpoint,
43 | data,
44 | HTTP_AUTHORIZATION=settings.PASS
45 | )
46 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ting",
3 | "version": "0.1.0",
4 | "description": "A chat platform",
5 | "main": "client.js",
6 | "scripts": {
7 | "test": "jest",
8 | "build": "webpack",
9 | "watch": "webpack --watch"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/dionyziz/ting.git"
14 | },
15 | "author": "the Ting team",
16 | "license": "MIT",
17 | "bugs": {
18 | "url": "https://github.com/dionyziz/ting/issues"
19 | },
20 | "homepage": "https://github.com/dionyziz/ting",
21 | "dependencies": {
22 | "autolinks": "^0.1.0",
23 | "bootstrap": "3",
24 | "bower": "^1.5.2",
25 | "classnames": "^2.1.3",
26 | "emoticons": "^0.1.8",
27 | "escape-html": "^1.0.2",
28 | "i18next": "^9.0.0",
29 | "immutability-helper": "^2.3.0",
30 | "jquery": "^3.2.1",
31 | "lodash": "^4.17.4",
32 | "react": "^15.3.1",
33 | "react-dom": "^15.3.1",
34 | "react-router": "^4.1.1",
35 | "react-router-dom": "^4.1.1",
36 | "socket.io-client": "^2.0.3"
37 | },
38 | "devDependencies": {
39 | "babel-core": "^6.26.0",
40 | "babel-eslint": "^8.0.0",
41 | "babel-jest": "^21.0.2",
42 | "babel-loader": "^7.1.2",
43 | "babel-plugin-transform-class-properties": "^6.24.1",
44 | "babel-preset-env": "^1.6.0",
45 | "babel-preset-react": "^6.24.1",
46 | "css-loader": "^0.28.7",
47 | "eslint": "^4.7.0",
48 | "eslint-plugin-react": "^7.3.0",
49 | "eslint-plugin-require-path-exists": "^1.0.15",
50 | "file-loader": "^0.11.2",
51 | "jest": "^21.1.0",
52 | "jest-cli": "^21.1.0",
53 | "style-loader": "^0.18.2",
54 | "webpack": "^3.5.6"
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/client/userlist.jsx:
--------------------------------------------------------------------------------
1 | const React = require('react'),
2 | Avatar = require('./avatar.jsx'),
3 | Update = require('immutability-helper');
4 |
5 | class UserList extends React.Component {
6 | state = {
7 | users: [],
8 | myUsername: null
9 | };
10 |
11 | onLogin = (myUsername, users) => {
12 | this.setState({myUsername, users});
13 | };
14 |
15 | onJoin = (username) => {
16 | if (username != this.state.myUsername) {
17 | var newState = Update(
18 | this.state, {
19 | users: {
20 | $push: [username]
21 | }
22 | }
23 | );
24 | this.setState(newState);
25 | }
26 | };
27 |
28 | onPart = (username) => {
29 | var newUsers = this.state.users.filter((name) => {
30 | return username != name;
31 | });
32 | this.setState({
33 | users: newUsers
34 | });
35 | };
36 |
37 | render() {
38 | var userNodes = this.state.users.map((user) => {
39 | return (
40 |
41 | );
42 | });
43 |
44 | return (
45 |
46 | - ting
47 | {userNodes}
48 |
49 | );
50 | }
51 | }
52 |
53 | class User extends React.Component {
54 | render() {
55 | return (
56 |
57 |
58 | {this.props.username}
59 |
60 | );
61 | }
62 | }
63 |
64 | module.exports = UserList;
65 |
--------------------------------------------------------------------------------
/API/chat/tests/test_privileged.py:
--------------------------------------------------------------------------------
1 | from chat.tests.common import *
2 | from chat.views import privileged
3 | from django.http import HttpResponse, HttpRequest
4 |
5 | class DecoratorTests(TestCase):
6 | @privileged
7 | def privileged_view_mock(self, request, *args, **kwargs):
8 | """
9 | Mock of a view that returns an HttpResponse
10 | with status code 200(OK).
11 | """
12 | return HttpResponse(status=200)
13 |
14 | def get_request(self, password=None):
15 | request = HttpRequest()
16 | request.META = {}
17 | request.META['HTTP_AUTHORIZATION'] = password
18 | return request
19 |
20 | def test_request_with_correct_password(self):
21 | """
22 | When the password is correct the decorator should call
23 | the function passed.
24 | """
25 | request = self.get_request(password=settings.PASS)
26 |
27 | response = self.privileged_view_mock(request)
28 | self.assertEqual(response.status_code, 200)
29 |
30 | def test_request_with_wrong_password(self):
31 | """
32 | When the password is wrong the decorator should respond
33 | with a 401(Unauthorized) status code.
34 | """
35 | request = self.get_request(password='wrong')
36 |
37 | response = self.privileged_view_mock(request)
38 | self.assertEqual(response.status_code, 401)
39 |
40 | def test_request_with_HTTP_AUTHORIZATION_not_defined(self):
41 | """
42 | When the password is not defined the decorator should
43 | respond with a 401(Unauthorized) status code.
44 | """
45 | request = self.get_request()
46 |
47 | response = self.privileged_view_mock(request)
48 | self.assertEqual(response.status_code, 401)
49 |
--------------------------------------------------------------------------------
/API/chat/forms.py:
--------------------------------------------------------------------------------
1 | import time
2 |
3 | from django import forms
4 | from .models import Message
5 | from .utils import timestamp_to_datetime, datetime_to_timestamp
6 |
7 |
8 |
9 | class MessageForm(forms.Form):
10 | message_content = forms.CharField(widget=forms.Textarea)
11 | typing = forms.BooleanField(required=False)
12 | message_type = forms.CharField(widget=forms.Textarea)
13 |
14 |
15 | class MessageCreationForm(MessageForm):
16 | username = forms.CharField(max_length=20)
17 | datetime_start = forms.IntegerField()
18 |
19 | def clean_datetime_start(self):
20 | now = int(round(time.time() * 1000))
21 | timestamp = int(self.data['datetime_start'])
22 | if now < timestamp:
23 | timestamp = now
24 |
25 | self.cleaned_data['datetime_start'] = timestamp_to_datetime(timestamp)
26 |
27 | def save(self):
28 | self.clean_datetime_start()
29 |
30 | message = Message.objects.create(channel=self.channel, **self.cleaned_data)
31 |
32 | if not message.typing:
33 | message.datetime_sent = message.datetime_start
34 | message.save()
35 |
36 | return message;
37 |
38 |
39 | class MessagePatchForm(MessageForm):
40 | datetime_sent = forms.IntegerField()
41 |
42 | def save(self, message):
43 | timestamp_start = datetime_to_timestamp(message.datetime_start)
44 | timestamp_sent = int(self.cleaned_data['datetime_sent'])
45 |
46 | if timestamp_sent < timestamp_start:
47 | timestamp_sent = timestamp_start
48 |
49 | message.datetime_sent = timestamp_to_datetime(timestamp_sent)
50 | message.message_content = self.cleaned_data['message_content']
51 | message.typing = self.cleaned_data.get('typing', False)
52 |
53 | message.save()
54 |
--------------------------------------------------------------------------------
/etc/mockups/topbar-mockup/topbar.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Ting Mockup
6 |
7 |
8 |
9 |
10 |
11 |
12 |
Ting
13 |

14 |
15 | Ρυθμίσεις
16 | Έξοδος
17 |
18 |
19 |
20 |
21 |
26 |
27 |
31 |
32 |
39 |
40 |
41 |
42 |
43 |
50 |
51 |