From 670913698776e09b7ff44f44ccdcf56303d79be3 Mon Sep 17 00:00:00 2001
From: Vitaly Takmazov
Date: Thu, 6 Sep 2018 13:58:40 +0300
Subject: merge legacy www
---
juick-server/src/main/assets/embed.js | 336 ++++++++
juick-server/src/main/assets/logo.png | Bin 0 -> 2447 bytes
juick-server/src/main/assets/logo@2x.png | Bin 0 -> 4822 bytes
juick-server/src/main/assets/scripts.js | 805 +++++++++++++++++
juick-server/src/main/assets/style.css | 952 +++++++++++++++++++++
.../java/com/juick/server/TelegramBotManager.java | 1 +
.../java/com/juick/server/api/ApiSocialLogin.java | 302 +++++++
.../src/main/java/com/juick/server/api/Index.java | 4 +-
.../main/java/com/juick/server/api/Messages.java | 12 +-
.../java/com/juick/server/api/Notifications.java | 14 +-
.../src/main/java/com/juick/server/api/PM.java | 6 +-
.../src/main/java/com/juick/server/api/Post.java | 12 +-
.../main/java/com/juick/server/api/Service.java | 2 +-
.../java/com/juick/server/api/SocialLogin.java | 302 -------
.../src/main/java/com/juick/server/api/Tags.java | 2 +-
.../src/main/java/com/juick/server/api/Users.java | 12 +-
.../com/juick/server/api/activity/Profile.java | 14 +-
.../server/configuration/ApiSecurityConfig.java | 127 ---
.../com/juick/server/configuration/PostConfig.java | 9 -
.../server/configuration/SapeConfiguration.java | 37 +
.../juick/server/configuration/SecurityConfig.java | 209 +++++
.../server/configuration/WwwAppConfiguration.java | 116 +++
.../java/com/juick/server/www/HelpService.java | 69 ++
.../src/main/java/com/juick/server/www/Utils.java | 45 +
.../src/main/java/com/juick/server/www/WebApp.java | 71 ++
.../server/www/controllers/AnythingFilter.java | 64 ++
.../server/www/controllers/AppSiteAssociation.java | 49 ++
.../com/juick/server/www/controllers/Help.java | 93 ++
.../com/juick/server/www/controllers/Login.java | 50 ++
.../juick/server/www/controllers/MessagesWWW.java | 595 +++++++++++++
.../juick/server/www/controllers/NewMessage.java | 263 ++++++
.../com/juick/server/www/controllers/Settings.java | 262 ++++++
.../com/juick/server/www/controllers/SignUp.java | 172 ++++
.../juick/server/www/controllers/SocialLogin.java | 321 +++++++
.../java/com/juick/server/www/facebook/User.java | 125 +++
.../java/com/juick/server/www/twitter/User.java | 38 +
.../main/java/com/juick/server/www/vk/Token.java | 56 ++
.../main/java/com/juick/server/www/vk/User.java | 65 ++
.../com/juick/server/www/vk/UsersResponse.java | 38 +
.../java/com/juick/service/EmailServiceImpl.java | 16 +
.../pebble/extension/FormatterExtension.java | 38 +
.../extension/filters/FormatMessageFilter.java | 54 ++
.../pebble/extension/filters/PrettyTimeFilter.java | 51 ++
.../pebble/extension/filters/TagsListFilter.java | 43 +
.../pebble/extension/filters/TimestampFilter.java | 25 +
juick-server/src/main/java/ru/sape/Sape.java | 23 +
.../src/main/java/ru/sape/SapeConnection.java | 108 +++
.../src/main/java/ru/sape/SapePageLinks.java | 76 ++
juick-server/src/main/resources/errors.properties | 3 +
.../src/main/resources/errors_ru.properties | 3 +
juick-server/src/main/resources/help | 1 +
.../src/main/resources/messages.properties | 80 ++
.../src/main/resources/messages_ru.properties | 78 ++
juick-server/src/main/resources/schema.sql | 6 +-
juick-server/src/main/resources/static/favicon.png | Bin 0 -> 244 bytes
juick-server/src/main/resources/static/logo.png | Bin 0 -> 1184 bytes
juick-server/src/main/resources/static/style.js | 2 +
.../src/main/resources/static/style.js.map | 1 +
.../src/main/resources/static/tagscloud.png | Bin 0 -> 42316 bytes
.../main/resources/templates/layouts/content.html | 50 ++
.../main/resources/templates/layouts/default.html | 16 +
.../main/resources/templates/layouts/minimal.html | 10 +
.../src/main/resources/templates/views/404.html | 11 +
.../src/main/resources/templates/views/blog.html | 25 +
.../main/resources/templates/views/blog_tags.html | 10 +
.../src/main/resources/templates/views/help.html | 10 +
.../src/main/resources/templates/views/index.html | 29 +
.../src/main/resources/templates/views/login.html | 144 ++++
.../resources/templates/views/login_success.html | 13 +
.../resources/templates/views/macros/tags.html | 5 +
.../resources/templates/views/partial/footer.html | 16 +
.../templates/views/partial/homecolumn.html | 25 +
.../resources/templates/views/partial/message.html | 76 ++
.../templates/views/partial/navigation.html | 40 +
.../templates/views/partial/settings_tabs.html | 6 +
.../templates/views/partial/tagcolumn.html | 33 +
.../resources/templates/views/partial/tags.html | 3 +
.../templates/views/partial/usercolumn.html | 89 ++
.../templates/views/partial/usertags.html | 3 +
.../main/resources/templates/views/pm_inbox.html | 35 +
.../main/resources/templates/views/pm_sent.html | 33 +
.../src/main/resources/templates/views/post.html | 19 +
.../resources/templates/views/post_success.html | 19 +
.../resources/templates/views/settings_about.html | 20 +
.../templates/views/settings_auth-email.html | 9 +
.../resources/templates/views/settings_main.html | 151 ++++
.../templates/views/settings_password.html | 17 +
.../templates/views/settings_privacy.html | 9 +
.../resources/templates/views/settings_result.html | 9 +
.../src/main/resources/templates/views/signup.html | 43 +
.../src/main/resources/templates/views/test.html | 2 +
.../src/main/resources/templates/views/thread.html | 173 ++++
.../src/main/resources/templates/views/users.html | 17 +
93 files changed, 6948 insertions(+), 480 deletions(-)
create mode 100644 juick-server/src/main/assets/embed.js
create mode 100644 juick-server/src/main/assets/logo.png
create mode 100644 juick-server/src/main/assets/logo@2x.png
create mode 100644 juick-server/src/main/assets/scripts.js
create mode 100644 juick-server/src/main/assets/style.css
create mode 100644 juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java
delete mode 100644 juick-server/src/main/java/com/juick/server/api/SocialLogin.java
delete mode 100644 juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java
delete mode 100644 juick-server/src/main/java/com/juick/server/configuration/PostConfig.java
create mode 100644 juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java
create mode 100644 juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java
create mode 100644 juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/HelpService.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/Utils.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/WebApp.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/Help.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/Login.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/Settings.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/facebook/User.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/twitter/User.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/vk/Token.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/vk/User.java
create mode 100644 juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java
create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java
create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java
create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java
create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java
create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java
create mode 100644 juick-server/src/main/java/ru/sape/Sape.java
create mode 100644 juick-server/src/main/java/ru/sape/SapeConnection.java
create mode 100644 juick-server/src/main/java/ru/sape/SapePageLinks.java
create mode 100644 juick-server/src/main/resources/errors.properties
create mode 100644 juick-server/src/main/resources/errors_ru.properties
create mode 160000 juick-server/src/main/resources/help
create mode 100644 juick-server/src/main/resources/messages.properties
create mode 100644 juick-server/src/main/resources/messages_ru.properties
create mode 100644 juick-server/src/main/resources/static/favicon.png
create mode 100644 juick-server/src/main/resources/static/logo.png
create mode 100644 juick-server/src/main/resources/static/style.js
create mode 100644 juick-server/src/main/resources/static/style.js.map
create mode 100644 juick-server/src/main/resources/static/tagscloud.png
create mode 100644 juick-server/src/main/resources/templates/layouts/content.html
create mode 100644 juick-server/src/main/resources/templates/layouts/default.html
create mode 100644 juick-server/src/main/resources/templates/layouts/minimal.html
create mode 100644 juick-server/src/main/resources/templates/views/404.html
create mode 100644 juick-server/src/main/resources/templates/views/blog.html
create mode 100644 juick-server/src/main/resources/templates/views/blog_tags.html
create mode 100644 juick-server/src/main/resources/templates/views/help.html
create mode 100644 juick-server/src/main/resources/templates/views/index.html
create mode 100644 juick-server/src/main/resources/templates/views/login.html
create mode 100644 juick-server/src/main/resources/templates/views/login_success.html
create mode 100644 juick-server/src/main/resources/templates/views/macros/tags.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/footer.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/homecolumn.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/message.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/navigation.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/settings_tabs.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/tagcolumn.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/tags.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/usercolumn.html
create mode 100644 juick-server/src/main/resources/templates/views/partial/usertags.html
create mode 100644 juick-server/src/main/resources/templates/views/pm_inbox.html
create mode 100644 juick-server/src/main/resources/templates/views/pm_sent.html
create mode 100644 juick-server/src/main/resources/templates/views/post.html
create mode 100644 juick-server/src/main/resources/templates/views/post_success.html
create mode 100644 juick-server/src/main/resources/templates/views/settings_about.html
create mode 100644 juick-server/src/main/resources/templates/views/settings_auth-email.html
create mode 100644 juick-server/src/main/resources/templates/views/settings_main.html
create mode 100644 juick-server/src/main/resources/templates/views/settings_password.html
create mode 100644 juick-server/src/main/resources/templates/views/settings_privacy.html
create mode 100644 juick-server/src/main/resources/templates/views/settings_result.html
create mode 100644 juick-server/src/main/resources/templates/views/signup.html
create mode 100644 juick-server/src/main/resources/templates/views/test.html
create mode 100644 juick-server/src/main/resources/templates/views/thread.html
create mode 100644 juick-server/src/main/resources/templates/views/users.html
(limited to 'juick-server/src/main')
diff --git a/juick-server/src/main/assets/embed.js b/juick-server/src/main/assets/embed.js
new file mode 100644
index 00000000..25c37142
--- /dev/null
+++ b/juick-server/src/main/assets/embed.js
@@ -0,0 +1,336 @@
+
+function insertAfter(newNode, referenceNode) {
+ referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
+}
+
+function setContent(containerNode, ...newNodes) {
+ removeAllFrom(containerNode);
+ newNodes.forEach(n => containerNode.appendChild(n));
+ return containerNode;
+}
+
+function removeAllFrom(fromNode) {
+ for (let c; c = fromNode.lastChild; ) { fromNode.removeChild(c); }
+}
+
+function htmlEscape(html) {
+ let textarea = document.createElement('textarea');
+ textarea.textContent = html;
+ return textarea.innerHTML;
+}
+
+// rules :: [{pr: number, re: RegExp, with: string}]
+// rules :: [{pr: number, re: RegExp, with: Function}]
+// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string]}]
+// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string, Function]}]
+function formatText(txt, rules) {
+ let idCounter = 0;
+ function nextId() { return idCounter++; }
+ function ft(txt, rules) {
+ let matches = rules.map(r => { r.re.lastIndex = 0; return [r, r.re.exec(txt)]; })
+ .filter(([,m]) => m !== null)
+ .sort(([r1,m1],[r2,m2]) => (r1.pr - r2.pr) || (m1.index - m2.index));
+ if (matches && matches.length > 0) {
+ let [rule, match] = matches[0];
+ let subsequentRules = rules.filter(r => r.pr >= rule.pr);
+ let idStr = `<>(${nextId()})<>`;
+ let outerStr = txt.substring(0, match.index) + idStr + txt.substring(rule.re.lastIndex);
+ let innerStr = (rule.brackets)
+ ? (() => { let [l ,r ,f] = rule.with; return l + ft((f ? f(match[1]) : match[1]), subsequentRules) + r; })()
+ : match[0].replace(rule.re, rule.with);
+ return ft(outerStr, subsequentRules).replace(idStr, innerStr);
+ }
+ return txt;
+ }
+ return ft(htmlEscape(txt), rules); // idStr above relies on the fact the text is escaped
+}
+
+function fixWwwLink(url) {
+ return url.replace(/^(?!([a-z]+:)?\/\/)/i, '//');
+}
+
+function makeNewNode(embedType, aNode, reResult) {
+ const withClasses = el => {
+ if (embedType.className) {
+ el.classList.add(...embedType.className.split(' '));
+ }
+ return el;
+ };
+ return embedType.makeNode(aNode, reResult, withClasses(document.createElement('div')));
+}
+
+function makeIframe(src, w, h, scrolling='no') {
+ let iframe = document.createElement('iframe');
+ iframe.style.width = w;
+ iframe.style.height = h;
+ iframe.frameBorder = 0;
+ iframe.scrolling = scrolling;
+ iframe.setAttribute('allowFullScreen', '');
+ iframe.src = src;
+ iframe.innerHTML = 'Cannot show iframes.';
+ return iframe;
+}
+
+function makeResizableToRatio(element, ratio) {
+ element.dataset['ratio'] = ratio;
+ makeResizable(element, w => w * element.dataset['ratio']);
+}
+
+// calcHeight :: Number -> Number -- calculate element height for a given width
+function makeResizable(element, calcHeight) {
+ const setHeight = el => {
+ if (document.body.contains(el) && (el.offsetWidth > 0)) {
+ el.style.height = (calcHeight(el.offsetWidth)).toFixed(2) + 'px';
+ }
+ };
+ window.addEventListener('resize', () => setHeight(element));
+ setHeight(element);
+}
+
+function extractDomain(url) {
+ const domainRe = /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/i;
+ return domainRe.exec(url)[1];
+}
+
+function urlReplace(match, p1, p2, p3) {
+ let isBrackets = (p1 !== undefined);
+ return (isBrackets)
+ ? `${p1} `
+ : `${extractDomain(match)} `;
+}
+
+function urlReplaceInCode(match, p1, p2, p3) {
+ let isBrackets = (p1 !== undefined);
+ return (isBrackets)
+ ? `${match} `
+ : `${match} `;
+}
+
+function messageReplyReplace(messageId) {
+ return function(match, mid, rid) {
+ let replyPart = (rid && rid != '0') ? '#' + rid : '';
+ return `${match} `;
+ };
+}
+
+/**
+ * Given "txt" message in unescaped plaintext with Juick markup, this function
+ * returns escaped formatted HTML string.
+ *
+ * @param {string} txt
+ * @param {string} messageId - current message id
+ * @param {boolean} isCode
+ * @returns {string}
+ */
+function juickFormat(txt, messageId, isCode) {
+ const urlRe = /(?:\[([^\]\[]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))/gi;
+ const bqReplace = m => m.replace(/^(?:>|>)\s?/gmi, '');
+ return (isCode)
+ ? formatText(txt, [
+ { pr: 1, re: urlRe, with: urlReplaceInCode },
+ { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) },
+ { pr: 1, re: /\B@([\w-]+)\b/gi, with: '@$1 ' },
+ ])
+ : formatText(txt, [
+ { pr: 0, re: /((?:^(?:>|>)\s?[\s\S]+?$\n?)+)/gmi, brackets: true, with: ['', ' ', bqReplace] },
+ { pr: 1, re: urlRe, with: urlReplace },
+ { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) },
+ { pr: 1, re: /\B@([\w-]+)\b/gi, with: '@$1 ' },
+ { pr: 2, re: /\B\*([^\n]+?)\*((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ' '] },
+ { pr: 2, re: /\B\/([^\n]+?)\/((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ' '] },
+ { pr: 2, re: /\b\_([^\n]+?)\_((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ' '] },
+ { pr: 3, re: /\n/g, with: ' ' },
+ ]);
+}
+
+function getEmbeddableLinkTypes() {
+ return [
+ {
+ name: 'Jpeg and png images',
+ id: 'embed_jpeg_and_png_images',
+ className: 'picture compact',
+ ctsDefault: false,
+ re: /\.(jpe?g|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ div.innerHTML = ` `;
+ return div;
+ }
+ },
+ {
+ name: 'Gif images',
+ id: 'embed_gif_images',
+ className: 'picture compact',
+ ctsDefault: true,
+ re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ div.innerHTML = ` `;
+ return div;
+ }
+ },
+ {
+ name: 'Video (webm, mp4, ogv)',
+ id: 'embed_webm_and_mp4_videos',
+ className: 'video compact',
+ ctsDefault: false,
+ re: /\.(webm|mp4|m4v|ogv)(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ div.innerHTML = ` `;
+ return div;
+ }
+ },
+ {
+ name: 'Audio (mp3, ogg, weba, opus, m4a, oga, wav)',
+ id: 'embed_sound_files',
+ className: 'audio singleColumn',
+ ctsDefault: false,
+ re: /\.(mp3|ogg|weba|opus|m4a|oga|wav)(?:\?[\w&;\?=]*)?$/i,
+ makeNode: function(aNode, reResult, div) {
+ div.innerHTML = ` `;
+ return div;
+ }
+ },
+ {
+ name: 'YouTube videos (and playlists)',
+ id: 'embed_youtube_videos',
+ className: 'youtube resizableV singleColumn',
+ ctsDefault: false,
+ re: /^(?:https?:)?\/\/(?:www\.|m\.|gaming\.)?(?:youtu(?:(?:\.be\/|be\.com\/(?:v|embed)\/)([-\w]+)|be\.com\/watch)((?:(?:\?|&(?:amp;)?)(?:\w+=[-\.\w]*[-\w]))*)|youtube\.com\/playlist\?list=([-\w]*)(&(amp;)?[-\w\?=]*)?)/i,
+ makeNode: function(aNode, reResult, div) {
+ let [url, v, args, plist] = reResult;
+ let iframeUrl;
+ if (plist) {
+ iframeUrl = '//www.youtube-nocookie.com/embed/videoseries?list=' + plist;
+ } else {
+ let pp = {}; args.replace(/^\?/, '')
+ .split('&')
+ .map(s => s.split('='))
+ .forEach(z => pp[z[0]] = z[1]);
+ let embedArgs = { rel: '0' };
+ if (pp.t) {
+ const tre = /^(?:(\d+)|(?:(\d+)h)?(?:(\d+)m)?(\d+)s|(?:(\d+)h)?(\d+)m|(\d+)h)$/i;
+ let [, t, h, m, s, h1, m1, h2] = tre.exec(pp.t);
+ embedArgs['start'] = (+t) || ((+(h || h1 || h2 || 0))*60*60 + (+(m || m1 || 0))*60 + (+(s || 0)));
+ }
+ if (pp.list) {
+ embedArgs['list'] = pp.list;
+ }
+ v = v || pp.v;
+ let argsStr = Object.keys(embedArgs)
+ .map(k => `${k}=${embedArgs[k]}`)
+ .join('&');
+ iframeUrl = `//www.youtube-nocookie.com/embed/${v}?${argsStr}`;
+ }
+ let iframe = makeIframe(iframeUrl, '100%', '360px');
+ iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0);
+ return setContent(div, iframe);
+ }
+ },
+ {
+ name: 'Vimeo videos',
+ id: 'embed_vimeo_videos',
+ className: 'vimeo resizableV',
+ ctsDefault: false,
+ re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/|album\/[\d]+\/video\/)?([\d]+)/i,
+ makeNode: function(aNode, reResult, div) {
+ let iframe = makeIframe('//player.vimeo.com/video/' + reResult[1], '100%', '360px');
+ iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0);
+ return setContent(div, iframe);
+ }
+ }
+ ];
+}
+
+function embedLink(aNode, linkTypes, container, afterNode) {
+ let anyEmbed = false;
+ let linkId = (aNode.href.replace(/^https?:/i, '').replace(/\'/gi,''));
+ let sameEmbed = container.querySelector(`*[data-linkid='${linkId}']`); // do not embed the same thing twice
+ if (sameEmbed === null) {
+ anyEmbed = [].some.call(linkTypes, function(linkType) {
+ let reResult = linkType.re.exec(aNode.href);
+ if (reResult) {
+ if (linkType.match && (linkType.match(aNode, reResult) === false)) { return false; }
+ let newNode = makeNewNode(linkType, aNode, reResult);
+ if (!newNode) { return false; }
+ newNode.setAttribute('data-linkid', linkId);
+ if (afterNode) {
+ insertAfter(newNode, afterNode);
+ } else {
+ container.appendChild(newNode);
+ }
+ aNode.classList.add('embedLink');
+ return true;
+ }
+ });
+ }
+ return anyEmbed;
+}
+
+function embedLinks(aNodes, container) {
+ let anyEmbed = false;
+ let embeddableLinkTypes = getEmbeddableLinkTypes();
+ Array.from(aNodes).forEach(aNode => {
+ let isEmbedded = embedLink(aNode, embeddableLinkTypes, container);
+ anyEmbed = anyEmbed || isEmbedded;
+ });
+ return anyEmbed;
+}
+
+/**
+ * Embed all the links inside element "x" that match to "allLinksSelector".
+ * All the embedded media is placed inside "div.embedContainer".
+ * "div.embedContainer" is inserted before an element matched by "beforeNodeSelector"
+ * if not present. Existing container is used otherwise.
+ *
+ * @param {Element} x
+ * @param {string} beforeNodeSelector
+ * @param {string} allLinksSelector
+ */
+function embedLinksToX(x, beforeNodeSelector, allLinksSelector) {
+ let isCtsPost = false;
+ let allLinks = x.querySelectorAll(allLinksSelector);
+
+ let existingContainer = x.querySelector('div.embedContainer');
+ if (existingContainer) {
+ embedLinks(allLinks, existingContainer);
+ } else {
+ let embedContainer = document.createElement('div');
+ embedContainer.className = 'embedContainer';
+
+ let anyEmbed = embedLinks(allLinks, embedContainer);
+ if (anyEmbed) {
+ let beforeNode = x.querySelector(beforeNodeSelector);
+ x.insertBefore(embedContainer, beforeNode);
+ }
+ }
+}
+
+function embedLinksToArticles() {
+ let beforeNodeSelector = 'nav.l';
+ let allLinksSelector = 'p:not(.ir) a, pre a';
+ Array.from(document.querySelectorAll('#content article')).forEach(article => {
+ embedLinksToX(article, beforeNodeSelector, allLinksSelector);
+ });
+}
+
+function embedLinksToPost() {
+ let beforeNodeSelector = '.msg-txt + *';
+ let allLinksSelector = '.msg-txt a';
+ Array.from(document.querySelectorAll('#content .msg-cont')).forEach(msg => {
+ embedLinksToX(msg, beforeNodeSelector, allLinksSelector);
+ });
+}
+
+/**
+ * Embed all the links in all messages/replies on the page.
+ */
+function embedAll() {
+ if (document.querySelector('#content article[data-mid]')) {
+ embedLinksToArticles();
+ } else {
+ embedLinksToPost();
+ }
+}
+
+exports.embedAll = embedAll;
+exports.embedLinksToX = embedLinksToX;
+exports.format = juickFormat;
diff --git a/juick-server/src/main/assets/logo.png b/juick-server/src/main/assets/logo.png
new file mode 100644
index 00000000..4e0f6d56
Binary files /dev/null and b/juick-server/src/main/assets/logo.png differ
diff --git a/juick-server/src/main/assets/logo@2x.png b/juick-server/src/main/assets/logo@2x.png
new file mode 100644
index 00000000..6febeaf9
Binary files /dev/null and b/juick-server/src/main/assets/logo@2x.png differ
diff --git a/juick-server/src/main/assets/scripts.js b/juick-server/src/main/assets/scripts.js
new file mode 100644
index 00000000..54e7958e
--- /dev/null
+++ b/juick-server/src/main/assets/scripts.js
@@ -0,0 +1,805 @@
+require('whatwg-fetch');
+require('element-closest');
+require('classlist.js');
+require('url-search-params-polyfill');
+let Awesomplete = require('awesomplete');
+import * as killy from './embed';
+
+if (!('remove' in Element.prototype)) { // Firefox <23
+ Element.prototype.remove = function () {
+ if (this.parentNode) {
+ this.parentNode.removeChild(this);
+ }
+ };
+}
+
+NodeList.prototype.forEach = Array.prototype.forEach;
+HTMLCollection.prototype.forEach = Array.prototype.forEach;
+
+NodeList.prototype.filter = Array.prototype.filter;
+HTMLCollection.prototype.filter = Array.prototype.filter;
+
+Element.prototype.selectText = function () {
+ let d = document;
+ if (d.body.createTextRange) {
+ let range = d.body.createTextRange();
+ range.moveToElementText(this);
+ range.select();
+ } else if (window.getSelection) {
+ let selection = window.getSelection();
+ let rangeSel = d.createRange();
+ rangeSel.selectNodeContents(this);
+ selection.removeAllRanges();
+ selection.addRange(rangeSel);
+ }
+};
+
+function autosize(el) {
+ let offset = (!window.opera)
+ ? (el.offsetHeight - el.clientHeight)
+ : (el.offsetHeight + parseInt(window.getComputedStyle(el, null).getPropertyValue('border-top-width')));
+
+ let resize = function (el) {
+ el.style.height = 'auto';
+ el.style.height = (el.scrollHeight + offset) + 'px';
+ };
+
+ if (el.addEventListener) {
+ el.addEventListener('input', () => resize(el));
+ } else if (el.attachEvent) {
+ el.attachEvent('onkeyup', () => resize(el));
+ }
+}
+
+function evilIcon(name) {
+ return `
`;
+}
+
+/* eslint-disable only-ascii/only-ascii */
+const translations = {
+ 'en': {
+ 'message.inReplyTo': 'in reply to',
+ 'message.reply': 'Reply',
+ 'message.likeThisMessage?': 'Recommend this message?',
+ 'postForm.pleaseInputMessageText': 'Please input message text',
+ 'postForm.upload': 'Upload',
+ 'postForm.newMessage': 'New message...',
+ 'postForm.imageLink': 'Link to image',
+ 'postForm.imageFormats': 'JPG/PNG, up to 10 MB',
+ 'postForm.or': 'or',
+ 'postForm.tags': 'Tags (space separated)',
+ 'postForm.submit': 'Send',
+ 'comment.writeComment': 'Write a comment...',
+ 'shareDialog.linkToMessage': 'Link to message',
+ 'shareDialog.messageNumber': 'Message number',
+ 'shareDialog.share': 'Share',
+ 'loginDialog.pleaseIntroduceYourself': 'Please introduce yourself',
+ 'loginDialog.registeredAlready': 'Registered already?',
+ 'loginDialog.username': 'Username',
+ 'loginDialog.password': 'Password',
+ 'loginDialog.facebook': 'Login with Facebook',
+ 'loginDialog.vk': 'Login with VK',
+ 'loginDialog.email': 'Registration',
+ 'error.error': 'Error'
+ },
+ 'ru': {
+ 'message.inReplyTo': 'в ответ на',
+ 'message.reply': 'Ответить',
+ 'message.likeThisMessage?': 'Рекомендовать это сообщение?',
+ 'postForm.pleaseInputMessageText': 'Пожалуйста, введите текст сообщения',
+ 'postForm.upload': 'загрузить',
+ 'postForm.newMessage': 'Новое сообщение...',
+ 'postForm.imageLink': 'Ссылка на изображение',
+ 'postForm.imageFormats': 'JPG/PNG, до 10Мб',
+ 'postForm.or': 'или',
+ 'postForm.tags': 'Теги (через пробел)',
+ 'postForm.submit': 'Отправить',
+ 'comment.writeComment': 'Написать комментарий...',
+ 'shareDialog.linkToMessage': 'Ссылка на сообщение',
+ 'shareDialog.messageNumber': 'Номер сообщения',
+ 'shareDialog.share': 'Поделиться',
+ 'loginDialog.pleaseIntroduceYourself': 'Пожалуйста, представьтесь',
+ 'loginDialog.registeredAlready': 'Уже зарегистрированы?',
+ 'loginDialog.username': 'Имя пользователя',
+ 'loginDialog.password': 'Пароль',
+ 'loginDialog.facebook': 'Войти через Facebook',
+ 'loginDialog.vk': 'Войти через ВКонтакте',
+ 'loginDialog.email': 'Регистрация',
+ 'error.error': 'Ошибка'
+ }
+};
+/* eslint-enable only-ascii/only-ascii */
+
+function getLang() {
+ return (window.navigator.languages && window.navigator.languages[0])
+ || window.navigator.userLanguage
+ || window.navigator.language;
+}
+function i18n(key, lang = undefined) {
+ const fallbackLang = 'ru';
+ lang = lang || getLang().split('-')[0];
+ return (translations[lang] && translations[lang][key])
+ || translations[fallbackLang][key]
+ || key;
+}
+
+var ws,
+ pageTitle;
+
+function initWS() {
+ let url = (window.location.protocol === 'https:' ? 'wss' : 'ws') + ':'
+ + '//api.juick.com/ws/';
+ let hash = document.getElementById('body').getAttribute('data-hash');
+ if (hash) {
+ url += '?hash=' + hash;
+ } else {
+ let content = document.getElementById('content');
+ if (content) {
+ let pageMID = content.getAttribute('data-mid');
+ if (pageMID) {
+ url += pageMID;
+ }
+ }
+ }
+
+ ws = new WebSocket(url);
+ ws.onopen = function () {
+ console.log('online');
+ if (!document.querySelector('#wsthread')) {
+ var d = document.createElement('div');
+ d.id = 'wsthread';
+ d.addEventListener('click', nextReply);
+ document.querySelector('body').appendChild(d);
+ pageTitle = document.title;
+ }
+ };
+ ws.onclose = function () {
+ console.log('offline');
+ ws = false;
+ setTimeout(function () {
+ initWS();
+ }, 2000);
+ };
+ ws.onmessage = function (msg) {
+ if (msg.data == ' ') {
+ ws.send(' ');
+ } else {
+ try {
+ var jsonMsg = JSON.parse(msg.data);
+ console.log('data: ' + msg.data);
+ if (jsonMsg.service) {
+ return;
+ }
+ wsIncomingReply(jsonMsg);
+ } catch (err) {
+ console.log(err);
+ }
+ }
+ };
+ setInterval(wsSendKeepAlive, 90000);
+}
+
+function wsSendKeepAlive() {
+ if (ws) {
+ ws.send(' ');
+ }
+}
+
+function wsShutdown() {
+ if (ws) {
+ ws.onclose = function () { };
+ ws.close();
+ }
+}
+
+function wsIncomingReply(msg) {
+ let content = document.getElementById('content');
+ if (!content) { return; }
+ let pageMID = content.getAttribute('data-mid');
+ if (!pageMID || pageMID != msg.mid) { return; }
+ let msgNum = '/' + msg.rid;
+ if (msg.replyto > 0) {
+ msgNum += ` ${i18n('message.inReplyTo')} /${msg.replyto} `;
+ }
+ let photoDiv = (msg.attach == null) ? '' : `
+ `;
+ let msgContHtml = `
+
+
+
${killy.format(msg.body, msg.mid, false)}
${photoDiv}
+
+
+
`;
+
+ let li = document.createElement('li');
+ li.setAttribute('class', 'msg reply-new');
+ li.setAttribute('id', msg.rid);
+ li.innerHTML = msgContHtml;
+ li.addEventListener('click', newReply);
+ li.addEventListener('mouseover', newReply);
+ li.querySelector('a.msg-reply-link').addEventListener('click', function (e) {
+ showCommentForm(msg.mid, msg.rid);
+ e.preventDefault();
+ });
+
+ killy.embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a');
+
+ document.getElementById('replies').appendChild(li);
+
+ updateRepliesCounter();
+}
+
+function newReply(e) {
+ var li = e.target;
+ li.classList.remove('reply-new');
+ li.removeEventListener('click', e);
+ li.removeEventListener('mouseover', e);
+ updateRepliesCounter();
+}
+
+function nextReply() {
+ var li = document.querySelector('#replies>li.reply-new');
+ if (li) {
+ li.classList.remove('reply-new');
+ li.removeEventListener('click', this);
+ li.children[0].scrollIntoView();
+ updateRepliesCounter();
+ }
+}
+
+function updateRepliesCounter() {
+ var replies = document.querySelectorAll('#replies>li.reply-new').length;
+ var wsthread = document.getElementById('wsthread');
+ if (replies) {
+ wsthread.textContent = replies;
+ wsthread.style.display = 'block';
+ document.title = '[' + replies + '] ' + pageTitle;
+ } else {
+ wsthread.style.display = 'none';
+ document.title = pageTitle;
+ }
+}
+
+/******************************************************************************/
+/******************************************************************************/
+/******************************************************************************/
+
+function postformListener(formEl, ev) {
+ if (ev.ctrlKey && (ev.keyCode == 10 || ev.keyCode == 13)) {
+ let form = formEl.closest('form');
+ if (!form.onsubmit || form.onsubmit()) {
+ form.submit();
+ }
+ }
+}
+function closeDialogListener(ev) {
+ ev = ev || window.event;
+ if (ev.keyCode == 27) {
+ closeDialog();
+ }
+}
+
+function newMessage(evt) {
+ document.querySelectorAll('#newmessage .dialogtxt').forEach(t => {
+ t.remove();
+ });
+ if (document.querySelector('#newmessage textarea').value.length == 0
+ && document.querySelector('#newmessage .img').value.length == 0
+ && !document.querySelector('#newmessage input[type="file"]')) {
+ document.querySelector('#newmessage').insertAdjacentHTML('afterbegin', `${i18n('postForm.pleaseInputMessageText')}
`);
+ evt.preventDefault();
+ }
+}
+
+function showCommentForm(mid, rid) {
+ let reply = document.getElementById(rid);
+ let formTarget = reply.querySelector('div.msg-cont .msg-comment-target');
+ if (formTarget) {
+ let formHtml = `
+ `;
+ formTarget.insertAdjacentHTML('afterend', formHtml);
+ formTarget.remove();
+
+ let form = reply.querySelector('form');
+ let submitButton = form.querySelector('input[type="submit"]');
+
+ let attachButton = form.querySelector('.msg-comment .attach-photo');
+ attachButton.addEventListener('click', e => attachCommentPhoto(e.target));
+
+ let textarea = form.querySelector('.msg-comment textarea');
+ textarea.addEventListener('keypress', e => postformListener(e.target, e));
+ autosize(textarea);
+
+ let validateMessage = () => {
+ let len = textarea.value.length;
+ if (len > 4096) { return 'Message is too long'; }
+ return '';
+ };
+ form.addEventListener('submit', e => {
+ let validationResult = validateMessage();
+ if (validationResult) {
+ e.preventDefault();
+ alert(validationResult);
+ return false;
+ }
+ submitButton.disabled = true;
+ });
+ }
+ reply.querySelector('.msg-comment textarea').focus();
+}
+
+function attachInput() {
+ let inp = document.createElement('input');
+ inp.setAttribute('type', 'file');
+ inp.setAttribute('name', 'attach');
+ inp.setAttribute('accept', 'image/jpeg,image/png');
+ inp.style.visibility = 'hidden';
+ return inp;
+}
+
+function attachCommentPhoto(div) {
+ let input = div.querySelector('input');
+ if (input) {
+ input.remove();
+ div.classList.remove('attach-photo-active');
+ } else {
+ let newInput = attachInput();
+ newInput.addEventListener('change', function () {
+ div.classList.add('attach-photo-active');
+ });
+ newInput.click();
+ div.appendChild(newInput);
+ }
+}
+
+function attachMessagePhoto(div) {
+ var f = div.closest('form'),
+ finput = f.querySelector('input[type="file"]');
+ if (!finput) {
+ var inp = attachInput();
+ inp.style.float = 'left';
+ inp.style.width = 0;
+ inp.style.height = 0;
+ inp.addEventListener('change', function () {
+ div.textContent = i18n('postForm.upload') + ' (✓)';
+ });
+ f.appendChild(inp);
+ inp.click();
+ } else {
+ finput.remove();
+ div.textContent = i18n('postForm.upload');
+ }
+}
+
+function showMessageLinksDialog(mid, rid) {
+ let hlink = window.location.protocol + '//juick.com/' + mid;
+ let mlink = '#' + mid;
+ if (rid > 0) {
+ hlink += '#' + rid;
+ mlink += '/' + rid;
+ }
+ let hlinkenc = encodeURIComponent(hlink);
+ let html = `
+
+ ${i18n('shareDialog.linkToMessage')}:
${hlink}
+ ${i18n('shareDialog.messageNumber')}:
${mlink}
+ ${i18n('shareDialog.share')}:
+
+
`;
+
+ openDialog(html);
+}
+
+function showPhotoDialog(fname) {
+ let width = window.innerWidth;
+ let height = window.innerHeight;
+ let minDimension = (width < height) ? width : height;
+ if (minDimension < 640) {
+ return true; // no dialog, open the link
+ } else if (minDimension < 1280) {
+ openDialog(` `, true);
+ return false;
+ } else {
+ openDialog(` `, true);
+ return false;
+ }
+}
+
+function openPostDialog() {
+ let newmessageTemplate = `
+
+ `;
+ return openDialog(newmessageTemplate);
+}
+
+function openDialog(html, image) {
+ var dialogHtml = `
+ `;
+ let body = document.querySelector('body');
+ body.classList.add('dialog-opened');
+ body.insertAdjacentHTML('afterbegin', dialogHtml);
+ if (image) {
+ let header = document.querySelector('#dialog_header');
+ header.classList.add('header_image');
+ }
+ document.addEventListener('keydown', closeDialogListener);
+ document.querySelector('#dialogb').addEventListener('click', closeDialog);
+ document.querySelector('#dialogc').addEventListener('click', closeDialog);
+}
+
+function closeDialog() {
+ let draft = document.querySelector('#newmessage textarea');
+ if (draft) {
+ window.draft = draft.value;
+ }
+ document.querySelector('body').classList.remove('dialog-opened');
+ document.querySelector('#dialogb').remove();
+ document.querySelector('#dialogt').remove();
+}
+
+function openSocialWindow(a) {
+ var w = window.open(a.href, 'juickshare', 'width=640,height=400');
+ if (window.focus) { w.focus(); }
+ return false;
+}
+
+function checkUsername() {
+ var uname = document.querySelector('#username').textContent,
+ style = document.querySelector('#username').style;
+ fetch('//api.juick.com/users?uname=' + uname)
+ .then(function () {
+ style.background = '#FFCCCC';
+ })
+ .catch(function () {
+ style.background = '#CCFFCC';
+ });
+}
+
+/******************************************************************************/
+
+function openDialogLogin() {
+ let html = `
+ `;
+ openDialog(html);
+ return false;
+}
+
+/******************************************************************************/
+
+function resultMessage(str) {
+ var result = document.createElement('p');
+ result.textContent = str;
+ return result;
+}
+
+function likeMessage(e, mid) {
+ if (confirm(i18n('message.likeThisMessage?'))) {
+ fetch('//api.juick.com/like?mid=' + mid
+ + '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ method: 'POST',
+ credentials: 'same-origin'
+ })
+ .then(function (response) {
+ if (response.ok) {
+ e.closest('article').appendChild(resultMessage('OK!'));
+ }
+ })
+ .catch(function () {
+ e.closest('article').appendChild(resultMessage(i18n('error.error')));
+ });
+ }
+ return false;
+}
+
+/******************************************************************************/
+
+function setPopular(e, mid, popular) {
+ fetch('//api.juick.com/messages/set_popular?mid=' + mid
+ + '&popular=' + popular
+ + '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ credentials: 'same-origin'
+ })
+ .then(function () {
+ e.closest('article').append(resultMessage('OK!'));
+ });
+ return false;
+}
+
+function setPrivacy(e, mid) {
+ fetch('//api.juick.com/messages/set_privacy?mid=' + mid
+ + '&hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ credentials: 'same-origin'
+ })
+ .then(function () {
+ e.closest('article').append(resultMessage('OK!'));
+ });
+ return false;
+}
+
+function getTags() {
+ fetch('//api.juick.com/tags?hash=' + document.getElementById('body').getAttribute('data-hash'), {
+ credentials: 'same-origin'
+ })
+ .then(response => {
+ return response.json();
+ })
+ .then(json => {
+ let tags = json.map(t => t.tag);
+ let input = document.getElementById('tags_input');
+ new Awesomplete(input, { list: tags });
+ });
+ return false;
+}
+
+function addTag(tag) {
+ document.forms['postmsg'].body.value = '*' + tag + ' ' + document.forms['postmsg'].body.value;
+ return false;
+}
+
+/******************************************************************************/
+
+function ready(fn) {
+ if (document.readyState != 'loading') {
+ fn();
+ } else {
+ document.addEventListener('DOMContentLoaded', fn);
+ }
+}
+
+ready(function () {
+ document.querySelectorAll('textarea').forEach((ta) => {
+ autosize(ta);
+ });
+
+ var insertPMButtons = function (e) {
+ e.target.classList.add('narrowpm');
+ e.target.parentNode.insertAdjacentHTML('afterend', ' ');
+ e.target.removeEventListener('click', insertPMButtons);
+ e.preventDefault();
+ };
+ document.querySelectorAll('textarea.replypm').forEach(function (e) {
+ e.addEventListener('click', insertPMButtons);
+ e.addEventListener('keypress', function (e) {
+ postformListener(e.target, e);
+ });
+ });
+ document.querySelectorAll('#postmsg textarea').forEach(function (e) {
+ e.addEventListener('keypress', function (e) {
+ postformListener(e.target, e);
+ });
+ });
+
+ var content = document.getElementById('content');
+ if (content) {
+ var pageMID = content.getAttribute('data-mid');
+ if (pageMID > 0) {
+ document.querySelectorAll('li.msg').forEach(li => {
+ let showReplyFormBtn = li.querySelector('.a-thread-comment');
+ if (showReplyFormBtn) {
+ showReplyFormBtn.addEventListener('click', function (e) {
+ showCommentForm(pageMID, li.id);
+ e.preventDefault();
+ });
+ }
+ });
+ let opMessage = document.querySelector('.msgthread');
+ if (opMessage) {
+ let replyTextarea = opMessage.querySelector('textarea.reply');
+ if (replyTextarea) {
+ replyTextarea.addEventListener('focus', e => showCommentForm(pageMID, 0));
+ replyTextarea.addEventListener('keypress', e => postformListener(e.target, e));
+ if (!window.location.hash) {
+ replyTextarea.focus();
+ }
+ }
+ }
+ }
+ }
+
+ var postmsg = document.getElementById('postmsg');
+ if (postmsg) {
+ document.querySelectorAll('a').filter(t => t.href.indexOf('?') >= 0).forEach(t => {
+ t.addEventListener('click', e => {
+ let params = new URLSearchParams(t.href.slice(t.href.indexOf('?') + 1));
+ if (params.has('tag')) {
+ addTag(params.get('tag'));
+ e.preventDefault();
+ }
+ });
+ });
+ }
+
+ document.querySelectorAll('.msg-menu').forEach(function (el) {
+ el.addEventListener('click', function (e) {
+ var reply = e.target.closest('li');
+ var rid = reply ? parseInt(reply.id) : 0;
+ var message = e.target.closest('section');
+ var mid = message.getAttribute('data-mid') || e.target.closest('article').getAttribute('data-mid');
+ showMessageLinksDialog(mid, rid);
+ e.preventDefault();
+ });
+ });
+ document.querySelectorAll('.l .a-privacy').forEach(function (e) {
+ e.addEventListener('click', function (e) {
+ setPrivacy(
+ e.target,
+ e.target.closest('article').getAttribute('data-mid'));
+ e.preventDefault();
+ });
+ });
+ document.querySelectorAll('.ir a[data-fname], .msg-media a[data-fname]').forEach(function (el) {
+ el.addEventListener('click', function (e) {
+ let fname = e.target.closest('[data-fname]').getAttribute('data-fname');
+ if (!showPhotoDialog(fname)) {
+ e.preventDefault();
+ }
+ });
+ });
+ document.querySelectorAll('.social a').forEach(function (e) {
+ e.addEventListener('click', function (e) {
+ openSocialWindow(e.target);
+ e.preventDefault();
+ });
+ });
+ var username = document.getElementById('username');
+ if (username) {
+ username.addEventListener('blur', function () {
+ checkUsername();
+ });
+ }
+
+ document.querySelectorAll('.l .a-like').forEach(function (e) {
+ e.addEventListener('click', function (e) {
+ likeMessage(
+ e.target,
+ e.target.closest('article').getAttribute('data-mid'));
+ e.preventDefault();
+ });
+ });
+ document.querySelectorAll('.a-login').forEach(function (el) {
+ el.addEventListener('click', function (e) {
+ openDialogLogin();
+ e.preventDefault();
+ });
+ });
+ var unfoldall = document.getElementById('unfoldall');
+ if (unfoldall) {
+ unfoldall.addEventListener('click', function (e) {
+ document.querySelectorAll('#replies>li').forEach(function (e) {
+ e.style.display = 'block';
+ });
+ document.querySelectorAll('#replies .msg-comments').forEach(function (e) {
+ e.style.display = 'none';
+ });
+ e.preventDefault();
+ });
+ }
+ document.querySelectorAll('article').forEach(function (article) {
+ if (Array.prototype.some.call(
+ article.querySelectorAll('.msg-tags a'),
+ function (a) {
+ return a.textContent === 'NSFW';
+ }
+ )) {
+ article.classList.add('nsfw');
+ }
+ });
+ initWS();
+
+ window.addEventListener('pagehide', wsShutdown);
+
+ killy.embedAll();
+ var elSelector = 'header',
+ elClassHidden = 'header--hidden',
+ elClassBackground = 'header--background',
+ throttleTimeout = 500,
+ element = document.querySelector(elSelector);
+
+ if (element) {
+
+ var dHeight = 0,
+ wHeight = 0,
+ wScrollCurrent = 0,
+ wScrollBefore = 0,
+ wScrollDiff = 0,
+
+ throttle = function (delay, fn) {
+ var last, deferTimer;
+ return function () {
+ var context = this, args = arguments, now = +new Date;
+ if (last && now < last + delay) {
+ clearTimeout(deferTimer);
+ deferTimer = setTimeout(
+ function () {
+ last = now;
+ fn.apply(context, args);
+ },
+ delay);
+ } else {
+ last = now;
+ fn.apply(context, args);
+ }
+ };
+ };
+
+ window.addEventListener('scroll', throttle(throttleTimeout, function () {
+ dHeight = document.body.offsetHeight;
+ wHeight = window.innerHeight;
+ wScrollCurrent = window.pageYOffset;
+ wScrollDiff = wScrollBefore - wScrollCurrent;
+
+ if (wScrollCurrent <= 0) {
+ // scrolled to the very top; element sticks to the top
+ element.classList.remove(elClassHidden);
+ element.classList.remove(elClassBackground);
+ } else if (wScrollDiff > 0 && element.classList.contains(elClassHidden)) {
+ // scrolled up; element slides in
+ element.classList.remove(elClassHidden);
+ element.classList.add(elClassBackground);
+ } else if (wScrollDiff < 0) {
+ // scrolled down
+ if (wScrollCurrent + wHeight >= dHeight && element.classList.contains(elClassHidden)) {
+ // scrolled to the very bottom; element slides in
+ element.classList.remove(elClassHidden);
+ element.classList.add(elClassBackground);
+ } else {
+ // scrolled down; element slides out
+ element.classList.add(elClassHidden);
+ }
+ }
+
+ wScrollBefore = wScrollCurrent;
+ }));
+ }
+});
diff --git a/juick-server/src/main/assets/style.css b/juick-server/src/main/assets/style.css
new file mode 100644
index 00000000..d7cd2223
--- /dev/null
+++ b/juick-server/src/main/assets/style.css
@@ -0,0 +1,952 @@
+/* #region generic */
+
+html,
+body,
+div,
+h1,
+h2,
+ul,
+li,
+p,
+form,
+input,
+textarea,
+pre {
+ margin: 0;
+ padding: 0;
+}
+html,
+input,
+textarea {
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+ font-size: 12pt;
+ -webkit-font-smoothing: subpixel-antialiased;
+}
+h1,
+h2 {
+ font-weight: normal;
+}
+ul {
+ list-style-type: none;
+}
+a {
+ color: #069;
+ text-decoration: none;
+}
+img,
+hr {
+ border: none;
+}
+hr {
+ background: #CCC;
+ height: 1px;
+ margin: 10px 0;
+}
+pre {
+ background: #222;
+ color: #0f0;
+ overflow-x: auto;
+ padding: 6px;
+ white-space: pre;
+}
+pre::selection {
+ background: #0f0;
+ color: #222;
+}
+pre::-moz-selection {
+ background: #0f0;
+ color: #222;
+}
+.u {
+ text-decoration: underline;
+}
+
+/* #endregion */
+
+/* #region overall layout */
+
+html {
+ background: #f8f8f8;
+ color: #222;
+}
+#wrapper {
+ margin: 0 auto;
+ width: 1000px;
+ margin-top: 52px;
+}
+#column {
+ float: left;
+ margin-left: 10px;
+ overflow: hidden;
+ padding-top: 10px;
+ width: 240px;
+}
+#content {
+ float: right;
+ margin: 12px 0 0 0;
+ width: 728px;
+}
+#minimal_content {
+ margin: 0 auto;
+ min-width: 310px;
+ width: auto;
+}
+*::selection {
+ background: #006699;
+ color: #fff;
+}
+body > header {
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 10;
+ transition-duration: 0.5s;
+ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
+ transition-property: transform;
+}
+@supports (backdrop-filter: blur(10px)) {
+ body > header--background {
+ background: rgba(255, 255, 255, 0.8);
+ backdrop-filter: blur(10px);
+ }
+}
+#header_wrapper {
+ margin: 0 auto;
+ width: 1000px;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ flex-wrap: wrap;
+ padding: 4px;
+}
+.header--background {
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.28);
+ background: #fff;
+}
+.header--hidden {
+ transform: translateY(-100%);
+}
+#footer {
+ clear: both;
+ color: #999;
+ font-size: 10pt;
+ margin: 40px;
+ padding: 10px 0;
+}
+
+@media screen and (max-width: 850px) {
+ body {
+ text-size-adjust: 100%;
+ }
+ body,
+ #wrapper,
+ #topwrapper,
+ #content,
+ #footer {
+ float: none;
+ margin: 0 auto;
+ min-width: 310px;
+ width: auto;
+ }
+ #wrapper {
+ margin-top: 50px;
+ }
+ body > header {
+ margin-bottom: 15px;
+ }
+ #column {
+ float: none;
+ margin: 0 10px;
+ padding-top: 0;
+ width: auto;
+ }
+}
+
+/* #endregion */
+
+/* #region header internals */
+
+#logo {
+ height: 36px;
+ width: 110px;
+}
+#logo a {
+ background: url("logo@2x.png") no-repeat;
+ background-size: cover;
+ border: 0;
+ display: block;
+ height: 36px;
+ overflow: hidden;
+ text-indent: 100%;
+ white-space: nowrap;
+ width: 110px;
+}
+#global {
+ display: flex;
+}
+#global a {
+ color: #888;
+ display: inline-block;
+ font-size: 13pt;
+ padding: 14px 6px;
+}
+#global li {
+ display: inline-block;
+}
+#ctitle a {
+ padding: 14px;
+}
+#global li:hover,
+#ctitle a:hover,
+.l a:hover {
+ background-color: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ cursor: pointer;
+ transition: box-shadow 0.2s ease-in;
+}
+#search input {
+ background: #FFF;
+ border: 1px solid #ccc;
+ outline: none !important;
+ padding: 4px;
+ -webkit-appearance: none;
+ border-radius: 0;
+}
+
+/* #endregion */
+
+/* #region left column internals */
+
+.toolbar {
+ border-top: 1px solid #CCC;
+}
+
+#column ul,
+#column p,
+#column hr {
+ margin: 10px 0;
+}
+#column li > a {
+ display: block;
+ height: 100%;
+ padding: 6px;
+}
+#column li > a:hover {
+ background-color: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ transition: background-color 0.2s ease-in;
+}
+#column .margtop {
+ margin-top: 15px;
+}
+
+#column .tags {
+ background: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ line-height: 140%;
+ padding: 6px;
+ text-align: justify;
+}
+#column .inp {
+ background: #fff;
+ border: 1px solid #ddddd5;
+ outline: none !important;
+ padding: 4px;
+ width: 222px;
+}
+#column .tags h4 {
+ background: #eee;
+ border: 1px solid #eee;
+ color: #888;
+ display: block;
+ text-align: center;
+}
+#ctitle {
+ font-size: 14pt;
+}
+#ctitle img {
+ margin-right: 5px;
+ vertical-align: middle;
+ width: 48px;
+}
+#ustats li {
+ font-size: 10pt;
+ margin: 3px 0;
+}
+#column table.iread {
+ width: 100%;
+}
+#column table.iread td {
+ text-align: center;
+}
+#column table.iread img {
+ height: 48px;
+ width: 48px;
+}
+
+/* #endregion */
+
+/* #region main content */
+#content > p,
+#content > h1,
+#content > h2,
+#minimal_content > p,
+#minimal_content > h1,
+#minimal_content > h2 {
+ margin: 1em 0;
+}
+.page {
+ background: #eee;
+ padding: 6px;
+ text-align: center;
+}
+
+.page a {
+ color: #888;
+}
+
+/* #endregion */
+
+/* #region article, message internals */
+
+article {
+ background: #fff;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ line-height: 140%;
+ margin-bottom: 10px;
+ padding: 20px;
+}
+article time {
+ color: #999;
+ font-size: 10pt;
+}
+article p {
+ clear: left;
+ margin: 5px 0 15px 0;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+article .ir {
+ text-align: center;
+}
+article .ir a {
+ cursor: zoom-in;
+ display: block;
+}
+article .ir img {
+ max-width: 100%;
+}
+article > nav.l,
+.msg-cont > nav.l {
+ border-top: 1px solid #eee;
+ display: flex;
+ justify-content: space-around;
+ font-size: 10pt;
+}
+article > nav.l a,
+.msg-cont > nav.l a {
+ color: #888;
+ margin-right: 15px;
+}
+article .likes {
+ padding-left: 20px;
+}
+article .replies {
+ margin-left: 18px;
+}
+article .tags {
+ margin-top: 3px;
+}
+.msg-tags {
+ margin-top: 12px;
+ min-height: 1px;
+}
+article .tags > a,
+.badge,
+.msg-tags > a {
+ background: #eee;
+ border: 1px solid #eee;
+ color: #888;
+ display: inline-block;
+ font-size: 10pt;
+ margin-bottom: 5px;
+ margin-right: 5px;
+ padding: 0 10px;
+}
+.l .msg-button {
+ align-items: center;
+ display: flex;
+ flex-basis: 0;
+ flex-direction: column;
+ flex-grow: 1;
+ padding-top: 12px;
+}
+.l .msg-button-icon {
+ font-weight: bold;
+}
+.msgthread {
+ margin-bottom: 0;
+}
+.msg-avatar {
+ float: left;
+ height: 48px;
+ margin-right: 10px;
+ width: 48px;
+}
+.msg-avatar img {
+ height: 48px;
+ vertical-align: top;
+ width: 48px;
+}
+.msg-cont {
+ background: #FFF;
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.16);
+ line-height: 140%;
+ margin-bottom: 12px;
+ padding: 20px;
+ width: 640px;
+}
+.reply-new .msg-cont {
+ border-right: 5px solid #0C0;
+}
+.msg-ts {
+ font-size: small;
+ vertical-align: top;
+}
+.msg-ts,
+.msg-ts > a {
+ color: #999;
+}
+.msg-txt {
+ clear: both;
+ margin: 0 0 12px;
+ padding-top: 10px;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+}
+.msg-media {
+ text-align: center;
+}
+.msg-links {
+ color: #999;
+ font-size: small;
+ margin: 5px 0 0 0;
+}
+.msg-comments {
+ color: #AAA;
+ font-size: small;
+ margin-top: 10px;
+ overflow: hidden;
+ text-indent: 10px;
+}
+.ta-wrapper {
+ border: 1px solid #DDD;
+ display: flex;
+ flex-grow: 1;
+}
+.msg-comment {
+ display: flex;
+ width: 100%;
+ margin-top: 10px;
+}
+.msg-comment-hidden {
+ display: none;
+}
+.msg-comment textarea {
+ border: 0;
+ flex-grow: 1;
+ outline: none !important;
+ padding: 4px;
+ resize: vertical;
+ vertical-align: top;
+}
+.attach-photo {
+ cursor: pointer;
+}
+.attach-photo-active {
+ color: green;
+}
+.msg-comment input {
+ align-self: flex-start;
+ background: #EEE;
+ border: 1px solid #CCC;
+ color: #999;
+ margin: 0 0 0 6px;
+ position: -webkit-sticky;
+ position: sticky;
+ top: 70px;
+ vertical-align: top;
+ width: 50px;
+}
+.msg-recomms {
+ color: #AAA;
+ font-size: small;
+ margin-top: 10px;
+ overflow: hidden;
+ text-indent: 10px;
+}
+#replies .msg-txt,
+#private-messages .msg-txt {
+ margin: 0;
+}
+.title2 {
+ background: #fff;
+ margin: 20px 0;
+ padding: 10px 20px;
+ width: 640px;
+}
+.title2-right {
+ float: right;
+ line-height: 24px;
+}
+#content .title2 h2 {
+ font-size: x-large;
+ margin: 0;
+}
+
+@media screen and (max-width: 850px) {
+ #header_wrapper {
+ width: auto;
+ }
+ #global {
+ justify-content: space-around;
+ flex-grow: 1;
+ }
+ #search {
+ padding: 4px;
+ }
+ article {
+ overflow: auto;
+ }
+ article p {
+ margin: 10px 0 8px 0;
+ }
+ .msg,
+ .msg-cont {
+ min-width: 280px;
+ width: auto;
+ }
+ .msg-cont {
+ margin: 8px 0;
+ }
+ .msg-media {
+ overflow: auto;
+ }
+ .title2 h2 {
+ font-size: large;
+ }
+ .msg-comment {
+ flex-direction: column;
+ }
+ .msg-comment input {
+ align-self: flex-end;
+ margin: 6px 0 0 0;
+ width: 100px;
+ }
+}
+
+@media screen and (max-width: 480px) {
+ #wrapper {
+ margin-top: 104px;
+ }
+ #search {
+ display: none;
+ }
+ #global a {
+ padding: 14px 2px;
+ font-size: 11pt;
+ }
+ .msg-cont > nav.l,
+ article > nav.l {
+ font-size: 9pt;
+ }
+ .msg-txt {
+ padding-top: 5px;
+ }
+ .title2 {
+ font-size: 11pt;
+ width: auto;
+ }
+ #content .title2 h2 {
+ font-size: 11pt;
+ }
+ .title2-right {
+ line-height: initial;
+ }
+}
+
+/* #endregion */
+
+/* #region user-generated texts */
+
+q:before,
+q:after {
+ content: "";
+}
+q,
+blockquote {
+ border-left: 3px solid #CCC;
+ color: #666;
+ display: block;
+ margin: 10px 0 10px 10px;
+ padding-left: 10px;
+}
+
+/* #endregion */
+
+/* #region new message form internals */
+
+#newmessage {
+ background: #E5E5E0;
+ margin-bottom: 20px;
+ padding: 15px;
+}
+#newmessage textarea {
+ border: 1px solid #CCC;
+ box-sizing: border-box;
+ margin: 0 0 5px 0;
+ margin-top: 20px;
+ max-height: 6em;
+ min-width: 280px;
+ padding: 4px;
+ width: 100%;
+}
+#newmessage input {
+ border: 1px solid #CCC;
+ margin: 5px 0;
+ padding: 2px 4px;
+}
+#newmessage .img {
+ width: 500px;
+}
+#newmessage .tags {
+ width: 500px;
+}
+#newmessage .subm {
+ background: #EEEEE5;
+ width: 150px;
+}
+@media screen and (max-width: 850px) {
+ #newmessage .img,
+ #newmessage .tags {
+ width: 100%;
+ }
+}
+
+/* #endregion */
+
+/* #region user lists */
+
+.users {
+ margin: 10px 0;
+ width: 100%;
+ display: flex;
+ flex-wrap: wrap;
+}
+.users > span {
+ overflow: hidden;
+ padding: 6px 0;
+ width: 200px;
+}
+.users img {
+ height: 32px;
+ margin-right: 6px;
+ vertical-align: middle;
+ width: 32px;
+}
+
+/* #endregion */
+
+/* #region signup form */
+
+.signup-h1 > img {
+ margin-right: 10px;
+ vertical-align: middle;
+}
+.signup-h1 {
+ font-size: x-large;
+ margin: 20px 0 10px 0;
+}
+.signup-h2 {
+ font-size: large;
+ margin: 10px 0 5px 0;
+}
+.signup-hr {
+ margin: 20px 0;
+}
+
+/* #endregion */
+
+/* #region PM */
+
+.newpm {
+ margin: 20px 60px 30px 60px;
+}
+.newpm textarea {
+ resize: vertical;
+ width: 100%;
+}
+.newpm-send input {
+ width: 100px;
+}
+
+/* #endregion */
+
+/* #region popup dialog (lightbox) */
+
+#dialogb {
+ background: #222;
+ height: 100%;
+ left: 0;
+ opacity: 0.6;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 10;
+}
+#dialogt {
+ height: 100%;
+ left: 0;
+ position: fixed;
+ top: 0;
+ width: 100%;
+ z-index: 10;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+#dialogw {
+ z-index: 11;
+ max-width: 96%;
+ max-height: calc(100% - 100px);
+ position: fixed;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+}
+#dialogw a {
+ display: block;
+}
+#dialogw img {
+ max-height: 100%;
+ max-height: 90vh;
+ max-width: 100%;
+}
+#dialog_header {
+ width: 100%;
+ height: 44px;
+ position: fixed;
+ display: flex;
+ flex-direction: row-reverse;
+ align-items: center;
+}
+.header_image {
+ background: rgba(0, 0, 0, 0.28);
+}
+#dialogc {
+ cursor: pointer;
+ color: #ccc;
+ padding-right: 6px;
+}
+.dialoglogin {
+ background: #fff;
+ padding: 25px;
+ width: 300px;
+}
+.dialog-opened {
+ overflow: hidden;
+}
+#signemail,
+#signfb,
+#signvk {
+ display: block;
+ line-height: 32px;
+ margin: 10px 0;
+ text-decoration: none;
+ width: 100%;
+}
+#signvk {
+ margin-bottom: 30px;
+}
+.dialoglogin form {
+ margin-top: 7px;
+}
+.signinput,
+.signsubmit {
+ border: 1px solid #CCC;
+ margin: 3px 0;
+ padding: 3px;
+}
+.signinput {
+ width: 292px;
+}
+.signsubmit {
+ width: 70px;
+}
+.dialogshare {
+ background: #fff;
+ min-width: 300px;
+ overflow: auto;
+ padding: 20px;
+}
+.dialogl {
+ background: #fff;
+ border: 1px solid #DDD;
+ margin: 3px 0 20px;
+ padding: 5px;
+}
+.dialogshare li {
+ float: left;
+ margin: 5px 10px 0 0;
+}
+.dialogshare a {
+ display: block;
+}
+.dialogtxt {
+ background: #fff;
+ padding: 20px;
+}
+
+@media screen and (max-width: 480px) {
+ .dialog-opened {
+ position: fixed;
+ width: 100%;
+ }
+}
+
+/* #endregion */
+
+/* #region misc */
+
+#wsthread {
+ background: #CCC;
+ bottom: 20px;
+ cursor: pointer;
+ display: none;
+ padding: 5px 10px;
+ position: fixed;
+ right: 20px;
+}
+.sharenew {
+ display: inline-block;
+ line-height: 32px;
+ min-height: 32px;
+ min-width: 200px;
+ padding: 0 12px 0 37px;
+}
+.icon {
+ margin-top: -2px;
+ vertical-align: middle;
+}
+.icon--ei-link {
+ margin-top: -1px;
+}
+.icon--ei-comment {
+ margin-top: -5px;
+}
+.newmessage {
+ /* textarea on the /post page */
+ border: 1px solid #DDD;
+ padding: 2px;
+ resize: vertical;
+ width: 100%;
+}
+
+/* #endregion */
+
+/* #region footer internals */
+
+#footer-social {
+ float: left;
+}
+#footer-social a {
+ border: 0;
+ display: inline-block;
+}
+#footer-left {
+ margin-left: 286px;
+ margin-right: 350px;
+}
+#footer-right {
+ float: right;
+}
+
+@media screen and (max-width: 850px) {
+ #footer {
+ margin: 0 10px;
+ }
+ #footer div {
+ float: none;
+ margin: 10px 0;
+ }
+}
+
+/* #endregion */
+
+/* #region settings */
+
+fieldset {
+ border: 1px dotted #ccc;
+ margin-top: 25px;
+}
+
+/* #endregion */
+
+/* #region embeds */
+
+.embedContainer {
+ display: flex;
+ flex-wrap: wrap;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ margin: 30px -3px 15px -3px;
+}
+.embedContainer > * {
+ box-sizing: border-box;
+ flex-grow: 1;
+ margin: 3px;
+ min-width: 49%;
+}
+.embedContainer > .compact {
+ flex-grow: 0;
+}
+.embedContainer .picture img {
+ display: block;
+}
+.embedContainer img,
+.embedContainer video {
+ max-width: 100%;
+ max-height: 80vh;
+}
+.embedContainer > .audio,
+.embedContainer > .youtube {
+ min-width: 90%;
+}
+.embedContainer audio {
+ width: 100%;
+}
+.embedContainer iframe {
+ overflow: hidden;
+ resize: vertical;
+ display: block;
+}
+
+/* #endregion */
+
+/* #region nsfw */
+
+article.nsfw .embedContainer img,
+article.nsfw .embedContainer video,
+article.nsfw .embedContainer iframe,
+article.nsfw .ir img {
+ opacity: 0.1;
+}
+article.nsfw .embedContainer img:hover,
+article.nsfw .embedContainer video:hover,
+article.nsfw .embedContainer iframe:hover,
+article.nsfw .ir img:hover {
+ opacity: 1;
+}
+
+/* #endregion */
diff --git a/juick-server/src/main/java/com/juick/server/TelegramBotManager.java b/juick-server/src/main/java/com/juick/server/TelegramBotManager.java
index 9f5be577..ac2febf7 100644
--- a/juick-server/src/main/java/com/juick/server/TelegramBotManager.java
+++ b/juick-server/src/main/java/com/juick/server/TelegramBotManager.java
@@ -46,6 +46,7 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;
diff --git a/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java b/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java
new file mode 100644
index 00000000..2e484e3d
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.api;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.scribejava.apis.FacebookApi;
+import com.github.scribejava.apis.VkontakteApi;
+import com.github.scribejava.core.builder.ServiceBuilder;
+import com.github.scribejava.core.model.OAuth2AccessToken;
+import com.github.scribejava.core.model.OAuthRequest;
+import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import com.juick.facebook.User;
+import com.juick.server.util.HttpBadRequestException;
+import com.juick.service.CrosspostService;
+import com.juick.service.EmailService;
+import com.juick.service.TelegramService;
+import com.juick.service.UserService;
+import com.juick.vk.UsersResponse;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.io.IOException;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+@Controller
+public class ApiSocialLogin {
+
+ private static final Logger logger = LoggerFactory.getLogger(ApiSocialLogin.class);
+
+ @Value("${facebook_appid:appid}")
+ private String FACEBOOK_APPID;
+ @Value("${facebook_secret:secret}")
+ private String FACEBOOK_SECRET;
+ private static final String FACEBOOK_REDIRECT = "https://api.juick.com/_fblogin";
+ private static final String VK_REDIRECT = "https://api.juick.com/_vklogin";
+ private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json";
+ @Inject
+ private ObjectMapper jsonMapper;
+ private ServiceBuilder facebookBuilder, twitterBuilder, vkBuilder;
+
+ @Value("${twitter_consumer_key:appid}")
+ private String twitterConsumerKey;
+ @Value("${twitter_consumer_secret:secret}")
+ private String twitterConsumerSecret;
+ @Value("${vk_appid:appid}")
+ private String VK_APPID;
+ @Value("${vk_secret:secret}")
+ private String VK_SECRET;
+ @Value("${telegram_token:secret}")
+ private String telegramToken;
+
+ @Inject
+ private CrosspostService crosspostService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private EmailService emailService;
+ @Inject
+ private TelegramService telegramService;
+
+ @PostConstruct
+ public void init() {
+ facebookBuilder = new ServiceBuilder(FACEBOOK_APPID);
+ twitterBuilder = new ServiceBuilder(twitterConsumerKey);
+ vkBuilder = new ServiceBuilder(VK_APPID);
+ }
+
+ @GetMapping("/api/_fblogin")
+ protected String doFacebookLogin(@RequestParam(required = false) String code,
+ @RequestParam(required = false) String state) throws IOException, ExecutionException, InterruptedException {
+ if (StringUtils.isBlank(code)) {
+ String fbstate = UUID.randomUUID().toString();
+ crosspostService.addFacebookState(fbstate, state);
+ OAuth20Service facebookAuthService = facebookBuilder
+ .apiSecret(FACEBOOK_SECRET)
+ .callback(FACEBOOK_REDIRECT)
+ .scope("email")
+ .state(fbstate)
+ .build(FacebookApi.instance());
+ return "redirect:" + facebookAuthService.getAuthorizationUrl();
+ }
+
+ String redirectUrl = crosspostService.verifyFacebookState(state);
+
+ if (StringUtils.isEmpty(redirectUrl)) {
+ logger.error("state is missing");
+ throw new HttpBadRequestException();
+ }
+ OAuth20Service facebookService = facebookBuilder
+ .apiKey(FACEBOOK_APPID)
+ .apiSecret(FACEBOOK_SECRET)
+ .callback(FACEBOOK_REDIRECT)
+ .scope("email")
+ .state(state)
+ .build(FacebookApi.instance());
+ OAuth2AccessToken token = facebookService.getAccessToken(code);
+ final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v2.10/me?fields=id,name,link,verified,email");
+ facebookService.signRequest(token, meRequest);
+ String graph = facebookService.execute(meRequest).getBody();
+ if (StringUtils.isBlank(graph)) {
+ logger.error("FACEBOOK GRAPH ERROR");
+ throw new HttpBadRequestException();
+ }
+ User fb = jsonMapper.readValue(graph, User.class);
+ long fbID = NumberUtils.toLong(fb.getId(), 0);
+ if (fbID == 0 || StringUtils.isBlank(fb.getName()) || StringUtils.isBlank(fb.getLink())) {
+ logger.error("Missing required fields, id: {}, name: {}, link: {}", fbID, fb.getName(), fb.getLink());
+ throw new HttpBadRequestException();
+ }
+
+ int uid = crosspostService.getUIDbyFBID(fbID);
+ if (uid > 0) {
+ if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName(), fb.getLink())) {
+ logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken());
+ throw new HttpBadRequestException();
+ }
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl);
+ uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid));
+ return "redirect:" + uriComponentsBuilder.build().toUriString();
+ } else if (fb.getVerified()) {
+ if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName(), fb.getLink())) {
+ if (StringUtils.isNotEmpty(fb.getEmail())) {
+ logger.info("found {} for facebook user {}", fb.getEmail(), fb.getLink());
+ Integer userId = crosspostService.getUIDbyFBID(fbID);
+ if (!emailService.getEmails(userId, false).contains(fb.getEmail())) {
+ emailService.addEmail(userId, fb.getEmail());
+ }
+ }
+ logger.info("email not found for facebook user {}", fb.getLink());
+ throw new HttpBadRequestException();
+ }
+ return "redirect:/signup?type=fb&hash=" + state;
+ } else {
+ logger.error("Facebook account is not verified, id: {}", fbID);
+ throw new HttpBadRequestException();
+ }
+ }/*
+ @GetMapping("/_twitter")
+ protected void doTwitterLogin(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ExecutionException, InterruptedException {
+ String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY;
+ String verifier = request.getParameter("oauth_verifier");
+ Cookie[] cookies = request.getCookies();
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals("hash")) {
+ hash = cookie.getValue();
+ }
+ if (cookie.getName().equals("request_token")) {
+ request_token = cookie.getValue();
+ }
+ if (cookie.getName().equals("request_token_secret")) {
+ request_token_secret = cookie.getValue();
+ }
+ }
+ com.juick.User user = UserUtils.getCurrentUser();
+ OAuth10aService oAuthService = twitterBuilder
+ .apiSecret(twitterConsumerSecret)
+ .callback("http://juick.com/_twitter")
+ .build(TwitterApi.instance());
+
+ if (request_token.isEmpty() && request_token_secret.isEmpty()
+ && (verifier == null || verifier.isEmpty())) {
+ OAuth1RequestToken requestToken = oAuthService.getRequestToken();
+ String authUrl = oAuthService.getAuthorizationUrl(requestToken);
+ response.addCookie(new Cookie("request_token", requestToken.getToken()));
+ response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret()));
+ response.setStatus(HttpServletResponse.SC_FOUND);
+ response.setHeader("Location", authUrl);
+ } else {
+ if (verifier != null && verifier.length() > 0) {
+ OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret);
+ OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier);
+ OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL);
+ oAuthService.signRequest(accessToken, oAuthRequest);
+ com.juick.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(),
+ com.juick.twitter.User.class);
+ if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(),
+ twitterUser.getScreenName())) {
+ response.setStatus(HttpServletResponse.SC_FOUND);
+ response.setHeader("Location", "http://juick.com/settings");
+ } else {
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+ }
+ }*/
+ @GetMapping("/api/_vklogin")
+ protected String doVKLogin(@RequestParam(required = false) String code,
+ @RequestParam String state) throws IOException, ExecutionException, InterruptedException {
+ if (StringUtils.isBlank(code)) {
+ String vkstate = UUID.randomUUID().toString();
+ crosspostService.addVKState(vkstate, state);
+ OAuth20Service vkAuthService = vkBuilder
+ .apiSecret(VK_SECRET)
+ .scope("friends,wall,offline")
+ .state(vkstate)
+ .callback(VK_REDIRECT)
+ .build(VkontakteApi.instance());
+ return "redirect:" + vkAuthService.getAuthorizationUrl();
+ }
+
+ String redirectUrl = crosspostService.verifyVKState(state);
+ if (StringUtils.isBlank(redirectUrl)) {
+ logger.error("state is missing");
+ throw new HttpBadRequestException();
+ }
+
+ OAuth20Service vkService = vkBuilder
+ .apiKey(VK_APPID)
+ .apiSecret(VK_SECRET)
+ .build(VkontakteApi.instance());
+ OAuth2AccessToken token = vkService.getAccessToken(code);
+
+ OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73");
+ vkService.signRequest(token, meRequest);
+ String graph = vkService.execute(meRequest).getBody();
+
+ com.juick.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0);
+ String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName();
+ String vkLink = jsonUser.getScreenName();
+
+ if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) {
+ logger.error("vk user error");
+ throw new HttpBadRequestException();
+ }
+
+ Long vkID = NumberUtils.toLong(jsonUser.getId(), 0);
+ int uid = crosspostService.getUIDbyVKID(vkID);
+ if (uid > 0) {
+ UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl);
+ uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid));
+ return "redirect:" + uriComponentsBuilder.build().toUriString();
+ } else {
+ String loginhash = UUID.randomUUID().toString();
+ if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) {
+ logger.error("create vk user error");
+ throw new HttpBadRequestException();
+ }
+ return "redirect:/signup?type=vk&hash=" + loginhash;
+ }
+ }
+ /*
+ @GetMapping("/_tglogin")
+ public String doDurovLogin(HttpServletRequest request,
+ @RequestParam Map params,
+ HttpServletResponse response) {
+ String dataCheckString = params.entrySet().stream()
+ .filter(p -> !p.getKey().equals("hash"))
+ .sorted(Map.Entry.comparingByKey())
+ .map(p -> p.getKey() + "=" + p.getValue())
+ .collect(Collectors.joining("\n"));
+ String hash = params.get("hash");
+ byte[] secretKey = DigestUtils.sha256(telegramToken);
+ String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString);
+ if (hash.equals(resultString)) {
+ Long tgUser = Long.valueOf(params.get("id"));
+ int uid = telegramService.getUser(tgUser);
+ if (uid > 0) {
+ Cookie c = new Cookie("hash", userService.getHashByUID(uid));
+ c.setMaxAge(50 * 24 * 60 * 60);
+ response.addCookie(c);
+ return Utils.getPreviousPageByRequest(request).orElse("redirect:/");
+ } else {
+ String username = StringUtils.defaultString(params.get("username"), params.get("first_name"));
+ telegramService.createTelegramUser(tgUser, username);
+ return "redirect:/signup?type=durov&hash=" + userService.getSignUpHashByTelegramID(tgUser, username);
+ }
+ } else {
+ logger.warn("invalid tg hash {} for {}", resultString, hash);
+ }
+ throw new HttpBadRequestException();
+ }*/
+}
diff --git a/juick-server/src/main/java/com/juick/server/api/Index.java b/juick-server/src/main/java/com/juick/server/api/Index.java
index 5ffa6341..0faf270f 100644
--- a/juick-server/src/main/java/com/juick/server/api/Index.java
+++ b/juick-server/src/main/java/com/juick/server/api/Index.java
@@ -46,7 +46,7 @@ public class Index {
@Inject
private XMPPServer xmpp;
- @RequestMapping(value = { "/", "/ws/" }, method = RequestMethod.GET, headers = "Connection!=Upgrade")
+ @RequestMapping(value = { "/api/", "/ws/" }, method = RequestMethod.GET, headers = "Connection!=Upgrade")
public ResponseEntity description() {
URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/swagger-ui.html").build().toUri();
return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).location(redirectUri).build();
@@ -56,7 +56,7 @@ public class Index {
public Status status() {
return Status.getStatus(String.valueOf(wsHandler.getClients().size()));
}
- @RequestMapping(method = RequestMethod.GET, value = "/xmpp/status", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(method = RequestMethod.GET, value = "/api/xmpp-status", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public XMPPStatus xmppStatus() {
XMPPStatus status = new XMPPStatus();
if (xmpp != null) {
diff --git a/juick-server/src/main/java/com/juick/server/api/Messages.java b/juick-server/src/main/java/com/juick/server/api/Messages.java
index 2b171489..672f328f 100644
--- a/juick-server/src/main/java/com/juick/server/api/Messages.java
+++ b/juick-server/src/main/java/com/juick/server/api/Messages.java
@@ -71,7 +71,7 @@ public class Messages {
// TODO: serialize image urls
- @GetMapping("/home")
+ @GetMapping("/api/home")
public ResponseEntity> getHome(
@RequestParam(defaultValue = "0") int before_mid) {
User visitor = UserUtils.getCurrentUser();
@@ -83,7 +83,7 @@ public class Messages {
return FORBIDDEN;
}
- @GetMapping("/messages")
+ @GetMapping("/api/messages")
public ResponseEntity> getMessages(
@RequestParam(required = false) String uname,
@RequestParam(name = "before_mid", defaultValue = "0") Integer before,
@@ -142,7 +142,7 @@ public class Messages {
}
return ResponseEntity.ok(messagesService.getMessages(visitor, mids));
}
- @DeleteMapping("/messages")
+ @DeleteMapping("/api/messages")
public CommandResult deleteMessage(@RequestParam int mid, @RequestParam(required = false, defaultValue = "0") int rid) {
User visitor = UserUtils.getCurrentUser();
if (rid > 0) {
@@ -155,12 +155,12 @@ public class Messages {
}
throw new HttpBadRequestException();
}
- @GetMapping("/messages/discussions")
+ @GetMapping("/api/messages/discussions")
public List getDiscussions(
@RequestParam(required = false, defaultValue = "0") Long to) {
return messagesService.getMessages(UserUtils.getCurrentUser(), messagesService.getDiscussions(UserUtils.getCurrentUser().getUid(), to));
}
- @GetMapping("/thread")
+ @GetMapping("/api/thread")
public ResponseEntity> getThread(
@RequestParam(defaultValue = "0") int mid) {
User visitor = UserUtils.getCurrentUser();
@@ -183,7 +183,7 @@ public class Messages {
}
return NOT_FOUND;
}
- @GetMapping(value = "/thread/mark_read/{mid}-{rid}.gif", produces = MediaType.IMAGE_GIF_VALUE)
+ @GetMapping(value = "/api/thread/mark_read/{mid}-{rid}.gif", produces = MediaType.IMAGE_GIF_VALUE)
public byte[] markThreadRead(@PathVariable int mid, @PathVariable int rid) throws IOException {
User visitor = UserUtils.getCurrentUser();
if (!visitor.isAnonymous()) {
diff --git a/juick-server/src/main/java/com/juick/server/api/Notifications.java b/juick-server/src/main/java/com/juick/server/api/Notifications.java
index e068cbe9..0b34f275 100644
--- a/juick-server/src/main/java/com/juick/server/api/Notifications.java
+++ b/juick-server/src/main/java/com/juick/server/api/Notifications.java
@@ -67,7 +67,7 @@ public class Notifications {
}
@ApiIgnore
- @RequestMapping(value = "/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity> doGet(
@RequestParam(required = false, defaultValue = "0") int uid,
@RequestParam(required = false, defaultValue = "0") int mid,
@@ -101,7 +101,7 @@ public class Notifications {
}
@ApiIgnore
- @RequestMapping(value = "/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Status doDelete(
@RequestBody List list) {
User visitor = UserUtils.getCurrentUser();
@@ -129,7 +129,7 @@ public class Notifications {
}
@ApiIgnore
- @RequestMapping(value = "/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Status doPut(
@RequestBody List list) throws IOException {
User visitor = UserUtils.getCurrentUser();
@@ -155,7 +155,7 @@ public class Notifications {
}
@Deprecated
- @RequestMapping(value = "/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Status doAndroidRegister(
@RequestParam(name = "regid") String regId) {
User visitor = UserUtils.getCurrentUser();
@@ -167,14 +167,14 @@ public class Notifications {
}
@Deprecated
- @RequestMapping(value = "/android/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/android/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Status doAndroidUnRegister(@RequestParam(name = "regid") String regId) {
pushQueriesService.deleteGCMToken(regId);
return Status.OK;
}
@Deprecated
- @RequestMapping(value = "/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Status doWinphoneRegister(
Principal principal,
@RequestParam(name = "url") String regId) {
@@ -184,7 +184,7 @@ public class Notifications {
}
@Deprecated
- @RequestMapping(value = "/winphone/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/winphone/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public Status doWinphoneUnRegister(@RequestParam(name = "url") String regId) {
pushQueriesService.deleteMPNSToken(regId);
return Status.OK;
diff --git a/juick-server/src/main/java/com/juick/server/api/PM.java b/juick-server/src/main/java/com/juick/server/api/PM.java
index cbd70bed..d3619662 100644
--- a/juick-server/src/main/java/com/juick/server/api/PM.java
+++ b/juick-server/src/main/java/com/juick/server/api/PM.java
@@ -47,7 +47,7 @@ public class PM {
@Inject
private ApplicationEventPublisher applicationEventPublisher;
- @RequestMapping(value = "/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public List doGetPM(
@RequestParam(required = false) String uname) {
User visitor = UserUtils.getCurrentUser();
@@ -66,7 +66,7 @@ public class PM {
return pmQueriesService.getPMMessages(visitor.getUid(), uid);
}
- @RequestMapping(value = "/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public com.juick.Message doPostPM(
@RequestParam String uname,
@RequestParam String body) {
@@ -98,7 +98,7 @@ public class PM {
}
throw new HttpBadRequestException();
}
- @RequestMapping(value = "groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public PrivateChats doGetGroupsPMs(
@RequestParam(defaultValue = "5") int cnt) {
User visitor = UserUtils.getCurrentUser();
diff --git a/juick-server/src/main/java/com/juick/server/api/Post.java b/juick-server/src/main/java/com/juick/server/api/Post.java
index b0be50b6..99d118c3 100644
--- a/juick-server/src/main/java/com/juick/server/api/Post.java
+++ b/juick-server/src/main/java/com/juick/server/api/Post.java
@@ -62,7 +62,7 @@ public class Post {
@Inject
CommandsManager commandsManager;
- @RequestMapping(value = "/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
@ResponseStatus(value = HttpStatus.OK)
public CommandResult doPostMessage(
@RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body,
@@ -101,7 +101,7 @@ public class Post {
return commandsManager.processCommand(visitor, body, attachmentFName);
}
- @RequestMapping(value = "/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public com.juick.Message doPostComment(
@RequestParam(defaultValue = "0") int mid,
@RequestParam(defaultValue = "0") int rid,
@@ -154,7 +154,7 @@ public class Post {
return commandsManager.processCommand(visitor, String.format("#%d/%d %s", mid, rid, body), attachmentFName).getNewMessage().get();
}
- @PostMapping("/like")
+ @PostMapping("/api/like")
@ResponseStatus(value = HttpStatus.OK)
public Status doPostRecomm(@RequestParam Integer mid) throws Exception {
com.juick.User visitor = UserUtils.getCurrentUser();
@@ -173,13 +173,13 @@ public class Post {
return Status.getStatus(status.getText());
}
- @GetMapping("/reactions")
+ @GetMapping("/api/reactions")
@ResponseStatus(value = HttpStatus.OK)
public List reactionsList() {
return messagesService.listReactions();
}
- @PostMapping("/react")
+ @PostMapping("/api/react")
@ResponseStatus(value = HttpStatus.OK)
public Status doPostReact(@RequestParam Integer mid,@RequestParam @NotNull int reactionId,
@RequestParam (required = false, defaultValue = "1") int count) {
@@ -204,7 +204,7 @@ public class Post {
return recommendStatus == MessagesService.RecommendStatus.Error ? Status.ERROR :Status.OK;
}
- @PostMapping("/update")
+ @PostMapping("/api/update")
public CommandResult updateMessage(@RequestParam Integer mid,
@RequestParam(required = false, defaultValue = "0") Integer rid,
@RequestParam String body) {
diff --git a/juick-server/src/main/java/com/juick/server/api/Service.java b/juick-server/src/main/java/com/juick/server/api/Service.java
index 3a01317b..0da5d46c 100644
--- a/juick-server/src/main/java/com/juick/server/api/Service.java
+++ b/juick-server/src/main/java/com/juick/server/api/Service.java
@@ -54,7 +54,7 @@ public class Service {
Session session = Session.getDefaultInstance(new Properties());
@ApiIgnore
- @PostMapping("/mail")
+ @PostMapping("/api/mail")
@ResponseStatus(value = HttpStatus.OK)
public void processMail(InputStream data) throws Exception {
if (UserUtils.getCurrentUser().getName().equals(serviceUser)) {
diff --git a/juick-server/src/main/java/com/juick/server/api/SocialLogin.java b/juick-server/src/main/java/com/juick/server/api/SocialLogin.java
deleted file mode 100644
index dc7425e1..00000000
--- a/juick-server/src/main/java/com/juick/server/api/SocialLogin.java
+++ /dev/null
@@ -1,302 +0,0 @@
-/*
- * Copyright (C) 2008-2017, Juick
- *
- * 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 .
- */
-package com.juick.server.api;
-
-import com.fasterxml.jackson.databind.ObjectMapper;
-import com.github.scribejava.apis.FacebookApi;
-import com.github.scribejava.apis.VkontakteApi;
-import com.github.scribejava.core.builder.ServiceBuilder;
-import com.github.scribejava.core.model.OAuth2AccessToken;
-import com.github.scribejava.core.model.OAuthRequest;
-import com.github.scribejava.core.model.Verb;
-import com.github.scribejava.core.oauth.OAuth20Service;
-import com.juick.facebook.User;
-import com.juick.server.util.HttpBadRequestException;
-import com.juick.service.CrosspostService;
-import com.juick.service.EmailService;
-import com.juick.service.TelegramService;
-import com.juick.service.UserService;
-import com.juick.vk.UsersResponse;
-import org.apache.commons.lang3.StringUtils;
-import org.apache.commons.lang3.math.NumberUtils;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Controller;
-import org.springframework.web.bind.annotation.GetMapping;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.util.UriComponentsBuilder;
-
-import javax.annotation.PostConstruct;
-import javax.inject.Inject;
-import java.io.IOException;
-import java.util.UUID;
-import java.util.concurrent.ExecutionException;
-
-/**
- *
- * @author Ugnich Anton
- */
-@Controller
-public class SocialLogin {
-
- private static final Logger logger = LoggerFactory.getLogger(SocialLogin.class);
-
- @Value("${facebook_appid:appid}")
- private String FACEBOOK_APPID;
- @Value("${facebook_secret:secret}")
- private String FACEBOOK_SECRET;
- private static final String FACEBOOK_REDIRECT = "https://api.juick.com/_fblogin";
- private static final String VK_REDIRECT = "https://api.juick.com/_vklogin";
- private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json";
- @Inject
- private ObjectMapper jsonMapper;
- private ServiceBuilder facebookBuilder, twitterBuilder, vkBuilder;
-
- @Value("${twitter_consumer_key:appid}")
- private String twitterConsumerKey;
- @Value("${twitter_consumer_secret:secret}")
- private String twitterConsumerSecret;
- @Value("${vk_appid:appid}")
- private String VK_APPID;
- @Value("${vk_secret:secret}")
- private String VK_SECRET;
- @Value("${telegram_token:secret}")
- private String telegramToken;
-
- @Inject
- private CrosspostService crosspostService;
- @Inject
- private UserService userService;
- @Inject
- private EmailService emailService;
- @Inject
- private TelegramService telegramService;
-
- @PostConstruct
- public void init() {
- facebookBuilder = new ServiceBuilder(FACEBOOK_APPID);
- twitterBuilder = new ServiceBuilder(twitterConsumerKey);
- vkBuilder = new ServiceBuilder(VK_APPID);
- }
-
- @GetMapping("/_fblogin")
- protected String doFacebookLogin(@RequestParam(required = false) String code,
- @RequestParam(required = false) String state) throws IOException, ExecutionException, InterruptedException {
- if (StringUtils.isBlank(code)) {
- String fbstate = UUID.randomUUID().toString();
- crosspostService.addFacebookState(fbstate, state);
- OAuth20Service facebookAuthService = facebookBuilder
- .apiSecret(FACEBOOK_SECRET)
- .callback(FACEBOOK_REDIRECT)
- .scope("email")
- .state(fbstate)
- .build(FacebookApi.instance());
- return "redirect:" + facebookAuthService.getAuthorizationUrl();
- }
-
- String redirectUrl = crosspostService.verifyFacebookState(state);
-
- if (StringUtils.isEmpty(redirectUrl)) {
- logger.error("state is missing");
- throw new HttpBadRequestException();
- }
- OAuth20Service facebookService = facebookBuilder
- .apiKey(FACEBOOK_APPID)
- .apiSecret(FACEBOOK_SECRET)
- .callback(FACEBOOK_REDIRECT)
- .scope("email")
- .state(state)
- .build(FacebookApi.instance());
- OAuth2AccessToken token = facebookService.getAccessToken(code);
- final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v2.10/me?fields=id,name,link,verified,email");
- facebookService.signRequest(token, meRequest);
- String graph = facebookService.execute(meRequest).getBody();
- if (StringUtils.isBlank(graph)) {
- logger.error("FACEBOOK GRAPH ERROR");
- throw new HttpBadRequestException();
- }
- User fb = jsonMapper.readValue(graph, User.class);
- long fbID = NumberUtils.toLong(fb.getId(), 0);
- if (fbID == 0 || StringUtils.isBlank(fb.getName()) || StringUtils.isBlank(fb.getLink())) {
- logger.error("Missing required fields, id: {}, name: {}, link: {}", fbID, fb.getName(), fb.getLink());
- throw new HttpBadRequestException();
- }
-
- int uid = crosspostService.getUIDbyFBID(fbID);
- if (uid > 0) {
- if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName(), fb.getLink())) {
- logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken());
- throw new HttpBadRequestException();
- }
- UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl);
- uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid));
- return "redirect:" + uriComponentsBuilder.build().toUriString();
- } else if (fb.getVerified()) {
- if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName(), fb.getLink())) {
- if (StringUtils.isNotEmpty(fb.getEmail())) {
- logger.info("found {} for facebook user {}", fb.getEmail(), fb.getLink());
- Integer userId = crosspostService.getUIDbyFBID(fbID);
- if (!emailService.getEmails(userId, false).contains(fb.getEmail())) {
- emailService.addEmail(userId, fb.getEmail());
- }
- }
- logger.info("email not found for facebook user {}", fb.getLink());
- throw new HttpBadRequestException();
- }
- return "redirect:/signup?type=fb&hash=" + state;
- } else {
- logger.error("Facebook account is not verified, id: {}", fbID);
- throw new HttpBadRequestException();
- }
- }/*
- @GetMapping("/_twitter")
- protected void doTwitterLogin(HttpServletRequest request, HttpServletResponse response)
- throws IOException, ExecutionException, InterruptedException {
- String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY;
- String verifier = request.getParameter("oauth_verifier");
- Cookie[] cookies = request.getCookies();
- for (Cookie cookie : cookies) {
- if (cookie.getName().equals("hash")) {
- hash = cookie.getValue();
- }
- if (cookie.getName().equals("request_token")) {
- request_token = cookie.getValue();
- }
- if (cookie.getName().equals("request_token_secret")) {
- request_token_secret = cookie.getValue();
- }
- }
- com.juick.User user = UserUtils.getCurrentUser();
- OAuth10aService oAuthService = twitterBuilder
- .apiSecret(twitterConsumerSecret)
- .callback("http://juick.com/_twitter")
- .build(TwitterApi.instance());
-
- if (request_token.isEmpty() && request_token_secret.isEmpty()
- && (verifier == null || verifier.isEmpty())) {
- OAuth1RequestToken requestToken = oAuthService.getRequestToken();
- String authUrl = oAuthService.getAuthorizationUrl(requestToken);
- response.addCookie(new Cookie("request_token", requestToken.getToken()));
- response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret()));
- response.setStatus(HttpServletResponse.SC_FOUND);
- response.setHeader("Location", authUrl);
- } else {
- if (verifier != null && verifier.length() > 0) {
- OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret);
- OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier);
- OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL);
- oAuthService.signRequest(accessToken, oAuthRequest);
- com.juick.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(),
- com.juick.twitter.User.class);
- if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(),
- twitterUser.getScreenName())) {
- response.setStatus(HttpServletResponse.SC_FOUND);
- response.setHeader("Location", "http://juick.com/settings");
- } else {
- response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
- }
- }
- }
- }*/
- @GetMapping("/_vklogin")
- protected String doVKLogin(@RequestParam(required = false) String code,
- @RequestParam String state) throws IOException, ExecutionException, InterruptedException {
- if (StringUtils.isBlank(code)) {
- String vkstate = UUID.randomUUID().toString();
- crosspostService.addVKState(vkstate, state);
- OAuth20Service vkAuthService = vkBuilder
- .apiSecret(VK_SECRET)
- .scope("friends,wall,offline")
- .state(vkstate)
- .callback(VK_REDIRECT)
- .build(VkontakteApi.instance());
- return "redirect:" + vkAuthService.getAuthorizationUrl();
- }
-
- String redirectUrl = crosspostService.verifyVKState(state);
- if (StringUtils.isBlank(redirectUrl)) {
- logger.error("state is missing");
- throw new HttpBadRequestException();
- }
-
- OAuth20Service vkService = vkBuilder
- .apiKey(VK_APPID)
- .apiSecret(VK_SECRET)
- .build(VkontakteApi.instance());
- OAuth2AccessToken token = vkService.getAccessToken(code);
-
- OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73");
- vkService.signRequest(token, meRequest);
- String graph = vkService.execute(meRequest).getBody();
-
- com.juick.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0);
- String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName();
- String vkLink = jsonUser.getScreenName();
-
- if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) {
- logger.error("vk user error");
- throw new HttpBadRequestException();
- }
-
- Long vkID = NumberUtils.toLong(jsonUser.getId(), 0);
- int uid = crosspostService.getUIDbyVKID(vkID);
- if (uid > 0) {
- UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl);
- uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid));
- return "redirect:" + uriComponentsBuilder.build().toUriString();
- } else {
- String loginhash = UUID.randomUUID().toString();
- if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) {
- logger.error("create vk user error");
- throw new HttpBadRequestException();
- }
- return "redirect:/signup?type=vk&hash=" + loginhash;
- }
- }
- /*
- @GetMapping("/_tglogin")
- public String doDurovLogin(HttpServletRequest request,
- @RequestParam Map params,
- HttpServletResponse response) {
- String dataCheckString = params.entrySet().stream()
- .filter(p -> !p.getKey().equals("hash"))
- .sorted(Map.Entry.comparingByKey())
- .map(p -> p.getKey() + "=" + p.getValue())
- .collect(Collectors.joining("\n"));
- String hash = params.get("hash");
- byte[] secretKey = DigestUtils.sha256(telegramToken);
- String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString);
- if (hash.equals(resultString)) {
- Long tgUser = Long.valueOf(params.get("id"));
- int uid = telegramService.getUser(tgUser);
- if (uid > 0) {
- Cookie c = new Cookie("hash", userService.getHashByUID(uid));
- c.setMaxAge(50 * 24 * 60 * 60);
- response.addCookie(c);
- return Utils.getPreviousPageByRequest(request).orElse("redirect:/");
- } else {
- String username = StringUtils.defaultString(params.get("username"), params.get("first_name"));
- telegramService.createTelegramUser(tgUser, username);
- return "redirect:/signup?type=durov&hash=" + userService.getSignUpHashByTelegramID(tgUser, username);
- }
- } else {
- logger.warn("invalid tg hash {} for {}", resultString, hash);
- }
- throw new HttpBadRequestException();
- }*/
-}
diff --git a/juick-server/src/main/java/com/juick/server/api/Tags.java b/juick-server/src/main/java/com/juick/server/api/Tags.java
index 8ced4ec9..38e71e3a 100644
--- a/juick-server/src/main/java/com/juick/server/api/Tags.java
+++ b/juick-server/src/main/java/com/juick/server/api/Tags.java
@@ -38,7 +38,7 @@ public class Tags {
@Inject
private TagService tagService;
- @RequestMapping(value = "/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public List tags(
@RequestParam(required = false, defaultValue = "0") int user_id
) {
diff --git a/juick-server/src/main/java/com/juick/server/api/Users.java b/juick-server/src/main/java/com/juick/server/api/Users.java
index 237b7ed6..7f29a327 100644
--- a/juick-server/src/main/java/com/juick/server/api/Users.java
+++ b/juick-server/src/main/java/com/juick/server/api/Users.java
@@ -52,12 +52,12 @@ public class Users {
private EmailService emailService;
@ApiOperation(value = "This returns user token", notes = "Pass login and password using HTTP Basic")
- @RequestMapping(value = "/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String getAuthToken() {
return userService.getHashByUID(UserUtils.getCurrentUser().getUid());
}
- @RequestMapping(value = "/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public List doGetUsers(
@RequestParam(value = "uname", required = false) List unames) {
List users = new ArrayList<>();
@@ -78,7 +78,7 @@ public class Users {
throw new HttpNotFoundException();
}
- @GetMapping("/me")
+ @GetMapping("/api/me")
public SecureUser getMe() {
User visitor = UserUtils.getCurrentUser();
SecureUser me = new SecureUser();
@@ -93,7 +93,7 @@ public class Users {
return me;
}
- @RequestMapping(value = "/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public List doGetUserRead(
@RequestParam String uname) {
User visitor = UserUtils.getCurrentUser();
@@ -118,7 +118,7 @@ public class Users {
throw new HttpNotFoundException();
}
- @RequestMapping(value = "/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+ @RequestMapping(value = "/api/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public List doGetUserReaders(
@RequestParam String uname) {
User visitor = UserUtils.getCurrentUser();
@@ -144,7 +144,7 @@ public class Users {
}
@ApiOperation(value = "This returns detailed user info")
- @GetMapping("/info/{uname}")
+ @GetMapping("/api/info/{uname}")
public UserInfo getUserInfo(@PathVariable String uname) {
User user = userService.getUserByName(uname);
if (!user.isBanned()) {
diff --git a/juick-server/src/main/java/com/juick/server/api/activity/Profile.java b/juick-server/src/main/java/com/juick/server/api/activity/Profile.java
index 0d987b58..89236f03 100644
--- a/juick-server/src/main/java/com/juick/server/api/activity/Profile.java
+++ b/juick-server/src/main/java/com/juick/server/api/activity/Profile.java
@@ -38,7 +38,7 @@ public class Profile {
@Value("${img_url:http://localhost:8080/i/}")
private String baseImagesUri;
- @GetMapping(value = "/u/{userName}", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
+ @GetMapping(value = "/api/u/{userName}", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
public Person getUser(@PathVariable String userName) {
User user = userService.getUserByName(userName);
if (!user.isAnonymous()) {
@@ -67,7 +67,7 @@ public class Profile {
}
throw new HttpNotFoundException();
}
- @GetMapping(value = "/u/{userName}/blog/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
+ @GetMapping(value = "/api/u/{userName}/blog/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
public OrderedCollection getOutbox(@PathVariable String userName) {
User user = userService.getUserByName(userName);
if (!user.isAnonymous()) {
@@ -80,7 +80,7 @@ public class Profile {
}
throw new HttpNotFoundException();
}
- @GetMapping(value = "/u/{userName}/blog", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
+ @GetMapping(value = "/api/u/{userName}/blog", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
public OrderedCollectionPage getOutboxPage(@PathVariable String userName,
@RequestParam(required = false, defaultValue = "0") int before) {
User visitor = UserUtils.getCurrentUser();
@@ -128,7 +128,7 @@ public class Profile {
}
throw new HttpNotFoundException();
}
- @GetMapping(value = "/u/{userName}/followers/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
+ @GetMapping(value = "/api/u/{userName}/followers/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
public OrderedCollection getFollowers(@PathVariable String userName) {
User user = userService.getUserByName(userName);
if (!user.isAnonymous()) {
@@ -141,7 +141,7 @@ public class Profile {
}
throw new HttpNotFoundException();
}
- @GetMapping(value = "/u/{userName}/followers", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
+ @GetMapping(value = "/api/u/{userName}/followers", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
public OrderedCollectionPage getFollowersPage(@PathVariable String userName,
@RequestParam(required = false, defaultValue = "0") int page) {
User user = userService.getUserByName(userName);
@@ -171,7 +171,7 @@ public class Profile {
}
throw new HttpNotFoundException();
}
- @GetMapping(value = "/u/{userName}/following/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
+ @GetMapping(value = "/api/u/{userName}/following/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
public OrderedCollection getFollowing(@PathVariable String userName) {
User user = userService.getUserByName(userName);
if (!user.isAnonymous()) {
@@ -184,7 +184,7 @@ public class Profile {
}
throw new HttpNotFoundException();
}
- @GetMapping(value = "/u/{userName}/following", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
+ @GetMapping(value = "/api/u/{userName}/following", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE })
public OrderedCollectionPage getFollowingPage(@PathVariable String userName,
@RequestParam(required = false, defaultValue = "0") int page) {
User user = userService.getUserByName(userName);
diff --git a/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java b/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java
deleted file mode 100644
index 2f263a78..00000000
--- a/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java
+++ /dev/null
@@ -1,127 +0,0 @@
-/*
- * Copyright (C) 2008-2017, Juick
- *
- * 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 .
- */
-
-package com.juick.server.configuration;
-
-import com.juick.service.UserService;
-import com.juick.service.security.JuickUserDetailsService;
-import com.juick.service.security.deprecated.RequestParamHashRememberMeServices;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.security.authentication.AuthenticationProvider;
-import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
-import org.springframework.security.config.annotation.web.builders.HttpSecurity;
-import org.springframework.security.config.annotation.web.builders.WebSecurity;
-import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
-import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
-import org.springframework.security.config.http.SessionCreationPolicy;
-import org.springframework.security.core.userdetails.UserDetailsService;
-import org.springframework.security.web.AuthenticationEntryPoint;
-import org.springframework.security.web.authentication.HttpStatusEntryPoint;
-import org.springframework.security.web.authentication.RememberMeServices;
-import org.springframework.web.cors.CorsConfiguration;
-import org.springframework.web.cors.CorsConfigurationSource;
-import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
-
-import javax.inject.Inject;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.concurrent.TimeUnit;
-
-/**
- * Created by aalexeev on 11/21/16.
- */
-@Configuration
-@EnableWebSecurity
-public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
- @Value("${auth_remember_me_key:secret}")
- private String rememberMeKey;
- @Inject
- private UserService userService;
-
- ApiSecurityConfig() {
- super(true);
- }
-
- @Override
- protected void configure(HttpSecurity http) throws Exception {
- http.authorizeRequests()
- .antMatchers(HttpMethod.OPTIONS).permitAll()
- .antMatchers("/", "/messages", "/users", "/thread", "/tags", "/tlgmbtwbhk", "/fbwbhk",
- "/skypebotendpoint", "/_fblogin", "/_vklogin", "_tglogin", "/u/**", "/.well-known/webfinger").permitAll()
- .anyRequest().hasRole("USER")
- .and().httpBasic().authenticationEntryPoint(juickAuthenticationEntryPoint())
- .and().anonymous()
- .and().cors().configurationSource(corsConfigurationSource())
- .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
- .and().exceptionHandling().authenticationEntryPoint(juickAuthenticationEntryPoint())
- .and()
- .rememberMe()
- .alwaysRemember(true)
- .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30))
- .rememberMeServices(rememberMeServices())
- .key(rememberMeKey)
- .and().authenticationProvider(authenticationProvider())
- .headers().defaultsDisabled().cacheControl();
- }
-
- @Bean
- public AuthenticationProvider authenticationProvider() {
- DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
-
- authenticationProvider.setUserDetailsService(userDetailsService());
-
- return authenticationProvider;
- }
-
- @Bean
- public UserDetailsService userDetailsService() {
- return new JuickUserDetailsService(userService);
- }
-
- @Bean
- public RememberMeServices rememberMeServices() {
- return new RequestParamHashRememberMeServices(rememberMeKey, userService);
- }
-
- @Bean
- public AuthenticationEntryPoint juickAuthenticationEntryPoint() {
- return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED);
- }
-
- @Bean
- public CorsConfigurationSource corsConfigurationSource() {
- CorsConfiguration configuration = new CorsConfiguration();
-
- configuration.setAllowedOrigins(Collections.singletonList("*"));
- configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE"));
- configuration.setAllowedHeaders(Collections.singletonList("*"));
-
- UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
- source.registerCorsConfiguration("/**", configuration);
-
- return source;
- }
- @Override
- public void configure(WebSecurity web) throws Exception {
- web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources/**",
- "/configuration/**", "/swagger-ui.html", "/webjars/**", "/ws/**", "/rss/**", "/h2-console/**");
- }
-}
diff --git a/juick-server/src/main/java/com/juick/server/configuration/PostConfig.java b/juick-server/src/main/java/com/juick/server/configuration/PostConfig.java
deleted file mode 100644
index 598a7435..00000000
--- a/juick-server/src/main/java/com/juick/server/configuration/PostConfig.java
+++ /dev/null
@@ -1,9 +0,0 @@
-package com.juick.server.configuration;
-
-import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
-import org.springframework.context.annotation.ComponentScan;
-
-@EnableAutoConfiguration
-@ComponentScan({"com.juick.server", "com.juick.service"})
-public class PostConfig {
-}
diff --git a/juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java
new file mode 100644
index 00000000..53b29415
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.configuration;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import ru.sape.Sape;
+
+/**
+ * Created by vitalyster on 29.03.2017.
+ */
+@Configuration
+public class SapeConfiguration {
+ @Value("${sape_user:secret}")
+ private String token;
+
+ @Bean
+ public Sape sape() {
+ return new Sape(token, "juick.com", 2000, 3600);
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java b/juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java
new file mode 100644
index 00000000..cd2ab13a
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.configuration;
+
+import com.juick.service.UserService;
+import com.juick.service.security.HashParamAuthenticationFilter;
+import com.juick.service.security.JuickUserDetailsService;
+import com.juick.service.security.deprecated.RequestParamHashRememberMeServices;
+import com.juick.service.security.entities.JuickUser;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.annotation.Order;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.builders.WebSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+import org.springframework.security.config.http.SessionCreationPolicy;
+import org.springframework.security.core.userdetails.UserDetailsService;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.security.web.authentication.HttpStatusEntryPoint;
+import org.springframework.security.web.authentication.RememberMeServices;
+import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices;
+import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import javax.annotation.Resource;
+import javax.inject.Inject;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Created by aalexeev on 11/21/16.
+ */
+@EnableWebSecurity
+public class SecurityConfig {
+ @Resource
+ private UserService userService;
+ @Value("${auth_remember_me_key:secret}")
+ private String rememberMeKey;
+ @Value("${web_domain:localhost}")
+ private String webDomain;
+
+ private static final String COOKIE_NAME = "juick-remember-me";
+
+ @Bean
+ public UserDetailsService userDetailsService() {
+ return new JuickUserDetailsService(userService);
+ }
+ @Bean
+ public RememberMeServices rememberMeServices() throws Exception {
+ TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(
+ rememberMeKey, userDetailsService());
+
+ services.setCookieName(COOKIE_NAME);
+ services.setCookieDomain(webDomain);
+ services.setAlwaysRemember(true);
+ services.setTokenValiditySeconds(6 * 30 * 24 * 3600);
+ services.setUseSecureCookie(false); // TODO set true if https is supports
+
+ return services;
+ }
+ @Bean
+ public HashParamAuthenticationFilter hashParamAuthenticationFilter() throws Exception {
+ return new HashParamAuthenticationFilter(userService, rememberMeServices());
+ }
+
+
+ @Configuration
+ @Order(1)
+ public static class ApiConfig extends WebSecurityConfigurerAdapter {
+ @Value("${auth_remember_me_key:secret}")
+ private String rememberMeKey;
+ @Value("${web_domain:localhost}")
+ private String webDomain;
+ @Resource
+ private UserService userService;
+ @Inject
+ private HashParamAuthenticationFilter hashParamAuthenticationFilter;
+ ApiConfig() {
+ super(true);
+ }
+ @Bean
+ RememberMeServices rememberMeServices(){
+ return new RequestParamHashRememberMeServices(rememberMeKey, userService);
+ }
+
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http.addFilterAfter(hashParamAuthenticationFilter, BasicAuthenticationFilter.class);
+ http.antMatcher("/api/**").authorizeRequests()
+ .antMatchers(HttpMethod.OPTIONS).permitAll()
+ .antMatchers("/api/", "/api/messages", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk",
+ "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/u/**", "/.well-known/webfinger").permitAll()
+ .anyRequest().hasRole("USER")
+ .and()
+ .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY)
+ .and()
+ .httpBasic().authenticationEntryPoint(juickAuthenticationEntryPoint())
+ .and().cors().configurationSource(corsConfigurationSource())
+ .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ .and().exceptionHandling().authenticationEntryPoint(juickAuthenticationEntryPoint())
+ .and()
+ .rememberMe()
+ .alwaysRemember(true)
+ .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30))
+ .rememberMeServices(rememberMeServices())
+ .key(rememberMeKey)
+ .and()
+ .headers().defaultsDisabled().cacheControl();
+ }
+
+ @Bean
+ public AuthenticationEntryPoint juickAuthenticationEntryPoint() {
+ return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED);
+ }
+
+ @Bean
+ public CorsConfigurationSource corsConfigurationSource() {
+ CorsConfiguration configuration = new CorsConfiguration();
+
+ configuration.setAllowedOrigins(Collections.singletonList("*"));
+ configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE"));
+ configuration.setAllowedHeaders(Collections.singletonList("*"));
+
+ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+ source.registerCorsConfiguration("/api/**", configuration);
+
+ return source;
+ }
+ @Override
+ public void configure(WebSecurity web) {
+ web.debug(false);
+ web.ignoring().antMatchers("/api/v2/api-docs", "/api/configuration/ui", "/api/swagger-resources/**",
+ "/api/configuration/**", "/swagger-ui.html", "/webjars/**", "/ws/**", "/rss/**", "/h2-console/**");
+ }
+ }
+
+ @Configuration
+ public static class WebConfig extends WebSecurityConfigurerAdapter {
+ @Inject
+ private RememberMeServices rememberMeServices;
+ @Value("${auth_remember_me_key:secret}")
+ private String rememberMeKey;
+ @Value("${web_domain:localhost}")
+ private String webDomain;
+ @Resource
+ private UserService userService;
+ @Override
+ protected void configure(HttpSecurity http) throws Exception {
+ http
+ .authorizeRequests()
+ .antMatchers("/settings", "/pm/**", "/**/bl", "/_twitter", "/post", "/post2", "/comment")
+ .authenticated()
+ .anyRequest().permitAll()
+ .and()
+ .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY)
+ .and()
+ .sessionManagement().invalidSessionUrl("/")
+ .and()
+ .logout()
+ .invalidateHttpSession(true)
+ .logoutUrl("/logout")
+ .logoutSuccessUrl("/login?logout")
+ .deleteCookies("hash", COOKIE_NAME)
+ .and()
+ .formLogin()
+ .loginPage("/login")
+ .permitAll()
+ .defaultSuccessUrl("/")
+ .loginProcessingUrl("/login")
+ .usernameParameter("username")
+ .passwordParameter("password")
+ .failureUrl("/login?error=1")
+ .and()
+ .rememberMe()
+ .rememberMeCookieDomain(webDomain).key(rememberMeKey)
+ .rememberMeServices(rememberMeServices)
+ .and()
+ .csrf().disable()
+ .headers().defaultsDisabled().cacheControl();
+ }
+ @Override
+ public void configure(WebSecurity web) throws Exception {
+ web.debug(false);
+ web.ignoring().antMatchers("/style.css*", "/scripts.js*", "/h2-console/**", "/.well-known/**");
+ }
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java
new file mode 100644
index 00000000..16d32ee4
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.configuration;
+
+import com.juick.service.TagService;
+import com.juick.service.UserService;
+import com.juick.server.www.HelpService;
+import com.mitchellbosecke.pebble.PebbleEngine;
+import com.mitchellbosecke.pebble.extension.FormatterExtension;
+import com.mitchellbosecke.pebble.loader.ClasspathLoader;
+import com.mitchellbosecke.pebble.loader.Loader;
+import com.mitchellbosecke.pebble.spring.PebbleViewResolver;
+import com.mitchellbosecke.pebble.spring.extension.SpringExtension;
+import org.apache.commons.codec.CharEncoding;
+import org.commonmark.ext.autolink.AutolinkExtension;
+import org.commonmark.node.Link;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.cache.caffeine.CaffeineCacheManager;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.web.servlet.ViewResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import javax.inject.Inject;
+import java.util.Collections;
+
+/**
+ * Created by aalexeev on 11/22/16.
+ */
+@Configuration
+@EnableCaching
+@Import({ BaseWebConfiguration.class, SecurityConfig.class, SapeConfiguration.class,
+ StorageConfiguration.class})
+public class WwwAppConfiguration implements WebMvcConfigurer {
+ @Inject
+ private UserService userService;
+ @Inject
+ private TagService tagService;
+ @Bean
+ public CaffeineCacheManager cacheManager() {
+ return new CaffeineCacheManager("help");
+ }
+
+ @Bean
+ public HelpService helpService() {
+ return new HelpService("help");
+ }
+
+ @Bean
+ public Parser cmParser() {
+ return Parser.builder().extensions(Collections.singletonList(AutolinkExtension.create())).build();
+ }
+ @Bean
+ public HtmlRenderer helpRenderer() {
+ return HtmlRenderer.builder()
+ .attributeProviderFactory(context -> (node, tagName, attributes) -> {
+ if (node instanceof Link) {
+ Link link = (Link) node;
+ if (link.getDestination().startsWith("/")) {
+ String destination = "/" + helpService().getHelpPath() + link.getDestination();
+ link.setDestination(destination);
+ attributes.put("href", destination);
+ }
+ }
+ })
+ .build();
+ }
+ @Bean
+ public Loader templateLoader() {
+ return new ClasspathLoader();
+ }
+
+ @Bean
+ public SpringExtension springExtension() {
+ return new SpringExtension();
+ }
+
+ @Bean
+ public PebbleEngine pebbleEngine() {
+ return new PebbleEngine.Builder()
+ .loader(this.templateLoader())
+ .extension(springExtension())
+ .extension(new FormatterExtension())
+ .strictVariables(true)
+ .build();
+ }
+
+ @Bean
+ public ViewResolver viewResolver() {
+ PebbleViewResolver viewResolver = new PebbleViewResolver();
+ viewResolver.setPrefix("templates");
+ viewResolver.setSuffix(".html");
+ viewResolver.setPebbleEngine(pebbleEngine());
+ viewResolver.setCharacterEncoding(CharEncoding.UTF_8);
+ return viewResolver;
+ }
+
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/HelpService.java b/juick-server/src/main/java/com/juick/server/www/HelpService.java
new file mode 100644
index 00000000..25727962
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/HelpService.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.www;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.cache.annotation.Cacheable;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.regex.Pattern;
+
+/**
+ * Created by aalexeev on 12/11/16.
+ */
+public class HelpService {
+ private static final Pattern LANG_PATTERN = Pattern.compile("[a-z]{2}");
+
+ private static final Pattern PAGE_PATTERN = Pattern.compile("[a-zA-Z0-9\\-_]+");
+
+ private final String helpPath;
+
+
+ public HelpService(String helpPath) {
+ this.helpPath = helpPath;
+ }
+
+ @Cacheable("help")
+ public String getHelp(final String page, final String lang) {
+ if (canBePage(page) && canBeLang(lang)) {
+ String path = StringUtils.joinWith("/", helpPath, lang, page + ".md");
+
+ try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) {
+ if (is != null)
+ return IOUtils.toString(is, StandardCharsets.UTF_8);
+ } catch (IOException e) {
+ }
+ }
+ return null;
+ }
+
+ public boolean canBePage(final String anything) {
+ return anything != null && PAGE_PATTERN.matcher(anything).matches();
+ }
+
+ public boolean canBeLang(final String anything) {
+ return anything != null && LANG_PATTERN.matcher(anything).matches();
+ }
+
+ public String getHelpPath() {
+ return helpPath;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/Utils.java b/juick-server/src/main/java/com/juick/server/www/Utils.java
new file mode 100644
index 00000000..61278e17
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/Utils.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.www;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.Optional;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+public class Utils {
+
+
+ public static String encodeSphinx(String str) {
+ return str.replaceAll("@", "\\\\@")
+ .replaceAll("\\'", "\\\\'");
+ }
+
+ /**
+ * Returns the viewName to return for coming back to the sender url
+ *
+ * @param request Instance of {@link HttpServletRequest} or use an injected instance
+ * @return Optional with the view name. Recomended to use an alternativa url with
+ * {@link Optional#orElse(java.lang.Object)}
+ */
+ public static Optional getPreviousPageByRequest(HttpServletRequest request)
+ {
+ return Optional.ofNullable(request.getHeader("Referer")).map(requestUrl -> "redirect:" + requestUrl);
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/WebApp.java b/juick-server/src/main/java/com/juick/server/www/WebApp.java
new file mode 100644
index 00000000..98327a5d
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/WebApp.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.www;
+
+import com.juick.Tag;
+import com.juick.service.TagService;
+import org.springframework.stereotype.Component;
+import org.springframework.web.servlet.resource.ResourceUrlProvider;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Stream;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+@Component
+public class WebApp {
+ @Inject
+ private TagService tagService;
+ @Inject
+ private ResourceUrlProvider resourceUrlProvider;
+
+ public List parseTags(String tagsStr) {
+ List tags = new ArrayList<>();
+ if (tagsStr != null && !tagsStr.isEmpty()) {
+ Stream tagsList = Arrays.stream(tagsStr.split("[ \\,]"))
+ .distinct().map( t -> {
+ if (t.startsWith("*")) {
+ t = t.substring(1);
+ }
+ if (t.length() > 64) {
+ t = t.substring(0, 64);
+ }
+ return t;
+ });
+ tags = tagService.getTags(tagsList, true);
+ while (tags.size() > 5) {
+ tags.remove(5);
+ }
+ }
+ return tags;
+ }
+
+ public String getStyleUrl() {
+ return resourceUrlProvider.getForLookupPath("/style.css");
+ }
+
+ public String getScriptsUrl() {
+ return resourceUrlProvider.getForLookupPath("/scripts.js");
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java b/juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java
new file mode 100644
index 00000000..9ab20003
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java
@@ -0,0 +1,64 @@
+package com.juick.server.www.controllers;
+
+import com.juick.server.util.WebUtils;
+import com.juick.service.MessagesService;
+import com.juick.service.UserService;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.springframework.stereotype.Component;
+import org.springframework.web.filter.OncePerRequestFilter;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import org.springframework.web.util.UriComponents;
+
+import javax.annotation.Nonnull;
+import javax.inject.Inject;
+import javax.servlet.*;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class AnythingFilter extends OncePerRequestFilter {
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private UserService userService;
+
+ @Override
+ public void doFilterInternal(@Nonnull HttpServletRequest servletRequest,
+ @Nonnull HttpServletResponse servletResponse,
+ @Nonnull FilterChain filterChain) throws IOException, ServletException {
+ UriComponents components = ServletUriComponentsBuilder.fromCurrentRequestUri().build();
+ String anything = components.getPath().substring(1);
+ int before = NumberUtils.toInt(components.getQueryParams().getFirst("before"), 0);
+ if (before == 0) {
+ boolean isPostNumber = WebUtils.isPostNumber(anything);
+ int messageId = isPostNumber ?
+ NumberUtils.toInt(anything) : 0;
+
+ if (isPostNumber && anything.equals(Integer.toString(messageId))) {
+ if (messageId > 0) {
+ com.juick.User author = messagesService.getMessageAuthor(messageId);
+
+ if (author != null) {
+ servletResponse.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
+ servletResponse.setHeader("Location", "/" + author.getName() + "/" + anything);
+ return;
+ }
+ }
+ }
+ com.juick.User user = userService.getUserByName(anything);
+ if (user.getUid() > 0) {
+ ((HttpServletResponse)servletResponse).sendRedirect("/" + user.getName() + "/");
+ } else {
+ filterChain.doFilter(servletRequest, servletResponse);
+ }
+ } else {
+ com.juick.User user = userService.getUserByName(anything);
+ if (!user.isAnonymous()) {
+ ((HttpServletResponse) servletResponse).sendRedirect("/" + user.getName() + "/?before=" + before);
+ } else {
+ filterChain.doFilter(servletRequest, servletResponse);
+ }
+ }
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java b/juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java
new file mode 100644
index 00000000..7ba7405a
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java
@@ -0,0 +1,49 @@
+package com.juick.server.www.controllers;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Collections;
+import java.util.List;
+
+@RestController
+public class AppSiteAssociation {
+ @Value("${ios_app_id:}")
+ private String appId;
+
+ @GetMapping("/.well-known/apple-app-site-association")
+ @ResponseBody
+ public SiteAssociations appSiteAssociations() {
+ WebCredentials webCredentials = new WebCredentials();
+ webCredentials.setApps(Collections.singletonList(appId));
+ SiteAssociations siteAssociations = new SiteAssociations();
+ siteAssociations.setWebcredentials(webCredentials);
+ return siteAssociations;
+ }
+
+ private class SiteAssociations {
+ private WebCredentials webcredentials;
+
+ public WebCredentials getWebcredentials() {
+ return webcredentials;
+ }
+
+ public void setWebcredentials(WebCredentials webcredentials) {
+ this.webcredentials = webcredentials;
+ }
+ }
+
+ private class WebCredentials {
+ private List apps;
+
+ public List getApps() {
+ return apps;
+ }
+
+ public void setApps(List apps) {
+ this.apps = apps;
+ }
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/Help.java b/juick-server/src/main/java/com/juick/server/www/controllers/Help.java
new file mode 100644
index 00000000..61b58a9d
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/Help.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.www.controllers;
+
+import com.juick.server.util.HttpNotFoundException;
+import com.juick.server.util.UserUtils;
+import com.juick.service.MessagesService;
+import com.juick.server.www.HelpService;
+import org.commonmark.parser.Parser;
+import org.commonmark.renderer.html.HtmlRenderer;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Created by aalexeev on 11/21/16.
+ */
+@Controller
+public class Help {
+ @Inject
+ private HelpService helpService;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private Parser cmParser;
+ @Inject
+ private HtmlRenderer helpRenderer;
+
+ @GetMapping({"/help/", "/help", "/help/{langOrPage}", "/help/{lang}/{page}"})
+ public String showHelp(
+ Locale locale,
+ @PathVariable(required = false, name = "lang") String lang,
+ @PathVariable(required = false, name = "page") String page,
+ @PathVariable(required = false, name = "langOrPage") String langOrPage,
+ Model model) throws IOException, URISyntaxException {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+
+ String navigation = null;
+
+ if (langOrPage != null) {
+ if (helpService.canBeLang(langOrPage)) {
+ navigation = helpService.getHelp("navigation", langOrPage);
+ if (navigation != null)
+ lang = langOrPage;
+ }
+
+ if (navigation == null && helpService.canBePage(langOrPage))
+ page = langOrPage;
+ }
+
+ if (lang == null) {
+ lang = locale.getLanguage();
+ }
+
+ String content = helpService.getHelp(page, lang);
+ if (content == null && !Objects.equals("tos", page))
+ content = helpService.getHelp("tos", lang);
+
+ if (navigation == null)
+ navigation = helpService.getHelp("navigation", lang);
+
+ if (content == null || navigation == null)
+ throw new HttpNotFoundException();
+
+ model.addAttribute("navigation", helpRenderer.render(cmParser.parse(navigation)));
+ model.addAttribute("content", helpRenderer.render(cmParser.parse(content)));
+ model.addAttribute("visitor", visitor);
+
+ return "views/help";
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/Login.java b/juick-server/src/main/java/com/juick/server/www/controllers/Login.java
new file mode 100644
index 00000000..d933934e
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/Login.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.www.controllers;
+
+import com.juick.server.util.UserUtils;
+import com.juick.service.UserService;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.inject.Inject;
+
+/**
+ * @author Ugnich Anton
+ */
+@Controller
+public class Login {
+ @Inject
+ private UserService userService;
+
+ @GetMapping("/login")
+ public String getloginForm(@RequestParam(required = false, defaultValue = "true") boolean redirect) {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+
+ if (!visitor.isAnonymous()) {
+ return redirect ? "redirect:/" : "redirect:/login/success";
+ }
+ return "views/login";
+ }
+ @GetMapping("/login/success")
+ public String getSuccessLogin(ModelMap model) {
+ model.addAttribute("hash", userService.getHashByUID(UserUtils.getCurrentUser().getUid()));
+ return "views/login_success";
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java b/juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java
new file mode 100644
index 00000000..10136fcf
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java
@@ -0,0 +1,595 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.www.controllers;
+
+import com.juick.Message;
+import com.juick.Tag;
+import com.juick.formatters.PlainTextFormatter;
+import com.juick.server.util.HttpForbiddenException;
+import com.juick.server.util.HttpNotFoundException;
+import com.juick.server.util.UserUtils;
+import com.juick.server.util.WebUtils;
+import com.juick.service.*;
+import com.juick.util.MessageUtils;
+import com.juick.server.www.Utils;
+import org.apache.commons.codec.CharEncoding;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.StringEscapeUtils;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
+import org.springframework.web.util.UriComponents;
+import ru.sape.Sape;
+
+import javax.inject.Inject;
+import javax.servlet.http.HttpServletRequest;
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+@Controller
+public class MessagesWWW {
+ @Inject
+ private UserService userService;
+ @Inject
+ private TagService tagService;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private Sape sape;
+ @Inject
+ private PMQueriesService pmQueriesService;
+ @Inject
+ private CrosspostService crosspostService;
+ @Inject
+ private ApplicationEventPublisher applicationEventPublisher;
+
+ void fillUserModel(ModelMap model, com.juick.User user, com.juick.User visitor) {
+ model.addAttribute("user", user);
+ model.addAttribute("isSubscribed", userService.isSubscribed(visitor.getUid(), user.getUid()));
+ model.addAttribute("isInBL", userService.isInBL(visitor.getUid(), user.getUid()));
+ model.addAttribute("isInBLAny", userService.isInBLAny(user.getUid(), visitor.getUid()));
+ model.addAttribute("statsIRead", userService.getUserFriends(user.getUid()));
+ model.addAttribute("statsMyReaders", userService.getStatsMyReaders(user.getUid()));
+ model.addAttribute("statsMyBL", userService.getUserBLUsers(user.getUid()).size());
+ model.addAttribute("statsMessages", userService.getStatsMessages(user.getUid()));
+ model.addAttribute("statsReplies", userService.getStatsReplies(user.getUid()));
+ model.addAttribute("iread", userService.getUserReadLeastPopular(user.getUid(), 8));
+ model.addAttribute("tagStats", tagService.getUserTagStats(user.getUid())
+ .stream().sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).limit(20).map(t -> t.getTag().getName()).collect(Collectors.toList()));
+ }
+
+ @GetMapping("/")
+ protected String doGet(
+ @RequestParam(required = false) String tag,
+ @RequestParam(name = "show", required = false) String paramShow,
+ @RequestParam(name = "search", required = false) String paramSearch,
+ @RequestParam(name = "before", required = false, defaultValue = "0") Integer paramBefore,
+ @RequestParam(name = "to", required = false, defaultValue = "0") Long paramTo,
+ @RequestParam(name = "page", required = false, defaultValue = "0") Integer page,
+ @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie,
+ ModelMap model) throws IOException {
+ if (tag != null) {
+ return "redirect:/tag/" + URLEncoder.encode(tag, CharEncoding.UTF_8);
+ }
+ com.juick.User visitor = UserUtils.getCurrentUser();
+
+ if (paramSearch != null && paramSearch.length() > 64) {
+ paramSearch = null;
+ }
+
+ model.addAttribute("discover", false);
+
+ String title;
+ List mids;
+
+ if (paramSearch != null) {
+ title = "Поиск: " + StringEscapeUtils.escapeHtml4(paramSearch);
+ mids = messagesService.getSearch(Utils.encodeSphinx(paramSearch), page);
+ } else if (paramShow == null) {
+ if (!visitor.isAnonymous()) {
+ title = "Популярные";
+ mids = messagesService.getPopular(visitor.getUid(), paramBefore);
+ model.addAttribute("discover", true);
+ } else {
+ title = "Микроблоги Juick: популярные записи";
+ mids = messagesService.getPopular(0, paramBefore);
+ }
+
+ } else if (paramShow.equals("top")) {
+ return "redirect:/";
+ } else if (paramShow.equals("my") && !visitor.isAnonymous()) {
+ title = "Моя лента";
+ mids = messagesService.getMyFeed(visitor.getUid(), paramBefore, true);
+ } else if (paramShow.equals("private") && !visitor.isAnonymous()) {
+ title = "Приватные";
+ mids = messagesService.getPrivate(visitor.getUid(), paramBefore);
+ } else if (paramShow.equals("discuss") && !visitor.isAnonymous()) {
+ title = "Обсуждения";
+ mids = messagesService.getDiscussions(visitor.getUid(), paramTo);
+ } else if (paramShow.equals("recommended") && !visitor.isAnonymous()) {
+ title = "Рекомендации";
+ mids = messagesService.getRecommended(visitor.getUid(), paramBefore);
+ } else if (paramShow.equals("photos")) {
+ title = "Фотографии";
+ mids = messagesService.getPhotos(visitor.getUid(), paramBefore);
+ model.addAttribute("discover", true);
+ } else if (paramShow.equals("all")) {
+ title = "Все сообщения";
+ mids = messagesService.getAll(visitor.getUid(), paramBefore);
+ model.addAttribute("discover", true);
+ } else {
+ throw new HttpNotFoundException();
+ }
+
+ String head = StringUtils.EMPTY;
+ if (paramBefore > 0 || paramShow != null) {
+ head = " ";
+ }
+ model.addAttribute("title", title);
+ model.addAttribute("headers", head);
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("noindex", !(paramShow == null && paramBefore == 0));
+ List msgs = messagesService.getMessages(visitor, mids);
+
+ if (!visitor.isAnonymous()) {
+ fillUserModel(model, visitor, visitor);
+ List unread = messagesService.getUnread(visitor);
+ visitor.setUnreadCount(unread.size());
+ List blUIDs = userService.checkBL(visitor.getUid(),
+ msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList()));
+ msgs.forEach(m -> {
+ m.ReadOnly |= blUIDs.contains(m.getUser().getUid());
+ m.setUnread(unread.contains(m.getMid()));
+ });
+ }
+ model.addAttribute("msgs", msgs);
+ model.addAttribute("tags", tagService.getPopularTags());
+ model.addAttribute("headers", head);
+ model.addAttribute("showAdv",
+ paramShow == null && paramBefore == 0 && paramSearch == null && visitor.isAnonymous());
+ if (mids.size() >= 20) {
+ String nextpage = (paramShow != null && paramShow.equals("discuss")) ? "?to=" + msgs.get(msgs.size() - 1).getUpdated().toEpochMilli() : paramSearch != null ? String.format("?page=%d", page + 1) : "?before=" + mids.get(mids.size() - 1);
+ if (paramShow != null) {
+ nextpage += "&show=" + paramShow;
+ }
+ if (paramSearch != null) {
+ nextpage += "&search=" + URLEncoder.encode(paramSearch, CharEncoding.UTF_8);
+ }
+ model.addAttribute("nextpage", nextpage);
+ }
+ UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build();
+ String queryString = builder.getQuery();
+ String requestURI = builder.toUri().getPath();
+ if (sape != null && visitor.isAnonymous() && queryString == null) {
+ String links = sape.getPageLinks(requestURI, sapeCookie).render();
+ model.addAttribute("links", links);
+ }
+ return "views/index";
+ }
+
+ @GetMapping("/{uname}/")
+ protected String doGetBlog(
+ @RequestParam(required = false, name = "show") String paramShow,
+ @RequestParam(required = false, name = "tag") String paramTagStr,
+ @RequestParam(required = false, name = "search") String paramSearch,
+ @RequestParam(required = false, name = "page", defaultValue = "0") Integer page,
+ @PathVariable String uname,
+ @RequestParam(required = false, defaultValue = "0") Integer before,
+ @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie,
+ ModelMap model) throws IOException {
+ com.juick.User user = userService.getUserByName(uname);
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (user.isBanned() || user.isAnonymous()) {
+ throw new HttpNotFoundException();
+ }
+
+ List mids;
+
+ com.juick.Tag paramTag = null;
+ if (paramTagStr != null) {
+ if (paramTagStr.length() < 64) {
+ paramTag = tagService.getTag(paramTagStr, false);
+ }
+ if (paramTag == null) {
+ throw new HttpNotFoundException();
+ } else if (!paramTag.getName().equals(paramTagStr)) {
+ String url = user.getName() + "/?tag=" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8);
+ return "redirect:/" + url;
+ }
+ }
+ if (paramSearch != null && paramSearch.length() > 64) {
+ paramSearch = null;
+ }
+
+ int privacy = 0;
+ if (!visitor.isAnonymous()) {
+ if (user.getUid() == visitor.getUid() || visitor.getUid() == 1) {
+ privacy = -3;
+ } else if (userService.isInWL(user.getUid(), visitor.getUid())) {
+ privacy = -2;
+ }
+ }
+
+ String title;
+ if (paramShow == null) {
+ if (paramTag != null) {
+ title = "Блог " + user.getName() + ": *" + StringEscapeUtils.escapeHtml4(paramTag.getName());
+ mids = messagesService.getUserTag(user.getUid(), paramTag.TID, privacy, before);
+ } else if (paramSearch != null) {
+ title = "Блог " + user.getName() + ": " + StringEscapeUtils.escapeHtml4(paramSearch);
+ mids = messagesService.getUserSearch(user.getUid(), Utils.encodeSphinx(paramSearch), privacy, page);
+ } else {
+ title = "Блог " + user.getName();
+ mids = messagesService.getUserBlog(user.getUid(), privacy, before);
+ }
+ } else if (paramShow.equals("recomm")) {
+ title = "Рекомендации " + user.getName();
+ mids = messagesService.getUserRecommendations(user.getUid(), before);
+ } else if (paramShow.equals("photos")) {
+ title = "Фотографии " + user.getName();
+ mids = messagesService.getUserPhotos(user.getUid(), privacy, before);
+ } else {
+ throw new HttpNotFoundException();
+ }
+
+ String head = " ";
+ if (paramTag != null && tagService.getTagNoIndex(paramTag.TID)) {
+ head += " ";
+ } else if (before > 0 || paramShow != null) {
+ head += " ";
+ }
+ model.addAttribute("pageUrl", "http://juick.com/" + user.getName());
+ model.addAttribute("title", title);
+ model.addAttribute("headers", head);
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("noindex", paramShow == null && before == 0);
+ fillUserModel(model, user, visitor);
+ model.addAttribute("paramTag", paramTag);
+ List msgs = messagesService.getMessages(visitor, mids);
+
+ if (!visitor.isAnonymous()) {
+ List unread = messagesService.getUnread(visitor);
+ visitor.setUnreadCount(unread.size());
+ List blUIDs = userService.checkBL(visitor.getUid(),
+ msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList()));
+ msgs.forEach(m -> {
+ m.ReadOnly |= blUIDs.contains(m.getUser().getUid());
+ m.setUnread(unread.contains(m.getMid()));
+ });
+ }
+ model.addAttribute("msgs", msgs);
+ model.addAttribute("headers", head);
+ model.addAttribute("showAdv",
+ paramShow == null && before == 0 && paramSearch == null && visitor.getUid() == 0);
+ if (mids.size() >= 20) {
+ String nextpage = paramSearch != null ? String.format("?page=%d", page + 1) : "?before=" + mids.get(mids.size() - 1);
+ if (paramShow != null) {
+ nextpage += "&show=" + paramShow;
+ }
+ if (paramSearch != null) {
+ nextpage += "&search=" + URLEncoder.encode(paramSearch, CharEncoding.UTF_8);
+ }
+ if (paramTag != null) {
+ nextpage += "&tag=" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8);
+ }
+ model.addAttribute("nextpage", nextpage);
+ }
+ UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build();
+ String queryString = builder.getQuery();
+ String requestURI = builder.toUri().getPath();
+ if (sape != null && visitor.isAnonymous() && queryString == null) {
+ String links = sape.getPageLinks(requestURI, sapeCookie).render();
+ model.addAttribute("links", links);
+ }
+ return "views/blog";
+ }
+
+ @GetMapping("/{uname}/tags")
+ protected String doGetTags(@PathVariable String uname, ModelMap model) throws IOException {
+ com.juick.User user = userService.getUserByName(uname);
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isBanned()) {
+ throw new HttpNotFoundException();
+ }
+
+ model.addAttribute("title", "Теги " + user.getName());
+ model.addAttribute("headers", " ");
+ model.addAttribute("visitor", visitor);
+ fillUserModel(model, user, visitor);
+ model.addAttribute("tags", tagService.getUserTagStats(user.getUid()).stream()
+ .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList()));
+
+ return "views/blog_tags";
+ }
+
+ @GetMapping("/{uname}/friends")
+ protected String doGetFriends(@PathVariable String uname, ModelMap model) throws IOException {
+ com.juick.User user = userService.getUserByName(uname);
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isBanned()) {
+ throw new HttpNotFoundException();
+ }
+ model.addAttribute("title", "Подписки " + user.getName());
+ model.addAttribute("headers", " ");
+ model.addAttribute("visitor", visitor);
+ fillUserModel(model, user, visitor);
+ model.addAttribute("users", userService.getUserFriends(user.getUid()));
+
+ return "views/users";
+ }
+
+ @GetMapping("/{uname}/readers")
+ protected String doGetReaders(@PathVariable String uname, ModelMap model) throws IOException {
+ com.juick.User user = userService.getUserByName(uname);
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isBanned()) {
+ throw new HttpForbiddenException();
+ }
+ model.addAttribute("title", "Читатели " + user.getName());
+ model.addAttribute("headers", " ");
+ model.addAttribute("visitor", visitor);
+ fillUserModel(model, user, visitor);
+ model.addAttribute("users", userService.getUserReaders(user.getUid()));
+
+ return "views/users";
+ }
+
+ @GetMapping("/{uname}/bl")
+ protected String doGetBL(@PathVariable String uname, ModelMap model) throws IOException {
+ com.juick.User user = userService.getUserByName(uname);
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isBanned() || visitor.getUid() != user.getUid()) {
+ throw new HttpForbiddenException();
+ }
+ model.addAttribute("title", "Черный список " + user.getName());
+ model.addAttribute("headers", " ");
+ model.addAttribute("visitor", visitor);
+ fillUserModel(model, user, visitor);
+ model.addAttribute("users", userService.getUserBLUsers(user.getUid()));
+
+ return "views/users";
+ }
+ @GetMapping("/tag/{tagName}")
+ protected String tagAction(HttpServletRequest request,
+ @PathVariable String tagName,
+ @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie,
+ @RequestParam(required = false, defaultValue = "0") int before,
+ ModelMap model) throws IOException {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+
+ String paramTagStr = StringEscapeUtils.unescapeHtml4(tagName);
+ com.juick.Tag paramTag = tagService.getTag(paramTagStr, false);
+ if (paramTag == null) {
+ throw new HttpNotFoundException();
+ } else if (paramTag.SynonymID > 0 && paramTag.TID != paramTag.SynonymID) {
+ com.juick.Tag synTag = tagService.getTag(paramTag.SynonymID);
+ String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(synTag.getName()), CharEncoding.UTF_8);
+ if (request.getQueryString() != null) {
+ url += "?" + request.getQueryString();
+ }
+ return "redirect:" + url;
+ } else if (!paramTag.getName().equals(paramTagStr)) {
+ String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(paramTag.getName()), CharEncoding.UTF_8);
+ if (request.getQueryString() != null) {
+ url += "?" + request.getQueryString();
+ }
+ return "redirect:" + url;
+ }
+
+ String title = "*" + StringEscapeUtils.escapeHtml4(paramTag.getName());
+ model.addAttribute("title", title);
+ List mids = messagesService.getTag(paramTag.TID, visitor.getUid(), before, (visitor.isAnonymous()) ? 40 : 20);
+ List msgs = messagesService.getMessages(visitor, mids);
+ if (!visitor.isAnonymous()) {
+ List unread = messagesService.getUnread(visitor);
+ visitor.setUnreadCount(unread.size());
+ List blUIDs = userService.checkBL(
+ visitor.getUid(),
+ msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())
+ );
+ msgs.forEach(m -> {
+ m.ReadOnly |= blUIDs.contains(m.getUser().getUid());
+ m.setUnread(unread.contains(m.getMid()));
+ });
+ fillUserModel(model, visitor, visitor);
+ }
+
+ String head = StringUtils.EMPTY;
+ if (tagService.getTagNoIndex(paramTag.TID)) {
+ head = " ";
+ } else if (before > 0 || mids.size() < 5) {
+ head = " ";
+ }
+ model.addAttribute("headers", head);
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("tag", paramTag);
+ model.addAttribute("title", title);
+ model.addAttribute("msgs", msgs);
+ model.addAttribute("tags", tagService.getPopularTags());
+ model.addAttribute("noindex", before > 0);
+ model.addAttribute("showAdv", before == 0 && visitor.isAnonymous());
+ model.addAttribute("isSubscribed", tagService.isSubscribed(visitor, paramTag));
+ model.addAttribute("isInBL", tagService.isInBL(visitor, paramTag));
+ if (mids.size() >= 20) {
+ String nextpage = "/tag/" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8) + "?before=" + mids.get(mids.size() - 1);
+ model.addAttribute("nextpage", nextpage);
+ }
+ UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build();
+ String queryString = builder.getQuery();
+ String requestURI = builder.toUri().getPath();
+ if (sape != null && visitor.isAnonymous() && queryString == null) {
+ String links = sape.getPageLinks(requestURI, sapeCookie).render();
+ model.addAttribute("links", links);
+ }
+ return "views/index";
+ }
+ @GetMapping("/pm/inbox")
+ protected String doGetInbox(ModelMap model) {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ return "redirect:/login";
+ }
+ String title = "PM: Inbox";
+ List msgs = pmQueriesService.getLastPMInbox(visitor.getUid());
+ fillUserModel(model, visitor, visitor);
+ model.addAttribute("title", title);
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("msgs", msgs);
+ model.addAttribute("tags", tagService.getPopularTags());
+ return "views/pm_inbox";
+ }
+
+ @GetMapping("/pm/sent")
+ protected String doGetSent(@RequestParam(required = false) String uname,
+ ModelMap model) {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ return "redirect:/login";
+ }
+ String title = "PM: Sent";
+ List msgs = pmQueriesService.getLastPMSent(visitor.getUid());
+
+ if (WebUtils.isNotUserName(uname)) {
+ uname = StringUtils.EMPTY;
+ }
+ fillUserModel(model, visitor, visitor);
+ model.addAttribute("title", title);
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("msgs", msgs);
+ model.addAttribute("tags", tagService.getPopularTags());
+ model.addAttribute("uname", uname);
+ return "views/pm_sent";
+ }
+ @GetMapping("/{uname}/{mid}")
+ protected String threadAction(ModelMap model,
+ @PathVariable String uname,
+ @PathVariable int mid,
+ @CookieValue(name = "sape_cookie",
+ required = false, defaultValue = StringUtils.EMPTY) String sapeCookie) {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+
+ if (!messagesService.canViewThread(mid, visitor.getUid())) {
+ throw new HttpForbiddenException();
+ }
+
+ com.juick.Message msg = messagesService.getMessage(mid);
+
+ if (msg == null || msg.getUser().isBanned()) {
+ throw new HttpNotFoundException();
+ }
+
+ com.juick.User user = userService.getUserByName(uname);
+ if (user.isAnonymous() || !msg.getUser().equals(user)) {
+ return String.format("redirect:/%s/%d", msg.getUser().getName(), mid);
+ }
+ msg.VisitorCanComment = !visitor.isAnonymous();
+ List replies = messagesService.getReplies(visitor, msg.getMid());
+ // this should be after getReplies to mark thread as read
+ fillUserModel(model, user, visitor);
+ if (!visitor.isAnonymous()) {
+ List unread = messagesService.getUnread(visitor);
+ visitor.setUnreadCount(unread.size());
+ boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid();
+ boolean isInBL = userService.isInBLAny(msg.getUser().getUid(), visitor.getUid());
+ msg.VisitorCanComment = isMsgAuthor || !(msg.ReadOnly || isInBL);
+ }
+ model.addAttribute("msg", msg);
+
+ String title = msg.getUser().getName() + ": " + MessageUtils.getTagsString(msg);
+
+ model.addAttribute("title", title);
+ model.addAttribute("visitor", visitor);
+ String headers = " ";
+ String pageUrl = "https://juick.com/" + msg.getUser().getName() + "/" + msg.getMid();
+ if (msg.Hidden) {
+ headers += " ";
+ }
+ String cardType = StringUtils.isNotEmpty(msg.getAttachmentType()) ? "summary_large_image" : "summary";
+ if (StringUtils.isNotEmpty(msg.getAttachmentType())) {
+ // additional check in case of broken images
+ if (msg.getAttachment() != null) {
+ String msgImage = msg.getAttachment().getMedium().getUrl();
+ headers += " ";
+ }
+ } else {
+ String msgImage ="https://i.juick.com/a/" + msg.getUser().getUid() + ".png";
+ headers += " ";
+ }
+ model.addAttribute("ogtype", "article");
+ String cardDescription = StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg));
+ headers += " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n" +
+ " \n";
+ String twitterName = crosspostService.getTwitterName(msg.getUser().getUid());
+ if (StringUtils.isNotEmpty(twitterName)) {
+ headers += " \n";
+ }
+ if (msg.getTags().size() > 0) {
+ headers += " \n";
+ }
+ model.addAttribute("headers", headers);
+ model.addAttribute("visitorSubscribed", messagesService.isSubscribed(visitor.getUid(), msg.getMid()));
+ model.addAttribute("visitorInBL", userService.isInBL(msg.getUser().getUid(), visitor.getUid()));
+ model.addAttribute("recomm", messagesService.getMessageRecommendations(msg.getMid()));
+ List blUIDs = new ArrayList<>();
+ for (Message reply : replies) {
+ if (reply.getUser().getUid() != msg.getUser().getUid()
+ && !blUIDs.contains(reply.getUser().getUid())) {
+ blUIDs.add(reply.getUser().getUid());
+ }
+ reply.VisitorCanComment = !visitor.isAnonymous();
+ if (!visitor.isAnonymous()) {
+ boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid();
+ boolean isReplyAuthor = visitor.getUid() == reply.getUser().getUid();
+ reply.VisitorCanComment = isMsgAuthor || (!msg.ReadOnly
+ && msg.VisitorCanComment && (isReplyAuthor || !userService.isInBLAny(visitor.getUid(), reply.getUser().getUid())));
+ }
+ }
+ model.addAttribute("replies", replies);
+ model.addAttribute("showAdv", visitor.isAnonymous());
+ UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build();
+ String queryString = builder.getQuery();
+ String requestURI = builder.toUri().getPath();
+ if (sape != null && visitor.isAnonymous() && queryString == null) {
+ String links = sape.getPageLinks(requestURI, sapeCookie).render();
+ model.addAttribute("links", links);
+ }
+ return "views/thread";
+ }
+
+ // when message id is not fit to int
+ @ExceptionHandler(NumberFormatException.class)
+ public ResponseEntity notFoundAction() {
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java b/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java
new file mode 100644
index 00000000..9e364ff8
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java
@@ -0,0 +1,263 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.www.controllers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.juick.Message;
+import com.juick.User;
+import com.juick.server.helpers.AnonymousUser;
+import com.juick.server.helpers.CommandResult;
+import com.juick.server.util.*;
+import com.juick.server.www.WebApp;
+import com.juick.service.*;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.text.StringEscapeUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.util.LinkedMultiValueMap;
+import org.springframework.util.MultiValueMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import javax.inject.Inject;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URL;
+import java.util.stream.Collectors;
+
+/**
+ * @author Ugnich Anton
+ */
+@Controller
+public class NewMessage {
+
+ @Inject
+ private TagService tagService;
+ @Inject
+ private MessagesService messagesService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private SubscriptionService subscriptionService;
+ @Inject
+ private CrosspostService crosspostService;
+ @Inject
+ private PMQueriesService pmQueriesService;
+ @Inject
+ private WebApp webApp;
+ @Inject
+ private ObjectMapper jsonMapper;
+ @Inject
+ private ImagesService imagesService;
+ @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String imgDir;
+ @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String tmpDir;
+ @Value("${api_url:http://localhost:8080}")
+ private String apiUrl;
+ private RestTemplate rest = new RestTemplate();
+
+ private static final Logger logger = LoggerFactory.getLogger(NewMessage.class);
+
+ @GetMapping("/post")
+ protected String postAction(@RequestParam(required = false) String body, ModelMap model) {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ model.addAttribute("title", "Написать");
+ model.addAttribute("headers", "");
+ model.addAttribute("visitor", visitor);
+ if (body == null) {
+ body = StringUtils.EMPTY;
+ } else {
+ if (body.length() > 4096) {
+ body = body.substring(0, 4096);
+ }
+ body = StringEscapeUtils.escapeHtml4(body);
+ }
+ model.addAttribute("body", body);
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("tags", tagService.getUserTagStats(visitor.getUid()).stream()
+ .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList()));
+ return "views/post";
+ }
+
+ @PostMapping("/comment")
+ public String doPostComment(
+ @RequestParam Integer mid,
+ @RequestParam(required = false, defaultValue = "0") Integer rid,
+ @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body,
+ @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String img,
+ @RequestParam(required = false) MultipartFile attach) throws IOException {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous() || visitor.isBanned()) {
+ throw new HttpForbiddenException();
+ }
+ com.juick.Message msg = messagesService.getMessage(mid);
+ if (msg == null) {
+ throw new HttpNotFoundException();
+ }
+
+ com.juick.Message reply = null;
+ if (rid > 0) {
+ reply = messagesService.getReply(mid, rid);
+ if (reply == null) {
+ throw new HttpNotFoundException();
+ }
+ }
+
+ if ((StringUtils.isEmpty(body) || body.length() > 4096) && StringUtils.isEmpty(img) && attach == null) {
+ throw new HttpBadRequestException();
+ }
+ body = body.replace("\r", StringUtils.EMPTY);
+
+ if ((msg.ReadOnly && msg.getUser().getUid() != visitor.getUid())
+ || userService.isInBLAny(msg.getUser().getUid(), visitor.getUid())
+ || (reply != null && userService.isInBLAny(reply.getUser().getUid(), visitor.getUid()))) {
+ throw new HttpForbiddenException();
+ }
+
+ URI attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir);
+
+ if (StringUtils.isBlank(attachmentFName.toString()) && img != null && img.length() > 10) {
+ try {
+ URL imgUrl = new URL(img);
+ attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir);
+ } catch (Exception e) {
+ logger.error("DOWNLOAD ERROR", e);
+ throw new HttpBadRequestException();
+ }
+ }
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+ MultiValueMap params = new LinkedMultiValueMap<>();
+ HttpEntity> request = new HttpEntity<>(params, headers);
+
+
+ params.add("body", rid == 0 ? String.format("#%d %s", mid, body) : String.format("#%d/%d %s", mid, rid, body));
+ params.add("hash", userService.getHashByUID(visitor.getUid()));
+ if (StringUtils.isNotEmpty(attachmentFName.toString())) {
+ params.add("img", attachmentFName.toASCIIString());
+ }
+ URI postUri = UriComponentsBuilder.fromUriString(apiUrl).path("/api/post").build().toUri();
+ ResponseEntity result = rest.postForEntity(
+ postUri,
+ request, CommandResult.class);
+ logger.info("/comment: {}", jsonMapper.writeValueAsString(result.getBody()));
+ boolean wasReply = result.getBody().getNewMessage().isPresent();
+ return wasReply ? "redirect:/" + msg.getUser().getName() + "/" + mid + "#" + result.getBody().getNewMessage().get().getRid() : "redirect:/" + msg.getUser().getName() + "/" + mid;
+ }
+
+ @PostMapping("/pm/send")
+ public String doPostPM(@RequestParam(name = "uname", required = false) String unameParam,
+ @RequestParam String body) throws IOException {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous() || visitor.isBanned()) {
+ throw new HttpForbiddenException();
+ }
+ String uname = unameParam;
+ if (uname.startsWith("@")) {
+ uname = uname.substring(1);
+ }
+ User userTo = AnonymousUser.INSTANCE;
+ if (WebUtils.isUserName(uname)) {
+ userTo = userService.getUserByName(uname);
+ }
+
+ if (userTo.isAnonymous() || body.length() > 10240) {
+ throw new HttpBadRequestException();
+ }
+
+ if (userService.isInBLAny(userTo.getUid(), visitor.getUid())) {
+ throw new HttpForbiddenException();
+ }
+
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+ MultiValueMap params = new LinkedMultiValueMap<>();
+ HttpEntity> request = new HttpEntity<>(params, headers);
+
+ params.add("body", String.format("@%s %s", userTo.getName(), body));
+ params.add("hash", userService.getHashByUID(visitor.getUid()));
+ URI postUri = UriComponentsBuilder.fromUriString(apiUrl).path("/api/post").build().toUri();
+ ResponseEntity result = rest.postForEntity(
+ postUri,
+ request, CommandResult.class);
+ logger.info("/pm: {}", jsonMapper.writeValueAsString(result.getBody()));
+ return "redirect:/pm/sent";
+
+ }
+ @PostMapping("/post2")
+ public String doPostMessage(@RequestParam(name = "body", required = false) String bodyParam,
+ @RequestParam(required = false) String img,
+ @RequestParam(required = false) MultipartFile attach) throws IOException {
+
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous() || visitor.isBanned()) {
+ throw new HttpForbiddenException();
+ }
+ String body = StringUtils.isNotEmpty(bodyParam) ? bodyParam.replace("\r", StringUtils.EMPTY) : StringUtils.EMPTY;
+
+ URI attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir);
+
+ if (StringUtils.isBlank(attachmentFName.toString()) && StringUtils.isNotBlank(img)) {
+ try {
+ URL imgUrl = new URL(img);
+ attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir);
+ } catch (Exception e) {
+ logger.error("DOWNLOAD ERROR", e);
+ throw new HttpBadRequestException();
+ }
+ }
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+ MultiValueMap params = new LinkedMultiValueMap<>();
+ HttpEntity> request = new HttpEntity<>(params, headers);
+
+ params.add("body", body);
+ params.add("hash", userService.getHashByUID(visitor.getUid()));
+ if (StringUtils.isNotEmpty(attachmentFName.toString())) {
+ params.add("img", attachmentFName.toASCIIString());
+ }
+ URI postUri = UriComponentsBuilder.fromUriString(apiUrl).path("/api/post").build().toUri();
+ try {
+ ResponseEntity result = rest.postForEntity(postUri,
+ request, CommandResult.class);
+ Message newMessage = result.getBody().getNewMessage().orElse(new Message());
+ if (newMessage.getMid() > 0) {
+ logger.info("/post: {}", jsonMapper.writeValueAsString(result.getBody()));
+ return String.format("redirect:/%d", newMessage.getMid());
+ } else {
+ logger.info("{} : {}", body, result.getBody().getText());
+ }
+ } catch (HttpClientErrorException e) {
+ logger.error("post error", e);
+ }
+ return "redirect:/?show=my";
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java b/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java
new file mode 100644
index 00000000..f2ecccf6
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java
@@ -0,0 +1,262 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.www.controllers;
+
+import com.juick.server.helpers.NotifyOpts;
+import com.juick.server.helpers.UserInfo;
+import com.juick.server.util.*;
+import com.juick.service.*;
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.inject.Inject;
+import javax.mail.Message;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+@Controller
+public class Settings {
+ private static final Logger logger = LoggerFactory.getLogger(Settings.class);
+
+ @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String imgDir;
+ @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}")
+ private String tmpDir;
+ @Inject
+ private TagService tagService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private CrosspostService crosspostService;
+ @Inject
+ private SubscriptionService subscriptionService;
+ @Inject
+ private EmailService emailService;
+ @Inject
+ private TelegramService telegramService;
+ @Inject
+ private ImagesService imagesService;
+
+ @GetMapping("/settings")
+ protected String doGet(HttpServletRequest request, HttpServletResponse response, ModelMap model) throws IOException {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ response.sendRedirect("/login");
+ }
+ List pages = Arrays.asList("main", "password", "about", "auth-email", "privacy");
+ String page = request.getParameter("page");
+ if (StringUtils.isEmpty(page) || !pages.contains(page)) {
+ page = "main";
+ }
+
+ model.addAttribute("title", "Настройки");
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("tags", tagService.getPopularTags());
+ model.addAttribute("auths", userService.getAuthCodes(visitor));
+ model.addAttribute("email_active", emailService.getNotificationsEmail(visitor.getUid()));
+ model.addAttribute("ehash", userService.getEmailHash(visitor));
+ model.addAttribute("emails", userService.getEmails(visitor));
+ model.addAttribute("jids", userService.getAllJIDs(visitor));
+ List hours = IntStream.rangeClosed(0, 23).boxed()
+ .map(i -> StringUtils.leftPad(String.format("%d", i), 2, "0")).collect(Collectors.toList());
+ model.addAttribute("hours", hours);
+ model.addAttribute("fbstatus", crosspostService.getFbCrossPostStatus(visitor.getUid()));
+ model.addAttribute("twitter_name", crosspostService.getTwitterName(visitor.getUid()));
+ model.addAttribute("telegram_name", crosspostService.getTelegramName(visitor.getUid()));
+ model.addAttribute("notify_options", subscriptionService.getNotifyOptions(visitor));
+ model.addAttribute("userinfo", userService.getUserInfo(visitor));
+ if (page.equals("auth-email")) {
+ if (emailService.verifyAddressByCode(visitor.getUid(), request.getParameter("code"))) {
+ ;
+ model.addAttribute("result", "OK!");
+ } else {
+ model.addAttribute("result", "Sorry, code unknown.");
+ }
+ }
+ return String.format("views/settings_%s", page);
+ }
+
+ @PostMapping("/settings")
+ protected String doPost(HttpServletRequest request, HttpServletResponse response,
+ @RequestParam(required = false) MultipartFile avatar,
+ ModelMap model)
+ throws IOException {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ if (visitor.isAnonymous()) {
+ throw new HttpBadRequestException();
+ }
+ List pages = Arrays.asList("main", "password", "about", "email", "email-add", "email-del",
+ "email-subscr", "auth-email", "privacy", "jid-del", "twitter-del", "telegram-del", "facebook-disable",
+ "facebook-enable", "vk-del");
+ String page = request.getParameter("page");
+ if (StringUtils.isEmpty(page) || !pages.contains(page)) {
+ throw new HttpBadRequestException();
+ }
+ String result = StringUtils.EMPTY;
+ switch (page) {
+ case "password":
+ if (userService.updatePassword(visitor, request.getParameter("password"))) {
+ result = "Password has been changed.
";
+ String hash = userService.getHashByUID(visitor.getUid());
+ Cookie c = new Cookie("hash", hash);
+ c.setMaxAge(365 * 24 * 60 * 60);
+ response.addCookie(c);
+ }
+ break;
+ case "main":
+ NotifyOpts opts = new NotifyOpts();
+ opts.setRepliesEnabled(StringUtils.isNotEmpty(request.getParameter("jnotify")));
+ opts.setSubscriptionsEnabled(StringUtils.isNotEmpty(request.getParameter("subscr_notify")));
+ opts.setRecommendationsEnabled(StringUtils.isNotEmpty(request.getParameter("recomm")));
+ if (subscriptionService.setNotifyOptions(visitor, opts)) {
+ result = "Notification options has been updated
";
+ }
+ break;
+ case "about":
+ UserInfo info = new UserInfo();
+ info.setFullName(request.getParameter("fullname"));
+ info.setCountry(request.getParameter("country"));
+ info.setUrl(request.getParameter("url"));
+ info.setDescription(request.getParameter("descr"));
+ String avatarTmpPath = HttpUtils.receiveMultiPartFile(avatar, tmpDir).getHost();
+ if (StringUtils.isNotEmpty(avatarTmpPath)) {
+ imagesService.saveAvatar(avatarTmpPath, visitor.getUid());
+ }
+ if (userService.updateUserInfo(visitor, info)) {
+ result = String.format("Your info is updated.
Back to blog .
", visitor.getName());
+ }
+ break;
+ case "jid-del":
+ // FIXME: stop using ugnich-csv in parameters
+ String[] params = request.getParameter("delete").split(";", 2);
+ boolean res = false;
+ if (params[0].equals("xmpp")) {
+ res = userService.deleteJID(visitor.getUid(), params[1]);
+ } else if (params[0].equals("xmpp-unauth")) {
+ res = userService.unauthJID(visitor.getUid(), params[1]);
+ }
+ if (res) {
+ result = "Deleted. Back .
";
+ } else {
+ result = "Error
";
+ }
+ break;
+ case "email-add":
+ if (!emailService.verifyAddressByCode(visitor.getUid(), request.getParameter("account"))) {
+ String authCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase();
+ if (emailService.addVerificationCode(visitor.getUid(), request.getParameter("account"), authCode)) {
+ Session session = Session.getDefaultInstance(System.getProperties());
+ try {
+ MimeMessage message = new MimeMessage(session);
+ message.setFrom(new InternetAddress("noreply@mail.juick.com"));
+ message.addRecipient(Message.RecipientType.TO, new InternetAddress(request.getParameter("account")));
+ message.setSubject("Juick authorization link");
+ message.setText(String.format("Follow link to attach this email to Juick account:\n" +
+ "http://juick.com/settings?page=auth-email&code=%s\n\n" +
+ "If you don't know, what this mean - just ignore this mail.\n", authCode));
+ Transport.send(message);
+ result = "Authorization link has been sent to your email. Follow it to proceed.
" +
+ "Back
";
+
+ } catch (MessagingException ex) {
+ logger.error("mail exception", ex);
+ throw new HttpBadRequestException();
+ }
+ }
+ }
+ break;
+ case "email-del":
+ if (emailService.deleteEmail(visitor.getUid(), request.getParameter("account"))) {
+ result = "Deleted. Back .
";
+ } else {
+ result = "An error occured while deleting.
";
+ }
+ break;
+ case "email-subscr":
+ if (emailService.setNotificationsEmail(visitor.getUid(), request.getParameter("account"))) {
+ result = String.format("Saved! Will send notifications to %s ." +
+ "
Back
", request.getParameter("account"));
+ } else {
+ result = "Disabled.
Back
";
+ }
+ break;
+ case "twitter-del":
+ crosspostService.deleteTwitterToken(visitor.getUid());
+ for (Cookie cookie : request.getCookies()) {
+ if (cookie.getName().equals("request_token")) {
+ cookie.setMaxAge(0);
+ response.addCookie(cookie);
+ }
+ if (cookie.getName().equals("request_token_secret")) {
+ cookie.setMaxAge(0);
+ response.addCookie(cookie);
+ }
+ }
+ result = "Back
";
+ break;
+ case "telegram-del":
+ telegramService.deleteTelegramUser(visitor.getUid());
+ result = "Back
";
+ break;
+ case "facebook-disable":
+ crosspostService.disableFBCrosspost(visitor.getUid());
+ result = "Back
";
+ break;
+ case "facebook-enable":
+ crosspostService.enableFBCrosspost(visitor.getUid());
+ result = "Back
";
+ break;
+ case "vk-del":
+ crosspostService.deleteVKUser(visitor.getUid());
+ result = "Back
";
+ break;
+ default:
+ throw new HttpBadRequestException();
+ }
+
+ model.addAttribute("title", "Настройки");
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("result", result);
+ return "views/settings_result";
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java b/juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java
new file mode 100644
index 00000000..6d72aecc
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.www.controllers;
+
+import com.juick.server.util.HttpBadRequestException;
+import com.juick.server.util.HttpForbiddenException;
+import com.juick.server.util.UserUtils;
+import com.juick.service.CrosspostService;
+import com.juick.service.EmailService;
+import com.juick.service.MessengerService;
+import com.juick.service.UserService;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.ModelMap;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.inject.Inject;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletResponse;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+@Controller
+public class SignUp {
+
+ @Inject
+ private UserService userService;
+ @Inject
+ private CrosspostService crosspostService;
+ @Inject
+ private MessengerService messengerService;
+ @Inject
+ private EmailService emailService;
+
+
+ @GetMapping("/signup")
+ protected String doGet(@RequestParam String type, @RequestParam String hash, ModelMap model) {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+
+ if (hash.length() > 36 || !type.matches("^[a-zA-Z0-9\\-]+$")
+ || !hash.matches("^[a-zA-Z0-9\\-]+$")) {
+ throw new HttpBadRequestException();
+ }
+
+ String account = null;
+ switch (type) {
+ case "fb":
+ account = crosspostService.getFacebookNameByHash(hash);
+ break;
+ case "vk":
+ account = crosspostService.getVKNameByHash(hash);
+ break;
+ case "xmpp":
+ account = crosspostService.getJIDByHash(hash);
+ break;
+ case "durov":
+ account = crosspostService.getTelegramNameByHash(hash);
+ break;
+ case "messenger":
+ account = messengerService.getDisplayName(hash);
+ break;
+ case "email":
+ account = emailService.getEmailByAuthCode(hash);
+ }
+ if (account == null) {
+ throw new HttpBadRequestException();
+ }
+
+ model.addAttribute("title", "Новый пользователь");
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("account", account);
+ model.addAttribute("type", type);
+ model.addAttribute("hash", hash);
+ return "views/signup";
+ }
+
+ @PostMapping("/signup")
+ protected String doPost(
+ HttpServletResponse response,
+ @RequestParam String type,
+ @RequestParam String hash,
+ @RequestParam String action,
+ @RequestParam(required = false) String username,
+ @RequestParam(required = false) String password) {
+ com.juick.User visitor = UserUtils.getCurrentUser();
+ int uid = 0;
+
+ if (hash.length() > 36 || !type.matches("^[a-zA-Z0-9\\-]+$") || !hash.matches("^[a-zA-Z0-9\\-]+$")) {
+ throw new HttpBadRequestException();
+ }
+
+ if (action.charAt(0) == 'l') {
+
+ if (visitor.isAnonymous()) {
+ if (username.length() > 32) {
+ throw new HttpBadRequestException();
+ }
+ uid = userService.checkPassword(username, password);
+ } else {
+ uid = visitor.getUid();
+ }
+
+ if (uid <= 0) {
+ throw new HttpForbiddenException();
+ }
+
+ if (!(type.charAt(0) == 'f' && crosspostService.setFacebookUser(hash, uid))
+ && !(type.charAt(0) == 'v' && crosspostService.setVKUser(hash, uid))
+ && !(type.charAt(0) == 'd' && crosspostService.setTelegramUser(hash, uid))
+ && !(type.charAt(0) == 'x' && crosspostService.setJIDUser(hash, uid))
+ && !(type.charAt(0) == 'm' && messengerService.linkMessengerUser(hash, uid))) {
+ if (type.equals("email")) {
+ String email = emailService.getEmailByAuthCode(hash);
+ emailService.addEmail(uid, email);
+ emailService.deleteAuthCode(hash);
+ } else {
+ throw new HttpBadRequestException();
+ }
+ }
+
+ } else { // Create new account
+ if (username.length() < 2 || username.length() > 16 || !username.matches("^[a-zA-Z0-9\\-]+$") || password.length() < 6 || password.length() > 32) {
+ throw new HttpBadRequestException();
+ }
+
+ // CHECK USERNAME
+
+ uid = userService.createUser(username, password);
+ if (uid <= 0) {
+ throw new HttpBadRequestException();
+ }
+
+ if (!(type.charAt(0) == 'f' && crosspostService.setFacebookUser(hash, uid))
+ && !(type.charAt(0) == 'v' && crosspostService.setVKUser(hash, uid))
+ && !(type.charAt(0) == 'd' && crosspostService.setTelegramUser(hash, uid))
+ && !(type.charAt(0) == 'm' && messengerService.linkMessengerUser(hash, uid))) {
+ if (type.equals("email")) {
+ String email = emailService.getEmailByAuthCode(hash);
+ emailService.addEmail(uid, email);
+ emailService.deleteAuthCode(hash);
+ } else {
+ throw new HttpBadRequestException();
+ }
+ }
+ }
+
+ if (visitor.isAnonymous()) {
+ hash = userService.getHashByUID(uid);
+ Cookie c = new Cookie("hash", hash);
+ c.setMaxAge(365 * 24 * 60 * 60);
+ response.addCookie(c);
+ }
+ return "redirect:/";
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java b/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java
new file mode 100644
index 00000000..014a728d
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+package com.juick.server.www.controllers;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.scribejava.apis.FacebookApi;
+import com.github.scribejava.apis.TwitterApi;
+import com.github.scribejava.apis.VkontakteApi;
+import com.github.scribejava.core.builder.ServiceBuilder;
+import com.github.scribejava.core.model.*;
+import com.github.scribejava.core.oauth.OAuth10aService;
+import com.github.scribejava.core.oauth.OAuth20Service;
+import com.juick.server.util.HttpBadRequestException;
+import com.juick.server.util.UserUtils;
+import com.juick.service.CrosspostService;
+import com.juick.service.EmailService;
+import com.juick.service.TelegramService;
+import com.juick.service.UserService;
+import com.juick.server.www.Utils;
+import com.juick.server.www.facebook.User;
+import com.juick.server.www.vk.UsersResponse;
+import org.apache.commons.codec.digest.DigestUtils;
+import org.apache.commons.codec.digest.HmacAlgorithms;
+import org.apache.commons.codec.digest.HmacUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.commons.lang3.math.NumberUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.CookieValue;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
+import javax.annotation.PostConstruct;
+import javax.inject.Inject;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.Map;
+import java.util.UUID;
+import java.util.concurrent.ExecutionException;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * @author Ugnich Anton
+ */
+@Controller
+public class SocialLogin {
+
+ private static final Logger logger = LoggerFactory.getLogger(SocialLogin.class);
+
+ @Value("${facebook_appid:appid}")
+ private String FACEBOOK_APPID;
+ @Value("${facebook_secret:secret}")
+ private String FACEBOOK_SECRET;
+ private static final String FACEBOOK_REDIRECT = "https://juick.com/_fblogin";
+ private static final String VK_REDIRECT = "http://juick.com/_vklogin";
+ private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json";
+ @Inject
+ private ObjectMapper jsonMapper;
+ private ServiceBuilder facebookBuilder, twitterBuilder, vkBuilder;
+
+ @Value("${twitter_consumer_key:appid}")
+ private String twitterConsumerKey;
+ @Value("${twitter_consumer_secret:secret}")
+ private String twitterConsumerSecret;
+ @Value("${vk_appid:appid}")
+ private String VK_APPID;
+ @Value("${vk_secret:secret}")
+ private String VK_SECRET;
+ @Value("${telegram_token:secret}")
+ private String telegramToken;
+
+ @Inject
+ private CrosspostService crosspostService;
+ @Inject
+ private UserService userService;
+ @Inject
+ private EmailService emailService;
+ @Inject
+ private TelegramService telegramService;
+
+ @PostConstruct
+ public void init() {
+ facebookBuilder = new ServiceBuilder(FACEBOOK_APPID);
+ twitterBuilder = new ServiceBuilder(twitterConsumerKey);
+ vkBuilder = new ServiceBuilder(VK_APPID);
+ }
+
+ @GetMapping("/_fblogin")
+ protected String doFacebookLogin(HttpServletRequest request,
+ @RequestParam(required = false) String code,
+ @RequestParam(required = false) String state,
+ HttpServletResponse response) throws IOException, ExecutionException, InterruptedException {
+ if (StringUtils.isBlank(code)) {
+ String fbstate = UUID.randomUUID().toString();
+ crosspostService.addFacebookState(fbstate, state);
+ OAuth20Service facebookAuthService = facebookBuilder
+ .apiSecret(FACEBOOK_SECRET)
+ .callback(FACEBOOK_REDIRECT)
+ .scope("email")
+ .state(fbstate)
+ .build(FacebookApi.instance());
+ return "redirect:" + facebookAuthService.getAuthorizationUrl();
+ }
+
+ String redirectUrl = crosspostService.verifyFacebookState(state);
+ if (StringUtils.isEmpty(redirectUrl)) {
+ logger.error("state is missing");
+ throw new HttpBadRequestException();
+ }
+ OAuth20Service facebookService = facebookBuilder
+ .apiKey(FACEBOOK_APPID)
+ .apiSecret(FACEBOOK_SECRET)
+ .callback(FACEBOOK_REDIRECT)
+ .scope("email")
+ .state(state)
+ .build(FacebookApi.instance());
+ OAuth2AccessToken token = facebookService.getAccessToken(code);
+ final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v2.10/me?fields=id,name,link,verified,email");
+ facebookService.signRequest(token, meRequest);
+ String graph = facebookService.execute(meRequest).getBody();
+ if (StringUtils.isBlank(graph)) {
+ logger.error("FACEBOOK GRAPH ERROR");
+ throw new HttpBadRequestException();
+ }
+ User fb = jsonMapper.readValue(graph, User.class);
+ long fbID = NumberUtils.toLong(fb.getId(), 0);
+ if (fbID == 0 || StringUtils.isBlank(fb.getName()) || StringUtils.isBlank(fb.getLink())) {
+ logger.error("Missing required fields, id: {}, name: {}, link: {}", fbID, fb.getName(), fb.getLink());
+ throw new HttpBadRequestException();
+ }
+
+ int uid = crosspostService.getUIDbyFBID(fbID);
+ if (uid > 0) {
+ if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName(), fb.getLink())) {
+ logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken());
+ throw new HttpBadRequestException();
+ }
+ Cookie c = new Cookie("hash", userService.getHashByUID(uid));
+ c.setMaxAge(50 * 24 * 60 * 60);
+ response.addCookie(c);
+ return "redirect:" + redirectUrl;
+ } else if (fb.getVerified()) {
+ if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName(), fb.getLink())) {
+ if (StringUtils.isNotEmpty(fb.getEmail())) {
+ logger.info("found {} for facebook user {}", fb.getEmail(), fb.getLink());
+ Integer userId = crosspostService.getUIDbyFBID(fbID);
+ if (!emailService.getEmails(userId, false).contains(fb.getEmail())) {
+ emailService.addEmail(userId, fb.getEmail());
+ }
+ }
+ logger.info("email not found for facebook user {}", fb.getLink());
+ throw new HttpBadRequestException();
+ }
+ return "redirect:/signup?type=fb&hash=" + state;
+ } else {
+ logger.error("Facebook account is not verified, id: {}", fbID);
+ throw new HttpBadRequestException();
+ }
+ }
+ @GetMapping("/_twitter")
+ protected void doTwitterLogin(HttpServletRequest request, HttpServletResponse response)
+ throws IOException, ExecutionException, InterruptedException {
+ String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY;
+ String verifier = request.getParameter("oauth_verifier");
+ Cookie[] cookies = request.getCookies();
+ for (Cookie cookie : cookies) {
+ if (cookie.getName().equals("hash")) {
+ hash = cookie.getValue();
+ }
+ if (cookie.getName().equals("request_token")) {
+ request_token = cookie.getValue();
+ }
+ if (cookie.getName().equals("request_token_secret")) {
+ request_token_secret = cookie.getValue();
+ }
+ }
+ com.juick.User user = UserUtils.getCurrentUser();
+ OAuth10aService oAuthService = twitterBuilder
+ .apiSecret(twitterConsumerSecret)
+ .callback("http://juick.com/_twitter")
+ .build(TwitterApi.instance());
+
+ if (request_token.isEmpty() && request_token_secret.isEmpty()
+ && (verifier == null || verifier.isEmpty())) {
+ OAuth1RequestToken requestToken = oAuthService.getRequestToken();
+ String authUrl = oAuthService.getAuthorizationUrl(requestToken);
+ response.addCookie(new Cookie("request_token", requestToken.getToken()));
+ response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret()));
+ response.setStatus(HttpServletResponse.SC_FOUND);
+ response.setHeader("Location", authUrl);
+ } else {
+ if (verifier != null && verifier.length() > 0) {
+ OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret);
+ OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier);
+ OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL);
+ oAuthService.signRequest(accessToken, oAuthRequest);
+ com.juick.server.www.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(),
+ com.juick.server.www.twitter.User.class);
+ if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(),
+ twitterUser.getScreenName())) {
+ response.setStatus(HttpServletResponse.SC_FOUND);
+ response.setHeader("Location", "http://juick.com/settings");
+ } else {
+ response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+ }
+ }
+ }
+ }
+ @GetMapping("/_vklogin")
+ protected String doVKLogin(HttpServletRequest request,
+ @RequestParam(required = false) String code,
+ @RequestParam(required = false) String state,
+ @CookieValue(required = false) String vkstate,
+ HttpServletResponse response) throws IOException, ExecutionException, InterruptedException {
+ if (StringUtils.isBlank(code)) {
+ vkstate = UUID.randomUUID().toString();
+ Cookie c = new Cookie("vkstate", vkstate);
+ response.addCookie(c);
+ OAuth20Service vkAuthService = vkBuilder
+ .apiSecret(VK_SECRET)
+ .scope("friends,wall,offline")
+ .state(vkstate)
+ .callback(VK_REDIRECT)
+ .build(VkontakteApi.instance());
+ return "redirect:" + vkAuthService.getAuthorizationUrl();
+ }
+
+ if (StringUtils.isBlank(vkstate) || !vkstate.equals(state)) {
+ throw new HttpBadRequestException();
+ } else {
+ Cookie c = new Cookie("vkstate", "-");
+ c.setMaxAge(0);
+ response.addCookie(c);
+ }
+
+ OAuth20Service vkService = vkBuilder
+ .apiKey(VK_APPID)
+ .apiSecret(VK_SECRET)
+ .build(VkontakteApi.instance());
+ OAuth2AccessToken token = vkService.getAccessToken(code);
+
+ OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73");
+ vkService.signRequest(token, meRequest);
+ String graph = vkService.execute(meRequest).getBody();
+
+ com.juick.server.www.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0);
+ String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName();
+ String vkLink = jsonUser.getScreenName();
+
+ if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) {
+ logger.error("vk user error");
+ throw new HttpBadRequestException();
+ }
+
+ Long vkID = NumberUtils.toLong(jsonUser.getId(), 0);
+ int uid = crosspostService.getUIDbyVKID(vkID);
+ if (uid > 0) {
+ Cookie c = new Cookie("hash", userService.getHashByUID(uid));
+ c.setMaxAge(50 * 24 * 60 * 60);
+ response.addCookie(c);
+ return Utils.getPreviousPageByRequest(request).orElse("redirect:/");
+ } else {
+ String loginhash = UUID.randomUUID().toString();
+ if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) {
+ logger.error("create vk user error");
+ throw new HttpBadRequestException();
+ }
+ return "redirect:/signup?type=vk&hash=" + loginhash;
+ }
+ }
+
+ @GetMapping("/_tglogin")
+ public String doDurovLogin(HttpServletRequest request,
+ @RequestParam Map params,
+ HttpServletResponse response) {
+ String dataCheckString = params.entrySet().stream()
+ .filter(p -> !p.getKey().equals("hash"))
+ .sorted(Map.Entry.comparingByKey())
+ .map(p -> p.getKey() + "=" + p.getValue())
+ .collect(Collectors.joining("\n"));
+ String hash = params.get("hash");
+ byte[] secretKey = DigestUtils.sha256(telegramToken);
+ String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString);
+ if (hash.equals(resultString)) {
+ Long tgUser = Long.valueOf(params.get("id"));
+ int uid = telegramService.getUser(tgUser);
+ if (uid > 0) {
+ Cookie c = new Cookie("hash", userService.getHashByUID(uid));
+ c.setMaxAge(50 * 24 * 60 * 60);
+ response.addCookie(c);
+ return Utils.getPreviousPageByRequest(request).orElse("redirect:/");
+ } else {
+ String username = StringUtils.defaultString(params.get("username"), params.get("first_name"));
+ telegramService.createTelegramUser(tgUser, username);
+ return "redirect:/signup?type=durov&hash=" + userService.getSignUpHashByTelegramID(tgUser, username);
+ }
+ } else {
+ logger.warn("invalid tg hash {} for {}", resultString, hash);
+ }
+ throw new HttpBadRequestException();
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/facebook/User.java b/juick-server/src/main/java/com/juick/server/www/facebook/User.java
new file mode 100644
index 00000000..b85cf65c
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/facebook/User.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.www.facebook;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class User {
+ private String id;
+ private String name;
+ private String link;
+ private boolean verified;
+ private String firstName;
+ private String lastName;
+ private String gender;
+ private String locale;
+ private String timezone;
+ private String updatedTime;
+ private String email;
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public String getLink() {
+ return link;
+ }
+
+ public void setLink(String link) {
+ this.link = link;
+ }
+
+ public boolean getVerified() {
+ return verified;
+ }
+
+ public void setVerified(boolean verified) {
+ this.verified = verified;
+ }
+
+ @JsonProperty("first_name")
+ public String getFirstName() {
+ return firstName;
+ }
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ public String getGender() {
+ return gender;
+ }
+
+ public void setGender(String gender) {
+ this.gender = gender;
+ }
+
+ @JsonProperty("last_name")
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ public String getLocale() {
+ return locale;
+ }
+
+ public void setLocale(String locale) {
+ this.locale = locale;
+ }
+
+ public String getTimezone() {
+ return timezone;
+ }
+
+ public void setTimezone(String timezone) {
+ this.timezone = timezone;
+ }
+
+ @JsonProperty("updated_time")
+ public String getUpdatedTime() {
+ return updatedTime;
+ }
+
+ public void setUpdatedTime(String updatedTime) {
+ this.updatedTime = updatedTime;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/twitter/User.java b/juick-server/src/main/java/com/juick/server/www/twitter/User.java
new file mode 100644
index 00000000..35f708f7
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/twitter/User.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.www.twitter;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class User {
+ private String screenName;
+
+ @JsonProperty("screen_name")
+ public String getScreenName() {
+ return screenName;
+ }
+
+ public void setScreenName(String screenName) {
+ this.screenName = screenName;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/vk/Token.java b/juick-server/src/main/java/com/juick/server/www/vk/Token.java
new file mode 100644
index 00000000..c6277245
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/vk/Token.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.www.vk;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+public class Token {
+ private Long userId;
+ private String accessToken;
+ private String expiresIn;
+
+ @JsonProperty("user_id")
+ public Long getUserId() {
+ return userId;
+ }
+
+ public void setUserId(Long userId) {
+ this.userId = userId;
+ }
+
+ @JsonProperty("access_token")
+ public String getAccessToken() {
+ return accessToken;
+ }
+
+ public void setAccessToken(String accessToken) {
+ this.accessToken = accessToken;
+ }
+
+ @JsonProperty("expires_in")
+ public String getExpiresIn() {
+ return expiresIn;
+ }
+
+ public void setExpiresIn(String expiresIn) {
+ this.expiresIn = expiresIn;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/vk/User.java b/juick-server/src/main/java/com/juick/server/www/vk/User.java
new file mode 100644
index 00000000..0c491166
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/vk/User.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.www.vk;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+public class User {
+ private String id;
+ private String firstName;
+ private String lastName;
+ private String screenName;
+
+ @JsonProperty("first_name")
+ public String getFirstName() {
+ return firstName;
+ }
+
+ public void setFirstName(String firstName) {
+ this.firstName = firstName;
+ }
+
+ @JsonProperty("last_name")
+ public String getLastName() {
+ return lastName;
+ }
+
+ public void setLastName(String lastName) {
+ this.lastName = lastName;
+ }
+
+ @JsonProperty("screen_name")
+ public String getScreenName() {
+ return screenName;
+ }
+
+ public void setScreenName(String screenName) {
+ this.screenName = screenName;
+ }
+
+ public String getId() {
+ return id;
+ }
+
+ public void setId(String id) {
+ this.id = id;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java b/juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java
new file mode 100644
index 00000000..d16b9921
--- /dev/null
+++ b/juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.juick.server.www.vk;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+/**
+ * Created by vitalyster on 28.11.2016.
+ */
+public class UsersResponse {
+ private List users;
+
+ @JsonProperty("response")
+ public List getUsers() {
+ return users;
+ }
+
+ public void setUsers(List users) {
+ this.users = users;
+ }
+}
diff --git a/juick-server/src/main/java/com/juick/service/EmailServiceImpl.java b/juick-server/src/main/java/com/juick/service/EmailServiceImpl.java
index 0cccc915..5ba44e24 100644
--- a/juick-server/src/main/java/com/juick/service/EmailServiceImpl.java
+++ b/juick-server/src/main/java/com/juick/service/EmailServiceImpl.java
@@ -89,4 +89,20 @@ public class EmailServiceImpl extends BaseJdbcService implements EmailService {
return getJdbcTemplate().queryForList("SELECT email FROM emails WHERE user_id=? " +
(active ? "AND subscr_hour IS NOT NULL" : ""), String.class, userId);
}
+ @Transactional(readOnly = true)
+ @Override
+ public String getEmailByAuthCode(String code) {
+ try {
+ return getJdbcTemplate().queryForObject("SELECT account FROM auth WHERE protocol='email' AND authcode=?", String.class, code);
+ } catch (EmptyResultDataAccessException e) {
+ return StringUtils.EMPTY;
+ }
+ }
+
+ @Transactional
+ @Override
+ public void deleteAuthCode(String code) {
+ getJdbcTemplate().update("DELETE FROM auth WHERE authcode=?", code);
+ }
+
}
diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java
new file mode 100644
index 00000000..9189c2be
--- /dev/null
+++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.mitchellbosecke.pebble.extension;
+
+import com.mitchellbosecke.pebble.extension.filters.*;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Created by vitalyster on 04.05.2017.
+ */
+public class FormatterExtension extends AbstractExtension {
+ @Override
+ public Map getFilters() {
+ Map filters = new HashMap<>();
+ filters.put("formatMessage", new FormatMessageFilter());
+ filters.put("prettyTime", new PrettyTimeFilter());
+ filters.put("timestamp", new TimestampFilter());
+ filters.put("tagsList", new TagsListFilter());
+ return filters;
+ }
+}
diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java
new file mode 100644
index 00000000..5b5291f1
--- /dev/null
+++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.mitchellbosecke.pebble.extension.filters;
+
+import com.juick.Message;
+import com.juick.util.MessageUtils;
+import com.mitchellbosecke.pebble.extension.Filter;
+import com.mitchellbosecke.pebble.extension.escaper.SafeString;
+import com.mitchellbosecke.pebble.template.EvaluationContext;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Created by vitalyster on 04.05.2017.
+ */
+public class FormatMessageFilter implements Filter {
+ @Override
+ public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+ if (input instanceof Message) {
+ Message msg = (Message) input;
+ boolean isCode = msg.getTags().stream().anyMatch(t -> t.getName().equals("code"));
+ String formatString = MessageUtils.replyStartsWithQuote(msg) ? "@%s,\n%s" : "@%s, %s";
+ String msgTxt = msg.getRid() > 0 ? String.format(formatString, msg.getTo().getName(), StringUtils.defaultString(msg.getText()))
+ : StringUtils.defaultString(msg.getText());
+ String formattedMessage = isCode ? MessageUtils.formatMessageCode(msgTxt)
+ : MessageUtils.formatMessage(msgTxt);
+ return new SafeString(formattedMessage);
+ }
+ throw new IllegalArgumentException("invalid input");
+ }
+
+ @Override
+ public List getArgumentNames() {
+ return null;
+ }
+}
diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java
new file mode 100644
index 00000000..72dab20d
--- /dev/null
+++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.mitchellbosecke.pebble.extension.filters;
+
+import com.juick.util.PrettyTimeFormatter;
+import com.mitchellbosecke.pebble.extension.Filter;
+import com.mitchellbosecke.pebble.template.EvaluationContext;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Created by vitalyster on 04.05.2017.
+ */
+public class PrettyTimeFilter implements Filter {
+
+ PrettyTimeFormatter formatter = new PrettyTimeFormatter();
+
+ @Override
+ public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+ if (input instanceof Instant) {
+ Locale locale = context.getLocale();
+ return formatter.format(locale, Date.from((Instant)input));
+ }
+ throw new IllegalArgumentException("invalid input");
+ }
+
+ @Override
+ public List getArgumentNames() {
+ return null;
+ }
+}
diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java
new file mode 100644
index 00000000..c7b00ea3
--- /dev/null
+++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2008-2017, Juick
+ *
+ * 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 .
+ */
+
+package com.mitchellbosecke.pebble.extension.filters;
+
+import com.juick.Tag;
+import com.mitchellbosecke.pebble.extension.Filter;
+import com.mitchellbosecke.pebble.template.EvaluationContext;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Created by vitalyster on 23.05.2017.
+ */
+public class TagsListFilter implements Filter {
+ @SuppressWarnings("unchecked")
+ @Override
+ public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+ return ((List) input).stream().map(Tag::getName).collect(Collectors.toList());
+ }
+
+ @Override
+ public List getArgumentNames() {
+ return null;
+ }
+}
diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java
new file mode 100644
index 00000000..5f98c167
--- /dev/null
+++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java
@@ -0,0 +1,25 @@
+package com.mitchellbosecke.pebble.extension.filters;
+
+import com.mitchellbosecke.pebble.extension.Filter;
+import com.mitchellbosecke.pebble.template.EvaluationContext;
+import com.mitchellbosecke.pebble.template.PebbleTemplate;
+
+import java.time.Instant;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+public class TimestampFilter implements Filter {
+ @Override
+ public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+ if (input instanceof Instant) {
+ return Date.from((Instant)input);
+ }
+ throw new IllegalArgumentException("invalid input");
+ }
+
+ @Override
+ public List getArgumentNames() {
+ return null;
+ }
+}
diff --git a/juick-server/src/main/java/ru/sape/Sape.java b/juick-server/src/main/java/ru/sape/Sape.java
new file mode 100644
index 00000000..38577c45
--- /dev/null
+++ b/juick-server/src/main/java/ru/sape/Sape.java
@@ -0,0 +1,23 @@
+/*
+ * http://code.google.com/p/javasape/
+ */
+package ru.sape;
+
+public class Sape {
+
+ private final String sapeUser;
+ private final SapeConnection sapePageLinkConnection;
+
+ public Sape(String sapeUser, String host, int socketTimeout, int cacheLifeTime) {
+ this.sapeUser = sapeUser;
+
+ this.sapePageLinkConnection = new SapeConnection(
+ "/code.php?user=" + sapeUser + "&host=" + host,
+ "SAPE_Client PHP", socketTimeout, cacheLifeTime);
+ }
+ public boolean debug = false;
+
+ public SapePageLinks getPageLinks(String requestUri, String cookie) {
+ return new SapePageLinks(sapePageLinkConnection, sapeUser, requestUri, cookie, debug);
+ }
+}
diff --git a/juick-server/src/main/java/ru/sape/SapeConnection.java b/juick-server/src/main/java/ru/sape/SapeConnection.java
new file mode 100644
index 00000000..a15658fa
--- /dev/null
+++ b/juick-server/src/main/java/ru/sape/SapeConnection.java
@@ -0,0 +1,108 @@
+package ru.sape;
+
+import com.github.ooxi.phparser.SerializedPhpParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.io.StringWriter;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.util.*;
+
+public class SapeConnection {
+ private static final Logger logger = LoggerFactory.getLogger(SapeConnection.class);
+ private final String version = "1.0.3";
+ private final List serverList = Arrays.asList("dispenser-01.sape.ru", "dispenser-02.sape.ru");
+ private final String dispenserPath;
+ private final String userAgent;
+ private final int socketTimeout;
+ private final int cacheLifeTime;
+
+ public SapeConnection(String dispenserPath, String userAgent, int socketTimeout, int cacheLifeTime) {
+ this.dispenserPath = dispenserPath;
+ this.userAgent = userAgent;
+ this.socketTimeout = socketTimeout;
+ this.cacheLifeTime = cacheLifeTime;
+ }
+
+ protected String fetchRemoteFile(String host, String path) throws IOException {
+ Reader r = null;
+
+ try {
+ HttpURLConnection connection = (HttpURLConnection) ((new URL(("http://" + host + path)).openConnection()));
+
+ if (socketTimeout > 0) {
+ connection.setConnectTimeout(socketTimeout);
+ connection.setReadTimeout(socketTimeout);
+ }
+
+ connection.addRequestProperty("User-Agent", userAgent + ' ' + version);
+
+ connection.setDoOutput(true);
+ connection.setDoInput(true);
+ connection.setUseCaches(false);
+ connection.setRequestMethod("GET");
+ connection.connect();
+
+ r = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8"));
+
+ StringWriter sw = new StringWriter();
+
+ int b;
+
+ while ((b = r.read()) != -1) {
+ sw.write(b);
+ }
+
+ return sw.toString();
+ } finally {
+ if (r != null) {
+ r.close();
+ }
+ }
+ }
+ Map cached;
+ long cacheUpdated;
+
+ @SuppressWarnings("unchecked")
+ public Map getData() {
+ if (cacheLifeTime <= (System.currentTimeMillis() - cacheUpdated) / 1000) {
+ for (String server : serverList) {
+ String data;
+
+ try {
+ data = fetchRemoteFile(server, dispenserPath + "&charset=UTF-8");
+ } catch (IOException e1) {
+ continue;
+ }
+
+ if (data.startsWith("FATAL ERROR:")) {
+ logger.error("Sape responded with error: {}", data);
+
+ continue;
+ }
+
+ try {
+ cached = (Map) new SerializedPhpParser(data).parse();
+ } catch (Exception e) {
+ logger.error("Can't parse Sape data", e);
+ continue;
+ }
+
+ cacheUpdated = System.currentTimeMillis();
+
+ return cached;
+ }
+
+ logger.error("Unable to fetch Sape data");
+
+ return Collections.emptyMap();
+ }
+
+ return cached;
+ }
+}
diff --git a/juick-server/src/main/java/ru/sape/SapePageLinks.java b/juick-server/src/main/java/ru/sape/SapePageLinks.java
new file mode 100644
index 00000000..e89b4e71
--- /dev/null
+++ b/juick-server/src/main/java/ru/sape/SapePageLinks.java
@@ -0,0 +1,76 @@
+package ru.sape;
+
+import java.util.*;
+
+public class SapePageLinks {
+
+ private boolean showCode;
+
+ public SapePageLinks(SapeConnection sapeConnection, String sapeUser, String requestUri, String sapeCookie) {
+ this(sapeConnection, sapeUser, requestUri, sapeCookie, false);
+ }
+
+ @SuppressWarnings("unchecked")
+ public SapePageLinks(SapeConnection sapeConnection, String sapeUser, String requestUri, String sapeCookie, boolean showCode) {
+ if (sapeUser.equals(sapeCookie)) {
+ showCode = true;
+ }
+
+ Map data = sapeConnection.getData();
+
+ if (data.containsKey("__sape_delimiter__")) {
+ linkDelimiter = (String) data.get("__sape_delimiter__");
+ }
+
+ if (data.containsKey(requestUri)) {
+ pageLinks = new ArrayList<>(((Map) data.get(requestUri)).values());
+ }
+
+ if (data.containsKey("__sape_new_url__")) {
+ if (showCode) {
+ Object newUrl = data.get("__sape_new_url__");
+
+ if (newUrl instanceof Map) {
+ pageLinks = new ArrayList<>(((Map) newUrl).values());
+ } else {
+ pageLinks = new ArrayList<>(Collections.singletonList((String) newUrl));
+ }
+ }
+ }
+
+ this.showCode = showCode;
+ }
+ private String linkDelimiter = ".";
+ private List pageLinks = new ArrayList<>();
+
+ public String render() {
+ return render(-1);
+ }
+
+ public String render(int count) {
+ StringBuilder s = new StringBuilder();
+
+ if (count < 0) {
+ count = pageLinks.size();
+ }
+
+ for (Iterator i = pageLinks.iterator(); i.hasNext() && count > 0; count--) {
+ if (s.length() > 0) {
+ s.append(linkDelimiter);
+ }
+
+ String l = i.next();
+
+ s.append(l);
+
+ i.remove();
+ }
+
+ if (showCode) {
+ s.insert(0, "");
+ s.append(" ");
+ }
+
+ return s.toString();
+ }
+}
diff --git a/juick-server/src/main/resources/errors.properties b/juick-server/src/main/resources/errors.properties
new file mode 100644
index 00000000..7ec8fbfd
--- /dev/null
+++ b/juick-server/src/main/resources/errors.properties
@@ -0,0 +1,3 @@
+error.title = Error page
+
+error.login=Wrong user or password
\ No newline at end of file
diff --git a/juick-server/src/main/resources/errors_ru.properties b/juick-server/src/main/resources/errors_ru.properties
new file mode 100644
index 00000000..ca13b926
--- /dev/null
+++ b/juick-server/src/main/resources/errors_ru.properties
@@ -0,0 +1,3 @@
+error.title = Произошла ошибка
+
+error.login=Произошла ошибка, проверьте имя пользователя и пароль
\ No newline at end of file
diff --git a/juick-server/src/main/resources/help b/juick-server/src/main/resources/help
new file mode 160000
index 00000000..ce103cd9
--- /dev/null
+++ b/juick-server/src/main/resources/help
@@ -0,0 +1 @@
+Subproject commit ce103cd9a2a8a200c6ebb3b41525e7c8f639d98c
diff --git a/juick-server/src/main/resources/messages.properties b/juick-server/src/main/resources/messages.properties
new file mode 100644
index 00000000..cfd8a826
--- /dev/null
+++ b/juick-server/src/main/resources/messages.properties
@@ -0,0 +1,80 @@
+date.format=MM/dd/yyyy
+
+link.settings=Settings
+link.returnToMain=Back to Home Page
+link.contacts=Contacts
+link.tos=TOS
+link.adv=Advertisement
+
+link.popular=Popular
+link.allMessages=Discover
+link.withPhotos=Photos
+link.trends=Trends
+link.my=My feed
+link.privateMessages=PM
+link.discuss=Discuss
+link.recommended=Recommended
+link.postMessage=Post
+link.Login=Login
+link.logout=Logout
+
+link.settings.main=Main
+link.settings.password=Password
+link.settings.about=About
+
+label.sponsor=Sponsor
+label.sponsors=Sponsors
+label.search=Search
+label.register=Register
+label.username=User name
+label.password=Password
+
+postForm.newMessage=New message...
+postForm.imageLink=Link to image
+postForm.imageFormats=JPG/PNG, up to 10 MB
+postForm.or=or
+postForm.upload=Upload
+postForm.tags=Tags (space separated)
+postForm.submit=Send
+
+message.recommend=Recommend
+message.recommendedBy=♡ recommended by
+message.recommendedOthers=and {0} others
+message.comment=Comment
+message.writeComment=Write a comment...
+message.share=Share
+message.subscribe=Subscribe
+message.subscribed=Subscribed
+message.delete=Delete
+message.loginForSending=Login to post messages and comments
+message.sendLoginToXmpp=Send LOGIN to juick@juick.com
+
+messages.next=Next
+
+reply.reply=Reply
+reply.inReplyTo=in reply to
+reply.replies=Replies
+
+replies.showAsList=Show as list
+replies.showAsTree=Show as tree
+replies.unfoldAll=Unfold all
+
+question.areRegistered=Already registered?
+
+title.help=Help
+title.loginOrSignup=Juick - Log In or Sign Up
+title.index.anonym=Juick microblogs: popular posts
+title.index.user=Popular
+
+error.pageNotFound=Page not found
+error.pageNotFound.description=User probably deleted this post, or this page never existed.
+
+blog.blog=Blog
+blog.recommendations=Recommendations
+blog.photos=Photos
+blog.iread=I read
+blog.readers=My readers
+blog.bl=My blacklist
+blog.messages=Messages
+blog.comments=Comments
+blog.allPostsWithTag=All posts tagged
\ No newline at end of file
diff --git a/juick-server/src/main/resources/messages_ru.properties b/juick-server/src/main/resources/messages_ru.properties
new file mode 100644
index 00000000..2a2269ae
--- /dev/null
+++ b/juick-server/src/main/resources/messages_ru.properties
@@ -0,0 +1,78 @@
+date.format=dd.MM.yyyy
+
+link.settings=Настройки
+link.returnToMain=Вернуться на главную
+link.contacts=Контакты
+link.tos=TOS
+
+link.popular=Популярные
+link.allMessages=Обзор
+link.withPhotos=Фото
+link.trends=Темы
+link.my=Моя лента
+link.privateMessages=Приватные
+link.discuss=Диалоги
+link.recommended=Рекомендации
+link.postMessage=Написать
+link.Login=Войти
+link.logout=Выйти
+
+link.settings.main=Главная
+link.settings.password=Пароль
+link.settings.about=О пользователе
+
+label.sponsor=Спонсор
+label.sponsors=Спонсоры
+label.search=Поиск
+label.register=Зарегистрироваться
+label.username=Имя пользователя
+label.password=Пароль
+
+postForm.newMessage=Новое сообщение...
+postForm.imageLink=Ссылка на изображение
+postForm.imageFormats=JPG/PNG, до 10Мб
+postForm.or=или
+postForm.upload=загрузить
+postForm.tags=Теги (через пробел)
+postForm.submit=Отправить
+
+message.recommend=Рекомендовать
+message.recommendedBy=♡ рекомендовали
+message.recommendedOthers=и еще {0}
+message.comment=Комментировать
+message.writeComment=Написать комментарий...
+message.share=Поделиться
+message.subscribe=Подписаться
+message.subscribed=Подписан
+message.delete=Удалить
+message.loginForSending=Чтобы добавлять сообщения и комментарии, представьтесь
+message.sendLoginToXmpp=Отправьте LOGIN на juick@juick.com
+
+messages.next=Читать дальше
+
+reply.reply=Ответить
+reply.inReplyTo=в ответ на
+reply.replies=Ответы
+replies.showAsList=Показать списком
+replies.showAsTree=Показать деревом
+replies.unfoldAll=Раскрыть все
+
+question.areRegistered=Уже зарегистрированы?
+
+title.help=Справка
+title.loginOrSignup=Juick - Войдите в систему или зарегистрируйтесь
+title.index.anonym=Микроблоги Juick: популярные записи
+title.index.user=Популярные
+
+error.pageNotFound=Страница не найдена
+error.pageNotFound.description=Сожалеем, но страницу с этим адресом удалил её автор, либо её никогда не существовало.
+
+blog.blog=Блог
+blog.recommendations=Рекомендации
+blog.photos=Фотографии
+blog.iread=Я читаю
+blog.readers=Мои подписчики
+blog.bl=Черный список
+blog.messages=Сообщения
+blog.comments=Комментарии
+blog.allPostsWithTag=Все записи с тегом
\ No newline at end of file
diff --git a/juick-server/src/main/resources/schema.sql b/juick-server/src/main/resources/schema.sql
index 851b764b..2e8fad9b 100644
--- a/juick-server/src/main/resources/schema.sql
+++ b/juick-server/src/main/resources/schema.sql
@@ -23,12 +23,12 @@ CREATE TABLE IF NOT EXISTS `bl_users` (
);
CREATE TABLE IF NOT EXISTS `facebook` (
`user_id` int(10) unsigned DEFAULT NULL,
- `fb_id` bigint(20) unsigned NOT NULL,
+ `fb_id` bigint(20) unsigned NULL,
`loginhash` char(36) DEFAULT NULL,
`access_token` char(255) DEFAULT NULL,
`ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
- `fb_name` char(64) NOT NULL,
- `fb_link` char(255) NOT NULL,
+ `fb_name` char(64) NULL,
+ `fb_link` char(255) NULL,
`crosspost` tinyint(1) unsigned NOT NULL DEFAULT '1'
);
diff --git a/juick-server/src/main/resources/static/favicon.png b/juick-server/src/main/resources/static/favicon.png
new file mode 100644
index 00000000..bc7161e2
Binary files /dev/null and b/juick-server/src/main/resources/static/favicon.png differ
diff --git a/juick-server/src/main/resources/static/logo.png b/juick-server/src/main/resources/static/logo.png
new file mode 100644
index 00000000..933f6099
Binary files /dev/null and b/juick-server/src/main/resources/static/logo.png differ
diff --git a/juick-server/src/main/resources/static/style.js b/juick-server/src/main/resources/static/style.js
new file mode 100644
index 00000000..8e1ce009
--- /dev/null
+++ b/juick-server/src/main/resources/static/style.js
@@ -0,0 +1,2 @@
+!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=10)}({10:function(e,t,n){n(11),n(13),e.exports=n(15)},11:function(e,t,n){},13:function(e,t,n){},15:function(e,t,n){}});
+//# sourceMappingURL=style.js.map
\ No newline at end of file
diff --git a/juick-server/src/main/resources/static/style.js.map b/juick-server/src/main/resources/static/style.js.map
new file mode 100644
index 00000000..f3493280
--- /dev/null
+++ b/juick-server/src/main/resources/static/style.js.map
@@ -0,0 +1 @@
+{"version":3,"sources":["webpack:///webpack/bootstrap"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s"],"mappings":"aACA,IAAAA,KAGA,SAAAC,EAAAC,GAGA,GAAAF,EAAAE,GACA,OAAAF,EAAAE,GAAAC,QAGA,IAAAC,EAAAJ,EAAAE,IACAG,EAAAH,EACAI,GAAA,EACAH,YAUA,OANAI,EAAAL,GAAAM,KAAAJ,EAAAD,QAAAC,IAAAD,QAAAF,GAGAG,EAAAE,GAAA,EAGAF,EAAAD,QAKAF,EAAAQ,EAAAF,EAGAN,EAAAS,EAAAV,EAGAC,EAAAU,EAAA,SAAAR,EAAAS,EAAAC,GACAZ,EAAAa,EAAAX,EAAAS,IACAG,OAAAC,eAAAb,EAAAS,GAA0CK,YAAA,EAAAC,IAAAL,KAK1CZ,EAAAkB,EAAA,SAAAhB,GACA,oBAAAiB,eAAAC,aACAN,OAAAC,eAAAb,EAAAiB,OAAAC,aAAwDC,MAAA,WAExDP,OAAAC,eAAAb,EAAA,cAAiDmB,OAAA,KAQjDrB,EAAAsB,EAAA,SAAAD,EAAAE,GAEA,GADA,EAAAA,IAAAF,EAAArB,EAAAqB,IACA,EAAAE,EAAA,OAAAF,EACA,KAAAE,GAAA,iBAAAF,QAAAG,WAAA,OAAAH,EACA,IAAAI,EAAAX,OAAAY,OAAA,MAGA,GAFA1B,EAAAkB,EAAAO,GACAX,OAAAC,eAAAU,EAAA,WAAyCT,YAAA,EAAAK,UACzC,EAAAE,GAAA,iBAAAF,EAAA,QAAAM,KAAAN,EAAArB,EAAAU,EAAAe,EAAAE,EAAA,SAAAA,GAAgH,OAAAN,EAAAM,IAAqBC,KAAA,KAAAD,IACrI,OAAAF,GAIAzB,EAAA6B,EAAA,SAAA1B,GACA,IAAAS,EAAAT,KAAAqB,WACA,WAA2B,OAAArB,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAH,EAAAU,EAAAE,EAAA,IAAAA,GACAA,GAIAZ,EAAAa,EAAA,SAAAiB,EAAAC,GAAsD,OAAAjB,OAAAkB,UAAAC,eAAA1B,KAAAuB,EAAAC,IAGtD/B,EAAAkC,EAAA,GAIAlC,IAAAmC,EAAA","file":"style.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 10);\n"],"sourceRoot":""}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/static/tagscloud.png b/juick-server/src/main/resources/static/tagscloud.png
new file mode 100644
index 00000000..3e1bf169
Binary files /dev/null and b/juick-server/src/main/resources/static/tagscloud.png differ
diff --git a/juick-server/src/main/resources/templates/layouts/content.html b/juick-server/src/main/resources/templates/layouts/content.html
new file mode 100644
index 00000000..2ca9fd7e
--- /dev/null
+++ b/juick-server/src/main/resources/templates/layouts/content.html
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+ {% block headers %}
+ {{ headers | default('') | raw }}
+ {% endblock %}
+ {{ title | default('Juick') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 %}data-hash="{{visitor.authHash}}"{% endif %}>
+{% block body %}
+{% endblock %}
+
+
diff --git a/juick-server/src/main/resources/templates/layouts/default.html b/juick-server/src/main/resources/templates/layouts/default.html
new file mode 100644
index 00000000..343885c4
--- /dev/null
+++ b/juick-server/src/main/resources/templates/layouts/default.html
@@ -0,0 +1,16 @@
+{% extends "layouts/content" %}
+{% block body %}
+{% include "views/partial/navigation" %}
+
+
+ {% block content %}
+ {% endblock %}
+
+
+ {% block column %}
+ {% endblock %}
+
+
+{% include "views/partial/footer" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/layouts/minimal.html b/juick-server/src/main/resources/templates/layouts/minimal.html
new file mode 100644
index 00000000..15924521
--- /dev/null
+++ b/juick-server/src/main/resources/templates/layouts/minimal.html
@@ -0,0 +1,10 @@
+{% extends "layouts/content" %}
+{% block body %}
+
+
+ {% block content %}
+ {% endblock %}
+
+
+{% include "views/partial/footer" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/404.html b/juick-server/src/main/resources/templates/views/404.html
new file mode 100644
index 00000000..02a790e6
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/404.html
@@ -0,0 +1,11 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+ Страница не найдена
+ Сожалеем, но страницу с этим адресом удалил её автор, либо её никогда не существовало.
+
+{% endblock %}
+
+{% block "column" %}
+{% include "views/partial/homecolumn" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/blog.html b/juick-server/src/main/resources/templates/views/blog.html
new file mode 100644
index 00000000..9cf4714e
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/blog.html
@@ -0,0 +1,25 @@
+{% extends "layouts/default" %}
+{% import "views/macros/tags" %}
+{% block content %}
+{% if noindex %}
+
+{% endif %}
+{% if paramTag | default('') is not empty %}
+← {{ i18n("messages","blog.allPostsWithTag") }} {{ paramTag.name | escape }}
+{% endif %}
+
+
+{% for msg in msgs %}
+{% include "views/partial/message" %}
+{% endfor %}
+
+{% if nextpage | default('') is not empty %}
+{{ i18n("messages","messages.next") }} →
+{% endif %}
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/usercolumn" %}
+{% if noindex %}
+
+{% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/blog_tags.html b/juick-server/src/main/resources/templates/views/blog_tags.html
new file mode 100644
index 00000000..48e517eb
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/blog_tags.html
@@ -0,0 +1,10 @@
+{% extends "layouts/default" %}
+{% import "views/macros/tags" %}
+{% block content %}
+
+ {{ tags(user.name, tags) }}
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/usercolumn" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/help.html b/juick-server/src/main/resources/templates/views/help.html
new file mode 100644
index 00000000..3a022497
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/help.html
@@ -0,0 +1,10 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+ {{ content | raw }}
+
+{% endblock %}
+
+{% block "column" %}
+{{ navigation | raw }}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/index.html b/juick-server/src/main/resources/templates/views/index.html
new file mode 100644
index 00000000..97d726de
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/index.html
@@ -0,0 +1,29 @@
+{% extends "layouts/default" %}
+{% import "views/macros/tags" %}
+{% block content %}
+{% if noindex %}
+
+{% endif %}
+{% for msg in msgs %}
+{% include "views/partial/message" %}
+{% endfor %}
+{% if nextpage | default('') is not empty %}
+{{ i18n("messages","messages.next") }} →
+{% endif %}
+{% endblock %}
+{% block "column" %}
+{% if tag | default('') is not empty %}
+{% include "views/partial/tagcolumn" %}
+{% elseif visitor.uid > 0 %}
+{% if discover %}
+{% include "views/partial/homecolumn" %}
+{% else %}
+{% include "views/partial/usercolumn" %}
+{% endif %}
+{% else %}
+{% include "views/partial/homecolumn" %}
+{% endif %}
+{% if noindex %}
+
+{% endif %}
+{% endblock %}
diff --git a/juick-server/src/main/resources/templates/views/login.html b/juick-server/src/main/resources/templates/views/login.html
new file mode 100644
index 00000000..a538cb26
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/login.html
@@ -0,0 +1,144 @@
+
+
+
+ Juick
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n("messages","label.register") }}:
+
+
+
+
+
+
+
+
+
+
diff --git a/juick-server/src/main/resources/templates/views/login_success.html b/juick-server/src/main/resources/templates/views/login_success.html
new file mode 100644
index 00000000..ee71f12f
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/login_success.html
@@ -0,0 +1,13 @@
+
+
+
+
+ Blank window
+
+
+
+
+
diff --git a/juick-server/src/main/resources/templates/views/macros/tags.html b/juick-server/src/main/resources/templates/views/macros/tags.html
new file mode 100644
index 00000000..09278ffe
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/macros/tags.html
@@ -0,0 +1,5 @@
+{% macro tags(uname="", tagsList) %}
+{% for tag in tagsList %}
+{{ tag | raw }}
+{% endfor %}
+{% endmacro %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/partial/footer.html b/juick-server/src/main/resources/templates/views/partial/footer.html
new file mode 100644
index 00000000..35972254
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/footer.html
@@ -0,0 +1,16 @@
+
diff --git a/juick-server/src/main/resources/templates/views/partial/homecolumn.html b/juick-server/src/main/resources/templates/views/partial/homecolumn.html
new file mode 100644
index 00000000..64dd9cbd
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/homecolumn.html
@@ -0,0 +1,25 @@
+
+
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/partial/message.html b/juick-server/src/main/resources/templates/views/partial/message.html
new file mode 100644
index 00000000..0b6db3df
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/message.html
@@ -0,0 +1,76 @@
+
+
+ {{ msg | formatMessage }}
+ {% if msg.AttachmentType is not empty %}
+
+
+
+ {% endif %}
+
+ {% if visitor.uid == msg.user.uid %}
+
+
+
+ {% if msg.likes > 0 %} {{ msg.likes }}{% endif %}
+
+ {{ i18n("messages","message.recommend") }}
+
+ {% elseif visitor.uid > 0 %}
+
+
+
+ {% if msg.likes > 0 %} {{ msg.likes }}{% endif %}
+
+ {{ i18n("messages","message.recommend") }}
+
+ {% else %}
+
+
+
+ {% if msg.likes > 0 %} {{ msg.likes }}{% endif %}
+
+ {{ i18n("messages","message.recommend") }}
+
+ {% endif %}
+ {% if (not msg.ReadOnly) or (visitor.uid == msg.user.uid) %}
+
+
+ {% endif %}
+ {% if msg.FriendsOnly %}
+ Открыть доступ
+ {% endif %}
+
+
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/partial/navigation.html b/juick-server/src/main/resources/templates/views/partial/navigation.html
new file mode 100644
index 00000000..fa1dadcc
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/navigation.html
@@ -0,0 +1,40 @@
+
diff --git a/juick-server/src/main/resources/templates/views/partial/settings_tabs.html b/juick-server/src/main/resources/templates/views/partial/settings_tabs.html
new file mode 100644
index 00000000..4715253e
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/settings_tabs.html
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/partial/tagcolumn.html b/juick-server/src/main/resources/templates/views/partial/tagcolumn.html
new file mode 100644
index 00000000..3e61d3d3
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/tagcolumn.html
@@ -0,0 +1,33 @@
+
+
*{{ tag.name }}
+
+{% if visitor is not empty and visitor.uid > 0 %}
+
+{% endif %}
diff --git a/juick-server/src/main/resources/templates/views/partial/tags.html b/juick-server/src/main/resources/templates/views/partial/tags.html
new file mode 100644
index 00000000..3235213e
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/tags.html
@@ -0,0 +1,3 @@
+{% for tag in tags %}
+ {{ tag | raw }}
+{% endfor %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/partial/usercolumn.html b/juick-server/src/main/resources/templates/views/partial/usercolumn.html
new file mode 100644
index 00000000..2b1963e3
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/usercolumn.html
@@ -0,0 +1,89 @@
+{% if visitor is not empty and visitor.uid > 0 and visitor.uid != user.uid %}
+
+
+{% else %}
+
+{% endif %}
+
+
+
+
+
+{% include "views/partial/usertags" %}
+
+
+
+ {% if iread is not empty %}
+
+ {% for u in iread %}
+
+
+
+
+
+ {% endfor %}
+
+ {% endif %}
+
+
diff --git a/juick-server/src/main/resources/templates/views/partial/usertags.html b/juick-server/src/main/resources/templates/views/partial/usertags.html
new file mode 100644
index 00000000..71d1303e
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/partial/usertags.html
@@ -0,0 +1,3 @@
+{% import "views/macros/tags" %}
+{{ tags(user.name, tagStats) }}
+...
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/pm_inbox.html b/juick-server/src/main/resources/templates/views/pm_inbox.html
new file mode 100644
index 00000000..d6a9b65f
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/pm_inbox.html
@@ -0,0 +1,35 @@
+{% extends "layouts/default" %}
+{% block content %}
+{% if not msgs.isEmpty() %}
+
+ {% for msg in msgs %}
+
+
+
+
+
{{ msg | formatMessage }}
+
+
+
+
+
+
+ {% endfor %}
+
+{% endif %}
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/usercolumn" %}
+{% endblock %}
diff --git a/juick-server/src/main/resources/templates/views/pm_sent.html b/juick-server/src/main/resources/templates/views/pm_sent.html
new file mode 100644
index 00000000..bc42c4ab
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/pm_sent.html
@@ -0,0 +1,33 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+
+
+{% if not msgs.isEmpty() %}
+
+ {% for msg in msgs %}
+
+
+
+
{{ msg | formatMessage }}
+
+
+ {% endfor %}
+
+{% endif %}
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/usercolumn" %}
+{% endblock %}
diff --git a/juick-server/src/main/resources/templates/views/post.html b/juick-server/src/main/resources/templates/views/post.html
new file mode 100644
index 00000000..1f642ce1
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/post.html
@@ -0,0 +1,19 @@
+{% extends "layouts/minimal" %}
+{% import "views/macros/tags" %}
+{% block content %}
+
+
+
+ Фото:
+ ({{ i18n("messages","postForm.imageFormats") }})
+
+
+ {{ body }}
+
+
+
+
+
+Теги:
+{{ tags(visitor.name, tags) }}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/post_success.html b/juick-server/src/main/resources/templates/views/post_success.html
new file mode 100644
index 00000000..2106f3cb
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/post_success.html
@@ -0,0 +1,19 @@
+{% extends "layouts/minimal" %}
+{% block content %}
+Сообщение опубликовано
+Поделитесь своим новым постом в социальных сетях:
+{% if sharetwi | default('') is not empty %}
+
+ Отправить в Twitter
+{% endif %}
+
+ Отправить в ВКонтакте
+{% if facebook | default('') is not empty %}
+
+ Отправить в Facebook
+{% endif %}
+Ссылка на сообщение: {{ url }}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/settings_about.html b/juick-server/src/main/resources/templates/views/settings_about.html
new file mode 100644
index 00000000..bbf9e772
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/settings_about.html
@@ -0,0 +1,20 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+
+ Full name:
+ Country:
+ URL:
+ Please, start with "http://"
+ About:
+
+ Max. 255 symbols
+ Avatar:
+ Recommendations: PNG, 96x96, <50Kb. Also, JPG and GIF supported.
+
+
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/settings_tabs" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/settings_auth-email.html b/juick-server/src/main/resources/templates/views/settings_auth-email.html
new file mode 100644
index 00000000..e906d704
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/settings_auth-email.html
@@ -0,0 +1,9 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+ {{ result }}
Settings .
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/settings_tabs" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/settings_main.html b/juick-server/src/main/resources/templates/views/settings_main.html
new file mode 100644
index 00000000..65fbc984
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/settings_main.html
@@ -0,0 +1,151 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+ Настройки
+
+
+ Notification options
+ Reply notifications ("Message posted")
+ Subscriptions notifications ("@user subscribed...")
+ Posts recommendations ("Recommended by @user")
+
+
+
+
+
+ Telegram
+ {% if telegram_name is not empty %}
+
+ Telegram: {{ telegram_name }} —
+
+
+
+
+ {% else %}
+ To connect Telegram account: send any text message to @Juick_bot
+
+ {% endif %}
+
+ {% if jids | length > 0 %}
+
+
+
+ XMPP accounts
+
+ Your accounts:
+
+ {% for jid in jids %}
+ {{ jid }}
+ {% endfor %}
+ {% for auth in auths %}
+ {{ auth.account }}
+ — Confirm
+ {% endfor %}
+
+ {% if jids | length > 1 %}
+
+ {% endif %}
+ To add new jabber account: send any text message to juick@juick.com
+
+
+
+ {% endif %}
+
+
+ E-mail
+
+
+ Add account:
+
+
+
+
+
+
+ Your accounts:
+
+ {% for email in emails %}
+ {{ email }}
+ {% endfor %}
+ {% if emails is empty %}
+ -
+ {% else %}
+
+ {% if jids | length > 1 %}
+
+ {% endif %}
+ {% endif %}
+
+ {% if emails is not empty %}
+
+
+ You can receive notifications to email:
+ Sent to
+ Disabled
+ {% for email in emails %}
+
+ {{ email }}
+
+ {% endfor %}
+
+
+
+
+
+ {% endif %}
+
+ You can post to Juick via e-mail. Send your plain text
+ messages to juick@juick.com . You can attach one photo or video file.
+
+
+
+ Facebook
+
+ {% if fbstatus.connected %}
+ {% if fbstatus.crosspostEnabled %}
+
+
+ Facebook: Enabled —
+
+
+
+
+ {% else %}
+
+
+ Facebook: Disabled —
+
+
+
+
+ {% endif %}
+ {% else %}
+ Cross-posting to Facebook:
+ {% endif %}
+
+
+
+ Twitter
+ {% if twitter_name is not empty %}
+
+ Twitter: {{ twitter_name }} —
+
+
+
+
+ {% else %}
+ Cross-posting to Twitter:
+ {% endif %}
+
+
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/settings_tabs" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/settings_password.html b/juick-server/src/main/resources/templates/views/settings_password.html
new file mode 100644
index 00000000..aba0b139
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/settings_password.html
@@ -0,0 +1,17 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+
+ Changing your password
+
+
+ Change password:
+ (max. length - 16 symbols)
+
+
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/settings_tabs" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/settings_privacy.html b/juick-server/src/main/resources/templates/views/settings_privacy.html
new file mode 100644
index 00000000..83b87b93
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/settings_privacy.html
@@ -0,0 +1,9 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+ Privacy
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/settings_tabs" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/settings_result.html b/juick-server/src/main/resources/templates/views/settings_result.html
new file mode 100644
index 00000000..d87a5ea6
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/settings_result.html
@@ -0,0 +1,9 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+ {{ result | raw }}
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/settings_tabs" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/signup.html b/juick-server/src/main/resources/templates/views/signup.html
new file mode 100644
index 00000000..d6eb921f
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/signup.html
@@ -0,0 +1,43 @@
+{% extends "layouts/default" %}
+{% block content %}
+
+ {% if type | slice(0, 1) == 'f' %}
+
+ {% elseif type | slice(0, 1) == 'v' %}
+
+ {% elseif type | slice(0, 1) == 'e' %}
+
+ {% elseif type | slice(0, 1) == 'd' %}
+
+ {% endif %}
+ {{ account | raw }}
+
+Связать с существующим аккаунтом Juick
+
+
+
+
+ {% if visitor.getUID() > 0 %}
+
+ {% else %}
+ Имя пользователя:
+ Пароль:
+
+ {% endif %}
+
+
+{% if type != "xmpp" %}
+
+
+Создать новый аккаунт Juick
+
+
+
+
+ Имя пользователя: (От 2-х до 16-и латинских символов
+ и/или цифр, дефис)
+ Пароль: (от 6-и до 32-х символов)
+
+
+{% endif %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/test.html b/juick-server/src/main/resources/templates/views/test.html
new file mode 100644
index 00000000..7700be6f
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/test.html
@@ -0,0 +1,2 @@
+{% import "views/macros/tags" %}
+{{ tags("ugnich", tagsList)}}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/thread.html b/juick-server/src/main/resources/templates/views/thread.html
new file mode 100644
index 00000000..d281e3bb
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/thread.html
@@ -0,0 +1,173 @@
+{% extends "layouts/default" %}
+{% import "views/macros/tags" %}
+{% block content %}
+
+
+
+
+
{{ msg | formatMessage }}
+ {% if msg.AttachmentType is not empty %}
+
+ {% endif %}
+
+ {% if visitor.uid == msg.user.uid %}
+
+
+
+ {% if msg.Likes > 0 %} {{ msg.Likes }}{% endif %}
+
+ {{ i18n("messages","message.recommend") }}
+
+ {% elseif visitor.uid > 0 %}
+
+
+
+ {% if msg.Likes > 0 %} {{ msg.Likes }}{% endif %}
+
+ {{ i18n("messages","message.recommend") }}
+
+ {% else %}
+
+
+
+ {% if msg.Likes > 0 %} {{ msg.Likes }}{% endif %}
+
+ {{ i18n("messages","message.recommend") }}
+
+
+ {% endif %}
+
+ {% if visitor.uid > 0 %}
+ {% if visitor.uid != msg.user.uid %}
+ {% if visitorSubscribed %}
+
+
+ {{ i18n("messages","message.subscribed") }}
+
+ {% else %}
+
+
+ {{ i18n("messages","message.subscribe") }}
+
+ {% endif %}
+ {% else %}
+
+
+ {{ i18n("messages","message.delete") }}
+
+ {% endif %}
+ {% endif %}
+ {% if msg.FriendsOnly %}
+ Открыть доступ
+ {% endif %}
+
+ {% if msg.VisitorCanComment %}
+
+ {% endif %}
+ {% if recomm is not empty %}
+
{{ i18n("messages","message.recommendedBy") }}
+ {% for rec in recomm %}
+
@{{ rec }} {% if loop.index < (loop.length - 1) %}, {% endif %}
+ {% endfor %}
+ {% if msg.likes > recomm.size() %}
+ {{ i18n("messages","message.recommendedOthers", msg.likes - recomm.size()) }}
+ {% endif %}
+
+ {% endif %}
+
+
+
+
+ {% if visitor.uid > 0 %}
+
+ {% endif %}
+
{{ i18n("messages","reply.replies") }} ({{ replies.size() }})
+
+
+
+ {% for msg in replies %}
+
+
+
+
{{ msg | formatMessage }}
+ {% if msg.AttachmentType is not empty %}
+
+ {% endif %}
+
/{{ msg.rid }}
+ {% if msg.replyto > 0 %}
+ {{ i18n("messages","reply.inReplyTo") }}
/{{ msg.replyto }}
+ {% endif %}
+ {% if msg.VisitorCanComment %}
+ ·
+
+
+ {% elseif visitor.uid == 0 %}
+ ·
{{ i18n("messages","reply.reply") }}
+
+ {% else %}
+
+ {% endif %}
+
+
+ {% endfor %}
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/usercolumn" %}
+{% endblock %}
\ No newline at end of file
diff --git a/juick-server/src/main/resources/templates/views/users.html b/juick-server/src/main/resources/templates/views/users.html
new file mode 100644
index 00000000..702ba6b9
--- /dev/null
+++ b/juick-server/src/main/resources/templates/views/users.html
@@ -0,0 +1,17 @@
+{% extends "layouts/default" %}
+{% import "views/macros/tags" %}
+{% block content %}
+
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/usercolumn" %}
+{% endblock %}
\ No newline at end of file
--
cgit v1.2.3