From 7ff29265785f5fe89bd2d622d219d235b8e2d735 Mon Sep 17 00:00:00 2001 From: mntmn Date: Fri, 7 Apr 2017 01:29:05 +0200 Subject: [PATCH] initial commit. --- .gitignore | 5 + Gulpfile.js | 62 + LICENSE | 661 + README.md | 68 + app.js | 173 + bin/www | 5 + config/default.json | 9 + helpers/artifact_converter.js | 615 + helpers/mailer.js | 61 + helpers/phantom.js | 64 + helpers/redis.js | 61 + helpers/space-render.js | 149 + helpers/uploader.js | 64 + helpers/websockets.js | 291 + locales/de.js | 321 + locales/en.js | 325 + locales/fr.js | 318 + middlewares/404.js | 19 + middlewares/500.js | 10 + middlewares/api_helpers.js | 55 + middlewares/artifact_helpers.js | 22 + middlewares/cors.js | 48 + middlewares/error_helpers.js | 16 + middlewares/i18n.js | 17 + middlewares/setuser.js | 38 + middlewares/space_helpers.js | 160 + middlewares/subdomain.js | 33 + middlewares/team_helpers.js | 23 + middlewares/templates.js | 31 + models/action.js | 32 + models/artifact.js | 88 + models/domain.js | 21 + models/membership.js | 45 + models/message.js | 31 + models/plan.js | 44 + models/schema.js | 12 + models/space.js | 273 + models/team.js | 70 + models/user.js | 53 + package.json | 91 + public/fonts/OpenSans-Regular.ttf | Bin 0 -> 217360 bytes public/fonts/font.scss | 10 + public/fonts/icon-regular-webfont.eot | Bin 0 -> 78175 bytes public/fonts/icon-regular-webfont.svg | 670 + public/fonts/icon-regular-webfont.ttf | Bin 0 -> 193196 bytes public/fonts/icon-regular-webfont.woff | Bin 0 -> 94152 bytes public/fonts/unicode.scss | 2648 +++ public/images/border-dotted.png | Bin 0 -> 1026 bytes public/images/cursors/crosshair.png | Bin 0 -> 1145 bytes public/images/cursors/crosshair.psd | Bin 0 -> 23710 bytes public/images/cursors/eyedrop.png | Bin 0 -> 18069 bytes public/images/cursors/eyedropper.png | Bin 0 -> 1154 bytes public/images/cursors/eyedropper.psd | Bin 0 -> 23818 bytes public/images/cursors/pencil.png | Bin 0 -> 1196 bytes public/images/cursors/pencil.psd | Bin 0 -> 24633 bytes public/images/cursors/text-frame.png | Bin 0 -> 1316 bytes public/images/cursors/text-frame.psd | Bin 0 -> 30131 bytes public/images/diamond.svg | 11 + public/images/favicon.png | Bin 0 -> 1869 bytes public/images/hourglass.gif | Bin 0 -> 21408 bytes public/images/hue.png | Bin 0 -> 19425 bytes public/images/huevalue.png | Bin 0 -> 25304 bytes public/images/ideate-char 2.svg | 80 + public/images/offline-char.svg | 85 + public/images/opacity-grid.png | Bin 0 -> 17882 bytes public/images/opacity-strip.png | Bin 0 -> 20908 bytes public/images/placeholder-4x3.gif | Bin 0 -> 1097 bytes public/images/sd5-hero2-compressed.jpg | Bin 0 -> 919673 bytes public/images/sd5-keyvisual-compressed.jpg | Bin 0 -> 990586 bytes public/images/sd5-logo-inverted.svg | 124 + public/images/sd5-logo.png | Bin 0 -> 6057 bytes public/images/sd5-logo.svg | 118 + public/images/spacedeck-logo.svg | 50 + public/images/spinner.gif | Bin 0 -> 19239 bytes public/images/spinner2.gif | Bin 0 -> 5274 bytes public/images/triangle.svg | 7 + public/javascripts/backend.js | 331 + public/javascripts/clipboard.js | 745 + public/javascripts/fastclick.js | 841 + public/javascripts/helper.js | 249 + public/javascripts/i18next-1.11.2.js | 2332 +++ public/javascripts/jquery-2.1.4.min.js | 4 + public/javascripts/link_parser.js | 133 + public/javascripts/locales.js | 946 + public/javascripts/lodash.compat.js | 7157 +++++++ public/javascripts/medium.patched.js | 1861 ++ public/javascripts/moment.js | 2610 +++ public/javascripts/mousetrap.js | 951 + public/javascripts/packer.growing.js | 147 + public/javascripts/route-recognizer.js | 627 + public/javascripts/smoke.js | 517 + public/javascripts/spacedeck.js | 15931 ++++++++++++++++ public/javascripts/spacedeck_account.js | 124 + public/javascripts/spacedeck_avatars.js | 81 + .../javascripts/spacedeck_board_artifacts.js | 670 + public/javascripts/spacedeck_directives.js | 568 + public/javascripts/spacedeck_formatting.js | 20 + public/javascripts/spacedeck_modals.js | 101 + public/javascripts/spacedeck_routes.js | 326 + public/javascripts/spacedeck_sections.js | 3042 +++ public/javascripts/spacedeck_spaces.js | 973 + public/javascripts/spacedeck_teams.js | 127 + public/javascripts/spacedeck_updates.js | 35 + public/javascripts/spacedeck_users.js | 310 + public/javascripts/spacedeck_vue.js | 167 + public/javascripts/spacedeck_websockets.js | 265 + public/javascripts/spacedeck_whiteboard.js | 1002 + public/javascripts/vector-render.js | 307 + public/javascripts/vue.js | 10030 ++++++++++ routes/api/memberships.js | 61 + routes/api/sessions.js | 82 + routes/api/space_artifacts.js | 201 + routes/api/space_digest.js | 159 + routes/api/space_exports.js | 364 + routes/api/space_memberships.js | 175 + routes/api/space_messages.js | 141 + routes/api/spaces.js | 569 + routes/api/teams.js | 265 + routes/api/users.js | 470 + routes/api/webgrabber.js | 74 + routes/root.js | 304 + styles/actions.scss | 188 + styles/alerts.scss | 113 + styles/annotation.scss | 84 + styles/artifact.scss | 656 + styles/audio.scss | 201 + styles/author.scss | 45 + styles/bounce.scss | 129 + styles/button.scss | 1162 ++ styles/canvas.scss | 8 + styles/chat.scss | 125 + styles/close.scss | 47 + styles/color.scss | 251 + styles/colors.scss | 33 + styles/column.scss | 93 + styles/dialog.scss | 231 + styles/dropdown.scss | 387 + styles/editors.scss | 96 + styles/files.scss | 72 + styles/filter.scss | 22 + styles/folder-item.scss | 220 + styles/folder.scss | 543 + styles/form-checkbox.scss | 142 + styles/form-file.scss | 12 + styles/form-input-group.scss | 152 + styles/form-range.scss | 64 + styles/form-select.scss | 26 + styles/form.scss | 699 + styles/guides.scss | 65 + styles/handles.scss | 465 + styles/header.scss | 110 + styles/helper.scss | 347 + styles/icon.scss | 87 + styles/landing.scss | 257 + styles/lasso.scss | 28 + styles/layout.scss | 18 + styles/list.scss | 617 + styles/login.scss | 60 + styles/main.scss | 122 + styles/margin-columns.scss | 90 + styles/members.scss | 96 + styles/metrics.scss | 42 + styles/mixins.scss | 374 + styles/modal.scss | 303 + styles/normalize.scss | 423 + styles/overflow.scss | 53 + styles/pages.scss | 80 + styles/pattern.scss | 44 + styles/player.scss | 113 + styles/profile.scss | 148 + styles/rich-text.scss | 71 + styles/row.scss | 52 + styles/ruler.scss | 65 + styles/search.scss | 102 + styles/section.scss | 32 + styles/select-list.scss | 157 + styles/settings.scss | 24 + styles/sidebar.scss | 231 + styles/smoke.scss | 72 + styles/space-grid.scss | 30 + styles/space-guides.scss | 65 + styles/space-profile.scss | 76 + styles/space-sections.scss | 414 + styles/style.scss | 189 + styles/table.scss | 97 + styles/team.scss | 3 + styles/toolbar.scss | 232 + styles/tools.scss | 334 + styles/type.scss | 115 + styles/typography.scss | 314 + styles/unicode.scss | 2648 +++ styles/updates.scss | 27 + styles/user.scss | 30 + styles/vars.scss | 51 + styles/video.scss | 38 + views/artifact_list.html | 24 + views/emails/action.html | 11 + views/error.html | 3 + views/facebook.html | 26 + views/index.html | 62 + views/layouts/outer.html | 61 + views/not_found.html | 11 + views/partials/account.html | 169 + views/partials/folders.html | 201 + views/partials/login.html | 140 + views/partials/meta-folder.html | 55 + views/partials/meta.html | 71 + views/partials/modal/access.html | 124 + views/partials/modal/create-space.html | 35 + views/partials/modal/folder-settings.html | 43 + views/partials/modal/login.html | 84 + views/partials/modal/pdfoptions.html | 36 + views/partials/modal/space-share.html | 24 + views/partials/modal/support.html | 31 + views/partials/share-dialog.html | 3 + views/partials/space-isolated.html | 135 + views/partials/space.html | 493 + views/partials/team.html | 91 + views/partials/tool/audio.html | 16 + views/partials/tool/background.html | 112 + views/partials/tool/canvas.html | 28 + views/partials/tool/color.html | 152 + views/partials/tool/crop.html | 30 + views/partials/tool/embed.html | 15 + views/partials/tool/filter.html | 73 + views/partials/tool/grid.html | 29 + views/partials/tool/image.html | 15 + views/partials/tool/layout.html | 65 + views/partials/tool/link.html | 13 + views/partials/tool/metrics.html | 88 + views/partials/tool/object-options.html | 37 + views/partials/tool/object.html | 24 + views/partials/tool/pattern.html | 48 + views/partials/tool/pick-mobile.html | 12 + views/partials/tool/search.html | 70 + views/partials/tool/selection.html | 31 + views/partials/tool/shapes.html | 69 + views/partials/tool/share.html | 19 + views/partials/tool/size.html | 110 + views/partials/tool/stroke.html | 68 + views/partials/tool/text-align.html | 28 + views/partials/tool/text-columns.html | 25 + views/partials/tool/text-digits.html | 13 + views/partials/tool/text-formats.html | 20 + views/partials/tool/text-styles.html | 29 + views/partials/tool/textbox.html | 101 + views/partials/tool/toolbar-elements.html | 110 + .../partials/tool/toolbar-object-options.html | 46 + views/partials/tool/toolbar-object.html | 130 + views/partials/tool/toolbar-social.html | 29 + views/partials/tool/toolbar-text.html | 72 + views/partials/tool/video.html | 15 + views/partials/tool/zones.html | 17 + views/public/contact.html | 10 + views/public/privacy.html | 9 + views/public/terms.html | 8 + views/space_list.html | 20 + views/spacedeck.html | 110 + 258 files changed, 83743 insertions(+) create mode 100644 .gitignore create mode 100644 Gulpfile.js create mode 100644 LICENSE create mode 100644 README.md create mode 100644 app.js create mode 100755 bin/www create mode 100644 config/default.json create mode 100644 helpers/artifact_converter.js create mode 100644 helpers/mailer.js create mode 100644 helpers/phantom.js create mode 100644 helpers/redis.js create mode 100644 helpers/space-render.js create mode 100644 helpers/uploader.js create mode 100644 helpers/websockets.js create mode 100644 locales/de.js create mode 100644 locales/en.js create mode 100644 locales/fr.js create mode 100644 middlewares/404.js create mode 100644 middlewares/500.js create mode 100644 middlewares/api_helpers.js create mode 100644 middlewares/artifact_helpers.js create mode 100644 middlewares/cors.js create mode 100644 middlewares/error_helpers.js create mode 100644 middlewares/i18n.js create mode 100644 middlewares/setuser.js create mode 100644 middlewares/space_helpers.js create mode 100644 middlewares/subdomain.js create mode 100644 middlewares/team_helpers.js create mode 100644 middlewares/templates.js create mode 100644 models/action.js create mode 100644 models/artifact.js create mode 100644 models/domain.js create mode 100644 models/membership.js create mode 100644 models/message.js create mode 100644 models/plan.js create mode 100644 models/schema.js create mode 100644 models/space.js create mode 100644 models/team.js create mode 100644 models/user.js create mode 100644 package.json create mode 100755 public/fonts/OpenSans-Regular.ttf create mode 100755 public/fonts/font.scss create mode 100755 public/fonts/icon-regular-webfont.eot create mode 100755 public/fonts/icon-regular-webfont.svg create mode 100755 public/fonts/icon-regular-webfont.ttf create mode 100755 public/fonts/icon-regular-webfont.woff create mode 100644 public/fonts/unicode.scss create mode 100644 public/images/border-dotted.png create mode 100644 public/images/cursors/crosshair.png create mode 100644 public/images/cursors/crosshair.psd create mode 100644 public/images/cursors/eyedrop.png create mode 100644 public/images/cursors/eyedropper.png create mode 100644 public/images/cursors/eyedropper.psd create mode 100644 public/images/cursors/pencil.png create mode 100644 public/images/cursors/pencil.psd create mode 100644 public/images/cursors/text-frame.png create mode 100644 public/images/cursors/text-frame.psd create mode 100644 public/images/diamond.svg create mode 100644 public/images/favicon.png create mode 100644 public/images/hourglass.gif create mode 100644 public/images/hue.png create mode 100644 public/images/huevalue.png create mode 100644 public/images/ideate-char 2.svg create mode 100644 public/images/offline-char.svg create mode 100644 public/images/opacity-grid.png create mode 100644 public/images/opacity-strip.png create mode 100644 public/images/placeholder-4x3.gif create mode 100644 public/images/sd5-hero2-compressed.jpg create mode 100644 public/images/sd5-keyvisual-compressed.jpg create mode 100644 public/images/sd5-logo-inverted.svg create mode 100644 public/images/sd5-logo.png create mode 100644 public/images/sd5-logo.svg create mode 100644 public/images/spacedeck-logo.svg create mode 100644 public/images/spinner.gif create mode 100644 public/images/spinner2.gif create mode 100644 public/images/triangle.svg create mode 100644 public/javascripts/backend.js create mode 100755 public/javascripts/clipboard.js create mode 100644 public/javascripts/fastclick.js create mode 100644 public/javascripts/helper.js create mode 100755 public/javascripts/i18next-1.11.2.js create mode 100644 public/javascripts/jquery-2.1.4.min.js create mode 100644 public/javascripts/link_parser.js create mode 100644 public/javascripts/locales.js create mode 100644 public/javascripts/lodash.compat.js create mode 100644 public/javascripts/medium.patched.js create mode 100644 public/javascripts/moment.js create mode 100644 public/javascripts/mousetrap.js create mode 100644 public/javascripts/packer.growing.js create mode 100644 public/javascripts/route-recognizer.js create mode 100644 public/javascripts/smoke.js create mode 100644 public/javascripts/spacedeck.js create mode 100644 public/javascripts/spacedeck_account.js create mode 100644 public/javascripts/spacedeck_avatars.js create mode 100644 public/javascripts/spacedeck_board_artifacts.js create mode 100644 public/javascripts/spacedeck_directives.js create mode 100644 public/javascripts/spacedeck_formatting.js create mode 100644 public/javascripts/spacedeck_modals.js create mode 100644 public/javascripts/spacedeck_routes.js create mode 100644 public/javascripts/spacedeck_sections.js create mode 100644 public/javascripts/spacedeck_spaces.js create mode 100644 public/javascripts/spacedeck_teams.js create mode 100644 public/javascripts/spacedeck_updates.js create mode 100644 public/javascripts/spacedeck_users.js create mode 100644 public/javascripts/spacedeck_vue.js create mode 100644 public/javascripts/spacedeck_websockets.js create mode 100644 public/javascripts/spacedeck_whiteboard.js create mode 100644 public/javascripts/vector-render.js create mode 100644 public/javascripts/vue.js create mode 100644 routes/api/memberships.js create mode 100644 routes/api/sessions.js create mode 100644 routes/api/space_artifacts.js create mode 100644 routes/api/space_digest.js create mode 100644 routes/api/space_exports.js create mode 100644 routes/api/space_memberships.js create mode 100644 routes/api/space_messages.js create mode 100644 routes/api/spaces.js create mode 100644 routes/api/teams.js create mode 100644 routes/api/users.js create mode 100644 routes/api/webgrabber.js create mode 100644 routes/root.js create mode 100644 styles/actions.scss create mode 100644 styles/alerts.scss create mode 100644 styles/annotation.scss create mode 100644 styles/artifact.scss create mode 100644 styles/audio.scss create mode 100644 styles/author.scss create mode 100644 styles/bounce.scss create mode 100644 styles/button.scss create mode 100644 styles/canvas.scss create mode 100644 styles/chat.scss create mode 100644 styles/close.scss create mode 100644 styles/color.scss create mode 100644 styles/colors.scss create mode 100644 styles/column.scss create mode 100644 styles/dialog.scss create mode 100644 styles/dropdown.scss create mode 100644 styles/editors.scss create mode 100644 styles/files.scss create mode 100644 styles/filter.scss create mode 100644 styles/folder-item.scss create mode 100644 styles/folder.scss create mode 100644 styles/form-checkbox.scss create mode 100644 styles/form-file.scss create mode 100644 styles/form-input-group.scss create mode 100644 styles/form-range.scss create mode 100644 styles/form-select.scss create mode 100644 styles/form.scss create mode 100644 styles/guides.scss create mode 100644 styles/handles.scss create mode 100644 styles/header.scss create mode 100644 styles/helper.scss create mode 100644 styles/icon.scss create mode 100644 styles/landing.scss create mode 100644 styles/lasso.scss create mode 100644 styles/layout.scss create mode 100644 styles/list.scss create mode 100644 styles/login.scss create mode 100644 styles/main.scss create mode 100644 styles/margin-columns.scss create mode 100644 styles/members.scss create mode 100644 styles/metrics.scss create mode 100644 styles/mixins.scss create mode 100644 styles/modal.scss create mode 100644 styles/normalize.scss create mode 100644 styles/overflow.scss create mode 100644 styles/pages.scss create mode 100644 styles/pattern.scss create mode 100644 styles/player.scss create mode 100644 styles/profile.scss create mode 100644 styles/rich-text.scss create mode 100644 styles/row.scss create mode 100644 styles/ruler.scss create mode 100644 styles/search.scss create mode 100644 styles/section.scss create mode 100644 styles/select-list.scss create mode 100644 styles/settings.scss create mode 100644 styles/sidebar.scss create mode 100644 styles/smoke.scss create mode 100644 styles/space-grid.scss create mode 100644 styles/space-guides.scss create mode 100644 styles/space-profile.scss create mode 100644 styles/space-sections.scss create mode 100644 styles/style.scss create mode 100644 styles/table.scss create mode 100644 styles/team.scss create mode 100644 styles/toolbar.scss create mode 100644 styles/tools.scss create mode 100644 styles/type.scss create mode 100644 styles/typography.scss create mode 100644 styles/unicode.scss create mode 100644 styles/updates.scss create mode 100644 styles/user.scss create mode 100644 styles/vars.scss create mode 100644 styles/video.scss create mode 100644 views/artifact_list.html create mode 100644 views/emails/action.html create mode 100644 views/error.html create mode 100644 views/facebook.html create mode 100644 views/index.html create mode 100644 views/layouts/outer.html create mode 100644 views/not_found.html create mode 100644 views/partials/account.html create mode 100644 views/partials/folders.html create mode 100644 views/partials/login.html create mode 100644 views/partials/meta-folder.html create mode 100644 views/partials/meta.html create mode 100644 views/partials/modal/access.html create mode 100644 views/partials/modal/create-space.html create mode 100644 views/partials/modal/folder-settings.html create mode 100644 views/partials/modal/login.html create mode 100644 views/partials/modal/pdfoptions.html create mode 100644 views/partials/modal/space-share.html create mode 100644 views/partials/modal/support.html create mode 100644 views/partials/share-dialog.html create mode 100644 views/partials/space-isolated.html create mode 100644 views/partials/space.html create mode 100644 views/partials/team.html create mode 100644 views/partials/tool/audio.html create mode 100644 views/partials/tool/background.html create mode 100644 views/partials/tool/canvas.html create mode 100644 views/partials/tool/color.html create mode 100644 views/partials/tool/crop.html create mode 100644 views/partials/tool/embed.html create mode 100644 views/partials/tool/filter.html create mode 100644 views/partials/tool/grid.html create mode 100644 views/partials/tool/image.html create mode 100644 views/partials/tool/layout.html create mode 100644 views/partials/tool/link.html create mode 100644 views/partials/tool/metrics.html create mode 100644 views/partials/tool/object-options.html create mode 100644 views/partials/tool/object.html create mode 100644 views/partials/tool/pattern.html create mode 100644 views/partials/tool/pick-mobile.html create mode 100644 views/partials/tool/search.html create mode 100644 views/partials/tool/selection.html create mode 100644 views/partials/tool/shapes.html create mode 100644 views/partials/tool/share.html create mode 100644 views/partials/tool/size.html create mode 100644 views/partials/tool/stroke.html create mode 100644 views/partials/tool/text-align.html create mode 100644 views/partials/tool/text-columns.html create mode 100644 views/partials/tool/text-digits.html create mode 100644 views/partials/tool/text-formats.html create mode 100644 views/partials/tool/text-styles.html create mode 100644 views/partials/tool/textbox.html create mode 100644 views/partials/tool/toolbar-elements.html create mode 100644 views/partials/tool/toolbar-object-options.html create mode 100644 views/partials/tool/toolbar-object.html create mode 100644 views/partials/tool/toolbar-social.html create mode 100644 views/partials/tool/toolbar-text.html create mode 100644 views/partials/tool/video.html create mode 100644 views/partials/tool/zones.html create mode 100644 views/public/contact.html create mode 100644 views/public/privacy.html create mode 100644 views/public/terms.html create mode 100644 views/space_list.html create mode 100644 views/spacedeck.html diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..216f193 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +public/stylesheets/* +javascripts/maps +javascripts/spacedeck.js + diff --git a/Gulpfile.js b/Gulpfile.js new file mode 100644 index 0000000..af47a5d --- /dev/null +++ b/Gulpfile.js @@ -0,0 +1,62 @@ +var gulp = require('gulp'); +var sass = require('gulp-sass'); +var concat = require('gulp-concat'); +var server = require('gulp-express'); +var nodemon = require('gulp-nodemon'); +var revReplace = require("gulp-rev-replace"); +var clean = require('gulp-clean'); + +var child_process = require('child_process'); +var path = require('path'); +var uglify = require('gulp-uglify'); +var fingerprint = require('gulp-fingerprint'); +var rev = require('gulp-rev'); + +var RevAll = require('gulp-rev-all'); + +gulp.task('rev', () => { + var revAll = new RevAll(); + return gulp.src(['public/**']) + .pipe(gulp.dest('build/assets')) + .pipe(revAll.revision()) + .pipe(gulp.dest('build/assets')) + .pipe(revAll.manifestFile()) + .pipe(gulp.dest('build/assets')); +}); + +gulp.task("all", ["styles", "uglify", "rev", "copylocales"], function(){ + var manifest = gulp.src("./build/assets/rev-manifest.json"); + return gulp.src("./views/**/*") + .pipe(revReplace({manifest: manifest})) + .pipe(gulp.dest("./build/views")); +}); + +gulp.task('copylocales', function(){ + return gulp.src('./locales/*.js').pipe(gulp.dest('./build/locales')); +}); + +gulp.task('clean', function () { + return gulp.src('./build').pipe(clean()); +}); + +gulp.task('styles', function() { + gulp.src('styles/**/*.scss') + .pipe(sass({ + errLogToConsole: true + })) + .pipe(gulp.dest('./public/stylesheets/')) + .pipe(concat('style.css')); +}); + +gulp.task('uglify', () => { + child_process.exec('sed -n \'s/.*script minify src="\\(.*\\)".*/.\\/public\\/\\1/p\' views/spacedeck.html', + function (error, stdout, stderr) { + var scripts = stdout.split('\n').map(function(p){return path.normalize(p)}).filter(function(p){return p!='.'}); + console.log("scripts: ",scripts); + + gulp.src(scripts) + .pipe(uglify({output:{beautify:true}})) + .pipe(concat('spacedeck.js')) + .pipe(gulp.dest('public/javascripts')); + }); +}); diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2def0e8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..03431ae --- /dev/null +++ b/README.md @@ -0,0 +1,68 @@ +# Spacedeck Open + +This is the free and open source version of Spacedeck, a web based, real time, collaborative whiteboard application with rich media support. Spacedeck was developed in 6 major releases during Autumn 2011 until the end of 2016 and was originally a commercial SaaS. The developers were Lukas F. Hartmann (mntmn) and Martin Güther (magegu). All icons and large parts of the CSS were designed by Thomas Helbig (dergraph). + +As we plan to retire the subscription based service at spacedeck.com in late 2017, we decided to open-source Spacedeck to allow educational and other organizations who currently rely on Spacedeck to migrate to a self-hosted version. + +Data migration features will be added soon. + +We appreciate filed issues, pull requests and general discussion. + +# Features + +- Create virtual whiteboards called "Spaces" with virtually unlimited size +- Drag & drop images, videos and audio from your computer or the web +- Write and format text with full control over fonts, colors and style +- Draw, annotate and highlight with included graphical shapes +- Turn your Space into a zooming presentation +- Collaborate and chat in realtime with teammates, students or friends +- Share Spaces on the web or via email +- Export your work as printable PDF or ZIP + +# Requirements, Installation + +Spacedeck uses the following major building blocks: + +- Node.js 4.x (Backend / API) +- MongoDB 3.x (Datastore) +- Redis 3.x (Datastore for realtime channels) +- Vue.js (Frontend) + +It also has some binary dependencies for media conversion and PDF export: + +- imagemagick + +Currently, media files are stored in Amazon S3, so you need an Amazon AWS account and have the ```AWS_ACCESS_KEY_ID``` and ```AWS_SECRET_ACCESS_KEY``` environment variables defined. For sending emails, Amazon SES is required. + +To install Spacedeck, you need node.js 4.x and a running MongoDB instance. Then, to install all node dependencies, run + + npm install + +To rebuild the frontend CSS styles (you need to do this at least once): + + gulp styles + +# Run + + export NODE_ENV=development + npm start + +# License + +Spacedeck Open is released under the GNU Affero General Public License Version 3 (GNU AGPLv3). + + Spacedeck Open - Web-based Collaborative Whiteboard For Rich Media + Copyright (C) 2011-2017 Lukas F. Hartmann, Martin Güther, Thomas Helbig + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as + published by the Free Software Foundation, either version 3 of the + License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . diff --git a/app.js b/app.js new file mode 100644 index 0000000..24b4fb5 --- /dev/null +++ b/app.js @@ -0,0 +1,173 @@ +"use strict"; + +require('./models/schema'); +require("log-timestamp"); + +const config = require('config'); +const redis = require('./helpers/redis'); +const websockets = require('./helpers/websockets'); + +const http = require('http'); +const path = require('path'); + +const _ = require('underscore'); +const favicon = require('serve-favicon'); +const logger = require('morgan'); +const cookieParser = require('cookie-parser'); +const bodyParser = require('body-parser'); +const mongoose = require('mongoose'); +const swig = require('swig'); +const i18n = require('i18n-2'); +const helmet = require('helmet'); + +const express = require('express'); +const app = express(); + +const isProduction = app.get('env') === 'production'; + +console.log("Booting Spacedeck Open… (environment: " + app.get('env') + ")"); + +app.use(logger(isProduction ? 'combined' : 'dev')); + +i18n.expressBind(app, { + locales: ["en", "de", "fr"], + defaultLocale: "en", + cookieName: "spacedeck_locale", + devMode: (app.get('env') == 'development') +}); + +swig.setDefaults({ + varControls: ["[[", "]]"] // otherwise it's not compatible with vue.js +}); + +swig.setFilter('cdn', function(input, idx) { + return input; +}); + +app.engine('html', swig.renderFile); +app.set('view engine', 'html'); + +if (app.get('env') != 'development') { + app.set('views', path.join(__dirname, 'build', 'views')); + app.use(favicon(path.join(__dirname, 'build', 'assets', 'images', 'favicon.png'))); + app.use(express.static(path.join(__dirname, 'build', 'assets'))); +} else { + app.set('views', path.join(__dirname, 'views')); + app.use(favicon(path.join(__dirname, 'public', 'images', 'favicon.png'))); + app.use(express.static(path.join(__dirname, 'public'))); +} + +app.use(bodyParser.json({ + limit: '50mb' +})); + +app.use(bodyParser.urlencoded({ + extended: false, + limit: '50mb' +})); + +app.use(cookieParser()); +app.use(helmet.noCache()) +app.use(helmet.frameguard()) +app.use(helmet.xssFilter()) +app.use(helmet.hsts({ + maxAge: 7776000000, + includeSubdomains: true +})) +app.disable('x-powered-by'); +app.use(helmet.noSniff()) + +// CUSTOM MIDDLEWARES + +app.use(require("./middlewares/templates")); +app.use(require("./middlewares/error_helpers")); +app.use(require("./middlewares/setuser")); +app.use(require("./middlewares/subdomain")); +app.use(require("./middlewares/cors")); +app.use(require("./middlewares/i18n")); +app.use("/api", require("./middlewares/api_helpers")); +app.use('/api/spaces/:id', require("./middlewares/space_helpers")); +app.use('/api/spaces/:id/artifacts/:artifact_id', require("./middlewares/artifact_helpers")); +app.use('/api/teams', require("./middlewares/team_helpers")); + +// REAL ROUTES + +app.use('/api/users', require('./routes/api/users')); +app.use('/api/memberships', require('./routes/api/memberships')); + +const spaceRouter = require('./routes/api/spaces'); +app.use('/api/spaces', spaceRouter); + +spaceRouter.use('/:id/artifacts', require('./routes/api/space_artifacts')); +spaceRouter.use('/:id/memberships', require('./routes/api/space_memberships')); +spaceRouter.use('/:id/messages', require('./routes/api/space_messages')); +spaceRouter.use('/:id/digest', require('./routes/api/space_digest')); +spaceRouter.use('/:id', require('./routes/api/space_exports')); + +app.use('/api/sessions', require('./routes/api/sessions')); +app.use('/api/teams', require('./routes/api/teams')); +app.use('/api/webgrabber', require('./routes/api/webgrabber')); +app.use('/', require('./routes/root')); + +// catch 404 and forward to error handler +app.use(require('./middlewares/404')); +if (app.get('env') == 'development') { + app.set('view cache', false); + swig.setDefaults({cache: false}); +} else { + app.use(require('./middlewares/500')); +} + +module.exports = app; + +// CONNECT TO DATABASE +const mongoHost = process.env.MONGO_PORT_27017_TCP_ADDR || 'localhost'; +mongoose.connect('mongodb://' + mongoHost + '/spacedeck'); + +// START WEBSERVER +const port = 9000; + +const server = http.Server(app).listen(port, () => { + + if ("send" in process) { + process.send('online'); + } + +}).on('listening', () => { + + const host = server.address().address; + const port = server.address().port; + console.log('Spacedeck Open listening at http://%s:%s', host, port); + +}).on('error', (error) => { + + if (error.syscall !== 'listen') { + throw error; + } + + const bind = typeof port === 'string' ? 'Pipe ' + port : 'Port ' + port; + switch (error.code) { + case 'EACCES': + console.error(bind + ' requires elevated privileges'); + process.exit(1); + break; + case 'EADDRINUSE': + console.error(bind + ' is already in use'); + process.exit(1); + break; + default: + throw error; + } +}); + +//WEBSOCKETS & WORKER +websockets.startWebsockets(server); +redis.connectRedis(); + +process.on('message', (message) => { + console.log("Process message:", message); + if (message === 'shutdown') { + console.log("Exiting spacedeck."); + process.exit(0); + } +}); diff --git a/bin/www b/bin/www new file mode 100755 index 0000000..a1d9973 --- /dev/null +++ b/bin/www @@ -0,0 +1,5 @@ +#!/usr/bin/env node + +var app = require('../app'); +var http = require('http'); +var server = http.createServer(app); diff --git a/config/default.json b/config/default.json new file mode 100644 index 0000000..445fa23 --- /dev/null +++ b/config/default.json @@ -0,0 +1,9 @@ +{ + "endpoint": "http://localhost:9000", + "storage_bucket": "my_spacedeck_s3_bucket", + "storage_cdn": "xyz.cloudfront.net", + "google_access" : "", + "google_secret" : "", + "admin_pass": "very_secret_admin_password", + "phantom_api_secret": "very_secret_phantom_password" +} diff --git a/helpers/artifact_converter.js b/helpers/artifact_converter.js new file mode 100644 index 0000000..3605a90 --- /dev/null +++ b/helpers/artifact_converter.js @@ -0,0 +1,615 @@ +'use strict'; + +const exec = require('child_process'); +const gm = require('gm'); +const async = require('async'); +const fs = require('fs'); +const Models = require('../models/schema'); +const uploader = require('../helpers/uploader'); +const path = require('path'); + +const fileExtensionMap = { + ".amr" : "audio/AMR", + ".ogg" : "audio/ogg", + ".aac" : "audio/aac", + ".mp3" : "audio/mpeg", + ".mpg" : "video/mpeg", + ".3ga" : "audio/3ga", + ".mp4" : "video/mp4", + ".wav" : "audio/wav", + ".mov" : "video/quicktime", + ".doc" : "application/msword", + ".dot" : "application/msword", + ".docx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + ".dotx" : "application/vnd.openxmlformats-officedocument.wordprocessingml.template", + ".docm" : "application/vnd.ms-word.document.macroEnabled.12", + ".dotm" : "application/vnd.ms-word.template.macroEnabled.12", + ".xls" : "application/vnd.ms-excel", + ".xlt" : "application/vnd.ms-excel", + ".xla" : "application/vnd.ms-excel", + ".xlsx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + ".xltx" : "application/vnd.openxmlformats-officedocument.spreadsheetml.template", + ".xlsm" : "application/vnd.ms-excel.sheet.macroEnabled.12", + ".xltm" : "application/vnd.ms-excel.template.macroEnabled.12", + ".xlam" : "application/vnd.ms-excel.addin.macroEnabled.12", + ".xlsb" : "application/vnd.ms-excel.sheet.binary.macroEnabled.12", + ".ppt" : "application/vnd.ms-powerpoint", + ".pot" : "application/vnd.ms-powerpoint", + ".pps" : "application/vnd.ms-powerpoint", + ".ppa" : "application/vnd.ms-powerpoint", + ".pptx" : "application/vnd.openxmlformats-officedocument.presentationml.presentation", + ".potx" : "application/vnd.openxmlformats-officedocument.presentationml.template", + ".ppsx" : "application/vnd.openxmlformats-officedocument.presentationml.slideshow", + ".ppam" : "application/vnd.ms-powerpoint.addin.macroEnabled.12", + ".pptm" : "application/vnd.ms-powerpoint.presentation.macroEnabled.12", + ".potm" : "application/vnd.ms-powerpoint.template.macroEnabled.12", + ".ppsm" : "application/vnd.ms-powerpoint.slideshow.macroEnabled.12", + ".key" : "application/x-iwork-keynote-sffkey", + ".pages" : "application/x-iwork-pages-sffpages", + ".numbers" : "application/x-iwork-numbers-sffnumbers", + ".ttf" : "application/x-font-ttf" +}; + +const convertableImageTypes = [ + "image/png", + "image/jpeg", + "application/pdf", + "image/jpg", + "image/gif", + "image/tiff", + "image/vnd.adobe.photoshop"]; + +const convertableVideoTypes = [ + "video/quicktime", + "video/3gpp", + "video/mpeg", + "video/mp4", + "video/ogg"]; + +const convertableAudioTypes = [ + "application/ogg", + "audio/AMR", + "audio/3ga", + "audio/wav", + "audio/3gpp", + "audio/x-wav", + "audio/aiff", + "audio/x-aiff", + "audio/ogg", + "audio/mp4", + "audio/x-m4a", + "audio/mpeg", + "audio/mp3", + "audio/x-hx-aac-adts", + "audio/aac"]; + + +function getDuration(localFilePath, callback){ + exec.execFile("ffprobe", ["-show_format", "-of", "json", localFilePath], function(error, stdout, stderr) { + var test = JSON.parse(stdout); + callback(parseFloat(test.format.duration)); + }); +} + +function createWaveform(fileName, localFilePath, callback){ + var filePathImage = localFilePath + "-" + (new Date().getTime()) + ".png"; + + getDuration(localFilePath, function(duration){ + var totalTime = duration || 1.0; + var pixelsPerSecond = 256.0; + do { + var targetWidth = parseInt(pixelsPerSecond*totalTime, 10); + if (targetWidth>2048) pixelsPerSecond/=2.0; + } while (targetWidth>2048 && pixelsPerSecond>1); + + exec.execFile("audiowaveform", + [ + "-w", + ""+targetWidth, + "--pixels-per-second", + ""+parseInt(pixelsPerSecond), + "--background-color", "ffffff00", + "--border-color", "ffffff", + "--waveform-color", "3498db", + "--no-axis-labels", + "-i", localFilePath, "-o", filePathImage + ], + {}, function(error, stdout, stderr) { + if(!error) { + callback(null, filePathImage); + } else { + console.log("error:", stdout, stderr); + callback(error, null); + } + }); + }); +} + +function convertVideo(fileName, filePath, codec, callback, progress_callback) { + var ext = path.extname(fileName); + var presetMime = fileExtensionMap[ext]; + + var newExt = codec == "mp4" ? "mp4" : "ogv"; + var convertedPath = filePath + "." + newExt; + + console.log("converting", filePath, "to", convertedPath, "progress_cb:",progress_callback); + + var convertArgs = (codec == "mp4") ? [ + "-i", filePath, + "-threads", "4", + "-vf", "scale=1280:trunc(ow/a/2)*2", // scale to width of 1280, truncating height to an even value + "-b:v", "2000k", + "-acodec", "libvo_aacenc", + "-b:a", "96k", + "-vcodec", "libx264", + "-y", convertedPath ] + : [ + "-i", filePath, + "-threads", "4", + "-vf", "scale=1280:trunc(ow/a/2)*2", // scale to width of 1280, truncating height to an even value + "-b:v", "2000k", + "-acodec", "libvorbis", + "-b:a", "96k", + "-vcodec", "libtheora", + "-y", convertedPath]; + + var ff = exec.spawn('ffmpeg', convertArgs, { + stdio: [ + 'pipe', // use parents stdin for child + 'pipe', // pipe child's stdout to parent + 'pipe' + ] + }); + + ff.stdout.on('data', function (data) { + console.log('[ffmpeg-video] stdout: ' + data); + }); + + ff.stderr.on('data', function (data) { + console.log('[ffmpeg-video] stderr: ' + data); + if (progress_callback) { + progress_callback(data); + } + }); + + ff.on('close', function (code) { + console.log('[ffmpeg-video] child process exited with code ' + code); + if (!code) { + console.log("converted", filePath, "to", convertedPath); + callback(null, convertedPath); + } else { + callback(code, null); + } + }); +} + +function convertAudio(fileName, filePath, codec, callback) { + var ext = path.extname(fileName); + var presetMime = fileExtensionMap[ext]; + + var newExt = codec == "mp3" ? "mp3" : "ogg"; + var convertedPath = filePath + "." + newExt; + + console.log("converting audio", filePath, "to", convertedPath); + + var convertArgs = (ext == ".aac") ? [ "-i", filePath, "-y", convertedPath ] + : [ "-i", filePath, + "-b:a", "128k", + "-y", convertedPath]; + + exec.execFile("ffmpeg", convertArgs , {}, function(error, stdout, stderr) { + if(!error){ + console.log("converted", filePath, "to", convertedPath); + callback(null, convertedPath); + }else{ + console.log(error,stdout, stderr); + callback(error, null); + } + }); +} + +function createThumbnailForVideo(fileName, filePath, callback) { + var filePathImage = filePath + ".jpg"; + exec.execFile("ffmpeg", ["-y", "-i", filePath, "-ss", "00:00:01.00", "-vcodec", "mjpeg", "-vframes", "1", "-f", "image2", filePathImage], {}, function(error, stdout, stderr){ + if(!error){ + callback(null, filePathImage); + }else{ + console.log("error:", stdout, stderr); + callback(error, null); + } + }); +} + +function getMime(fileName, filePath, callback) { + var ext = path.extname(fileName); + var presetMime = fileExtensionMap[ext]; + + if (presetMime) { + callback(null, presetMime); + } else { + exec.execFile("file", ["-b","--mime-type", filePath], {}, function(error, stdout, stderr) { + console.log("file stdout: ",stdout); + if (stderr === '' && error == null) { + //filter special chars from commandline + var mime = stdout.replace(/[^a-zA-Z0-9\/\-]/g,''); + callback(null, mime); + } else { + console.log("getMime file error: ", error); + callback(error, null); + } + }); + } +} + +function resizeAndUpload(a, size, max, fileName, localFilePath, callback) { + if (max>320 || size.width > max || size.height > max) { + var resizedFileName = max + "_"+fileName; + var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + resizedFileName; + var localResizedFilePath = "/tmp/"+resizedFileName; + gm(localFilePath).resize(max, max).autoOrient().write(localResizedFilePath, function (err) { + if(!err) { + uploader.uploadFile(s3Key, "image/jpeg", localResizedFilePath, function(err, url) { + if (err) callback(err); + else{ + console.log(localResizedFilePath); + fs.unlink(localResizedFilePath, function (err) { + if (err) { + console.error(err); + callback(null, url); + } + else callback(null, url); + }); + } + }); + } else { + console.error(err); + callback(err); + } + }); + } else { + callback(null, ""); + } +} + + +var resizeAndUploadImage = function(a, mime, size, fileName, fileNameOrg, imageFilePath, originalFilePath, payloadCallback) { + async.parallel({ + small: function(callback){ + resizeAndUpload(a, size, 320, fileName, imageFilePath, callback); + }, + medium: function(callback){ + resizeAndUpload(a, size, 800, fileName, imageFilePath, callback); + }, + big: function(callback){ + resizeAndUpload(a, size, 1920, fileName, imageFilePath, callback); + }, + original: function(callback){ + var s3Key = "s"+ a.space_id.toString() + "/a" + a._id + "/" + fileNameOrg; + uploader.uploadFile(s3Key, mime, originalFilePath, function(err, url){ + callback(null, url); + }); + } + }, function(err, results) { + a.state = "idle"; + a.mime = mime; + var stats = fs.statSync(originalFilePath); + + a.payload_size = stats["size"]; + a.payload_thumbnail_web_uri = results.small; + a.payload_thumbnail_medium_uri = results.medium; + a.payload_thumbnail_big_uri = results.big; + a.payload_uri = results.original; + + var factor = 320/size.width; + var newBoardSpecs = a.board; + newBoardSpecs.w = Math.round(size.width*factor); + newBoardSpecs.h = Math.round(size.height*factor); + a.board = newBoardSpecs; + + a.updated_at = new Date(); + a.save(function(err) { + if(err) payloadCallback(err, null); + else { + fs.unlink(originalFilePath, function (err) { + if (err){ + console.error(err); + payloadCallback(err, null); + } else { + console.log('successfully deleted ' + originalFilePath); + payloadCallback(null, a); + } + }); + } + }); + }); +}; + +module.exports = { + convert: function(a, fileName, localFilePath, payloadCallback, progress_callback) { + getMime(fileName, localFilePath, function(err, mime){ + console.log("[convert] fn: "+fileName+" local: "+localFilePath+" mime:", mime); + + if (!err) { + if (convertableImageTypes.indexOf(mime) > -1) { + + gm(localFilePath).size(function (err, size) { + console.log("[convert] gm:", err, size); + + if (!err) { + if(mime == "application/pdf") { + var firstImagePath = localFilePath + ".jpeg"; + exec.execFile("gs", ["-sDEVICE=jpeg","-dNOPAUSE", "-dJPEGQ=80", "-dBATCH", "-dFirstPage=1", "-dLastPage=1", "-sOutputFile=" + firstImagePath, "-r90", "-f", localFilePath], {}, function(error, stdout, stderr) { + if(error === null) { + resizeAndUploadImage(a, mime, size, fileName + ".jpeg", fileName, firstImagePath, localFilePath, function(err, a) { + fs.unlink(firstImagePath, function (err) { + payloadCallback(err, a); + }); + }); + } else { + payloadCallback(error, null); + } + }); + + } else if(mime == "image/gif") { + //gifs are buggy after convertion, so we should not convert them + + var s3Key = "s"+ a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName; + + uploader.uploadFile(s3Key, "image/gif", localFilePath, function(err, url) { + if(err)callback(err); + else{ + console.log(localFilePath); + var stats = fs.statSync(localFilePath); + + a.state = "idle"; + a.mime = mime; + + a.payload_size = stats["size"]; + a.payload_thumbnail_web_uri = url; + a.payload_thumbnail_medium_uri = url; + a.payload_thumbnail_big_uri = url; + a.payload_uri = url; + + var factor = 320/size.width; + var newBoardSpecs = a.board; + newBoardSpecs.w = Math.round(size.width*factor); + newBoardSpecs.h = Math.round(size.height*factor); + a.board = newBoardSpecs; + + a.updated_at = new Date(); + a.save(function(err){ + if(err) payloadCallback(err, null); + else { + fs.unlink(localFilePath, function (err) { + if (err){ + console.error(err); + payloadCallback(err, null); + } else { + console.log('successfully deleted ' + localFilePath); + payloadCallback(null, a); + } + }); + } + }); + } + }); + + } else { + resizeAndUploadImage(a, mime, size, fileName, fileName, localFilePath, localFilePath, payloadCallback); + } + } else payloadCallback(err); + }); + + } else if (convertableVideoTypes.indexOf(mime) > -1) { + async.parallel({ + thumbnail: function(callback) { + createThumbnailForVideo(fileName, localFilePath, function(err, created){ + console.log("thumbnail created: ", err, created); + if(err) callback(err); + else{ + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".jpg" ; + uploader.uploadFile(keyName, "image/jpeg", created, function(err, url){ + if (err) callback(err); + else callback(null, url); + }); + } + }); + }, + ogg: function(callback) { + if (mime == "video/ogg") { + callback(null, "org"); + } else { + convertVideo(fileName, localFilePath, "ogg", function(err, file) { + if(err) callback(err); + else { + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".ogv" ; + uploader.uploadFile(keyName, "video/ogg", file, function(err, url){ + if (err) callback(err); + else callback(null, url); + }); + } + }, progress_callback); + } + }, + mp4: function(callback) { + if (mime == "video/mp4") { + callback(null, "org"); + } else { + convertVideo(fileName, localFilePath, "mp4", function(err, file) { + if (err) callback(err); + else { + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".mp4"; + uploader.uploadFile(keyName, "video/mp4" ,file, function(err, url) { + if (err) callback(err); + else callback(null, url); + }); + } + }, progress_callback); + } + }, + original: function(callback){ + uploader.uploadFile(fileName, mime, localFilePath, function(err, url){ + callback(null, url); + }); + } + }, function(err, results){ + console.log(err, results); + + if (err) payloadCallback(err, a); + else { + a.state = "idle"; + a.mime = mime; + var stats = fs.statSync(localFilePath); + + a.payload_size = stats["size"]; + a.payload_thumbnail_web_uri = results.thumbnail; + a.payload_thumbnail_medium_uri = results.thumbnail; + a.payload_thumbnail_big_uri = results.thumbnail; + a.payload_uri = results.original; + + if (mime == "video/mp4") { + a.payload_alternatives = [ + { + mime: "video/ogg", + payload_uri: results.ogg + } + ]; + } else { + a.payload_alternatives = [ + { + mime: "video/mp4", + payload_uri: results.mp4 + } + ]; + } + + a.updated_at = new Date(); + a.save(function(err) { + if (err) payloadCallback(err, null); + else { + fs.unlink(localFilePath, function (err) { + if (err){ + console.error(err); + payloadCallback(err, null); + } else { + console.log('successfully deleted ' + localFilePath); + payloadCallback(null, a); + } + }); + } + }); + } + }); + + } else if (convertableAudioTypes.indexOf(mime) > -1) { + + async.parallel({ + ogg: function(callback) { + convertAudio(fileName, localFilePath, "ogg", function(err, file) { + if(err) callback(err); + else { + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".ogg" ; + uploader.uploadFile(keyName, "audio/ogg", file, function(err, url){ + if (err) callback(err); + else callback(null, url); + }); + } + }); + }, + mp3_waveform: function(callback) { + convertAudio(fileName, localFilePath, "mp3", function(err, file) { + if(err) callback(err); + else { + + createWaveform(fileName, file, function(err, filePath){ + + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + "-" + (new Date().getTime()) + ".png"; + uploader.uploadFile(keyName, "image/png", filePath, function(err, pngUrl){ + + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName + ".mp3" ; + uploader.uploadFile(keyName, "audio/mp3", file, function(err, mp3Url){ + if (err) callback(err); + else callback(null, {waveform: pngUrl, mp3: mp3Url}); + }); + + }); + }); + } + }); + }, + original: function(callback) { + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName; + uploader.uploadFile(keyName, mime, localFilePath, function(err, url){ + callback(null, url); + }); + } + }, function(err, results) { + console.log(err, results); + + if (err) payloadCallback(err, a); + else { + + a.state = "idle"; + a.mime = mime; + var stats = fs.statSync(localFilePath); + + a.payload_size = stats["size"]; + a.payload_thumbnail_web_uri = results.mp3_waveform.waveform; + a.payload_thumbnail_medium_uri = results.mp3_waveform.waveform; + a.payload_thumbnail_big_uri = results.mp3_waveform.waveform; + a.payload_uri = results.original; + a.payload_alternatives = [ + {payload_uri:results.ogg, mime:"audio/ogg"}, + {payload_uri:results.mp3_waveform.mp3, mime:"audio/mpeg"} + ]; + + a.updated_at = new Date(); + a.save(function(err){ + if(err) payloadCallback(err, null); + else { + fs.unlink(localFilePath, function (err) { + if (err){ + console.error(err); + payloadCallback(err, null); + } else { + console.log('successfully deleted ' + localFilePath); + payloadCallback(null, a); + } + }); + } + }); + } + }); + + + } else { + console.log("mime not matched for conversion, storing file"); + var keyName = "s" + a.space_id.toString() + "/a" + a._id.toString() + "/" + fileName; + uploader.uploadFile(keyName, mime, localFilePath, function(err, url) { + + a.state = "idle"; + a.mime = mime; + var stats = fs.statSync(localFilePath); + a.payload_size = stats["size"]; + a.payload_uri = url; + + a.updated_at = new Date(); + a.save(function(err) { + if(err) payloadCallback(err, null); + else { + fs.unlink(localFilePath, function (err) { + payloadCallback(null, a); + }); + } + }); + }); + } + } else { + //there was an error getting mime + payloadCallback(err); + } + }); + } +}; + + diff --git a/helpers/mailer.js b/helpers/mailer.js new file mode 100644 index 0000000..8da189d --- /dev/null +++ b/helpers/mailer.js @@ -0,0 +1,61 @@ +'use strict'; + +var swig = require('swig'); +var AWS = require('aws-sdk'); + +module.exports = { + sendMail: (to_email, subject, body, options) => { + + if (!options) { + options = {}; + } + + // FIXME + const teamname = options.teamname || "My Open Spacedeck" + const from = teamname + ' '; + + let reply_to = [from]; + if (options.reply_to) { + reply_to = [options.reply_to]; + } + + let plaintext = body; + if (options.action && options.action.link) { + plaintext+="\n"+options.action.link+"\n\n"; + } + + const htmlText = swig.renderFile('./views/emails/action.html', { + text: body.replace(/(?:\n)/g, '
'), + options: options + }); + + if (process.env.NODE_ENV === 'development') { + console.log("Email: to " + to_email + " in production.\nreply_to: " + reply_to + "\nsubject: " + subject + "\nbody: \n" + htmlText + "\n\n plaintext:\n" + plaintext); + } else { + AWS.config.update({region: 'eu-west-1'}); + var ses = new AWS.SES(); + + ses.sendEmail( { + Source: from, + Destination: { ToAddresses: [to_email] }, + ReplyToAddresses: reply_to, + Message: { + Subject: { + Data: subject + }, + Body: { + Text: { + Data: plaintext, + }, + Html: { + Data: htmlText + } + } + } + }, function(err, data) { + if(err) console.log('Email not sent:', err); + else console.log("Email sent."); + }); + } + } +}; diff --git a/helpers/phantom.js b/helpers/phantom.js new file mode 100644 index 0000000..c195a52 --- /dev/null +++ b/helpers/phantom.js @@ -0,0 +1,64 @@ +'use strict'; + +require('../models/schema'); +var config = require('config'); +var phantom = require('node-phantom-simple'); + +module.exports = { + // type = "pdf" or "png" + takeScreenshot: function(space,type,on_success,on_error) { + var spaceId = space._id; + var space_url = config.get("endpoint")+"/api/spaces/"+spaceId+"/html"; + + var export_path = "/tmp/"+spaceId+"."+type; + + var timeout = 5000; + if (type=="pdf") timeout = 30000; + + space_url += "?api_token="+config.get("phantom_api_secret"); + + console.log("[space-screenshot] url: "+space_url); + console.log("[space-screenshot] export_path: "+export_path); + + var on_success_called = false; + + var on_exit = function(exit_code) { + if (exit_code>0) { + console.log("phantom abnormal exit for url "+space_url); + if (!on_success_called && on_error) { + on_error(); + } + } + }; + + phantom.create({ path: require('phantomjs-prebuilt').path }, function (err, browser) { + return browser.createPage(function (err, page) { + console.log("page created, opening ",space_url); + + if (type=="pdf") { + var psz = { + width: space.advanced.width+"px", + height: space.advanced.height+"px" + }; + page.set('paperSize', psz); + } + + page.set('settings.resourceTimeout',timeout); + page.set('settings.javascriptEnabled',false); + + return page.open(space_url, function (err,status) { + page.render(export_path, function() { + on_success_called = true; + if (on_success) { + on_success(export_path); + } + page.close(); + browser.exit(); + }); + }); + }); + }, { + onExit: on_exit + }); + } +}; diff --git a/helpers/redis.js b/helpers/redis.js new file mode 100644 index 0000000..90b440a --- /dev/null +++ b/helpers/redis.js @@ -0,0 +1,61 @@ +'use strict'; + +const RedisConnection = require('ioredis'); +const websockets = require('./websockets'); + +module.exports = { + connectRedis(){ + const redisHost = process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost'; + this.connection = new RedisConnection(6379, redisHost); + }, + sendMessage(action, model, attributes, channelId) { + const data = JSON.stringify({ + channel_id: channelId, + action: action, + model: model, + object: attributes + }); + this.connection.publish('updates', data); + }, + logIp(ip, cb) { + this.connection.incr("ip_"+ ip, (err, socketCounter) => { + cb(); + }); + }, + rateLimit(namespace, ip, cb) { + const key = "limit_"+ namespace + "_"+ ip; + const redis = this.connection; + + redis.get(key, (err, count)=> { + if (count) { + if(count < 150) { + redis.incr(key, (err, newCount) => { + if (newCount==150) { + // limit + } + cb(true); + }); + } else { + cb(false); + } + } else { + redis.set(key, 1, (err, count) => { + redis.expire(key, 1800, (err, expResult) => { + cb(true); + }); + }); + } + }); + }, + isOnlineInSpace(user, space, cb) { + this.connection.smembers("space_" + space._id.toString(), function(err, list) { + if (err) cb(err); + else { + var users = list.filter(function(item) { + return user._id.toString() === item; + }); + cb(null, (users.length > 0)); + } + }); + } +}; diff --git a/helpers/space-render.js b/helpers/space-render.js new file mode 100644 index 0000000..99164ef --- /dev/null +++ b/helpers/space-render.js @@ -0,0 +1,149 @@ +var fs = require('fs'); +var cheerio = require("cheerio"); + +var artifact_vector_render = require("../public/javascripts/vector-render.js"); + +global.render_vector_shape = artifact_vector_render.render_vector_shape; +global.render_vector_drawing = artifact_vector_render.render_vector_drawing; + +var artifact_view_model = require("../public/javascripts/spacedeck_board_artifacts.js").SpacedeckBoardArtifacts; + +var template = fs.readFileSync("views/partials/space-isolated.html"); + +var dom = cheerio.load(template); + +var compiled_js = ""; + +function emit(str,indent) { + var spaces=""; + for (var i=0; i0; i--) { + indent--; + emit("}",indent); + } + } +} + +function render_space_as_html(space, artifacts) { + if (!compiled_js.length) { + walk(dom("#space")[0],0); + //console.log("compiled template: \n"+compiled_js); + } + + // -------- + var mouse_state = "idle"; + var active_tool = "pointer"; + var active_space = space; + var active_space_artifacts = artifacts; + + var bounds_zoom = 1; + var bounds_margin_horiz = 0; + var bounds_margin_vert = 0; + var viewport_zoom = 1; + // -------- + + var editing_artifact_id = null; + var urls_to_links = function(html) { + return html; + } + + artifact_view_model.selected_artifacts_dict = {}; + + for (var i=0; i\n\n\n'+h+"\n\n"; + + return h; +} + +exports.render_space_as_html = render_space_as_html; + diff --git a/helpers/uploader.js b/helpers/uploader.js new file mode 100644 index 0000000..c314062 --- /dev/null +++ b/helpers/uploader.js @@ -0,0 +1,64 @@ +'use strict'; + +var AWS = require('aws-sdk'); +AWS.config.region = 'eu-central-1'; + +var fs = require('fs'); +var config = require('config'); + +module.exports = { + removeFile: (path, callback) => { + const s3 = new AWS.S3({ + region: 'eu-central-1' + }); + const bucket = config.get("storage_bucket"); + s3.deleteObject({ + Bucket: bucket, Key: path + }, (err, res) => { + if (err){ + console.error(err); + callback(err); + }else { + callback(null, res); + } + }); + }, + uploadFile: function(fileName, mime, localFilePath, callback) { + if (typeof(localFilePath)!="string") { + callback({error:"missing path"}, null); + return; + } + console.log("[s3] uploading", localFilePath, " to ", fileName); + + const bucket = config.get("storage_bucket"); + const fileStream = fs.createReadStream(localFilePath); + fileStream.on('error', function (err) { + if (err) { + console.error(err); + callback(err); + } + }); + fileStream.on('open', function () { + // FIXME + var s3 = new AWS.S3({ + region: 'eu-central-1' + }); + + s3.putObject({ + Bucket: bucket, + Key: fileName, + ContentType: mime, + Body: fileStream + }, function (err) { + if (err){ + console.error(err); + callback(err); + }else { + const url = "https://"+ config.get("storage_cdn") + "/" + fileName; + console.log("[s3]" + localFilePath + " to " + url); + callback(null, url); + } + }); + }); + } +}; diff --git a/helpers/websockets.js b/helpers/websockets.js new file mode 100644 index 0000000..26fcd1c --- /dev/null +++ b/helpers/websockets.js @@ -0,0 +1,291 @@ +'use strict'; +require('../models/schema'); + +const WebSocketServer = require('ws').Server; + +const Redis = require('ioredis'); +const async = require('async'); +const _ = require("underscore"); +const mongoose = require("mongoose"); +const crypto = require('crypto'); + +module.exports = { + startWebsockets: function(server){ + this.setupSubscription(); + this.state = new Redis(6379, process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost'); + + if(!this.current_websockets){ + this.current_websockets = []; + } + + const wss = new WebSocketServer({ server:server, path: "/socket" }); + wss.on('connection', function(ws) { + + this.state.incr("socket_id", function(err, socketCounter) { + const socketId = "socket_" + socketCounter + "_" + crypto.randomBytes(64).toString('hex').substring(0,8); + const serverScope = this; + + ws.on('message', function(msgString){ + const socket = this; + + const msg = JSON.parse(msgString); + + if(msg.action == "auth"){ + + const token = msg.auth_token; + const editorName = msg.editor_name; + const editorAuth = msg.editor_auth; + const spaceId = msg.space_id; + + Space.findOne({"_id": spaceId}).populate('creator').exec((err, space) => { + if (space) { + const upgradeSocket = function() { + if (token) { + User.findBySessionToken(token, function(err, user) { + if (err) { + console.error(err, user); + } else { + if (user) { + serverScope.addUserInSpace(user._id, space, ws, function(err){ + serverScope.addLocalUser(user._id, ws); + console.log("[websockets] user " + user.email + " online in space " + space._id); + }); + } + } + }); + } else { + const anonymousUserId = space._id + "-" + editorName; + + if(space.access_mode == "private" && space.edit_hash != editorAuth){ + console.error("closing websocket: unauthed."); + ws.send(JSON.stringify({error: "auth_failed"})); + // ws.close(); + return; + } + + serverScope.addUserInSpace(anonymousUserId, space, ws, function(err){ + serverScope.addLocalUser(anonymousUserId, ws); + console.log("[websockets] anonymous user " + anonymousUserId + " online in space " + space._id); + }); + } + }; + + if (!ws.id) { + ws['id'] = socketId; + try { + ws.send(JSON.stringify({"action": "init", "channel_id": socketId})); + } catch (e) { + console.log("ws.send error: "+e); + } + } + + if (ws.space_id) { + serverScope.removeUserInSpace(ws.space_id, ws, function(err) { + upgradeSocket(); + }); + } else { + upgradeSocket(); + } + } else { + ws.send(JSON.stringify({error: "space not found"})); + ws.close(); + return; + } + }); + + } else if (msg.action == "cursor" || msg.action == "viewport" || msg.action=="media") { + msg.space_id = socket.space_id; + msg.from_socket_id = socket.id; + serverScope.state.publish('cursors', JSON.stringify(msg)); + } + }); + + ws.on('close', function(evt) { + console.log("websocket closed: ", ws.id, ws.space_id); + const spaceId = ws.space_id; + serverScope.removeUserInSpace(spaceId, ws, function(err) { + this.removeLocalUser(ws, function(err) { + }.bind(this)); + }.bind(this)); + }.bind(this)); + + ws.on('error', function(ws, err) { + console.error(err, res); + }.bind(this)); + }.bind(this)); + }.bind(this)); + }, + + setupSubscription: function() { + this.cursorSubscriber = new Redis(6379, process.env.REDIS_PORT_6379_TCP_ADDR || 'localhost'); + this.cursorSubscriber.subscribe(['cursors', 'users', 'updates'], function (err, count) { + console.log("[redis] websockets to " + count + " topics." ); + }); + this.cursorSubscriber.on('message', function (channel, rawMessage) { + const msg = JSON.parse(rawMessage); + const spaceId = msg.space_id; + + const websockets = this.current_websockets; + + if(channel === "updates") { + for(let i=0;i { + console.log("old websocket removed"); + }); + } + } + } + } + } else { + console.error("userlist undefined for websocket"); + } + } else if(channel === "cursors") { + const socketId = msg.from_socket_id; + for (let i=0;i -1) { + this.removed_items = this.current_websockets.splice(idx, 1); + console.log("removed local socket, current online on this process: ", this.current_websockets.length); + } else { + console.log("websocket not found to remove"); + } + + this.state.del(ws.id, function(err, res) { + if (err) console.error(err, res); + else { + this.removeUserInSpace(ws.space_id, ws, (err) => { + console.log("removed user from space list"); + this.distributeUsers(ws.space_id); + }) + if(cb) + cb(err); + } + }.bind(this)); + }, + + addUserInSpace: function(username, space, ws, cb) { + console.log("[websockets] user "+username+" in "+space.access_mode +" space " + space._id + " with socket " + ws.id); + this.state.set(ws.id, username, function(err, res) { + if(err) console.error(err, res); + else { + this.state.sadd("space_" + space._id, ws.id, function(err, res) { + if(err) cb(err); + else { + ws['space_id'] = space._id.toString(); + + this.distributeUsers(ws.space_id); + if(cb) + cb(); + } + }.bind(this)); + } + }.bind(this)); + }, + removeUserInSpace: function(spaceId, ws, cb) { + this.state.srem("space_" + spaceId, ws.id, function(err, res) { + if (err) cb(err); + else { + console.log("[websockets] socket "+ ws.id + " went offline in space " + spaceId); + this.distributeUsers(spaceId); + ws['space_id'] = null; + + if (cb) + cb(); + } + }.bind(this)); + }, + + distributeUsers: function(spaceId) { + if(!spaceId) + return; + + this.state.smembers("space_" + spaceId, function(err, list) { + async.map(list, function(item, callback) { + this.state.get(item, function(err, userId) { + console.log(item, "->", userId); + callback(null, userId); + }); + }.bind(this), function(err, userIds) { + const uniqueUserIds = _.unique(userIds); + const validUserIds = _.filter(uniqueUserIds, function(uId) { + return mongoose.Types.ObjectId.isValid(uId); + }); + + const nonValidUserIds = _.filter(uniqueUserIds, function(uId) { + return (uId !== null && !mongoose.Types.ObjectId.isValid(uId)); + }); + + const anonymousUsers = _.map(nonValidUserIds, function(nonValidId) { + const realNickname = nonValidId.slice(nonValidId.indexOf("-")+1); + return {nickname: realNickname, email: null, avatar_thumbnail_uri: null }; + }); + + User.find({"_id" : { "$in" : validUserIds }}, { "nickname" : 1 , "email" : 1, "avatar_thumbnail_uri": 1 }, function(err, users) { + if (err) + console.error(err); + else { + const allUsers = users.concat(anonymousUsers); + const strUsers = JSON.stringify({users: allUsers, space_id: spaceId}); + this.state.publish("users", strUsers); + } + }.bind(this)); + }.bind(this)); + }.bind(this)); + } +}; diff --git a/locales/de.js b/locales/de.js new file mode 100644 index 0000000..05b2605 --- /dev/null +++ b/locales/de.js @@ -0,0 +1,321 @@ +{ + "lang": "de", + "ok": "OK", + "cancel": "Abbrechen", + "close": "Schließen", + "open": "Öffnen", + "folder": "Ordner", + "duplicate": "Duplizieren", + "save": "Speichern", + "saved": "Gespeichert", + "created": "Erstellt", + "delete": "Löschen", + "remove": "Entfernen", + "set": "Übernehmen", + "reset": "Zurücksetzen", + "thanks": "Danke", + "share": "Teilen", + "signup": "Registrieren", + "login": "Anmelden", + "logout": "Abmelden", + "email": "E-Mail-Adresse", + "password": "Passwort", + "width": "Breite", + "height": "Höhe", + "nick": "Benutzername", + "role": "Rolle", + "members": "Mitglieder", + "actions": "Aktionen", + "or": "oder", + "you": "du", + "via": "via", + "by": "von", + "new": "Neu", + "zero": "Null", + "page": "Seite", + "copy": "Kopie", + "home": "Übersicht", + "owner": "Besitzer", + "space": "Space", + "second": "Sekunde", + "not_found": "Nicht Gefunden.", + "untitled_space": "Unbenannter Space", + "untitled_folder": "Unbenannter Order", + "untitled": "Unbenannter", + "sure": "Bist du sicher?", + "specify": "Bitte spezifiziere", + "confirm": "Bitte bestätige", + "signup_google": "Mit Google anmelden", + "error_unknown_email": "Unbekannte Kombination von Email und Passwort. Oder versuche dich mit Google anzumelden.", + "error_password_confirmation": "Die beiden Passwörter stimmen nicht überein.", + "error_domain_blocked": "Diese Domain ist gesperrt.", + "error_user_email_already_used": "Diese Email-Adresse ist bereits registriert.", + "support": "Spacedeck-Support", + "offline": "Verbindungsverlust. Mehr Infos hier.", + "error": "Entschuldigung, etwas ist schiefgegangen. Bitte kontaktiere support@spacedeck.com", + "welcome": "Willkommen", + "claim": "Dein digitales Whiteboard.", + "trynow": "Jetzt probieren.", + "about": "Über uns", + "terms": "AGBs", + "contact": "Kontakt", + "privacy": "Privatsphäre", + "post_adress": "Postadresse", + "phone": "Phone", + "business_address": "business_address", + "ceo": "Geschäftsführer", + "business_adress": "business_adress", + "title": "Titel", + "name": "Name", + "confirm_subject": "E-Mail Bestätigung für Spacedeck", + "confirm_body": "Danke, dass du dich bei Spacedeck angemeldet hast.\nBitte klicke auf den folgenden Link, um deine E-Mail Adresse zu bestätigen.\n ", + "confirm_action": "E-Mail Bestätigen", + "team_invite_membership_subject": "Einladung zu %s auf Spacedeck", + "team_invite_membership_body": "Du wurdest zu %s auf Spacedeck eingeladen. \n\nBitte klicke auf den folgenden Link, um die Einladung anzunehmen.\n", + "team_invite_user_body": "Du wurdest zu %s auf Spacedeck eingeladen. Dein temporäres Passwort ist \"%s\".\n Bitte klicke auf den folgenden Link, um die Einladung anzunehmen.", + "team_invite_admin_body": " %s wurde zu %s auf Spacedeck eingeladen. Das temporäres Passwort ist \"%s\".", + "team_invite_membership_action": "Annehmen", + "team_new_member_subject": "Neues Team Mitglied", + "team_new_member_body": "%s hat gerade seine Einladung zum Team %s angenommen.", + "space_invite_membership_subject": "Einladung von %s in Space %s", + "space_invite_membership_body": "Du wurdest von %s in den Space '%s' eingeladen.\nBitte klicke auf den folgenden Link um die Einladung anzunehmen.", + "space_invite_membership_action": "Annehmen", + "folder_invite_membership_subject": "Einladung von %s in Ordner %s", + "folder_invite_membership_body": "Du wurdest von %s in den Space '%s' eingeladen.\nBitte klicke auf den folgenden Link um die Einladung anzunehmen.", + "folder_invite_membership_action": "Accept", + "upgrade": "Upgrade", + "upgrade_now": "Jetzt Upgraden", + "create_space": "Space Erstellen", + "create_folder": "Ordner Erstellen", + "email_unconfirmed": "Email Unbestätigt", + "confirmation_sent": "Email Versandt", + "folder_filter": "Filter", + "sort_by": "Reihenfolge", + "last_modified": "Zuletzt Geändert", + "last_opened": "Zuletzt Geöffnet", + "edit_team": "Team Verwalten", + "edit_account": "Konto Bearbeiten", + "log_out": "Abmelden", + "no_spaces_yet": "Du hast noch keine Spaces erstellt.", + "new_folder_title": "Neuer Titel für Ordner", + "folder_settings": "Ordner-Einstellungen", + "upload_cover_image": "Ordnerbild Hochladen", + "spacedeck_pro_ad_folders": "Mit Spacedeck Pro kannst du beliebig viele Spaces in Ordnerstrukturen organisieren und für jeden Ordner Zugriffsrechte regeln. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_versions": "Mit Spacedeck Pro kannst du beliebig viele Versionen deines Spaces festhalten und später die Entwicklungsgeschichte nachvollziehen. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_pdf": "Mit Spacedeck Pro kannst du Spaces und sogar ganze Ordner als druckreife PDFs exportieren oder per Mail versenden. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_zip": "Mit Spacedeck Pro kannst du die Inhalte deiner Spaces jederzeit als ZIP-Paket exportieren. Möchtest du mehr über die Pro-Version erfahren?", + "spacedeck_pro_ad_colors": "Spacedeck Pro enthält einen Profi-Farbmischer, mit dem du deine eigenen Farben mischen kannst.", + "profile_caption": "Profil", + "upload_avatar": "Profilbild Hochladen", + "uploading_avatar": "Profilbild wird hochgeladen…", + "avatar_dimensions": "Bestes Format: 200×200 Pixel.", + "profile_name": "Name", + "profile_email": "Email-Adresse", + "send_again": "Erneut Senden", + "confirmation_sent_long": "Email-Bestätigungslink versandt. Bitte überprüfe deine Mails.", + "confirmation_sent_another": "Wir haben eine weiteren Bestätigungslink versandt.", + "confirmation_sent_dialog_text": "Wir haben dir eine Email geschickt, die erklärt, wie das mit der Bestätigung läuft.", + "payment_caption": "Bezahlung", + "language_caption": "Sprache", + "notifications_caption": "Emails", + "notifications_option_chat": "Haltet mich über neue Kommentare auf dem Laufenden.", + "notifications_option_spaces": "Schickt mir täglich eine Zusammenfassung über Änderungen an meinen Spaces und Ordnern.", + "password_caption": "Passwort", + "current_password": "Altes Passwort", + "new_password": "Neues Passwort", + "verify_password": "Zur Sicherheit nochmal", + "change_password": "Passwort Ändern", + "reset_password": "Passwort Zurücksetzen", + "terminate_caption": "Kündigen", + "terminate_warning": "Wenn du kündigst, werden all deine Spaces, Ordner und Nachrichten und alle ihre Inhalte gelöscht.", + "terminate_warning2": "Das kann man nicht rückgängig machen.", + "terminate_reason": "Kündigungsgrund", + "terminate_reason_caption": "Wenn du uns mitteilst, was dich gestört hat, hilft uns das dabei ein besseres Produkt zu machen.", + "terminate_terminate": "Wirklich Kündigen", + "space_blank1": "Dies ist dein brandneuer, leerer Space!", + "space_blank2": "Wirf Dateien rein, paste Web-Links", + "space_blank3": "oder nutz die Werkzeuge da unten.", + "space_blank4": "Sei kreativ und tob dich aus!", + "draft": "Entwurf", + "publish": "Veröffentlichen", + "published": "Veröffentlicht", + "save_version": "Version Speichern", + "version_saved": "Version Gespeichert", + "post": "Abschicken", + "chat_invite_cta1": "Zusammen arbeiten macht Spaß!", + "chat_invite_cta2": "Warum ", + "chat_invite_cta3": "lädst du nicht ein paar Leute ein", + "chat_invite_cta4": "mit denen du dann zusammen arbeiten kannst?", + "chat_message_placeholder": "Schreib hier deine Nachricht…", + "view": "Ansicht", + "edit": "Bearb.", + "present": "Präse.", + "chat": "Chat", + "meta": "Teilen", + "tool_search": "Suche", + "tool_upload": "Upload", + "tool_text": "Text", + "tool_shape": "Grafik", + "tool_zones": "Zonen", + "tool_canvas": "Wand", + "search_media": "Medien im Web suchen…", + "type_here": "Hier was eingeben", + "text_formats": "Formate", + "format_p": "Absatz", + "format_bullets": "Bullet-Liste", + "format_numbers": "Nummerierte Liste", + "format_h1": "Überschrift 1", + "format_h2": "Überschrift 2", + "format_h3": "Überschrift 3", + "font_size": "Schriftgröße", + "line_height": "Zeilenhöhe", + "tool_align": "Bund", + "tool_styles": "Stil", + "tool_bullets": "Bullets", + "tool_numbers": "Zahlen", + "color_fill": "Füllung", + "color_stroke": "Strich", + "color_text": "Text", + "tool_type": "Typo", + "tool_box": "Box", + "tool_link": "Link", + "tool_layout": "Layout", + "tool_options": "Mehr", + "tool_stroke": "Strich", + "tool_delete": "Löschen", + "tool_lock": "Sperren", + "tool_copy": "Kopie", + "stack": "Anordnung", + "tool_circle": "Kreis", + "tool_hexagon": "Sechseck", + "tool_square": "Quadrat", + "tool_diamond": "Diamant", + "tool_bubble": "Blase", + "tool_cloud": "Wolke", + "tool_burst": "Burst", + "tool_star": "Stern", + "tool_heart": "Herz", + "tool_scribble": "Kritzeln", + "tool_line": "Linie", + "tool_arrow": "Pfeil", + "search_media_placeholder": "Online-Medien suchen…", + "add_zone": "Neue Zone", + "palette": "Palette", + "picker": "Mischen", + "background_image_caption": "Bild", + "background_color_caption": "Farbe", + "upload_background_caption": "Klicke hier, um ein Hintergrundbild hochzuladen.", + "upload_background": "Hintergrund Hochladen", + "access_caption": "Zugriff", + "versions_caption": "Versionen", + "info_caption": "Info", + "mode_private": "Privat: Nur Mitglieder können zugreifen", + "mode_public": "Öffentlich: Jede(r) mit Kenntnis des Links darf reinschauen", + "invite_collaborators": "Mitarbeiter Einladen", + "invitee_email_address": "Email-Adresse des neuen Mitglieds", + "optional_message": "Optionale Nachricht", + "revoke_access": "Zugriff Entfernen", + "invite": "Einladen", + "role_viewer": "Betrachter", + "role_editor": "Bearbeiter", + "role_admin": "Admin", + "new_space_title": "Neuer Titel für Space", + "logging_in": "logging_in", + "password_confirmation": "Passwort Wiederholung", + "confirm_again": "In deinem Postfach solltest du eine Bestätigungsmail finden. Bitte klicke auf den Link darin.", + "confirmed": "E-Mail Adresse wurde erfolgreich bestätigt. Danke!", + "password_check_inbox": "password_check_inbox", + "viewer": "Zuschauer", + "editor": "Bearbeiter", + "admin": "Admin", + "mobile": "Mobil", + "image": "Bild", + "tool_filter": "Filter", + "team": "Team", + "search": "Suche", + "search_no_results": "Keine Resultate", + "search_clear": "Zurücksetzen", + "rename": "Umbennen", + "login_google": "Mit Google anmelden", + "save_changes": "Änderungen speichern", + "what_is_your_name": "Willkommen bei %s ! Bitte wähle einen Benutzernamen.", + "landing_title": "Dein Online-Whiteboard.", + "landing_claim": "Mit Spacedeck kannst du multimedial auf virtuellen Whiteboards im Internet zusammenarbeiten: Kombiniere Texte, Fotos, Websites oder sogar Videos und Sounds. ", + "landing_example": "Spacedeck ist ideal, um Ideen zu visualisieren, in kreativen Teams Projekte zu überblicken oder um den Unterricht in Schulen und Universitäten interaktiv zu gestalten.", + "spaces": "Meine Spaces", + "access_editor_link": "Sofort-Mitmachen-Link", + "access_editor_link_desc": "Mit diesem Link kann man sogar ohne Spacedeck-Account sofort mitarbeiten. Praktisch!", + "access_editor_link_desc_slug": "Dieser Link beinhaltet den Namen vom Space.", + "access_anonymous_edit_blocking": "Anonyme Mitarbeiter dürfen keine Daten anderer anonymer Mitarbeiter ändern.", + "access_current_members": "Aktuelle Mitarbeiter", + "access_new_members": "Neue Mitarbeiter einladen", + "landing_customers": "Tausende Anwender weltweit vertrauen uns.", + "landing_features_title": "Schneller zum Ergebnis.", + "landing_features_text": "Spacedeck 5 hat eine brandneue Benutzeroberfläche, die das Arbeiten einfacher und intuitiver und macht - gleichzeitig aber auch mehr mächtige Werkzeuge bereitstellt.", + "landing_features_1": "Drag & Drop: Bilder-, Video- und Ton-Dateien direkt vom Desktop oder von anderen Webseiten in Spaces ziehen", + "landing_features_2": "Textnotizen mit allen Möglichkeiten bei Schriftart, Farbe und Stil", + "landing_features_3": "Zeichne und Markiere freihändig oder mit fertigen Formen", + "landing_features_4": "Verwandle dein Whiteboard in eine zoombare Präsentation", + "landing_features_5": "Arbeite in Echtzeit mit deinen Kollegen, Schülern oder Freunden zusammen", + "landing_features_6": "Teile deine Whiteboards per Link oder per E-Mail", + "landing_features_7": "Exportiere deine Arbeit als PDF- oder ZIP-Datei", + "landing_pricing": "Unfassbar günstig.", + "landing_pricing_lite": "Private Nutzung", + "landing_pricing_lite_text": "Basisvariante, ausreichend um multimedial zu arbeiten.", + "landing_pricing_pro_features_list": "