aboutsummaryrefslogtreecommitdiff
path: root/juick-server/src/main
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2018-09-06 13:58:40 +0300
committerGravatar Vitaly Takmazov2018-09-07 05:15:51 -0400
commit670913698776e09b7ff44f44ccdcf56303d79be3 (patch)
tree96f932645ab0faf9de6861f89072d1eb4b2622a3 /juick-server/src/main
parent51489a954463567f8dd50854826c03ce51c45cb3 (diff)
merge legacy www
Diffstat (limited to 'juick-server/src/main')
-rw-r--r--juick-server/src/main/assets/embed.js336
-rw-r--r--juick-server/src/main/assets/logo.pngbin0 -> 2447 bytes
-rw-r--r--juick-server/src/main/assets/logo@2x.pngbin0 -> 4822 bytes
-rw-r--r--juick-server/src/main/assets/scripts.js805
-rw-r--r--juick-server/src/main/assets/style.css952
-rw-r--r--juick-server/src/main/java/com/juick/server/TelegramBotManager.java1
-rw-r--r--juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java302
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Index.java4
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Messages.java12
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Notifications.java14
-rw-r--r--juick-server/src/main/java/com/juick/server/api/PM.java6
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Post.java12
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Service.java2
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Tags.java2
-rw-r--r--juick-server/src/main/java/com/juick/server/api/Users.java12
-rw-r--r--juick-server/src/main/java/com/juick/server/api/activity/Profile.java14
-rw-r--r--juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java127
-rw-r--r--juick-server/src/main/java/com/juick/server/configuration/PostConfig.java9
-rw-r--r--juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java37
-rw-r--r--juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java209
-rw-r--r--juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java116
-rw-r--r--juick-server/src/main/java/com/juick/server/www/HelpService.java69
-rw-r--r--juick-server/src/main/java/com/juick/server/www/Utils.java45
-rw-r--r--juick-server/src/main/java/com/juick/server/www/WebApp.java71
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java64
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java49
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/Help.java93
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/Login.java50
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java595
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java263
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/Settings.java262
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java172
-rw-r--r--juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java (renamed from juick-server/src/main/java/com/juick/server/api/SocialLogin.java)83
-rw-r--r--juick-server/src/main/java/com/juick/server/www/facebook/User.java125
-rw-r--r--juick-server/src/main/java/com/juick/server/www/twitter/User.java38
-rw-r--r--juick-server/src/main/java/com/juick/server/www/vk/Token.java56
-rw-r--r--juick-server/src/main/java/com/juick/server/www/vk/User.java65
-rw-r--r--juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java38
-rw-r--r--juick-server/src/main/java/com/juick/service/EmailServiceImpl.java16
-rw-r--r--juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java38
-rw-r--r--juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java54
-rw-r--r--juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java51
-rw-r--r--juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java43
-rw-r--r--juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java25
-rw-r--r--juick-server/src/main/java/ru/sape/Sape.java23
-rw-r--r--juick-server/src/main/java/ru/sape/SapeConnection.java108
-rw-r--r--juick-server/src/main/java/ru/sape/SapePageLinks.java76
-rw-r--r--juick-server/src/main/resources/errors.properties3
-rw-r--r--juick-server/src/main/resources/errors_ru.properties3
m---------juick-server/src/main/resources/help0
-rw-r--r--juick-server/src/main/resources/messages.properties80
-rw-r--r--juick-server/src/main/resources/messages_ru.properties78
-rw-r--r--juick-server/src/main/resources/schema.sql6
-rw-r--r--juick-server/src/main/resources/static/favicon.pngbin0 -> 244 bytes
-rw-r--r--juick-server/src/main/resources/static/logo.pngbin0 -> 1184 bytes
-rw-r--r--juick-server/src/main/resources/static/style.js2
-rw-r--r--juick-server/src/main/resources/static/style.js.map1
-rw-r--r--juick-server/src/main/resources/static/tagscloud.pngbin0 -> 42316 bytes
-rw-r--r--juick-server/src/main/resources/templates/layouts/content.html50
-rw-r--r--juick-server/src/main/resources/templates/layouts/default.html16
-rw-r--r--juick-server/src/main/resources/templates/layouts/minimal.html10
-rw-r--r--juick-server/src/main/resources/templates/views/404.html11
-rw-r--r--juick-server/src/main/resources/templates/views/blog.html25
-rw-r--r--juick-server/src/main/resources/templates/views/blog_tags.html10
-rw-r--r--juick-server/src/main/resources/templates/views/help.html10
-rw-r--r--juick-server/src/main/resources/templates/views/index.html29
-rw-r--r--juick-server/src/main/resources/templates/views/login.html144
-rw-r--r--juick-server/src/main/resources/templates/views/login_success.html13
-rw-r--r--juick-server/src/main/resources/templates/views/macros/tags.html5
-rw-r--r--juick-server/src/main/resources/templates/views/partial/footer.html16
-rw-r--r--juick-server/src/main/resources/templates/views/partial/homecolumn.html25
-rw-r--r--juick-server/src/main/resources/templates/views/partial/message.html76
-rw-r--r--juick-server/src/main/resources/templates/views/partial/navigation.html40
-rw-r--r--juick-server/src/main/resources/templates/views/partial/settings_tabs.html6
-rw-r--r--juick-server/src/main/resources/templates/views/partial/tagcolumn.html33
-rw-r--r--juick-server/src/main/resources/templates/views/partial/tags.html3
-rw-r--r--juick-server/src/main/resources/templates/views/partial/usercolumn.html89
-rw-r--r--juick-server/src/main/resources/templates/views/partial/usertags.html3
-rw-r--r--juick-server/src/main/resources/templates/views/pm_inbox.html35
-rw-r--r--juick-server/src/main/resources/templates/views/pm_sent.html33
-rw-r--r--juick-server/src/main/resources/templates/views/post.html19
-rw-r--r--juick-server/src/main/resources/templates/views/post_success.html19
-rw-r--r--juick-server/src/main/resources/templates/views/settings_about.html20
-rw-r--r--juick-server/src/main/resources/templates/views/settings_auth-email.html9
-rw-r--r--juick-server/src/main/resources/templates/views/settings_main.html151
-rw-r--r--juick-server/src/main/resources/templates/views/settings_password.html17
-rw-r--r--juick-server/src/main/resources/templates/views/settings_privacy.html9
-rw-r--r--juick-server/src/main/resources/templates/views/settings_result.html9
-rw-r--r--juick-server/src/main/resources/templates/views/signup.html43
-rw-r--r--juick-server/src/main/resources/templates/views/test.html2
-rw-r--r--juick-server/src/main/resources/templates/views/thread.html173
-rw-r--r--juick-server/src/main/resources/templates/views/users.html17
92 files changed, 6677 insertions, 210 deletions
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)
+ ? `<a href="${fixWwwLink(p2 || p3)}">${p1}</a>`
+ : `<a href="${fixWwwLink(match)}">${extractDomain(match)}</a>`;
+}
+
+function urlReplaceInCode(match, p1, p2, p3) {
+ let isBrackets = (p1 !== undefined);
+ return (isBrackets)
+ ? `<a href="${fixWwwLink(p2 || p3)}">${match}</a>`
+ : `<a href="${fixWwwLink(match)}">${match}</a>`;
+}
+
+function messageReplyReplace(messageId) {
+ return function(match, mid, rid) {
+ let replyPart = (rid && rid != '0') ? '#' + rid : '';
+ return `<a href="/${mid || messageId}${replyPart}">${match}</a>`;
+ };
+}
+
+/**
+ * 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(/^(?:>|&gt;)\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: '<a href="/$1">@$1</a>' },
+ ])
+ : formatText(txt, [
+ { pr: 0, re: /((?:^(?:>|&gt;)\s?[\s\S]+?$\n?)+)/gmi, brackets: true, with: ['<q>', '</q>', 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: '<a href="/$1">@$1</a>' },
+ { pr: 2, re: /\B\*([^\n]+?)\*((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<b>', '</b>'] },
+ { pr: 2, re: /\B\/([^\n]+?)\/((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<i>', '</i>'] },
+ { pr: 2, re: /\b\_([^\n]+?)\_((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<span class="u">', '</span>'] },
+ { pr: 3, re: /\n/g, with: '<br/>' },
+ ]);
+}
+
+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 = `<a href="${aNode.href}"><img src="${aNode.href}"></a>`;
+ 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 = `<a href="${aNode.href}"><img src="${aNode.href}"></a>`;
+ 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 = `<video src="${aNode.href}" title="${aNode.href}" controls></video>`;
+ 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 = `<audio src="${aNode.href}" title="${aNode.href}" controls></audio>`;
+ 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
--- /dev/null
+++ b/juick-server/src/main/assets/logo.png
Binary files 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
--- /dev/null
+++ b/juick-server/src/main/assets/logo@2x.png
Binary files 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 `<div class="icon icon--${name}"><svg class="icon__cnt"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#${name}-icon"></use></svg></div>`;
+}
+
+/* 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')} <a href="#${msg.replyto}">/${msg.replyto}</a>`;
+ }
+ let photoDiv = (msg.attach == null) ? '' : `
+ <div class="msg-media"><a href="//i.juick.com/p/${msg.mid}-${msg.rid}.${msg.attach}">
+ <img src="//i.juick.com/photos-512/${msg.mid}-${msg.rid}.${msg.attach}"/></a>
+ </div>`;
+ let msgContHtml = `
+ <div class="msg-cont">
+ <div class="msg-header">
+ <a href="/${msg.user.uname}/">${msg.user.uname}</a>:
+ <div class="msg-avatar">
+ <a href="/${msg.user.uname}/"><img src="//i.juick.com/a/${msg.user.uid}.png" alt="${msg.user.uname}"/></a>
+ </div>
+ <div class="msg-ts">
+ <a href="/m/${msg.mid}#${msg.rid}" title="${msg.timestamp}GMT">${msg.timestamp}</a>
+ </div>
+ </div>
+ <div class="msg-txt">${killy.format(msg.body, msg.mid, false)}</div>${photoDiv}
+ <div class="msg-links">${msgNum} &middot; <a class="msg-reply-link" href="#">${i18n('message.reply')}</a></div>
+ <div class="msg-comment-target msg-comment-hidden"></div>
+ </div>`;
+
+ 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', `<p class="dialogtxt">${i18n('postForm.pleaseInputMessageText')}</p>`);
+ 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 = `
+ <form action="/comment" method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="mid" value="${mid}">
+ <input type="hidden" name="rid" value="${rid}">
+ <div class="msg-comment">
+ <div class="ta-wrapper">
+ <textarea name="body" rows="1" class="reply" placeholder="${i18n('comment.writeComment')}"></textarea>
+ <div class="attach-photo">${evilIcon('ei-camera')}</div>
+ </div>
+ <input type="submit" value="OK">
+ </div>
+ </form>`;
+ 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 = `
+ <div class="dialogshare">
+ ${i18n('shareDialog.linkToMessage')}: <div onclick="this.selectText()" class="dialogl">${hlink}</div>
+ ${i18n('shareDialog.messageNumber')}: <div onclick="this.selectText()" class="dialogl">${mlink}</div>
+ ${i18n('shareDialog.share')}:
+ <ul>
+ <li><a href="https://www.facebook.com/sharer/sharer.php?u=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-facebook')}</a></li>
+ <li><a href="https://twitter.com/intent/tweet?url=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-twitter')}</a></li>
+ <li><a href="https://vk.com/share.php?url=${hlinkenc}" onclick="return openSocialWindow(this)">${evilIcon('ei-sc-vk')}</a></li>
+ </ul>
+ </div>`;
+
+ 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(`<a href="//i.juick.com/p/${fname}"><img src="//i.juick.com/photos-1024/${fname}"/></a>`, true);
+ return false;
+ } else {
+ openDialog(`<a href="//i.juick.com/p/${fname}"><img src="//i.juick.com/p/${fname}"/></a>`, true);
+ return false;
+ }
+}
+
+function openPostDialog() {
+ let newmessageTemplate = `
+ <form id="newmessage" action="/post" method="post" enctype="multipart/form-data">
+ <textarea name="body" placeholder="${i18n('postForm.newMessage')}"></textarea>
+ <div>
+ <input class="img" name="img" placeholder="${i18n('postForm.imageLink')} (${i18n('postForm.imageFormats')})"/>
+ ${i18n('postForm.or')} <a href="#">${i18n('postForm.upload')}</a><br/>
+ <input id="tags_input" class="tags" name="tags" placeholder="${i18n('postForm.tags')}"/><br/>
+ <input type="submit" class="subm" value="${i18n('postForm.submit')}"/>
+ </div>
+ </form>
+ `;
+ return openDialog(newmessageTemplate);
+}
+
+function openDialog(html, image) {
+ var dialogHtml = `
+ <div id="dialogt">
+ <div id="dialogb"></div>
+ <div id="dialogw">
+ <div id="dialog_header">
+ <div id="dialogc">${evilIcon('ei-close')}</div>
+ </div>
+ ${html}
+ </div>
+ </div>`;
+ 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 = `
+ <div class="dialoglogin">
+ <p>${i18n('loginDialog.pleaseIntroduceYourself')}:</p>
+ <a href="mailto:juick@juick.com?subject=LOGIN" id="signemail">${evilIcon('ei-envelope')}${i18n('loginDialog.email')}</a>
+ <a href="/_fblogin" id="signfb">${evilIcon('ei-sc-facebook')}${i18n('loginDialog.facebook')}</a>
+ <a href="/_vklogin" id="signvk">${evilIcon('ei-sc-vk')}${i18n('loginDialog.vk')}</a>
+ <p>${i18n('loginDialog.registeredAlready')}</p>
+ <form action="/login" method="POST">
+ <input class="signinput" type="text" name="username" placeholder="${i18n('loginDialog.username')}"/><br/>
+ <input class="signinput" type="password" name="password" placeholder="${i18n('loginDialog.password')}"/><br/>
+ <input class="signsubmit" type="submit" value="OK"/>
+ </form>
+ </div>`;
+ 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', '<input type="submit" value="OK"/>');
+ 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 <http://www.gnu.org/licenses/>.
+ */
+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<String, String> 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<Void> 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<List<com.juick.Message>> 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<List<com.juick.Message>> 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<Message> 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<List<com.juick.Message>> 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<List<User>> 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<ExternalToken> 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<ExternalToken> 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<com.juick.Message> 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<Reaction> 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/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<TagStats> 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<User> doGetUsers(
@RequestParam(value = "uname", required = false) List<String> unames) {
List<com.juick.User> 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<User> 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<User> 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 <http://www.gnu.org/licenses/>.
- */
-
-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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+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<String> 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 <http://www.gnu.org/licenses/>.
+ */
+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<Tag> parseTags(String tagsStr) {
+ List<Tag> tags = new ArrayList<>();
+ if (tagsStr != null && !tagsStr.isEmpty()) {
+ Stream<String> 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<String> apps;
+
+ public List<String> getApps() {
+ return apps;
+ }
+
+ public void setApps(List<String> 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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+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 <http://www.gnu.org/licenses/>.
+ */
+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<Integer> 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 = "<meta name=\"robots\" content=\"noindex\"/>";
+ }
+ model.addAttribute("title", title);
+ model.addAttribute("headers", head);
+ model.addAttribute("visitor", visitor);
+ model.addAttribute("noindex", !(paramShow == null && paramBefore == 0));
+ List<com.juick.Message> msgs = messagesService.getMessages(visitor, mids);
+
+ if (!visitor.isAnonymous()) {
+ fillUserModel(model, visitor, visitor);
+ List<Integer> unread = messagesService.getUnread(visitor);
+ visitor.setUnreadCount(unread.size());
+ List<Integer> 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 += "&amp;show=" + paramShow;
+ }
+ if (paramSearch != null) {
+ nextpage += "&amp;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<Integer> 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 = "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"@" +
+ user.getName() + "\" href=\"//rss.juick.com/" + user.getName() + "/blog\"/>";
+ if (paramTag != null && tagService.getTagNoIndex(paramTag.TID)) {
+ head += "<meta name=\"robots\" content=\"noindex,nofollow\"/>";
+ } else if (before > 0 || paramShow != null) {
+ head += "<meta name=\"robots\" content=\"noindex\"/>";
+ }
+ 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<com.juick.Message> msgs = messagesService.getMessages(visitor, mids);
+
+ if (!visitor.isAnonymous()) {
+ List<Integer> unread = messagesService.getUnread(visitor);
+ visitor.setUnreadCount(unread.size());
+ List<Integer> 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 += "&amp;show=" + paramShow;
+ }
+ if (paramSearch != null) {
+ nextpage += "&amp;search=" + URLEncoder.encode(paramSearch, CharEncoding.UTF_8);
+ }
+ if (paramTag != null) {
+ nextpage += "&amp;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", "<meta name=\"robots\" content=\"noindex,nofollow\"/>");
+ 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", "<meta name=\"robots\" content=\"noindex\"/>");
+ 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", "<meta name=\"robots\" content=\"noindex\"/>");
+ 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", "<meta name=\"robots\" content=\"noindex\"/>");
+ 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<Integer> mids = messagesService.getTag(paramTag.TID, visitor.getUid(), before, (visitor.isAnonymous()) ? 40 : 20);
+ List<com.juick.Message> msgs = messagesService.getMessages(visitor, mids);
+ if (!visitor.isAnonymous()) {
+ List<Integer> unread = messagesService.getUnread(visitor);
+ visitor.setUnreadCount(unread.size());
+ List<Integer> 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 = "<meta name=\"robots\" content=\"noindex,nofollow\"/>";
+ } else if (before > 0 || mids.size() < 5) {
+ head = "<meta name=\"robots\" content=\"noindex\"/>";
+ }
+ 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<com.juick.Message> 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<com.juick.Message> 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<com.juick.Message> replies = messagesService.getReplies(visitor, msg.getMid());
+ // this should be after getReplies to mark thread as read
+ fillUserModel(model, user, visitor);
+ if (!visitor.isAnonymous()) {
+ List<Integer> 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 = "<link rel=\"alternate\" type=\"application/rss+xml\" title=\"@" + msg.getUser().getName() + "\" href=\"//rss.juick.com/" + msg.getUser().getName() + "/blog\"/>";
+ String pageUrl = "https://juick.com/" + msg.getUser().getName() + "/" + msg.getMid();
+ if (msg.Hidden) {
+ headers += "<meta name=\"robots\" content=\"noindex\"/>";
+ }
+ 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 += "<meta property=\"og:image\" content=\"" + msgImage + "\" />";
+ }
+ } else {
+ String msgImage ="https://i.juick.com/a/" + msg.getUser().getUid() + ".png";
+ headers += "<meta property=\"og:image\" content=\"" + msgImage + "\" />";
+ }
+ model.addAttribute("ogtype", "article");
+ String cardDescription = StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg));
+ headers += "<meta name=\"twitter:card\" content=\"" + cardType + "\" />\n" +
+ "<meta name=\"twitter:site\" content=\"@juick\" />\n" +
+ "<meta property=\"og:url\" content=\"" + pageUrl + "\" />\n" +
+ "<meta property=\"og:title\" content=\"" + msg.getUser().getName() + " at Juick\" />\n" +
+ "<meta property=\"og:description\" content=\"" + cardDescription + "\" />\n" +
+ "<meta name=\"Description\" content=\"" + cardDescription + "\" />\n";
+ String twitterName = crosspostService.getTwitterName(msg.getUser().getUid());
+ if (StringUtils.isNotEmpty(twitterName)) {
+ headers += "<meta name=\"twitter:creator\" content=\"@" + twitterName + "\" />\n";
+ }
+ if (msg.getTags().size() > 0) {
+ headers += "<meta name=\"Keywords\" content=\"" + msg.getTags().stream().map(Tag::getName)
+ .collect(Collectors.joining(", ")) + "\" />\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<Integer> 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<String> 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 <http://www.gnu.org/licenses/>.
+ */
+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<String, String> params = new LinkedMultiValueMap<>();
+ HttpEntity<MultiValueMap<String, String>> 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<CommandResult> 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<String, String> params = new LinkedMultiValueMap<>();
+ HttpEntity<MultiValueMap<String, String>> 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<CommandResult> 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<String, String> params = new LinkedMultiValueMap<>();
+ HttpEntity<MultiValueMap<String, String>> 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<CommandResult> 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 <http://www.gnu.org/licenses/>.
+ */
+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<String> 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<String> 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<String> 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 = "<p>Password has been changed.</p>";
+ 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 = "<p>Notification options has been updated</p>";
+ }
+ 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("<p>Your info is updated.</p><p><a href='/%s/'>Back to blog</a>.</p>", 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 = "<p>Deleted. <a href=\"/settings\">Back</a>.</p>";
+ } else {
+ result = "<p>Error</p>";
+ }
+ 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 = "<p>Authorization link has been sent to your email. Follow it to proceed.</p>" +
+ "<p><a href=\"/settings\">Back</a></p>";
+
+ } catch (MessagingException ex) {
+ logger.error("mail exception", ex);
+ throw new HttpBadRequestException();
+ }
+ }
+ }
+ break;
+ case "email-del":
+ if (emailService.deleteEmail(visitor.getUid(), request.getParameter("account"))) {
+ result = "<p>Deleted. <a href=\"/settings\">Back</a>.</p>";
+ } else {
+ result = "<p>An error occured while deleting.</p>";
+ }
+ break;
+ case "email-subscr":
+ if (emailService.setNotificationsEmail(visitor.getUid(), request.getParameter("account"))) {
+ result = String.format("<p>Saved! Will send notifications to <strong>%s</strong>." +
+ "</p><p><a href=\"/settings\">Back</a></p>", request.getParameter("account"));
+ } else {
+ result = "<p>Disabled.</p><p><a href=\"/settings\">Back</a></p>";
+ }
+ 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 = "<p><a href=\"/settings\">Back</a></p>";
+ break;
+ case "telegram-del":
+ telegramService.deleteTelegramUser(visitor.getUid());
+ result = "<p><a href=\"/settings\">Back</a></p>";
+ break;
+ case "facebook-disable":
+ crosspostService.disableFBCrosspost(visitor.getUid());
+ result = "<p><a href=\"/settings\">Back</a></p>";
+ break;
+ case "facebook-enable":
+ crosspostService.enableFBCrosspost(visitor.getUid());
+ result = "<p><a href=\"/settings\">Back</a></p>";
+ break;
+ case "vk-del":
+ crosspostService.deleteVKUser(visitor.getUid());
+ result = "<p><a href=\"/settings\">Back</a></p>";
+ 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 <http://www.gnu.org/licenses/>.
+ */
+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/api/SocialLogin.java b/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java
index dc7425e1..014a728d 100644
--- a/juick-server/src/main/java/com/juick/server/api/SocialLogin.java
+++ b/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java
@@ -14,38 +14,48 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-package com.juick.server.api;
+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.OAuth2AccessToken;
-import com.github.scribejava.core.model.OAuthRequest;
-import com.github.scribejava.core.model.Verb;
+import com.github.scribejava.core.model.*;
+import com.github.scribejava.core.oauth.OAuth10aService;
import com.github.scribejava.core.oauth.OAuth20Service;
-import com.juick.facebook.User;
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.vk.UsersResponse;
+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 org.springframework.web.util.UriComponentsBuilder;
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;
/**
*
@@ -60,8 +70,8 @@ public class SocialLogin {
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 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;
@@ -95,8 +105,10 @@ public class SocialLogin {
}
@GetMapping("/_fblogin")
- protected String doFacebookLogin(@RequestParam(required = false) String code,
- @RequestParam(required = false) String state) throws IOException, ExecutionException, InterruptedException {
+ 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);
@@ -110,7 +122,6 @@ public class SocialLogin {
}
String redirectUrl = crosspostService.verifyFacebookState(state);
-
if (StringUtils.isEmpty(redirectUrl)) {
logger.error("state is missing");
throw new HttpBadRequestException();
@@ -143,9 +154,10 @@ public class SocialLogin {
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();
+ 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())) {
@@ -163,7 +175,7 @@ public class SocialLogin {
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 {
@@ -201,8 +213,8 @@ public class SocialLogin {
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);
+ 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);
@@ -212,13 +224,17 @@ public class SocialLogin {
}
}
}
- }*/
+ }
@GetMapping("/_vklogin")
- protected String doVKLogin(@RequestParam(required = false) String code,
- @RequestParam String state) throws IOException, ExecutionException, InterruptedException {
+ 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)) {
- String vkstate = UUID.randomUUID().toString();
- crosspostService.addVKState(vkstate, state);
+ vkstate = UUID.randomUUID().toString();
+ Cookie c = new Cookie("vkstate", vkstate);
+ response.addCookie(c);
OAuth20Service vkAuthService = vkBuilder
.apiSecret(VK_SECRET)
.scope("friends,wall,offline")
@@ -228,10 +244,12 @@ public class SocialLogin {
return "redirect:" + vkAuthService.getAuthorizationUrl();
}
- String redirectUrl = crosspostService.verifyVKState(state);
- if (StringUtils.isBlank(redirectUrl)) {
- logger.error("state is missing");
+ 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
@@ -244,7 +262,7 @@ public class SocialLogin {
vkService.signRequest(token, meRequest);
String graph = vkService.execute(meRequest).getBody();
- com.juick.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0);
+ 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();
@@ -256,9 +274,10 @@ public class SocialLogin {
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();
+ 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)) {
@@ -268,7 +287,7 @@ public class SocialLogin {
return "redirect:/signup?type=vk&hash=" + loginhash;
}
}
- /*
+
@GetMapping("/_tglogin")
public String doDurovLogin(HttpServletRequest request,
@RequestParam Map<String, String> params,
@@ -298,5 +317,5 @@ public class SocialLogin {
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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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 <http://www.gnu.org/licenses/>.
+ */
+
+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<User> users;
+
+ @JsonProperty("response")
+ public List<User> getUsers() {
+ return users;
+ }
+
+ public void setUsers(List<User> 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String, Filter> getFilters() {
+ Map<String, Filter> 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String, Object> 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<String> 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String, Object> 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<String> 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 <http://www.gnu.org/licenses/>.
+ */
+
+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<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+ return ((List<Tag>) input).stream().map(Tag::getName).collect(Collectors.toList());
+ }
+
+ @Override
+ public List<String> 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<String, Object> args, PebbleTemplate self, EvaluationContext context, int lineNumber) {
+ if (input instanceof Instant) {
+ return Date.from((Instant)input);
+ }
+ throw new IllegalArgumentException("invalid input");
+ }
+
+ @Override
+ public List<String> 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<String> 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<String, Object> cached;
+ long cacheUpdated;
+
+ @SuppressWarnings("unchecked")
+ public Map<String, Object> 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<String, Object>) 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<String, Object> data = sapeConnection.getData();
+
+ if (data.containsKey("__sape_delimiter__")) {
+ linkDelimiter = (String) data.get("__sape_delimiter__");
+ }
+
+ if (data.containsKey(requestUri)) {
+ pageLinks = new ArrayList<>(((Map<Object, String>) 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<Object, String>) newUrl).values());
+ } else {
+ pageLinks = new ArrayList<>(Collections.singletonList((String) newUrl));
+ }
+ }
+ }
+
+ this.showCode = showCode;
+ }
+ private String linkDelimiter = ".";
+ private List<String> 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<String> 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, "<sape_noindex>");
+ s.append("</sape_noindex>");
+ }
+
+ 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
+Subproject ce103cd9a2a8a200c6ebb3b41525e7c8f639d98
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=<a href="{0}" class="a-login">Login</a> to post messages and comments
+message.sendLoginToXmpp=Send <b>LOGIN</b> to <a href="xmpp:juick@juick.com?message;body=LOGIN">juick@juick.com</a>
+
+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=Чтобы добавлять сообщения и комментарии, <a href="{0}" class="a-login">представьтесь</a>
+message.sendLoginToXmpp=Отправьте <b>LOGIN</b> на <a href="xmpp:juick@juick.com?message;body=LOGIN">juick@juick.com</a>
+
+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
--- /dev/null
+++ b/juick-server/src/main/resources/static/favicon.png
Binary files 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
--- /dev/null
+++ b/juick-server/src/main/resources/static/logo.png
Binary files 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
--- /dev/null
+++ b/juick-server/src/main/resources/static/tagscloud.png
Binary files 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 @@
+<!DOCTYPE html>
+<html prefix="og: http://ogp.me/ns#">
+<head id="org" itemprop="publisher" itemscope="" itemtype="http://schema.org/Organization">
+ <meta charset="utf-8"/>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+ <script type="text/javascript" src="{{ beans.webApp.scriptsUrl }}"></script>
+ <link rel="stylesheet" type="text/css" href="{{ beans.webApp.styleUrl }}"/>
+ {% block headers %}
+ {{ headers | default('') | raw }}
+ {% endblock %}
+ <title itemprop="name">{{ title | default('Juick') }}</title>
+ <meta itemprop="url" content="https://juick.com/" />
+ <meta property="og:type" content="{{ ogtype | default('website') }}" />
+ <meta property="fb:app_id" content="130568668304" />
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
+ <meta name="msapplication-config" content="//i.juick.com/browserconfig.xml"/>
+ <meta name="msapplication-TileColor" content="#ffffff"/>
+ <meta name="msapplication-TileImage" content="//i.juick.com/ms-icon-144x144.png"/>
+ <meta name="theme-color" content="#ffffff"/>
+ <meta name="apple-mobile-web-app-capable" content="yes" />
+ <link rel="apple-touch-icon" sizes="57x57" href="//i.juick.com/apple-icon-57x57.png"/>
+ <link rel="apple-touch-icon" sizes="60x60" href="//i.juick.com/apple-icon-60x60.png"/>
+ <link rel="apple-touch-icon" sizes="72x72" href="//i.juick.com/apple-icon-72x72.png"/>
+ <link rel="apple-touch-icon" sizes="76x76" href="//i.juick.com/apple-icon-76x76.png"/>
+ <link rel="apple-touch-icon" sizes="114x114" href="//i.juick.com/apple-icon-114x114.png"/>
+ <link rel="apple-touch-icon" sizes="120x120" href="//i.juick.com/apple-icon-120x120.png"/>
+ <link rel="apple-touch-icon" sizes="144x144" href="//i.juick.com/apple-icon-144x144.png"/>
+ <link rel="apple-touch-icon" sizes="152x152" href="//i.juick.com/apple-icon-152x152.png"/>
+ <link rel="apple-touch-icon" sizes="180x180" href="//i.juick.com/apple-icon-180x180.png"/>
+ <link itemprop="logo" href="http://juick.com/#juick-logo" itemtype="http://schema.org/ImageObject" />
+ <link rel="icon" type="image/png" sizes="32x32" href="//i.juick.com/favicon-32x32.png"/>
+ <link rel="icon" type="image/png" sizes="96x96" href="//i.juick.com/favicon-96x96.png"/>
+ <link rel="icon" type="image/png" sizes="16x16" href="//i.juick.com/favicon-16x16.png"/>
+ <link rel="manifest" href="//i.juick.com/manifest.json"/>
+ <script type="application/ld+json">
+{
+ "@context": "http://schema.org",
+ "@id": "http://juick.com/#juick-logo",
+ "@type": "ImageObject",
+ "url": "http://juick.com/logo.png",
+ "width": 110,
+ "height": 36
+}
+ </script>
+</head>
+<body id="body" {% if visitor.uid > 0 %}data-hash="{{visitor.authHash}}"{% endif %}>
+{% block body %}
+{% endblock %}
+</body>
+</html>
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" %}
+<div id="wrapper">
+ <section id="content"
+ {% if msg | default('') is not empty %}data-mid="{{ msg.mid }}"{% endif %}>
+ {% block content %}
+ {% endblock %}
+ </section>
+ <aside id="column">
+ {% block column %}
+ {% endblock %}
+ </aside>
+</div>
+{% 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 %}
+<div id="wrapper">
+ <section id="minimal_content">
+ {% block content %}
+ {% endblock %}
+ </section>
+</div>
+{% 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 %}
+ <article>
+ <h1>Страница не найдена</h1>
+ <p>Сожалеем, но страницу с этим адресом удалил её автор, либо её никогда не существовало.</p>
+ </article>
+{% 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 %}
+<!--noindex-->
+{% endif %}
+{% if paramTag | default('') is not empty %}
+<p class="page"><a href="/tag/{{ paramTag.name | urlencode }}">← {{ i18n("messages","blog.allPostsWithTag") }} <b>{{ paramTag.name | escape }}</b></a></p>
+{% endif %}
+<div itemscope="" itemtype="http://schema.org/Blog">
+ <meta itemprop="url" content="{{ pageUrl }}"/>
+{% for msg in msgs %}
+{% include "views/partial/message" %}
+{% endfor %}
+</div>
+{% if nextpage | default('') is not empty %}
+<p class="page"><a href="{{ nextpage | raw }}" rel="prev">{{ i18n("messages","messages.next") }} →</a></p>
+{% endif %}
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/usercolumn" %}
+{% if noindex %}
+<!--/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 %}
+<p>
+ {{ tags(user.name, tags) }}
+</p>
+{% 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 %}
+<article>
+ {{ content | raw }}
+</article>
+{% 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 %}
+<!--noindex-->
+{% endif %}
+{% for msg in msgs %}
+{% include "views/partial/message" %}
+{% endfor %}
+{% if nextpage | default('') is not empty %}
+<p class="page"><a href="{{ nextpage | raw }}" rel="prev">{{ i18n("messages","messages.next") }} →</a></p>
+{% 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 %}
+<!--/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 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Juick</title>
+ <script type="text/javascript" src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js" defer="defer"></script>
+ <style>
+ * { margin: 0; padding: 0; }
+ html { font-family: sans-serif; font-size: 12pt; }
+ html { background: #f8f8f8; }
+ body { margin: 100px auto 0 auto; width: 1000px; }
+ a { color: #069; }
+ ul { float: left; width: 700px; height: 350px; list-style-type: none; background: url(/tagscloud.png) no-repeat; position: relative; box-shadow: 0 0 3px rgba(0,0,0,.16); }
+ ul a { position: absolute; display: block; text-indent: 100%; white-space: nowrap; overflow: hidden; }
+
+ #bottom1 { position: absolute; left: 0px; bottom: 10px; width: 100%; text-align: center; color: #555; }
+ #bottom2 { position: absolute; left: 0px; bottom: -50px; width: 100%; padding-bottom: 20px; text-align: center; font-size: small; color: #777; }
+
+ #signup,#signin { margin-left: 730px; width: 250px; }
+ #signup { padding-top: 25px; }
+ #signup>div { width: 100%; margin: 15px 0; }
+ #signup>div>a { display: block; width: 100%; height: 32px; line-height: 32px; text-indent: 37px; text-decoration: none; overflow: hidden; }
+
+ #facebook a { color: #FFF; background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAADNQTFRFO1edX3ewl6bLnKrOoK3QrrrYvMXe2N7r3OLu3+Tv5urz7O/29vf6+Pn7+vv9/Pz9////ykQjsQAAAEZJREFUOMtjYBgFuAATO68ADxdOaUYuATDAqYBbAL8CFgECCjiBcqz4XMiPz3oQEKCtAgEkwEdIAQchBWyEFDAPkDdHsAIAhZkIwz/VK/UAAAAASUVORK5CYII=") no-repeat #3A569C; }
+ #vk a { color: #FFF; background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAHJQTFRFbY+zbo+zbpCzb5C0cpO1c5O2dZW3dpa4e5m6gJ29gZ69lq/In7bNo7jPrcDUs8XXvs3dv87dy9fkztnlz9rm0Nrm093o1N7o1+Dq3OTt3ubu4Ofv5Orw7fH27vL28PP38vX49Pb5+vv8+/z9/Pz9////2jSYlQAAAG5JREFUOMvtkEcOgDAMBE3vvXdIyP+/iMMRKfYHmMtcRtE6AD8f1Is8pyKgAs0RGYO2HSWqMQaoBHVRgYsS3AsrtyFlrqgdJlCLb95gxQO6IkZCqL+KCjz0TQU5ejOf2a3aJXPF7BOB2PvMhp8PDzGRFgEe7xvEAAAAAElFTkSuQmCC") no-repeat #6d8fb3; }
+ #xmpp>a { color: #333; background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IArs4c6QAAAt9QTFRFBj5rCUFoFz5mDEFuDUNqGUJkGUNgAEprEkVtGkRhHURnHEZjAE+BIElmEEx/HEx0PUdTHE5wMEtfO0laJk5sFlN6Nk1cKFBuIlJ1R0pYRExTGF2KMVh1OFhxDGSQT1deNlx6TVhkIGKPUlphKWOFU1tiOmB+Vl1kmlAuNmaDQGpaIG6ba2Q2SGeBN2uUNW+LF3mGRmyLV3BAL3xWgmVJ2VAa2lEb0VYX11UafWlmam9mam5xy1km3lUea29y2VccvF8obHBz21gd4VcXS3ebPoVG1lwl5FkaU3iYYHaH2F0ejHFIx2Iv4V0aP4o+02As218g4l4bPYtFX351KZFaymU43mIrVYoz5mEfT4w0xGsrSo82eXqDw2s+z2k1OZVAT4SoPZU5RZM4NJVYbYc8VoSiWY43WItbuXBK52gYOI20TpM6YYSYfX98T5Q7foB9aYSZSZc8Ro21cYxH7GwdT46e0HFGeopO2HA8V5ZEf4s8528l1HM7UJs5UI+xXoyrWZhGY4ylyHdMwXhXQZS7XpdNU5tH4XJBYJZZcIuhbpJk0HdPU588kohqOZ2pVZS30XpX6XcxSaFrz4FIUqVWWqVCYZm23YBGxYRrZZ2QXpy/5YFC7IA+6oBRb6ZUcKZn44ZSgKJu54hHmJqXbbBNfKh3cq5nYqrMpJyVdrBjx5WCo56diamAjaWdyZeE8JFV8ZJWnqOlaK/Sd7Zh7pVdgbN6x52N8JdfwqCU8ZhgZLbXb7a5grtTmK+UxKKWtqahq6qhd7bHqauop6yvqqyp755pbbzRrK6rwamgib53qq+xra+sobSg86Jza8PJsLKujMVctrGwqLapv7Crr7Wr8ad1srSxj8R9uLOys7WyubSztLazkcZ/sre6tbe0r7m0tri1t7m2vri3uLq3v7m4ubu4nc9mpNBvos2UptJxtNWXtteFuNmbuNyQud2Rut6SwN+aweCbwuGdw+KeweKkF4OfHQAAAadJREFUOMtjeEoAMAwfBQ9vYJO68RCu4BYDq1bXmr2XHoMlnlzet2qGpTgX8y2EFWwensEdrW2FbUAlJ2zDKsqaYwo6eZHcIGzoor/s5IdPSfefPt3nf3Xn3HZpPU8xJAXWIvU88VOffcm78vTptrijJe4OfAmy1kgKzjMlaIfUPvvQcObp03U120ucTZQaGc8je1NZrd8g8cL7eUufPp0wfX2mvVydripKONzkTG1JXvL6TuXTR/Zb5gR6F+Vw30QNqNUss/pqn3/MvrTD7Wy1V9Y0jtXoIZnLv2nSnrfzF3bP3hDRu1wwHzOo1VWOTXn5yi/q7MTJuxTUscTFQyHNc5vfbU1btOCgjNBDbJH1UF5i9/XP6VVrJRUfYo/Nx3YcpS/euOo4Pn6MqeDx44f3LoY2SYnOXBlUfPPug8cwRQxQ6Qf39sda+HqZaQiwGydFlx+4eQ+qAqrg4b3bPqbmNhkrNm5cbGUUHply7d6Dh8hWPH744Oa186eOHDl06NDh46fP33zwEMUEiCMe3L13GwjuPbj38PHTx9jT5OPHj5G9MFQyDgA8riWAv9eLFAAAAABJRU5ErkJggg==") no-repeat #BBB; }
+ #xmppinfo { background: #FFF; padding: 10px; display: none; }
+
+ #signin { text-align: center; font-size: small; }
+ #signinform { background: #FFF; padding: 10px 15px; margin-top: 15px; display: none; }
+ input.txt { width: 212px; border: 1px solid #CCC; margin: 3px 0; padding: 3px; }
+ input.submit { width: 70px; border: 1px solid #CCC; margin: 3px 0; padding: 3px; }
+ </style>
+ <link rel="icon" href="//i.juick.com/favicon.png"/>
+ </head>
+
+<body>
+
+<ul id="tags">
+ <li><a href="/tag/juick" style="left: 359px; top: 120px; width: 311px; height: 99px">juick</a></li>
+ <li><a href="/tag/linux" style="left: 201px; top: 100px; width: 98px; height: 35px">linux</a></li>
+ <li><a href="/tag/android" style="left: 314px; top: 42px; width: 45px; height: 158px">android</a></li>
+ <li><a href="/tag/работа" style="left: 149px; top: 138px; width: 165px; height: 41px">работа</a></li>
+ <li><a href="/tag/music" style="left: 119px; top: 249px; width: 124px; height: 32px">music</a></li>
+ <li><a href="/tag/windows" style="left: 448px; top: 234px; width: 186px; height: 32px">windows</a></li>
+ <li><a href="/tag/google" style="left: 244px; top: 252px; width: 134px; height: 41px">google</a></li>
+ <li><a href="/tag/кино" style="left: 68px; top: 83px; width: 97px; height: 28px">кино</a></li>
+ <li><a href="/tag/фото" style="left: 400px; top: 266px; width: 101px; height: 29px">фото</a></li>
+ <li><a href="/tag/жизнь" style="left: 554px; top: 266px; width: 125px; height: 27px">жизнь</a></li>
+ <li><a href="/tag/еда" style="left: 46px; top: 196px; width: 71px; height: 32px">еда</a></li>
+ <li><a href="/tag/музыка" style="left: 61px; top: 111px; width: 139px; height: 27px">музыка</a></li>
+ <li><a href="/tag/прекрасное" style="left: 152px; top: 200px; width: 205px; height: 32px">прекрасное</a></li>
+ <li><a href="/tag/книги" style="left: 148px; top: 293px; width: 103px; height: 25px">книги</a></li>
+ <li><a href="/tag/цитата" style="left: 325px; top: 301px; width: 126px; height: 27px">цитата</a></li> <li><a href="/tag/games" style="left: 117px; top: 142px; width: 30px; height: 104px">games</a></li>
+ <li><a href="/tag/ubuntu" style="left: 503px; top: 2px; width: 28px; height: 102px">ubuntu</a></li>
+ <li><a href="/tag/котэ" style="left: 534px; top: 27px; width: 76px; height: 28px">котэ</a></li>
+ <li><a href="/tag/ВНЕЗАПНО" style="left: 501px; top: 293px; width: 146px; height: 23px">ВНЕЗАПНО</a></li>
+ <li><a href="/tag/юмор" style="left: 73px; top: 53px; width: 84px; height: 28px">юмор</a></li>
+ <li><a href="/tag/мысли" style="left: 202px; top: 179px; width: 102px; height: 21px">мысли</a></li>
+ <li><a href="/tag/pic" style="left: 400px; top: 78px; width: 33px; height: 38px">pic</a></li>
+ <li><a href="/tag/политота" style="left: 531px; top: 60px; width: 130px; height: 24px">политота</a></li>
+ <li><a href="/tag/WOT" style="left: 159px; top: 63px; width: 48px; height: 20px">WOT</a></li>
+ <li><a href="/tag/fail" style="left: 8px; top: 170px; width: 34px; height: 27px">fail</a></li>
+ <li><a href="/tag/погода" style="left: 670px; top: 126px; width: 24px; height: 93px">погода</a></li>
+ <li><a href="/tag/apple" style="left: 42px; top: 167px; width: 64px; height: 29px">apple</a></li>
+ <li><a href="/tag/jabber" style="left: 436px; top: 43px; width: 25px; height: 75px">jabber</a></li>
+ <li><a href="/tag/тян" style="left: 532px; top: 94px; width: 47px; height: 21px">тян</a></li>
+ <li><a href="/tag/work" style="left: 359px; top: 55px; width: 58px; height: 23px">work</a></li>
+ <li><a href="/tag/Python" style="left: 240px; top: 63px; width: 74px; height: 23px">Python</a></li>
+ <li><a href="/tag/Видео" style="left: 266px; top: 232px; width: 76px; height: 20px">Видео</a></li>
+ <li><a href="/tag/авто" style="left: 359px; top: 30px; width: 58px; height: 24px">авто</a></li>
+ <li><a href="/tag/Anime" style="left: 360px; top: 328px; width: 66px; height: 21px">Anime</a></li>
+ <li><a href="/tag/игры" style="left: 378px; top: 242px; width: 22px; height: 58px">игры</a></li>
+ <li><a href="/tag/вело" style="left: 176px; top: 9px; width: 18px; height: 54px">вело</a></li>
+ <li><a href="/tag/web" style="left: 661px; top: 219px; width: 22px; height: 47px">web</a></li>
+ <li><a href="/tag/YouTube" style="left: 498px; top: 316px; width: 81px; height: 24px">YouTube</a></li>
+ <li><a href="/tag/Вопрос" style="left: 208px; top: 18px; width: 22px; height: 72px">Вопрос</a></li>
+ <li><a href="/tag/железо" style="left: 159px; top: 318px; width: 75px; height: 16px">железо</a></li>
+ <li><a href="/tag/Microsoft" style="left: 20px; top: 146px; width: 86px; height: 21px">Microsoft</a></li>
+ <li><a href="/tag/video" style="left: 616px; top: 101px; width: 51px; height: 19px">video</a></li>
+ <li><a href="/tag/Россия" style="left: 32px; top: 242px; width: 68px; height: 16px">Россия</a></li>
+ <li><a href="/tag/java" style="left: 409px; top: 226px; width: 39px; height: 22px">java</a></li>
+ <li><a href="/tag/новости" style="left: 39px; top: 67px; width: 21px; height: 79px">новости</a></li>
+ <li><a href="/tag/интернет" style="left: 100px; top: 233px; width: 17px; height: 85px">интернет</a></li>
+ <li><a href="/tag/steam" style="left: 14px; top: 228px; width: 52px; height: 13px">steam</a></li>
+ <li><a href="/tag/слова" style="left: 501px; top: 272px; width: 51px; height: 18px">слова</a></li>
+ <li><a href="/tag/почта" style="left: 477px; top: 27px; width: 17px; height: 56px">почта</a></li>
+ <li><a href="/tag/help" style="left: 123px; top: 281px; width: 21px; height: 35px">help</a></li>
+ <li><a href="/tag/skype" style="left: 110px; top: 320px; width: 49px; height: 20px">skype</a></li>
+ <li><a href="/tag/debian" style="left: 461px; top: 47px; width: 16px; height: 51px">debian</a></li>
+ <li><a href="/tag/win" style="left: 505px; top: 104px; width: 27px; height: 16px">win</a></li>
+ <li><a href="/tag/Религия" style="left: 33px; top: 281px; width: 67px; height: 17px">Религия</a></li>
+ <li><a href="/tag/soft" style="left: 286px; top: 86px; width: 28px; height: 14px">soft</a></li>
+ <li><a href="/tag/Политика" style="left: 144px; top: 281px; width: 75px; height: 12px">Политика</a></li>
+ <li><a href="/tag/сны" style="left: 426px; top: 328px; width: 33px; height: 13px">сны</a></li>
+ <li><a href="/tag/Питер" style="left: 146px; top: 233px; width: 50px; height: 16px">Питер</a></li>
+ <li><a href="/tag/bash" style="left: 451px; top: 311px; width: 38px; height: 16px">bash</a></li>
+ <li><a href="/tag/code" style="left: 279px; top: 310px; width: 39px; height: 16px">code</a></li>
+ <li><a href="/tag/yandex" style="left: 19px; top: 263px; width: 56px; height: 18px">yandex</a></li>
+ <li><a href="/tag/firefox" style="left: 452px; top: 295px; width: 48px; height: 16px">firefox</a></li>
+ <li><a href="/tag/hardware" style="left: 230px; top: 40px; width: 67px; height: 18px">hardware</a></li>
+ <li><a href="/tag/git" style="left: 78px; top: 258px; width: 20px; height: 19px">git</a></li>
+ <li><a href="/tag/dev" style="left: 165px; top: 88px; width: 31px; height: 19px">dev</a></li>
+ <li><a href="/tag/mobile" style="left: 421px; top: 24px; width: 15px; height: 47px">mobile</a></li>
+ <li><a href="/tag/люди" style="left: 151px; top: 184px; width: 43px; height: 15px">люди</a></li>
+ <li><a href="/tag/php" style="left: 149px; top: 24px; width: 27px; height: 18px">php</a></li>
+ <li><a href="/tag/haskell" style="left: 271px; top: 293px; width: 48px; height: 16px">haskell</a></li>
+ <li><a href="/tag/стихи" style="left: 135px; top: 42px; width: 41px; height: 11px">стихи</a></li>
+ <li><a href="/tag/photo" style="left: 639px; top: 219px; width: 20px; height: 39px">photo</a></li>
+ <li><a href="/tag/чай" style="left: 448px; top: 220px; width: 27px; height: 14px">чай</a></li>
+ <li><a href="/tag/Опрос" style="left: 297px; top: 22px; width: 14px; height: 41px">Опрос</a></li>
+ <li><a href="/tag/Chrome" style="left: 311px; top: 25px; width: 48px; height: 17px">Chrome</a></li>
+ <li><a href="/tag/life" style="left: 255px; top: 311px; width: 23px; height: 16px">life</a></li>
+ <li><a href="/tag/opera" style="left: 226px; top: 232px; width: 38px; height: 14px">opera</a></li>
+ <li><a href="/tag/programming" style="left: 234px; top: 327px; width: 81px; height: 14px">programming</a></li>
+ <li><a href="/tag/дети" style="left: 15px; top: 197px; width: 31px; height: 13px">дети</a></li>
+ <li><a href="/tag/сериалы" style="left: 575px; top: 219px; width: 61px; height: 13px">сериалы</a></li>
+ <li><a href="/tag/учеба" style="left: 616px; top: 84px; width: 43px; height: 17px">учеба</a></li>
+ </ul>
+
+<div id="bottom1">juick.com &copy; 2008-2018 &nbsp; <a href="/help/ru/contacts" rel="nofollow">Контакты</a> &middot; <a href="/help/" rel="nofollow">Помощь</a></div>
+
+<div id="signup">
+ {{ i18n("messages","label.register") }}:
+ <div id="facebook"><a href="/_fblogin" rel="nofollow">Facebook</a></div>
+ <div id="vk"><a href="/_vklogin" rel="nofollow">ВКонтакте</a></div>
+ <div id="tg">
+ <script async src="https://telegram.org/js/telegram-widget.js?3"
+ data-telegram-login="Juick_bot" data-size="medium" data-radius="0"
+ data-auth-url="https://juick.com/_tglogin" data-request-access="write"></script>
+ </div>
+ </div>
+<div id="signin">
+ <a href="#" onclick="$('#signinform').toggle(); $('#nickinput').focus(); return false">
+ {{ i18n("messages","question.areRegistered") }}
+ </a>
+ <div id="signinform"><form action="/login" method="POST">
+ <input class="txt" type="text" name="username" placeholder='{{ i18n("messages","label.username") }}' id="nickinput"/>
+ <input class="txt" type="password" name="password" placeholder='{{ i18n("messages","label.password") }}'/>
+ <input class="submit" type="submit" value="OK"/>
+ </form></div>
+ </div>
+
+</body>
+</html>
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 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <title>Blank window</title>
+</head>
+<body>
+ <script type="text/javascript">
+ window.opener.postMessage("{{ hash }}", "*");
+ window.close();
+ </script>
+</body>
+</html>
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 %}
+<a href="/{{ uname }}/?tag={{ tag | urlencode }}">{{ tag | raw }}</a>
+{% 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 @@
+<div id="footer">
+ <div id="footer-right"> &middot;
+ <a href="/help/contacts" rel="nofollow">{{ i18n("messages","link.contacts") }}</a> &middot;
+ <a href="/help/tos" rel="nofollow">{{ i18n("messages","link.tos") }}</a>
+ </div>
+ <div id="footer-social">
+ <a href="https://twitter.com/Juick" rel="nofollow"><i data-icon="ei-sc-twitter" data-size="m"></i></a>
+ <a href="https://vk.com/juick" rel="nofollow"><i data-icon="ei-sc-vk" data-size="m"></i></a>
+ <a href="https://www.facebook.com/JuickCom" rel="nofollow"><i data-icon="ei-sc-facebook" data-size="m"></i></a>
+ </div>
+ <div id="footer-left">juick.com &copy; 2008-2018
+ {% if links | default ('') is not empty %}
+ <br/>{{ i18n("messages","label.sponsors") }}: {{ links | raw }}
+ {% endif %}
+ </div>
+</div>
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 @@
+<ul class="toolbar">
+ <li>
+ <a href="/" title="Top">
+ <i data-icon="ei-heart" data-size="s"></i>Top
+ </a>
+ </li>
+ <li>
+ <a href="/?show=all" title="{{ i18n("messages","link.allMessages") }}">
+ <i data-icon="ei-search" data-size="s"></i>{{ i18n("messages","link.allMessages") }}
+ </a>
+ </li>
+ <li>
+ <a href="/?show=photos" title="{{ i18n("messages","link.withPhotos") }}">
+ <i data-icon="ei-camera" data-size="s"></i>{{ i18n("messages","link.withPhotos") }}
+ </a>
+ </li>
+</ul>
+<div class="tags">
+ <h4>{{ i18n("messages","link.trends") }}</h4>
+ {% include "views/partial/tags" %}
+ {% if showAdv | default(false) %}
+ <h4>Наши друзья</h4>
+ <a href="https://ru.wix.com/">конструктор сайтов</a>
+ {% endif %}
+</div> \ 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 @@
+<article data-mid="{{ msg.mid }}" itemprop="blogPost" itemscope="" itemtype="http://schema.org/BlogPosting" itemref="org">
+ <header class="h">
+ <span itemprop="author" itemscope="" itemtype="http://schema.org/Person">
+ <a href="/{{ msg.user.name }}/" itemprop="url" rel="author"><span itemprop="name">{{ msg.user.name }}</span></a>
+ </span>
+ <div class="msg-avatar"><a href="/{{ msg.user.name }}/">
+ <img src="//i.juick.com/a/{{ msg.user.uid }}.png" alt="{{ msg.user.name }}"/></a>
+ </div>
+ <div class="msg-ts">
+ <a href="/{{ msg.user.name }}/{{ msg.mid }}">
+ <time itemprop="datePublished dateModified" itemtype="http://schema.org/Date" datetime="{{ msg.timestamp | timestamp | date('yyyy-MM-dd HH:mm:ss') }}Z"
+ title="{{ msg.timestamp | timestamp | date('yyyy-MM-dd HH:mm:ss') }} GMT">
+ {{ msg.timestamp | prettyTime }}
+ </time>
+ </a>
+ </div>
+ <div class="msg-tags" itemprop="headline">
+ {{ tags(msg.user.name, msg.tags | tagsList) }}
+ </div>
+ </header>
+ <p itemprop="description">{{ msg | formatMessage }}</p>
+ {% if msg.AttachmentType is not empty %}
+ <p class="ir"><a href="//i.juick.com/p/{{ msg.mid }}.{{ msg.AttachmentType }}" data-fname="{{ msg.mid }}.{{ msg.AttachmentType }}">
+ <img itemprop="image" src="//i.juick.com/photos-512/{{ msg.mid }}.{{ msg.AttachmentType }}" alt=""/></a>
+ </p>
+ {% endif %}
+ <nav class="l">
+ {% if visitor.uid == msg.user.uid %}
+ <a href="/{{ msg.mid }}" class="a-like msg-button">
+ <span class="msg-button-icon">
+ <i data-icon="ei-heart" data-size="s"></i>
+ {% if msg.likes > 0 %}&nbsp;{{ msg.likes }}{% endif %}
+ </span>
+ <span>&nbsp;{{ i18n("messages","message.recommend") }}</span>
+ </a>
+ {% elseif visitor.uid > 0 %}
+ <a href="/post?body=!+%23{{ msg.mid }}" class="a-like msg-button">
+ <span class="msg-button-icon">
+ <i data-icon="ei-heart" data-size="s"></i>
+ {% if msg.likes > 0 %}&nbsp;{{ msg.likes }}{% endif %}
+ </span>
+ <span>&nbsp;{{ i18n("messages","message.recommend") }}</span>
+ </a>
+ {% else %}
+ <a href="/login" class="a-login msg-button">
+ <span class="msg-button-icon">
+ <i data-icon="ei-heart" data-size="s"></i>
+ {% if msg.likes > 0 %}&nbsp;{{ msg.likes }}{% endif %}
+ </span>
+ <span>&nbsp;{{ i18n("messages","message.recommend") }}</span>
+ </a>
+ {% endif %}
+ {% if (not msg.ReadOnly) or (visitor.uid == msg.user.uid) %}
+ <a href="/{{ msg.mid }}" class="a-comment msg-button">
+ <span class="msg-button-icon">
+ <i data-icon="ei-comment" data-size="s"></i>
+ {% if msg.Replies > 0 %}&nbsp;
+ {% if msg.unread %}
+ <span class="badge">{{ msg.Replies }}</span>
+ {% else %}
+ {{ msg.Replies }}
+ {% endif %}
+ {% endif %}
+ </span>
+ <span>&nbsp;{{ i18n("messages","message.comment") }}</span>
+ </a>
+ <a href="#" class="msg-menu msg-button">
+ <i data-icon="ei-link" data-size="s"></i>
+ <span>&nbsp;{{ i18n("messages","message.share") }}</span>
+ </a>
+ {% endif %}
+ {% if msg.FriendsOnly %}
+ <a href="#" class="a-privacy">Открыть доступ</a>
+ {% endif %}
+ </nav>
+</article> \ 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 @@
+<header>
+ <div id="header_wrapper">
+ {% if visitor.uid > 0 %}
+ <div id="ctitle">
+ <a href="/{{ visitor.name }}">
+ <img src="//i.juick.com/a/{{ visitor.uid }}.png" alt=""/>{{ visitor.name }}
+ </a>
+ </div>
+ {% else %}
+ <div id="logo"><a href="/{% if visitor.uid > 0 %}?show=my{% endif %}">Juick</a></div>
+ {% endif %}
+ <div id="search">
+ <form action="/">
+ <input name="search" class="text"
+ placeholder="{{ i18n('messages','label.search') }}" value="{{ search | default('') }}"/>
+ </form>
+ </div>
+ <nav id="global">
+ <ul>
+ {% if visitor.uid > 0 %}
+ <li><a href="/?show=discuss"><i data-icon="ei-comment" data-size="s"></i>{{ i18n("messages","link.discuss") }}{% if visitor.unreadCount > 0 %}<span class="badge">{{ visitor.unreadCount }}</span>{% endif %}</a></li>
+ {% else %}
+ <li><a href="/?show=photos" rel="nofollow"><i data-icon="ei-camera" data-size="s"></i>{{ i18n("messages","link.withPhotos") }}</a></li>
+ {% endif %}
+ <li><a href="/?show=all" rel="nofollow"><i data-icon="ei-search" data-size="s"></i>{{ i18n("messages","link.allMessages") }}</a></li>
+ {% if visitor.uid > 0 %}
+ <li><a id="post" href="/post">
+ <i data-icon="ei-pencil" data-size="s"></i>{{ i18n("messages","link.postMessage") }}</a>
+ </li>
+ {% else %}
+ <li>
+ <a class="a-login" href="/login" rel="nofollow">
+ <i data-icon="ei-user" data-size="s"></i>{{ i18n("messages", "link.Login") }}
+ </a>
+ </li>
+ {% endif %}
+ </ul>
+ </nav>
+ </div>
+</header>
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 @@
+<div id="pagetabs"><ul>
+ <li><a href="/settings">{{ i18n("messages","link.settings.main") }}</a></li>
+ <li><a href="/settings?page=password">{{ i18n("messages","link.settings.password") }}</a></li>
+ <li><a href="/settings?page=about">{{ i18n("messages","link.settings.about") }}</a></li>
+ <li><a href="/logout"><i data-icon="ei-user" data-size="s"></i>{{ i18n("messages","link.logout") }}</a></li>
+</ul></div> \ 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 @@
+<div id="ctitle">
+ <h2>*{{ tag.name }}</h2>
+</div>
+{% if visitor is not empty and visitor.uid > 0 %}
+<ul class="toolbar">
+ {% if isSubscribed %}
+ <li>
+ <a href="/post?body=U+%2A{{ tag.name }}" title="Подписан">
+ <i data-icon="ei-check" data-size="s"></i>Subscribed
+ </a>
+ </li>
+ {% else %}
+ <li>
+ <a href="/post?body=S+%2A{{ tag.name }}" title="Подписаться">
+ <i data-icon="ei-plus" data-size="s"></i>Subscribe
+ </a>
+ </li>
+ {% endif %}
+ {% if isInBL %}
+ <li>
+ <a href="/post?body=BL+%2A{{ tag.name }}" title="Разблокировать">
+ <i data-icon="ei-close-o" data-size="s"></i>Unblock
+ </a>
+ </li>
+ {% else %}
+ <li>
+ <a href="/post?body=BL+%2A{{ tag.name }}" title="Заблокировать">
+ <i data-icon="ei-close" data-size="s"></i>Block
+ </a>
+ </li>
+ {% endif %}
+</ul>
+{% 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 %}
+ <a href="/tag/{{ tag | urlencode }}" title="{{ tag }}">{{ tag | raw }}</a>
+{% 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 %}
+<div id="ctitle">
+ <a href="/{{ user.name }}">
+ <img src="//i.juick.com/a/{{ user.uid }}.png" alt=""/>{{ user.name }}
+ </a>
+</div>
+<ul class="toolbar">
+ {% if isSubscribed %}
+ <li>
+ <a href="/post?body=U+%40{{ user.name }}" title="Подписан">
+ <i data-icon="ei-check" data-size="s"></i>Subscribed
+ </a>
+ </li>
+ {% else %}
+ <li>
+ <a href="/post?body=S+%40{{ user.name }}" title="Подписаться">
+ <i data-icon="ei-plus" data-size="s"></i>Subscribe
+ </a>
+ </li>
+ {% endif %}
+ {% if isInBL %}
+ <li>
+ <a href="/post?body=BL+%40{{ user.name }}" title="Разблокировать">
+ <i data-icon="ei-close-o" data-size="s"></i>Unblock
+ </a>
+ </li>
+ {% else %}
+ <li>
+ <a href="/post?body=BL+%40{{ user.name }}" title="Заблокировать">
+ <i data-icon="ei-close" data-size="s"></i>Block
+ </a>
+ </li>
+ {% endif %}
+ {% if not isInBLAny %}
+ <li>
+ <a href="/pm/sent?uname={{ user.name }}" title="Написать приватное сообщение">
+ <i data-icon="ei-envelope" data-size="s"></i>PM
+ </a>
+ </li>
+ {% endif %}
+</ul>
+{% else %}
+<hr/>
+{% endif %}
+<ul>
+ {% if visitor is not empty and visitor.uid == user.uid %}
+ <li><a href="/?show=my"><i data-icon="ei-clock" data-size="s"></i>{{ i18n("messages","link.my") }}</a></li>
+ <li><a href="/pm/inbox"><i data-icon="ei-envelope" data-size="s"></i>{{ i18n("messages","link.privateMessages") }}</a></li>
+ <li><a href="/?show=discuss"><i data-icon="ei-comment" data-size="s"></i>{{ i18n("messages","link.discuss") }}</a></li>
+ {% endif %}
+ <li><a href="/{{ user.name }}/?show=recomm" rel="nofollow"><i data-icon="ei-heart" data-size="s"></i>{{ i18n("messages","blog.recommendations") }}</a></li>
+ <li><a href="/{{ user.name }}/?show=photos" rel="nofollow"><i data-icon="ei-camera" data-size="s"></i>{{ i18n("messages","blog.photos") }}</a></li>
+ {% if visitor is not empty and visitor.uid == user.uid and false %}
+ <li><a href="/?show=mycomments" rel="nofollow">{{ i18n("messages","blog.comments") }}</a></li>
+ <li><a href="/?show=unanswered" rel="nofollow">Неотвеченные</a></li>
+ {% endif %}
+ {% if visitor is not empty and visitor.uid == user.uid %}
+ <li><a href="/settings" rel="nofollow"><i data-icon="ei-gear" data-size="s"></i>{{ i18n("messages","link.settings") }}</a></li>
+ {% endif %}
+</ul>
+<hr/>
+<form action="/{{ user.name }}/">
+ <p><input type="text" name="search" class="inp" placeholder="{{ i18n('messages','label.search') }}"/></p>
+</form>
+{% include "views/partial/usertags" %}
+<hr/>
+<div id="ustats">
+ <ul>
+ <li><a href="/{{ user.name }}/friends">{{ i18n("messages","blog.iread") }}: {{ statsIRead }}</a></li>
+ <li><a href="/{{ user.name }}/readers">{{ i18n("messages","blog.readers") }}: {{ statsMyReaders }}</a></li>
+ {% if statsMyBL > 0 and visitor.uid == user.uid %}
+ <li><a href="/{{ user.name }}/bl">{{ i18n("messages","blog.bl") }}: {{ statsMyBL }}</a></li>
+ {% endif %}
+ <li>{{ i18n("messages","blog.messages") }}: {{ statsMessages }}</li>
+ <li>{{ i18n("messages","blog.comments") }}: {{ statsReplies }}</li>
+ </ul>
+ {% if iread is not empty %}
+ <div class="iread">
+ {% for u in iread %}
+ <span>
+ <a href="/{{ u.name }}/">
+ <img src="//i.juick.com/as/{{ u.uid }}.png" alt="{{ u.name }}"/>
+ </a>
+ </span>
+ {% endfor %}
+ </div>
+ {% endif %}
+
+</div>
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) }}
+<a href="/{{ user.name }}/tags" rel="nofollow">...</a> \ 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() %}
+<ul id="private-messages">
+ {% for msg in msgs %}
+ <li class="msg">
+ <div class="msg-cont">
+ <div class="msg-header">
+ @<a href="/{{ msg.user.name }}/">{{ msg.user.name }}</a>:
+ <div class="msg-avatar">
+ <a href="/{{ msg.user.name }}/">
+ <img src="//i.juick.com/a/{{ msg.user.uid }}.png" alt="{{ msg.user.name }}"/>
+ </a>
+ </div>
+ <div class="msg-ts">{{ msg.timestamp | prettyTime }}</div>
+ </div>
+
+ <div class="msg-txt">{{ msg | formatMessage }}</div>
+ <form action="/pm/send" method="POST" enctype="multipart/form-data">
+ <input type="hidden" name="uname" value="{{ msg.user.name }}"/>
+ <div class="msg-comment">
+ <div class="ta-wrapper">
+ <textarea name="body" rows="1" class="replypm" placeholder="Написать ответ"></textarea>
+ </div>
+ </div>
+ </form>
+ </div>
+ </li>
+ {% endfor %}
+</ul>
+{% 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 %}
+<form action="/pm/send" method="POST" enctype="multipart/form-data">
+ <div class="newpm">
+ <div class="newpm-to">To: <input type="text" name="uname" placeholder="username" value="{{ uname }}"/></div>
+ <div class="newpm-body"><textarea name="body" rows="2"></textarea></div>
+ <div class="newpm-send"><input type="submit" value="OK"/></div>
+ </div>
+</form>
+{% if not msgs.isEmpty() %}
+<ul id="private-messages">
+ {% for msg in msgs %}
+ <li class="msg">
+ <div class="msg-cont">
+ <div class="msg-header">
+ @<a href="/{{ msg.user.name }}/">{{ msg.user.name }}</a>:
+ <div class="msg-avatar">
+ <a href="/{{ msg.user.name }}/">
+ <img src="//i.juick.com/a/{{ msg.user.uid }}.png" alt="{{ msg.user.name }}"/>
+ </a>
+ </div>
+ <div class="msg-ts">{{ msg.timestamp | prettyTime }}</div>
+ </div>
+ <div class="msg-txt">{{ msg | formatMessage }}</div>
+ </div>
+ </li>
+ {% endfor %}
+</ul>
+{% 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 %}
+<article>
+<form action="/post2" method="post" id="postmsg" enctype="multipart/form-data">
+ <p style="text-align: left">
+ <b>Фото:</b> <span id="attachmentfile">
+ <input style="width: 100%;" type="file" name="attach"/> <i>({{ i18n("messages","postForm.imageFormats") }})</i></span>
+ </p>
+ <p>
+ <textarea name="body" class="newmessage" rows="7" cols="10" placeholder="*weather It's very cold today!">{{ body }}</textarea>
+ <br/>
+ <input type="submit" class="subm" value=" {{ i18n("messages","postForm.submit") }} "/>
+ </p>
+</form>
+</article>
+<p style="text-align: left"><b>Теги:</b></p>
+{{ 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 %}
+<h1>Сообщение опубликовано</h1>
+<p>Поделитесь своим новым постом в социальных сетях:</p>
+{% if sharetwi | default('') is not empty %}
+<p class="social">
+ <a href="https://twitter.com/intent/tweet?text={{ sharetwi }}"
+ class="sharenew"><i data-icon="ei-sc-twitter" data-size="m"></i>Отправить в Twitter</a></p>
+{% endif %}
+<p class="social">
+ <a href="https://vk.com/share.php?url={{ url | urlencode }}"
+ class="sharenew"><i data-icon="ei-sc-vk" data-size="m"></i>Отправить в ВКонтакте</a></p>
+{% if facebook | default('') is not empty %}
+<p class="social">
+ <a href="https://www.facebook.com/sharer/sharer.php?u={{ url | urlencode }}"
+ class="sharenew"><i data-icon="ei-sc-facebook" data-size="m"></i>Отправить в Facebook</a></p>
+{% endif %}
+<p>Ссылка на сообщение: <a href="{{ url | raw }}">{{ url }}</a></p>
+{% 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 %}
+<article>
+ <form action="/settings" method="POST" enctype="multipart/form-data">
+ <p>Full name: <input type="text" name="fullname" value="{{ userinfo.fullName }}"/></p>
+ <p>Country: <input type="text" name="country" value="{{ userinfo.country }}"/></p>
+ <p>URL: <input type="text" name="url" value="{{ userinfo.url }}" size="32"/><br/>
+ <small>Please, start with &quot;http://&quot;</small></p>
+ <p>About:<br/>
+ <input type="text" name="descr" value="{{ userinfo.description }}" style="width: 100%"/><br/>
+ <small>Max. 255 symbols</small></p>
+ <p>Avatar: <input type="file" name="avatar"/><br/>
+ <small>Recommendations: PNG, 96x96, &lt;50Kb. Also, JPG and GIF supported.</small></p>
+ <p><input type="hidden" name="page" value="about"/><input type="submit" value=" OK "/></p>
+ </form>
+</article>
+{% 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 %}
+<article>
+ <p>{{ result }}</p><p><a href="/settings">Settings</a>.</p>
+</article>
+{% 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 %}
+<article>
+ <h1>Настройки</h1>
+ <form action="/settings" method="POST" enctype="multipart/form-data">
+ <fieldset>
+ <legend>Notification options</legend>
+ <p><input type="checkbox" name="jnotify" value="1" {% if notify_options.repliesEnabled %}
+ checked="checked" {% endif %}/> Reply notifications (&quot;Message posted&quot;)</p>
+ <p><input type="checkbox" name="subscr_notify" value="1" {% if notify_options.subscriptionsEnabled %}
+ checked="checked" {% endif %}/> Subscriptions notifications (&quot;@user subscribed...&quot;)</p>
+ <p><input type="checkbox" name="recomm" value="1" {% if notify_options.recommendationsEnabled %}
+ checked="checked" {% endif %}/> Posts recommendations (&quot;Recommended by @user&quot;)</p>
+ <p><input type="hidden" name="page" value="main"/><input type="submit" value=" OK "/></p>
+ </fieldset>
+ </form>
+ <fieldset>
+ <legend style="background: url(//telegram.org/favicon.ico?3) no-repeat; padding-left: 58px; line-height: 48px;">
+ Telegram</legend>
+ {% if telegram_name is not empty %}
+ <form action="/settings" method="post">
+ <div>Telegram: <b>{{ telegram_name }}</b> &mdash;
+ <input type="hidden" name="page" value="telegram-del"/>
+ <input type="submit" value=" Disable "/>
+ </div>
+ </form>
+ {% else %}
+ <p>To connect Telegram account: send any text message to <a href="https://telegram.me/Juick_bot">@Juick_bot</a>
+ </p>
+ {% endif %}
+ </fieldset>
+ {% if jids | length > 0 %}
+ <form action="/settings" method="POST" enctype="multipart/form-data">
+ <fieldset>
+ <legend style="background: url(//static.juick.com/settings/xmpp.png) no-repeat; padding-left: 58px; line-height: 48px;">
+ XMPP accounts
+ </legend>
+ <p>Your accounts:</p>
+ <p>
+ {% for jid in jids %}
+ <label><input type="radio" name="delete" value="xmpp;{{ jid }}">{{ jid }}</label><br/>
+ {% endfor %}
+ {% for auth in auths %}
+ <label><input type="radio" name="delete"
+ value="xmpp-unauth;{{ auth.account }}">{{ auth.account }}</label>
+ &mdash; <a href="#"
+ onclick="alert(\'To confirm, please send &quot;AUTH {{ auth.getAuthCode() }}&quot; (without quotes) from this account to &quot;juick@juick.com&quot;.\'); return false;">Confirm</a><br/>
+ {% endfor %}
+ </p>
+ {% if jids | length > 1 %}
+ <p><input type="hidden" name="page" value="jid-del"/><input type="submit" value=" Delete "/></p>
+ {% endif %}
+ <p>To add new jabber account: send any text message to <a href="xmpp:juick@juick.com?message;body=login">juick@juick.com</a>
+ </p>
+ </fieldset>
+ </form>
+ {% endif %}
+ <fieldset>
+ <legend style="background: url(//static.juick.com/settings/email.png) no-repeat; padding-left: 58px; line-height: 48px;">
+ E-mail
+ </legend>
+ <form action="/settings" method="POST" enctype="multipart/form-data">
+ <p>Add account:<br/>
+ <input type="text" name="account"/>
+ <input type="hidden" name="page" value="email-add"/>
+ <input type="submit" value=" Add "/>
+ </p>
+ </form>
+ <form action="/settings" method="POST" enctype="multipart/form-data">
+ <p>Your accounts:</p>
+ <p>
+ {% for email in emails %}
+ <label><input type="radio" name="account" value="{{ email }}">{{ email }}</label><br/>
+ {% endfor %}
+ {% if emails is empty %}
+ - </p>
+ {% else %}
+ </p>
+ {% if jids | length > 1 %}
+ <p><input type="hidden" name="page" value="email-del"/><input type="submit" value=" Delete "/></p>
+ {% endif %}
+ {% endif %}
+ </form>
+ {% if emails is not empty %}
+ <!--email_off-->
+ <form action="/settings" method="POST" enctype="multipart/form-data">
+ <p>You can receive notifications to email:<br/>
+ Sent to <select name="account">
+ <option value="">Disabled</option>
+ {% for email in emails %}
+ <option value="{{ email }}" {% if email_active == email %} selected="selected" {% endif %}>
+ {{ email }}
+ </option>
+ {% endfor %}
+ </select>
+ <input type="hidden" name="page" value="email-subscr"/>
+ <input type="submit" value="OK"/></p>
+ </form>
+ <!--/email_off-->
+ {% endif %}
+ <p>&nbsp;</p>
+ <p>You can post to Juick via e-mail. Send your <span style="text-decoration: underline">plain text</span>
+ messages to <span><a href="mailto:juick@juick.com">juick@juick.com</a></span>. You can attach one photo or video file.</p>
+ </fieldset>
+ <fieldset>
+ <legend style="background: url(//static.juick.com/settings/facebook.png) no-repeat; padding-left: 58px; line-height: 48px;">
+ Facebook
+ </legend>
+ {% if fbstatus.connected %}
+ {% if fbstatus.crosspostEnabled %}
+ <form action="/settings" method="post">
+ <div>
+ Facebook: <b>Enabled</b> &mdash;
+ <input type="hidden" name="page" value="facebook-disable"/>
+ <input type="submit" value=" Disable "/>
+ </div>
+ </form>
+ {% else %}
+ <form action="/settings" method="post">
+ <div>
+ Facebook: <b>Disabled</b> &mdash;
+ <input type="hidden" name="page" value="facebook-enable"/>
+ <input type="submit" value=" Enable "/>
+ </div>
+ </form>
+ {% endif %}
+ {% else %}
+ <p>Cross-posting to Facebook: <a href="/_fblogin"><img src="//static.juick.com/facebook-connect.png" alt="Connect to Facebook"/></a></p>
+ {% endif %}
+ </fieldset>
+ <fieldset>
+ <legend style="background: url(//static.juick.com/settings/twitter.png) no-repeat; padding-left: 58px; line-height: 48px;">
+ Twitter</legend>
+ {% if twitter_name is not empty %}
+ <form action="/settings" method="post">
+ <div>Twitter: <b>{{ twitter_name }}</b> &mdash;
+ <input type="hidden" name="page" value="twitter-del"/>
+ <input type="submit" value=" Disable "/>
+ </div>
+ </form>
+ {% else %}
+ <p>Cross-posting to Twitter: <a href="/_twitter"><img src="//static.juick.com/twitter-connect.png"
+ alt="Connect to Twitter"/></a></p>
+ {% endif %}
+ </fieldset>
+
+</article>
+{% 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 %}
+<article>
+ <fieldset>
+ <legend>Changing your password</legend>
+ <form action="/settings" method="post">
+ <input type="hidden" name="page" value="password"/>
+ <p>Change password: <input type="password" name="password" size="8"/> <input type="submit"
+ value=" Update "/><br/>
+ <i>(max. length - 16 symbols)</i></p>
+ </form>
+ </fieldset>
+</article>
+{% 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 %}
+<article>
+ <p>Privacy</p>
+</article>
+{% 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 %}
+<article>
+ <p>{{ result | raw }}</p>
+</article>
+{% 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 %}
+<h1 class="signup-h1">
+ {% if type | slice(0, 1) == 'f' %}
+ <img src="//static.juick.com/settings/facebook.png" alt="Facebook"/>
+ {% elseif type | slice(0, 1) == 'v' %}
+ <img src="//static.juick.com/settings/vk.png" alt="VKontakte"/>
+ {% elseif type | slice(0, 1) == 'e' %}
+ <img src="//static.juick.com/settings/email.png" alt="Email"/>
+ {% elseif type | slice(0, 1) == 'd' %}
+ <img src="//telegram.org/favicon.ico?3" alt="Telegram"/>
+ {% endif %}
+ {{ account | raw }}</h1>
+
+<h2 class="signup-h2">Связать с существующим аккаунтом Juick</h2>
+<form action="/signup" method="post">
+ <input type="hidden" name="action" value="link"/>
+ <input type="hidden" name="type" value="{{ type }}"/>
+ <input type="hidden" name="hash" value="{{ hash }}"/>
+ {% if visitor.getUID() > 0 %}
+ <input type="submit" value="Связать с этим аккаунтом"/>
+ {% else %}
+ <p>Имя пользователя: <input type="text" name="username"/></p>
+ <p>Пароль: <input type="password" name="password"/></p>
+ <p><input type="submit" value=" OK "/></p>
+ {% endif %}
+</form>
+
+{% if type != "xmpp" %}
+<hr class="signup-hr"/>
+
+<h2 class="signup-h2">Создать новый аккаунт Juick</h2>
+<form action="/signup" method="post">
+ <input type="hidden" name="action" value="new"/>
+ <input type="hidden" name="type" value="{{ type }}"/>
+ <input type="hidden" name="hash" value="{{ hash }}"/>
+ <p>Имя пользователя: <input type="text" name="username" id="username"/><br/><i>(От 2-х до 16-и латинских символов
+ и/или цифр, дефис)</i></p>
+ <p>Пароль: <input type="password" name="password"/><br/><i>(от 6-и до 32-х символов)</i></p>
+ <p><input type="submit" value=" OK "/></p>
+</form>
+{% 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 %}
+<ul id="0">
+ <li id="msg-{{ msg.mid }}" class="msg msgthread">
+ <div class="msg-cont" itemscope="" itemtype="http://schema.org/BlogPosting" itemref="org">
+ <div class="msg-header">
+ <div class="msg-avatar">
+ <a href="/{{ msg.user.name }}/"><img src="//i.juick.com/a/{{ msg.user.uid }}.png" alt="{{ msg.user.name }}"/></a>
+ </div>
+ <span itemprop="author" itemscope="" itemtype="http://schema.org/Person">
+ <a itemprop="url" rel="author" href="/{{ msg.user.name }}/"><span itemprop="name">{{ msg.user.name }}</span></a>
+ </span>
+ <div class="msg-ts">
+ <a href="/{{ msg.user.name }}/{{ msg.mid }}">
+ <time itemprop="datePublished dateModified" datetime="{{ msg.timestamp | timestamp | date('yyyy-MM-dd HH:mm:ss') }}Z"
+ title="{{ msg.timestamp | timestamp | date('yyyy-MM-dd HH:mm:ss') }} GMT">
+ {{ msg.timestamp | prettyTime }}
+ </time>
+ </a>
+ </div>
+ <div class="msg-tags" itemprop="headline">
+ {{ tags(msg.user.name, msg.tags | tagsList) }}
+ </div>
+ </div>
+ <div class="msg-txt" itemprop="articleBody">{{ msg | formatMessage }}</div>
+ {% if msg.AttachmentType is not empty %}
+ <div class="msg-media">
+ <a href="//i.juick.com/p/{{ msg.mid }}.{{ msg.AttachmentType }}" data-fname="{{ msg.mid }}.{{ msg.AttachmentType }}">
+ <img itemprop="image" src="//i.juick.com/photos-512/{{ msg.mid }}.{{ msg.AttachmentType }}" alt=""/>
+ </a>
+ </div>
+ {% endif %}
+ <nav class="l">
+ {% if visitor.uid == msg.user.uid %}
+ <a href="/{{ msg.mid }}" class="a-like msg-button">
+ <span class="msg-button-icon">
+ <i data-icon="ei-heart" data-size="s"></i>
+ {% if msg.Likes > 0 %}&nbsp;{{ msg.Likes }}{% endif %}
+ </span>
+ <span>&nbsp;{{ i18n("messages","message.recommend") }}</span>
+ </a>
+ {% elseif visitor.uid > 0 %}
+ <a href="/post?body=!+%23{{ msg.mid }}" class="a-like msg-button">
+ <span class="msg-button-icon">
+ <i data-icon="ei-heart" data-size="s"></i>
+ {% if msg.Likes > 0 %}&nbsp;{{ msg.Likes }}{% endif %}
+ </span>
+ <span>&nbsp;{{ i18n("messages","message.recommend") }}</span>
+ </a>
+ {% else %}
+ <a href="/login" class="a-login msg-button">
+ <span class="msg-button-icon">
+ <i data-icon="ei-heart" data-size="s"></i>
+ {% if msg.Likes > 0 %}&nbsp;{{ msg.Likes }}{% endif %}
+ </span>
+ <span>&nbsp;{{ i18n("messages","message.recommend") }}</span>
+
+ </a>
+ {% endif %}
+ <a href="#" class="msg-menu msg-button">
+ <i data-icon="ei-link" data-size="s"></i>
+ <span>&nbsp;{{ i18n("messages","message.share") }}</span>
+ </a>
+ {% if visitor.uid > 0 %}
+ {% if visitor.uid != msg.user.uid %}
+ {% if visitorSubscribed %}
+ <a href="/post?body=U+%23{{ msg.mid }}" class="msg-button">
+ <i data-icon="ei-check" data-size="s"></i>
+ <span>&nbsp;{{ i18n("messages","message.subscribed") }}</span>
+ </a>
+ {% else %}
+ <a href="/post?body=S+%23{{ msg.mid }}" class="msg-button">
+ <i data-icon="ei-eye" data-size="s"></i>
+ <span>&nbsp;{{ i18n("messages","message.subscribe") }}</span>
+ </a>
+ {% endif %}
+ {% else %}
+ <a href="/post?body=D+%23{{ msg.mid }}" class="msg-button">
+ <i data-icon="ei-close" data-size="s"></i>
+ <span>&nbsp;{{ i18n("messages","message.delete") }}</span>
+ </a>
+ {% endif %}
+ {% endif %}
+ {% if msg.FriendsOnly %}
+ <a href="#" class="a-privacy">Открыть доступ</a>
+ {% endif %}
+ </nav>
+ {% if msg.VisitorCanComment %}
+ <form action="/comment" method="POST" enctype="multipart/form-data" class="msg-comment-target">
+ <input type="hidden" name="mid" value="{{ msg.mid }}"/>
+ <div class="msg-comment">
+ <div class="ta-wrapper">
+ <textarea name="body" rows="1" class="reply" placeholder="{{ i18n("messages","message.writeComment") }}"></textarea>
+ </div>
+ </div>
+ </form>
+ {% endif %}
+ {% if recomm is not empty %}
+ <div class="msg-recomms">{{ i18n("messages","message.recommendedBy") }}
+ {% for rec in recomm %}
+ <a href="/{{ rec }}/">@{{ rec }}</a>{% if loop.index < (loop.length - 1) %}, {% endif %}
+ {% endfor %}
+ {% if msg.likes > recomm.size() %}
+ &nbsp;{{ i18n("messages","message.recommendedOthers", msg.likes - recomm.size()) }}
+ {% endif %}
+ </div>
+ {% endif %}
+ </div>
+ </li>
+</ul>
+<div class="title2">
+ {% if visitor.uid > 0 %}
+ <img src="https://api.juick.com/thread/mark_read/{{ msg.mid }}-{{ msg.rid }}.gif?hash={{visitor.authHash}}" />
+ {% endif %}
+ <h2>{{ i18n("messages","reply.replies") }} ({{ replies.size() }})</h2>
+</div>
+
+<ul id="replies">
+ {% for msg in replies %}
+ <li id="{{ msg.rid }}" class="msg">
+ <div class="msg-cont">
+ <div class="msg-header">
+ {% if not msg.user.banned %}
+ <a href="/{{ msg.user.name }}/">{{ msg.user.name }}</a>
+ <div class="msg-avatar"><a href="/{{ msg.user.name }}/">
+ <img src="//i.juick.com/a/{{ msg.user.uid }}.png" alt="{{ msg.user.name }}"/></a>
+ </div>
+ {% else %}
+ [удалено]:
+ <div class="msg-avatar">
+ <img src="//i.juick.com/av-96.png"/>
+ </div>
+ {% endif %}
+ <div class="msg-ts">
+ <a href="/{{ msg.mid }}#{{ msg.rid }}">
+ <time datetime="{{ msg.timestamp | timestamp | date('yyyy-MM-dd HH:mm:ss') }}Z"
+ title="{{ msg.timestamp | timestamp | date('yyyy-MM-dd HH:mm:ss') }} GMT">
+ {{ msg.timestamp | prettyTime }}
+ </time>
+ </a>
+ </div>
+ </div>
+ <div class="msg-txt">{{ msg | formatMessage }}</div>
+ {% if msg.AttachmentType is not empty %}
+ <div class="msg-media">
+ <a href="//i.juick.com/p/{{ msg.mid }}-{{ msg.rid }}.{{ msg.AttachmentType }}" data-fname="{{ msg.mid }}-{{ msg.rid }}.{{ msg.AttachmentType }}">
+ <img src="//i.juick.com/photos-512/{{ msg.mid }}-{{ msg.rid }}.{{ msg.AttachmentType }}" alt=""/>
+ </a>
+ </div>
+ {% endif %}
+ <div class="msg-links">/{{ msg.rid }}
+ {% if msg.replyto > 0 %}
+ {{ i18n("messages","reply.inReplyTo") }} <a href="#{{ msg.replyto }}">/{{ msg.replyto }}</a>
+ {% endif %}
+ {% if msg.VisitorCanComment %}
+ &middot; <a href="/post?body=%23{{ msg.mid }}/{{ msg.rid }}%20" class="a-thread-comment">{{ i18n("messages","reply.reply") }}</a>
+ </div>
+ <div class="msg-comment-target msg-comment-hidden"></div>
+ {% elseif visitor.uid == 0 %}
+ &middot; <a href="#" class="a-login">{{ i18n("messages","reply.reply") }}</a>
+ </div>
+ {% else %}
+ </div>
+ {% endif %}
+ </div>
+ </li>
+ {% endfor %}
+</ul>
+{% 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 %}
+<div class="users">
+ {% for u in users %}
+ <span>
+ <a href="/{{ u.name }}/">
+ <img src="//i.juick.com/as/{{ u.uid }}.png" alt="{{ u.name }}"/>
+ {{ u.name }}
+ </a>
+ </span>
+ {% endfor %}
+</div>
+{% endblock %}
+{% block "column" %}
+{% include "views/partial/usercolumn" %}
+{% endblock %} \ No newline at end of file