From 670913698776e09b7ff44f44ccdcf56303d79be3 Mon Sep 17 00:00:00 2001 From: Vitaly Takmazov Date: Thu, 6 Sep 2018 13:58:40 +0300 Subject: merge legacy www --- juick-server/src/main/assets/embed.js | 336 ++++++++ juick-server/src/main/assets/logo.png | Bin 0 -> 2447 bytes juick-server/src/main/assets/logo@2x.png | Bin 0 -> 4822 bytes juick-server/src/main/assets/scripts.js | 805 +++++++++++++++++ juick-server/src/main/assets/style.css | 952 +++++++++++++++++++++ .../java/com/juick/server/TelegramBotManager.java | 1 + .../java/com/juick/server/api/ApiSocialLogin.java | 302 +++++++ .../src/main/java/com/juick/server/api/Index.java | 4 +- .../main/java/com/juick/server/api/Messages.java | 12 +- .../java/com/juick/server/api/Notifications.java | 14 +- .../src/main/java/com/juick/server/api/PM.java | 6 +- .../src/main/java/com/juick/server/api/Post.java | 12 +- .../main/java/com/juick/server/api/Service.java | 2 +- .../java/com/juick/server/api/SocialLogin.java | 302 ------- .../src/main/java/com/juick/server/api/Tags.java | 2 +- .../src/main/java/com/juick/server/api/Users.java | 12 +- .../com/juick/server/api/activity/Profile.java | 14 +- .../server/configuration/ApiSecurityConfig.java | 127 --- .../com/juick/server/configuration/PostConfig.java | 9 - .../server/configuration/SapeConfiguration.java | 37 + .../juick/server/configuration/SecurityConfig.java | 209 +++++ .../server/configuration/WwwAppConfiguration.java | 116 +++ .../java/com/juick/server/www/HelpService.java | 69 ++ .../src/main/java/com/juick/server/www/Utils.java | 45 + .../src/main/java/com/juick/server/www/WebApp.java | 71 ++ .../server/www/controllers/AnythingFilter.java | 64 ++ .../server/www/controllers/AppSiteAssociation.java | 49 ++ .../com/juick/server/www/controllers/Help.java | 93 ++ .../com/juick/server/www/controllers/Login.java | 50 ++ .../juick/server/www/controllers/MessagesWWW.java | 595 +++++++++++++ .../juick/server/www/controllers/NewMessage.java | 263 ++++++ .../com/juick/server/www/controllers/Settings.java | 262 ++++++ .../com/juick/server/www/controllers/SignUp.java | 172 ++++ .../juick/server/www/controllers/SocialLogin.java | 321 +++++++ .../java/com/juick/server/www/facebook/User.java | 125 +++ .../java/com/juick/server/www/twitter/User.java | 38 + .../main/java/com/juick/server/www/vk/Token.java | 56 ++ .../main/java/com/juick/server/www/vk/User.java | 65 ++ .../com/juick/server/www/vk/UsersResponse.java | 38 + .../java/com/juick/service/EmailServiceImpl.java | 16 + .../pebble/extension/FormatterExtension.java | 38 + .../extension/filters/FormatMessageFilter.java | 54 ++ .../pebble/extension/filters/PrettyTimeFilter.java | 51 ++ .../pebble/extension/filters/TagsListFilter.java | 43 + .../pebble/extension/filters/TimestampFilter.java | 25 + juick-server/src/main/java/ru/sape/Sape.java | 23 + .../src/main/java/ru/sape/SapeConnection.java | 108 +++ .../src/main/java/ru/sape/SapePageLinks.java | 76 ++ juick-server/src/main/resources/errors.properties | 3 + .../src/main/resources/errors_ru.properties | 3 + juick-server/src/main/resources/help | 1 + .../src/main/resources/messages.properties | 80 ++ .../src/main/resources/messages_ru.properties | 78 ++ juick-server/src/main/resources/schema.sql | 6 +- juick-server/src/main/resources/static/favicon.png | Bin 0 -> 244 bytes juick-server/src/main/resources/static/logo.png | Bin 0 -> 1184 bytes juick-server/src/main/resources/static/style.js | 2 + .../src/main/resources/static/style.js.map | 1 + .../src/main/resources/static/tagscloud.png | Bin 0 -> 42316 bytes .../main/resources/templates/layouts/content.html | 50 ++ .../main/resources/templates/layouts/default.html | 16 + .../main/resources/templates/layouts/minimal.html | 10 + .../src/main/resources/templates/views/404.html | 11 + .../src/main/resources/templates/views/blog.html | 25 + .../main/resources/templates/views/blog_tags.html | 10 + .../src/main/resources/templates/views/help.html | 10 + .../src/main/resources/templates/views/index.html | 29 + .../src/main/resources/templates/views/login.html | 144 ++++ .../resources/templates/views/login_success.html | 13 + .../resources/templates/views/macros/tags.html | 5 + .../resources/templates/views/partial/footer.html | 16 + .../templates/views/partial/homecolumn.html | 25 + .../resources/templates/views/partial/message.html | 76 ++ .../templates/views/partial/navigation.html | 40 + .../templates/views/partial/settings_tabs.html | 6 + .../templates/views/partial/tagcolumn.html | 33 + .../resources/templates/views/partial/tags.html | 3 + .../templates/views/partial/usercolumn.html | 89 ++ .../templates/views/partial/usertags.html | 3 + .../main/resources/templates/views/pm_inbox.html | 35 + .../main/resources/templates/views/pm_sent.html | 33 + .../src/main/resources/templates/views/post.html | 19 + .../resources/templates/views/post_success.html | 19 + .../resources/templates/views/settings_about.html | 20 + .../templates/views/settings_auth-email.html | 9 + .../resources/templates/views/settings_main.html | 151 ++++ .../templates/views/settings_password.html | 17 + .../templates/views/settings_privacy.html | 9 + .../resources/templates/views/settings_result.html | 9 + .../src/main/resources/templates/views/signup.html | 43 + .../src/main/resources/templates/views/test.html | 2 + .../src/main/resources/templates/views/thread.html | 173 ++++ .../src/main/resources/templates/views/users.html | 17 + .../java/com/juick/server/tests/ServerTests.java | 110 +-- .../java/com/juick/server/tests/WebAppTests.java | 466 ++++++++++ 95 files changed, 7469 insertions(+), 535 deletions(-) create mode 100644 juick-server/src/main/assets/embed.js create mode 100644 juick-server/src/main/assets/logo.png create mode 100644 juick-server/src/main/assets/logo@2x.png create mode 100644 juick-server/src/main/assets/scripts.js create mode 100644 juick-server/src/main/assets/style.css create mode 100644 juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java delete mode 100644 juick-server/src/main/java/com/juick/server/api/SocialLogin.java delete mode 100644 juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java delete mode 100644 juick-server/src/main/java/com/juick/server/configuration/PostConfig.java create mode 100644 juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java create mode 100644 juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java create mode 100644 juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java create mode 100644 juick-server/src/main/java/com/juick/server/www/HelpService.java create mode 100644 juick-server/src/main/java/com/juick/server/www/Utils.java create mode 100644 juick-server/src/main/java/com/juick/server/www/WebApp.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/Help.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/Login.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/Settings.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java create mode 100644 juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java create mode 100644 juick-server/src/main/java/com/juick/server/www/facebook/User.java create mode 100644 juick-server/src/main/java/com/juick/server/www/twitter/User.java create mode 100644 juick-server/src/main/java/com/juick/server/www/vk/Token.java create mode 100644 juick-server/src/main/java/com/juick/server/www/vk/User.java create mode 100644 juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java create mode 100644 juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java create mode 100644 juick-server/src/main/java/ru/sape/Sape.java create mode 100644 juick-server/src/main/java/ru/sape/SapeConnection.java create mode 100644 juick-server/src/main/java/ru/sape/SapePageLinks.java create mode 100644 juick-server/src/main/resources/errors.properties create mode 100644 juick-server/src/main/resources/errors_ru.properties create mode 160000 juick-server/src/main/resources/help create mode 100644 juick-server/src/main/resources/messages.properties create mode 100644 juick-server/src/main/resources/messages_ru.properties create mode 100644 juick-server/src/main/resources/static/favicon.png create mode 100644 juick-server/src/main/resources/static/logo.png create mode 100644 juick-server/src/main/resources/static/style.js create mode 100644 juick-server/src/main/resources/static/style.js.map create mode 100644 juick-server/src/main/resources/static/tagscloud.png create mode 100644 juick-server/src/main/resources/templates/layouts/content.html create mode 100644 juick-server/src/main/resources/templates/layouts/default.html create mode 100644 juick-server/src/main/resources/templates/layouts/minimal.html create mode 100644 juick-server/src/main/resources/templates/views/404.html create mode 100644 juick-server/src/main/resources/templates/views/blog.html create mode 100644 juick-server/src/main/resources/templates/views/blog_tags.html create mode 100644 juick-server/src/main/resources/templates/views/help.html create mode 100644 juick-server/src/main/resources/templates/views/index.html create mode 100644 juick-server/src/main/resources/templates/views/login.html create mode 100644 juick-server/src/main/resources/templates/views/login_success.html create mode 100644 juick-server/src/main/resources/templates/views/macros/tags.html create mode 100644 juick-server/src/main/resources/templates/views/partial/footer.html create mode 100644 juick-server/src/main/resources/templates/views/partial/homecolumn.html create mode 100644 juick-server/src/main/resources/templates/views/partial/message.html create mode 100644 juick-server/src/main/resources/templates/views/partial/navigation.html create mode 100644 juick-server/src/main/resources/templates/views/partial/settings_tabs.html create mode 100644 juick-server/src/main/resources/templates/views/partial/tagcolumn.html create mode 100644 juick-server/src/main/resources/templates/views/partial/tags.html create mode 100644 juick-server/src/main/resources/templates/views/partial/usercolumn.html create mode 100644 juick-server/src/main/resources/templates/views/partial/usertags.html create mode 100644 juick-server/src/main/resources/templates/views/pm_inbox.html create mode 100644 juick-server/src/main/resources/templates/views/pm_sent.html create mode 100644 juick-server/src/main/resources/templates/views/post.html create mode 100644 juick-server/src/main/resources/templates/views/post_success.html create mode 100644 juick-server/src/main/resources/templates/views/settings_about.html create mode 100644 juick-server/src/main/resources/templates/views/settings_auth-email.html create mode 100644 juick-server/src/main/resources/templates/views/settings_main.html create mode 100644 juick-server/src/main/resources/templates/views/settings_password.html create mode 100644 juick-server/src/main/resources/templates/views/settings_privacy.html create mode 100644 juick-server/src/main/resources/templates/views/settings_result.html create mode 100644 juick-server/src/main/resources/templates/views/signup.html create mode 100644 juick-server/src/main/resources/templates/views/test.html create mode 100644 juick-server/src/main/resources/templates/views/thread.html create mode 100644 juick-server/src/main/resources/templates/views/users.html create mode 100644 juick-server/src/test/java/com/juick/server/tests/WebAppTests.java (limited to 'juick-server/src') diff --git a/juick-server/src/main/assets/embed.js b/juick-server/src/main/assets/embed.js new file mode 100644 index 00000000..25c37142 --- /dev/null +++ b/juick-server/src/main/assets/embed.js @@ -0,0 +1,336 @@ + +function insertAfter(newNode, referenceNode) { + referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); +} + +function setContent(containerNode, ...newNodes) { + removeAllFrom(containerNode); + newNodes.forEach(n => containerNode.appendChild(n)); + return containerNode; +} + +function removeAllFrom(fromNode) { + for (let c; c = fromNode.lastChild; ) { fromNode.removeChild(c); } +} + +function htmlEscape(html) { + let textarea = document.createElement('textarea'); + textarea.textContent = html; + return textarea.innerHTML; +} + +// rules :: [{pr: number, re: RegExp, with: string}] +// rules :: [{pr: number, re: RegExp, with: Function}] +// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string]}] +// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string, Function]}] +function formatText(txt, rules) { + let idCounter = 0; + function nextId() { return idCounter++; } + function ft(txt, rules) { + let matches = rules.map(r => { r.re.lastIndex = 0; return [r, r.re.exec(txt)]; }) + .filter(([,m]) => m !== null) + .sort(([r1,m1],[r2,m2]) => (r1.pr - r2.pr) || (m1.index - m2.index)); + if (matches && matches.length > 0) { + let [rule, match] = matches[0]; + let subsequentRules = rules.filter(r => r.pr >= rule.pr); + let idStr = `<>(${nextId()})<>`; + let outerStr = txt.substring(0, match.index) + idStr + txt.substring(rule.re.lastIndex); + let innerStr = (rule.brackets) + ? (() => { let [l ,r ,f] = rule.with; return l + ft((f ? f(match[1]) : match[1]), subsequentRules) + r; })() + : match[0].replace(rule.re, rule.with); + return ft(outerStr, subsequentRules).replace(idStr, innerStr); + } + return txt; + } + return ft(htmlEscape(txt), rules); // idStr above relies on the fact the text is escaped +} + +function fixWwwLink(url) { + return url.replace(/^(?!([a-z]+:)?\/\/)/i, '//'); +} + +function makeNewNode(embedType, aNode, reResult) { + const withClasses = el => { + if (embedType.className) { + el.classList.add(...embedType.className.split(' ')); + } + return el; + }; + return embedType.makeNode(aNode, reResult, withClasses(document.createElement('div'))); +} + +function makeIframe(src, w, h, scrolling='no') { + let iframe = document.createElement('iframe'); + iframe.style.width = w; + iframe.style.height = h; + iframe.frameBorder = 0; + iframe.scrolling = scrolling; + iframe.setAttribute('allowFullScreen', ''); + iframe.src = src; + iframe.innerHTML = 'Cannot show iframes.'; + return iframe; +} + +function makeResizableToRatio(element, ratio) { + element.dataset['ratio'] = ratio; + makeResizable(element, w => w * element.dataset['ratio']); +} + +// calcHeight :: Number -> Number -- calculate element height for a given width +function makeResizable(element, calcHeight) { + const setHeight = el => { + if (document.body.contains(el) && (el.offsetWidth > 0)) { + el.style.height = (calcHeight(el.offsetWidth)).toFixed(2) + 'px'; + } + }; + window.addEventListener('resize', () => setHeight(element)); + setHeight(element); +} + +function extractDomain(url) { + const domainRe = /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/i; + return domainRe.exec(url)[1]; +} + +function urlReplace(match, p1, p2, p3) { + let isBrackets = (p1 !== undefined); + return (isBrackets) + ? `${p1}` + : `${extractDomain(match)}`; +} + +function urlReplaceInCode(match, p1, p2, p3) { + let isBrackets = (p1 !== undefined); + return (isBrackets) + ? `${match}` + : `${match}`; +} + +function messageReplyReplace(messageId) { + return function(match, mid, rid) { + let replyPart = (rid && rid != '0') ? '#' + rid : ''; + return `${match}`; + }; +} + +/** + * Given "txt" message in unescaped plaintext with Juick markup, this function + * returns escaped formatted HTML string. + * + * @param {string} txt + * @param {string} messageId - current message id + * @param {boolean} isCode + * @returns {string} + */ +function juickFormat(txt, messageId, isCode) { + const urlRe = /(?:\[([^\]\[]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))/gi; + const bqReplace = m => m.replace(/^(?:>|>)\s?/gmi, ''); + return (isCode) + ? formatText(txt, [ + { pr: 1, re: urlRe, with: urlReplaceInCode }, + { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) }, + { pr: 1, re: /\B@([\w-]+)\b/gi, with: '@$1' }, + ]) + : formatText(txt, [ + { pr: 0, re: /((?:^(?:>|>)\s?[\s\S]+?$\n?)+)/gmi, brackets: true, with: ['', '', bqReplace] }, + { pr: 1, re: urlRe, with: urlReplace }, + { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) }, + { pr: 1, re: /\B@([\w-]+)\b/gi, with: '@$1' }, + { pr: 2, re: /\B\*([^\n]+?)\*((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] }, + { pr: 2, re: /\B\/([^\n]+?)\/((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] }, + { pr: 2, re: /\b\_([^\n]+?)\_((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] }, + { pr: 3, re: /\n/g, with: '
' }, + ]); +} + +function getEmbeddableLinkTypes() { + return [ + { + name: 'Jpeg and png images', + id: 'embed_jpeg_and_png_images', + className: 'picture compact', + ctsDefault: false, + re: /\.(jpe?g|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i, + makeNode: function(aNode, reResult, div) { + div.innerHTML = ``; + return div; + } + }, + { + name: 'Gif images', + id: 'embed_gif_images', + className: 'picture compact', + ctsDefault: true, + re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i, + makeNode: function(aNode, reResult, div) { + div.innerHTML = ``; + return div; + } + }, + { + name: 'Video (webm, mp4, ogv)', + id: 'embed_webm_and_mp4_videos', + className: 'video compact', + ctsDefault: false, + re: /\.(webm|mp4|m4v|ogv)(?:\?[\w&;\?=]*)?$/i, + makeNode: function(aNode, reResult, div) { + div.innerHTML = ``; + return div; + } + }, + { + name: 'Audio (mp3, ogg, weba, opus, m4a, oga, wav)', + id: 'embed_sound_files', + className: 'audio singleColumn', + ctsDefault: false, + re: /\.(mp3|ogg|weba|opus|m4a|oga|wav)(?:\?[\w&;\?=]*)?$/i, + makeNode: function(aNode, reResult, div) { + div.innerHTML = ``; + return div; + } + }, + { + name: 'YouTube videos (and playlists)', + id: 'embed_youtube_videos', + className: 'youtube resizableV singleColumn', + ctsDefault: false, + re: /^(?:https?:)?\/\/(?:www\.|m\.|gaming\.)?(?:youtu(?:(?:\.be\/|be\.com\/(?:v|embed)\/)([-\w]+)|be\.com\/watch)((?:(?:\?|&(?:amp;)?)(?:\w+=[-\.\w]*[-\w]))*)|youtube\.com\/playlist\?list=([-\w]*)(&(amp;)?[-\w\?=]*)?)/i, + makeNode: function(aNode, reResult, div) { + let [url, v, args, plist] = reResult; + let iframeUrl; + if (plist) { + iframeUrl = '//www.youtube-nocookie.com/embed/videoseries?list=' + plist; + } else { + let pp = {}; args.replace(/^\?/, '') + .split('&') + .map(s => s.split('=')) + .forEach(z => pp[z[0]] = z[1]); + let embedArgs = { rel: '0' }; + if (pp.t) { + const tre = /^(?:(\d+)|(?:(\d+)h)?(?:(\d+)m)?(\d+)s|(?:(\d+)h)?(\d+)m|(\d+)h)$/i; + let [, t, h, m, s, h1, m1, h2] = tre.exec(pp.t); + embedArgs['start'] = (+t) || ((+(h || h1 || h2 || 0))*60*60 + (+(m || m1 || 0))*60 + (+(s || 0))); + } + if (pp.list) { + embedArgs['list'] = pp.list; + } + v = v || pp.v; + let argsStr = Object.keys(embedArgs) + .map(k => `${k}=${embedArgs[k]}`) + .join('&'); + iframeUrl = `//www.youtube-nocookie.com/embed/${v}?${argsStr}`; + } + let iframe = makeIframe(iframeUrl, '100%', '360px'); + iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0); + return setContent(div, iframe); + } + }, + { + name: 'Vimeo videos', + id: 'embed_vimeo_videos', + className: 'vimeo resizableV', + ctsDefault: false, + re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/|album\/[\d]+\/video\/)?([\d]+)/i, + makeNode: function(aNode, reResult, div) { + let iframe = makeIframe('//player.vimeo.com/video/' + reResult[1], '100%', '360px'); + iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0); + return setContent(div, iframe); + } + } + ]; +} + +function embedLink(aNode, linkTypes, container, afterNode) { + let anyEmbed = false; + let linkId = (aNode.href.replace(/^https?:/i, '').replace(/\'/gi,'')); + let sameEmbed = container.querySelector(`*[data-linkid='${linkId}']`); // do not embed the same thing twice + if (sameEmbed === null) { + anyEmbed = [].some.call(linkTypes, function(linkType) { + let reResult = linkType.re.exec(aNode.href); + if (reResult) { + if (linkType.match && (linkType.match(aNode, reResult) === false)) { return false; } + let newNode = makeNewNode(linkType, aNode, reResult); + if (!newNode) { return false; } + newNode.setAttribute('data-linkid', linkId); + if (afterNode) { + insertAfter(newNode, afterNode); + } else { + container.appendChild(newNode); + } + aNode.classList.add('embedLink'); + return true; + } + }); + } + return anyEmbed; +} + +function embedLinks(aNodes, container) { + let anyEmbed = false; + let embeddableLinkTypes = getEmbeddableLinkTypes(); + Array.from(aNodes).forEach(aNode => { + let isEmbedded = embedLink(aNode, embeddableLinkTypes, container); + anyEmbed = anyEmbed || isEmbedded; + }); + return anyEmbed; +} + +/** + * Embed all the links inside element "x" that match to "allLinksSelector". + * All the embedded media is placed inside "div.embedContainer". + * "div.embedContainer" is inserted before an element matched by "beforeNodeSelector" + * if not present. Existing container is used otherwise. + * + * @param {Element} x + * @param {string} beforeNodeSelector + * @param {string} allLinksSelector + */ +function embedLinksToX(x, beforeNodeSelector, allLinksSelector) { + let isCtsPost = false; + let allLinks = x.querySelectorAll(allLinksSelector); + + let existingContainer = x.querySelector('div.embedContainer'); + if (existingContainer) { + embedLinks(allLinks, existingContainer); + } else { + let embedContainer = document.createElement('div'); + embedContainer.className = 'embedContainer'; + + let anyEmbed = embedLinks(allLinks, embedContainer); + if (anyEmbed) { + let beforeNode = x.querySelector(beforeNodeSelector); + x.insertBefore(embedContainer, beforeNode); + } + } +} + +function embedLinksToArticles() { + let beforeNodeSelector = 'nav.l'; + let allLinksSelector = 'p:not(.ir) a, pre a'; + Array.from(document.querySelectorAll('#content article')).forEach(article => { + embedLinksToX(article, beforeNodeSelector, allLinksSelector); + }); +} + +function embedLinksToPost() { + let beforeNodeSelector = '.msg-txt + *'; + let allLinksSelector = '.msg-txt a'; + Array.from(document.querySelectorAll('#content .msg-cont')).forEach(msg => { + embedLinksToX(msg, beforeNodeSelector, allLinksSelector); + }); +} + +/** + * Embed all the links in all messages/replies on the page. + */ +function embedAll() { + if (document.querySelector('#content article[data-mid]')) { + embedLinksToArticles(); + } else { + embedLinksToPost(); + } +} + +exports.embedAll = embedAll; +exports.embedLinksToX = embedLinksToX; +exports.format = juickFormat; diff --git a/juick-server/src/main/assets/logo.png b/juick-server/src/main/assets/logo.png new file mode 100644 index 00000000..4e0f6d56 Binary files /dev/null and b/juick-server/src/main/assets/logo.png differ diff --git a/juick-server/src/main/assets/logo@2x.png b/juick-server/src/main/assets/logo@2x.png new file mode 100644 index 00000000..6febeaf9 Binary files /dev/null and b/juick-server/src/main/assets/logo@2x.png differ diff --git a/juick-server/src/main/assets/scripts.js b/juick-server/src/main/assets/scripts.js new file mode 100644 index 00000000..54e7958e --- /dev/null +++ b/juick-server/src/main/assets/scripts.js @@ -0,0 +1,805 @@ +require('whatwg-fetch'); +require('element-closest'); +require('classlist.js'); +require('url-search-params-polyfill'); +let Awesomplete = require('awesomplete'); +import * as killy from './embed'; + +if (!('remove' in Element.prototype)) { // Firefox <23 + Element.prototype.remove = function () { + if (this.parentNode) { + this.parentNode.removeChild(this); + } + }; +} + +NodeList.prototype.forEach = Array.prototype.forEach; +HTMLCollection.prototype.forEach = Array.prototype.forEach; + +NodeList.prototype.filter = Array.prototype.filter; +HTMLCollection.prototype.filter = Array.prototype.filter; + +Element.prototype.selectText = function () { + let d = document; + if (d.body.createTextRange) { + let range = d.body.createTextRange(); + range.moveToElementText(this); + range.select(); + } else if (window.getSelection) { + let selection = window.getSelection(); + let rangeSel = d.createRange(); + rangeSel.selectNodeContents(this); + selection.removeAllRanges(); + selection.addRange(rangeSel); + } +}; + +function autosize(el) { + let offset = (!window.opera) + ? (el.offsetHeight - el.clientHeight) + : (el.offsetHeight + parseInt(window.getComputedStyle(el, null).getPropertyValue('border-top-width'))); + + let resize = function (el) { + el.style.height = 'auto'; + el.style.height = (el.scrollHeight + offset) + 'px'; + }; + + if (el.addEventListener) { + el.addEventListener('input', () => resize(el)); + } else if (el.attachEvent) { + el.attachEvent('onkeyup', () => resize(el)); + } +} + +function evilIcon(name) { + return `
`; +} + +/* eslint-disable only-ascii/only-ascii */ +const translations = { + 'en': { + 'message.inReplyTo': 'in reply to', + 'message.reply': 'Reply', + 'message.likeThisMessage?': 'Recommend this message?', + 'postForm.pleaseInputMessageText': 'Please input message text', + 'postForm.upload': 'Upload', + 'postForm.newMessage': 'New message...', + 'postForm.imageLink': 'Link to image', + 'postForm.imageFormats': 'JPG/PNG, up to 10 MB', + 'postForm.or': 'or', + 'postForm.tags': 'Tags (space separated)', + 'postForm.submit': 'Send', + 'comment.writeComment': 'Write a comment...', + 'shareDialog.linkToMessage': 'Link to message', + 'shareDialog.messageNumber': 'Message number', + 'shareDialog.share': 'Share', + 'loginDialog.pleaseIntroduceYourself': 'Please introduce yourself', + 'loginDialog.registeredAlready': 'Registered already?', + 'loginDialog.username': 'Username', + 'loginDialog.password': 'Password', + 'loginDialog.facebook': 'Login with Facebook', + 'loginDialog.vk': 'Login with VK', + 'loginDialog.email': 'Registration', + 'error.error': 'Error' + }, + 'ru': { + 'message.inReplyTo': 'в ответ на', + 'message.reply': 'Ответить', + 'message.likeThisMessage?': 'Рекомендовать это сообщение?', + 'postForm.pleaseInputMessageText': 'Пожалуйста, введите текст сообщения', + 'postForm.upload': 'загрузить', + 'postForm.newMessage': 'Новое сообщение...', + 'postForm.imageLink': 'Ссылка на изображение', + 'postForm.imageFormats': 'JPG/PNG, до 10Мб', + 'postForm.or': 'или', + 'postForm.tags': 'Теги (через пробел)', + 'postForm.submit': 'Отправить', + 'comment.writeComment': 'Написать комментарий...', + 'shareDialog.linkToMessage': 'Ссылка на сообщение', + 'shareDialog.messageNumber': 'Номер сообщения', + 'shareDialog.share': 'Поделиться', + 'loginDialog.pleaseIntroduceYourself': 'Пожалуйста, представьтесь', + 'loginDialog.registeredAlready': 'Уже зарегистрированы?', + 'loginDialog.username': 'Имя пользователя', + 'loginDialog.password': 'Пароль', + 'loginDialog.facebook': 'Войти через Facebook', + 'loginDialog.vk': 'Войти через ВКонтакте', + 'loginDialog.email': 'Регистрация', + 'error.error': 'Ошибка' + } +}; +/* eslint-enable only-ascii/only-ascii */ + +function getLang() { + return (window.navigator.languages && window.navigator.languages[0]) + || window.navigator.userLanguage + || window.navigator.language; +} +function i18n(key, lang = undefined) { + const fallbackLang = 'ru'; + lang = lang || getLang().split('-')[0]; + return (translations[lang] && translations[lang][key]) + || translations[fallbackLang][key] + || key; +} + +var ws, + pageTitle; + +function initWS() { + let url = (window.location.protocol === 'https:' ? 'wss' : 'ws') + ':' + + '//api.juick.com/ws/'; + let hash = document.getElementById('body').getAttribute('data-hash'); + if (hash) { + url += '?hash=' + hash; + } else { + let content = document.getElementById('content'); + if (content) { + let pageMID = content.getAttribute('data-mid'); + if (pageMID) { + url += pageMID; + } + } + } + + ws = new WebSocket(url); + ws.onopen = function () { + console.log('online'); + if (!document.querySelector('#wsthread')) { + var d = document.createElement('div'); + d.id = 'wsthread'; + d.addEventListener('click', nextReply); + document.querySelector('body').appendChild(d); + pageTitle = document.title; + } + }; + ws.onclose = function () { + console.log('offline'); + ws = false; + setTimeout(function () { + initWS(); + }, 2000); + }; + ws.onmessage = function (msg) { + if (msg.data == ' ') { + ws.send(' '); + } else { + try { + var jsonMsg = JSON.parse(msg.data); + console.log('data: ' + msg.data); + if (jsonMsg.service) { + return; + } + wsIncomingReply(jsonMsg); + } catch (err) { + console.log(err); + } + } + }; + setInterval(wsSendKeepAlive, 90000); +} + +function wsSendKeepAlive() { + if (ws) { + ws.send(' '); + } +} + +function wsShutdown() { + if (ws) { + ws.onclose = function () { }; + ws.close(); + } +} + +function wsIncomingReply(msg) { + let content = document.getElementById('content'); + if (!content) { return; } + let pageMID = content.getAttribute('data-mid'); + if (!pageMID || pageMID != msg.mid) { return; } + let msgNum = '/' + msg.rid; + if (msg.replyto > 0) { + msgNum += ` ${i18n('message.inReplyTo')} /${msg.replyto}`; + } + let photoDiv = (msg.attach == null) ? '' : ` +
+ +
`; + let msgContHtml = ` +
+
+ ${msg.user.uname}: +
+ ${msg.user.uname} +
+ +
+
${killy.format(msg.body, msg.mid, false)}
${photoDiv} + +
+
`; + + let li = document.createElement('li'); + li.setAttribute('class', 'msg reply-new'); + li.setAttribute('id', msg.rid); + li.innerHTML = msgContHtml; + li.addEventListener('click', newReply); + li.addEventListener('mouseover', newReply); + li.querySelector('a.msg-reply-link').addEventListener('click', function (e) { + showCommentForm(msg.mid, msg.rid); + e.preventDefault(); + }); + + killy.embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a'); + + document.getElementById('replies').appendChild(li); + + updateRepliesCounter(); +} + +function newReply(e) { + var li = e.target; + li.classList.remove('reply-new'); + li.removeEventListener('click', e); + li.removeEventListener('mouseover', e); + updateRepliesCounter(); +} + +function nextReply() { + var li = document.querySelector('#replies>li.reply-new'); + if (li) { + li.classList.remove('reply-new'); + li.removeEventListener('click', this); + li.children[0].scrollIntoView(); + updateRepliesCounter(); + } +} + +function updateRepliesCounter() { + var replies = document.querySelectorAll('#replies>li.reply-new').length; + var wsthread = document.getElementById('wsthread'); + if (replies) { + wsthread.textContent = replies; + wsthread.style.display = 'block'; + document.title = '[' + replies + '] ' + pageTitle; + } else { + wsthread.style.display = 'none'; + document.title = pageTitle; + } +} + +/******************************************************************************/ +/******************************************************************************/ +/******************************************************************************/ + +function postformListener(formEl, ev) { + if (ev.ctrlKey && (ev.keyCode == 10 || ev.keyCode == 13)) { + let form = formEl.closest('form'); + if (!form.onsubmit || form.onsubmit()) { + form.submit(); + } + } +} +function closeDialogListener(ev) { + ev = ev || window.event; + if (ev.keyCode == 27) { + closeDialog(); + } +} + +function newMessage(evt) { + document.querySelectorAll('#newmessage .dialogtxt').forEach(t => { + t.remove(); + }); + if (document.querySelector('#newmessage textarea').value.length == 0 + && document.querySelector('#newmessage .img').value.length == 0 + && !document.querySelector('#newmessage input[type="file"]')) { + document.querySelector('#newmessage').insertAdjacentHTML('afterbegin', `

${i18n('postForm.pleaseInputMessageText')}

`); + evt.preventDefault(); + } +} + +function showCommentForm(mid, rid) { + let reply = document.getElementById(rid); + let formTarget = reply.querySelector('div.msg-cont .msg-comment-target'); + if (formTarget) { + let formHtml = ` +
+ + +
+
+ +
${evilIcon('ei-camera')}
+
+ +
+
`; + formTarget.insertAdjacentHTML('afterend', formHtml); + formTarget.remove(); + + let form = reply.querySelector('form'); + let submitButton = form.querySelector('input[type="submit"]'); + + let attachButton = form.querySelector('.msg-comment .attach-photo'); + attachButton.addEventListener('click', e => attachCommentPhoto(e.target)); + + let textarea = form.querySelector('.msg-comment textarea'); + textarea.addEventListener('keypress', e => postformListener(e.target, e)); + autosize(textarea); + + let validateMessage = () => { + let len = textarea.value.length; + if (len > 4096) { return 'Message is too long'; } + return ''; + }; + form.addEventListener('submit', e => { + let validationResult = validateMessage(); + if (validationResult) { + e.preventDefault(); + alert(validationResult); + return false; + } + submitButton.disabled = true; + }); + } + reply.querySelector('.msg-comment textarea').focus(); +} + +function attachInput() { + let inp = document.createElement('input'); + inp.setAttribute('type', 'file'); + inp.setAttribute('name', 'attach'); + inp.setAttribute('accept', 'image/jpeg,image/png'); + inp.style.visibility = 'hidden'; + return inp; +} + +function attachCommentPhoto(div) { + let input = div.querySelector('input'); + if (input) { + input.remove(); + div.classList.remove('attach-photo-active'); + } else { + let newInput = attachInput(); + newInput.addEventListener('change', function () { + div.classList.add('attach-photo-active'); + }); + newInput.click(); + div.appendChild(newInput); + } +} + +function attachMessagePhoto(div) { + var f = div.closest('form'), + finput = f.querySelector('input[type="file"]'); + if (!finput) { + var inp = attachInput(); + inp.style.float = 'left'; + inp.style.width = 0; + inp.style.height = 0; + inp.addEventListener('change', function () { + div.textContent = i18n('postForm.upload') + ' (✓)'; + }); + f.appendChild(inp); + inp.click(); + } else { + finput.remove(); + div.textContent = i18n('postForm.upload'); + } +} + +function showMessageLinksDialog(mid, rid) { + let hlink = window.location.protocol + '//juick.com/' + mid; + let mlink = '#' + mid; + if (rid > 0) { + hlink += '#' + rid; + mlink += '/' + rid; + } + let hlinkenc = encodeURIComponent(hlink); + let html = ` +
+ ${i18n('shareDialog.linkToMessage')}:
${hlink}
+ ${i18n('shareDialog.messageNumber')}:
${mlink}
+ ${i18n('shareDialog.share')}: + +
`; + + openDialog(html); +} + +function showPhotoDialog(fname) { + let width = window.innerWidth; + let height = window.innerHeight; + let minDimension = (width < height) ? width : height; + if (minDimension < 640) { + return true; // no dialog, open the link + } else if (minDimension < 1280) { + openDialog(``, true); + return false; + } else { + openDialog(``, true); + return false; + } +} + +function openPostDialog() { + let newmessageTemplate = ` +
+ +
+ + ${i18n('postForm.or')} ${i18n('postForm.upload')}
+
+ +
+
+ `; + return openDialog(newmessageTemplate); +} + +function openDialog(html, image) { + var dialogHtml = ` +
+
+
+
+
${evilIcon('ei-close')}
+
+ ${html} +
+
`; + 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 = ` +
+

${i18n('loginDialog.pleaseIntroduceYourself')}:

+ ${evilIcon('ei-envelope')}${i18n('loginDialog.email')} + ${evilIcon('ei-sc-facebook')}${i18n('loginDialog.facebook')} + ${evilIcon('ei-sc-vk')}${i18n('loginDialog.vk')} +

${i18n('loginDialog.registeredAlready')}

+
+
+
+ +
+
`; + openDialog(html); + return false; +} + +/******************************************************************************/ + +function resultMessage(str) { + var result = document.createElement('p'); + result.textContent = str; + return result; +} + +function likeMessage(e, mid) { + if (confirm(i18n('message.likeThisMessage?'))) { + fetch('//api.juick.com/like?mid=' + mid + + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { + method: 'POST', + credentials: 'same-origin' + }) + .then(function (response) { + if (response.ok) { + e.closest('article').appendChild(resultMessage('OK!')); + } + }) + .catch(function () { + e.closest('article').appendChild(resultMessage(i18n('error.error'))); + }); + } + return false; +} + +/******************************************************************************/ + +function setPopular(e, mid, popular) { + fetch('//api.juick.com/messages/set_popular?mid=' + mid + + '&popular=' + popular + + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(function () { + e.closest('article').append(resultMessage('OK!')); + }); + return false; +} + +function setPrivacy(e, mid) { + fetch('//api.juick.com/messages/set_privacy?mid=' + mid + + '&hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(function () { + e.closest('article').append(resultMessage('OK!')); + }); + return false; +} + +function getTags() { + fetch('//api.juick.com/tags?hash=' + document.getElementById('body').getAttribute('data-hash'), { + credentials: 'same-origin' + }) + .then(response => { + return response.json(); + }) + .then(json => { + let tags = json.map(t => t.tag); + let input = document.getElementById('tags_input'); + new Awesomplete(input, { list: tags }); + }); + return false; +} + +function addTag(tag) { + document.forms['postmsg'].body.value = '*' + tag + ' ' + document.forms['postmsg'].body.value; + return false; +} + +/******************************************************************************/ + +function ready(fn) { + if (document.readyState != 'loading') { + fn(); + } else { + document.addEventListener('DOMContentLoaded', fn); + } +} + +ready(function () { + document.querySelectorAll('textarea').forEach((ta) => { + autosize(ta); + }); + + var insertPMButtons = function (e) { + e.target.classList.add('narrowpm'); + e.target.parentNode.insertAdjacentHTML('afterend', ''); + e.target.removeEventListener('click', insertPMButtons); + e.preventDefault(); + }; + document.querySelectorAll('textarea.replypm').forEach(function (e) { + e.addEventListener('click', insertPMButtons); + e.addEventListener('keypress', function (e) { + postformListener(e.target, e); + }); + }); + document.querySelectorAll('#postmsg textarea').forEach(function (e) { + e.addEventListener('keypress', function (e) { + postformListener(e.target, e); + }); + }); + + var content = document.getElementById('content'); + if (content) { + var pageMID = content.getAttribute('data-mid'); + if (pageMID > 0) { + document.querySelectorAll('li.msg').forEach(li => { + let showReplyFormBtn = li.querySelector('.a-thread-comment'); + if (showReplyFormBtn) { + showReplyFormBtn.addEventListener('click', function (e) { + showCommentForm(pageMID, li.id); + e.preventDefault(); + }); + } + }); + let opMessage = document.querySelector('.msgthread'); + if (opMessage) { + let replyTextarea = opMessage.querySelector('textarea.reply'); + if (replyTextarea) { + replyTextarea.addEventListener('focus', e => showCommentForm(pageMID, 0)); + replyTextarea.addEventListener('keypress', e => postformListener(e.target, e)); + if (!window.location.hash) { + replyTextarea.focus(); + } + } + } + } + } + + var postmsg = document.getElementById('postmsg'); + if (postmsg) { + document.querySelectorAll('a').filter(t => t.href.indexOf('?') >= 0).forEach(t => { + t.addEventListener('click', e => { + let params = new URLSearchParams(t.href.slice(t.href.indexOf('?') + 1)); + if (params.has('tag')) { + addTag(params.get('tag')); + e.preventDefault(); + } + }); + }); + } + + document.querySelectorAll('.msg-menu').forEach(function (el) { + el.addEventListener('click', function (e) { + var reply = e.target.closest('li'); + var rid = reply ? parseInt(reply.id) : 0; + var message = e.target.closest('section'); + var mid = message.getAttribute('data-mid') || e.target.closest('article').getAttribute('data-mid'); + showMessageLinksDialog(mid, rid); + e.preventDefault(); + }); + }); + document.querySelectorAll('.l .a-privacy').forEach(function (e) { + e.addEventListener('click', function (e) { + setPrivacy( + e.target, + e.target.closest('article').getAttribute('data-mid')); + e.preventDefault(); + }); + }); + document.querySelectorAll('.ir a[data-fname], .msg-media a[data-fname]').forEach(function (el) { + el.addEventListener('click', function (e) { + let fname = e.target.closest('[data-fname]').getAttribute('data-fname'); + if (!showPhotoDialog(fname)) { + e.preventDefault(); + } + }); + }); + document.querySelectorAll('.social a').forEach(function (e) { + e.addEventListener('click', function (e) { + openSocialWindow(e.target); + e.preventDefault(); + }); + }); + var username = document.getElementById('username'); + if (username) { + username.addEventListener('blur', function () { + checkUsername(); + }); + } + + document.querySelectorAll('.l .a-like').forEach(function (e) { + e.addEventListener('click', function (e) { + likeMessage( + e.target, + e.target.closest('article').getAttribute('data-mid')); + e.preventDefault(); + }); + }); + document.querySelectorAll('.a-login').forEach(function (el) { + el.addEventListener('click', function (e) { + openDialogLogin(); + e.preventDefault(); + }); + }); + var unfoldall = document.getElementById('unfoldall'); + if (unfoldall) { + unfoldall.addEventListener('click', function (e) { + document.querySelectorAll('#replies>li').forEach(function (e) { + e.style.display = 'block'; + }); + document.querySelectorAll('#replies .msg-comments').forEach(function (e) { + e.style.display = 'none'; + }); + e.preventDefault(); + }); + } + document.querySelectorAll('article').forEach(function (article) { + if (Array.prototype.some.call( + article.querySelectorAll('.msg-tags a'), + function (a) { + return a.textContent === 'NSFW'; + } + )) { + article.classList.add('nsfw'); + } + }); + initWS(); + + window.addEventListener('pagehide', wsShutdown); + + killy.embedAll(); + var elSelector = 'header', + elClassHidden = 'header--hidden', + elClassBackground = 'header--background', + throttleTimeout = 500, + element = document.querySelector(elSelector); + + if (element) { + + var dHeight = 0, + wHeight = 0, + wScrollCurrent = 0, + wScrollBefore = 0, + wScrollDiff = 0, + + throttle = function (delay, fn) { + var last, deferTimer; + return function () { + var context = this, args = arguments, now = +new Date; + if (last && now < last + delay) { + clearTimeout(deferTimer); + deferTimer = setTimeout( + function () { + last = now; + fn.apply(context, args); + }, + delay); + } else { + last = now; + fn.apply(context, args); + } + }; + }; + + window.addEventListener('scroll', throttle(throttleTimeout, function () { + dHeight = document.body.offsetHeight; + wHeight = window.innerHeight; + wScrollCurrent = window.pageYOffset; + wScrollDiff = wScrollBefore - wScrollCurrent; + + if (wScrollCurrent <= 0) { + // scrolled to the very top; element sticks to the top + element.classList.remove(elClassHidden); + element.classList.remove(elClassBackground); + } else if (wScrollDiff > 0 && element.classList.contains(elClassHidden)) { + // scrolled up; element slides in + element.classList.remove(elClassHidden); + element.classList.add(elClassBackground); + } else if (wScrollDiff < 0) { + // scrolled down + if (wScrollCurrent + wHeight >= dHeight && element.classList.contains(elClassHidden)) { + // scrolled to the very bottom; element slides in + element.classList.remove(elClassHidden); + element.classList.add(elClassBackground); + } else { + // scrolled down; element slides out + element.classList.add(elClassHidden); + } + } + + wScrollBefore = wScrollCurrent; + })); + } +}); diff --git a/juick-server/src/main/assets/style.css b/juick-server/src/main/assets/style.css new file mode 100644 index 00000000..d7cd2223 --- /dev/null +++ b/juick-server/src/main/assets/style.css @@ -0,0 +1,952 @@ +/* #region generic */ + +html, +body, +div, +h1, +h2, +ul, +li, +p, +form, +input, +textarea, +pre { + margin: 0; + padding: 0; +} +html, +input, +textarea { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; + font-size: 12pt; + -webkit-font-smoothing: subpixel-antialiased; +} +h1, +h2 { + font-weight: normal; +} +ul { + list-style-type: none; +} +a { + color: #069; + text-decoration: none; +} +img, +hr { + border: none; +} +hr { + background: #CCC; + height: 1px; + margin: 10px 0; +} +pre { + background: #222; + color: #0f0; + overflow-x: auto; + padding: 6px; + white-space: pre; +} +pre::selection { + background: #0f0; + color: #222; +} +pre::-moz-selection { + background: #0f0; + color: #222; +} +.u { + text-decoration: underline; +} + +/* #endregion */ + +/* #region overall layout */ + +html { + background: #f8f8f8; + color: #222; +} +#wrapper { + margin: 0 auto; + width: 1000px; + margin-top: 52px; +} +#column { + float: left; + margin-left: 10px; + overflow: hidden; + padding-top: 10px; + width: 240px; +} +#content { + float: right; + margin: 12px 0 0 0; + width: 728px; +} +#minimal_content { + margin: 0 auto; + min-width: 310px; + width: auto; +} +*::selection { + background: #006699; + color: #fff; +} +body > header { + position: fixed; + top: 0; + width: 100%; + z-index: 10; + transition-duration: 0.5s; + transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); + transition-property: transform; +} +@supports (backdrop-filter: blur(10px)) { + body > header--background { + background: rgba(255, 255, 255, 0.8); + backdrop-filter: blur(10px); + } +} +#header_wrapper { + margin: 0 auto; + width: 1000px; + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + padding: 4px; +} +.header--background { + box-shadow: 0 0 3px rgba(0, 0, 0, 0.28); + background: #fff; +} +.header--hidden { + transform: translateY(-100%); +} +#footer { + clear: both; + color: #999; + font-size: 10pt; + margin: 40px; + padding: 10px 0; +} + +@media screen and (max-width: 850px) { + body { + text-size-adjust: 100%; + } + body, + #wrapper, + #topwrapper, + #content, + #footer { + float: none; + margin: 0 auto; + min-width: 310px; + width: auto; + } + #wrapper { + margin-top: 50px; + } + body > header { + margin-bottom: 15px; + } + #column { + float: none; + margin: 0 10px; + padding-top: 0; + width: auto; + } +} + +/* #endregion */ + +/* #region header internals */ + +#logo { + height: 36px; + width: 110px; +} +#logo a { + background: url("logo@2x.png") no-repeat; + background-size: cover; + border: 0; + display: block; + height: 36px; + overflow: hidden; + text-indent: 100%; + white-space: nowrap; + width: 110px; +} +#global { + display: flex; +} +#global a { + color: #888; + display: inline-block; + font-size: 13pt; + padding: 14px 6px; +} +#global li { + display: inline-block; +} +#ctitle a { + padding: 14px; +} +#global li:hover, +#ctitle a:hover, +.l a:hover { + background-color: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + cursor: pointer; + transition: box-shadow 0.2s ease-in; +} +#search input { + background: #FFF; + border: 1px solid #ccc; + outline: none !important; + padding: 4px; + -webkit-appearance: none; + border-radius: 0; +} + +/* #endregion */ + +/* #region left column internals */ + +.toolbar { + border-top: 1px solid #CCC; +} + +#column ul, +#column p, +#column hr { + margin: 10px 0; +} +#column li > a { + display: block; + height: 100%; + padding: 6px; +} +#column li > a:hover { + background-color: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + transition: background-color 0.2s ease-in; +} +#column .margtop { + margin-top: 15px; +} + +#column .tags { + background: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + line-height: 140%; + padding: 6px; + text-align: justify; +} +#column .inp { + background: #fff; + border: 1px solid #ddddd5; + outline: none !important; + padding: 4px; + width: 222px; +} +#column .tags h4 { + background: #eee; + border: 1px solid #eee; + color: #888; + display: block; + text-align: center; +} +#ctitle { + font-size: 14pt; +} +#ctitle img { + margin-right: 5px; + vertical-align: middle; + width: 48px; +} +#ustats li { + font-size: 10pt; + margin: 3px 0; +} +#column table.iread { + width: 100%; +} +#column table.iread td { + text-align: center; +} +#column table.iread img { + height: 48px; + width: 48px; +} + +/* #endregion */ + +/* #region main content */ +#content > p, +#content > h1, +#content > h2, +#minimal_content > p, +#minimal_content > h1, +#minimal_content > h2 { + margin: 1em 0; +} +.page { + background: #eee; + padding: 6px; + text-align: center; +} + +.page a { + color: #888; +} + +/* #endregion */ + +/* #region article, message internals */ + +article { + background: #fff; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + line-height: 140%; + margin-bottom: 10px; + padding: 20px; +} +article time { + color: #999; + font-size: 10pt; +} +article p { + clear: left; + margin: 5px 0 15px 0; + word-wrap: break-word; + overflow-wrap: break-word; +} +article .ir { + text-align: center; +} +article .ir a { + cursor: zoom-in; + display: block; +} +article .ir img { + max-width: 100%; +} +article > nav.l, +.msg-cont > nav.l { + border-top: 1px solid #eee; + display: flex; + justify-content: space-around; + font-size: 10pt; +} +article > nav.l a, +.msg-cont > nav.l a { + color: #888; + margin-right: 15px; +} +article .likes { + padding-left: 20px; +} +article .replies { + margin-left: 18px; +} +article .tags { + margin-top: 3px; +} +.msg-tags { + margin-top: 12px; + min-height: 1px; +} +article .tags > a, +.badge, +.msg-tags > a { + background: #eee; + border: 1px solid #eee; + color: #888; + display: inline-block; + font-size: 10pt; + margin-bottom: 5px; + margin-right: 5px; + padding: 0 10px; +} +.l .msg-button { + align-items: center; + display: flex; + flex-basis: 0; + flex-direction: column; + flex-grow: 1; + padding-top: 12px; +} +.l .msg-button-icon { + font-weight: bold; +} +.msgthread { + margin-bottom: 0; +} +.msg-avatar { + float: left; + height: 48px; + margin-right: 10px; + width: 48px; +} +.msg-avatar img { + height: 48px; + vertical-align: top; + width: 48px; +} +.msg-cont { + background: #FFF; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.16); + line-height: 140%; + margin-bottom: 12px; + padding: 20px; + width: 640px; +} +.reply-new .msg-cont { + border-right: 5px solid #0C0; +} +.msg-ts { + font-size: small; + vertical-align: top; +} +.msg-ts, +.msg-ts > a { + color: #999; +} +.msg-txt { + clear: both; + margin: 0 0 12px; + padding-top: 10px; + word-wrap: break-word; + overflow-wrap: break-word; +} +.msg-media { + text-align: center; +} +.msg-links { + color: #999; + font-size: small; + margin: 5px 0 0 0; +} +.msg-comments { + color: #AAA; + font-size: small; + margin-top: 10px; + overflow: hidden; + text-indent: 10px; +} +.ta-wrapper { + border: 1px solid #DDD; + display: flex; + flex-grow: 1; +} +.msg-comment { + display: flex; + width: 100%; + margin-top: 10px; +} +.msg-comment-hidden { + display: none; +} +.msg-comment textarea { + border: 0; + flex-grow: 1; + outline: none !important; + padding: 4px; + resize: vertical; + vertical-align: top; +} +.attach-photo { + cursor: pointer; +} +.attach-photo-active { + color: green; +} +.msg-comment input { + align-self: flex-start; + background: #EEE; + border: 1px solid #CCC; + color: #999; + margin: 0 0 0 6px; + position: -webkit-sticky; + position: sticky; + top: 70px; + vertical-align: top; + width: 50px; +} +.msg-recomms { + color: #AAA; + font-size: small; + margin-top: 10px; + overflow: hidden; + text-indent: 10px; +} +#replies .msg-txt, +#private-messages .msg-txt { + margin: 0; +} +.title2 { + background: #fff; + margin: 20px 0; + padding: 10px 20px; + width: 640px; +} +.title2-right { + float: right; + line-height: 24px; +} +#content .title2 h2 { + font-size: x-large; + margin: 0; +} + +@media screen and (max-width: 850px) { + #header_wrapper { + width: auto; + } + #global { + justify-content: space-around; + flex-grow: 1; + } + #search { + padding: 4px; + } + article { + overflow: auto; + } + article p { + margin: 10px 0 8px 0; + } + .msg, + .msg-cont { + min-width: 280px; + width: auto; + } + .msg-cont { + margin: 8px 0; + } + .msg-media { + overflow: auto; + } + .title2 h2 { + font-size: large; + } + .msg-comment { + flex-direction: column; + } + .msg-comment input { + align-self: flex-end; + margin: 6px 0 0 0; + width: 100px; + } +} + +@media screen and (max-width: 480px) { + #wrapper { + margin-top: 104px; + } + #search { + display: none; + } + #global a { + padding: 14px 2px; + font-size: 11pt; + } + .msg-cont > nav.l, + article > nav.l { + font-size: 9pt; + } + .msg-txt { + padding-top: 5px; + } + .title2 { + font-size: 11pt; + width: auto; + } + #content .title2 h2 { + font-size: 11pt; + } + .title2-right { + line-height: initial; + } +} + +/* #endregion */ + +/* #region user-generated texts */ + +q:before, +q:after { + content: ""; +} +q, +blockquote { + border-left: 3px solid #CCC; + color: #666; + display: block; + margin: 10px 0 10px 10px; + padding-left: 10px; +} + +/* #endregion */ + +/* #region new message form internals */ + +#newmessage { + background: #E5E5E0; + margin-bottom: 20px; + padding: 15px; +} +#newmessage textarea { + border: 1px solid #CCC; + box-sizing: border-box; + margin: 0 0 5px 0; + margin-top: 20px; + max-height: 6em; + min-width: 280px; + padding: 4px; + width: 100%; +} +#newmessage input { + border: 1px solid #CCC; + margin: 5px 0; + padding: 2px 4px; +} +#newmessage .img { + width: 500px; +} +#newmessage .tags { + width: 500px; +} +#newmessage .subm { + background: #EEEEE5; + width: 150px; +} +@media screen and (max-width: 850px) { + #newmessage .img, + #newmessage .tags { + width: 100%; + } +} + +/* #endregion */ + +/* #region user lists */ + +.users { + margin: 10px 0; + width: 100%; + display: flex; + flex-wrap: wrap; +} +.users > span { + overflow: hidden; + padding: 6px 0; + width: 200px; +} +.users img { + height: 32px; + margin-right: 6px; + vertical-align: middle; + width: 32px; +} + +/* #endregion */ + +/* #region signup form */ + +.signup-h1 > img { + margin-right: 10px; + vertical-align: middle; +} +.signup-h1 { + font-size: x-large; + margin: 20px 0 10px 0; +} +.signup-h2 { + font-size: large; + margin: 10px 0 5px 0; +} +.signup-hr { + margin: 20px 0; +} + +/* #endregion */ + +/* #region PM */ + +.newpm { + margin: 20px 60px 30px 60px; +} +.newpm textarea { + resize: vertical; + width: 100%; +} +.newpm-send input { + width: 100px; +} + +/* #endregion */ + +/* #region popup dialog (lightbox) */ + +#dialogb { + background: #222; + height: 100%; + left: 0; + opacity: 0.6; + position: fixed; + top: 0; + width: 100%; + z-index: 10; +} +#dialogt { + height: 100%; + left: 0; + position: fixed; + top: 0; + width: 100%; + z-index: 10; + display: flex; + align-items: center; + justify-content: center; +} +#dialogw { + z-index: 11; + max-width: 96%; + max-height: calc(100% - 100px); + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} +#dialogw a { + display: block; +} +#dialogw img { + max-height: 100%; + max-height: 90vh; + max-width: 100%; +} +#dialog_header { + width: 100%; + height: 44px; + position: fixed; + display: flex; + flex-direction: row-reverse; + align-items: center; +} +.header_image { + background: rgba(0, 0, 0, 0.28); +} +#dialogc { + cursor: pointer; + color: #ccc; + padding-right: 6px; +} +.dialoglogin { + background: #fff; + padding: 25px; + width: 300px; +} +.dialog-opened { + overflow: hidden; +} +#signemail, +#signfb, +#signvk { + display: block; + line-height: 32px; + margin: 10px 0; + text-decoration: none; + width: 100%; +} +#signvk { + margin-bottom: 30px; +} +.dialoglogin form { + margin-top: 7px; +} +.signinput, +.signsubmit { + border: 1px solid #CCC; + margin: 3px 0; + padding: 3px; +} +.signinput { + width: 292px; +} +.signsubmit { + width: 70px; +} +.dialogshare { + background: #fff; + min-width: 300px; + overflow: auto; + padding: 20px; +} +.dialogl { + background: #fff; + border: 1px solid #DDD; + margin: 3px 0 20px; + padding: 5px; +} +.dialogshare li { + float: left; + margin: 5px 10px 0 0; +} +.dialogshare a { + display: block; +} +.dialogtxt { + background: #fff; + padding: 20px; +} + +@media screen and (max-width: 480px) { + .dialog-opened { + position: fixed; + width: 100%; + } +} + +/* #endregion */ + +/* #region misc */ + +#wsthread { + background: #CCC; + bottom: 20px; + cursor: pointer; + display: none; + padding: 5px 10px; + position: fixed; + right: 20px; +} +.sharenew { + display: inline-block; + line-height: 32px; + min-height: 32px; + min-width: 200px; + padding: 0 12px 0 37px; +} +.icon { + margin-top: -2px; + vertical-align: middle; +} +.icon--ei-link { + margin-top: -1px; +} +.icon--ei-comment { + margin-top: -5px; +} +.newmessage { + /* textarea on the /post page */ + border: 1px solid #DDD; + padding: 2px; + resize: vertical; + width: 100%; +} + +/* #endregion */ + +/* #region footer internals */ + +#footer-social { + float: left; +} +#footer-social a { + border: 0; + display: inline-block; +} +#footer-left { + margin-left: 286px; + margin-right: 350px; +} +#footer-right { + float: right; +} + +@media screen and (max-width: 850px) { + #footer { + margin: 0 10px; + } + #footer div { + float: none; + margin: 10px 0; + } +} + +/* #endregion */ + +/* #region settings */ + +fieldset { + border: 1px dotted #ccc; + margin-top: 25px; +} + +/* #endregion */ + +/* #region embeds */ + +.embedContainer { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: center; + padding: 0; + margin: 30px -3px 15px -3px; +} +.embedContainer > * { + box-sizing: border-box; + flex-grow: 1; + margin: 3px; + min-width: 49%; +} +.embedContainer > .compact { + flex-grow: 0; +} +.embedContainer .picture img { + display: block; +} +.embedContainer img, +.embedContainer video { + max-width: 100%; + max-height: 80vh; +} +.embedContainer > .audio, +.embedContainer > .youtube { + min-width: 90%; +} +.embedContainer audio { + width: 100%; +} +.embedContainer iframe { + overflow: hidden; + resize: vertical; + display: block; +} + +/* #endregion */ + +/* #region nsfw */ + +article.nsfw .embedContainer img, +article.nsfw .embedContainer video, +article.nsfw .embedContainer iframe, +article.nsfw .ir img { + opacity: 0.1; +} +article.nsfw .embedContainer img:hover, +article.nsfw .embedContainer video:hover, +article.nsfw .embedContainer iframe:hover, +article.nsfw .ir img:hover { + opacity: 1; +} + +/* #endregion */ diff --git a/juick-server/src/main/java/com/juick/server/TelegramBotManager.java b/juick-server/src/main/java/com/juick/server/TelegramBotManager.java index 9f5be577..ac2febf7 100644 --- a/juick-server/src/main/java/com/juick/server/TelegramBotManager.java +++ b/juick-server/src/main/java/com/juick/server/TelegramBotManager.java @@ -46,6 +46,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Component; import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; diff --git a/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java b/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java new file mode 100644 index 00000000..2e484e3d --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/api/ApiSocialLogin.java @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.scribejava.apis.FacebookApi; +import com.github.scribejava.apis.VkontakteApi; +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.OAuth2AccessToken; +import com.github.scribejava.core.model.OAuthRequest; +import com.github.scribejava.core.model.Verb; +import com.github.scribejava.core.oauth.OAuth20Service; +import com.juick.facebook.User; +import com.juick.server.util.HttpBadRequestException; +import com.juick.service.CrosspostService; +import com.juick.service.EmailService; +import com.juick.service.TelegramService; +import com.juick.service.UserService; +import com.juick.vk.UsersResponse; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.ExecutionException; + +/** + * + * @author Ugnich Anton + */ +@Controller +public class ApiSocialLogin { + + private static final Logger logger = LoggerFactory.getLogger(ApiSocialLogin.class); + + @Value("${facebook_appid:appid}") + private String FACEBOOK_APPID; + @Value("${facebook_secret:secret}") + private String FACEBOOK_SECRET; + private static final String FACEBOOK_REDIRECT = "https://api.juick.com/_fblogin"; + private static final String VK_REDIRECT = "https://api.juick.com/_vklogin"; + private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json"; + @Inject + private ObjectMapper jsonMapper; + private ServiceBuilder facebookBuilder, twitterBuilder, vkBuilder; + + @Value("${twitter_consumer_key:appid}") + private String twitterConsumerKey; + @Value("${twitter_consumer_secret:secret}") + private String twitterConsumerSecret; + @Value("${vk_appid:appid}") + private String VK_APPID; + @Value("${vk_secret:secret}") + private String VK_SECRET; + @Value("${telegram_token:secret}") + private String telegramToken; + + @Inject + private CrosspostService crosspostService; + @Inject + private UserService userService; + @Inject + private EmailService emailService; + @Inject + private TelegramService telegramService; + + @PostConstruct + public void init() { + facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); + twitterBuilder = new ServiceBuilder(twitterConsumerKey); + vkBuilder = new ServiceBuilder(VK_APPID); + } + + @GetMapping("/api/_fblogin") + protected String doFacebookLogin(@RequestParam(required = false) String code, + @RequestParam(required = false) String state) throws IOException, ExecutionException, InterruptedException { + if (StringUtils.isBlank(code)) { + String fbstate = UUID.randomUUID().toString(); + crosspostService.addFacebookState(fbstate, state); + OAuth20Service facebookAuthService = facebookBuilder + .apiSecret(FACEBOOK_SECRET) + .callback(FACEBOOK_REDIRECT) + .scope("email") + .state(fbstate) + .build(FacebookApi.instance()); + return "redirect:" + facebookAuthService.getAuthorizationUrl(); + } + + String redirectUrl = crosspostService.verifyFacebookState(state); + + if (StringUtils.isEmpty(redirectUrl)) { + logger.error("state is missing"); + throw new HttpBadRequestException(); + } + OAuth20Service facebookService = facebookBuilder + .apiKey(FACEBOOK_APPID) + .apiSecret(FACEBOOK_SECRET) + .callback(FACEBOOK_REDIRECT) + .scope("email") + .state(state) + .build(FacebookApi.instance()); + OAuth2AccessToken token = facebookService.getAccessToken(code); + final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v2.10/me?fields=id,name,link,verified,email"); + facebookService.signRequest(token, meRequest); + String graph = facebookService.execute(meRequest).getBody(); + if (StringUtils.isBlank(graph)) { + logger.error("FACEBOOK GRAPH ERROR"); + throw new HttpBadRequestException(); + } + User fb = jsonMapper.readValue(graph, User.class); + long fbID = NumberUtils.toLong(fb.getId(), 0); + if (fbID == 0 || StringUtils.isBlank(fb.getName()) || StringUtils.isBlank(fb.getLink())) { + logger.error("Missing required fields, id: {}, name: {}, link: {}", fbID, fb.getName(), fb.getLink()); + throw new HttpBadRequestException(); + } + + int uid = crosspostService.getUIDbyFBID(fbID); + if (uid > 0) { + if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName(), fb.getLink())) { + logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken()); + throw new HttpBadRequestException(); + } + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); + uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid)); + return "redirect:" + uriComponentsBuilder.build().toUriString(); + } else if (fb.getVerified()) { + if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName(), fb.getLink())) { + if (StringUtils.isNotEmpty(fb.getEmail())) { + logger.info("found {} for facebook user {}", fb.getEmail(), fb.getLink()); + Integer userId = crosspostService.getUIDbyFBID(fbID); + if (!emailService.getEmails(userId, false).contains(fb.getEmail())) { + emailService.addEmail(userId, fb.getEmail()); + } + } + logger.info("email not found for facebook user {}", fb.getLink()); + throw new HttpBadRequestException(); + } + return "redirect:/signup?type=fb&hash=" + state; + } else { + logger.error("Facebook account is not verified, id: {}", fbID); + throw new HttpBadRequestException(); + } + }/* + @GetMapping("/_twitter") + protected void doTwitterLogin(HttpServletRequest request, HttpServletResponse response) + throws IOException, ExecutionException, InterruptedException { + String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY; + String verifier = request.getParameter("oauth_verifier"); + Cookie[] cookies = request.getCookies(); + for (Cookie cookie : cookies) { + if (cookie.getName().equals("hash")) { + hash = cookie.getValue(); + } + if (cookie.getName().equals("request_token")) { + request_token = cookie.getValue(); + } + if (cookie.getName().equals("request_token_secret")) { + request_token_secret = cookie.getValue(); + } + } + com.juick.User user = UserUtils.getCurrentUser(); + OAuth10aService oAuthService = twitterBuilder + .apiSecret(twitterConsumerSecret) + .callback("http://juick.com/_twitter") + .build(TwitterApi.instance()); + + if (request_token.isEmpty() && request_token_secret.isEmpty() + && (verifier == null || verifier.isEmpty())) { + OAuth1RequestToken requestToken = oAuthService.getRequestToken(); + String authUrl = oAuthService.getAuthorizationUrl(requestToken); + response.addCookie(new Cookie("request_token", requestToken.getToken())); + response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret())); + response.setStatus(HttpServletResponse.SC_FOUND); + response.setHeader("Location", authUrl); + } else { + if (verifier != null && verifier.length() > 0) { + OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret); + OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier); + OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL); + oAuthService.signRequest(accessToken, oAuthRequest); + com.juick.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(), + com.juick.twitter.User.class); + if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(), + twitterUser.getScreenName())) { + response.setStatus(HttpServletResponse.SC_FOUND); + response.setHeader("Location", "http://juick.com/settings"); + } else { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + } + }*/ + @GetMapping("/api/_vklogin") + protected String doVKLogin(@RequestParam(required = false) String code, + @RequestParam String state) throws IOException, ExecutionException, InterruptedException { + if (StringUtils.isBlank(code)) { + String vkstate = UUID.randomUUID().toString(); + crosspostService.addVKState(vkstate, state); + OAuth20Service vkAuthService = vkBuilder + .apiSecret(VK_SECRET) + .scope("friends,wall,offline") + .state(vkstate) + .callback(VK_REDIRECT) + .build(VkontakteApi.instance()); + return "redirect:" + vkAuthService.getAuthorizationUrl(); + } + + String redirectUrl = crosspostService.verifyVKState(state); + if (StringUtils.isBlank(redirectUrl)) { + logger.error("state is missing"); + throw new HttpBadRequestException(); + } + + OAuth20Service vkService = vkBuilder + .apiKey(VK_APPID) + .apiSecret(VK_SECRET) + .build(VkontakteApi.instance()); + OAuth2AccessToken token = vkService.getAccessToken(code); + + OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73"); + vkService.signRequest(token, meRequest); + String graph = vkService.execute(meRequest).getBody(); + + com.juick.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0); + String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName(); + String vkLink = jsonUser.getScreenName(); + + if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) { + logger.error("vk user error"); + throw new HttpBadRequestException(); + } + + Long vkID = NumberUtils.toLong(jsonUser.getId(), 0); + int uid = crosspostService.getUIDbyVKID(vkID); + if (uid > 0) { + UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); + uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid)); + return "redirect:" + uriComponentsBuilder.build().toUriString(); + } else { + String loginhash = UUID.randomUUID().toString(); + if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) { + logger.error("create vk user error"); + throw new HttpBadRequestException(); + } + return "redirect:/signup?type=vk&hash=" + loginhash; + } + } + /* + @GetMapping("/_tglogin") + public String doDurovLogin(HttpServletRequest request, + @RequestParam Map params, + HttpServletResponse response) { + String dataCheckString = params.entrySet().stream() + .filter(p -> !p.getKey().equals("hash")) + .sorted(Map.Entry.comparingByKey()) + .map(p -> p.getKey() + "=" + p.getValue()) + .collect(Collectors.joining("\n")); + String hash = params.get("hash"); + byte[] secretKey = DigestUtils.sha256(telegramToken); + String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString); + if (hash.equals(resultString)) { + Long tgUser = Long.valueOf(params.get("id")); + int uid = telegramService.getUser(tgUser); + if (uid > 0) { + Cookie c = new Cookie("hash", userService.getHashByUID(uid)); + c.setMaxAge(50 * 24 * 60 * 60); + response.addCookie(c); + return Utils.getPreviousPageByRequest(request).orElse("redirect:/"); + } else { + String username = StringUtils.defaultString(params.get("username"), params.get("first_name")); + telegramService.createTelegramUser(tgUser, username); + return "redirect:/signup?type=durov&hash=" + userService.getSignUpHashByTelegramID(tgUser, username); + } + } else { + logger.warn("invalid tg hash {} for {}", resultString, hash); + } + throw new HttpBadRequestException(); + }*/ +} diff --git a/juick-server/src/main/java/com/juick/server/api/Index.java b/juick-server/src/main/java/com/juick/server/api/Index.java index 5ffa6341..0faf270f 100644 --- a/juick-server/src/main/java/com/juick/server/api/Index.java +++ b/juick-server/src/main/java/com/juick/server/api/Index.java @@ -46,7 +46,7 @@ public class Index { @Inject private XMPPServer xmpp; - @RequestMapping(value = { "/", "/ws/" }, method = RequestMethod.GET, headers = "Connection!=Upgrade") + @RequestMapping(value = { "/api/", "/ws/" }, method = RequestMethod.GET, headers = "Connection!=Upgrade") public ResponseEntity description() { URI redirectUri = ServletUriComponentsBuilder.fromCurrentRequestUri().path("/swagger-ui.html").build().toUri(); return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).location(redirectUri).build(); @@ -56,7 +56,7 @@ public class Index { public Status status() { return Status.getStatus(String.valueOf(wsHandler.getClients().size())); } - @RequestMapping(method = RequestMethod.GET, value = "/xmpp/status", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(method = RequestMethod.GET, value = "/api/xmpp-status", produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public XMPPStatus xmppStatus() { XMPPStatus status = new XMPPStatus(); if (xmpp != null) { diff --git a/juick-server/src/main/java/com/juick/server/api/Messages.java b/juick-server/src/main/java/com/juick/server/api/Messages.java index 2b171489..672f328f 100644 --- a/juick-server/src/main/java/com/juick/server/api/Messages.java +++ b/juick-server/src/main/java/com/juick/server/api/Messages.java @@ -71,7 +71,7 @@ public class Messages { // TODO: serialize image urls - @GetMapping("/home") + @GetMapping("/api/home") public ResponseEntity> getHome( @RequestParam(defaultValue = "0") int before_mid) { User visitor = UserUtils.getCurrentUser(); @@ -83,7 +83,7 @@ public class Messages { return FORBIDDEN; } - @GetMapping("/messages") + @GetMapping("/api/messages") public ResponseEntity> getMessages( @RequestParam(required = false) String uname, @RequestParam(name = "before_mid", defaultValue = "0") Integer before, @@ -142,7 +142,7 @@ public class Messages { } return ResponseEntity.ok(messagesService.getMessages(visitor, mids)); } - @DeleteMapping("/messages") + @DeleteMapping("/api/messages") public CommandResult deleteMessage(@RequestParam int mid, @RequestParam(required = false, defaultValue = "0") int rid) { User visitor = UserUtils.getCurrentUser(); if (rid > 0) { @@ -155,12 +155,12 @@ public class Messages { } throw new HttpBadRequestException(); } - @GetMapping("/messages/discussions") + @GetMapping("/api/messages/discussions") public List getDiscussions( @RequestParam(required = false, defaultValue = "0") Long to) { return messagesService.getMessages(UserUtils.getCurrentUser(), messagesService.getDiscussions(UserUtils.getCurrentUser().getUid(), to)); } - @GetMapping("/thread") + @GetMapping("/api/thread") public ResponseEntity> getThread( @RequestParam(defaultValue = "0") int mid) { User visitor = UserUtils.getCurrentUser(); @@ -183,7 +183,7 @@ public class Messages { } return NOT_FOUND; } - @GetMapping(value = "/thread/mark_read/{mid}-{rid}.gif", produces = MediaType.IMAGE_GIF_VALUE) + @GetMapping(value = "/api/thread/mark_read/{mid}-{rid}.gif", produces = MediaType.IMAGE_GIF_VALUE) public byte[] markThreadRead(@PathVariable int mid, @PathVariable int rid) throws IOException { User visitor = UserUtils.getCurrentUser(); if (!visitor.isAnonymous()) { diff --git a/juick-server/src/main/java/com/juick/server/api/Notifications.java b/juick-server/src/main/java/com/juick/server/api/Notifications.java index e068cbe9..0b34f275 100644 --- a/juick-server/src/main/java/com/juick/server/api/Notifications.java +++ b/juick-server/src/main/java/com/juick/server/api/Notifications.java @@ -67,7 +67,7 @@ public class Notifications { } @ApiIgnore - @RequestMapping(value = "/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/notifications", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public ResponseEntity> doGet( @RequestParam(required = false, defaultValue = "0") int uid, @RequestParam(required = false, defaultValue = "0") int mid, @@ -101,7 +101,7 @@ public class Notifications { } @ApiIgnore - @RequestMapping(value = "/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/notifications", method = RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Status doDelete( @RequestBody List list) { User visitor = UserUtils.getCurrentUser(); @@ -129,7 +129,7 @@ public class Notifications { } @ApiIgnore - @RequestMapping(value = "/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/notifications", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Status doPut( @RequestBody List list) throws IOException { User visitor = UserUtils.getCurrentUser(); @@ -155,7 +155,7 @@ public class Notifications { } @Deprecated - @RequestMapping(value = "/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/android/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Status doAndroidRegister( @RequestParam(name = "regid") String regId) { User visitor = UserUtils.getCurrentUser(); @@ -167,14 +167,14 @@ public class Notifications { } @Deprecated - @RequestMapping(value = "/android/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/android/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Status doAndroidUnRegister(@RequestParam(name = "regid") String regId) { pushQueriesService.deleteGCMToken(regId); return Status.OK; } @Deprecated - @RequestMapping(value = "/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/winphone/register", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Status doWinphoneRegister( Principal principal, @RequestParam(name = "url") String regId) { @@ -184,7 +184,7 @@ public class Notifications { } @Deprecated - @RequestMapping(value = "/winphone/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/winphone/unregister", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public Status doWinphoneUnRegister(@RequestParam(name = "url") String regId) { pushQueriesService.deleteMPNSToken(regId); return Status.OK; diff --git a/juick-server/src/main/java/com/juick/server/api/PM.java b/juick-server/src/main/java/com/juick/server/api/PM.java index cbd70bed..d3619662 100644 --- a/juick-server/src/main/java/com/juick/server/api/PM.java +++ b/juick-server/src/main/java/com/juick/server/api/PM.java @@ -47,7 +47,7 @@ public class PM { @Inject private ApplicationEventPublisher applicationEventPublisher; - @RequestMapping(value = "/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/pm", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public List doGetPM( @RequestParam(required = false) String uname) { User visitor = UserUtils.getCurrentUser(); @@ -66,7 +66,7 @@ public class PM { return pmQueriesService.getPMMessages(visitor.getUid(), uid); } - @RequestMapping(value = "/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/pm", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public com.juick.Message doPostPM( @RequestParam String uname, @RequestParam String body) { @@ -98,7 +98,7 @@ public class PM { } throw new HttpBadRequestException(); } - @RequestMapping(value = "groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/groups_pms", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public PrivateChats doGetGroupsPMs( @RequestParam(defaultValue = "5") int cnt) { User visitor = UserUtils.getCurrentUser(); diff --git a/juick-server/src/main/java/com/juick/server/api/Post.java b/juick-server/src/main/java/com/juick/server/api/Post.java index b0be50b6..99d118c3 100644 --- a/juick-server/src/main/java/com/juick/server/api/Post.java +++ b/juick-server/src/main/java/com/juick/server/api/Post.java @@ -62,7 +62,7 @@ public class Post { @Inject CommandsManager commandsManager; - @RequestMapping(value = "/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/post", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) @ResponseStatus(value = HttpStatus.OK) public CommandResult doPostMessage( @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body, @@ -101,7 +101,7 @@ public class Post { return commandsManager.processCommand(visitor, body, attachmentFName); } - @RequestMapping(value = "/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/comment", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public com.juick.Message doPostComment( @RequestParam(defaultValue = "0") int mid, @RequestParam(defaultValue = "0") int rid, @@ -154,7 +154,7 @@ public class Post { return commandsManager.processCommand(visitor, String.format("#%d/%d %s", mid, rid, body), attachmentFName).getNewMessage().get(); } - @PostMapping("/like") + @PostMapping("/api/like") @ResponseStatus(value = HttpStatus.OK) public Status doPostRecomm(@RequestParam Integer mid) throws Exception { com.juick.User visitor = UserUtils.getCurrentUser(); @@ -173,13 +173,13 @@ public class Post { return Status.getStatus(status.getText()); } - @GetMapping("/reactions") + @GetMapping("/api/reactions") @ResponseStatus(value = HttpStatus.OK) public List reactionsList() { return messagesService.listReactions(); } - @PostMapping("/react") + @PostMapping("/api/react") @ResponseStatus(value = HttpStatus.OK) public Status doPostReact(@RequestParam Integer mid,@RequestParam @NotNull int reactionId, @RequestParam (required = false, defaultValue = "1") int count) { @@ -204,7 +204,7 @@ public class Post { return recommendStatus == MessagesService.RecommendStatus.Error ? Status.ERROR :Status.OK; } - @PostMapping("/update") + @PostMapping("/api/update") public CommandResult updateMessage(@RequestParam Integer mid, @RequestParam(required = false, defaultValue = "0") Integer rid, @RequestParam String body) { diff --git a/juick-server/src/main/java/com/juick/server/api/Service.java b/juick-server/src/main/java/com/juick/server/api/Service.java index 3a01317b..0da5d46c 100644 --- a/juick-server/src/main/java/com/juick/server/api/Service.java +++ b/juick-server/src/main/java/com/juick/server/api/Service.java @@ -54,7 +54,7 @@ public class Service { Session session = Session.getDefaultInstance(new Properties()); @ApiIgnore - @PostMapping("/mail") + @PostMapping("/api/mail") @ResponseStatus(value = HttpStatus.OK) public void processMail(InputStream data) throws Exception { if (UserUtils.getCurrentUser().getName().equals(serviceUser)) { diff --git a/juick-server/src/main/java/com/juick/server/api/SocialLogin.java b/juick-server/src/main/java/com/juick/server/api/SocialLogin.java deleted file mode 100644 index dc7425e1..00000000 --- a/juick-server/src/main/java/com/juick/server/api/SocialLogin.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -package com.juick.server.api; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.github.scribejava.apis.FacebookApi; -import com.github.scribejava.apis.VkontakteApi; -import com.github.scribejava.core.builder.ServiceBuilder; -import com.github.scribejava.core.model.OAuth2AccessToken; -import com.github.scribejava.core.model.OAuthRequest; -import com.github.scribejava.core.model.Verb; -import com.github.scribejava.core.oauth.OAuth20Service; -import com.juick.facebook.User; -import com.juick.server.util.HttpBadRequestException; -import com.juick.service.CrosspostService; -import com.juick.service.EmailService; -import com.juick.service.TelegramService; -import com.juick.service.UserService; -import com.juick.vk.UsersResponse; -import org.apache.commons.lang3.StringUtils; -import org.apache.commons.lang3.math.NumberUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.util.UriComponentsBuilder; - -import javax.annotation.PostConstruct; -import javax.inject.Inject; -import java.io.IOException; -import java.util.UUID; -import java.util.concurrent.ExecutionException; - -/** - * - * @author Ugnich Anton - */ -@Controller -public class SocialLogin { - - private static final Logger logger = LoggerFactory.getLogger(SocialLogin.class); - - @Value("${facebook_appid:appid}") - private String FACEBOOK_APPID; - @Value("${facebook_secret:secret}") - private String FACEBOOK_SECRET; - private static final String FACEBOOK_REDIRECT = "https://api.juick.com/_fblogin"; - private static final String VK_REDIRECT = "https://api.juick.com/_vklogin"; - private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json"; - @Inject - private ObjectMapper jsonMapper; - private ServiceBuilder facebookBuilder, twitterBuilder, vkBuilder; - - @Value("${twitter_consumer_key:appid}") - private String twitterConsumerKey; - @Value("${twitter_consumer_secret:secret}") - private String twitterConsumerSecret; - @Value("${vk_appid:appid}") - private String VK_APPID; - @Value("${vk_secret:secret}") - private String VK_SECRET; - @Value("${telegram_token:secret}") - private String telegramToken; - - @Inject - private CrosspostService crosspostService; - @Inject - private UserService userService; - @Inject - private EmailService emailService; - @Inject - private TelegramService telegramService; - - @PostConstruct - public void init() { - facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); - twitterBuilder = new ServiceBuilder(twitterConsumerKey); - vkBuilder = new ServiceBuilder(VK_APPID); - } - - @GetMapping("/_fblogin") - protected String doFacebookLogin(@RequestParam(required = false) String code, - @RequestParam(required = false) String state) throws IOException, ExecutionException, InterruptedException { - if (StringUtils.isBlank(code)) { - String fbstate = UUID.randomUUID().toString(); - crosspostService.addFacebookState(fbstate, state); - OAuth20Service facebookAuthService = facebookBuilder - .apiSecret(FACEBOOK_SECRET) - .callback(FACEBOOK_REDIRECT) - .scope("email") - .state(fbstate) - .build(FacebookApi.instance()); - return "redirect:" + facebookAuthService.getAuthorizationUrl(); - } - - String redirectUrl = crosspostService.verifyFacebookState(state); - - if (StringUtils.isEmpty(redirectUrl)) { - logger.error("state is missing"); - throw new HttpBadRequestException(); - } - OAuth20Service facebookService = facebookBuilder - .apiKey(FACEBOOK_APPID) - .apiSecret(FACEBOOK_SECRET) - .callback(FACEBOOK_REDIRECT) - .scope("email") - .state(state) - .build(FacebookApi.instance()); - OAuth2AccessToken token = facebookService.getAccessToken(code); - final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v2.10/me?fields=id,name,link,verified,email"); - facebookService.signRequest(token, meRequest); - String graph = facebookService.execute(meRequest).getBody(); - if (StringUtils.isBlank(graph)) { - logger.error("FACEBOOK GRAPH ERROR"); - throw new HttpBadRequestException(); - } - User fb = jsonMapper.readValue(graph, User.class); - long fbID = NumberUtils.toLong(fb.getId(), 0); - if (fbID == 0 || StringUtils.isBlank(fb.getName()) || StringUtils.isBlank(fb.getLink())) { - logger.error("Missing required fields, id: {}, name: {}, link: {}", fbID, fb.getName(), fb.getLink()); - throw new HttpBadRequestException(); - } - - int uid = crosspostService.getUIDbyFBID(fbID); - if (uid > 0) { - if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName(), fb.getLink())) { - logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken()); - throw new HttpBadRequestException(); - } - UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); - uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid)); - return "redirect:" + uriComponentsBuilder.build().toUriString(); - } else if (fb.getVerified()) { - if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName(), fb.getLink())) { - if (StringUtils.isNotEmpty(fb.getEmail())) { - logger.info("found {} for facebook user {}", fb.getEmail(), fb.getLink()); - Integer userId = crosspostService.getUIDbyFBID(fbID); - if (!emailService.getEmails(userId, false).contains(fb.getEmail())) { - emailService.addEmail(userId, fb.getEmail()); - } - } - logger.info("email not found for facebook user {}", fb.getLink()); - throw new HttpBadRequestException(); - } - return "redirect:/signup?type=fb&hash=" + state; - } else { - logger.error("Facebook account is not verified, id: {}", fbID); - throw new HttpBadRequestException(); - } - }/* - @GetMapping("/_twitter") - protected void doTwitterLogin(HttpServletRequest request, HttpServletResponse response) - throws IOException, ExecutionException, InterruptedException { - String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY; - String verifier = request.getParameter("oauth_verifier"); - Cookie[] cookies = request.getCookies(); - for (Cookie cookie : cookies) { - if (cookie.getName().equals("hash")) { - hash = cookie.getValue(); - } - if (cookie.getName().equals("request_token")) { - request_token = cookie.getValue(); - } - if (cookie.getName().equals("request_token_secret")) { - request_token_secret = cookie.getValue(); - } - } - com.juick.User user = UserUtils.getCurrentUser(); - OAuth10aService oAuthService = twitterBuilder - .apiSecret(twitterConsumerSecret) - .callback("http://juick.com/_twitter") - .build(TwitterApi.instance()); - - if (request_token.isEmpty() && request_token_secret.isEmpty() - && (verifier == null || verifier.isEmpty())) { - OAuth1RequestToken requestToken = oAuthService.getRequestToken(); - String authUrl = oAuthService.getAuthorizationUrl(requestToken); - response.addCookie(new Cookie("request_token", requestToken.getToken())); - response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret())); - response.setStatus(HttpServletResponse.SC_FOUND); - response.setHeader("Location", authUrl); - } else { - if (verifier != null && verifier.length() > 0) { - OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret); - OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier); - OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL); - oAuthService.signRequest(accessToken, oAuthRequest); - com.juick.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(), - com.juick.twitter.User.class); - if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(), - twitterUser.getScreenName())) { - response.setStatus(HttpServletResponse.SC_FOUND); - response.setHeader("Location", "http://juick.com/settings"); - } else { - response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - } - } - } - }*/ - @GetMapping("/_vklogin") - protected String doVKLogin(@RequestParam(required = false) String code, - @RequestParam String state) throws IOException, ExecutionException, InterruptedException { - if (StringUtils.isBlank(code)) { - String vkstate = UUID.randomUUID().toString(); - crosspostService.addVKState(vkstate, state); - OAuth20Service vkAuthService = vkBuilder - .apiSecret(VK_SECRET) - .scope("friends,wall,offline") - .state(vkstate) - .callback(VK_REDIRECT) - .build(VkontakteApi.instance()); - return "redirect:" + vkAuthService.getAuthorizationUrl(); - } - - String redirectUrl = crosspostService.verifyVKState(state); - if (StringUtils.isBlank(redirectUrl)) { - logger.error("state is missing"); - throw new HttpBadRequestException(); - } - - OAuth20Service vkService = vkBuilder - .apiKey(VK_APPID) - .apiSecret(VK_SECRET) - .build(VkontakteApi.instance()); - OAuth2AccessToken token = vkService.getAccessToken(code); - - OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73"); - vkService.signRequest(token, meRequest); - String graph = vkService.execute(meRequest).getBody(); - - com.juick.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0); - String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName(); - String vkLink = jsonUser.getScreenName(); - - if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) { - logger.error("vk user error"); - throw new HttpBadRequestException(); - } - - Long vkID = NumberUtils.toLong(jsonUser.getId(), 0); - int uid = crosspostService.getUIDbyVKID(vkID); - if (uid > 0) { - UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUriString(redirectUrl); - uriComponentsBuilder.queryParam("hash", userService.getHashByUID(uid)); - return "redirect:" + uriComponentsBuilder.build().toUriString(); - } else { - String loginhash = UUID.randomUUID().toString(); - if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) { - logger.error("create vk user error"); - throw new HttpBadRequestException(); - } - return "redirect:/signup?type=vk&hash=" + loginhash; - } - } - /* - @GetMapping("/_tglogin") - public String doDurovLogin(HttpServletRequest request, - @RequestParam Map params, - HttpServletResponse response) { - String dataCheckString = params.entrySet().stream() - .filter(p -> !p.getKey().equals("hash")) - .sorted(Map.Entry.comparingByKey()) - .map(p -> p.getKey() + "=" + p.getValue()) - .collect(Collectors.joining("\n")); - String hash = params.get("hash"); - byte[] secretKey = DigestUtils.sha256(telegramToken); - String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString); - if (hash.equals(resultString)) { - Long tgUser = Long.valueOf(params.get("id")); - int uid = telegramService.getUser(tgUser); - if (uid > 0) { - Cookie c = new Cookie("hash", userService.getHashByUID(uid)); - c.setMaxAge(50 * 24 * 60 * 60); - response.addCookie(c); - return Utils.getPreviousPageByRequest(request).orElse("redirect:/"); - } else { - String username = StringUtils.defaultString(params.get("username"), params.get("first_name")); - telegramService.createTelegramUser(tgUser, username); - return "redirect:/signup?type=durov&hash=" + userService.getSignUpHashByTelegramID(tgUser, username); - } - } else { - logger.warn("invalid tg hash {} for {}", resultString, hash); - } - throw new HttpBadRequestException(); - }*/ -} diff --git a/juick-server/src/main/java/com/juick/server/api/Tags.java b/juick-server/src/main/java/com/juick/server/api/Tags.java index 8ced4ec9..38e71e3a 100644 --- a/juick-server/src/main/java/com/juick/server/api/Tags.java +++ b/juick-server/src/main/java/com/juick/server/api/Tags.java @@ -38,7 +38,7 @@ public class Tags { @Inject private TagService tagService; - @RequestMapping(value = "/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/tags", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public List tags( @RequestParam(required = false, defaultValue = "0") int user_id ) { diff --git a/juick-server/src/main/java/com/juick/server/api/Users.java b/juick-server/src/main/java/com/juick/server/api/Users.java index 237b7ed6..7f29a327 100644 --- a/juick-server/src/main/java/com/juick/server/api/Users.java +++ b/juick-server/src/main/java/com/juick/server/api/Users.java @@ -52,12 +52,12 @@ public class Users { private EmailService emailService; @ApiOperation(value = "This returns user token", notes = "Pass login and password using HTTP Basic") - @RequestMapping(value = "/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/auth", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public String getAuthToken() { return userService.getHashByUID(UserUtils.getCurrentUser().getUid()); } - @RequestMapping(value = "/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/users", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public List doGetUsers( @RequestParam(value = "uname", required = false) List unames) { List users = new ArrayList<>(); @@ -78,7 +78,7 @@ public class Users { throw new HttpNotFoundException(); } - @GetMapping("/me") + @GetMapping("/api/me") public SecureUser getMe() { User visitor = UserUtils.getCurrentUser(); SecureUser me = new SecureUser(); @@ -93,7 +93,7 @@ public class Users { return me; } - @RequestMapping(value = "/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/users/read", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public List doGetUserRead( @RequestParam String uname) { User visitor = UserUtils.getCurrentUser(); @@ -118,7 +118,7 @@ public class Users { throw new HttpNotFoundException(); } - @RequestMapping(value = "/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) + @RequestMapping(value = "/api/users/readers", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE) public List doGetUserReaders( @RequestParam String uname) { User visitor = UserUtils.getCurrentUser(); @@ -144,7 +144,7 @@ public class Users { } @ApiOperation(value = "This returns detailed user info") - @GetMapping("/info/{uname}") + @GetMapping("/api/info/{uname}") public UserInfo getUserInfo(@PathVariable String uname) { User user = userService.getUserByName(uname); if (!user.isBanned()) { diff --git a/juick-server/src/main/java/com/juick/server/api/activity/Profile.java b/juick-server/src/main/java/com/juick/server/api/activity/Profile.java index 0d987b58..89236f03 100644 --- a/juick-server/src/main/java/com/juick/server/api/activity/Profile.java +++ b/juick-server/src/main/java/com/juick/server/api/activity/Profile.java @@ -38,7 +38,7 @@ public class Profile { @Value("${img_url:http://localhost:8080/i/}") private String baseImagesUri; - @GetMapping(value = "/u/{userName}", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) + @GetMapping(value = "/api/u/{userName}", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) public Person getUser(@PathVariable String userName) { User user = userService.getUserByName(userName); if (!user.isAnonymous()) { @@ -67,7 +67,7 @@ public class Profile { } throw new HttpNotFoundException(); } - @GetMapping(value = "/u/{userName}/blog/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) + @GetMapping(value = "/api/u/{userName}/blog/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) public OrderedCollection getOutbox(@PathVariable String userName) { User user = userService.getUserByName(userName); if (!user.isAnonymous()) { @@ -80,7 +80,7 @@ public class Profile { } throw new HttpNotFoundException(); } - @GetMapping(value = "/u/{userName}/blog", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) + @GetMapping(value = "/api/u/{userName}/blog", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) public OrderedCollectionPage getOutboxPage(@PathVariable String userName, @RequestParam(required = false, defaultValue = "0") int before) { User visitor = UserUtils.getCurrentUser(); @@ -128,7 +128,7 @@ public class Profile { } throw new HttpNotFoundException(); } - @GetMapping(value = "/u/{userName}/followers/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) + @GetMapping(value = "/api/u/{userName}/followers/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) public OrderedCollection getFollowers(@PathVariable String userName) { User user = userService.getUserByName(userName); if (!user.isAnonymous()) { @@ -141,7 +141,7 @@ public class Profile { } throw new HttpNotFoundException(); } - @GetMapping(value = "/u/{userName}/followers", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) + @GetMapping(value = "/api/u/{userName}/followers", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) public OrderedCollectionPage getFollowersPage(@PathVariable String userName, @RequestParam(required = false, defaultValue = "0") int page) { User user = userService.getUserByName(userName); @@ -171,7 +171,7 @@ public class Profile { } throw new HttpNotFoundException(); } - @GetMapping(value = "/u/{userName}/following/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) + @GetMapping(value = "/api/u/{userName}/following/toc", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) public OrderedCollection getFollowing(@PathVariable String userName) { User user = userService.getUserByName(userName); if (!user.isAnonymous()) { @@ -184,7 +184,7 @@ public class Profile { } throw new HttpNotFoundException(); } - @GetMapping(value = "/u/{userName}/following", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) + @GetMapping(value = "/api/u/{userName}/following", produces = { ActivityObject.LD_JSON_MEDIA_TYPE, ActivityObject.ACTIVITY_JSON_MEDIA_TYPE }) public OrderedCollectionPage getFollowingPage(@PathVariable String userName, @RequestParam(required = false, defaultValue = "0") int page) { User user = userService.getUserByName(userName); diff --git a/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java b/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java deleted file mode 100644 index 2f263a78..00000000 --- a/juick-server/src/main/java/com/juick/server/configuration/ApiSecurityConfig.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * Copyright (C) 2008-2017, Juick - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package com.juick.server.configuration; - -import com.juick.service.UserService; -import com.juick.service.security.JuickUserDetailsService; -import com.juick.service.security.deprecated.RequestParamHashRememberMeServices; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.security.authentication.AuthenticationProvider; -import org.springframework.security.authentication.dao.DaoAuthenticationProvider; -import org.springframework.security.config.annotation.web.builders.HttpSecurity; -import org.springframework.security.config.annotation.web.builders.WebSecurity; -import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; -import org.springframework.security.config.http.SessionCreationPolicy; -import org.springframework.security.core.userdetails.UserDetailsService; -import org.springframework.security.web.AuthenticationEntryPoint; -import org.springframework.security.web.authentication.HttpStatusEntryPoint; -import org.springframework.security.web.authentication.RememberMeServices; -import org.springframework.web.cors.CorsConfiguration; -import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; - -import javax.inject.Inject; -import java.util.Arrays; -import java.util.Collections; -import java.util.concurrent.TimeUnit; - -/** - * Created by aalexeev on 11/21/16. - */ -@Configuration -@EnableWebSecurity -public class ApiSecurityConfig extends WebSecurityConfigurerAdapter { - @Value("${auth_remember_me_key:secret}") - private String rememberMeKey; - @Inject - private UserService userService; - - ApiSecurityConfig() { - super(true); - } - - @Override - protected void configure(HttpSecurity http) throws Exception { - http.authorizeRequests() - .antMatchers(HttpMethod.OPTIONS).permitAll() - .antMatchers("/", "/messages", "/users", "/thread", "/tags", "/tlgmbtwbhk", "/fbwbhk", - "/skypebotendpoint", "/_fblogin", "/_vklogin", "_tglogin", "/u/**", "/.well-known/webfinger").permitAll() - .anyRequest().hasRole("USER") - .and().httpBasic().authenticationEntryPoint(juickAuthenticationEntryPoint()) - .and().anonymous() - .and().cors().configurationSource(corsConfigurationSource()) - .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) - .and().exceptionHandling().authenticationEntryPoint(juickAuthenticationEntryPoint()) - .and() - .rememberMe() - .alwaysRemember(true) - .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30)) - .rememberMeServices(rememberMeServices()) - .key(rememberMeKey) - .and().authenticationProvider(authenticationProvider()) - .headers().defaultsDisabled().cacheControl(); - } - - @Bean - public AuthenticationProvider authenticationProvider() { - DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider(); - - authenticationProvider.setUserDetailsService(userDetailsService()); - - return authenticationProvider; - } - - @Bean - public UserDetailsService userDetailsService() { - return new JuickUserDetailsService(userService); - } - - @Bean - public RememberMeServices rememberMeServices() { - return new RequestParamHashRememberMeServices(rememberMeKey, userService); - } - - @Bean - public AuthenticationEntryPoint juickAuthenticationEntryPoint() { - return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED); - } - - @Bean - public CorsConfigurationSource corsConfigurationSource() { - CorsConfiguration configuration = new CorsConfiguration(); - - configuration.setAllowedOrigins(Collections.singletonList("*")); - configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); - configuration.setAllowedHeaders(Collections.singletonList("*")); - - UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); - source.registerCorsConfiguration("/**", configuration); - - return source; - } - @Override - public void configure(WebSecurity web) throws Exception { - web.ignoring().antMatchers("/v2/api-docs", "/configuration/ui", "/swagger-resources/**", - "/configuration/**", "/swagger-ui.html", "/webjars/**", "/ws/**", "/rss/**", "/h2-console/**"); - } -} diff --git a/juick-server/src/main/java/com/juick/server/configuration/PostConfig.java b/juick-server/src/main/java/com/juick/server/configuration/PostConfig.java deleted file mode 100644 index 598a7435..00000000 --- a/juick-server/src/main/java/com/juick/server/configuration/PostConfig.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.juick.server.configuration; - -import org.springframework.boot.autoconfigure.EnableAutoConfiguration; -import org.springframework.context.annotation.ComponentScan; - -@EnableAutoConfiguration -@ComponentScan({"com.juick.server", "com.juick.service"}) -public class PostConfig { -} diff --git a/juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java new file mode 100644 index 00000000..53b29415 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/configuration/SapeConfiguration.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.configuration; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import ru.sape.Sape; + +/** + * Created by vitalyster on 29.03.2017. + */ +@Configuration +public class SapeConfiguration { + @Value("${sape_user:secret}") + private String token; + + @Bean + public Sape sape() { + return new Sape(token, "juick.com", 2000, 3600); + } +} diff --git a/juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java b/juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java new file mode 100644 index 00000000..cd2ab13a --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/configuration/SecurityConfig.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.configuration; + +import com.juick.service.UserService; +import com.juick.service.security.HashParamAuthenticationFilter; +import com.juick.service.security.JuickUserDetailsService; +import com.juick.service.security.deprecated.RequestParamHashRememberMeServices; +import com.juick.service.security.entities.JuickUser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.annotation.Order; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.HttpStatusEntryPoint; +import org.springframework.security.web.authentication.RememberMeServices; +import org.springframework.security.web.authentication.rememberme.TokenBasedRememberMeServices; +import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import javax.annotation.Resource; +import javax.inject.Inject; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.TimeUnit; + +/** + * Created by aalexeev on 11/21/16. + */ +@EnableWebSecurity +public class SecurityConfig { + @Resource + private UserService userService; + @Value("${auth_remember_me_key:secret}") + private String rememberMeKey; + @Value("${web_domain:localhost}") + private String webDomain; + + private static final String COOKIE_NAME = "juick-remember-me"; + + @Bean + public UserDetailsService userDetailsService() { + return new JuickUserDetailsService(userService); + } + @Bean + public RememberMeServices rememberMeServices() throws Exception { + TokenBasedRememberMeServices services = new TokenBasedRememberMeServices( + rememberMeKey, userDetailsService()); + + services.setCookieName(COOKIE_NAME); + services.setCookieDomain(webDomain); + services.setAlwaysRemember(true); + services.setTokenValiditySeconds(6 * 30 * 24 * 3600); + services.setUseSecureCookie(false); // TODO set true if https is supports + + return services; + } + @Bean + public HashParamAuthenticationFilter hashParamAuthenticationFilter() throws Exception { + return new HashParamAuthenticationFilter(userService, rememberMeServices()); + } + + + @Configuration + @Order(1) + public static class ApiConfig extends WebSecurityConfigurerAdapter { + @Value("${auth_remember_me_key:secret}") + private String rememberMeKey; + @Value("${web_domain:localhost}") + private String webDomain; + @Resource + private UserService userService; + @Inject + private HashParamAuthenticationFilter hashParamAuthenticationFilter; + ApiConfig() { + super(true); + } + @Bean + RememberMeServices rememberMeServices(){ + return new RequestParamHashRememberMeServices(rememberMeKey, userService); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.addFilterAfter(hashParamAuthenticationFilter, BasicAuthenticationFilter.class); + http.antMatcher("/api/**").authorizeRequests() + .antMatchers(HttpMethod.OPTIONS).permitAll() + .antMatchers("/api/", "/api/messages", "/api/users", "/api/thread", "/api/tags", "/api/tlgmbtwbhk", "/api/fbwbhk", + "/api/skypebotendpoint", "/api/_fblogin", "/api/_vklogin", "/api/_tglogin", "/api/u/**", "/.well-known/webfinger").permitAll() + .anyRequest().hasRole("USER") + .and() + .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) + .and() + .httpBasic().authenticationEntryPoint(juickAuthenticationEntryPoint()) + .and().cors().configurationSource(corsConfigurationSource()) + .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and().exceptionHandling().authenticationEntryPoint(juickAuthenticationEntryPoint()) + .and() + .rememberMe() + .alwaysRemember(true) + .tokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(6 * 30)) + .rememberMeServices(rememberMeServices()) + .key(rememberMeKey) + .and() + .headers().defaultsDisabled().cacheControl(); + } + + @Bean + public AuthenticationEntryPoint juickAuthenticationEntryPoint() { + return new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + + configuration.setAllowedOrigins(Collections.singletonList("*")); + configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "DELETE")); + configuration.setAllowedHeaders(Collections.singletonList("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/api/**", configuration); + + return source; + } + @Override + public void configure(WebSecurity web) { + web.debug(false); + web.ignoring().antMatchers("/api/v2/api-docs", "/api/configuration/ui", "/api/swagger-resources/**", + "/api/configuration/**", "/swagger-ui.html", "/webjars/**", "/ws/**", "/rss/**", "/h2-console/**"); + } + } + + @Configuration + public static class WebConfig extends WebSecurityConfigurerAdapter { + @Inject + private RememberMeServices rememberMeServices; + @Value("${auth_remember_me_key:secret}") + private String rememberMeKey; + @Value("${web_domain:localhost}") + private String webDomain; + @Resource + private UserService userService; + @Override + protected void configure(HttpSecurity http) throws Exception { + http + .authorizeRequests() + .antMatchers("/settings", "/pm/**", "/**/bl", "/_twitter", "/post", "/post2", "/comment") + .authenticated() + .anyRequest().permitAll() + .and() + .anonymous().principal(JuickUser.ANONYMOUS_USER).authorities(JuickUser.ANONYMOUS_AUTHORITY) + .and() + .sessionManagement().invalidSessionUrl("/") + .and() + .logout() + .invalidateHttpSession(true) + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .deleteCookies("hash", COOKIE_NAME) + .and() + .formLogin() + .loginPage("/login") + .permitAll() + .defaultSuccessUrl("/") + .loginProcessingUrl("/login") + .usernameParameter("username") + .passwordParameter("password") + .failureUrl("/login?error=1") + .and() + .rememberMe() + .rememberMeCookieDomain(webDomain).key(rememberMeKey) + .rememberMeServices(rememberMeServices) + .and() + .csrf().disable() + .headers().defaultsDisabled().cacheControl(); + } + @Override + public void configure(WebSecurity web) throws Exception { + web.debug(false); + web.ignoring().antMatchers("/style.css*", "/scripts.js*", "/h2-console/**", "/.well-known/**"); + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java b/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java new file mode 100644 index 00000000..16d32ee4 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/configuration/WwwAppConfiguration.java @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.configuration; + +import com.juick.service.TagService; +import com.juick.service.UserService; +import com.juick.server.www.HelpService; +import com.mitchellbosecke.pebble.PebbleEngine; +import com.mitchellbosecke.pebble.extension.FormatterExtension; +import com.mitchellbosecke.pebble.loader.ClasspathLoader; +import com.mitchellbosecke.pebble.loader.Loader; +import com.mitchellbosecke.pebble.spring.PebbleViewResolver; +import com.mitchellbosecke.pebble.spring.extension.SpringExtension; +import org.apache.commons.codec.CharEncoding; +import org.commonmark.ext.autolink.AutolinkExtension; +import org.commonmark.node.Link; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import javax.inject.Inject; +import java.util.Collections; + +/** + * Created by aalexeev on 11/22/16. + */ +@Configuration +@EnableCaching +@Import({ BaseWebConfiguration.class, SecurityConfig.class, SapeConfiguration.class, + StorageConfiguration.class}) +public class WwwAppConfiguration implements WebMvcConfigurer { + @Inject + private UserService userService; + @Inject + private TagService tagService; + @Bean + public CaffeineCacheManager cacheManager() { + return new CaffeineCacheManager("help"); + } + + @Bean + public HelpService helpService() { + return new HelpService("help"); + } + + @Bean + public Parser cmParser() { + return Parser.builder().extensions(Collections.singletonList(AutolinkExtension.create())).build(); + } + @Bean + public HtmlRenderer helpRenderer() { + return HtmlRenderer.builder() + .attributeProviderFactory(context -> (node, tagName, attributes) -> { + if (node instanceof Link) { + Link link = (Link) node; + if (link.getDestination().startsWith("/")) { + String destination = "/" + helpService().getHelpPath() + link.getDestination(); + link.setDestination(destination); + attributes.put("href", destination); + } + } + }) + .build(); + } + @Bean + public Loader templateLoader() { + return new ClasspathLoader(); + } + + @Bean + public SpringExtension springExtension() { + return new SpringExtension(); + } + + @Bean + public PebbleEngine pebbleEngine() { + return new PebbleEngine.Builder() + .loader(this.templateLoader()) + .extension(springExtension()) + .extension(new FormatterExtension()) + .strictVariables(true) + .build(); + } + + @Bean + public ViewResolver viewResolver() { + PebbleViewResolver viewResolver = new PebbleViewResolver(); + viewResolver.setPrefix("templates"); + viewResolver.setSuffix(".html"); + viewResolver.setPebbleEngine(pebbleEngine()); + viewResolver.setCharacterEncoding(CharEncoding.UTF_8); + return viewResolver; + } + +} diff --git a/juick-server/src/main/java/com/juick/server/www/HelpService.java b/juick-server/src/main/java/com/juick/server/www/HelpService.java new file mode 100644 index 00000000..25727962 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/HelpService.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.www; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.lang3.StringUtils; +import org.springframework.cache.annotation.Cacheable; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.regex.Pattern; + +/** + * Created by aalexeev on 12/11/16. + */ +public class HelpService { + private static final Pattern LANG_PATTERN = Pattern.compile("[a-z]{2}"); + + private static final Pattern PAGE_PATTERN = Pattern.compile("[a-zA-Z0-9\\-_]+"); + + private final String helpPath; + + + public HelpService(String helpPath) { + this.helpPath = helpPath; + } + + @Cacheable("help") + public String getHelp(final String page, final String lang) { + if (canBePage(page) && canBeLang(lang)) { + String path = StringUtils.joinWith("/", helpPath, lang, page + ".md"); + + try (InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(path)) { + if (is != null) + return IOUtils.toString(is, StandardCharsets.UTF_8); + } catch (IOException e) { + } + } + return null; + } + + public boolean canBePage(final String anything) { + return anything != null && PAGE_PATTERN.matcher(anything).matches(); + } + + public boolean canBeLang(final String anything) { + return anything != null && LANG_PATTERN.matcher(anything).matches(); + } + + public String getHelpPath() { + return helpPath; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/Utils.java b/juick-server/src/main/java/com/juick/server/www/Utils.java new file mode 100644 index 00000000..61278e17 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/Utils.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.www; + +import javax.servlet.http.HttpServletRequest; +import java.util.Optional; + +/** + * + * @author Ugnich Anton + */ +public class Utils { + + + public static String encodeSphinx(String str) { + return str.replaceAll("@", "\\\\@") + .replaceAll("\\'", "\\\\'"); + } + + /** + * Returns the viewName to return for coming back to the sender url + * + * @param request Instance of {@link HttpServletRequest} or use an injected instance + * @return Optional with the view name. Recomended to use an alternativa url with + * {@link Optional#orElse(java.lang.Object)} + */ + public static Optional getPreviousPageByRequest(HttpServletRequest request) + { + return Optional.ofNullable(request.getHeader("Referer")).map(requestUrl -> "redirect:" + requestUrl); + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/WebApp.java b/juick-server/src/main/java/com/juick/server/www/WebApp.java new file mode 100644 index 00000000..98327a5d --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/WebApp.java @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.www; + +import com.juick.Tag; +import com.juick.service.TagService; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.resource.ResourceUrlProvider; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +/** + * + * @author Ugnich Anton + */ +@Component +public class WebApp { + @Inject + private TagService tagService; + @Inject + private ResourceUrlProvider resourceUrlProvider; + + public List parseTags(String tagsStr) { + List tags = new ArrayList<>(); + if (tagsStr != null && !tagsStr.isEmpty()) { + Stream tagsList = Arrays.stream(tagsStr.split("[ \\,]")) + .distinct().map( t -> { + if (t.startsWith("*")) { + t = t.substring(1); + } + if (t.length() > 64) { + t = t.substring(0, 64); + } + return t; + }); + tags = tagService.getTags(tagsList, true); + while (tags.size() > 5) { + tags.remove(5); + } + } + return tags; + } + + public String getStyleUrl() { + return resourceUrlProvider.getForLookupPath("/style.css"); + } + + public String getScriptsUrl() { + return resourceUrlProvider.getForLookupPath("/scripts.js"); + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java b/juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java new file mode 100644 index 00000000..9ab20003 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/AnythingFilter.java @@ -0,0 +1,64 @@ +package com.juick.server.www.controllers; + +import com.juick.server.util.WebUtils; +import com.juick.service.MessagesService; +import com.juick.service.UserService; +import org.apache.commons.lang3.math.NumberUtils; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; + +import javax.annotation.Nonnull; +import javax.inject.Inject; +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +@Component +public class AnythingFilter extends OncePerRequestFilter { + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + + @Override + public void doFilterInternal(@Nonnull HttpServletRequest servletRequest, + @Nonnull HttpServletResponse servletResponse, + @Nonnull FilterChain filterChain) throws IOException, ServletException { + UriComponents components = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); + String anything = components.getPath().substring(1); + int before = NumberUtils.toInt(components.getQueryParams().getFirst("before"), 0); + if (before == 0) { + boolean isPostNumber = WebUtils.isPostNumber(anything); + int messageId = isPostNumber ? + NumberUtils.toInt(anything) : 0; + + if (isPostNumber && anything.equals(Integer.toString(messageId))) { + if (messageId > 0) { + com.juick.User author = messagesService.getMessageAuthor(messageId); + + if (author != null) { + servletResponse.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY); + servletResponse.setHeader("Location", "/" + author.getName() + "/" + anything); + return; + } + } + } + com.juick.User user = userService.getUserByName(anything); + if (user.getUid() > 0) { + ((HttpServletResponse)servletResponse).sendRedirect("/" + user.getName() + "/"); + } else { + filterChain.doFilter(servletRequest, servletResponse); + } + } else { + com.juick.User user = userService.getUserByName(anything); + if (!user.isAnonymous()) { + ((HttpServletResponse) servletResponse).sendRedirect("/" + user.getName() + "/?before=" + before); + } else { + filterChain.doFilter(servletRequest, servletResponse); + } + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java b/juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java new file mode 100644 index 00000000..7ba7405a --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/AppSiteAssociation.java @@ -0,0 +1,49 @@ +package com.juick.server.www.controllers; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Collections; +import java.util.List; + +@RestController +public class AppSiteAssociation { + @Value("${ios_app_id:}") + private String appId; + + @GetMapping("/.well-known/apple-app-site-association") + @ResponseBody + public SiteAssociations appSiteAssociations() { + WebCredentials webCredentials = new WebCredentials(); + webCredentials.setApps(Collections.singletonList(appId)); + SiteAssociations siteAssociations = new SiteAssociations(); + siteAssociations.setWebcredentials(webCredentials); + return siteAssociations; + } + + private class SiteAssociations { + private WebCredentials webcredentials; + + public WebCredentials getWebcredentials() { + return webcredentials; + } + + public void setWebcredentials(WebCredentials webcredentials) { + this.webcredentials = webcredentials; + } + } + + private class WebCredentials { + private List apps; + + public List getApps() { + return apps; + } + + public void setApps(List apps) { + this.apps = apps; + } + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/Help.java b/juick-server/src/main/java/com/juick/server/www/controllers/Help.java new file mode 100644 index 00000000..61b58a9d --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/Help.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.www.controllers; + +import com.juick.server.util.HttpNotFoundException; +import com.juick.server.util.UserUtils; +import com.juick.service.MessagesService; +import com.juick.server.www.HelpService; +import org.commonmark.parser.Parser; +import org.commonmark.renderer.html.HtmlRenderer; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +import javax.inject.Inject; +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.Locale; +import java.util.Objects; + +/** + * Created by aalexeev on 11/21/16. + */ +@Controller +public class Help { + @Inject + private HelpService helpService; + @Inject + private MessagesService messagesService; + @Inject + private Parser cmParser; + @Inject + private HtmlRenderer helpRenderer; + + @GetMapping({"/help/", "/help", "/help/{langOrPage}", "/help/{lang}/{page}"}) + public String showHelp( + Locale locale, + @PathVariable(required = false, name = "lang") String lang, + @PathVariable(required = false, name = "page") String page, + @PathVariable(required = false, name = "langOrPage") String langOrPage, + Model model) throws IOException, URISyntaxException { + com.juick.User visitor = UserUtils.getCurrentUser(); + + String navigation = null; + + if (langOrPage != null) { + if (helpService.canBeLang(langOrPage)) { + navigation = helpService.getHelp("navigation", langOrPage); + if (navigation != null) + lang = langOrPage; + } + + if (navigation == null && helpService.canBePage(langOrPage)) + page = langOrPage; + } + + if (lang == null) { + lang = locale.getLanguage(); + } + + String content = helpService.getHelp(page, lang); + if (content == null && !Objects.equals("tos", page)) + content = helpService.getHelp("tos", lang); + + if (navigation == null) + navigation = helpService.getHelp("navigation", lang); + + if (content == null || navigation == null) + throw new HttpNotFoundException(); + + model.addAttribute("navigation", helpRenderer.render(cmParser.parse(navigation))); + model.addAttribute("content", helpRenderer.render(cmParser.parse(content))); + model.addAttribute("visitor", visitor); + + return "views/help"; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/Login.java b/juick-server/src/main/java/com/juick/server/www/controllers/Login.java new file mode 100644 index 00000000..d933934e --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/Login.java @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.www.controllers; + +import com.juick.server.util.UserUtils; +import com.juick.service.UserService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.inject.Inject; + +/** + * @author Ugnich Anton + */ +@Controller +public class Login { + @Inject + private UserService userService; + + @GetMapping("/login") + public String getloginForm(@RequestParam(required = false, defaultValue = "true") boolean redirect) { + com.juick.User visitor = UserUtils.getCurrentUser(); + + if (!visitor.isAnonymous()) { + return redirect ? "redirect:/" : "redirect:/login/success"; + } + return "views/login"; + } + @GetMapping("/login/success") + public String getSuccessLogin(ModelMap model) { + model.addAttribute("hash", userService.getHashByUID(UserUtils.getCurrentUser().getUid())); + return "views/login_success"; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java b/juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java new file mode 100644 index 00000000..10136fcf --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/MessagesWWW.java @@ -0,0 +1,595 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.www.controllers; + +import com.juick.Message; +import com.juick.Tag; +import com.juick.formatters.PlainTextFormatter; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.HttpNotFoundException; +import com.juick.server.util.UserUtils; +import com.juick.server.util.WebUtils; +import com.juick.service.*; +import com.juick.util.MessageUtils; +import com.juick.server.www.Utils; +import org.apache.commons.codec.CharEncoding; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; +import org.springframework.web.util.UriComponents; +import ru.sape.Sape; + +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * + * @author Ugnich Anton + */ +@Controller +public class MessagesWWW { + @Inject + private UserService userService; + @Inject + private TagService tagService; + @Inject + private MessagesService messagesService; + @Inject + private Sape sape; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private CrosspostService crosspostService; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + + void fillUserModel(ModelMap model, com.juick.User user, com.juick.User visitor) { + model.addAttribute("user", user); + model.addAttribute("isSubscribed", userService.isSubscribed(visitor.getUid(), user.getUid())); + model.addAttribute("isInBL", userService.isInBL(visitor.getUid(), user.getUid())); + model.addAttribute("isInBLAny", userService.isInBLAny(user.getUid(), visitor.getUid())); + model.addAttribute("statsIRead", userService.getUserFriends(user.getUid())); + model.addAttribute("statsMyReaders", userService.getStatsMyReaders(user.getUid())); + model.addAttribute("statsMyBL", userService.getUserBLUsers(user.getUid()).size()); + model.addAttribute("statsMessages", userService.getStatsMessages(user.getUid())); + model.addAttribute("statsReplies", userService.getStatsReplies(user.getUid())); + model.addAttribute("iread", userService.getUserReadLeastPopular(user.getUid(), 8)); + model.addAttribute("tagStats", tagService.getUserTagStats(user.getUid()) + .stream().sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).limit(20).map(t -> t.getTag().getName()).collect(Collectors.toList())); + } + + @GetMapping("/") + protected String doGet( + @RequestParam(required = false) String tag, + @RequestParam(name = "show", required = false) String paramShow, + @RequestParam(name = "search", required = false) String paramSearch, + @RequestParam(name = "before", required = false, defaultValue = "0") Integer paramBefore, + @RequestParam(name = "to", required = false, defaultValue = "0") Long paramTo, + @RequestParam(name = "page", required = false, defaultValue = "0") Integer page, + @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, + ModelMap model) throws IOException { + if (tag != null) { + return "redirect:/tag/" + URLEncoder.encode(tag, CharEncoding.UTF_8); + } + com.juick.User visitor = UserUtils.getCurrentUser(); + + if (paramSearch != null && paramSearch.length() > 64) { + paramSearch = null; + } + + model.addAttribute("discover", false); + + String title; + List mids; + + if (paramSearch != null) { + title = "Поиск: " + StringEscapeUtils.escapeHtml4(paramSearch); + mids = messagesService.getSearch(Utils.encodeSphinx(paramSearch), page); + } else if (paramShow == null) { + if (!visitor.isAnonymous()) { + title = "Популярные"; + mids = messagesService.getPopular(visitor.getUid(), paramBefore); + model.addAttribute("discover", true); + } else { + title = "Микроблоги Juick: популярные записи"; + mids = messagesService.getPopular(0, paramBefore); + } + + } else if (paramShow.equals("top")) { + return "redirect:/"; + } else if (paramShow.equals("my") && !visitor.isAnonymous()) { + title = "Моя лента"; + mids = messagesService.getMyFeed(visitor.getUid(), paramBefore, true); + } else if (paramShow.equals("private") && !visitor.isAnonymous()) { + title = "Приватные"; + mids = messagesService.getPrivate(visitor.getUid(), paramBefore); + } else if (paramShow.equals("discuss") && !visitor.isAnonymous()) { + title = "Обсуждения"; + mids = messagesService.getDiscussions(visitor.getUid(), paramTo); + } else if (paramShow.equals("recommended") && !visitor.isAnonymous()) { + title = "Рекомендации"; + mids = messagesService.getRecommended(visitor.getUid(), paramBefore); + } else if (paramShow.equals("photos")) { + title = "Фотографии"; + mids = messagesService.getPhotos(visitor.getUid(), paramBefore); + model.addAttribute("discover", true); + } else if (paramShow.equals("all")) { + title = "Все сообщения"; + mids = messagesService.getAll(visitor.getUid(), paramBefore); + model.addAttribute("discover", true); + } else { + throw new HttpNotFoundException(); + } + + String head = StringUtils.EMPTY; + if (paramBefore > 0 || paramShow != null) { + head = ""; + } + model.addAttribute("title", title); + model.addAttribute("headers", head); + model.addAttribute("visitor", visitor); + model.addAttribute("noindex", !(paramShow == null && paramBefore == 0)); + List msgs = messagesService.getMessages(visitor, mids); + + if (!visitor.isAnonymous()) { + fillUserModel(model, visitor, visitor); + List unread = messagesService.getUnread(visitor); + visitor.setUnreadCount(unread.size()); + List blUIDs = userService.checkBL(visitor.getUid(), + msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); + msgs.forEach(m -> { + m.ReadOnly |= blUIDs.contains(m.getUser().getUid()); + m.setUnread(unread.contains(m.getMid())); + }); + } + model.addAttribute("msgs", msgs); + model.addAttribute("tags", tagService.getPopularTags()); + model.addAttribute("headers", head); + model.addAttribute("showAdv", + paramShow == null && paramBefore == 0 && paramSearch == null && visitor.isAnonymous()); + if (mids.size() >= 20) { + String nextpage = (paramShow != null && paramShow.equals("discuss")) ? "?to=" + msgs.get(msgs.size() - 1).getUpdated().toEpochMilli() : paramSearch != null ? String.format("?page=%d", page + 1) : "?before=" + mids.get(mids.size() - 1); + if (paramShow != null) { + nextpage += "&show=" + paramShow; + } + if (paramSearch != null) { + nextpage += "&search=" + URLEncoder.encode(paramSearch, CharEncoding.UTF_8); + } + model.addAttribute("nextpage", nextpage); + } + UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); + String queryString = builder.getQuery(); + String requestURI = builder.toUri().getPath(); + if (sape != null && visitor.isAnonymous() && queryString == null) { + String links = sape.getPageLinks(requestURI, sapeCookie).render(); + model.addAttribute("links", links); + } + return "views/index"; + } + + @GetMapping("/{uname}/") + protected String doGetBlog( + @RequestParam(required = false, name = "show") String paramShow, + @RequestParam(required = false, name = "tag") String paramTagStr, + @RequestParam(required = false, name = "search") String paramSearch, + @RequestParam(required = false, name = "page", defaultValue = "0") Integer page, + @PathVariable String uname, + @RequestParam(required = false, defaultValue = "0") Integer before, + @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, + ModelMap model) throws IOException { + com.juick.User user = userService.getUserByName(uname); + com.juick.User visitor = UserUtils.getCurrentUser(); + if (user.isBanned() || user.isAnonymous()) { + throw new HttpNotFoundException(); + } + + List mids; + + com.juick.Tag paramTag = null; + if (paramTagStr != null) { + if (paramTagStr.length() < 64) { + paramTag = tagService.getTag(paramTagStr, false); + } + if (paramTag == null) { + throw new HttpNotFoundException(); + } else if (!paramTag.getName().equals(paramTagStr)) { + String url = user.getName() + "/?tag=" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8); + return "redirect:/" + url; + } + } + if (paramSearch != null && paramSearch.length() > 64) { + paramSearch = null; + } + + int privacy = 0; + if (!visitor.isAnonymous()) { + if (user.getUid() == visitor.getUid() || visitor.getUid() == 1) { + privacy = -3; + } else if (userService.isInWL(user.getUid(), visitor.getUid())) { + privacy = -2; + } + } + + String title; + if (paramShow == null) { + if (paramTag != null) { + title = "Блог " + user.getName() + ": *" + StringEscapeUtils.escapeHtml4(paramTag.getName()); + mids = messagesService.getUserTag(user.getUid(), paramTag.TID, privacy, before); + } else if (paramSearch != null) { + title = "Блог " + user.getName() + ": " + StringEscapeUtils.escapeHtml4(paramSearch); + mids = messagesService.getUserSearch(user.getUid(), Utils.encodeSphinx(paramSearch), privacy, page); + } else { + title = "Блог " + user.getName(); + mids = messagesService.getUserBlog(user.getUid(), privacy, before); + } + } else if (paramShow.equals("recomm")) { + title = "Рекомендации " + user.getName(); + mids = messagesService.getUserRecommendations(user.getUid(), before); + } else if (paramShow.equals("photos")) { + title = "Фотографии " + user.getName(); + mids = messagesService.getUserPhotos(user.getUid(), privacy, before); + } else { + throw new HttpNotFoundException(); + } + + String head = ""; + if (paramTag != null && tagService.getTagNoIndex(paramTag.TID)) { + head += ""; + } else if (before > 0 || paramShow != null) { + head += ""; + } + model.addAttribute("pageUrl", "http://juick.com/" + user.getName()); + model.addAttribute("title", title); + model.addAttribute("headers", head); + model.addAttribute("visitor", visitor); + model.addAttribute("noindex", paramShow == null && before == 0); + fillUserModel(model, user, visitor); + model.addAttribute("paramTag", paramTag); + List msgs = messagesService.getMessages(visitor, mids); + + if (!visitor.isAnonymous()) { + List unread = messagesService.getUnread(visitor); + visitor.setUnreadCount(unread.size()); + List blUIDs = userService.checkBL(visitor.getUid(), + msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList())); + msgs.forEach(m -> { + m.ReadOnly |= blUIDs.contains(m.getUser().getUid()); + m.setUnread(unread.contains(m.getMid())); + }); + } + model.addAttribute("msgs", msgs); + model.addAttribute("headers", head); + model.addAttribute("showAdv", + paramShow == null && before == 0 && paramSearch == null && visitor.getUid() == 0); + if (mids.size() >= 20) { + String nextpage = paramSearch != null ? String.format("?page=%d", page + 1) : "?before=" + mids.get(mids.size() - 1); + if (paramShow != null) { + nextpage += "&show=" + paramShow; + } + if (paramSearch != null) { + nextpage += "&search=" + URLEncoder.encode(paramSearch, CharEncoding.UTF_8); + } + if (paramTag != null) { + nextpage += "&tag=" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8); + } + model.addAttribute("nextpage", nextpage); + } + UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); + String queryString = builder.getQuery(); + String requestURI = builder.toUri().getPath(); + if (sape != null && visitor.isAnonymous() && queryString == null) { + String links = sape.getPageLinks(requestURI, sapeCookie).render(); + model.addAttribute("links", links); + } + return "views/blog"; + } + + @GetMapping("/{uname}/tags") + protected String doGetTags(@PathVariable String uname, ModelMap model) throws IOException { + com.juick.User user = userService.getUserByName(uname); + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isBanned()) { + throw new HttpNotFoundException(); + } + + model.addAttribute("title", "Теги " + user.getName()); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + fillUserModel(model, user, visitor); + model.addAttribute("tags", tagService.getUserTagStats(user.getUid()).stream() + .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); + + return "views/blog_tags"; + } + + @GetMapping("/{uname}/friends") + protected String doGetFriends(@PathVariable String uname, ModelMap model) throws IOException { + com.juick.User user = userService.getUserByName(uname); + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isBanned()) { + throw new HttpNotFoundException(); + } + model.addAttribute("title", "Подписки " + user.getName()); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + fillUserModel(model, user, visitor); + model.addAttribute("users", userService.getUserFriends(user.getUid())); + + return "views/users"; + } + + @GetMapping("/{uname}/readers") + protected String doGetReaders(@PathVariable String uname, ModelMap model) throws IOException { + com.juick.User user = userService.getUserByName(uname); + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isBanned()) { + throw new HttpForbiddenException(); + } + model.addAttribute("title", "Читатели " + user.getName()); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + fillUserModel(model, user, visitor); + model.addAttribute("users", userService.getUserReaders(user.getUid())); + + return "views/users"; + } + + @GetMapping("/{uname}/bl") + protected String doGetBL(@PathVariable String uname, ModelMap model) throws IOException { + com.juick.User user = userService.getUserByName(uname); + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isBanned() || visitor.getUid() != user.getUid()) { + throw new HttpForbiddenException(); + } + model.addAttribute("title", "Черный список " + user.getName()); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + fillUserModel(model, user, visitor); + model.addAttribute("users", userService.getUserBLUsers(user.getUid())); + + return "views/users"; + } + @GetMapping("/tag/{tagName}") + protected String tagAction(HttpServletRequest request, + @PathVariable String tagName, + @CookieValue(name = "sape_cookie", required = false, defaultValue = StringUtils.EMPTY) String sapeCookie, + @RequestParam(required = false, defaultValue = "0") int before, + ModelMap model) throws IOException { + com.juick.User visitor = UserUtils.getCurrentUser(); + + String paramTagStr = StringEscapeUtils.unescapeHtml4(tagName); + com.juick.Tag paramTag = tagService.getTag(paramTagStr, false); + if (paramTag == null) { + throw new HttpNotFoundException(); + } else if (paramTag.SynonymID > 0 && paramTag.TID != paramTag.SynonymID) { + com.juick.Tag synTag = tagService.getTag(paramTag.SynonymID); + String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(synTag.getName()), CharEncoding.UTF_8); + if (request.getQueryString() != null) { + url += "?" + request.getQueryString(); + } + return "redirect:" + url; + } else if (!paramTag.getName().equals(paramTagStr)) { + String url = "/tag/" + URLEncoder.encode(StringEscapeUtils.escapeHtml4(paramTag.getName()), CharEncoding.UTF_8); + if (request.getQueryString() != null) { + url += "?" + request.getQueryString(); + } + return "redirect:" + url; + } + + String title = "*" + StringEscapeUtils.escapeHtml4(paramTag.getName()); + model.addAttribute("title", title); + List mids = messagesService.getTag(paramTag.TID, visitor.getUid(), before, (visitor.isAnonymous()) ? 40 : 20); + List msgs = messagesService.getMessages(visitor, mids); + if (!visitor.isAnonymous()) { + List unread = messagesService.getUnread(visitor); + visitor.setUnreadCount(unread.size()); + List blUIDs = userService.checkBL( + visitor.getUid(), + msgs.stream().map(m -> m.getUser().getUid()).collect(Collectors.toList()) + ); + msgs.forEach(m -> { + m.ReadOnly |= blUIDs.contains(m.getUser().getUid()); + m.setUnread(unread.contains(m.getMid())); + }); + fillUserModel(model, visitor, visitor); + } + + String head = StringUtils.EMPTY; + if (tagService.getTagNoIndex(paramTag.TID)) { + head = ""; + } else if (before > 0 || mids.size() < 5) { + head = ""; + } + model.addAttribute("headers", head); + model.addAttribute("visitor", visitor); + model.addAttribute("tag", paramTag); + model.addAttribute("title", title); + model.addAttribute("msgs", msgs); + model.addAttribute("tags", tagService.getPopularTags()); + model.addAttribute("noindex", before > 0); + model.addAttribute("showAdv", before == 0 && visitor.isAnonymous()); + model.addAttribute("isSubscribed", tagService.isSubscribed(visitor, paramTag)); + model.addAttribute("isInBL", tagService.isInBL(visitor, paramTag)); + if (mids.size() >= 20) { + String nextpage = "/tag/" + URLEncoder.encode(paramTag.getName(), CharEncoding.UTF_8) + "?before=" + mids.get(mids.size() - 1); + model.addAttribute("nextpage", nextpage); + } + UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); + String queryString = builder.getQuery(); + String requestURI = builder.toUri().getPath(); + if (sape != null && visitor.isAnonymous() && queryString == null) { + String links = sape.getPageLinks(requestURI, sapeCookie).render(); + model.addAttribute("links", links); + } + return "views/index"; + } + @GetMapping("/pm/inbox") + protected String doGetInbox(ModelMap model) { + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isAnonymous()) { + return "redirect:/login"; + } + String title = "PM: Inbox"; + List msgs = pmQueriesService.getLastPMInbox(visitor.getUid()); + fillUserModel(model, visitor, visitor); + model.addAttribute("title", title); + model.addAttribute("visitor", visitor); + model.addAttribute("msgs", msgs); + model.addAttribute("tags", tagService.getPopularTags()); + return "views/pm_inbox"; + } + + @GetMapping("/pm/sent") + protected String doGetSent(@RequestParam(required = false) String uname, + ModelMap model) { + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isAnonymous()) { + return "redirect:/login"; + } + String title = "PM: Sent"; + List msgs = pmQueriesService.getLastPMSent(visitor.getUid()); + + if (WebUtils.isNotUserName(uname)) { + uname = StringUtils.EMPTY; + } + fillUserModel(model, visitor, visitor); + model.addAttribute("title", title); + model.addAttribute("visitor", visitor); + model.addAttribute("msgs", msgs); + model.addAttribute("tags", tagService.getPopularTags()); + model.addAttribute("uname", uname); + return "views/pm_sent"; + } + @GetMapping("/{uname}/{mid}") + protected String threadAction(ModelMap model, + @PathVariable String uname, + @PathVariable int mid, + @CookieValue(name = "sape_cookie", + required = false, defaultValue = StringUtils.EMPTY) String sapeCookie) { + com.juick.User visitor = UserUtils.getCurrentUser(); + + if (!messagesService.canViewThread(mid, visitor.getUid())) { + throw new HttpForbiddenException(); + } + + com.juick.Message msg = messagesService.getMessage(mid); + + if (msg == null || msg.getUser().isBanned()) { + throw new HttpNotFoundException(); + } + + com.juick.User user = userService.getUserByName(uname); + if (user.isAnonymous() || !msg.getUser().equals(user)) { + return String.format("redirect:/%s/%d", msg.getUser().getName(), mid); + } + msg.VisitorCanComment = !visitor.isAnonymous(); + List replies = messagesService.getReplies(visitor, msg.getMid()); + // this should be after getReplies to mark thread as read + fillUserModel(model, user, visitor); + if (!visitor.isAnonymous()) { + List unread = messagesService.getUnread(visitor); + visitor.setUnreadCount(unread.size()); + boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); + boolean isInBL = userService.isInBLAny(msg.getUser().getUid(), visitor.getUid()); + msg.VisitorCanComment = isMsgAuthor || !(msg.ReadOnly || isInBL); + } + model.addAttribute("msg", msg); + + String title = msg.getUser().getName() + ": " + MessageUtils.getTagsString(msg); + + model.addAttribute("title", title); + model.addAttribute("visitor", visitor); + String headers = ""; + String pageUrl = "https://juick.com/" + msg.getUser().getName() + "/" + msg.getMid(); + if (msg.Hidden) { + headers += ""; + } + String cardType = StringUtils.isNotEmpty(msg.getAttachmentType()) ? "summary_large_image" : "summary"; + if (StringUtils.isNotEmpty(msg.getAttachmentType())) { + // additional check in case of broken images + if (msg.getAttachment() != null) { + String msgImage = msg.getAttachment().getMedium().getUrl(); + headers += ""; + } + } else { + String msgImage ="https://i.juick.com/a/" + msg.getUser().getUid() + ".png"; + headers += ""; + } + model.addAttribute("ogtype", "article"); + String cardDescription = StringEscapeUtils.escapeHtml4(PlainTextFormatter.formatTwitterCard(msg)); + headers += "\n" + + "\n" + + "\n" + + "\n" + + "\n" + + "\n"; + String twitterName = crosspostService.getTwitterName(msg.getUser().getUid()); + if (StringUtils.isNotEmpty(twitterName)) { + headers += "\n"; + } + if (msg.getTags().size() > 0) { + headers += "\n"; + } + model.addAttribute("headers", headers); + model.addAttribute("visitorSubscribed", messagesService.isSubscribed(visitor.getUid(), msg.getMid())); + model.addAttribute("visitorInBL", userService.isInBL(msg.getUser().getUid(), visitor.getUid())); + model.addAttribute("recomm", messagesService.getMessageRecommendations(msg.getMid())); + List blUIDs = new ArrayList<>(); + for (Message reply : replies) { + if (reply.getUser().getUid() != msg.getUser().getUid() + && !blUIDs.contains(reply.getUser().getUid())) { + blUIDs.add(reply.getUser().getUid()); + } + reply.VisitorCanComment = !visitor.isAnonymous(); + if (!visitor.isAnonymous()) { + boolean isMsgAuthor = visitor.getUid() == msg.getUser().getUid(); + boolean isReplyAuthor = visitor.getUid() == reply.getUser().getUid(); + reply.VisitorCanComment = isMsgAuthor || (!msg.ReadOnly + && msg.VisitorCanComment && (isReplyAuthor || !userService.isInBLAny(visitor.getUid(), reply.getUser().getUid()))); + } + } + model.addAttribute("replies", replies); + model.addAttribute("showAdv", visitor.isAnonymous()); + UriComponents builder = ServletUriComponentsBuilder.fromCurrentRequestUri().build(); + String queryString = builder.getQuery(); + String requestURI = builder.toUri().getPath(); + if (sape != null && visitor.isAnonymous() && queryString == null) { + String links = sape.getPageLinks(requestURI, sapeCookie).render(); + model.addAttribute("links", links); + } + return "views/thread"; + } + + // when message id is not fit to int + @ExceptionHandler(NumberFormatException.class) + public ResponseEntity notFoundAction() { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java b/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java new file mode 100644 index 00000000..9e364ff8 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/NewMessage.java @@ -0,0 +1,263 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.www.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.juick.Message; +import com.juick.User; +import com.juick.server.helpers.AnonymousUser; +import com.juick.server.helpers.CommandResult; +import com.juick.server.util.*; +import com.juick.server.www.WebApp; +import com.juick.service.*; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.util.UriComponentsBuilder; + +import javax.inject.Inject; +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.stream.Collectors; + +/** + * @author Ugnich Anton + */ +@Controller +public class NewMessage { + + @Inject + private TagService tagService; + @Inject + private MessagesService messagesService; + @Inject + private UserService userService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private CrosspostService crosspostService; + @Inject + private PMQueriesService pmQueriesService; + @Inject + private WebApp webApp; + @Inject + private ObjectMapper jsonMapper; + @Inject + private ImagesService imagesService; + @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String imgDir; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Value("${api_url:http://localhost:8080}") + private String apiUrl; + private RestTemplate rest = new RestTemplate(); + + private static final Logger logger = LoggerFactory.getLogger(NewMessage.class); + + @GetMapping("/post") + protected String postAction(@RequestParam(required = false) String body, ModelMap model) { + com.juick.User visitor = UserUtils.getCurrentUser(); + model.addAttribute("title", "Написать"); + model.addAttribute("headers", ""); + model.addAttribute("visitor", visitor); + if (body == null) { + body = StringUtils.EMPTY; + } else { + if (body.length() > 4096) { + body = body.substring(0, 4096); + } + body = StringEscapeUtils.escapeHtml4(body); + } + model.addAttribute("body", body); + model.addAttribute("visitor", visitor); + model.addAttribute("tags", tagService.getUserTagStats(visitor.getUid()).stream() + .sorted((e1, e2) -> Integer.compare(e2.getUsageCount(), e1.getUsageCount())).map(t -> t.getTag().getName()).collect(Collectors.toList())); + return "views/post"; + } + + @PostMapping("/comment") + public String doPostComment( + @RequestParam Integer mid, + @RequestParam(required = false, defaultValue = "0") Integer rid, + @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String body, + @RequestParam(required = false, defaultValue = StringUtils.EMPTY) String img, + @RequestParam(required = false) MultipartFile attach) throws IOException { + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isAnonymous() || visitor.isBanned()) { + throw new HttpForbiddenException(); + } + com.juick.Message msg = messagesService.getMessage(mid); + if (msg == null) { + throw new HttpNotFoundException(); + } + + com.juick.Message reply = null; + if (rid > 0) { + reply = messagesService.getReply(mid, rid); + if (reply == null) { + throw new HttpNotFoundException(); + } + } + + if ((StringUtils.isEmpty(body) || body.length() > 4096) && StringUtils.isEmpty(img) && attach == null) { + throw new HttpBadRequestException(); + } + body = body.replace("\r", StringUtils.EMPTY); + + if ((msg.ReadOnly && msg.getUser().getUid() != visitor.getUid()) + || userService.isInBLAny(msg.getUser().getUid(), visitor.getUid()) + || (reply != null && userService.isInBLAny(reply.getUser().getUid(), visitor.getUid()))) { + throw new HttpForbiddenException(); + } + + URI attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir); + + if (StringUtils.isBlank(attachmentFName.toString()) && img != null && img.length() > 10) { + try { + URL imgUrl = new URL(img); + attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir); + } catch (Exception e) { + logger.error("DOWNLOAD ERROR", e); + throw new HttpBadRequestException(); + } + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap params = new LinkedMultiValueMap<>(); + HttpEntity> request = new HttpEntity<>(params, headers); + + + params.add("body", rid == 0 ? String.format("#%d %s", mid, body) : String.format("#%d/%d %s", mid, rid, body)); + params.add("hash", userService.getHashByUID(visitor.getUid())); + if (StringUtils.isNotEmpty(attachmentFName.toString())) { + params.add("img", attachmentFName.toASCIIString()); + } + URI postUri = UriComponentsBuilder.fromUriString(apiUrl).path("/api/post").build().toUri(); + ResponseEntity result = rest.postForEntity( + postUri, + request, CommandResult.class); + logger.info("/comment: {}", jsonMapper.writeValueAsString(result.getBody())); + boolean wasReply = result.getBody().getNewMessage().isPresent(); + return wasReply ? "redirect:/" + msg.getUser().getName() + "/" + mid + "#" + result.getBody().getNewMessage().get().getRid() : "redirect:/" + msg.getUser().getName() + "/" + mid; + } + + @PostMapping("/pm/send") + public String doPostPM(@RequestParam(name = "uname", required = false) String unameParam, + @RequestParam String body) throws IOException { + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isAnonymous() || visitor.isBanned()) { + throw new HttpForbiddenException(); + } + String uname = unameParam; + if (uname.startsWith("@")) { + uname = uname.substring(1); + } + User userTo = AnonymousUser.INSTANCE; + if (WebUtils.isUserName(uname)) { + userTo = userService.getUserByName(uname); + } + + if (userTo.isAnonymous() || body.length() > 10240) { + throw new HttpBadRequestException(); + } + + if (userService.isInBLAny(userTo.getUid(), visitor.getUid())) { + throw new HttpForbiddenException(); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap params = new LinkedMultiValueMap<>(); + HttpEntity> request = new HttpEntity<>(params, headers); + + params.add("body", String.format("@%s %s", userTo.getName(), body)); + params.add("hash", userService.getHashByUID(visitor.getUid())); + URI postUri = UriComponentsBuilder.fromUriString(apiUrl).path("/api/post").build().toUri(); + ResponseEntity result = rest.postForEntity( + postUri, + request, CommandResult.class); + logger.info("/pm: {}", jsonMapper.writeValueAsString(result.getBody())); + return "redirect:/pm/sent"; + + } + @PostMapping("/post2") + public String doPostMessage(@RequestParam(name = "body", required = false) String bodyParam, + @RequestParam(required = false) String img, + @RequestParam(required = false) MultipartFile attach) throws IOException { + + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isAnonymous() || visitor.isBanned()) { + throw new HttpForbiddenException(); + } + String body = StringUtils.isNotEmpty(bodyParam) ? bodyParam.replace("\r", StringUtils.EMPTY) : StringUtils.EMPTY; + + URI attachmentFName = HttpUtils.receiveMultiPartFile(attach, tmpDir); + + if (StringUtils.isBlank(attachmentFName.toString()) && StringUtils.isNotBlank(img)) { + try { + URL imgUrl = new URL(img); + attachmentFName = HttpUtils.downloadImage(imgUrl, tmpDir); + } catch (Exception e) { + logger.error("DOWNLOAD ERROR", e); + throw new HttpBadRequestException(); + } + } + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + MultiValueMap params = new LinkedMultiValueMap<>(); + HttpEntity> request = new HttpEntity<>(params, headers); + + params.add("body", body); + params.add("hash", userService.getHashByUID(visitor.getUid())); + if (StringUtils.isNotEmpty(attachmentFName.toString())) { + params.add("img", attachmentFName.toASCIIString()); + } + URI postUri = UriComponentsBuilder.fromUriString(apiUrl).path("/api/post").build().toUri(); + try { + ResponseEntity result = rest.postForEntity(postUri, + request, CommandResult.class); + Message newMessage = result.getBody().getNewMessage().orElse(new Message()); + if (newMessage.getMid() > 0) { + logger.info("/post: {}", jsonMapper.writeValueAsString(result.getBody())); + return String.format("redirect:/%d", newMessage.getMid()); + } else { + logger.info("{} : {}", body, result.getBody().getText()); + } + } catch (HttpClientErrorException e) { + logger.error("post error", e); + } + return "redirect:/?show=my"; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java b/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java new file mode 100644 index 00000000..f2ecccf6 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/Settings.java @@ -0,0 +1,262 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.www.controllers; + +import com.juick.server.helpers.NotifyOpts; +import com.juick.server.helpers.UserInfo; +import com.juick.server.util.*; +import com.juick.service.*; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; + +import javax.inject.Inject; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +/** + * + * @author Ugnich Anton + */ +@Controller +public class Settings { + private static final Logger logger = LoggerFactory.getLogger(Settings.class); + + @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String imgDir; + @Value("${upload_tmp_dir:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String tmpDir; + @Inject + private TagService tagService; + @Inject + private UserService userService; + @Inject + private CrosspostService crosspostService; + @Inject + private SubscriptionService subscriptionService; + @Inject + private EmailService emailService; + @Inject + private TelegramService telegramService; + @Inject + private ImagesService imagesService; + + @GetMapping("/settings") + protected String doGet(HttpServletRequest request, HttpServletResponse response, ModelMap model) throws IOException { + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isAnonymous()) { + response.sendRedirect("/login"); + } + List pages = Arrays.asList("main", "password", "about", "auth-email", "privacy"); + String page = request.getParameter("page"); + if (StringUtils.isEmpty(page) || !pages.contains(page)) { + page = "main"; + } + + model.addAttribute("title", "Настройки"); + model.addAttribute("visitor", visitor); + model.addAttribute("tags", tagService.getPopularTags()); + model.addAttribute("auths", userService.getAuthCodes(visitor)); + model.addAttribute("email_active", emailService.getNotificationsEmail(visitor.getUid())); + model.addAttribute("ehash", userService.getEmailHash(visitor)); + model.addAttribute("emails", userService.getEmails(visitor)); + model.addAttribute("jids", userService.getAllJIDs(visitor)); + List hours = IntStream.rangeClosed(0, 23).boxed() + .map(i -> StringUtils.leftPad(String.format("%d", i), 2, "0")).collect(Collectors.toList()); + model.addAttribute("hours", hours); + model.addAttribute("fbstatus", crosspostService.getFbCrossPostStatus(visitor.getUid())); + model.addAttribute("twitter_name", crosspostService.getTwitterName(visitor.getUid())); + model.addAttribute("telegram_name", crosspostService.getTelegramName(visitor.getUid())); + model.addAttribute("notify_options", subscriptionService.getNotifyOptions(visitor)); + model.addAttribute("userinfo", userService.getUserInfo(visitor)); + if (page.equals("auth-email")) { + if (emailService.verifyAddressByCode(visitor.getUid(), request.getParameter("code"))) { + ; + model.addAttribute("result", "OK!"); + } else { + model.addAttribute("result", "Sorry, code unknown."); + } + } + return String.format("views/settings_%s", page); + } + + @PostMapping("/settings") + protected String doPost(HttpServletRequest request, HttpServletResponse response, + @RequestParam(required = false) MultipartFile avatar, + ModelMap model) + throws IOException { + com.juick.User visitor = UserUtils.getCurrentUser(); + if (visitor.isAnonymous()) { + throw new HttpBadRequestException(); + } + List pages = Arrays.asList("main", "password", "about", "email", "email-add", "email-del", + "email-subscr", "auth-email", "privacy", "jid-del", "twitter-del", "telegram-del", "facebook-disable", + "facebook-enable", "vk-del"); + String page = request.getParameter("page"); + if (StringUtils.isEmpty(page) || !pages.contains(page)) { + throw new HttpBadRequestException(); + } + String result = StringUtils.EMPTY; + switch (page) { + case "password": + if (userService.updatePassword(visitor, request.getParameter("password"))) { + result = "

Password has been changed.

"; + String hash = userService.getHashByUID(visitor.getUid()); + Cookie c = new Cookie("hash", hash); + c.setMaxAge(365 * 24 * 60 * 60); + response.addCookie(c); + } + break; + case "main": + NotifyOpts opts = new NotifyOpts(); + opts.setRepliesEnabled(StringUtils.isNotEmpty(request.getParameter("jnotify"))); + opts.setSubscriptionsEnabled(StringUtils.isNotEmpty(request.getParameter("subscr_notify"))); + opts.setRecommendationsEnabled(StringUtils.isNotEmpty(request.getParameter("recomm"))); + if (subscriptionService.setNotifyOptions(visitor, opts)) { + result = "

Notification options has been updated

"; + } + break; + case "about": + UserInfo info = new UserInfo(); + info.setFullName(request.getParameter("fullname")); + info.setCountry(request.getParameter("country")); + info.setUrl(request.getParameter("url")); + info.setDescription(request.getParameter("descr")); + String avatarTmpPath = HttpUtils.receiveMultiPartFile(avatar, tmpDir).getHost(); + if (StringUtils.isNotEmpty(avatarTmpPath)) { + imagesService.saveAvatar(avatarTmpPath, visitor.getUid()); + } + if (userService.updateUserInfo(visitor, info)) { + result = String.format("

Your info is updated.

Back to blog.

", visitor.getName()); + } + break; + case "jid-del": + // FIXME: stop using ugnich-csv in parameters + String[] params = request.getParameter("delete").split(";", 2); + boolean res = false; + if (params[0].equals("xmpp")) { + res = userService.deleteJID(visitor.getUid(), params[1]); + } else if (params[0].equals("xmpp-unauth")) { + res = userService.unauthJID(visitor.getUid(), params[1]); + } + if (res) { + result = "

Deleted. Back.

"; + } else { + result = "

Error

"; + } + break; + case "email-add": + if (!emailService.verifyAddressByCode(visitor.getUid(), request.getParameter("account"))) { + String authCode = RandomStringUtils.randomAlphanumeric(8).toUpperCase(); + if (emailService.addVerificationCode(visitor.getUid(), request.getParameter("account"), authCode)) { + Session session = Session.getDefaultInstance(System.getProperties()); + try { + MimeMessage message = new MimeMessage(session); + message.setFrom(new InternetAddress("noreply@mail.juick.com")); + message.addRecipient(Message.RecipientType.TO, new InternetAddress(request.getParameter("account"))); + message.setSubject("Juick authorization link"); + message.setText(String.format("Follow link to attach this email to Juick account:\n" + + "http://juick.com/settings?page=auth-email&code=%s\n\n" + + "If you don't know, what this mean - just ignore this mail.\n", authCode)); + Transport.send(message); + result = "

Authorization link has been sent to your email. Follow it to proceed.

" + + "

Back

"; + + } catch (MessagingException ex) { + logger.error("mail exception", ex); + throw new HttpBadRequestException(); + } + } + } + break; + case "email-del": + if (emailService.deleteEmail(visitor.getUid(), request.getParameter("account"))) { + result = "

Deleted. Back.

"; + } else { + result = "

An error occured while deleting.

"; + } + break; + case "email-subscr": + if (emailService.setNotificationsEmail(visitor.getUid(), request.getParameter("account"))) { + result = String.format("

Saved! Will send notifications to %s." + + "

Back

", request.getParameter("account")); + } else { + result = "

Disabled.

Back

"; + } + break; + case "twitter-del": + crosspostService.deleteTwitterToken(visitor.getUid()); + for (Cookie cookie : request.getCookies()) { + if (cookie.getName().equals("request_token")) { + cookie.setMaxAge(0); + response.addCookie(cookie); + } + if (cookie.getName().equals("request_token_secret")) { + cookie.setMaxAge(0); + response.addCookie(cookie); + } + } + result = "

Back

"; + break; + case "telegram-del": + telegramService.deleteTelegramUser(visitor.getUid()); + result = "

Back

"; + break; + case "facebook-disable": + crosspostService.disableFBCrosspost(visitor.getUid()); + result = "

Back

"; + break; + case "facebook-enable": + crosspostService.enableFBCrosspost(visitor.getUid()); + result = "

Back

"; + break; + case "vk-del": + crosspostService.deleteVKUser(visitor.getUid()); + result = "

Back

"; + break; + default: + throw new HttpBadRequestException(); + } + + model.addAttribute("title", "Настройки"); + model.addAttribute("visitor", visitor); + model.addAttribute("result", result); + return "views/settings_result"; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java b/juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java new file mode 100644 index 00000000..6d72aecc --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/SignUp.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.www.controllers; + +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.HttpForbiddenException; +import com.juick.server.util.UserUtils; +import com.juick.service.CrosspostService; +import com.juick.service.EmailService; +import com.juick.service.MessengerService; +import com.juick.service.UserService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.inject.Inject; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletResponse; + +/** + * + * @author Ugnich Anton + */ +@Controller +public class SignUp { + + @Inject + private UserService userService; + @Inject + private CrosspostService crosspostService; + @Inject + private MessengerService messengerService; + @Inject + private EmailService emailService; + + + @GetMapping("/signup") + protected String doGet(@RequestParam String type, @RequestParam String hash, ModelMap model) { + com.juick.User visitor = UserUtils.getCurrentUser(); + + if (hash.length() > 36 || !type.matches("^[a-zA-Z0-9\\-]+$") + || !hash.matches("^[a-zA-Z0-9\\-]+$")) { + throw new HttpBadRequestException(); + } + + String account = null; + switch (type) { + case "fb": + account = crosspostService.getFacebookNameByHash(hash); + break; + case "vk": + account = crosspostService.getVKNameByHash(hash); + break; + case "xmpp": + account = crosspostService.getJIDByHash(hash); + break; + case "durov": + account = crosspostService.getTelegramNameByHash(hash); + break; + case "messenger": + account = messengerService.getDisplayName(hash); + break; + case "email": + account = emailService.getEmailByAuthCode(hash); + } + if (account == null) { + throw new HttpBadRequestException(); + } + + model.addAttribute("title", "Новый пользователь"); + model.addAttribute("visitor", visitor); + model.addAttribute("account", account); + model.addAttribute("type", type); + model.addAttribute("hash", hash); + return "views/signup"; + } + + @PostMapping("/signup") + protected String doPost( + HttpServletResponse response, + @RequestParam String type, + @RequestParam String hash, + @RequestParam String action, + @RequestParam(required = false) String username, + @RequestParam(required = false) String password) { + com.juick.User visitor = UserUtils.getCurrentUser(); + int uid = 0; + + if (hash.length() > 36 || !type.matches("^[a-zA-Z0-9\\-]+$") || !hash.matches("^[a-zA-Z0-9\\-]+$")) { + throw new HttpBadRequestException(); + } + + if (action.charAt(0) == 'l') { + + if (visitor.isAnonymous()) { + if (username.length() > 32) { + throw new HttpBadRequestException(); + } + uid = userService.checkPassword(username, password); + } else { + uid = visitor.getUid(); + } + + if (uid <= 0) { + throw new HttpForbiddenException(); + } + + if (!(type.charAt(0) == 'f' && crosspostService.setFacebookUser(hash, uid)) + && !(type.charAt(0) == 'v' && crosspostService.setVKUser(hash, uid)) + && !(type.charAt(0) == 'd' && crosspostService.setTelegramUser(hash, uid)) + && !(type.charAt(0) == 'x' && crosspostService.setJIDUser(hash, uid)) + && !(type.charAt(0) == 'm' && messengerService.linkMessengerUser(hash, uid))) { + if (type.equals("email")) { + String email = emailService.getEmailByAuthCode(hash); + emailService.addEmail(uid, email); + emailService.deleteAuthCode(hash); + } else { + throw new HttpBadRequestException(); + } + } + + } else { // Create new account + if (username.length() < 2 || username.length() > 16 || !username.matches("^[a-zA-Z0-9\\-]+$") || password.length() < 6 || password.length() > 32) { + throw new HttpBadRequestException(); + } + + // CHECK USERNAME + + uid = userService.createUser(username, password); + if (uid <= 0) { + throw new HttpBadRequestException(); + } + + if (!(type.charAt(0) == 'f' && crosspostService.setFacebookUser(hash, uid)) + && !(type.charAt(0) == 'v' && crosspostService.setVKUser(hash, uid)) + && !(type.charAt(0) == 'd' && crosspostService.setTelegramUser(hash, uid)) + && !(type.charAt(0) == 'm' && messengerService.linkMessengerUser(hash, uid))) { + if (type.equals("email")) { + String email = emailService.getEmailByAuthCode(hash); + emailService.addEmail(uid, email); + emailService.deleteAuthCode(hash); + } else { + throw new HttpBadRequestException(); + } + } + } + + if (visitor.isAnonymous()) { + hash = userService.getHashByUID(uid); + Cookie c = new Cookie("hash", hash); + c.setMaxAge(365 * 24 * 60 * 60); + response.addCookie(c); + } + return "redirect:/"; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java b/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java new file mode 100644 index 00000000..014a728d --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/controllers/SocialLogin.java @@ -0,0 +1,321 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +package com.juick.server.www.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.scribejava.apis.FacebookApi; +import com.github.scribejava.apis.TwitterApi; +import com.github.scribejava.apis.VkontakteApi; +import com.github.scribejava.core.builder.ServiceBuilder; +import com.github.scribejava.core.model.*; +import com.github.scribejava.core.oauth.OAuth10aService; +import com.github.scribejava.core.oauth.OAuth20Service; +import com.juick.server.util.HttpBadRequestException; +import com.juick.server.util.UserUtils; +import com.juick.service.CrosspostService; +import com.juick.service.EmailService; +import com.juick.service.TelegramService; +import com.juick.service.UserService; +import com.juick.server.www.Utils; +import com.juick.server.www.facebook.User; +import com.juick.server.www.vk.UsersResponse; +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.codec.digest.HmacAlgorithms; +import org.apache.commons.codec.digest.HmacUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.math.NumberUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.CookieValue; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; + +/** + * + * @author Ugnich Anton + */ +@Controller +public class SocialLogin { + + private static final Logger logger = LoggerFactory.getLogger(SocialLogin.class); + + @Value("${facebook_appid:appid}") + private String FACEBOOK_APPID; + @Value("${facebook_secret:secret}") + private String FACEBOOK_SECRET; + private static final String FACEBOOK_REDIRECT = "https://juick.com/_fblogin"; + private static final String VK_REDIRECT = "http://juick.com/_vklogin"; + private static final String TWITTER_VERIFY_URL = "https://api.twitter.com/1.1/account/verify_credentials.json"; + @Inject + private ObjectMapper jsonMapper; + private ServiceBuilder facebookBuilder, twitterBuilder, vkBuilder; + + @Value("${twitter_consumer_key:appid}") + private String twitterConsumerKey; + @Value("${twitter_consumer_secret:secret}") + private String twitterConsumerSecret; + @Value("${vk_appid:appid}") + private String VK_APPID; + @Value("${vk_secret:secret}") + private String VK_SECRET; + @Value("${telegram_token:secret}") + private String telegramToken; + + @Inject + private CrosspostService crosspostService; + @Inject + private UserService userService; + @Inject + private EmailService emailService; + @Inject + private TelegramService telegramService; + + @PostConstruct + public void init() { + facebookBuilder = new ServiceBuilder(FACEBOOK_APPID); + twitterBuilder = new ServiceBuilder(twitterConsumerKey); + vkBuilder = new ServiceBuilder(VK_APPID); + } + + @GetMapping("/_fblogin") + protected String doFacebookLogin(HttpServletRequest request, + @RequestParam(required = false) String code, + @RequestParam(required = false) String state, + HttpServletResponse response) throws IOException, ExecutionException, InterruptedException { + if (StringUtils.isBlank(code)) { + String fbstate = UUID.randomUUID().toString(); + crosspostService.addFacebookState(fbstate, state); + OAuth20Service facebookAuthService = facebookBuilder + .apiSecret(FACEBOOK_SECRET) + .callback(FACEBOOK_REDIRECT) + .scope("email") + .state(fbstate) + .build(FacebookApi.instance()); + return "redirect:" + facebookAuthService.getAuthorizationUrl(); + } + + String redirectUrl = crosspostService.verifyFacebookState(state); + if (StringUtils.isEmpty(redirectUrl)) { + logger.error("state is missing"); + throw new HttpBadRequestException(); + } + OAuth20Service facebookService = facebookBuilder + .apiKey(FACEBOOK_APPID) + .apiSecret(FACEBOOK_SECRET) + .callback(FACEBOOK_REDIRECT) + .scope("email") + .state(state) + .build(FacebookApi.instance()); + OAuth2AccessToken token = facebookService.getAccessToken(code); + final OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://graph.facebook.com/v2.10/me?fields=id,name,link,verified,email"); + facebookService.signRequest(token, meRequest); + String graph = facebookService.execute(meRequest).getBody(); + if (StringUtils.isBlank(graph)) { + logger.error("FACEBOOK GRAPH ERROR"); + throw new HttpBadRequestException(); + } + User fb = jsonMapper.readValue(graph, User.class); + long fbID = NumberUtils.toLong(fb.getId(), 0); + if (fbID == 0 || StringUtils.isBlank(fb.getName()) || StringUtils.isBlank(fb.getLink())) { + logger.error("Missing required fields, id: {}, name: {}, link: {}", fbID, fb.getName(), fb.getLink()); + throw new HttpBadRequestException(); + } + + int uid = crosspostService.getUIDbyFBID(fbID); + if (uid > 0) { + if (!crosspostService.updateFacebookUser(fbID, token.getAccessToken(), fb.getName(), fb.getLink())) { + logger.error("error updating facebook user, id: {}, token: {}", fbID, token.getAccessToken()); + throw new HttpBadRequestException(); + } + Cookie c = new Cookie("hash", userService.getHashByUID(uid)); + c.setMaxAge(50 * 24 * 60 * 60); + response.addCookie(c); + return "redirect:" + redirectUrl; + } else if (fb.getVerified()) { + if (!crosspostService.createFacebookUser(fbID, state, token.getAccessToken(), fb.getName(), fb.getLink())) { + if (StringUtils.isNotEmpty(fb.getEmail())) { + logger.info("found {} for facebook user {}", fb.getEmail(), fb.getLink()); + Integer userId = crosspostService.getUIDbyFBID(fbID); + if (!emailService.getEmails(userId, false).contains(fb.getEmail())) { + emailService.addEmail(userId, fb.getEmail()); + } + } + logger.info("email not found for facebook user {}", fb.getLink()); + throw new HttpBadRequestException(); + } + return "redirect:/signup?type=fb&hash=" + state; + } else { + logger.error("Facebook account is not verified, id: {}", fbID); + throw new HttpBadRequestException(); + } + } + @GetMapping("/_twitter") + protected void doTwitterLogin(HttpServletRequest request, HttpServletResponse response) + throws IOException, ExecutionException, InterruptedException { + String hash = StringUtils.EMPTY, request_token = StringUtils.EMPTY, request_token_secret = StringUtils.EMPTY; + String verifier = request.getParameter("oauth_verifier"); + Cookie[] cookies = request.getCookies(); + for (Cookie cookie : cookies) { + if (cookie.getName().equals("hash")) { + hash = cookie.getValue(); + } + if (cookie.getName().equals("request_token")) { + request_token = cookie.getValue(); + } + if (cookie.getName().equals("request_token_secret")) { + request_token_secret = cookie.getValue(); + } + } + com.juick.User user = UserUtils.getCurrentUser(); + OAuth10aService oAuthService = twitterBuilder + .apiSecret(twitterConsumerSecret) + .callback("http://juick.com/_twitter") + .build(TwitterApi.instance()); + + if (request_token.isEmpty() && request_token_secret.isEmpty() + && (verifier == null || verifier.isEmpty())) { + OAuth1RequestToken requestToken = oAuthService.getRequestToken(); + String authUrl = oAuthService.getAuthorizationUrl(requestToken); + response.addCookie(new Cookie("request_token", requestToken.getToken())); + response.addCookie(new Cookie("request_token_secret", requestToken.getTokenSecret())); + response.setStatus(HttpServletResponse.SC_FOUND); + response.setHeader("Location", authUrl); + } else { + if (verifier != null && verifier.length() > 0) { + OAuth1RequestToken requestToken = new OAuth1RequestToken(request_token, request_token_secret); + OAuth1AccessToken accessToken = oAuthService.getAccessToken(requestToken, verifier); + OAuthRequest oAuthRequest = new OAuthRequest(Verb.GET, TWITTER_VERIFY_URL); + oAuthService.signRequest(accessToken, oAuthRequest); + com.juick.server.www.twitter.User twitterUser = jsonMapper.readValue(oAuthService.execute(oAuthRequest).getBody(), + com.juick.server.www.twitter.User.class); + if (userService.linkTwitterAccount(user, accessToken.getToken(), accessToken.getTokenSecret(), + twitterUser.getScreenName())) { + response.setStatus(HttpServletResponse.SC_FOUND); + response.setHeader("Location", "http://juick.com/settings"); + } else { + response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + } + } + @GetMapping("/_vklogin") + protected String doVKLogin(HttpServletRequest request, + @RequestParam(required = false) String code, + @RequestParam(required = false) String state, + @CookieValue(required = false) String vkstate, + HttpServletResponse response) throws IOException, ExecutionException, InterruptedException { + if (StringUtils.isBlank(code)) { + vkstate = UUID.randomUUID().toString(); + Cookie c = new Cookie("vkstate", vkstate); + response.addCookie(c); + OAuth20Service vkAuthService = vkBuilder + .apiSecret(VK_SECRET) + .scope("friends,wall,offline") + .state(vkstate) + .callback(VK_REDIRECT) + .build(VkontakteApi.instance()); + return "redirect:" + vkAuthService.getAuthorizationUrl(); + } + + if (StringUtils.isBlank(vkstate) || !vkstate.equals(state)) { + throw new HttpBadRequestException(); + } else { + Cookie c = new Cookie("vkstate", "-"); + c.setMaxAge(0); + response.addCookie(c); + } + + OAuth20Service vkService = vkBuilder + .apiKey(VK_APPID) + .apiSecret(VK_SECRET) + .build(VkontakteApi.instance()); + OAuth2AccessToken token = vkService.getAccessToken(code); + + OAuthRequest meRequest = new OAuthRequest(Verb.GET, "https://api.vk.com/method/users.get?fields=screen_name&v=5.73"); + vkService.signRequest(token, meRequest); + String graph = vkService.execute(meRequest).getBody(); + + com.juick.server.www.vk.User jsonUser = jsonMapper.readValue(graph, UsersResponse.class).getUsers().get(0); + String vkName = jsonUser.getFirstName() + " " + jsonUser.getLastName(); + String vkLink = jsonUser.getScreenName(); + + if (vkName.length() == 1 || StringUtils.isBlank(vkLink)) { + logger.error("vk user error"); + throw new HttpBadRequestException(); + } + + Long vkID = NumberUtils.toLong(jsonUser.getId(), 0); + int uid = crosspostService.getUIDbyVKID(vkID); + if (uid > 0) { + Cookie c = new Cookie("hash", userService.getHashByUID(uid)); + c.setMaxAge(50 * 24 * 60 * 60); + response.addCookie(c); + return Utils.getPreviousPageByRequest(request).orElse("redirect:/"); + } else { + String loginhash = UUID.randomUUID().toString(); + if (!crosspostService.createVKUser(vkID, loginhash, token.getAccessToken(), vkName, vkLink)) { + logger.error("create vk user error"); + throw new HttpBadRequestException(); + } + return "redirect:/signup?type=vk&hash=" + loginhash; + } + } + + @GetMapping("/_tglogin") + public String doDurovLogin(HttpServletRequest request, + @RequestParam Map params, + HttpServletResponse response) { + String dataCheckString = params.entrySet().stream() + .filter(p -> !p.getKey().equals("hash")) + .sorted(Map.Entry.comparingByKey()) + .map(p -> p.getKey() + "=" + p.getValue()) + .collect(Collectors.joining("\n")); + String hash = params.get("hash"); + byte[] secretKey = DigestUtils.sha256(telegramToken); + String resultString = new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secretKey).hmacHex(dataCheckString); + if (hash.equals(resultString)) { + Long tgUser = Long.valueOf(params.get("id")); + int uid = telegramService.getUser(tgUser); + if (uid > 0) { + Cookie c = new Cookie("hash", userService.getHashByUID(uid)); + c.setMaxAge(50 * 24 * 60 * 60); + response.addCookie(c); + return Utils.getPreviousPageByRequest(request).orElse("redirect:/"); + } else { + String username = StringUtils.defaultString(params.get("username"), params.get("first_name")); + telegramService.createTelegramUser(tgUser, username); + return "redirect:/signup?type=durov&hash=" + userService.getSignUpHashByTelegramID(tgUser, username); + } + } else { + logger.warn("invalid tg hash {} for {}", resultString, hash); + } + throw new HttpBadRequestException(); + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/facebook/User.java b/juick-server/src/main/java/com/juick/server/www/facebook/User.java new file mode 100644 index 00000000..b85cf65c --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/facebook/User.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.www.facebook; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vitalyster on 28.11.2016. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class User { + private String id; + private String name; + private String link; + private boolean verified; + private String firstName; + private String lastName; + private String gender; + private String locale; + private String timezone; + private String updatedTime; + private String email; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getLink() { + return link; + } + + public void setLink(String link) { + this.link = link; + } + + public boolean getVerified() { + return verified; + } + + public void setVerified(boolean verified) { + this.verified = verified; + } + + @JsonProperty("first_name") + public String getFirstName() { + return firstName; + } + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getGender() { + return gender; + } + + public void setGender(String gender) { + this.gender = gender; + } + + @JsonProperty("last_name") + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getLocale() { + return locale; + } + + public void setLocale(String locale) { + this.locale = locale; + } + + public String getTimezone() { + return timezone; + } + + public void setTimezone(String timezone) { + this.timezone = timezone; + } + + @JsonProperty("updated_time") + public String getUpdatedTime() { + return updatedTime; + } + + public void setUpdatedTime(String updatedTime) { + this.updatedTime = updatedTime; + } + + public String getEmail() { + return email; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/twitter/User.java b/juick-server/src/main/java/com/juick/server/www/twitter/User.java new file mode 100644 index 00000000..35f708f7 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/twitter/User.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.www.twitter; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vitalyster on 28.11.2016. + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class User { + private String screenName; + + @JsonProperty("screen_name") + public String getScreenName() { + return screenName; + } + + public void setScreenName(String screenName) { + this.screenName = screenName; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/vk/Token.java b/juick-server/src/main/java/com/juick/server/www/vk/Token.java new file mode 100644 index 00000000..c6277245 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/vk/Token.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.www.vk; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vitalyster on 28.11.2016. + */ +public class Token { + private Long userId; + private String accessToken; + private String expiresIn; + + @JsonProperty("user_id") + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + @JsonProperty("access_token") + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + @JsonProperty("expires_in") + public String getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(String expiresIn) { + this.expiresIn = expiresIn; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/vk/User.java b/juick-server/src/main/java/com/juick/server/www/vk/User.java new file mode 100644 index 00000000..0c491166 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/vk/User.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.www.vk; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Created by vitalyster on 28.11.2016. + */ +public class User { + private String id; + private String firstName; + private String lastName; + private String screenName; + + @JsonProperty("first_name") + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + @JsonProperty("last_name") + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + @JsonProperty("screen_name") + public String getScreenName() { + return screenName; + } + + public void setScreenName(String screenName) { + this.screenName = screenName; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } +} diff --git a/juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java b/juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java new file mode 100644 index 00000000..d16b9921 --- /dev/null +++ b/juick-server/src/main/java/com/juick/server/www/vk/UsersResponse.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.www.vk; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Created by vitalyster on 28.11.2016. + */ +public class UsersResponse { + private List users; + + @JsonProperty("response") + public List getUsers() { + return users; + } + + public void setUsers(List users) { + this.users = users; + } +} diff --git a/juick-server/src/main/java/com/juick/service/EmailServiceImpl.java b/juick-server/src/main/java/com/juick/service/EmailServiceImpl.java index 0cccc915..5ba44e24 100644 --- a/juick-server/src/main/java/com/juick/service/EmailServiceImpl.java +++ b/juick-server/src/main/java/com/juick/service/EmailServiceImpl.java @@ -89,4 +89,20 @@ public class EmailServiceImpl extends BaseJdbcService implements EmailService { return getJdbcTemplate().queryForList("SELECT email FROM emails WHERE user_id=? " + (active ? "AND subscr_hour IS NOT NULL" : ""), String.class, userId); } + @Transactional(readOnly = true) + @Override + public String getEmailByAuthCode(String code) { + try { + return getJdbcTemplate().queryForObject("SELECT account FROM auth WHERE protocol='email' AND authcode=?", String.class, code); + } catch (EmptyResultDataAccessException e) { + return StringUtils.EMPTY; + } + } + + @Transactional + @Override + public void deleteAuthCode(String code) { + getJdbcTemplate().update("DELETE FROM auth WHERE authcode=?", code); + } + } diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java new file mode 100644 index 00000000..9189c2be --- /dev/null +++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/FormatterExtension.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.mitchellbosecke.pebble.extension; + +import com.mitchellbosecke.pebble.extension.filters.*; + +import java.util.HashMap; +import java.util.Map; + +/** + * Created by vitalyster on 04.05.2017. + */ +public class FormatterExtension extends AbstractExtension { + @Override + public Map getFilters() { + Map filters = new HashMap<>(); + filters.put("formatMessage", new FormatMessageFilter()); + filters.put("prettyTime", new PrettyTimeFilter()); + filters.put("timestamp", new TimestampFilter()); + filters.put("tagsList", new TagsListFilter()); + return filters; + } +} diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java new file mode 100644 index 00000000..5b5291f1 --- /dev/null +++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/FormatMessageFilter.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.mitchellbosecke.pebble.extension.filters; + +import com.juick.Message; +import com.juick.util.MessageUtils; +import com.mitchellbosecke.pebble.extension.Filter; +import com.mitchellbosecke.pebble.extension.escaper.SafeString; +import com.mitchellbosecke.pebble.template.EvaluationContext; +import com.mitchellbosecke.pebble.template.PebbleTemplate; +import org.apache.commons.lang3.StringUtils; + +import java.util.List; +import java.util.Map; + +/** + * Created by vitalyster on 04.05.2017. + */ +public class FormatMessageFilter implements Filter { + @Override + public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + if (input instanceof Message) { + Message msg = (Message) input; + boolean isCode = msg.getTags().stream().anyMatch(t -> t.getName().equals("code")); + String formatString = MessageUtils.replyStartsWithQuote(msg) ? "@%s,\n%s" : "@%s, %s"; + String msgTxt = msg.getRid() > 0 ? String.format(formatString, msg.getTo().getName(), StringUtils.defaultString(msg.getText())) + : StringUtils.defaultString(msg.getText()); + String formattedMessage = isCode ? MessageUtils.formatMessageCode(msgTxt) + : MessageUtils.formatMessage(msgTxt); + return new SafeString(formattedMessage); + } + throw new IllegalArgumentException("invalid input"); + } + + @Override + public List getArgumentNames() { + return null; + } +} diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java new file mode 100644 index 00000000..72dab20d --- /dev/null +++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/PrettyTimeFilter.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.mitchellbosecke.pebble.extension.filters; + +import com.juick.util.PrettyTimeFormatter; +import com.mitchellbosecke.pebble.extension.Filter; +import com.mitchellbosecke.pebble.template.EvaluationContext; +import com.mitchellbosecke.pebble.template.PebbleTemplate; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Created by vitalyster on 04.05.2017. + */ +public class PrettyTimeFilter implements Filter { + + PrettyTimeFormatter formatter = new PrettyTimeFormatter(); + + @Override + public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + if (input instanceof Instant) { + Locale locale = context.getLocale(); + return formatter.format(locale, Date.from((Instant)input)); + } + throw new IllegalArgumentException("invalid input"); + } + + @Override + public List getArgumentNames() { + return null; + } +} diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java new file mode 100644 index 00000000..c7b00ea3 --- /dev/null +++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TagsListFilter.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.mitchellbosecke.pebble.extension.filters; + +import com.juick.Tag; +import com.mitchellbosecke.pebble.extension.Filter; +import com.mitchellbosecke.pebble.template.EvaluationContext; +import com.mitchellbosecke.pebble.template.PebbleTemplate; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Created by vitalyster on 23.05.2017. + */ +public class TagsListFilter implements Filter { + @SuppressWarnings("unchecked") + @Override + public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + return ((List) input).stream().map(Tag::getName).collect(Collectors.toList()); + } + + @Override + public List getArgumentNames() { + return null; + } +} diff --git a/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java new file mode 100644 index 00000000..5f98c167 --- /dev/null +++ b/juick-server/src/main/java/com/mitchellbosecke/pebble/extension/filters/TimestampFilter.java @@ -0,0 +1,25 @@ +package com.mitchellbosecke.pebble.extension.filters; + +import com.mitchellbosecke.pebble.extension.Filter; +import com.mitchellbosecke.pebble.template.EvaluationContext; +import com.mitchellbosecke.pebble.template.PebbleTemplate; + +import java.time.Instant; +import java.util.Date; +import java.util.List; +import java.util.Map; + +public class TimestampFilter implements Filter { + @Override + public Object apply(Object input, Map args, PebbleTemplate self, EvaluationContext context, int lineNumber) { + if (input instanceof Instant) { + return Date.from((Instant)input); + } + throw new IllegalArgumentException("invalid input"); + } + + @Override + public List getArgumentNames() { + return null; + } +} diff --git a/juick-server/src/main/java/ru/sape/Sape.java b/juick-server/src/main/java/ru/sape/Sape.java new file mode 100644 index 00000000..38577c45 --- /dev/null +++ b/juick-server/src/main/java/ru/sape/Sape.java @@ -0,0 +1,23 @@ +/* + * http://code.google.com/p/javasape/ + */ +package ru.sape; + +public class Sape { + + private final String sapeUser; + private final SapeConnection sapePageLinkConnection; + + public Sape(String sapeUser, String host, int socketTimeout, int cacheLifeTime) { + this.sapeUser = sapeUser; + + this.sapePageLinkConnection = new SapeConnection( + "/code.php?user=" + sapeUser + "&host=" + host, + "SAPE_Client PHP", socketTimeout, cacheLifeTime); + } + public boolean debug = false; + + public SapePageLinks getPageLinks(String requestUri, String cookie) { + return new SapePageLinks(sapePageLinkConnection, sapeUser, requestUri, cookie, debug); + } +} diff --git a/juick-server/src/main/java/ru/sape/SapeConnection.java b/juick-server/src/main/java/ru/sape/SapeConnection.java new file mode 100644 index 00000000..a15658fa --- /dev/null +++ b/juick-server/src/main/java/ru/sape/SapeConnection.java @@ -0,0 +1,108 @@ +package ru.sape; + +import com.github.ooxi.phparser.SerializedPhpParser; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringWriter; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.*; + +public class SapeConnection { + private static final Logger logger = LoggerFactory.getLogger(SapeConnection.class); + private final String version = "1.0.3"; + private final List serverList = Arrays.asList("dispenser-01.sape.ru", "dispenser-02.sape.ru"); + private final String dispenserPath; + private final String userAgent; + private final int socketTimeout; + private final int cacheLifeTime; + + public SapeConnection(String dispenserPath, String userAgent, int socketTimeout, int cacheLifeTime) { + this.dispenserPath = dispenserPath; + this.userAgent = userAgent; + this.socketTimeout = socketTimeout; + this.cacheLifeTime = cacheLifeTime; + } + + protected String fetchRemoteFile(String host, String path) throws IOException { + Reader r = null; + + try { + HttpURLConnection connection = (HttpURLConnection) ((new URL(("http://" + host + path)).openConnection())); + + if (socketTimeout > 0) { + connection.setConnectTimeout(socketTimeout); + connection.setReadTimeout(socketTimeout); + } + + connection.addRequestProperty("User-Agent", userAgent + ' ' + version); + + connection.setDoOutput(true); + connection.setDoInput(true); + connection.setUseCaches(false); + connection.setRequestMethod("GET"); + connection.connect(); + + r = new BufferedReader(new InputStreamReader(connection.getInputStream(), "UTF-8")); + + StringWriter sw = new StringWriter(); + + int b; + + while ((b = r.read()) != -1) { + sw.write(b); + } + + return sw.toString(); + } finally { + if (r != null) { + r.close(); + } + } + } + Map cached; + long cacheUpdated; + + @SuppressWarnings("unchecked") + public Map getData() { + if (cacheLifeTime <= (System.currentTimeMillis() - cacheUpdated) / 1000) { + for (String server : serverList) { + String data; + + try { + data = fetchRemoteFile(server, dispenserPath + "&charset=UTF-8"); + } catch (IOException e1) { + continue; + } + + if (data.startsWith("FATAL ERROR:")) { + logger.error("Sape responded with error: {}", data); + + continue; + } + + try { + cached = (Map) new SerializedPhpParser(data).parse(); + } catch (Exception e) { + logger.error("Can't parse Sape data", e); + continue; + } + + cacheUpdated = System.currentTimeMillis(); + + return cached; + } + + logger.error("Unable to fetch Sape data"); + + return Collections.emptyMap(); + } + + return cached; + } +} diff --git a/juick-server/src/main/java/ru/sape/SapePageLinks.java b/juick-server/src/main/java/ru/sape/SapePageLinks.java new file mode 100644 index 00000000..e89b4e71 --- /dev/null +++ b/juick-server/src/main/java/ru/sape/SapePageLinks.java @@ -0,0 +1,76 @@ +package ru.sape; + +import java.util.*; + +public class SapePageLinks { + + private boolean showCode; + + public SapePageLinks(SapeConnection sapeConnection, String sapeUser, String requestUri, String sapeCookie) { + this(sapeConnection, sapeUser, requestUri, sapeCookie, false); + } + + @SuppressWarnings("unchecked") + public SapePageLinks(SapeConnection sapeConnection, String sapeUser, String requestUri, String sapeCookie, boolean showCode) { + if (sapeUser.equals(sapeCookie)) { + showCode = true; + } + + Map data = sapeConnection.getData(); + + if (data.containsKey("__sape_delimiter__")) { + linkDelimiter = (String) data.get("__sape_delimiter__"); + } + + if (data.containsKey(requestUri)) { + pageLinks = new ArrayList<>(((Map) data.get(requestUri)).values()); + } + + if (data.containsKey("__sape_new_url__")) { + if (showCode) { + Object newUrl = data.get("__sape_new_url__"); + + if (newUrl instanceof Map) { + pageLinks = new ArrayList<>(((Map) newUrl).values()); + } else { + pageLinks = new ArrayList<>(Collections.singletonList((String) newUrl)); + } + } + } + + this.showCode = showCode; + } + private String linkDelimiter = "."; + private List pageLinks = new ArrayList<>(); + + public String render() { + return render(-1); + } + + public String render(int count) { + StringBuilder s = new StringBuilder(); + + if (count < 0) { + count = pageLinks.size(); + } + + for (Iterator i = pageLinks.iterator(); i.hasNext() && count > 0; count--) { + if (s.length() > 0) { + s.append(linkDelimiter); + } + + String l = i.next(); + + s.append(l); + + i.remove(); + } + + if (showCode) { + s.insert(0, ""); + s.append(""); + } + + return s.toString(); + } +} diff --git a/juick-server/src/main/resources/errors.properties b/juick-server/src/main/resources/errors.properties new file mode 100644 index 00000000..7ec8fbfd --- /dev/null +++ b/juick-server/src/main/resources/errors.properties @@ -0,0 +1,3 @@ +error.title = Error page + +error.login=Wrong user or password \ No newline at end of file diff --git a/juick-server/src/main/resources/errors_ru.properties b/juick-server/src/main/resources/errors_ru.properties new file mode 100644 index 00000000..ca13b926 --- /dev/null +++ b/juick-server/src/main/resources/errors_ru.properties @@ -0,0 +1,3 @@ +error.title = Произошла ошибка + +error.login=Произошла ошибка, проверьте имя пользователя и пароль \ No newline at end of file diff --git a/juick-server/src/main/resources/help b/juick-server/src/main/resources/help new file mode 160000 index 00000000..ce103cd9 --- /dev/null +++ b/juick-server/src/main/resources/help @@ -0,0 +1 @@ +Subproject commit ce103cd9a2a8a200c6ebb3b41525e7c8f639d98c diff --git a/juick-server/src/main/resources/messages.properties b/juick-server/src/main/resources/messages.properties new file mode 100644 index 00000000..cfd8a826 --- /dev/null +++ b/juick-server/src/main/resources/messages.properties @@ -0,0 +1,80 @@ +date.format=MM/dd/yyyy + +link.settings=Settings +link.returnToMain=Back to Home Page +link.contacts=Contacts +link.tos=TOS +link.adv=Advertisement + +link.popular=Popular +link.allMessages=Discover +link.withPhotos=Photos +link.trends=Trends +link.my=My feed +link.privateMessages=PM +link.discuss=Discuss +link.recommended=Recommended +link.postMessage=Post +link.Login=Login +link.logout=Logout + +link.settings.main=Main +link.settings.password=Password +link.settings.about=About + +label.sponsor=Sponsor +label.sponsors=Sponsors +label.search=Search +label.register=Register +label.username=User name +label.password=Password + +postForm.newMessage=New message... +postForm.imageLink=Link to image +postForm.imageFormats=JPG/PNG, up to 10 MB +postForm.or=or +postForm.upload=Upload +postForm.tags=Tags (space separated) +postForm.submit=Send + +message.recommend=Recommend +message.recommendedBy=♡ recommended by +message.recommendedOthers=and {0} others +message.comment=Comment +message.writeComment=Write a comment... +message.share=Share +message.subscribe=Subscribe +message.subscribed=Subscribed +message.delete=Delete +message.loginForSending= to post messages and comments +message.sendLoginToXmpp=Send LOGIN to juick@juick.com + +messages.next=Next + +reply.reply=Reply +reply.inReplyTo=in reply to +reply.replies=Replies + +replies.showAsList=Show as list +replies.showAsTree=Show as tree +replies.unfoldAll=Unfold all + +question.areRegistered=Already registered? + +title.help=Help +title.loginOrSignup=Juick - Log In or Sign Up +title.index.anonym=Juick microblogs: popular posts +title.index.user=Popular + +error.pageNotFound=Page not found +error.pageNotFound.description=User probably deleted this post, or this page never existed. + +blog.blog=Blog +blog.recommendations=Recommendations +blog.photos=Photos +blog.iread=I read +blog.readers=My readers +blog.bl=My blacklist +blog.messages=Messages +blog.comments=Comments +blog.allPostsWithTag=All posts tagged \ No newline at end of file diff --git a/juick-server/src/main/resources/messages_ru.properties b/juick-server/src/main/resources/messages_ru.properties new file mode 100644 index 00000000..2a2269ae --- /dev/null +++ b/juick-server/src/main/resources/messages_ru.properties @@ -0,0 +1,78 @@ +date.format=dd.MM.yyyy + +link.settings=Настройки +link.returnToMain=Вернуться на главную +link.contacts=Контакты +link.tos=TOS + +link.popular=Популярные +link.allMessages=Обзор +link.withPhotos=Фото +link.trends=Темы +link.my=Моя лента +link.privateMessages=Приватные +link.discuss=Диалоги +link.recommended=Рекомендации +link.postMessage=Написать +link.Login=Войти +link.logout=Выйти + +link.settings.main=Главная +link.settings.password=Пароль +link.settings.about=О пользователе + +label.sponsor=Спонсор +label.sponsors=Спонсоры +label.search=Поиск +label.register=Зарегистрироваться +label.username=Имя пользователя +label.password=Пароль + +postForm.newMessage=Новое сообщение... +postForm.imageLink=Ссылка на изображение +postForm.imageFormats=JPG/PNG, до 10Мб +postForm.or=или +postForm.upload=загрузить +postForm.tags=Теги (через пробел) +postForm.submit=Отправить + +message.recommend=Рекомендовать +message.recommendedBy=♡ рекомендовали +message.recommendedOthers=и еще {0} +message.comment=Комментировать +message.writeComment=Написать комментарий... +message.share=Поделиться +message.subscribe=Подписаться +message.subscribed=Подписан +message.delete=Удалить +message.loginForSending=Чтобы добавлять сообщения и комментарии, +message.sendLoginToXmpp=Отправьте LOGIN на juick@juick.com + +messages.next=Читать дальше + +reply.reply=Ответить +reply.inReplyTo=в ответ на +reply.replies=Ответы +replies.showAsList=Показать списком +replies.showAsTree=Показать деревом +replies.unfoldAll=Раскрыть все + +question.areRegistered=Уже зарегистрированы? + +title.help=Справка +title.loginOrSignup=Juick - Войдите в систему или зарегистрируйтесь +title.index.anonym=Микроблоги Juick: популярные записи +title.index.user=Популярные + +error.pageNotFound=Страница не найдена +error.pageNotFound.description=Сожалеем, но страницу с этим адресом удалил её автор, либо её никогда не существовало. + +blog.blog=Блог +blog.recommendations=Рекомендации +blog.photos=Фотографии +blog.iread=Я читаю +blog.readers=Мои подписчики +blog.bl=Черный список +blog.messages=Сообщения +blog.comments=Комментарии +blog.allPostsWithTag=Все записи с тегом \ No newline at end of file diff --git a/juick-server/src/main/resources/schema.sql b/juick-server/src/main/resources/schema.sql index 851b764b..2e8fad9b 100644 --- a/juick-server/src/main/resources/schema.sql +++ b/juick-server/src/main/resources/schema.sql @@ -23,12 +23,12 @@ CREATE TABLE IF NOT EXISTS `bl_users` ( ); CREATE TABLE IF NOT EXISTS `facebook` ( `user_id` int(10) unsigned DEFAULT NULL, - `fb_id` bigint(20) unsigned NOT NULL, + `fb_id` bigint(20) unsigned NULL, `loginhash` char(36) DEFAULT NULL, `access_token` char(255) DEFAULT NULL, `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, - `fb_name` char(64) NOT NULL, - `fb_link` char(255) NOT NULL, + `fb_name` char(64) NULL, + `fb_link` char(255) NULL, `crosspost` tinyint(1) unsigned NOT NULL DEFAULT '1' ); diff --git a/juick-server/src/main/resources/static/favicon.png b/juick-server/src/main/resources/static/favicon.png new file mode 100644 index 00000000..bc7161e2 Binary files /dev/null and b/juick-server/src/main/resources/static/favicon.png differ diff --git a/juick-server/src/main/resources/static/logo.png b/juick-server/src/main/resources/static/logo.png new file mode 100644 index 00000000..933f6099 Binary files /dev/null and b/juick-server/src/main/resources/static/logo.png differ diff --git a/juick-server/src/main/resources/static/style.js b/juick-server/src/main/resources/static/style.js new file mode 100644 index 00000000..8e1ce009 --- /dev/null +++ b/juick-server/src/main/resources/static/style.js @@ -0,0 +1,2 @@ +!function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=10)}({10:function(e,t,n){n(11),n(13),e.exports=n(15)},11:function(e,t,n){},13:function(e,t,n){},15:function(e,t,n){}}); +//# sourceMappingURL=style.js.map \ No newline at end of file diff --git a/juick-server/src/main/resources/static/style.js.map b/juick-server/src/main/resources/static/style.js.map new file mode 100644 index 00000000..f3493280 --- /dev/null +++ b/juick-server/src/main/resources/static/style.js.map @@ -0,0 +1 @@ +{"version":3,"sources":["webpack:///webpack/bootstrap"],"names":["installedModules","__webpack_require__","moduleId","exports","module","i","l","modules","call","m","c","d","name","getter","o","Object","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","prototype","hasOwnProperty","p","s"],"mappings":"aACA,IAAAA,KAGA,SAAAC,EAAAC,GAGA,GAAAF,EAAAE,GACA,OAAAF,EAAAE,GAAAC,QAGA,IAAAC,EAAAJ,EAAAE,IACAG,EAAAH,EACAI,GAAA,EACAH,YAUA,OANAI,EAAAL,GAAAM,KAAAJ,EAAAD,QAAAC,IAAAD,QAAAF,GAGAG,EAAAE,GAAA,EAGAF,EAAAD,QAKAF,EAAAQ,EAAAF,EAGAN,EAAAS,EAAAV,EAGAC,EAAAU,EAAA,SAAAR,EAAAS,EAAAC,GACAZ,EAAAa,EAAAX,EAAAS,IACAG,OAAAC,eAAAb,EAAAS,GAA0CK,YAAA,EAAAC,IAAAL,KAK1CZ,EAAAkB,EAAA,SAAAhB,GACA,oBAAAiB,eAAAC,aACAN,OAAAC,eAAAb,EAAAiB,OAAAC,aAAwDC,MAAA,WAExDP,OAAAC,eAAAb,EAAA,cAAiDmB,OAAA,KAQjDrB,EAAAsB,EAAA,SAAAD,EAAAE,GAEA,GADA,EAAAA,IAAAF,EAAArB,EAAAqB,IACA,EAAAE,EAAA,OAAAF,EACA,KAAAE,GAAA,iBAAAF,QAAAG,WAAA,OAAAH,EACA,IAAAI,EAAAX,OAAAY,OAAA,MAGA,GAFA1B,EAAAkB,EAAAO,GACAX,OAAAC,eAAAU,EAAA,WAAyCT,YAAA,EAAAK,UACzC,EAAAE,GAAA,iBAAAF,EAAA,QAAAM,KAAAN,EAAArB,EAAAU,EAAAe,EAAAE,EAAA,SAAAA,GAAgH,OAAAN,EAAAM,IAAqBC,KAAA,KAAAD,IACrI,OAAAF,GAIAzB,EAAA6B,EAAA,SAAA1B,GACA,IAAAS,EAAAT,KAAAqB,WACA,WAA2B,OAAArB,EAAA,SAC3B,WAAiC,OAAAA,GAEjC,OADAH,EAAAU,EAAAE,EAAA,IAAAA,GACAA,GAIAZ,EAAAa,EAAA,SAAAiB,EAAAC,GAAsD,OAAAjB,OAAAkB,UAAAC,eAAA1B,KAAAuB,EAAAC,IAGtD/B,EAAAkC,EAAA,GAIAlC,IAAAmC,EAAA","file":"style.js","sourcesContent":[" \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(__webpack_require__.s = 10);\n"],"sourceRoot":""} \ No newline at end of file diff --git a/juick-server/src/main/resources/static/tagscloud.png b/juick-server/src/main/resources/static/tagscloud.png new file mode 100644 index 00000000..3e1bf169 Binary files /dev/null and b/juick-server/src/main/resources/static/tagscloud.png differ diff --git a/juick-server/src/main/resources/templates/layouts/content.html b/juick-server/src/main/resources/templates/layouts/content.html new file mode 100644 index 00000000..2ca9fd7e --- /dev/null +++ b/juick-server/src/main/resources/templates/layouts/content.html @@ -0,0 +1,50 @@ + + + + + + + + {% block headers %} + {{ headers | default('') | raw }} + {% endblock %} + {{ title | default('Juick') }} + + + + + + + + + + + + + + + + + + + + + + + + + + 0 %}data-hash="{{visitor.authHash}}"{% endif %}> +{% block body %} +{% endblock %} + + diff --git a/juick-server/src/main/resources/templates/layouts/default.html b/juick-server/src/main/resources/templates/layouts/default.html new file mode 100644 index 00000000..343885c4 --- /dev/null +++ b/juick-server/src/main/resources/templates/layouts/default.html @@ -0,0 +1,16 @@ +{% extends "layouts/content" %} +{% block body %} +{% include "views/partial/navigation" %} +
+
+ {% block content %} + {% endblock %} +
+ +
+{% include "views/partial/footer" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/layouts/minimal.html b/juick-server/src/main/resources/templates/layouts/minimal.html new file mode 100644 index 00000000..15924521 --- /dev/null +++ b/juick-server/src/main/resources/templates/layouts/minimal.html @@ -0,0 +1,10 @@ +{% extends "layouts/content" %} +{% block body %} +
+
+ {% block content %} + {% endblock %} +
+
+{% include "views/partial/footer" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/404.html b/juick-server/src/main/resources/templates/views/404.html new file mode 100644 index 00000000..02a790e6 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/404.html @@ -0,0 +1,11 @@ +{% extends "layouts/default" %} +{% block content %} +
+

Страница не найдена

+

Сожалеем, но страницу с этим адресом удалил её автор, либо её никогда не существовало.

+
+{% endblock %} + +{% block "column" %} +{% include "views/partial/homecolumn" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/blog.html b/juick-server/src/main/resources/templates/views/blog.html new file mode 100644 index 00000000..9cf4714e --- /dev/null +++ b/juick-server/src/main/resources/templates/views/blog.html @@ -0,0 +1,25 @@ +{% extends "layouts/default" %} +{% import "views/macros/tags" %} +{% block content %} +{% if noindex %} + +{% endif %} +{% if paramTag | default('') is not empty %} +

← {{ i18n("messages","blog.allPostsWithTag") }} {{ paramTag.name | escape }}

+{% endif %} +
+ +{% for msg in msgs %} +{% include "views/partial/message" %} +{% endfor %} +
+{% if nextpage | default('') is not empty %} +

+{% endif %} +{% endblock %} +{% block "column" %} +{% include "views/partial/usercolumn" %} +{% if noindex %} + +{% endif %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/blog_tags.html b/juick-server/src/main/resources/templates/views/blog_tags.html new file mode 100644 index 00000000..48e517eb --- /dev/null +++ b/juick-server/src/main/resources/templates/views/blog_tags.html @@ -0,0 +1,10 @@ +{% extends "layouts/default" %} +{% import "views/macros/tags" %} +{% block content %} +

+ {{ tags(user.name, tags) }} +

+{% endblock %} +{% block "column" %} +{% include "views/partial/usercolumn" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/help.html b/juick-server/src/main/resources/templates/views/help.html new file mode 100644 index 00000000..3a022497 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/help.html @@ -0,0 +1,10 @@ +{% extends "layouts/default" %} +{% block content %} +
+ {{ content | raw }} +
+{% endblock %} + +{% block "column" %} +{{ navigation | raw }} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/index.html b/juick-server/src/main/resources/templates/views/index.html new file mode 100644 index 00000000..97d726de --- /dev/null +++ b/juick-server/src/main/resources/templates/views/index.html @@ -0,0 +1,29 @@ +{% extends "layouts/default" %} +{% import "views/macros/tags" %} +{% block content %} +{% if noindex %} + +{% endif %} +{% for msg in msgs %} +{% include "views/partial/message" %} +{% endfor %} +{% if nextpage | default('') is not empty %} +

+{% endif %} +{% endblock %} +{% block "column" %} +{% if tag | default('') is not empty %} +{% include "views/partial/tagcolumn" %} +{% elseif visitor.uid > 0 %} +{% if discover %} +{% include "views/partial/homecolumn" %} +{% else %} +{% include "views/partial/usercolumn" %} +{% endif %} +{% else %} +{% include "views/partial/homecolumn" %} +{% endif %} +{% if noindex %} + +{% endif %} +{% endblock %} diff --git a/juick-server/src/main/resources/templates/views/login.html b/juick-server/src/main/resources/templates/views/login.html new file mode 100644 index 00000000..a538cb26 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/login.html @@ -0,0 +1,144 @@ + + + + Juick + + + + + + + + + +
juick.com © 2008-2018   Контакты · Помощь
+ +
+ {{ i18n("messages","label.register") }}: + + +
+ +
+
+ + + + diff --git a/juick-server/src/main/resources/templates/views/login_success.html b/juick-server/src/main/resources/templates/views/login_success.html new file mode 100644 index 00000000..ee71f12f --- /dev/null +++ b/juick-server/src/main/resources/templates/views/login_success.html @@ -0,0 +1,13 @@ + + + + + Blank window + + + + + diff --git a/juick-server/src/main/resources/templates/views/macros/tags.html b/juick-server/src/main/resources/templates/views/macros/tags.html new file mode 100644 index 00000000..09278ffe --- /dev/null +++ b/juick-server/src/main/resources/templates/views/macros/tags.html @@ -0,0 +1,5 @@ +{% macro tags(uname="", tagsList) %} +{% for tag in tagsList %} +{{ tag | raw }} +{% endfor %} +{% endmacro %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/partial/footer.html b/juick-server/src/main/resources/templates/views/partial/footer.html new file mode 100644 index 00000000..35972254 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/footer.html @@ -0,0 +1,16 @@ + diff --git a/juick-server/src/main/resources/templates/views/partial/homecolumn.html b/juick-server/src/main/resources/templates/views/partial/homecolumn.html new file mode 100644 index 00000000..64dd9cbd --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/homecolumn.html @@ -0,0 +1,25 @@ + +
+

{{ i18n("messages","link.trends") }}

+ {% include "views/partial/tags" %} + {% if showAdv | default(false) %} +

Наши друзья

+ конструктор сайтов + {% endif %} +
\ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/partial/message.html b/juick-server/src/main/resources/templates/views/partial/message.html new file mode 100644 index 00000000..0b6db3df --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/message.html @@ -0,0 +1,76 @@ +
+
+ +
+ {{ msg.user.name }} +
+ +
+ {{ tags(msg.user.name, msg.tags | tagsList) }} +
+
+

{{ msg | formatMessage }}

+ {% if msg.AttachmentType is not empty %} +

+ +

+ {% endif %} + +
\ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/partial/navigation.html b/juick-server/src/main/resources/templates/views/partial/navigation.html new file mode 100644 index 00000000..fa1dadcc --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/navigation.html @@ -0,0 +1,40 @@ +
+
+ {% if visitor.uid > 0 %} + + {% else %} + + {% endif %} + + +
+
diff --git a/juick-server/src/main/resources/templates/views/partial/settings_tabs.html b/juick-server/src/main/resources/templates/views/partial/settings_tabs.html new file mode 100644 index 00000000..4715253e --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/settings_tabs.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/partial/tagcolumn.html b/juick-server/src/main/resources/templates/views/partial/tagcolumn.html new file mode 100644 index 00000000..3e61d3d3 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/tagcolumn.html @@ -0,0 +1,33 @@ +
+

*{{ tag.name }}

+
+{% if visitor is not empty and visitor.uid > 0 %} + +{% endif %} diff --git a/juick-server/src/main/resources/templates/views/partial/tags.html b/juick-server/src/main/resources/templates/views/partial/tags.html new file mode 100644 index 00000000..3235213e --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/tags.html @@ -0,0 +1,3 @@ +{% for tag in tags %} + {{ tag | raw }} +{% endfor %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/partial/usercolumn.html b/juick-server/src/main/resources/templates/views/partial/usercolumn.html new file mode 100644 index 00000000..2b1963e3 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/usercolumn.html @@ -0,0 +1,89 @@ +{% if visitor is not empty and visitor.uid > 0 and visitor.uid != user.uid %} + + +{% else %} +
+{% endif %} + +
+
+

+
+{% include "views/partial/usertags" %} +
+
+ + {% if iread is not empty %} +
+ {% for u in iread %} + + + {{ u.name }} + + + {% endfor %} +
+ {% endif %} + +
diff --git a/juick-server/src/main/resources/templates/views/partial/usertags.html b/juick-server/src/main/resources/templates/views/partial/usertags.html new file mode 100644 index 00000000..71d1303e --- /dev/null +++ b/juick-server/src/main/resources/templates/views/partial/usertags.html @@ -0,0 +1,3 @@ +{% import "views/macros/tags" %} +{{ tags(user.name, tagStats) }} +... \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/pm_inbox.html b/juick-server/src/main/resources/templates/views/pm_inbox.html new file mode 100644 index 00000000..d6a9b65f --- /dev/null +++ b/juick-server/src/main/resources/templates/views/pm_inbox.html @@ -0,0 +1,35 @@ +{% extends "layouts/default" %} +{% block content %} +{% if not msgs.isEmpty() %} +
    + {% for msg in msgs %} +
  • +
    +
    + @{{ msg.user.name }}: +
    + + {{ msg.user.name }} + +
    +
    {{ msg.timestamp | prettyTime }}
    +
    + +
    {{ msg | formatMessage }}
    +
    + +
    +
    + +
    +
    +
    +
    +
  • + {% endfor %} +
+{% endif %} +{% endblock %} +{% block "column" %} +{% include "views/partial/usercolumn" %} +{% endblock %} diff --git a/juick-server/src/main/resources/templates/views/pm_sent.html b/juick-server/src/main/resources/templates/views/pm_sent.html new file mode 100644 index 00000000..bc42c4ab --- /dev/null +++ b/juick-server/src/main/resources/templates/views/pm_sent.html @@ -0,0 +1,33 @@ +{% extends "layouts/default" %} +{% block content %} +
+
+
To:
+
+
+
+
+{% if not msgs.isEmpty() %} +
    + {% for msg in msgs %} +
  • +
    +
    + @{{ msg.user.name }}: +
    + + {{ msg.user.name }} + +
    +
    {{ msg.timestamp | prettyTime }}
    +
    +
    {{ msg | formatMessage }}
    +
    +
  • + {% endfor %} +
+{% endif %} +{% endblock %} +{% block "column" %} +{% include "views/partial/usercolumn" %} +{% endblock %} diff --git a/juick-server/src/main/resources/templates/views/post.html b/juick-server/src/main/resources/templates/views/post.html new file mode 100644 index 00000000..1f642ce1 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/post.html @@ -0,0 +1,19 @@ +{% extends "layouts/minimal" %} +{% import "views/macros/tags" %} +{% block content %} +
+
+

+ Фото: + ({{ i18n("messages","postForm.imageFormats") }}) +

+

+ +
+ +

+
+
+

Теги:

+{{ tags(visitor.name, tags) }} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/post_success.html b/juick-server/src/main/resources/templates/views/post_success.html new file mode 100644 index 00000000..2106f3cb --- /dev/null +++ b/juick-server/src/main/resources/templates/views/post_success.html @@ -0,0 +1,19 @@ +{% extends "layouts/minimal" %} +{% block content %} +

Сообщение опубликовано

+

Поделитесь своим новым постом в социальных сетях:

+{% if sharetwi | default('') is not empty %} + +{% endif %} + +{% if facebook | default('') is not empty %} + +{% endif %} +

Ссылка на сообщение: {{ url }}

+{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/settings_about.html b/juick-server/src/main/resources/templates/views/settings_about.html new file mode 100644 index 00000000..bbf9e772 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/settings_about.html @@ -0,0 +1,20 @@ +{% extends "layouts/default" %} +{% block content %} +
+
+

Full name:

+

Country:

+

URL:
+ Please, start with "http://"

+

About:
+
+ Max. 255 symbols

+

Avatar:
+ Recommendations: PNG, 96x96, <50Kb. Also, JPG and GIF supported.

+

+
+
+{% endblock %} +{% block "column" %} +{% include "views/partial/settings_tabs" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/settings_auth-email.html b/juick-server/src/main/resources/templates/views/settings_auth-email.html new file mode 100644 index 00000000..e906d704 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/settings_auth-email.html @@ -0,0 +1,9 @@ +{% extends "layouts/default" %} +{% block content %} + +{% endblock %} +{% block "column" %} +{% include "views/partial/settings_tabs" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/settings_main.html b/juick-server/src/main/resources/templates/views/settings_main.html new file mode 100644 index 00000000..65fbc984 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/settings_main.html @@ -0,0 +1,151 @@ +{% extends "layouts/default" %} +{% block content %} +
+

Настройки

+
+
+ Notification options +

Reply notifications ("Message posted")

+

Subscriptions notifications ("@user subscribed...")

+

Posts recommendations ("Recommended by @user")

+

+
+
+
+ + Telegram + {% if telegram_name is not empty %} +
+
Telegram: {{ telegram_name }} — + + +
+
+ {% else %} +

To connect Telegram account: send any text message to @Juick_bot +

+ {% endif %} +
+ {% if jids | length > 0 %} +
+
+ + XMPP accounts + +

Your accounts:

+

+ {% for jid in jids %} +
+ {% endfor %} + {% for auth in auths %} + + — Confirm
+ {% endfor %} +

+ {% if jids | length > 1 %} +

+ {% endif %} +

To add new jabber account: send any text message to juick@juick.com +

+
+
+ {% endif %} +
+ + E-mail + +
+

Add account:
+ + + +

+
+
+

Your accounts:

+

+ {% for email in emails %} +
+ {% endfor %} + {% if emails is empty %} + -

+ {% else %} +

+ {% if jids | length > 1 %} +

+ {% endif %} + {% endif %} +
+ {% if emails is not empty %} + +
+

You can receive notifications to email:
+ Sent to + +

+
+ + {% endif %} +

 

+

You can post to Juick via e-mail. Send your plain text + messages to juick@juick.com. You can attach one photo or video file.

+
+
+ + Facebook + + {% if fbstatus.connected %} + {% if fbstatus.crosspostEnabled %} +
+
+ Facebook: Enabled — + + +
+
+ {% else %} +
+
+ Facebook: Disabled — + + +
+
+ {% endif %} + {% else %} +

Cross-posting to Facebook: Connect to Facebook

+ {% endif %} +
+
+ + Twitter + {% if twitter_name is not empty %} +
+
Twitter: {{ twitter_name }} — + + +
+
+ {% else %} +

Cross-posting to Twitter: Connect to Twitter

+ {% endif %} +
+ +
+{% endblock %} +{% block "column" %} +{% include "views/partial/settings_tabs" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/settings_password.html b/juick-server/src/main/resources/templates/views/settings_password.html new file mode 100644 index 00000000..aba0b139 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/settings_password.html @@ -0,0 +1,17 @@ +{% extends "layouts/default" %} +{% block content %} +
+
+ Changing your password +
+ +

Change password:
+ (max. length - 16 symbols)

+
+
+
+{% endblock %} +{% block "column" %} +{% include "views/partial/settings_tabs" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/settings_privacy.html b/juick-server/src/main/resources/templates/views/settings_privacy.html new file mode 100644 index 00000000..83b87b93 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/settings_privacy.html @@ -0,0 +1,9 @@ +{% extends "layouts/default" %} +{% block content %} +
+

Privacy

+
+{% endblock %} +{% block "column" %} +{% include "views/partial/settings_tabs" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/settings_result.html b/juick-server/src/main/resources/templates/views/settings_result.html new file mode 100644 index 00000000..d87a5ea6 --- /dev/null +++ b/juick-server/src/main/resources/templates/views/settings_result.html @@ -0,0 +1,9 @@ +{% extends "layouts/default" %} +{% block content %} +
+

{{ result | raw }}

+
+{% endblock %} +{% block "column" %} +{% include "views/partial/settings_tabs" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/signup.html b/juick-server/src/main/resources/templates/views/signup.html new file mode 100644 index 00000000..d6eb921f --- /dev/null +++ b/juick-server/src/main/resources/templates/views/signup.html @@ -0,0 +1,43 @@ +{% extends "layouts/default" %} +{% block content %} +

+ {% if type | slice(0, 1) == 'f' %} + Facebook + {% elseif type | slice(0, 1) == 'v' %} + VKontakte + {% elseif type | slice(0, 1) == 'e' %} + Email + {% elseif type | slice(0, 1) == 'd' %} + Telegram + {% endif %} + {{ account | raw }}

+ + +
+ + + + {% if visitor.getUID() > 0 %} + + {% else %} +

Имя пользователя:

+

Пароль:

+

+ {% endif %} +
+ +{% if type != "xmpp" %} + + + +
+ + + +

Имя пользователя:
(От 2-х до 16-и латинских символов + и/или цифр, дефис)

+

Пароль:
(от 6-и до 32-х символов)

+

+
+{% endif %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/test.html b/juick-server/src/main/resources/templates/views/test.html new file mode 100644 index 00000000..7700be6f --- /dev/null +++ b/juick-server/src/main/resources/templates/views/test.html @@ -0,0 +1,2 @@ +{% import "views/macros/tags" %} +{{ tags("ugnich", tagsList)}} \ No newline at end of file diff --git a/juick-server/src/main/resources/templates/views/thread.html b/juick-server/src/main/resources/templates/views/thread.html new file mode 100644 index 00000000..d281e3bb --- /dev/null +++ b/juick-server/src/main/resources/templates/views/thread.html @@ -0,0 +1,173 @@ +{% extends "layouts/default" %} +{% import "views/macros/tags" %} +{% block content %} + +
+ {% if visitor.uid > 0 %} + + {% endif %} +

{{ i18n("messages","reply.replies") }} ({{ replies.size() }})

+
+ + +{% 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 %} +
+ {% for u in users %} + + + {{ u.name }} + {{ u.name }} + + + {% endfor %} +
+{% endblock %} +{% block "column" %} +{% include "views/partial/usercolumn" %} +{% endblock %} \ No newline at end of file diff --git a/juick-server/src/test/java/com/juick/server/tests/ServerTests.java b/juick-server/src/test/java/com/juick/server/tests/ServerTests.java index 1931f13b..fc18dbf5 100644 --- a/juick-server/src/test/java/com/juick/server/tests/ServerTests.java +++ b/juick-server/src/test/java/com/juick/server/tests/ServerTests.java @@ -368,19 +368,19 @@ public class ServerTests { @Test public void testAllUnAuthorized() throws Exception { - mockMvc.perform(get("/")) + mockMvc.perform(get("/api/")) .andExpect(status().isMovedPermanently()); - mockMvc.perform(get("/auth")) + mockMvc.perform(get("/api/auth")) .andExpect(status().isUnauthorized()); - mockMvc.perform(get("/home")) + mockMvc.perform(get("/api/home")) .andExpect(status().isUnauthorized()); - mockMvc.perform(get("/messages/recommended")) + mockMvc.perform(get("/api/messages/recommended")) .andExpect(status().isUnauthorized()); - mockMvc.perform(get("/messages/set_privacy")) + mockMvc.perform(get("/api/messages/set_privacy")) .andExpect(status().isUnauthorized()); } @@ -392,7 +392,7 @@ public class ServerTests { Message msg = messagesService.getMessage(mid); tagService.createTag("тест"); mockMvc.perform( - get("/home") + get("/api/home") .with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) @@ -411,12 +411,12 @@ public class ServerTests { public void homeTestWithMessagesAndRememberMe() throws Exception { String ugnichHash = userService.getHashByUID(ugnich.getUid()); mockMvc.perform( - get("/home") + get("/api/home") .with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isOk()) .andReturn(); - mockMvc.perform(get("/home") + mockMvc.perform(get("/api/home") .param("hash", ugnichHash)) .andExpect(status().isOk()); } @@ -424,7 +424,7 @@ public class ServerTests { @Test public void homeTestWithMessagesAndSimpleCors() throws Exception { mockMvc.perform( - get("/home") + get("/api/home") .with(httpBasic(ugnichName, ugnichPassword)) .header("Origin", "http://api.example.net")) .andExpect(status().isOk()) @@ -434,7 +434,7 @@ public class ServerTests { @Test public void homeTestWithPreflightCors() throws Exception { mockMvc.perform( - options("/home") + options("/api/home") .with(httpBasic(ugnichName, ugnichPassword)) .header("Origin", "http://api.example.net") .header("Access-Control-Request-Method", "POST") @@ -449,10 +449,10 @@ public class ServerTests { public void anonymousApis() throws Exception { - mockMvc.perform(get("/messages")) + mockMvc.perform(get("/api/messages")) .andExpect(status().isOk()); - mockMvc.perform(get("/users") + mockMvc.perform(get("/api/users") .param("uname", "ugnich") .param("uname", "freefd")) .andExpect(status().isOk()) @@ -473,7 +473,7 @@ public class ServerTests { messagesService.likeMessage(mid, freefdId, 2 ); messagesService.likeMessage(mid, freefdId, 3 ); - mockMvc.perform(get("/messages?"+ "hash=" + userIdHash)) + mockMvc.perform(get("/api/messages?"+ "hash=" + userIdHash)) .andDo(print()) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) @@ -482,7 +482,7 @@ public class ServerTests { .andExpect((jsonPath("$[0].reactions[?(@.id == 2)].count", is(Collections.singletonList(2))))); - mockMvc.perform(get("/reactions?hash=" + userIdHash)) + mockMvc.perform(get("/api/reactions?hash=" + userIdHash)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) .andExpect(jsonPath("$.length()", is(7))); @@ -494,14 +494,14 @@ public class ServerTests { Tag yo = tagService.getTag("yo", true); messagesService.createMessage(ugnich.getUid(), "text", null, Arrays.asList(yo, weather)); messagesService.createMessage(freefd.getUid(), "text2", null, Collections.singletonList(yo)); - MvcResult result = mockMvc.perform(get("/tags")) + MvcResult result = mockMvc.perform(get("/api/tags")) .andExpect(status().isOk()) .andReturn(); List tagsFromApi = jsonMapper.readValue(result.getResponse().getContentAsString(), new TypeReference>(){}); TagStats yoStats = tagsFromApi.stream().filter(t -> t.getTag().getName().equals("yo")).findFirst().get(); assertThat(yoStats.getUsageCount(), is(2)); - MvcResult result2 = mockMvc.perform(get("/tags") + MvcResult result2 = mockMvc.perform(get("/api/tags") .param("user_id", String.valueOf(ugnich.getUid()))) .andExpect(status().isOk()) .andReturn(); @@ -513,40 +513,40 @@ public class ServerTests { @Test public void postWithReferer() throws Exception { - mockMvc.perform(post("/post") + mockMvc.perform(post("/api/post") .param("body", "yo") .with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isOk()); } @Test public void threadWithEphemeralNumberShouldReturn404() throws Exception { - mockMvc.perform(get("/thread").param("mid", "999999999") + mockMvc.perform(get("/api/thread").param("mid", "999999999") .with(httpBasic(ugnichName, ugnichPassword))).andExpect(status().is4xxClientError()); } @Test public void performRequestsWithIssuedToken() throws Exception { String ugnichHash = userService.getHashByUID(ugnich.getUid()); - mockMvc.perform(get("/home")).andExpect(status().isUnauthorized()); - mockMvc.perform(get("/auth")) + mockMvc.perform(get("/api/home")).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/auth")) .andExpect(status().isUnauthorized()); - mockMvc.perform(get("/auth").with(httpBasic(ugnichName, "wrongpassword"))) + mockMvc.perform(get("/api/auth").with(httpBasic(ugnichName, "wrongpassword"))) .andExpect(status().isUnauthorized()); - MvcResult result = mockMvc.perform(get("/auth").with(httpBasic(ugnichName, ugnichPassword))) + MvcResult result = mockMvc.perform(get("/api/auth").with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isOk()) .andReturn(); String authHash = result.getResponse().getContentAsString(); assertThat(authHash, equalTo(ugnichHash)); - mockMvc.perform(get("/home").param("hash", ugnichHash)).andExpect(status().isOk()); + mockMvc.perform(get("/api/home").param("hash", ugnichHash)).andExpect(status().isOk()); } @Test public void registerForNotificationsTests() throws Exception { String token = "123456"; ExternalToken registration = new ExternalToken(null, "apns", token, null); - mockMvc.perform(put("/notifications").with(httpBasic(ugnichName, ugnichPassword)) + mockMvc.perform(put("/api/notifications").with(httpBasic(ugnichName, ugnichPassword)) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(jsonMapper.writeValueAsBytes(Collections.singletonList(registration)))) .andExpect(status().isOk()); - MvcResult result = mockMvc.perform(get("/notifications") + MvcResult result = mockMvc.perform(get("/api/notifications") .param("uid", String.valueOf(ugnich.getUid())) .with(httpBasic(juickName, juickPassword))) .andExpect(status().isOk()) @@ -565,10 +565,10 @@ public class ServerTests { @Test public void notificationsTokensTest() throws Exception { List tokens = Collections.singletonList(new ExternalToken(null, "gcm", "123456", null)); - mockMvc.perform(delete("/notifications").with(httpBasic(ugnichName, ugnichPassword)) + mockMvc.perform(delete("/api/notifications").with(httpBasic(ugnichName, ugnichPassword)) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(jsonMapper.writeValueAsBytes(tokens))).andExpect(status().isForbidden()); - mockMvc.perform(delete("/notifications").with(httpBasic(juickName, juickPassword)) + mockMvc.perform(delete("/api/notifications").with(httpBasic(juickName, juickPassword)) .contentType(MediaType.APPLICATION_JSON_UTF8) .content(jsonMapper.writeValueAsBytes(tokens))).andExpect(status().isOk()); } @@ -576,9 +576,9 @@ public class ServerTests { public void notificationsSettingsAllowedOnlyForServiceUser() throws Exception { CommandResult result = commandsManager.processCommand(ugnich, "yo", emptyUri); String stringValueOfMid = String.valueOf(result.getNewMessage().get().getMid()); - mockMvc.perform(get("/notifications").with(httpBasic(juickName, juickPassword)) + mockMvc.perform(get("/api/notifications").with(httpBasic(juickName, juickPassword)) .param("mid", stringValueOfMid).param("uid", String.valueOf(ugnich.getUid()))).andExpect(status().isOk()); - mockMvc.perform(get("/notifications") + mockMvc.perform(get("/api/notifications") .param("mid", stringValueOfMid).param("uid", String.valueOf(ugnich.getUid()))).andExpect(status().isUnauthorized()); } @Test @@ -858,7 +858,7 @@ public class ServerTests { "
s2313334
\n" + "\n" + "--001a11454886e42be5056786ca70--"; - mockMvc.perform(post("/mail").with(httpBasic(juickName, juickPassword)).content(mail)) + mockMvc.perform(post("/api/mail").with(httpBasic(juickName, juickPassword)).content(mail)) .andExpect(status().isOk()); } @@ -869,14 +869,14 @@ public class ServerTests { String freefdHash = userService.getHashByUID(freefd.getUid()); int freefdMid = messagesService.createMessage(freefd.getUid(), "to be not liked", null, null); - mockMvc.perform(post("/like?mid=" + mid + "&hash=" + freefdHash)) + mockMvc.perform(post("/api/like?mid=" + mid + "&hash=" + freefdHash)) .andExpect(status().isOk()) .andExpect(jsonPath("$.status", is("Message is added to your recommendations"))); - mockMvc.perform(get("/thread?mid=" + mid + "&hash=" + freefdHash)) + mockMvc.perform(get("/api/thread?mid=" + mid + "&hash=" + freefdHash)) .andExpect(status().isOk()) .andExpect(jsonPath("$[0].recommendations.length()", is(1))) .andExpect(jsonPath("$[0].recommendations[0]", is(freefdName))); - mockMvc.perform(post("/like?mid=" + freefdMid + "&hash=" + freefdHash)) + mockMvc.perform(post("/api/like?mid=" + freefdMid + "&hash=" + freefdHash)) .andExpect(status().isForbidden()); } @@ -886,7 +886,7 @@ public class ServerTests { String freefdHash = userService.getHashByUID(freefd.getUid()); int mid1 = messagesService.createMessage(user_id, "yo", null, new ArrayList<>()); - mockMvc.perform(post("/react?mid=" + mid1 + "&hash=" + freefdHash+ "&reactionId=2")) + mockMvc.perform(post("/api/react?mid=" + mid1 + "&hash=" + freefdHash+ "&reactionId=2")) .andExpect(status().isOk()); Message msg4 = messagesService.getMessage(mid1); @@ -894,9 +894,9 @@ public class ServerTests { assertThat(messagesService.getMessages(AnonymousUser.INSTANCE, Collections.singletonList(mid1)).get(0).getLikes(), is(0)); Assert.assertEquals(1, msg4.getReactions().stream().filter(r -> r.getId() == 2) .findFirst().orElseThrow(IllegalStateException::new).getCount()); - mockMvc.perform(post("/react?mid=" + mid1 + "&hash=" + freefdHash+ "&reactionId=1")) + mockMvc.perform(post("/api/react?mid=" + mid1 + "&hash=" + freefdHash+ "&reactionId=1")) .andExpect(status().isOk()); - mockMvc.perform(post("/react?mid=" + mid1 + "&hash=" + freefdHash+ "&reactionId=1")) + mockMvc.perform(post("/api/react?mid=" + mid1 + "&hash=" + freefdHash+ "&reactionId=1")) .andExpect(status().isOk()); assertThat(messagesService.getMessage(mid1).getLikes(), is(1)); } @@ -920,7 +920,7 @@ public class ServerTests { assertThat(lastRead.apply(ugnich, mid), is(1)); String ugnichHash = userService.getHashByUID(ugnich.getUid()); int freefdrid = messagesService.createReply(mid, 0, freefd, "again", null); - mockMvc.perform(get(String.format("/thread/mark_read/%d-%d.gif?hash=%s", mid, freefdrid, ugnichHash))) + mockMvc.perform(get(String.format("/api/thread/mark_read/%d-%d.gif?hash=%s", mid, freefdrid, ugnichHash))) .andExpect(status().isOk()) .andExpect(content().bytes(IOUtils.toByteArray( Objects.requireNonNull(getClass().getClassLoader().getResource("Transparent.gif"))))); @@ -934,7 +934,7 @@ public class ServerTests { privacyQueriesService.blacklistUser(ugnich, freefd); newfreefdrid = messagesService.createReply(mid, 0, freefd, "after ban", null); assertThat(lastRead.apply(ugnich, mid), lessThan(newfreefdrid)); - mockMvc.perform(get(String.format("/thread?mid=%d&hash=%s", mid, ugnichHash))) + mockMvc.perform(get(String.format("/api/thread?mid=%d&hash=%s", mid, ugnichHash))) .andExpect(status().isOk()); assertThat(lastRead.apply(ugnich, mid), is(newfreefdrid)); } @@ -1035,13 +1035,13 @@ public class ServerTests { map.add("body", "yo"); map.add("hash", userService.getHashByUID(ugnich.getUid())); ResponseEntity result = restTemplate.postForEntity( - "/post", + "/api/post", request, CommandResult.class); assertThat(result.getStatusCode(), is(HttpStatus.OK)); } @Test public void emptyAuthenticatedPostShouldThrowBadRequest() throws Exception { - mockMvc.perform(post("/post") + mockMvc.perform(post("/api/post") .with(httpBasic(juickName, juickPassword))) .andExpect(status().isBadRequest()); } @@ -1061,7 +1061,7 @@ public class ServerTests { commandsManager.processCommand(ugnich, "S @freefd", emptyUri); assertThat(userService.getUserReaders(ugnich.getUid()).size(), is(1)); String hash = userService.getHashByUID(ugnich.getUid()); - mockMvc.perform(get("/me") + mockMvc.perform(get("/api/me") .with(httpBasic(ugnichName, ugnichPassword))) .andExpect(jsonPath("$.hash", is(hash))) .andExpect(jsonPath("$.readers.length()", is(1))) @@ -1148,7 +1148,7 @@ public class ServerTests { } @Test public void messageEditingSpec() throws Exception { - MvcResult result = mockMvc.perform(post("/post").with(httpBasic(ugnichName, ugnichPassword)) + MvcResult result = mockMvc.perform(post("/api/post").with(httpBasic(ugnichName, ugnichPassword)) .param("body", "YO")).andExpect(status().is2xxSuccessful()).andReturn(); Message original = jsonMapper.readValue(result.getResponse().getContentAsString(), CommandResult.class) .getNewMessage().get(); @@ -1156,17 +1156,17 @@ public class ServerTests { assertThat(original.getUpdatedAt(), equalTo(original.getTimestamp())); // to have updated_at greater than ts Thread.sleep(1000); - result = mockMvc.perform(post("/update").with(httpBasic(ugnichName, ugnichPassword)) + result = mockMvc.perform(post("/api/update").with(httpBasic(ugnichName, ugnichPassword)) .param("mid", String.valueOf(original.getMid())) .param("body", "PEOPLE")).andExpect(status().is2xxSuccessful()).andReturn(); Message edited = jsonMapper.readValue(result.getResponse().getContentAsString(), CommandResult.class) .getNewMessage().get(); assertThat(edited.getText(), equalTo("PEOPLE")); assertThat(edited.getUpdatedAt(), greaterThan(edited.getTimestamp())); - mockMvc.perform(post("/update").with(httpBasic(freefdName, freefdPassword)) + mockMvc.perform(post("/api/update").with(httpBasic(freefdName, freefdPassword)) .param("mid", String.valueOf(original.getMid())) .param("body", "PEOPLE")).andExpect(status().is(403)); - result = mockMvc.perform(post("/comment").with(httpBasic(freefdName, freefdPassword)) + result = mockMvc.perform(post("/api/comment").with(httpBasic(freefdName, freefdPassword)) .param("mid", String.valueOf(original.getMid())) .param("body", "HEY")).andExpect(status().is2xxSuccessful()).andReturn(); Message comment = jsonMapper.readValue(result.getResponse().getContentAsString(), Message.class); @@ -1174,7 +1174,7 @@ public class ServerTests { assertThat(comment.getUpdatedAt(), is(comment.getTimestamp())); // to have updated_at greater than ts Thread.sleep(1000); - result = mockMvc.perform(post("/update").with(httpBasic(freefdName, freefdPassword)) + result = mockMvc.perform(post("/api/update").with(httpBasic(freefdName, freefdPassword)) .param("mid", String.valueOf(comment.getMid())) .param("rid", String.valueOf(comment.getRid())) .param("body", "HEY, JOE")).andExpect(status().is2xxSuccessful()).andReturn(); @@ -1223,7 +1223,7 @@ public class ServerTests { public void xmppStatusApi() throws Exception { Supplier getStatus = () -> { try { - MvcResult result = mockMvc.perform(get("/xmpp/status").with(httpBasic(ugnichName, ugnichPassword))) + MvcResult result = mockMvc.perform(get("/api/xmpp-status").with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isOk()).andReturn(); return jsonMapper.readValue(result.getResponse().getContentAsString(), XMPPStatus.class); } catch (Exception e) { @@ -1273,16 +1273,16 @@ public class ServerTests { User isilmine = userService.getUserByUID(userService.createUser(userName, userPassword)).orElseThrow(IllegalStateException::new); int mid = messagesService.createMessage(isilmine.getUid(), msgText, null, null); - mockMvc.perform(get(String.format("/thread?mid=%d", mid)).with(httpBasic(ugnichName, ugnichPassword))) + mockMvc.perform(get(String.format("/api/thread?mid=%d", mid)).with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isOk()); jdbcTemplate.update("UPDATE users SET banned=1 WHERE id=?", isilmine.getUid()); - mockMvc.perform(get(String.format("/thread?mid=%d", mid)).with(httpBasic(ugnichName, ugnichPassword))) + mockMvc.perform(get(String.format("/api/thread?mid=%d", mid)).with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isNotFound()); - mockMvc.perform(get("/messages?uname=isilmine").with(httpBasic(ugnichName, ugnichPassword))) + mockMvc.perform(get("/api/messages?uname=isilmine").with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isNotFound()); - mockMvc.perform(get("/info/isilmine").with(httpBasic(ugnichName, ugnichPassword))) + mockMvc.perform(get("/api/info/isilmine").with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isNotFound()); - mockMvc.perform(get("/info/ugnich").with(httpBasic(ugnichName, ugnichPassword))) + mockMvc.perform(get("/api/info/ugnich").with(httpBasic(ugnichName, ugnichPassword))) .andExpect(status().isOk()); } @@ -1293,7 +1293,7 @@ public class ServerTests { userService.createUser(userName, userPassword); - mockMvc.perform(get("/auth").with(httpBasic(userName, userPassword))).andExpect(status().isUnauthorized()); + mockMvc.perform(get("/api/auth").with(httpBasic(userName, userPassword))).andExpect(status().isUnauthorized()); } @Test public void bannedUserShouldBeShadowedFromRecommendationsList() throws IOException { @@ -1342,7 +1342,7 @@ public class ServerTests { } @Test public void userProfileAndBlogShouldBeExposedAsActivityStream() throws Exception { - mockMvc.perform(get("/u/ugnich").accept(ActivityObject.LD_JSON_MEDIA_TYPE)) + mockMvc.perform(get("/api/u/ugnich").accept(ActivityObject.LD_JSON_MEDIA_TYPE)) .andExpect(status().isOk()) .andExpect(jsonPath("$.icon.url", is("http://localhost:8080/i/a/1.png"))) .andExpect(jsonPath("$.publicKey.publicKeyPem", is(keystoreManager.getPublicKey()))); @@ -1352,7 +1352,7 @@ public class ServerTests { String.format("message %d", i), null, null)) .collect(Collectors.toCollection(ArrayDeque::new)).descendingIterator()); List midsPage = mids.stream().limit(20).collect(Collectors.toList()); - mockMvc.perform(get("/u/ugnich/blog").accept(ActivityObject.ACTIVITY_JSON_MEDIA_TYPE)) + mockMvc.perform(get("/api/u/ugnich/blog").accept(ActivityObject.ACTIVITY_JSON_MEDIA_TYPE)) .andExpect(status().isOk()) .andExpect(jsonPath("$.orderedItems", hasSize(20))) .andExpect(jsonPath("$.next", is("http://localhost:8080/u/ugnich/blog?before=" + midsPage.get(midsPage.size() - 1)))); diff --git a/juick-server/src/test/java/com/juick/server/tests/WebAppTests.java b/juick-server/src/test/java/com/juick/server/tests/WebAppTests.java new file mode 100644 index 00000000..456c5f32 --- /dev/null +++ b/juick-server/src/test/java/com/juick/server/tests/WebAppTests.java @@ -0,0 +1,466 @@ +/* + * Copyright (C) 2008-2017, Juick + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.juick.server.tests; + +import com.gargoylesoftware.htmlunit.CookieManager; +import com.gargoylesoftware.htmlunit.Page; +import com.gargoylesoftware.htmlunit.WebClient; +import com.gargoylesoftware.htmlunit.css.StyleElement; +import com.gargoylesoftware.htmlunit.html.DomElement; +import com.gargoylesoftware.htmlunit.html.HtmlPage; +import com.juick.Message; +import com.juick.Tag; +import com.juick.User; +import com.juick.server.www.Utils; +import com.juick.server.www.WebApp; +import com.juick.service.*; +import com.juick.util.MessageUtils; +import com.mitchellbosecke.pebble.PebbleEngine; +import com.mitchellbosecke.pebble.error.PebbleException; +import com.mitchellbosecke.pebble.template.PebbleTemplate; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.text.StringEscapeUtils; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.core.io.ClassPathResource; +import org.springframework.http.MediaType; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.TestPropertySource; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.util.FileSystemUtils; + +import javax.inject.Inject; +import javax.servlet.http.Cookie; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.StreamSupport; + +import static junit.framework.TestCase.assertTrue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Created by vitalyster on 12.01.2017. + */ +@RunWith(SpringRunner.class) +@AutoConfigureMockMvc +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT) +@TestPropertySource(properties = {"ios_app_id=12345678.com.juick.ExampleApp"}) +public class WebAppTests { + @MockBean + private ImagesService imagesService; + @Inject + private WebApp webApp; + + @Inject + private MockMvc mockMvc; + @Inject + private WebClient webClient; + + @Inject + private UserService userService; + @Inject + private MessagesService messagesService; + @Inject + private PrivacyQueriesService privacyQueriesService; + @Inject + private JdbcTemplate jdbcTemplate; + @Inject + private SubscriptionService subscriptionService; + @Inject + private ApplicationEventPublisher applicationEventPublisher; + + @Inject + private PebbleEngine pebbleEngine; + @Value("${img_path:#{systemEnvironment['TEMP'] ?: '/tmp'}}") + private String imgPath; + @Value("${ios_app_id:}") + private String appId; + + private static User ugnich, freefd; + private static String ugnichName, ugnichPassword, freefdName, freefdPassword; + + private static boolean isSetUp = false; + + @Before + public void setup() throws IOException { + if (!isSetUp) { + webClient.getOptions().setJavaScriptEnabled(false); + webClient.getOptions().setCssEnabled(false); + webClient.getOptions().setThrowExceptionOnFailingStatusCode(false); + + ugnichName = "ugnich"; + ugnichPassword = "secret"; + freefdName = "freefd"; + freefdPassword = "MyPassw0rd!"; + + userService.createUser(ugnichName, ugnichPassword); + ugnich = userService.getUserByName(ugnichName); + int freefdId = userService.createUser(freefdName, freefdPassword); + freefd = userService.getUserByUID(freefdId).orElseThrow(IllegalStateException::new); + + isSetUp = true; + } + Files.createDirectory(Paths.get(imgPath, "p")); + Files.createDirectory(Paths.get(imgPath, "photos-1024")); + Files.createDirectory(Paths.get(imgPath, "photos-512")); + Files.createDirectory(Paths.get(imgPath, "ps")); + } + + @After + public void teardown() throws IOException { + FileSystemUtils.deleteRecursively(Paths.get(imgPath, "p")); + FileSystemUtils.deleteRecursively(Paths.get(imgPath, "photos-1024")); + FileSystemUtils.deleteRecursively(Paths.get(imgPath, "photos-512")); + FileSystemUtils.deleteRecursively(Paths.get(imgPath, "ps")); + } + + @Test + public void postWithoutTagsShouldNotHaveAsteriskInTitle() throws Exception { + String msgText = "Привет, я - Угнич"; + int mid = messagesService.createMessage(ugnich.getUid(), msgText, null, null); + HtmlPage threadPage = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); + assertThat(threadPage.getTitleText(), equalTo("ugnich:")); + } + @Test + public void bannedUserBlogandPostShouldReturn404() throws IOException { + String userName = "isilmine"; + String userPassword = "secret"; + String msgText = "автор этого поста был забанен"; + String hash = "12345678"; + + User isilmine = userService.getUserByUID(userService.createUser(userName, userPassword)).orElseThrow(IllegalStateException::new); + int mid = messagesService.createMessage(isilmine.getUid(), msgText, null, null); + jdbcTemplate.update("UPDATE users SET banned=1 WHERE id=?", isilmine.getUid()); + Page blogPage = webClient.getPage("http://localhost:8080/isilmine"); + Page threadPage = webClient.getPage(String.format("http://localhost:8080/isilmine/%d", mid)); + assertThat(blogPage.getWebResponse().getStatusCode(), equalTo(404)); + assertThat(threadPage.getWebResponse().getStatusCode(), equalTo(404)); + } + + @Test + public void emptyPasswordMeansUserIsDisabledForWeb() throws Exception { + String userName = "oldschooluser"; + String userPassword = ""; + + User daddy = userService.getUserByUID(userService.createUser(userName, userPassword)).orElseThrow(IllegalStateException::new); + + mockMvc.perform(post("/login") + .param("username", userName) + .param("password", userPassword)).andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login?error=1")); + } + @Test + public void repliesList() throws IOException { + int mid = messagesService.createMessage(ugnich.getUid(), "hello", null, null); + IntStream.range(1, 15).forEach(i -> + messagesService.createReply(mid, i-1, freefd, String.valueOf(i-1), null )); + + HtmlPage threadPage = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); + assertThat(threadPage.getWebResponse().getStatusCode(), equalTo(200)); + Long visibleItems = StreamSupport.stream(threadPage.getHtmlElementById("replies") + .getChildElements().spliterator(), false).filter(e -> { + StyleElement display = e.getStyleElement("display"); + return display == null || !display.getValue().equals("none"); + }).count(); + assertThat(visibleItems, equalTo(14L)); + } + @Test + public void userShouldNotSeeReplyButtonToBannedUser() throws Exception { + int mid = messagesService.createMessage(ugnich.getUid(), "freefd bl me", null, null); + messagesService.createReply(mid, 0, ugnich, "yo", null); + MvcResult loginResult = mockMvc.perform(post("/login") + .param("username", freefdName) + .param("password", freefdPassword)) + .andExpect(status().isFound()).andReturn(); + Cookie loginCookie = loginResult.getResponse().getCookie("juick-remember-me"); + webClient.setCookieManager(new CookieManager()); + webClient.getCookieManager().addCookie( + new com.gargoylesoftware.htmlunit.util.Cookie(loginCookie.getDomain(), + loginCookie.getName(), + loginCookie.getValue())); + HtmlPage threadPage = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); + assertThat(threadPage.getWebResponse().getStatusCode(), equalTo(200)); + assertThat(threadPage.querySelectorAll(".msg-comment-target").isEmpty(), equalTo(false)); + assertThat(threadPage.querySelectorAll(".a-thread-comment").isEmpty(), equalTo(false)); + privacyQueriesService.blacklistUser(freefd, ugnich); + assertThat(userService.isInBLAny(freefd.getUid(), ugnich.getUid()), equalTo(true)); + int renhaId = userService.createUser("renha", "secret"); + messagesService.createReply(mid, 0, userService.getUserByUID(renhaId).orElseThrow(IllegalStateException::new), + "people", null); + threadPage = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); + assertThat(threadPage.getWebResponse().getStatusCode(), equalTo(200)); + assertThat(threadPage.querySelectorAll(".msg-comment-target").isEmpty(), equalTo(true)); + assertThat(threadPage.querySelectorAll(".a-thread-comment").isEmpty(), equalTo(true)); + } + @Test + public void correctTagsEscaping() throws PebbleException, IOException { + PebbleTemplate template = pebbleEngine.getTemplate("views/test"); + Writer writer = new StringWriter(); + template.evaluate(writer, + Collections.singletonMap("tagsList", + Collections.singletonList(StringEscapeUtils.escapeHtml4(new Tag(">_<").getName())))); + String output = writer.toString().trim(); + assertThat(output, equalTo(">_<")); + } + + public DomElement fetchMeta(String url, String name) throws IOException { + HtmlPage page = webClient.getPage(url); + DomElement emptyMeta = new DomElement("", "meta", null, null); + return page.getElementsByTagName("meta").stream() + .filter(t -> t.getAttribute("name").equals(name)).findFirst().orElse(emptyMeta); + } + @Test + public void testTwitterCards() throws Exception { + + int mid = messagesService.createMessage(ugnich.getUid(), "without image", null, null); + + assertThat(fetchMeta(String.format("http://localhost:8080/ugnich/%d", mid), "twitter:card") + .getAttribute("content"), equalTo("summary")); + int mid2 = messagesService.createMessage(ugnich.getUid(), "with image", "png", null); + Message message = messagesService.getMessage(mid2); + assertThat(fetchMeta(String.format("http://localhost:8080/ugnich/%d", mid2), "twitter:card") + .getAttribute("content"), equalTo("summary_large_image")); + assertThat(fetchMeta(String.format("http://localhost:8080/ugnich/%d", mid2), "og:description") + .getAttribute("content"), + startsWith(StringEscapeUtils.escapeHtml4(MessageUtils.getMessageHashTags(message)))); + } + @Test + public void postMessageTests() throws Exception { + mockMvc.perform(post("/post2").param("body", "yo")) + .andExpect(status().is3xxRedirection()).andExpect(redirectedUrl("http://localhost/login")); + MvcResult loginResult = mockMvc.perform(post("/login") + .param("username", ugnichName) + .param("password", ugnichPassword)).andReturn(); + String msgText = "yoppppl"; + mockMvc.perform(post("/post2") + .cookie(loginResult.getResponse().getCookies()) + .param("body", msgText)).andExpect(status().isFound()); + Message lastMessage = messagesService.getMessage(messagesService.getMyFeed(ugnich.getUid(), 0, false).get(0)); + assertThat(lastMessage.getText(), equalTo(msgText)); + mockMvc.perform(post("/post2") + .cookie(loginResult.getResponse().getCookies()) + .param("img", "http://static.juick.com/settings/facebook.png")).andExpect(status().isFound()); + lastMessage = messagesService.getMessage(messagesService.getMyFeed(ugnich.getUid(), 0, false).get(0)); + assertThat(lastMessage.getAttachmentType(), equalTo("png")); + mockMvc.perform(post("/post2") + .cookie(loginResult.getResponse().getCookies()) + .param("img", "bad_url")).andExpect(status().isBadRequest()); + FileInputStream fi = new FileInputStream(new ClassPathResource("static/tagscloud.png").getFile()); + MockMultipartFile file = new MockMultipartFile("attach", fi); + mockMvc.perform(multipart("/post2") + .file(file) + .cookie(loginResult.getResponse().getCookies())).andExpect(status().isFound()); + int mid = messagesService.createMessage(ugnich.getUid(), "dummy message", null, null); + mockMvc.perform(post("/comment") + .param("mid", String.valueOf(mid)) + .param("body", "yo")).andExpect(redirectedUrl("http://localhost/login")); + mockMvc.perform(post("/comment") + .cookie(loginResult.getResponse().getCookies()) + .param("wrong_param", "yo")).andExpect(status().isBadRequest()); + mockMvc.perform(post("/comment") + .cookie(loginResult.getResponse().getCookies()) + .param("mid", String.valueOf(mid)) + .param("wrong_param", "yo")).andExpect(status().isBadRequest()); + mockMvc.perform(post("/comment") + .cookie(loginResult.getResponse().getCookies()) + .param("mid", String.valueOf(mid)) + .param("img", "http://static.juick.com/settings/facebook.png")).andExpect(status().isFound()); + mockMvc.perform(multipart("/comment") + .file(file) + .cookie(loginResult.getResponse().getCookies()) + .param("mid", String.valueOf(mid))).andExpect(status().isFound()); + mockMvc.perform(post("/comment") + .cookie(loginResult.getResponse().getCookies()) + .param("mid", String.valueOf(mid)) + .param("body", "yo")).andExpect(redirectedUrl(String.format("/%s/%d#%d", ugnichName, mid, 3))); + mockMvc.perform(post("/post2") + .cookie(loginResult.getResponse().getCookies()) + .param("body", String.format("D #%d/%d", mid, 3))) + .andExpect(status().isFound()); + assertThat(messagesService.getReplies(ugnich, mid).size(), equalTo(2)); + } + @Test + public void hashLoginShouldNotUseSession() throws Exception { + String hash = userService.getHashByUID(ugnich.getUid()); + MvcResult hashLoginResult = mockMvc.perform(get("/?show=my&hash=" + hash)) + .andExpect(status().isOk()) + .andExpect(model().attribute("visitor", hasProperty("authHash", equalTo(hash)))) + .andExpect(content().string(containsString(hash))) + .andReturn(); + Cookie rememberMeFromHash = hashLoginResult.getResponse().getCookie("juick-remember-me"); + MvcResult formLoginResult = mockMvc.perform(post("/login") + .param("username", ugnichName) + .param("password", ugnichPassword)) + .andExpect(status().is3xxRedirection()).andReturn(); + Cookie rememberMeFromForm = formLoginResult.getResponse().getCookie("juick-remember-me"); + mockMvc.perform(get("/?show=my").cookie(rememberMeFromForm)).andExpect(status().isOk()) + .andExpect(model().attribute("visitor", hasProperty("authHash", equalTo(hash)))) + .andExpect(content().string(containsString(hash))); + mockMvc.perform(get("/?show=my").cookie(rememberMeFromHash)).andExpect(status().isOk()) + .andExpect(model().attribute("visitor", hasProperty("authHash", equalTo(hash)))) + .andExpect(content().string(containsString(hash))); + } + @Test + public void nonExistentBlogShouldReturn404() throws Exception { + mockMvc.perform(get("/ololoe/")).andExpect(status().isNotFound()); + } + @Test + public void discussionsShouldBePageableByTimestamp() throws Exception { + String msgText = "Привет, я снова Угнич"; + int mid = messagesService.createMessage(ugnich.getUid(), msgText, null, null); + int midNew = messagesService.createMessage(ugnich.getUid(), "Я более новый Угнич", null, null); + MvcResult loginResult = mockMvc.perform(post("/login") + .param("username", freefdName) + .param("password", freefdPassword)) + .andExpect(status().is3xxRedirection()).andReturn(); + Cookie loginCookie = loginResult.getResponse().getCookie("juick-remember-me"); + webClient.setCookieManager(new CookieManager()); + webClient.getCookieManager().addCookie( + new com.gargoylesoftware.htmlunit.util.Cookie(loginCookie.getDomain(), + loginCookie.getName(), + loginCookie.getValue())); + String discussionsUrl = "http://localhost:8080/?show=discuss"; + HtmlPage discussions = webClient.getPage(discussionsUrl); + assertThat(discussions.querySelectorAll("article").size(), is(0)); + subscriptionService.subscribeMessage(messagesService.getMessage(mid), freefd); + discussions = (HtmlPage) discussions.refresh(); + assertThat(discussions.querySelectorAll("article").size(), is(1)); + subscriptionService.subscribeMessage(messagesService.getMessage(midNew), freefd); + discussions = (HtmlPage) discussions.refresh(); + assertThat(discussions.querySelectorAll("article").size(), is(2)); + assertThat(discussions.querySelectorAll("article").get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(midNew))); + messagesService.createReply(mid, 0, freefd, "I'm replied", null); + discussions = (HtmlPage) discussions.refresh(); + assertThat(discussions.querySelectorAll("article").size(), is(2)); + assertThat(discussions.querySelectorAll("article").get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(mid))); + Message msg = messagesService.getMessage(mid); + HtmlPage discussionsOld = webClient.getPage(discussionsUrl + "&to=" + msg.getUpdated().toEpochMilli()); + assertThat(discussionsOld.querySelectorAll("article").size(), is(1)); + assertThat(discussionsOld.querySelectorAll("article").get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(midNew))); + List newMids = IntStream.rangeClosed(1, 19).map(i -> messagesService.createMessage(ugnich.getUid(), String.valueOf(i), null, null)).boxed().collect(Collectors.toList()); + for (Integer m : newMids) { + subscriptionService.subscribeMessage(messagesService.getMessage(m), freefd); + } + discussions = (HtmlPage) discussions.refresh(); + assertThat(discussions.querySelectorAll("article").size(), is(20)); + assertThat(discussions.querySelectorAll("article") + .get(19).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(mid))); + messagesService.createReply(midNew, 0, freefd, "I'm replied", null); + discussions = (HtmlPage) discussions.refresh(); + assertThat(discussions.querySelectorAll("article") + .get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(midNew))); + Message old = messagesService.getMessage(newMids.get(0)); + discussionsOld = webClient.getPage(discussionsUrl + "&to=" + old.getUpdated().toEpochMilli()); + assertThat(discussionsOld.querySelectorAll("article").size(), is(1)); + assertThat(discussionsOld.querySelectorAll("article") + .get(0).getAttributes().getNamedItem("data-mid").getNodeValue(), is(String.valueOf(mid))); + } + @Test + public void redirectParamShouldCorrectlyRedirectLoggedUser() throws Exception { + MvcResult formLoginResult = mockMvc.perform(post("/login") + .param("username", ugnichName) + .param("password", ugnichPassword)) + .andExpect(status().isFound()).andReturn(); + Cookie rememberMeFromForm = formLoginResult.getResponse().getCookie("juick-remember-me"); + mockMvc.perform(get("/login").cookie(rememberMeFromForm)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/")); + mockMvc.perform(get("/login?redirect=false").cookie(rememberMeFromForm)) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/login/success")); + } + @Test + public void anythingRedirects() throws Exception { + int mid = messagesService.createMessage(ugnich.getUid(), "yo", null, null); + mockMvc.perform(get(String.format("/%d", mid))) + .andExpect(status().isMovedPermanently()) + .andExpect(redirectedUrl(String.format("/%s/%d", ugnich.getName(), mid))); + } + @Test + public void appAssociationsTest() throws Exception { + mockMvc.perform((get("/.well-known/apple-app-site-association"))) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_UTF8)) + .andExpect(jsonPath("$.webcredentials.apps[0]", is(appId))); + } + @Test + public void notificationsTests() throws Exception { + MvcResult loginResult = mockMvc.perform(post("/login") + .param("username", freefdName) + .param("password", freefdPassword)) + .andExpect(status().is3xxRedirection()).andReturn(); + Cookie loginCookie = loginResult.getResponse().getCookie("juick-remember-me"); + webClient.setCookieManager(new CookieManager()); + webClient.getCookieManager().addCookie( + new com.gargoylesoftware.htmlunit.util.Cookie(loginCookie.getDomain(), + loginCookie.getName(), + loginCookie.getValue())); + int mid = messagesService.createMessage(ugnich.getUid(), "new test", null, null); + subscriptionService.subscribeMessage(messagesService.getMessage(mid), freefd); + int rid = messagesService.createReply(mid, 0, ugnich, "new reply", null); + HtmlPage discussionsPage = webClient.getPage("http://localhost:8080/?show=discuss"); + assertThat(discussionsPage.querySelectorAll("#global a .badge").size(), is(1)); + HtmlPage unreadThread = webClient.getPage(String.format("http://localhost:8080/ugnich/%d", mid)); + assertThat(unreadThread.querySelectorAll("#global a .badge").size(), is(0)); + } + @Test + public void escapeSqlTests() { + String sql = String.format("SELECT * FROM table WHERE data='%s'", Utils.encodeSphinx("';-- DROP TABLE table")); + assertThat(sql, is("SELECT * FROM table WHERE data='\\';-- DROP TABLE table\'")); + } + @Test + public void bannedUserShouldBeShadowedFromRecommendationsList() throws IOException { + int ermineId = userService.createUser("ermine", "secret"); + int monstreekId = userService.createUser("monstreek", "secret"); + int pogoId = userService.createUser("pogo", "secret"); + int fmapId = userService.createUser("fmap", "secret"); + int mid = messagesService.createMessage(monstreekId, "KURWA", null, null); + assertThat(messagesService.recommendMessage(mid, ermineId), is(MessagesService.RecommendStatus.Added)); + assertThat(messagesService.recommendMessage(mid, fmapId), is(MessagesService.RecommendStatus.Added)); + assertThat(messagesService.recommendMessage(mid, pogoId), is(MessagesService.RecommendStatus.Added)); + assertThat(messagesService.getMessage(mid).getLikes(), is(3)); + assertThat(CollectionUtils.isEqualCollection(messagesService.getMessageRecommendations(mid), Arrays.asList("fmap", "ermine", "pogo")), is(true)); + privacyQueriesService.blacklistUser(userService.getUserByName("monstreek"), userService.getUserByName("pogo")); + assertThat(messagesService.getMessage(mid).getLikes(), is(3)); + assertThat(CollectionUtils.isEqualCollection(messagesService.getMessageRecommendations(mid), Arrays.asList("fmap", "ermine")), is(true)); + HtmlPage page = webClient.getPage(String.format("http://localhost:8080/monstreek/%d", mid)); + assertTrue(page.querySelector(".msg-recomms").getTextContent().contains("и еще 1")); + } +} -- cgit v1.2.3