Resolve merge conflicts

v1.18.x
Peter Jaszkowiak 8 years ago
commit cd1f0ddc79

@ -17,3 +17,4 @@ logs/
/coverage
/build
.eslintrc
test/files

1
.gitignore vendored

@ -1,3 +1,4 @@
yarn.lock
npm-debug.log
node_modules/
sftp-config.json

@ -8,6 +8,8 @@ var initWorker;
var incomplete = [];
var running = 0;
env.NODE_ENV = env.NODE_ENV || 'development';
module.exports = function (grunt) {
var args = [];
var initArgs = ['--build'];

@ -58,14 +58,14 @@
"nodebb-plugin-composer-default": "4.4.8",
"nodebb-plugin-dbsearch": "2.0.2",
"nodebb-plugin-emoji-extended": "1.1.1",
"nodebb-plugin-emoji-one": "1.1.5",
"nodebb-plugin-emoji-one": "1.2.1",
"nodebb-plugin-markdown": "7.1.1",
"nodebb-plugin-mentions": "2.0.3",
"nodebb-plugin-soundpack-default": "1.0.0",
"nodebb-plugin-spam-be-gone": "0.5.0",
"nodebb-rewards-essentials": "0.0.9",
"nodebb-theme-lavender": "4.0.0",
"nodebb-theme-persona": "5.0.0",
"nodebb-theme-persona": "5.0.1",
"nodebb-theme-vanilla": "6.0.2",
"nodebb-widget-essentials": "3.0.0",
"nodemailer": "2.6.4",

@ -17,6 +17,7 @@
"quote": "Quote",
"reply": "Reply",
"replies_to_this_post": "%1 Replies",
"one_reply_to_this_post": "1 Reply",
"last_reply_time": "Last reply",
"reply-as-topic": "Reply as topic",
"guest-login-reply": "Log in to reply",

@ -7,7 +7,7 @@
"none-active": "No hay Plug-ins activos",
"find-plugins": "Buscar Plug-in",
"plugin-search": "Plug-in de Búsqueda",
"plugin-search": "Buscar",
"plugin-search-placeholder": "Búscando Plug-in",
"reorder-plugins": "Re-ordenar Plug-ins",
"order-active": "Ordenar Plug-ins Activos",
@ -18,14 +18,14 @@
"order.explanation": "Los plug-in son cargados en el orden especificado, de arriba a abajo.",
"plugin-item.themes": "Temas",
"plugin-item.deactivate": "Desactivado",
"plugin-item.activate": "Activado",
"plugin-item.deactivate": "Desactivar",
"plugin-item.activate": "Activar",
"plugin-item.install": "Instalar",
"plugin-item.uninstall": "Desinstalar",
"plugin-item.settings": "Configuraciones",
"plugin-item.installed": "Instalados",
"plugin-item.latest": "Ultimos",
"plugin-item.upgrade": "Actualizado",
"plugin-item.upgrade": "Actualizar",
"plugin-item.more-info": "Para mas información:",
"plugin-item.unknown": "Desconocido",
"plugin-item.unknown-explanation": "El estado de este plug-in no puede determinsarse, posiblemente es debido a un error de configuración.",
@ -35,9 +35,9 @@
"alert.upgraded": "Plug-in Actualizado",
"alert.installed": "Plug-in Instalado",
"alert.uninstalled": "Plug-in Desinstalado",
"alert.activate-success": "Por favor reinicia NodeBB para activar el plug-in por completo",
"alert.activate-success": "Por favor reiniciá NodeBB para activar el plug-in por completo",
"alert.deactivate-success": "Plugin successfully deactivated",
"alert.upgrade-success": "Please reload your NodeBB to fully upgrade this plugin",
"alert.upgrade-success": "Por favor recargá NodeBB para actualizar el plug-in por completo",
"alert.install-success": "Plugin successfully installed, please activate the plugin.",
"alert.uninstall-success": "The plugin has been successfully deactivated and uninstalled.",
"alert.suggest-error": "<p>NodeBB could not reach the package manager, proceed with installation of latest version?</p><div class=\"alert alert-danger\"><strong>Server returned (%1)</strong>: %2</div>",

@ -1,20 +1,20 @@
{
"category": "Category",
"subcategories": "Subcategories",
"new_topic_button": "กระทู้",
"guest-login-post": "เข้าสู่ระบบเพื่อโพส",
"category": "หมวดหมู่",
"subcategories": "หมวดหมู่ย่อย",
"new_topic_button": "ตั้งกระทู้",
"guest-login-post": "เข้าสู่ระบบเพื่อโพสต์",
"no_topics": "<strong>ยังไม่มีกระทู้ในหมวดนี้</strong><br />โพสต์กระทู้แรก?",
"browsing": "เรียกดู",
"no_replies": "ยังไม่มีใครตอบ",
"no_new_posts": "No new posts.",
"share_this_category": "แชร์ Category นี้",
"watch": "Watch",
"no_new_posts": "ไม่มีกระทู้ใหม่",
"share_this_category": "แชร์หมวดนี้",
"watch": "ตามดู",
"ignore": "ไม่ต้องสนใจอีก",
"watching": "Watching",
"ignoring": "Ignoring",
"watching.description": "Show topics in unread",
"ignoring.description": "Do not show topics in unread",
"watch.message": "You are now watching updates from this category and all subcategories",
"ignore.message": "You are now ignoring updates from this category and all subcategories",
"watched-categories": "Watched categories"
"watching": "กำลังตามดู",
"ignoring": "เมินเฉย",
"watching.description": "แสดงกระทู้ที่ยังไม่ได้อ่าน",
"ignoring.description": "ไม่แสดงกระทู้ที่ยังไม่ได้อ่าน",
"watch.message": "ตอนนี้คุณกำลังตามดูอัพเดทจากกระทู้หมวดนี้และหมวดหมู่ย่อยทั้งหมดในนี้",
"ignore.message": "ตอนนี้คุณกำลังเมินเฉยต่ออัพเดทจากกระทู้หมวดนี้และหมวดหมู่ย่อยทั้งหมดในนี้",
"watched-categories": "หมวดหมู่ที่ดูแล้ว"
}

@ -1,40 +1,40 @@
{
"password-reset-requested": "Password Reset Requested - %1!",
"password-reset-requested": "ส่งคำขอตั้งค่ารหัสผ่านใหม่แล้ว - %1!",
"welcome-to": "ยินดีต้อนรับ %1",
"invite": "Invitation from %1",
"invite": "คำเชิญจาก %1",
"greeting_no_name": "สวัสดี",
"greeting_with_name": "สวัสดี %1",
"welcome.text1": "ขอบคุณที่ลงทะเบียนกับ %1",
"welcome.text2": "To fully activate your account, we need to verify that you own the email address you registered with.",
"welcome.text3": "An administrator has accepted your registration application. You can login with your username/password now.",
"welcome.text2": "เพื่อให้การบัญชีของคุณใช้งานได้อย่างเสร็จสมบูรณ์ เราจำเป็นต้องยืนยันว่าคุณเป็นเจ้าของที่แท้จริงอีเมล์ที่ใช้สมัครสมาชิก",
"welcome.text3": "ผู้ดูแลระบบได้ทำการยอมรับการสมัครสมาชิกของคุณแล้ว คุณสามารถเข้าสู่ระบบด้วย ชื่อผู้ใช้/รหัสผ่าน ได้แล้วตอนนี้",
"welcome.cta": "กดตรงนี้เพื่อยืนยันอีเมลของคุณ",
"invitation.text1": "%1 has invited you to join %2",
"invitation.ctr": "Click here to create your account.",
"reset.text1": "We received a request to reset your password, possibly because you have forgotten it. If this is not the case, please ignore this email.",
"invitation.text1": "%1 ได้เชิญคุณให้เข้าร่วม %2",
"invitation.ctr": "คลิกที่นี่เพื่อสร้างบัญชีของคุณ",
"reset.text1": "เราได้รับคำร้องให้ตั้งค่ารหัสผ่านใหม่ของคุณ อาจจะเป็นเพราะว่าคุณลืมรหัสผ่านและได้ทำการส่งคำขอเข้ามา หากไม่ใช่ กรุณาเพิกเฉยต่ออีเมล์นี้และไม่ต้องดำเนินการใดๆทั้งสิ้น",
"reset.text2": "เพื่อดำเนินการตั้งรหัสผ่านใหม่ต่อไป, โปรดกดที่ลิ้งค์นี้:",
"reset.cta": "กดตรงนี้เพื่อตั้งรหัสผ่านใหม่",
"reset.notify.subject": "Password successfully changed",
"reset.notify.text1": "We are notifying you that on %1, your password was changed successfully.",
"reset.notify.text2": "If you did not authorise this, please notify an administrator immediately.",
"reset.notify.subject": "ตั้งค่ารหัสผ่านใหม่เรียบร้อยแล้ว",
"reset.notify.text1": "เรากำลังแจ้งคุณว่าตอน %1 รหัสผ่านของคุณถูกเปลี่ยนเรียบร้อยแล้ว",
"reset.notify.text2": "หากคุณไม่ได้เป็นคนอนุญาตสิ่งนี้ กรุณาแจ้งไปยังผู้ดูแลระบบโดยทันที",
"digest.notifications": "คุณมีข้อความแจ้งเตือนที่ยังไม่ได้อ่านจาก %1:",
"digest.latest_topics": "หัวข้อสนทนาล่าสุดจาก %1",
"digest.cta": "กดตรงนี้เพื่อเข้าดู %1",
"digest.unsub.info": "This digest was sent to you due to your subscription settings.",
"digest.no_topics": "There have been no active topics in the past %1",
"digest.day": "day",
"digest.week": "week",
"digest.month": "month",
"digest.subject": "Digest for %1",
"notif.chat.subject": "New chat message received from %1",
"digest.unsub.info": "คำชี้แจงถูกส่งไปให้คุณแล้ว เนื่องมาจากการตั้งค่าสมาชิกของคุณ",
"digest.no_topics": "ไม่มีกระทู้ใดๆเลยใน %1 ที่ผ่านมา",
"digest.day": "วัน",
"digest.week": "สัปดาห์",
"digest.month": "เดือน",
"digest.subject": "คำชี้แจงสำหรับ %1",
"notif.chat.subject": "ได้รับข้อความแชทใหม่จาก %1",
"notif.chat.cta": "กดตรงนี้เพื่อกลับไปยังบทสนทนา",
"notif.chat.unsub.info": "This chat notification was sent to you due to your subscription settings.",
"notif.post.cta": "Click here to read the full topic",
"notif.post.unsub.info": "This post notification was sent to you due to your subscription settings.",
"notif.chat.unsub.info": "การแจ้งเตือนแชทนี้ถูกส่งไปหาคุณเนื่องจากการตั้งค่าสมาชิกของคุณ",
"notif.post.cta": "คลิกที่นี่เพื่ออ่านกระทู้ฉบับเต็ม",
"notif.post.unsub.info": "การแจ้งเตือนกระทู้นี้ถูกส่งไปยังคุณเนื่องการตั้งค่าสมาชิกของคุณ",
"test.text1": "นี่คืออีเมลทดสอบเพื่อยืนยันว่าระบบอีเมลมีการตั้งค่าที่ถูกต้องสำหรับ NodeBB ของคุณ",
"unsub.cta": "กดตรงนี้เพื่อเปลี่ยนแปลงการตั้งค่า",
"banned.subject": "You have been banned from %1",
"banned.text1": "The user %1 has been banned from %2.",
"banned.text2": "This ban will last until %1.",
"banned.text3": "This is the reason why you have been banned:",
"banned.subject": "คุณถูกแบนจาก %1 แล้ว",
"banned.text1": "ผู้ใช้ %1 ได้ถูกแบนจาก %2",
"banned.text2": "การแบนนี้จะใช้เวลาจนถึง %1",
"banned.text3": "นี่คือเหตุผลที่ทำไมคุณถึงถูกแบน",
"closing": "ขอบคุณ!"
}

@ -2,7 +2,7 @@
"invalid-data": "ข้อมูลไม่ถูกต้อง",
"not-logged-in": "คุณยังไม่ได้ลงชื่อเข้าระบบ",
"account-locked": "บัญชีของคุณถูกระงับการใช้งานชั่วคราว",
"search-requires-login": "Searching requires an account - please login or register.",
"search-requires-login": "\"ฟังก์ชั่นการค้นหา\" ต้องการบัญชีผู้ใช้ กรุณาเข้าสู่ระบบหรือสมัครสมาชิก",
"invalid-cid": "Category ID ไม่ถูกต้อง",
"invalid-tid": "Topic ID ไม่ถูกต้อง",
"invalid-pid": "Post ID ไม่ถูกต้อง",
@ -14,35 +14,35 @@
"invalid-password": "รหัสผ่านไม่ถูกต้อง",
"invalid-username-or-password": "กรุณาระบุชื่อผู้ใช้และรหัสผ่าน",
"invalid-search-term": "ข้อความค้นหาไม่ถูกต้อง",
"csrf-invalid": "We were unable to log you in, likely due to an expired session. Please try again",
"invalid-pagination-value": "Invalid pagination value, must be at least %1 and at most %2",
"csrf-invalid": "เราไม่สามารถนำท่านเข้าสู่ระบบได้ เหมือนกับว่าเซสชั่นหมดอายุแล้ว กรุณาลองใหม่อีกครั้ง",
"invalid-pagination-value": "หมายเลขหน้าไม่ถูกต้อง จำเป็นต้องเป็นตัวเลขอย่างน้อย %1 และอย่างมาก %2",
"username-taken": "ชื่อผู้ใช้นี้มีการใช้แล้ว",
"email-taken": "อีเมลนี้มีการใช้แล้ว",
"email-not-confirmed": "ยังไม่มีการยืนยันอีเมลของคุณ, โปรดกดยืนยันอีเมลของคุณตรงนี้",
"email-not-confirmed-chat": "You are unable to chat until your email is confirmed, please click here to confirm your email.",
"email-not-confirmed-email-sent": "Your email has not been confirmed yet, please check your inbox for the confirmation email.",
"email-not-confirmed-chat": "คุณไม่สามารถแชทได้จนกว่าอีเมล์ของคุณจะได้รับการยืนยัน กรุณาคลิกที่นี่เพื่อยืนยันอีกมเมล์ของคุณ",
"email-not-confirmed-email-sent": "อีเมล์ของคุณยังไม่ได้รับการยืนยัน กรุณาเช็คกล่องข้อความในอีเมล์เพื่อกดยืนยัน",
"no-email-to-confirm": "Forum นี้ต้องการการยืนยันอีเมล กรุณากดที่นี่เพื่อระบุอีเมล",
"email-confirm-failed": "เราไม่สามารถยืนยันอีเมลของคุณ ณ ขณะนี้ กรุณาลองใหม่อีกครั้งภายหลัง",
"confirm-email-already-sent": "Confirmation email already sent, please wait %1 minute(s) to send another one.",
"sendmail-not-found": "The sendmail executable could not be found, please ensure it is installed and executable by the user running NodeBB.",
"confirm-email-already-sent": "อีเมล์ยืนยันตัวตนถูกส่งไปยังคุณเรียบร้อยแล้ว กรุณารอ %1 นาที(s) ก่อนการตัดสินใจส่งอีกครั้ง",
"sendmail-not-found": "ไม่พบการประมวลผลสำหรับการส่งอีเมล์ กรุณาตรวจสอบให้แน่ใจว่าได้มีการติดตั้งโปรแกรมการประมวลผลแล้วโดยผู้ใช้ที่กำลังใช้ NodeBB",
"username-too-short": "ชื่อบัญชีผู้ใช้ สั้นเกินไป",
"username-too-long": "ชื่อบัญชีผู้ใช้ ยาวเกินไป",
"password-too-long": "Password too long",
"user-banned": "User banned",
"user-banned-reason": "Sorry, this account has been banned (Reason: %1)",
"user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)",
"user-too-new": "Sorry, you are required to wait %1 second(s) before making your first post",
"blacklisted-ip": "Sorry, your IP address has been banned from this community. If you feel this is in error, please contact an administrator.",
"ban-expiry-missing": "Please provide an end date for this ban",
"password-too-long": "รหัสผ่านยาวเกินไป",
"user-banned": "ผู้ใช้ได้รับการแบน",
"user-banned-reason": "ขออภัย บัญชีผู้ใช้นี้ได้รับการแบน (เหตุผล : %1)",
"user-banned-reason-until": "ขออภัย บัญชีผู้ใช้นี้ได้รับการแบนจนถึง %1 (เหตุผล : %2)",
"user-too-new": "ขออภัย คุณจำเป็นต้องรอ %1 วินาที(s) ก่อนการสร้างกระทู้แรกของคุณ",
"blacklisted-ip": "ขออภัย IP Address ของคุณถูกแบนจากชุมชนนี้ หากคุณคิดว่านี่เป็นเออเร่อของระบบ กรุณาติดต่อผู้ดูแลระบบ",
"ban-expiry-missing": "กรุณาระบุวันสิ้นสุดสำหรับการแบนในครั้งนี้",
"no-category": "ยังไม่มี Category นี้",
"no-topic": "ยังไม่มี Topic นี้",
"no-post": "ยังไม่มี Post นี้",
"no-group": "ยังไม่มี Group นี้",
"no-user": "ยังไม่มีผู้ใช้งานนี้",
"no-teaser": "Teaser does not exist",
"no-teaser": "ยังไม่มีทีเซอร์นี้",
"no-privileges": "คุณมีสิทธิ์ไม่เพียงพอที่จะทำรายการนี้",
"category-disabled": "Category นี้ถูกปิดการใช้งานแล้ว",
"topic-locked": "Topic Locked",
"topic-locked": "กระทู้ถูกล็อก",
"post-edit-duration-expired": "You are only allowed to edit posts for %1 second(s) after posting",
"post-edit-duration-expired-minutes": "You are only allowed to edit posts for %1 minute(s) after posting",
"post-edit-duration-expired-minutes-seconds": "You are only allowed to edit posts for %1 minute(s) %2 second(s) after posting",

@ -1,6 +1,6 @@
{
"uploading-file": "Uploading the file...",
"select-file-to-upload": "Select a file to upload!",
"upload-success": "File uploaded successfully!",
"maximum-file-size": "Maximum %1 kb"
"uploading-file": "กำลังอัพโหลดไฟล์ ...",
"select-file-to-upload": "กรุณาเลือกไฟล์ที่จะอัพโหลด",
"upload-success": "อัพโหลดไฟล์เรียบร้อยแล้ว",
"maximum-file-size": "มากที่สุดได้ %1 kb"
}

@ -104,7 +104,7 @@
"open_links_in_new_tab": "เปิดลิงค์ในแท็บใหม่",
"enable_topic_searching": "เปิดใช้การค้นหาแบบ In-Topic",
"topic_search_help": "หากเปิดใช้งาน, \"การค้นหาภายในกระทู้\" จะแทนที่ระบบ \"การค้นหาจากค่าเริ่มต้นของเบราเซอร์\" และจะทำให้คุณค้นหาข้อมูลต่างๆภายในกระทู้ได้ แทนที่จะเป็นการหาแค่สิ่งที่แสดงบนหน้าจอเท่านั้น",
"delay_image_loading": "Delay Image Loading",
"delay_image_loading": "การโหลดรูปภาพช้าลง",
"image_load_delay_help": "หากเปิดใช้งาน, รูปภาพในกระทู้จะไม่โหลดจนกว่าจะมีการเลื่อนไปดู",
"scroll_to_my_post": "After posting a reply, show the new post",
"follow_topics_you_reply_to": "ดูกระทู้ที่คุณตอบ",

@ -8,13 +8,13 @@
"mongo": "Mongo",
"mongo.version": "MongoDB Sürümü",
"mongo.storage-engine": "Storage Engine",
"mongo.collections": "Collections",
"mongo.objects": "Objects",
"mongo.collections": "Koleksiyonlar",
"mongo.objects": "Objeler",
"mongo.avg-object-size": "Avg. Object Size",
"mongo.data-size": "Veri Boyutu",
"mongo.storage-size": "Storage Size",
"mongo.index-size": "Index Size",
"mongo.file-size": "File Size",
"mongo.index-size": "İndex Boyutu",
"mongo.file-size": "Dosya Boyutu",
"mongo.resident-memory": "Resident Memory",
"mongo.virtual-memory": "Virtual Memory",
"mongo.mapped-memory": "Mapped Memory",

@ -1,6 +1,6 @@
{
"events": "Olaylar",
"no-events": "Olay yok",
"control-panel": "Events Control Panel",
"control-panel": "Etkinlik Kontrol Paneli",
"delete-events": "Olayları Sil"
}

@ -1,9 +1,9 @@
{
"custom-css": "Özel CSS",
"custom-css.description": "Enter your own CSS declarations here, which will be applied after all other styles.",
"custom-css.enable": "Özel CSS Aktif",
"custom-css.description": "Özel CSS kodlarınızı bu alana girin.",
"custom-css.enable": "Özel CSS Etkinleştir",
"custom-header": "Özel Header",
"custom-header.description": "Enter custom HTML here (ex. JavaScript, Meta Tags, etc.), which will be appended to the <code>&lt;head&gt;</code> section of your forum's markup.",
"custom-header.enable": "Enable Custom Header"
"custom-header.description": "Forumunuzun biçimlendirmesini sağlayacak <code>&lt;head&gt;</code> bölümüne eklenecek özel HTML'yi (ör. JavaScript, Meta Etiketler vb.) Girin.",
"custom-header.enable": "Özel Header'ı Etkinleştir"
}

@ -2,8 +2,8 @@
"loading": "Deriler yükleniyor...",
"homepage": "Anasayfa",
"select-skin": "Deri Seç",
"current-skin": "Current Skin",
"skin-updated": "Skin Updated",
"applied-success": "%1 skin was succesfully applied",
"revert-success": "Skin reverted to base colours"
"current-skin": "Mevcut Deri",
"skin-updated": "Deri Güncellendi",
"applied-success": "%1 deri başarıyla uygulandı",
"revert-success": "Deri taban renkleri geri döndürüldü"
}

@ -2,10 +2,10 @@
"checking-for-installed": "Yüklü temalar kontrol ediliyor...",
"homepage": "Anasayfa",
"select-theme": "Tema Seç",
"current-theme": "Current Theme",
"no-themes": "No installed themes found",
"revert-confirm": "Are you sure you wish to restore the default NodeBB theme?",
"theme-changed": "Theme Changed",
"revert-success": "You have successfully reverted your NodeBB back to it's default theme.",
"restart-to-activate": "Please restart your NodeBB to fully activate this theme"
"current-theme": "Geçerli Tema",
"no-themes": "Yüklü tema bulunamadı",
"revert-confirm": "Varsayılan NodeBB temasını geri yüklemek istediğinizden emin misiniz?",
"theme-changed": "Tema Değiştirildi",
"revert-success": "NodeBB'nin varsayılan temasına başarıyla geri dönüş yaptınız.",
"restart-to-activate": "Temayı tamamen aktif hale getirebilmek için NodeBB'yi yeniden başlat"
}

@ -5,14 +5,14 @@
"users": "Kullanıcılar",
"posts": "İletiler",
"topics": "Başlıklar",
"page-views-seven": "Last 7 Days",
"page-views-thirty": "Last 30 Days",
"page-views-last-day": "Last 24 hours",
"page-views-custom": "Custom Date Range",
"page-views-custom-start": "Range Start",
"page-views-custom-end": "Range End",
"page-views-seven": "Son 7 Gün",
"page-views-thirty": "Son 30 Gün",
"page-views-last-day": "Son 24 saat",
"page-views-custom": "Özel Tarih Aralığı",
"page-views-custom-start": "Başlangıç",
"page-views-custom-end": "Son",
"page-views-custom-help": "Enter a date range of page views you would like to view. If no date picker is available, the accepted format is <code>YYYY-MM-DD</code>",
"page-views-custom-error": "Please enter a valid date range in the format <code>YYYY-MM-DD</code>",
"page-views-custom-error": "Lütfen tarih aralığını geçerli formatta girin <code>YYYY-MM-DD</code>",
"stats.day": "Gün",
"stats.week": "Hafta",
@ -36,9 +36,9 @@
"search-plugin-tooltip": "Install a search plugin from the plugin page in order to activate search functionality",
"control-panel": "Sistem Kontrol",
"reload": "Tekrar Yükle",
"restart": "Yeniden Başlat",
"restart-warning": "Reloading or Restarting your NodeBB will drop all existing connections for a few seconds.",
"reload": "Reload",
"restart": "Restart",
"restart-warning": "NodeBB yeniden yüklemek veya yeniden başlatmak için mevcut tüm bağlantıları birkaç saniye düşürür.",
"maintenance-mode": "Bakım Modu",
"maintenance-mode-title": "NodeBB için bakım modunu ayarlamak için buraya tıklayın",
"realtime-chart-updates": "Gerçek Zamanlı Grafik Güncellemeleri",
@ -60,7 +60,7 @@
"recent": "Yeni",
"unread": "Okunmamış",
"high-presence-topics": "High Presence Topics",
"high-presence-topics": "Öne Çıkan Başlıklar",
"graphs.page-views": "Sayfa Gösterimi",
"graphs.unique-visitors": "Benzersiz Ziyaretçiler",

@ -1,7 +1,7 @@
{
"home-page": "Ana Sayfa",
"description": "Choose what page is shown when users navigate to the root URL of your forum.",
"home-page-route": "Home Page Route",
"custom-route": "Custom Route",
"allow-user-home-pages": "Allow User Home Pages"
"home-page-route": "Anasayfa Yolu",
"custom-route": "Özel Yol",
"allow-user-home-pages": "Kullanıcılara anasayfayı özelleştirmeye izin ver"
}

@ -2,8 +2,8 @@
"notifications": "Bildiriler",
"chat-messages": "Sohbet Mesajları",
"play-sound": "Oynat",
"incoming-message": "Incoming Message",
"outgoing-message": "Outgoing Message",
"incoming-message": "Gelen İleti",
"outgoing-message": "Giden İleti",
"upload-new-sound": "Yeni Ses Yükle",
"saved": "Settings Saved"
"saved": "Ayarlar Kaydedildi"
}

@ -1,30 +1,30 @@
{
"settings": "Kategori Ayarları",
"privileges": "Privileges",
"privileges": "İzinler",
"name": "Kategori Adı",
"description": "Category Description",
"bg-color": "Background Colour",
"description": "Kategori Açıklama",
"bg-color": "Arkaplan Rengi",
"text-color": "Yazı Rengi",
"bg-image-size": "Arkaplan Görseli Boyutu",
"custom-class": "Özel Sınıf",
"num-recent-replies": "# of Recent Replies",
"ext-link": "External Link",
"ext-link": "Harici Bağlantı",
"upload-image": "Görsel Yükle",
"delete-image": "Remove",
"category-image": "Category Image",
"delete-image": "Sil",
"category-image": "Kategori Görseli",
"parent-category": "Parent Category",
"optional-parent-category": "(Optional) Parent Category",
"parent-category-none": "(None)",
"parent-category-none": "(Hiçbiri)",
"copy-settings": "Copy Settings From",
"optional-clone-settings": "(Optional) Clone Settings From Category",
"purge": "Purge Category",
"enable": "Enable",
"disable": "Disable",
"edit": "Edit",
"enable": "Etkinleştir",
"disable": "Devredışı",
"edit": "Düzenle",
"select-category": "Select Category",
"select-category": "Kategori Seç",
"set-parent-category": "Set Parent Category",
"privileges.description": "You can configure the access control privileges for this category in this section. Privileges can be granted on a per-user or a per-group basis. You can add a new user to this table by searching for them in the form below.",
@ -32,27 +32,27 @@
"privileges.section-viewing": "Viewing Privileges",
"privileges.section-posting": "Posting Privileges",
"privileges.section-moderation": "Moderation Privileges",
"privileges.section-user": "User",
"privileges.search-user": "Add User",
"privileges.section-user": "Kullanıcı",
"privileges.search-user": "Kullanıcı Ekle",
"privileges.no-users": "No user-specific privileges in this category.",
"privileges.section-group": "Group",
"privileges.group-private": "This group is private",
"privileges.search-group": "Add Group",
"privileges.section-group": "Grup",
"privileges.group-private": "Bu grup gizlidir",
"privileges.search-group": "Grup Ekle",
"privileges.copy-to-children": "Copy to Children",
"privileges.copy-from-category": "Copy from Category",
"privileges.inherit": "If the <code>registered-users</code> group is granted a specific privilege, all other groups receive an <strong>implicit privilege</strong>, even if they are not explicitly defined/checked. This implicit privilege is shown to you because all users are part of the <code>registered-users</code> user group, and so, privileges for additional groups need not be explicitly granted.",
"analytics.back": "Back to Categories List",
"analytics.back": "Kategori listesine geri dön",
"analytics.title": "Analytics for \"%1\" category",
"analytics.pageviews-hourly": "<strong>Figure 1</strong> &ndash; Hourly page views for this category</small>",
"analytics.pageviews-daily": "<strong>Figure 2</strong> &ndash; Daily page views for this category</small>",
"analytics.topics-daily": "<strong>Figure 3</strong> &ndash; Daily topics created in this category</small>",
"analytics.posts-daily": "<strong>Figure 4</strong> &ndash; Daily posts made in this category</small>",
"alert.created": "Created",
"alert.created": "Yaratıldı",
"alert.create-success": "Category successfully created!",
"alert.none-active": "You have no active categories.",
"alert.create": "Create a Category",
"alert.create": "Bir Kategori Yarat",
"alert.confirm-moderate": "<strong>Are you sure you wish to grant the moderation privilege to this user group?</strong> This group is public, and any users can join at will.",
"alert.confirm-purge": "<p class=\"lead\">Do you really want to purge this category \"%1\"?</p><h5><strong class=\"text-danger\">Warning!</strong> All topics and posts in this category will be purged!</h5> <p class=\"help-block\">Purging a category will remove all topics and posts, and delete the category from the database. If you want to remove a category <em>temporarily</em>, you'll want to \"disable\" the category instead.</p>",
"alert.purge-success": "Category purged!",
@ -61,7 +61,7 @@
"alert.updated": "Updated Categories",
"alert.updated-success": "Category IDs %1 successfully updated.",
"alert.upload-image": "Kategori görseli yükle",
"alert.find-user": "Find a User",
"alert.find-user": "Bir Kullanıcı Ara",
"alert.user-search": "Search for a user here...",
"alert.find-group": "Find a Group",
"alert.group-search": "Search for a group here..."

@ -2,7 +2,7 @@
"users": "Kullanıcılar",
"edit": "Düzenle",
"make-admin": "Yönetici Yap",
"remove-admin": "Remove Admin",
"remove-admin": "Yöneticiliği Sil",
"validate-email": "Validate Email",
"send-validation-email": "Send Validation Email",
"password-reset-email": "Send Password Reset Email",
@ -10,7 +10,7 @@
"temp-ban": "Ban User(s) Temporarily",
"unban": "Unban User(s)",
"reset-lockout": "Reset Lockout",
"reset-flags": "Reset Flags",
"reset-flags": "Bayrakları Sıfırla",
"delete": "Delete User(s)",
"purge": "Delete User(s) and Content",
"download-csv": "Download CSV",
@ -24,7 +24,7 @@
"pills.top-rep": "Most Reputation",
"pills.inactive": "Inactive",
"pills.flagged": "Most Flagged",
"pills.banned": "Banned",
"pills.banned": "Yasaklandı",
"pills.search": "User Search",
"search.username": "By User Name",

@ -8,19 +8,19 @@
"max-file-size-help": "(in kilobytes, default: 2048 KiB)",
"allow-topic-thumbnails": "Allow users to upload topic thumbnails",
"topic-thumb-size": "Topic Thumb Size",
"allowed-file-extensions": "Allowed File Extensions",
"allowed-file-extensions": "İzin Verilen Dosya Uzantıları",
"allowed-file-extensions-help": "Enter comma-separated list of file extensions here (e.g. <code>pdf,xls,doc</code>). An empty list means all extensions are allowed.",
"profile-avatars": "Profile Avatars",
"profile-avatars": "Profil Avatarları",
"allow-profile-image-uploads": "Allow users to upload profile images",
"convert-profile-image-png": "Convert profile image uploads to PNG",
"default-avatar": "Custom Default Avatar",
"upload": "Yükle",
"profile-image-dimension": "Profile Image Dimension",
"profile-image-dimension-help": "(in pixels, default: 128 pixels)",
"profile-image-dimension": "Profil Resmi Boyutu",
"profile-image-dimension-help": "(Piksel cinsinden, varsayılan: 128 piksel)",
"max-profile-image-size": "Maximum Profile Image File Size",
"max-profile-image-size-help": "(in kilobytes, default: 256 KiB)",
"max-profile-image-size-help": "(Kilobayt, varsayılan: 256 KiB)",
"max-cover-image-size": "Maksimum Kapak Görseli Dosya Boyutu",
"max-cover-image-size-help": "(in kilobytes, default: 2,048 KiB)",
"max-cover-image-size-help": "(Kilobayt, varsayılan: 2,048 KiB)",
"keep-all-user-images": "Keep old versions of avatars and profile covers on the server",
"profile-covers": "Profil Kapakları",
"default-covers": "Varsayılan Kapak Görseli",

@ -32,9 +32,9 @@
"notif.post.unsub.info": "Bu yazı bildirimi size abonelik ayarlarınız nedeni ile gönderilmiştir.",
"test.text1": "Bu ileti NodeBB e-posta ayarlarınızın doğru çalışıp çalışmadığını kontrol etmek için gönderildi.",
"unsub.cta": "Buraya tıklayarak ayarlarınızı değiştirebilirsiniz.",
"banned.subject": "You have been banned from %1",
"banned.text1": "The user %1 has been banned from %2.",
"banned.text2": "This ban will last until %1.",
"banned.text3": "This is the reason why you have been banned:",
"banned.subject": "%1 'den yasaklandınız",
"banned.text1": "%1 kullanıcısı %2 'den yasaklandı.",
"banned.text2": "Bu yasak %1 'e kadar sürecek.",
"banned.text3": "Yasaklanmanın nedeni:",
"closing": "Teşekkürler!"
}

@ -30,7 +30,7 @@
"password-too-long": "Parola çok uzun",
"user-banned": "Kullanıcı Yasaklı",
"user-banned-reason": "Maalesef, bu hesap yasaklandı (Sebep:% 1)",
"user-banned-reason-until": "Sorry, this account has been banned until %1 (Reason: %2)",
"user-banned-reason-until": "Maalesef, bu hesap %1 kadar yasaklandı (Sebep: %2)",
"user-too-new": "Özür dileriz, ilk iletinizi yapmadan önce %1 saniye beklemeniz gerekiyor",
"blacklisted-ip": "Üzgünüz, IP adresiniz, bu toplulukta yasaklandı. Bunun bir hata olduğunu düşünüyorsanız, bir yönetici ile irtibata geçiniz.",
"ban-expiry-missing": "Bu yasak için bir bitiş tarihi girin",
@ -105,7 +105,7 @@
"chat-disabled": "Sohbet özelliği kapalı",
"too-many-messages": "Ardı ardına çok fazla mesaj yolladınız, lütfen biraz bekleyiniz.",
"invalid-chat-message": "Geçersiz sohbet mesajı",
"chat-message-too-long": "Chat messages can not be longer than %1 characters.",
"chat-message-too-long": "Sohbet mesajı %1 karakterden daha uzun olamaz.",
"cant-edit-chat-message": "Bu mesajı düzenlemek için izin verilmez",
"cant-remove-last-user": "Son kullanıcıyı silemezsiniz",
"cant-delete-chat-message": "Bu mesajı silmek için izin verilmez",

@ -20,7 +20,7 @@
"chat.three_months": "3 Ay",
"chat.delete_message_confirm": "Bu mesajı silmek istediğinden emin misin?",
"chat.add-users-to-room": "Odaya Kullanıcı Ekle",
"chat.confirm-chat-with-dnd-user": "This user has set their status to DnD(Do not disturb). Do you still want to chat with them?",
"chat.confirm-chat-with-dnd-user": "Bu kullanıcı durumunu rahatsız etmeyin olarak ayarladı. Hala onunla sohbet etmek istiyor musun?",
"composer.compose": "Yaz",
"composer.show_preview": "Önizleme Göster",
"composer.hide_preview": "Önizleme Sakla",

@ -40,7 +40,7 @@
"user_started_following_you_multiple": "<strong>%1</strong> ve %2 kişi daha seni takip etmeye başladı.",
"new_register": "<strong>%1</strong> kayıt olma isteği gönderdi.",
"new_register_multiple": "Beklemede <strong>%1</strong> kayıt olma isteği bulunmaktadır.",
"flag_assigned_to_you": "<strong>Flag %1</strong> has been assigned to you",
"flag_assigned_to_you": "<Strong>Bayrak %1</ strong> size devredildi",
"email-confirmed": "E-posta onaylandı",
"email-confirmed-message": "E-postanızı onaylandığınız için teşekkürler. Hesabınız tamamen aktive edildi.",
"email-confirm-error-message": "E-posta adresinizi onaylarken bir hata oluştu. Kodunuz geçersiz ya da eski olabilir.",

@ -89,8 +89,19 @@ define('forum/topic/replies', ['navigator', 'components', 'forum/topic/posts'],
var timestamp = replyCount.find('.timeago').attr('title', post.timestampISO);
countEl.attr('data-replies', count);
replyCount.toggleClass('hidden', !count);
countEl.translateText('[[topic:replies_to_this_post, ' + count + ']]');
replyCount.toggleClass('hidden', count <= 0);
if (count > 1) {
countEl.translateText('[[topic:replies_to_this_post, ' + count + ']]');
} else {
countEl.translateText('[[topic:one_reply_to_this_post]]');
}
if (!avatars.find('[data-uid="' + post.uid + '"]').length && count < 7) {
app.parseAndTranslate('topic', 'posts', { posts: [{ replies: { users: [post.user] } }] }, function (html) {
avatars.prepend(html.find('[component="post/reply-count/avatars"] [component="user/picture"]'));
});
}
avatars.addClass('hasMore');
timestamp.data('timeago', null).timeago();

@ -304,7 +304,7 @@
* Load translation file (or use a cached version), and optionally return the translation of a certain key
* @param {string} namespace - The file name of the translation namespace
* @param {string} [key] - The key of the specific translation to getJSON
* @returns {Promise<Object>|Promise<string>}
* @returns {Promise<{ [key: string]: string }>|Promise<string>}
*/
Translator.prototype.getTranslation = function getTranslation(namespace, key) {
var translation;
@ -324,6 +324,70 @@
return translation;
};
/**
* @param {Node} node
* @returns {Node[]}
*/
function descendantTextNodes(node) {
var textNodes = [];
function helper(node) {
if (node.nodeType === 3) {
textNodes.push(node);
} else {
for (var i = 0, c = node.childNodes, l = c.length; i < l; i += 1) {
helper(c[i]);
}
}
}
helper(node);
return textNodes;
}
/**
* Recursively translate a DOM element in place
* @param {Element} element - Root element to translate
* @param {string[]} [attributes] - Array of node attributes to translate
* @returns {Promise<void>}
*/
Translator.prototype.translateInPlace = function translateInPlace(element, attributes) {
attributes = attributes || ['placeholder', 'title'];
var nodes = descendantTextNodes(element);
var text = nodes.map(function (node) {
return node.nodeValue;
}).join(' || ');
var attrNodes = attributes.reduce(function (prev, attr) {
var tuples = Array.prototype.map.call(element.querySelectorAll('[' + attr + '*="[["]'), function (el) {
return [attr, el];
});
return prev.concat(tuples);
}, []);
var attrText = attrNodes.map(function (node) {
return node[1].getAttribute(node[0]);
}).join(' || ');
return Promise.all([
this.translate(text),
this.translate(attrText),
]).then(function (ref) {
var translated = ref[0];
var translatedAttrs = ref[1];
if (translated) {
translated.split(' || ').forEach(function (html, i) {
$(nodes[i]).replaceWith(html);
});
}
if (translatedAttrs) {
translatedAttrs.split(' || ').forEach(function (text, i) {
attrNodes[i][1].setAttribute(attrNodes[i][0], text);
});
}
});
};
/**
* Get the language of the current environment, falling back to defaults
* @returns {string}

@ -1,67 +1,20 @@
/* global bootbox */
require(['translator'], function (shim) {
"use strict";
function descendantTextNodes(node) {
var textNodes = [];
function helper(node) {
if (node.nodeType === 3) {
textNodes.push(node);
} else {
for (var i = 0, c = node.childNodes, l = c.length; i < l; i += 1) {
helper(c[i]);
}
}
}
helper(node);
return textNodes;
}
'use strict';
var translator = shim.Translator.create();
var dialog = bootbox.dialog;
var attrsToTranslate = ['placeholder', 'title', 'value'];
bootbox.dialog = function (options) {
var show, $elem, nodes, text, attrNodes, attrText;
show = options.show !== false;
var show = options.show !== false;
options.show = false;
$elem = dialog.call(bootbox, options);
var $elem = dialog.call(bootbox, options);
var element = $elem[0];
if (/\[\[.+\]\]/.test($elem[0].outerHTML)) {
nodes = descendantTextNodes($elem[0]);
text = nodes.map(function (node) {
return node.nodeValue;
}).join(' || ');
attrNodes = attrsToTranslate.reduce(function (prev, attr) {
return prev.concat(nodes.map.call($elem.find('[' + attr + '*="[["]'), function (el) {
return [attr, el];
}));
}, []);
attrText = attrNodes.map(function (node) {
return node[1].getAttribute(node[0]);
}).join(' || ');
Promise.all([
translator.translate(text),
translator.translate(attrText),
]).then(function (ref) {
var translated = ref[0];
var translatedAttrs = ref[1];
if (translated) {
translated.split(' || ').forEach(function (html, i) {
$(nodes[i]).replaceWith(html);
});
}
if (translatedAttrs) {
translatedAttrs.split(' || ').forEach(function (text, i) {
attrNodes[i][1].setAttribute(attrNodes[i][0], text);
});
}
if (/\[\[.+\]\]/.test(element.outerHTML)) {
translator.translateInPlace(element, attrsToTranslate).then(function () {
if (show) {
$elem.modal('show');
}
@ -84,7 +37,7 @@ require(['translator'], function (shim) {
CANCEL: translations[1],
CONFIRM: translations[2],
});
bootbox.setLocale(lang);
});
});

File diff suppressed because it is too large Load Diff

@ -30,18 +30,20 @@ exports.handle404 = function (req, res) {
}
meta.errors.log404(req.path.replace(/^\/api/, '') || '');
res.status(404);
var path = String(req.path || '');
if (res.locals.isAPI) {
return res.json({ path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]' });
}
var middleware = require('../middleware');
middleware.buildHeader(req, res, function () {
res.render('404', { path: validator.escape(path), title: '[[global:404.title]]' });
});
exports.send404(req, res);
} else {
res.status(404).type('txt').send('Not found');
}
};
exports.send404 = function (req, res) {
res.status(404);
var path = String(req.path || '');
if (res.locals.isAPI) {
return res.json({ path: validator.escape(path.replace(/^\/api/, '')), title: '[[global:404.title]]' });
}
var middleware = require('../middleware');
middleware.buildHeader(req, res, function () {
res.render('404', { path: validator.escape(path), title: '[[global:404.title]]' });
});
};

@ -26,13 +26,7 @@ authenticationController.register = function (req, res) {
return res.sendStatus(403);
}
var userData = {};
for (var key in req.body) {
if (req.body.hasOwnProperty(key)) {
userData[key] = req.body[key];
}
}
var userData = req.body;
async.waterfall([
function (next) {
@ -88,7 +82,7 @@ authenticationController.register = function (req, res) {
return res.status(400).send(err.message);
}
if (req.body.userLang) {
if (data.uid && req.body.userLang) {
user.setSetting(data.uid, 'userLang', req.body.userLang);
}
@ -103,21 +97,18 @@ function registerAndLoginUser(req, res, userData, callback) {
plugins.fireHook('filter:register.interstitial', {
userData: userData,
interstitials: [],
}, function (err, data) {
if (err) {
return next(err);
}
// If interstitials are found, save registration attempt into session and abort
var deferRegistration = data.interstitials.length;
}, next);
},
function (data, next) {
// If interstitials are found, save registration attempt into session and abort
var deferRegistration = data.interstitials.length;
if (!deferRegistration) {
return next();
}
userData.register = true;
req.session.registration = userData;
return res.json({ referrer: nconf.get('relative_path') + '/register/complete' });
});
if (!deferRegistration) {
return next();
}
userData.register = true;
req.session.registration = userData;
return res.json({ referrer: nconf.get('relative_path') + '/register/complete' });
},
function (next) {
user.create(userData, next);
@ -282,14 +273,14 @@ authenticationController.doLogin = function (req, uid, callback) {
if (!uid) {
return callback();
}
req.login({ uid: uid }, function (err) {
if (err) {
return callback(err);
}
authenticationController.onSuccessfulLogin(req, uid, callback);
});
async.waterfall([
function (next) {
req.login({ uid: uid }, next);
},
function (next) {
authenticationController.onSuccessfulLogin(req, uid, next);
},
], callback);
};
authenticationController.onSuccessfulLogin = function (req, uid, callback) {
@ -312,28 +303,30 @@ authenticationController.onSuccessfulLogin = function (req, uid, callback) {
version: req.useragent.version,
});
// Associate login session with user
async.parallel([
function (next) {
user.auth.addSession(uid, req.sessionID, next);
},
async.waterfall([
function (next) {
db.setObjectField('uid:' + uid + ':sessionUUID:sessionId', uuid, req.sessionID, next);
async.parallel([
function (next) {
user.auth.addSession(uid, req.sessionID, next);
},
function (next) {
db.setObjectField('uid:' + uid + ':sessionUUID:sessionId', uuid, req.sessionID, next);
},
function (next) {
user.updateLastOnlineTime(uid, next);
},
], function (err) {
next(err);
});
},
function (next) {
user.updateLastOnlineTime(uid, next);
},
], function (err) {
if (err) {
return callback(err);
}
// Force session check for all connected socket.io clients with the same session id
sockets.in('sess_' + req.sessionID).emit('checkSession', uid);
// Force session check for all connected socket.io clients with the same session id
sockets.in('sess_' + req.sessionID).emit('checkSession', uid);
plugins.fireHook('action:user.loggedIn', { uid: uid, req: req });
callback();
});
plugins.fireHook('action:user.loggedIn', { uid: uid, req: req });
next();
},
], callback);
};
authenticationController.localLogin = function (req, username, password, next) {

@ -49,7 +49,7 @@ topicsController.get = function (req, res, callback) {
userPrivileges = results.privileges;
if (!userPrivileges.read || !userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) {
if (!userPrivileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !userPrivileges.view_deleted)) {
return helpers.notAllowed(req, res);
}

@ -161,8 +161,11 @@ mongoModule.createIndices = function (callback) {
mongoModule.checkCompatibility = function (callback) {
var mongoPkg = require('mongodb/package.json');
mongoModule.checkCompatibilityVersion(mongoPkg.version, callback);
};
if (semver.lt(mongoPkg.version, '2.0.0')) {
mongoModule.checkCompatibilityVersion = function (version, callback) {
if (semver.lt(version, '2.0.0')) {
return callback(new Error('The `mongodb` package is out-of-date, please run `./nodebb setup` again.'));
}
@ -173,66 +176,75 @@ mongoModule.info = function (db, callback) {
if (!db) {
return callback();
}
async.parallel({
serverStatus: function (next) {
db.command({ serverStatus: 1 }, next);
},
stats: function (next) {
db.command({ dbStats: 1 }, next);
async.waterfall([
function (next) {
async.parallel({
serverStatus: function (next) {
db.command({ serverStatus: 1 }, next);
},
stats: function (next) {
db.command({ dbStats: 1 }, next);
},
listCollections: function (next) {
getCollectionStats(db, next);
},
}, next);
},
listCollections: function (next) {
db.listCollections().toArray(function (err, items) {
if (err) {
return next(err);
}
async.map(items, function (collection, next) {
db.collection(collection.name).stats(next);
}, next);
function (results, next) {
var stats = results.stats;
var scale = 1024 * 1024 * 1024;
results.listCollections = results.listCollections.map(function (collectionInfo) {
return {
name: collectionInfo.ns,
count: collectionInfo.count,
size: collectionInfo.size,
avgObjSize: collectionInfo.avgObjSize,
storageSize: collectionInfo.storageSize,
totalIndexSize: collectionInfo.totalIndexSize,
indexSizes: collectionInfo.indexSizes,
};
});
},
}, function (err, results) {
if (err) {
return callback(err);
}
var stats = results.stats;
var scale = 1024 * 1024 * 1024;
results.listCollections = results.listCollections.map(function (collectionInfo) {
return {
name: collectionInfo.ns,
count: collectionInfo.count,
size: collectionInfo.size,
avgObjSize: collectionInfo.avgObjSize,
storageSize: collectionInfo.storageSize,
totalIndexSize: collectionInfo.totalIndexSize,
indexSizes: collectionInfo.indexSizes,
};
});
stats.mem = results.serverStatus.mem;
stats.mem = results.serverStatus.mem;
stats.mem.resident = (stats.mem.resident / 1024).toFixed(2);
stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2);
stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2);
stats.collectionData = results.listCollections;
stats.network = results.serverStatus.network;
stats.raw = JSON.stringify(stats, null, 4);
stats.avgObjSize = stats.avgObjSize.toFixed(2);
stats.dataSize = (stats.dataSize / scale).toFixed(2);
stats.storageSize = (stats.storageSize / scale).toFixed(2);
stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0;
stats.indexSize = (stats.indexSize / scale).toFixed(2);
stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1';
stats.host = results.serverStatus.host;
stats.version = results.serverStatus.version;
stats.uptime = results.serverStatus.uptime;
stats.mongo = true;
callback(null, stats);
});
stats.mem = results.serverStatus.mem;
stats.mem = results.serverStatus.mem;
stats.mem.resident = (stats.mem.resident / 1024).toFixed(2);
stats.mem.virtual = (stats.mem.virtual / 1024).toFixed(2);
stats.mem.mapped = (stats.mem.mapped / 1024).toFixed(2);
stats.collectionData = results.listCollections;
stats.network = results.serverStatus.network;
stats.raw = JSON.stringify(stats, null, 4);
stats.avgObjSize = stats.avgObjSize.toFixed(2);
stats.dataSize = (stats.dataSize / scale).toFixed(2);
stats.storageSize = (stats.storageSize / scale).toFixed(2);
stats.fileSize = stats.fileSize ? (stats.fileSize / scale).toFixed(2) : 0;
stats.indexSize = (stats.indexSize / scale).toFixed(2);
stats.storageEngine = results.serverStatus.storageEngine ? results.serverStatus.storageEngine.name : 'mmapv1';
stats.host = results.serverStatus.host;
stats.version = results.serverStatus.version;
stats.uptime = results.serverStatus.uptime;
stats.mongo = true;
next(null, stats);
},
], callback);
};
mongoModule.close = function () {
db.close();
function getCollectionStats(db, callback) {
async.waterfall([
function (next) {
db.listCollections().toArray(next);
},
function (items, next) {
async.map(items, function (collection, next) {
db.collection(collection.name).stats(next);
}, next);
},
], callback);
}
mongoModule.close = function (callback) {
callback = callback || function () {};
db.close(callback);
};

@ -1,6 +1,7 @@
'use strict';
var _ = require('underscore');
var async = require('async');
var winston = require('winston');
var nconf = require('nconf');
var semver = require('semver');
@ -71,10 +72,6 @@ redisModule.connect = function (options) {
var redis_socket_or_host = nconf.get('redis:host');
var cxn;
if (!redis) {
redis = require('redis');
}
options = options || {};
if (nconf.get('redis:password')) {
@ -101,10 +98,10 @@ redisModule.connect = function (options) {
}
var dbIdx = parseInt(nconf.get('redis:database'), 10);
if (dbIdx) {
cxn.select(dbIdx, function (error) {
if (error) {
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + error.message);
if (dbIdx >= 0) {
cxn.select(dbIdx, function (err) {
if (err) {
winston.error('NodeBB could not connect to your Redis database. Redis returned the following error: ' + err.message);
process.exit();
}
});
@ -118,46 +115,52 @@ redisModule.createIndices = function (callback) {
};
redisModule.checkCompatibility = function (callback) {
redisModule.info(redisModule.client, function (err, info) {
if (err) {
return callback(err);
}
if (semver.lt(info.redis_version, '2.8.9')) {
return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
}
async.waterfall([
function (next) {
redisModule.info(redisModule.client, next);
},
function (info, next) {
redisModule.checkCompatibilityVersion(info.redis_version, next);
},
], callback);
};
callback();
});
redisModule.checkCompatibilityVersion = function (version, callback) {
if (semver.lt(version, '2.8.9')) {
return callback(new Error('Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.'));
}
callback();
};
redisModule.close = function () {
redisClient.quit();
redisModule.close = function (callback) {
callback = callback || function () {};
redisClient.quit(callback);
};
redisModule.info = function (cxn, callback) {
if (!cxn) {
return callback();
}
cxn.info(function (err, data) {
if (err) {
return callback(err);
}
var lines = data.toString().split('\r\n').sort();
var redisData = {};
lines.forEach(function (line) {
var parts = line.split(':');
if (parts[1]) {
redisData[parts[0]] = parts[1];
}
});
redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2);
redisData.raw = JSON.stringify(redisData, null, 4);
redisData.redis = true;
callback(null, redisData);
});
async.waterfall([
function (next) {
cxn.info(next);
},
function (data, next) {
var lines = data.toString().split('\r\n').sort();
var redisData = {};
lines.forEach(function (line) {
var parts = line.split(':');
if (parts[1]) {
redisData[parts[0]] = parts[1];
}
});
redisData.used_memory_human = (redisData.used_memory / (1024 * 1024 * 1024)).toFixed(2);
redisData.raw = JSON.stringify(redisData, null, 4);
redisData.redis = true;
next(null, redisData);
},
], callback);
};
redisModule.helpers = redisModule.helpers || {};

@ -3,6 +3,9 @@
module.exports = function (redisClient, module) {
module.listPrepend = function (key, value, callback) {
callback = callback || function () {};
if (!key) {
return callback();
}
redisClient.lpush(key, value, function (err) {
callback(err);
});
@ -10,6 +13,9 @@ module.exports = function (redisClient, module) {
module.listAppend = function (key, value, callback) {
callback = callback || function () {};
if (!key) {
return callback();
}
redisClient.rpush(key, value, function (err) {
callback(err);
});
@ -17,11 +23,17 @@ module.exports = function (redisClient, module) {
module.listRemoveLast = function (key, callback) {
callback = callback || function () {};
if (!key) {
return callback();
}
redisClient.rpop(key, callback);
};
module.listRemoveAll = function (key, value, callback) {
callback = callback || function () {};
if (!key) {
return callback();
}
redisClient.lrem(key, 0, value, function (err) {
callback(err);
});
@ -29,6 +41,9 @@ module.exports = function (redisClient, module) {
module.listTrim = function (key, start, stop, callback) {
callback = callback || function () {};
if (!key) {
return callback();
}
redisClient.ltrim(key, start, stop, function (err) {
callback(err);
});
@ -36,6 +51,9 @@ module.exports = function (redisClient, module) {
module.getListRange = function (key, start, stop, callback) {
callback = callback || function () {};
if (!key) {
return callback();
}
redisClient.lrange(key, start, stop, callback);
};
};

@ -129,7 +129,13 @@ module.exports = function (redisClient, module) {
}
redisClient.zscore(key, value, function (err, score) {
callback(err, !err ? parseFloat(score) : null);
if (err) {
return callback(err);
}
if (score === null) {
return callback(null, score);
}
callback(null, parseFloat(score));
});
};

@ -9,26 +9,33 @@ var plugins = require('../plugins');
module.exports = function (Messaging) {
Messaging.getRoomData = function (roomId, callback) {
db.getObject('chat:room:' + roomId, function (err, data) {
if (err || !data) {
return callback(err || new Error('[[error:no-chat-room]]'));
}
modifyRoomData([data]);
callback(null, data);
});
async.waterfall([
function (next) {
db.getObject('chat:room:' + roomId, next);
},
function (data, next) {
if (!data) {
return callback(new Error('[[error:no-chat-room]]'));
}
modifyRoomData([data]);
next(null, data);
},
], callback);
};
Messaging.getRoomsData = function (roomIds, callback) {
var keys = roomIds.map(function (roomId) {
return 'chat:room:' + roomId;
});
db.getObjects(keys, function (err, roomData) {
if (err) {
return callback(err);
}
modifyRoomData(roomData);
callback(null, roomData);
});
async.waterfall([
function (next) {
db.getObjects(keys, next);
},
function (roomData, next) {
modifyRoomData(roomData);
next(null, roomData);
},
], callback);
};
function modifyRoomData(rooms) {
@ -96,13 +103,14 @@ module.exports = function (Messaging) {
};
Messaging.isRoomOwner = function (uid, roomId, callback) {
db.getObjectField('chat:room:' + roomId, 'owner', function (err, owner) {
if (err) {
return callback(err);
}
callback(null, parseInt(uid, 10) === parseInt(owner, 10));
});
async.waterfall([
function (next) {
db.getObjectField('chat:room:' + roomId, 'owner', next);
},
function (owner, next) {
next(null, parseInt(uid, 10) === parseInt(owner, 10));
},
], callback);
};
Messaging.addUsersToRoom = function (uid, uids, roomId, callback) {

@ -8,71 +8,71 @@ var nconf = require('nconf');
var pubsub = require('./pubsub');
var utils = require('./utils');
(function (Meta) {
Meta.reloadRequired = false;
var Meta = module.exports;
require('./meta/configs')(Meta);
require('./meta/themes')(Meta);
require('./meta/js')(Meta);
require('./meta/css')(Meta);
require('./meta/sounds')(Meta);
require('./meta/settings')(Meta);
require('./meta/logs')(Meta);
require('./meta/errors')(Meta);
require('./meta/tags')(Meta);
require('./meta/dependencies')(Meta);
Meta.templates = require('./meta/templates');
Meta.blacklist = require('./meta/blacklist');
Meta.languages = require('./meta/languages');
Meta.reloadRequired = false;
/* Assorted */
Meta.userOrGroupExists = function (slug, callback) {
var user = require('./user');
var groups = require('./groups');
slug = utils.slugify(slug);
async.parallel([
async.apply(user.existsBySlug, slug),
async.apply(groups.existsBySlug, slug),
], function (err, results) {
callback(err, results ? results.some(function (result) { return result; }) : false);
});
};
require('./meta/configs')(Meta);
require('./meta/themes')(Meta);
require('./meta/js')(Meta);
require('./meta/css')(Meta);
require('./meta/sounds')(Meta);
require('./meta/settings')(Meta);
require('./meta/logs')(Meta);
require('./meta/errors')(Meta);
require('./meta/tags')(Meta);
require('./meta/dependencies')(Meta);
Meta.templates = require('./meta/templates');
Meta.blacklist = require('./meta/blacklist');
Meta.languages = require('./meta/languages');
/**
* Reload deprecated as of v1.1.2+, remove in v2.x
*/
Meta.reload = function (callback) {
restart();
callback();
};
/* Assorted */
Meta.userOrGroupExists = function (slug, callback) {
var user = require('./user');
var groups = require('./groups');
slug = utils.slugify(slug);
async.parallel([
async.apply(user.existsBySlug, slug),
async.apply(groups.existsBySlug, slug),
], function (err, results) {
callback(err, results ? results.some(function (result) { return result; }) : false);
});
};
Meta.restart = function () {
pubsub.publish('meta:restart', { hostname: os.hostname() });
restart();
};
/**
* Reload deprecated as of v1.1.2+, remove in v2.x
*/
Meta.reload = function (callback) {
restart();
callback();
};
Meta.getSessionTTLSeconds = function () {
var ttlDays = 60 * 60 * 24 * (parseInt(Meta.config.loginDays, 10) || 0);
var ttlSeconds = (parseInt(Meta.config.loginSeconds, 10) || 0);
var ttl = ttlSeconds || ttlDays || 1209600; // Default to 14 days
return ttl;
};
Meta.restart = function () {
pubsub.publish('meta:restart', { hostname: os.hostname() });
restart();
};
if (nconf.get('isPrimary') === 'true') {
pubsub.on('meta:restart', function (data) {
if (data.hostname !== os.hostname()) {
restart();
}
});
}
Meta.getSessionTTLSeconds = function () {
var ttlDays = 60 * 60 * 24 * (parseInt(Meta.config.loginDays, 10) || 0);
var ttlSeconds = (parseInt(Meta.config.loginSeconds, 10) || 0);
var ttl = ttlSeconds || ttlDays || 1209600; // Default to 14 days
return ttl;
};
function restart() {
if (process.send) {
process.send({
action: 'restart',
});
} else {
winston.error('[meta.restart] Could not restart, are you sure NodeBB was started with `./nodebb start`?');
if (nconf.get('isPrimary') === 'true') {
pubsub.on('meta:restart', function (data) {
if (data.hostname !== os.hostname()) {
restart();
}
});
}
function restart() {
if (process.send) {
process.send({
action: 'restart',
});
} else {
winston.error('[meta.restart] Could not restart, are you sure NodeBB was started with `./nodebb start`?');
}
}(exports));
}

@ -2,7 +2,6 @@
var async = require('async');
var winston = require('winston');
var os = require('os');
var nconf = require('nconf');
var padstart = require('lodash.padstart');
@ -183,11 +182,14 @@ function build(targets, callback) {
var startTime;
var totalTime;
async.series([
async.apply(beforeBuild, targets),
function (next) {
beforeBuild(targets, next);
},
function (next) {
var parallel = os.cpus().length > 1 && !nconf.get('series');
var threads = parseInt(nconf.get('threads'), 10);
if (threads) {
require('./minifier').maxThreads = threads - 1;
}
var parallel = !nconf.get('series');
if (parallel) {
winston.info('[build] Building in parallel mode');
} else {

@ -89,43 +89,40 @@ module.exports = function (Meta) {
};
function minifyModules(modules, fork, callback) {
// for it to never fork
// otherwise it spawns way too many processes
// maybe eventually we can pool modules
// and pass the pools to the minifer
// to reduce the total number of threads
fork = false;
var moduleDirs = modules.reduce(function (prev, mod) {
var dir = path.resolve(path.dirname(mod.destPath));
if (prev.indexOf(dir) === -1) {
prev.push(dir);
}
return prev;
}, []);
async.eachLimit(modules, 500, function (mod, next) {
var srcPath = mod.srcPath;
var destPath = mod.destPath;
async.eachLimit(moduleDirs, 1000, mkdirp, function (err) {
if (err) {
return callback(err);
}
async.parallel({
dirped: function (cb) {
mkdirp(path.dirname(destPath), cb);
},
minified: function (cb) {
fs.readFile(srcPath, function (err, buffer) {
if (err) {
return cb(err);
}
var filtered = modules.reduce(function (prev, mod) {
if (mod.srcPath.endsWith('.min.js') || path.dirname(mod.srcPath).endsWith('min')) {
prev.skip.push(mod);
} else {
prev.minify.push(mod);
}
if (srcPath.endsWith('.min.js') || path.dirname(srcPath).endsWith('min')) {
return cb(null, { code: buffer.toString() });
}
return prev;
}, { minify: [], skip: [] });
minifier.js.minify(buffer.toString(), fork, cb);
});
async.parallel([
function (cb) {
minifier.js.minifyBatch(filtered.minify, fork, cb);
},
}, function (err, results) {
if (err) {
return next(err);
}
var minified = results.minified;
fs.writeFile(destPath, minified.code, next);
});
}, callback);
function (cb) {
async.eachLimit(filtered.skip, 500, function (mod, next) {
file.link(mod.srcPath, mod.destPath, next);
}, cb);
},
], callback);
});
}
function linkModules(callback) {

@ -5,6 +5,7 @@ var async = require('async');
var fs = require('fs');
var childProcess = require('child_process');
var os = require('os');
var winston = require('winston');
var less = require('less');
var postcss = require('postcss');
var autoprefixer = require('autoprefixer');
@ -37,23 +38,38 @@ function setupDebugging() {
return forkProcessParams;
}
var children = [];
var pool = [];
var free = [];
var maxThreads = 0;
Object.defineProperty(Minifier, 'maxThreads', {
get: function () {
return maxThreads;
},
set: function (val) {
maxThreads = val;
winston.verbose('[minifier] utilizing a maximum of ' + maxThreads + ' additional threads');
},
configurable: true,
enumerable: true,
});
Minifier.maxThreads = os.cpus().length - 1;
Minifier.killAll = function () {
children.forEach(function (child) {
pool.forEach(function (child) {
child.kill('SIGTERM');
});
children = [];
pool.length = 0;
};
function removeChild(proc) {
children = children.filter(function (child) {
return child !== proc;
});
}
function getChild() {
if (free.length) {
return free.shift();
}
function forkAction(action, callback) {
var forkProcessParams = setupDebugging();
var proc = childProcess.fork(__filename, [], Object.assign({}, forkProcessParams, {
cwd: __dirname,
@ -61,17 +77,32 @@ function forkAction(action, callback) {
minifier_child: true,
},
}));
pool.push(proc);
return proc;
}
function freeChild(proc) {
proc.removeAllListeners();
free.push(proc);
}
children.push(proc);
function removeChild(proc) {
var i = pool.indexOf(proc);
pool.splice(i, 1);
}
function forkAction(action, callback) {
var proc = getChild();
proc.on('message', function (message) {
freeChild(proc);
if (message.type === 'error') {
proc.kill();
return callback(new Error(message.message));
return callback(message.err);
}
if (message.type === 'end') {
proc.kill();
callback(null, message.result);
}
});
@ -85,10 +116,6 @@ function forkAction(action, callback) {
type: 'action',
action: action,
});
proc.on('close', function () {
removeChild(proc);
});
}
var actions = {};
@ -100,7 +127,7 @@ if (process.env.minifier_child) {
if (typeof actions[action.act] !== 'function') {
process.send({
type: 'error',
message: 'Unknown action',
err: Error('Unknown action'),
});
return;
}
@ -109,7 +136,7 @@ if (process.env.minifier_child) {
if (err) {
process.send({
type: 'error',
message: err.message,
err: err,
});
return;
}
@ -124,7 +151,7 @@ if (process.env.minifier_child) {
}
function executeAction(action, fork, callback) {
if (fork) {
if (fork && (pool.length - free.length) < Minifier.maxThreads) {
forkAction(action, callback);
} else {
if (typeof actions[action.act] !== 'function') {
@ -141,7 +168,7 @@ function concat(data, callback) {
return callback(err);
}
var output = files.join(os.EOL + ';');
var output = files.join('\n;');
callback(null, { code: output });
});
@ -153,32 +180,38 @@ function concat(data, callback) {
actions.concat = concat;
function minifyJS(data, callback) {
var minified;
if (data.batch) {
async.eachLimit(data.files, 1000, function (ref, next) {
var srcPath = ref.srcPath;
var destPath = ref.destPath;
fs.readFile(srcPath, function (err, buffer) {
if (err && err.code === 'ENOENT') {
return next(null, null);
}
if (err) {
return next(err);
}
if (data.fromSource) {
var sources = data.source;
var multiple = Array.isArray(sources);
if (!multiple) {
sources = [sources];
}
try {
var minified = uglifyjs.minify(buffer.toString(), {
// outSourceMap: data.filename + '.map',
compress: data.compress,
fromString: true,
output: {
// suppress uglify line length warnings
max_line_len: 400000,
},
});
try {
minified = sources.map(function (source) {
return uglifyjs.minify(source, {
// outSourceMap: data.filename + '.map',
compress: data.compress,
fromString: true,
output: {
// suppress uglify line length warnings
max_line_len: 400000,
},
});
fs.writeFile(destPath, minified.code, next);
} catch (e) {
next(e);
}
});
} catch (e) {
return callback(e);
}
}, callback);
return callback(null, multiple ? minified : minified[0]);
return;
}
if (data.files && data.files.length) {
@ -188,16 +221,16 @@ function minifyJS(data, callback) {
}
try {
minified = uglifyjs.minify(scripts, {
var minified = uglifyjs.minify(scripts, {
// outSourceMap: data.filename + '.map',
compress: data.compress,
fromString: false,
});
callback(null, minified);
} catch (e) {
return callback(e);
callback(e);
}
callback(null, minified);
});
return;
@ -216,11 +249,11 @@ Minifier.js.bundle = function (scripts, minify, fork, callback) {
}, fork, callback);
};
Minifier.js.minify = function (source, fork, callback) {
Minifier.js.minifyBatch = function (scripts, fork, callback) {
executeAction({
act: 'minifyJS',
fromSource: true,
source: source,
files: scripts,
batch: true,
}, fork, callback);
};

@ -29,7 +29,7 @@ Notifications.get = function (nid, callback) {
};
Notifications.getMultiple = function (nids, callback) {
if (!nids.length) {
if (!Array.isArray(nids) || !nids.length) {
return setImmediate(callback, null, []);
}
var keys = nids.map(function (nid) {
@ -106,50 +106,47 @@ Notifications.findRelated = function (mergeIds, set, callback) {
db.getObjectsFields(keys, ['mergeId'], next);
},
], function (err, sets) {
if (err) {
return callback(err);
}
sets = sets.map(function (set) {
return set.mergeId;
});
function (sets, next) {
sets = sets.map(function (set) {
return set.mergeId;
});
callback(null, _nids.filter(function (nid, idx) {
return mergeIds.indexOf(sets[idx]) !== -1;
}));
});
next(null, _nids.filter(function (nid, idx) {
return mergeIds.indexOf(sets[idx]) !== -1;
}));
},
], callback);
};
Notifications.create = function (data, callback) {
if (!data.nid) {
return callback(new Error('no-notification-id'));
return callback(new Error('[[error:no-notification-id]]'));
}
data.importance = data.importance || 5;
db.getObject('notifications:' + data.nid, function (err, oldNotification) {
if (err) {
return callback(err);
}
if (oldNotification) {
if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
return callback(null, null);
async.waterfall([
function (next) {
db.getObject('notifications:' + data.nid, next);
},
function (oldNotification, next) {
if (oldNotification) {
if (parseInt(oldNotification.pid, 10) === parseInt(data.pid, 10) && parseInt(oldNotification.importance, 10) > parseInt(data.importance, 10)) {
return callback(null, null);
}
}
}
var now = Date.now();
data.datetime = now;
async.parallel([
function (next) {
db.sortedSetAdd('notifications', now, data.nid, next);
},
function (next) {
db.setObject('notifications:' + data.nid, data, next);
},
], function (err) {
callback(err, data);
});
});
var now = Date.now();
data.datetime = now;
async.parallel([
function (next) {
db.sortedSetAdd('notifications', now, data.nid, next);
},
function (next) {
db.setObject('notifications:' + data.nid, data, next);
},
], function (err) {
next(err, data);
});
},
], callback);
};
Notifications.push = function (notification, uids, callback) {
@ -233,25 +230,31 @@ function pushToUids(uids, notification, callback) {
Notifications.pushGroup = function (notification, groupName, callback) {
callback = callback || function () {};
groups.getMembers(groupName, 0, -1, function (err, members) {
if (err || !Array.isArray(members) || !members.length) {
return callback(err);
}
async.waterfall([
function (next) {
groups.getMembers(groupName, 0, -1, next);
},
function (members, next) {
if (!Array.isArray(members) || !members.length) {
return callback();
}
Notifications.push(notification, members, callback);
});
Notifications.push(notification, members, next);
},
], callback);
};
Notifications.pushGroups = function (notification, groupNames, callback) {
callback = callback || function () {};
groups.getMembersOfGroups(groupNames, function (err, groupMembers) {
if (err) {
return callback(err);
}
var members = _.unique(_.flatten(groupMembers));
Notifications.push(notification, members, callback);
});
async.waterfall([
function (next) {
groups.getMembersOfGroups(groupNames, next);
},
function (groupMembers, next) {
var members = _.unique(_.flatten(groupMembers));
Notifications.push(notification, members, next);
},
], callback);
};
Notifications.rescind = function (nid, callback) {
@ -261,13 +264,7 @@ Notifications.rescind = function (nid, callback) {
async.apply(db.sortedSetRemove, 'notifications', nid),
async.apply(db.delete, 'notifications:' + nid),
], function (err) {
if (err) {
winston.error('Encountered error rescinding notification (' + nid + '): ' + err.message);
} else {
winston.verbose('[notifications/rescind] Rescinded notification "' + nid + '"');
}
callback(err, nid);
callback(err);
});
};
@ -284,18 +281,22 @@ Notifications.markUnread = function (nid, uid, callback) {
if (!parseInt(uid, 10) || !nid) {
return callback();
}
async.waterfall([
function (next) {
db.getObject('notifications:' + nid, next);
},
function (notification, next) {
if (!notification) {
return callback(new Error('[[error:no-notification]]'));
}
notification.datetime = notification.datetime || Date.now();
db.getObject('notifications:' + nid, function (err, notification) {
if (err || !notification) {
return callback(err || new Error('[[error:no-notification]]'));
}
notification.datetime = notification.datetime || Date.now();
async.parallel([
async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
], callback);
});
async.parallel([
async.apply(db.sortedSetRemove, 'uid:' + uid + ':notifications:read', nid),
async.apply(db.sortedSetAdd, 'uid:' + uid + ':notifications:unread', notification.datetime, nid),
], next);
},
], callback);
};
Notifications.markReadMultiple = function (nids, uid, callback) {
@ -377,9 +378,9 @@ Notifications.markAllRead = function (uid, callback) {
Notifications.prune = function (callback) {
callback = callback || function () {};
var week = 604800000;
var week = 604800000;
var cutoffTime = Date.now() - week;
var cutoffTime = Date.now() - week;
async.waterfall([
function (next) {
@ -390,7 +391,7 @@ Notifications.prune = function (callback) {
return callback();
}
var keys = nids.map(function (nid) {
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});

@ -12,6 +12,7 @@ var categories = require('../categories');
var meta = require('../meta');
var helpers = require('../controllers/helpers');
var privileges = require('../privileges');
var controllers404 = require('../controllers/404.js');
module.exports = function (app, middleware) {
app.get('/topic/:topic_id.rss', middleware.maintenanceMode, generateForTopic);
@ -25,10 +26,9 @@ module.exports = function (app, middleware) {
app.get('/tags/:tag.rss', middleware.maintenanceMode, generateForTag);
};
function generateForTopic(req, res, callback) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return callback();
return controllers404.send404(req, res);
}
var tid = req.params.topic_id;
@ -45,65 +45,59 @@ function generateForTopic(req, res, callback) {
}, next);
},
function (results, next) {
if (!results.topic) {
return callback();
if (!results.topic || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) {
return controllers404.send404(req, res);
}
if (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted) {
return callback();
}
if (!results.privileges.read || !results.privileges['topics:read']) {
if (!results.privileges['topics:read']) {
return helpers.notAllowed(req, res);
}
userPrivileges = results.privileges;
topics.getTopicWithPosts(results.topic, 'tid:' + tid + ':posts', req.uid, 0, 25, false, next);
},
], function (err, topicData) {
if (err) {
return callback(err);
}
topics.modifyPostsByPrivilege(topicData, userPrivileges);
var description = topicData.posts.length ? topicData.posts[0].content : '';
var image_url = topicData.posts.length ? topicData.posts[0].picture : '';
var author = topicData.posts.length ? topicData.posts[0].username : '';
var feed = new rss({
title: topicData.title,
description: description,
feed_url: nconf.get('url') + '/topic/' + tid + '.rss',
site_url: nconf.get('url') + '/topic/' + topicData.slug,
image_url: image_url,
author: author,
ttl: 60,
});
var dateStamp;
if (topicData.posts.length > 0) {
feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString();
}
topicData.posts.forEach(function (postData) {
if (!postData.deleted) {
dateStamp = new Date(parseInt(parseInt(postData.edited, 10) === 0 ? postData.timestamp : postData.edited, 10)).toUTCString();
feed.item({
title: 'Reply to ' + topicData.title + ' on ' + dateStamp,
description: postData.content,
url: nconf.get('url') + '/post/' + postData.pid,
author: postData.user ? postData.user.username : '',
date: dateStamp,
});
function (topicData) {
topics.modifyPostsByPrivilege(topicData, userPrivileges);
var description = topicData.posts.length ? topicData.posts[0].content : '';
var image_url = topicData.posts.length ? topicData.posts[0].picture : '';
var author = topicData.posts.length ? topicData.posts[0].username : '';
var feed = new rss({
title: topicData.title,
description: description,
feed_url: nconf.get('url') + '/topic/' + tid + '.rss',
site_url: nconf.get('url') + '/topic/' + topicData.slug,
image_url: image_url,
author: author,
ttl: 60,
});
var dateStamp;
if (topicData.posts.length > 0) {
feed.pubDate = new Date(parseInt(topicData.posts[0].timestamp, 10)).toUTCString();
}
});
sendFeed(feed, res);
});
topicData.posts.forEach(function (postData) {
if (!postData.deleted) {
dateStamp = new Date(parseInt(parseInt(postData.edited, 10) === 0 ? postData.timestamp : postData.edited, 10)).toUTCString();
feed.item({
title: 'Reply to ' + topicData.title + ' on ' + dateStamp,
description: postData.content,
url: nconf.get('url') + '/post/' + postData.pid,
author: postData.user ? postData.user.username : '',
date: dateStamp,
});
}
});
sendFeed(feed, res);
},
], callback);
}
function generateForUserTopics(req, res, callback) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return callback();
return controllers404.send404(req, res);
}
var userslug = req.params.userslug;
@ -118,24 +112,21 @@ function generateForUserTopics(req, res, callback) {
}
user.getUserFields(uid, ['uid', 'username'], next);
},
], function (err, userData) {
if (err) {
return callback(err);
}
generateForTopics({
uid: req.uid,
title: 'Topics by ' + userData.username,
description: 'A list of topics that are posted by ' + userData.username,
feed_url: '/user/' + userslug + '/topics.rss',
site_url: '/user/' + userslug + '/topics',
}, 'uid:' + userData.uid + ':topics', req, res, callback);
});
function (userData, next) {
generateForTopics({
uid: req.uid,
title: 'Topics by ' + userData.username,
description: 'A list of topics that are posted by ' + userData.username,
feed_url: '/user/' + userslug + '/topics.rss',
site_url: '/user/' + userslug + '/topics',
}, 'uid:' + userData.uid + ':topics', req, res, next);
},
], callback);
}
function generateForCategory(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return next();
return controllers404.send404(req, res);
}
var cid = req.params.category_id;
@ -169,17 +160,15 @@ function generateForCategory(req, res, next) {
site_url: '/category/' + results.category.cid,
}, results.category.topics, next);
},
], function (err, feed) {
if (err) {
return next(err);
}
sendFeed(feed, res);
});
function (feed) {
sendFeed(feed, res);
},
], next);
}
function generateForRecent(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return next();
return controllers404.send404(req, res);
}
generateForTopics({
uid: req.uid,
@ -192,7 +181,7 @@ function generateForRecent(req, res, next) {
function generateForPopular(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return next();
return controllers404.send404(req, res);
}
var terms = {
daily: 'day',
@ -215,12 +204,10 @@ function generateForPopular(req, res, next) {
site_url: '/popular/' + (req.params.term || 'daily'),
}, topics, next);
},
], function (err, feed) {
if (err) {
return next(err);
}
sendFeed(feed, res);
});
function (feed) {
sendFeed(feed, res);
},
], next);
}
function generateForTopics(options, set, req, res, next) {
@ -233,12 +220,10 @@ function generateForTopics(options, set, req, res, next) {
function (data, next) {
generateTopicsFeed(options, data.topics, next);
},
], function (err, feed) {
if (err) {
return next(err);
}
sendFeed(feed, res);
});
function (feed) {
sendFeed(feed, res);
},
], next);
}
function generateTopicsFeed(feedOptions, feedTopics, callback) {
@ -254,7 +239,7 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
feed.pubDate = new Date(parseInt(feedTopics[0].lastposttime, 10)).toUTCString();
}
async.map(feedTopics, function (topicData, next) {
async.each(feedTopics, function (topicData, next) {
var feedItem = {
title: topicData.title,
url: nconf.get('url') + '/topic/' + topicData.slug,
@ -272,83 +257,80 @@ function generateTopicsFeed(feedOptions, feedTopics, callback) {
return next(err);
}
if (!mainPost) {
return next(null, feedItem);
feed.item(feedItem);
return next();
}
feedItem.description = mainPost.content;
feedItem.author = mainPost.user.username;
next(null, feedItem);
feed.item(feedItem);
next();
});
}, function (err, feedItems) {
if (err) {
return callback(err);
}
feedItems.forEach(function (feedItem) {
if (feedItem) {
feed.item(feedItem);
}
});
callback(null, feed);
}, function (err) {
callback(err, feed);
});
}
function generateForRecentPosts(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return next();
return controllers404.send404(req, res);
}
posts.getRecentPosts(req.uid, 0, 19, 'month', function (err, posts) {
if (err) {
return next(err);
}
var feed = generateForPostsFeed({
title: 'Recent Posts',
description: 'A list of recent posts',
feed_url: '/recentposts.rss',
site_url: '/recentposts',
}, posts);
sendFeed(feed, res);
});
async.waterfall([
function (next) {
posts.getRecentPosts(req.uid, 0, 19, 'month', next);
},
function (posts) {
var feed = generateForPostsFeed({
title: 'Recent Posts',
description: 'A list of recent posts',
feed_url: '/recentposts.rss',
site_url: '/recentposts',
}, posts);
sendFeed(feed, res);
},
], next);
}
function generateForCategoryRecentPosts(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return next();
return controllers404.send404(req, res);
}
var cid = req.params.category_id;
async.parallel({
privileges: function (next) {
privileges.categories.get(cid, req.uid, next);
},
category: function (next) {
categories.getCategoryData(cid, next);
},
posts: function (next) {
categories.getRecentReplies(cid, req.uid, 20, next);
async.waterfall([
function (next) {
async.parallel({
privileges: function (next) {
privileges.categories.get(cid, req.uid, next);
},
category: function (next) {
categories.getCategoryData(cid, next);
},
posts: function (next) {
categories.getRecentReplies(cid, req.uid, 20, next);
},
}, next);
},
}, function (err, results) {
if (err) {
return next(err);
}
if (!results.category) {
return next();
}
function (results, next) {
if (!results.category) {
return next();
}
if (!results.privileges.read) {
return helpers.notAllowed(req, res);
}
if (!results.privileges.read) {
return helpers.notAllowed(req, res);
}
var feed = generateForPostsFeed({
title: results.category.name + ' Recent Posts',
description: 'A list of recent posts from ' + results.category.name,
feed_url: '/category/' + cid + '/recentposts.rss',
site_url: '/category/' + cid + '/recentposts',
}, results.posts);
var feed = generateForPostsFeed({
title: results.category.name + ' Recent Posts',
description: 'A list of recent posts from ' + results.category.name,
feed_url: '/category/' + cid + '/recentposts.rss',
site_url: '/category/' + cid + '/recentposts',
}, results.posts);
sendFeed(feed, res);
});
sendFeed(feed, res);
},
], next);
}
function generateForPostsFeed(feedOptions, posts) {
@ -377,7 +359,7 @@ function generateForPostsFeed(feedOptions, posts) {
function generateForTag(req, res, next) {
if (parseInt(meta.config['feeds:disableRSS'], 10) === 1) {
return next();
return controllers404.send404(req, res);
}
var tag = validator.escape(String(req.params.tag));
var page = parseInt(req.query.page, 10) || 1;

@ -169,21 +169,26 @@ SocketHelpers.sendNotificationToTopicOwner = function (tid, fromuid, command, no
};
SocketHelpers.rescindUpvoteNotification = function (pid, fromuid) {
var nid = 'upvote:post:' + pid + ':uid:' + fromuid;
notifications.rescind(nid);
posts.getPostField(pid, 'uid', function (err, uid) {
var uid;
async.waterfall([
function (next) {
notifications.rescind('upvote:post:' + pid + ':uid:' + fromuid, next);
},
function (next) {
posts.getPostField(pid, 'uid', next);
},
function (_uid, next) {
uid = _uid;
user.notifications.getUnreadCount(uid, next);
},
function (count, next) {
websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
next();
},
], function (err) {
if (err) {
return winston.error(err);
winston.error(err);
}
user.notifications.getUnreadCount(uid, function (err, count) {
if (err) {
return winston.error(err);
}
websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
});
});
};

@ -12,16 +12,16 @@ var utils = require('../utils');
var server = require('./');
var user = require('../user');
var SocketModules = {
chats: {},
sounds: {},
settings: {},
};
var SocketModules = module.exports;
SocketModules.chats = {};
SocketModules.sounds = {};
SocketModules.settings = {};
/* Chat */
SocketModules.chats.getRaw = function (socket, data, callback) {
if (!data || !data.hasOwnProperty('mid')) {
if (!data || !data.hasOwnProperty('mid') || !data.hasOwnProperty('roomId')) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
@ -57,13 +57,14 @@ SocketModules.chats.newRoom = function (socket, data, callback) {
return callback(new Error('[[error:too-many-messages]]'));
}
Messaging.canMessageUser(socket.uid, data.touid, function (err) {
if (err) {
return callback(err);
}
Messaging.newRoom(socket.uid, [data.touid], callback);
});
async.waterfall([
function (next) {
Messaging.canMessageUser(socket.uid, data.touid, next);
},
function (next) {
Messaging.newRoom(socket.uid, [data.touid], next);
},
], callback);
};
SocketModules.chats.send = function (socket, data, callback) {
@ -223,17 +224,21 @@ SocketModules.chats.leave = function (socket, roomid, callback) {
SocketModules.chats.edit = function (socket, data, callback) {
if (!data || !data.roomId) {
if (!data || !data.roomId || !data.message) {
return callback(new Error('[[error:invalid-data]]'));
}
Messaging.canEdit(data.mid, socket.uid, function (err, allowed) {
if (err || !allowed) {
return callback(err || new Error('[[error:cant-edit-chat-message]]'));
}
Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, callback);
});
async.waterfall([
function (next) {
Messaging.canEdit(data.mid, socket.uid, next);
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[error:cant-edit-chat-message]]'));
}
Messaging.editMessage(socket.uid, data.mid, data.roomId, data.message, next);
},
], callback);
};
SocketModules.chats.delete = function (socket, data, callback) {
@ -241,13 +246,18 @@ SocketModules.chats.delete = function (socket, data, callback) {
return callback(new Error('[[error:invalid-data]]'));
}
Messaging.canEdit(data.messageId, socket.uid, function (err, allowed) {
if (err || !allowed) {
return callback(err || new Error('[[error:cant-delete-chat-message]]'));
}
async.waterfall([
function (next) {
Messaging.canEdit(data.messageId, socket.uid, next);
},
function (allowed, next) {
if (!allowed) {
return next(new Error('[[error:cant-delete-chat-message]]'));
}
Messaging.deleteMessage(data.messageId, data.roomId, callback);
});
Messaging.deleteMessage(data.messageId, data.roomId, next);
},
], callback);
};
SocketModules.chats.canMessage = function (socket, roomId, callback) {
@ -255,37 +265,38 @@ SocketModules.chats.canMessage = function (socket, roomId, callback) {
};
SocketModules.chats.markRead = function (socket, roomId, callback) {
if (!socket.uid) {
if (!socket.uid || !roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
async.parallel({
uidsInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1),
markRead: async.apply(Messaging.markRead, socket.uid, roomId),
}, function (err, results) {
if (err) {
return callback(err);
}
Messaging.pushUnreadCount(socket.uid);
server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId });
if (results.uidsInRoom.indexOf(socket.uid.toString()) === -1) {
return callback();
}
// Mark notification read
var nids = results.uidsInRoom.filter(function (uid) {
return parseInt(uid, 10) !== socket.uid;
}).map(function (uid) {
return 'chat_' + uid + '_' + roomId;
});
notifications.markReadMultiple(nids, socket.uid, function () {
user.notifications.pushCount(socket.uid);
});
callback();
});
async.waterfall([
function (next) {
async.parallel({
uidsInRoom: async.apply(Messaging.getUidsInRoom, roomId, 0, -1),
markRead: async.apply(Messaging.markRead, socket.uid, roomId),
}, next);
},
function (results, next) {
Messaging.pushUnreadCount(socket.uid);
server.in('uid_' + socket.uid).emit('event:chats.markedAsRead', { roomId: roomId });
if (results.uidsInRoom.indexOf(socket.uid.toString()) === -1) {
return callback();
}
// Mark notification read
var nids = results.uidsInRoom.filter(function (uid) {
return parseInt(uid, 10) !== socket.uid;
}).map(function (uid) {
return 'chat_' + uid + '_' + roomId;
});
notifications.markReadMultiple(nids, socket.uid, function () {
user.notifications.pushCount(socket.uid);
});
next();
},
], callback);
};
SocketModules.chats.markAllRead = function (socket, data, callback) {
@ -301,8 +312,8 @@ SocketModules.chats.markAllRead = function (socket, data, callback) {
};
SocketModules.chats.renameRoom = function (socket, data, callback) {
if (!data) {
return callback(new Error('[[error:invalid-name]]'));
if (!data || !data.roomId || !data.newName) {
return callback(new Error('[[error:invalid-data]]'));
}
async.waterfall([
@ -333,13 +344,13 @@ SocketModules.chats.getRecentChats = function (socket, data, callback) {
SocketModules.chats.hasPrivateChat = function (socket, uid, callback) {
if (!socket.uid || !uid) {
return callback(null, new Error('[[error:invalid-data]]'));
return callback(new Error('[[error:invalid-data]]'));
}
Messaging.hasPrivateChat(socket.uid, uid, callback);
};
SocketModules.chats.getMessages = function (socket, data, callback) {
if (!socket.uid || !data.uid || !data.roomId) {
if (!socket.uid || !data || !data.uid || !data.roomId) {
return callback(new Error('[[error:invalid-data]]'));
}
@ -358,5 +369,3 @@ SocketModules.chats.getMessages = function (socket, data, callback) {
SocketModules.sounds.getUserSoundMap = function getUserSoundMap(socket, data, callback) {
meta.sounds.getUserSoundMap(socket.uid, callback);
};
module.exports = SocketModules;

@ -27,7 +27,7 @@ module.exports = function (SocketTopics) {
}, next);
},
function (results, next) {
if (!results.privileges.read || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) {
if (!results.privileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) {
return callback(new Error('[[error:no-privileges]]'));
}

@ -392,30 +392,36 @@ module.exports = function (Topics) {
function getPostReplies(pids, callerUid, callback) {
async.map(pids, function (pid, next) {
db.getSortedSetRange('pid:' + pid + ':replies', 0, -1, function (err, replyPids) {
if (err) {
return next(err);
}
var uids = [];
var count = 0;
async.until(function () {
return count === replyPids.length || uids.length === 6;
}, function (next) {
posts.getPostField(replyPids[count], 'uid', function (err, uid) {
uid = parseInt(uid, 10);
if (uids.indexOf(uid) === -1) {
uids.push(uid);
}
count += 1;
next(err);
});
}, function (err) {
if (err) {
return next(err);
}
var replyPids;
var uids = [];
async.waterfall([
function (next) {
db.getSortedSetRange('pid:' + pid + ':replies', 0, -1, next);
},
function (_replyPids, next) {
replyPids = _replyPids;
var count = 0;
async.until(function () {
return count === replyPids.length || uids.length === 6;
}, function (next) {
async.waterfall([
function (next) {
posts.getPostField(replyPids[count], 'uid', next);
},
function (uid, next) {
uid = parseInt(uid, 10);
if (uids.indexOf(uid) === -1) {
uids.push(uid);
}
count += 1;
next();
},
], next);
}, next);
},
function (next) {
async.parallel({
users: function (next) {
user.getUsersWithFields(uids, ['uid', 'username', 'userslug', 'picture'], callerUid, next);
@ -425,17 +431,19 @@ module.exports = function (Topics) {
next(err, utils.toISOString(timestamp));
});
},
}, function (err, replies) {
if (replies.users.length > 5) {
replies.users.shift();
replies.hasMore = true;
}
}, next);
},
function (replies, next) {
if (replies.users.length > 5) {
replies.users.shift();
replies.hasMore = true;
}
replies.count = replyPids.length;
next(err, replies);
});
});
});
replies.count = replyPids.length;
replies.text = replies.count > 1 ? '[[topic:replies_to_this_post, ' + replies.count + ']]' : '[[topic:one_reply_to_this_post]]';
next(null, replies);
},
], next);
}, callback);
}
};

@ -267,6 +267,7 @@ module.exports = function (Topics) {
categories.markAsRead(cids, uid, next);
},
function (next) {
plugins.fireHook('action:topics.markAsRead', { uid: uid, tids: tids });
next(null, true);
},
], callback);

@ -13,9 +13,8 @@ var file = require('../src/file');
*
* 1. Copy TEMPLATE to a file name of your choice. Try to be succinct.
* 2. Open up that file and change the user-friendly name (can be longer/more descriptive than the file name)
* and timestamp
* and timestamp (don't forget the timestamp!)
* 3. Add your script under the "method" property
* 4. Append your filename to the array below for the next NodeBB version.
*/
var Upgrade = {};

@ -61,7 +61,7 @@ module.exports = function (User) {
async.waterfall([
async.apply(User.getUserFields, uid, ['banned', 'banned:expire']),
function (userData, next) {
var banned = parseInt(userData.banned, 10) === 1;
var banned = userData && parseInt(userData.banned, 10) === 1;
if (!banned) {
return next(null, banned);
}

@ -57,7 +57,9 @@ module.exports = function (User) {
], next);
}
},
], callback);
], function (err) {
callback(err);
});
}
User.getFollowing = function (uid, start, stop, callback) {

@ -10,16 +10,17 @@ var meta = require('../meta');
var notifications = require('../notifications');
var privileges = require('../privileges');
(function (UserNotifications) {
UserNotifications.get = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback(null, { read: [], unread: [] });
}
getNotifications(uid, 0, 9, function (err, notifications) {
if (err) {
return callback(err);
}
var UserNotifications = module.exports;
UserNotifications.get = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback(null, { read: [], unread: [] });
}
async.waterfall([
function (next) {
getNotifications(uid, 0, 9, next);
},
function (notifications, next) {
notifications.read = notifications.read.filter(Boolean);
notifications.unread = notifications.unread.filter(Boolean);
@ -28,326 +29,336 @@ var privileges = require('../privileges');
notifications.read.length = maxNotifs - notifications.unread.length;
}
callback(null, notifications);
});
};
function filterNotifications(nids, filter, callback) {
if (!filter) {
return setImmediate(callback, null, nids);
}
async.waterfall([
function (next) {
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(keys, ['nid', 'type'], next);
},
function (notifications, next) {
nids = notifications.filter(function (notification) {
return notification && notification.nid && notification.type === filter;
}).map(function (notification) {
return notification.nid;
});
next(null, nids);
},
], callback);
}
UserNotifications.getAll = function (uid, filter, callback) {
var nids;
async.waterfall([
function (next) {
async.parallel({
unread: function (next) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, -1, next);
},
read: function (next) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:read', 0, -1, next);
},
}, next);
},
function (results, next) {
nids = results.unread.concat(results.read);
db.isSortedSetMembers('notifications', nids, next);
},
function (exists, next) {
var deleteNids = [];
nids = nids.filter(function (nid, index) {
if (!nid || !exists[index]) {
deleteNids.push(nid);
}
return nid && exists[index];
});
deleteUserNids(deleteNids, uid, next);
},
function (next) {
filterNotifications(nids, filter, next);
},
], callback);
};
function deleteUserNids(nids, uid, callback) {
callback = callback || function () {};
if (!nids.length) {
return setImmediate(callback);
}
async.parallel([
function (next) {
db.sortedSetRemove('uid:' + uid + ':notifications:read', nids, next);
},
function (next) {
db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
},
], function (err) {
callback(err);
});
}
next(null, notifications);
},
], callback);
};
function getNotifications(uid, start, stop, callback) {
async.parallel({
unread: function (next) {
getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next);
},
read: function (next) {
getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next);
},
}, callback);
function filterNotifications(nids, filter, callback) {
if (!filter) {
return setImmediate(callback, null, nids);
}
function getNotificationsFromSet(set, read, uid, start, stop, callback) {
async.waterfall([
function (next) {
db.getSortedSetRevRange(set, start, stop, next);
},
function (nids, next) {
if (!Array.isArray(nids) || !nids.length) {
return callback(null, []);
async.waterfall([
function (next) {
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(keys, ['nid', 'type'], next);
},
function (notifications, next) {
nids = notifications.filter(function (notification) {
return notification && notification.nid && notification.type === filter;
}).map(function (notification) {
return notification.nid;
});
next(null, nids);
},
], callback);
}
UserNotifications.getAll = function (uid, filter, callback) {
var nids;
async.waterfall([
function (next) {
async.parallel({
unread: function (next) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, -1, next);
},
read: function (next) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:read', 0, -1, next);
},
}, next);
},
function (results, next) {
nids = results.unread.concat(results.read);
db.isSortedSetMembers('notifications', nids, next);
},
function (exists, next) {
var deleteNids = [];
nids = nids.filter(function (nid, index) {
if (!nid || !exists[index]) {
deleteNids.push(nid);
}
return nid && exists[index];
});
UserNotifications.getNotifications(nids, uid, next);
},
], callback);
deleteUserNids(deleteNids, uid, next);
},
function (next) {
filterNotifications(nids, filter, next);
},
], callback);
};
function deleteUserNids(nids, uid, callback) {
callback = callback || function () {};
if (!nids.length) {
return setImmediate(callback);
}
UserNotifications.getNotifications = function (nids, uid, callback) {
var notificationData = [];
async.waterfall([
function (next) {
async.parallel({
notifications: function (next) {
notifications.getMultiple(nids, next);
},
hasRead: function (next) {
db.isSortedSetMembers('uid:' + uid + ':notifications:read', nids, next);
},
}, next);
},
function (results, next) {
var deletedNids = [];
notificationData = results.notifications.filter(function (notification, index) {
if (!notification || !notification.nid) {
deletedNids.push(nids[index]);
}
if (notification) {
notification.read = results.hasRead[index];
notification.readClass = !notification.read ? 'unread' : '';
}
return notification && notification.path;
});
deleteUserNids(deletedNids, uid, next);
},
function (next) {
notifications.merge(notificationData, next);
},
], callback);
};
UserNotifications.getDailyUnread = function (uid, callback) {
var yesterday = Date.now() - (1000 * 60 * 60 * 24); // Approximate, can be more or less depending on time changes, makes no difference really.
db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, function (err, nids) {
if (err) {
return callback(err);
}
async.parallel([
function (next) {
db.sortedSetRemove('uid:' + uid + ':notifications:read', nids, next);
},
function (next) {
db.sortedSetRemove('uid:' + uid + ':notifications:unread', nids, next);
},
], function (err) {
callback(err);
});
}
function getNotifications(uid, start, stop, callback) {
async.parallel({
unread: function (next) {
getNotificationsFromSet('uid:' + uid + ':notifications:unread', false, uid, start, stop, next);
},
read: function (next) {
getNotificationsFromSet('uid:' + uid + ':notifications:read', true, uid, start, stop, next);
},
}, callback);
}
function getNotificationsFromSet(set, read, uid, start, stop, callback) {
async.waterfall([
function (next) {
db.getSortedSetRevRange(set, start, stop, next);
},
function (nids, next) {
if (!Array.isArray(nids) || !nids.length) {
return callback(null, []);
}
UserNotifications.getNotifications(nids, uid, callback);
});
};
UserNotifications.getUnreadCount = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback(null, 0);
}
UserNotifications.getNotifications(nids, uid, next);
},
], callback);
}
UserNotifications.getNotifications = function (nids, uid, callback) {
var notificationData = [];
async.waterfall([
function (next) {
async.parallel({
notifications: function (next) {
notifications.getMultiple(nids, next);
},
hasRead: function (next) {
db.isSortedSetMembers('uid:' + uid + ':notifications:read', nids, next);
},
}, next);
},
function (results, next) {
var deletedNids = [];
notificationData = results.notifications.filter(function (notification, index) {
if (!notification || !notification.nid) {
deletedNids.push(nids[index]);
}
if (notification) {
notification.read = results.hasRead[index];
notification.readClass = !notification.read ? 'unread' : '';
}
// Collapse any notifications with identical mergeIds
async.waterfall([
async.apply(db.getSortedSetRevRange, 'uid:' + uid + ':notifications:unread', 0, 99),
async.apply(notifications.filterExists),
function (nids, next) {
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(keys, ['mergeId'], next);
},
function (mergeIds, next) {
mergeIds = mergeIds.map(function (set) {
return set.mergeId;
});
next(null, mergeIds.reduce(function (count, mergeId, idx, arr) {
// A missing (null) mergeId means that notification is counted separately.
if (mergeId === null || idx === arr.indexOf(mergeId)) {
count += 1;
}
return count;
}, 0));
},
], callback);
};
UserNotifications.getUnreadByField = function (uid, field, values, callback) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, function (err, nids) {
if (err) {
return callback(err);
}
return notification && notification.path;
});
deleteUserNids(deletedNids, uid, next);
},
function (next) {
notifications.merge(notificationData, next);
},
], callback);
};
UserNotifications.getDailyUnread = function (uid, callback) {
var yesterday = Date.now() - (1000 * 60 * 60 * 24); // Approximate, can be more or less depending on time changes, makes no difference really.
async.waterfall([
function (next) {
db.getSortedSetRevRangeByScore('uid:' + uid + ':notifications:unread', 0, 20, '+inf', yesterday, next);
},
function (nids, next) {
if (!Array.isArray(nids) || !nids.length) {
return callback(null, []);
}
UserNotifications.getNotifications(nids, uid, next);
},
], callback);
};
UserNotifications.getUnreadCount = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback(null, 0);
}
async.waterfall([
function (next) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, next);
},
function (nids, next) {
notifications.filterExists(nids, next);
},
function (nids, next) {
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
db.getObjectsFields(keys, ['nid', field], function (err, notifications) {
if (err) {
return callback(err);
db.getObjectsFields(keys, ['mergeId'], next);
},
function (mergeIds, next) {
// Collapse any notifications with identical mergeIds
mergeIds = mergeIds.map(function (set) {
return set.mergeId;
});
next(null, mergeIds.reduce(function (count, mergeId, idx, arr) {
// A missing (null) mergeId means that notification is counted separately.
if (mergeId === null || idx === arr.indexOf(mergeId)) {
count += 1;
}
values = values.map(function () { return values.toString(); });
nids = notifications.filter(function (notification) {
return notification && notification[field] && values.indexOf(notification[field].toString()) !== -1;
}).map(function (notification) {
return notification.nid;
});
return count;
}, 0));
},
], callback);
};
UserNotifications.getUnreadByField = function (uid, field, values, callback) {
var nids;
async.waterfall([
function (next) {
db.getSortedSetRevRange('uid:' + uid + ':notifications:unread', 0, 99, next);
},
function (_nids, next) {
nids = _nids;
if (!Array.isArray(nids) || !nids.length) {
return callback(null, []);
}
callback(null, nids);
var keys = nids.map(function (nid) {
return 'notifications:' + nid;
});
});
};
UserNotifications.deleteAll = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback();
}
async.parallel([
function (next) {
db.delete('uid:' + uid + ':notifications:unread', next);
},
function (next) {
db.delete('uid:' + uid + ':notifications:read', next);
},
], callback);
};
UserNotifications.sendTopicNotificationToFollowers = function (uid, topicData, postData) {
var followers;
async.waterfall([
function (next) {
db.getSortedSetRange('followers:' + uid, 0, -1, next);
},
function (followers, next) {
if (!Array.isArray(followers) || !followers.length) {
return;
}
privileges.categories.filterUids('read', topicData.cid, followers, next);
},
function (_followers, next) {
followers = _followers;
if (!followers.length) {
return;
}
db.getObjectsFields(keys, ['nid', field], next);
},
function (notifications, next) {
values = values.map(function () { return values.toString(); });
nids = notifications.filter(function (notification) {
return notification && notification[field] && values.indexOf(notification[field].toString()) !== -1;
}).map(function (notification) {
return notification.nid;
});
var title = topicData.title;
if (title) {
title = S(title).decodeHTMLEntities().s;
}
next(null, nids);
},
], callback);
};
notifications.create({
type: 'new-topic',
bodyShort: '[[notifications:user_posted_topic, ' + postData.user.username + ', ' + title + ']]',
bodyLong: postData.content,
pid: postData.pid,
path: '/post/' + postData.pid,
nid: 'tid:' + postData.tid + ':uid:' + uid,
tid: postData.tid,
from: uid,
}, next);
},
], function (err, notification) {
if (err) {
return winston.error(err);
UserNotifications.deleteAll = function (uid, callback) {
if (!parseInt(uid, 10)) {
return callback();
}
async.parallel([
function (next) {
db.delete('uid:' + uid + ':notifications:unread', next);
},
function (next) {
db.delete('uid:' + uid + ':notifications:read', next);
},
], callback);
};
UserNotifications.sendTopicNotificationToFollowers = function (uid, topicData, postData) {
var followers;
async.waterfall([
function (next) {
db.getSortedSetRange('followers:' + uid, 0, -1, next);
},
function (followers, next) {
if (!Array.isArray(followers) || !followers.length) {
return;
}
privileges.categories.filterUids('read', topicData.cid, followers, next);
},
function (_followers, next) {
followers = _followers;
if (!followers.length) {
return;
}
if (notification) {
notifications.push(notification, followers);
var title = topicData.title;
if (title) {
title = S(title).decodeHTMLEntities().s;
}
});
};
UserNotifications.sendWelcomeNotification = function (uid, callback) {
callback = callback || function () {};
if (!meta.config.welcomeNotification) {
return callback();
notifications.create({
type: 'new-topic',
bodyShort: '[[notifications:user_posted_topic, ' + postData.user.username + ', ' + title + ']]',
bodyLong: postData.content,
pid: postData.pid,
path: '/post/' + postData.pid,
nid: 'tid:' + postData.tid + ':uid:' + uid,
tid: postData.tid,
from: uid,
}, next);
},
], function (err, notification) {
if (err) {
return winston.error(err);
}
var path = meta.config.welcomeLink ? meta.config.welcomeLink : '#';
if (notification) {
notifications.push(notification, followers);
}
});
};
notifications.create({
bodyShort: meta.config.welcomeNotification,
path: path,
nid: 'welcome_' + uid,
}, function (err, notification) {
if (err || !notification) {
return callback(err);
}
UserNotifications.sendWelcomeNotification = function (uid, callback) {
callback = callback || function () {};
if (!meta.config.welcomeNotification) {
return callback();
}
notifications.push(notification, [uid], callback);
});
};
UserNotifications.sendNameChangeNotification = function (uid, username) {
notifications.create({
bodyShort: '[[user:username_taken_workaround, ' + username + ']]',
image: 'brand:logo',
nid: 'username_taken:' + uid,
datetime: Date.now(),
}, function (err, notification) {
if (!err && notification) {
notifications.push(notification, uid);
}
});
};
UserNotifications.pushCount = function (uid) {
var websockets = require('./../socket.io');
UserNotifications.getUnreadCount(uid, function (err, count) {
if (err) {
return winston.error(err.stack);
var path = meta.config.welcomeLink ? meta.config.welcomeLink : '#';
async.waterfall([
function (next) {
notifications.create({
bodyShort: meta.config.welcomeNotification,
path: path,
nid: 'welcome_' + uid,
}, next);
},
function (notification, next) {
if (!notification) {
return next();
}
notifications.push(notification, [uid], next);
},
], callback);
};
UserNotifications.sendNameChangeNotification = function (uid, username) {
notifications.create({
bodyShort: '[[user:username_taken_workaround, ' + username + ']]',
image: 'brand:logo',
nid: 'username_taken:' + uid,
datetime: Date.now(),
}, function (err, notification) {
if (!err && notification) {
notifications.push(notification, uid);
}
});
};
UserNotifications.pushCount = function (uid) {
var websockets = require('./../socket.io');
UserNotifications.getUnreadCount(uid, function (err, count) {
if (err) {
return winston.error(err.stack);
}
websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
});
};
}(exports));
websockets.in('uid_' + uid).emit('event:notifications.updateCount', count);
});
};

@ -68,16 +68,17 @@ module.exports = function (User) {
var resultsPerPage = parseInt(meta.config.userSearchResultsPerPage, 10) || 20;
var hardCap = resultsPerPage * 10;
db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, function (err, data) {
if (err) {
return callback(err);
}
var uids = data.map(function (data) {
return data.split(':')[1];
});
callback(null, uids);
});
async.waterfall([
function (next) {
db.getSortedSetRangeByLex(searchBy + ':sorted', min, max, 0, hardCap, next);
},
function (data, next) {
var uids = data.map(function (data) {
return data.split(':')[1];
});
next(null, uids);
},
], callback);
}
function filterAndSortUids(uids, data, callback) {
@ -94,37 +95,38 @@ module.exports = function (User) {
fields.push('flags');
}
User.getUsersFields(uids, fields, function (err, userData) {
if (err) {
return callback(err);
}
if (data.onlineOnly) {
userData = userData.filter(function (user) {
return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000);
});
}
async.waterfall([
function (next) {
User.getUsersFields(uids, fields, next);
},
function (userData, next) {
if (data.onlineOnly) {
userData = userData.filter(function (user) {
return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000);
});
}
if (data.bannedOnly) {
userData = userData.filter(function (user) {
return user && user.banned;
});
}
if (data.bannedOnly) {
userData = userData.filter(function (user) {
return user && parseInt(user.banned, 10) === 1;
});
}
if (data.flaggedOnly) {
userData = userData.filter(function (user) {
return user && parseInt(user.flags, 10) > 0;
});
}
if (data.flaggedOnly) {
userData = userData.filter(function (user) {
return user && parseInt(user.flags, 10) > 0;
});
}
sortUsers(userData, sortBy);
sortUsers(userData, sortBy);
uids = userData.map(function (user) {
return user && user.uid;
});
uids = userData.map(function (user) {
return user && user.uid;
});
callback(null, uids);
});
next(null, uids);
},
], callback);
}
function sortUsers(userData, sortBy) {

@ -8,6 +8,7 @@ var meta = require('../meta');
var db = require('../database');
var plugins = require('../plugins');
var pubsub = require('../pubsub');
var LRU = require('lru-cache');
var cache = LRU({
@ -19,6 +20,10 @@ var cache = LRU({
module.exports = function (User) {
User.settingsCache = cache;
pubsub.on('user:settings:cache:del', function (uid) {
cache.del('user:' + uid + ':settings');
});
User.getSettings = function (uid, callback) {
if (!parseInt(uid, 10)) {
return onSettingsLoaded(0, {}, callback);
@ -178,6 +183,7 @@ module.exports = function (User) {
},
function (next) {
cache.del('user:' + uid + ':settings');
pubsub.publish('user:settings:cache:del', uid);
User.getSettings(uid, next);
},
], callback);

@ -7,8 +7,64 @@ var request = require('request');
var db = require('./mocks/databasemock');
var user = require('../src/user');
var meta = require('../src/meta');
describe('authentication', function () {
function loginUser(username, password, callback) {
var jar = request.jar();
request({
url: nconf.get('url') + '/api/config',
json: true,
jar: jar,
}, function (err, response, body) {
if (err) {
return callback(err);
}
request.post(nconf.get('url') + '/login', {
form: {
username: username,
password: password,
},
json: true,
jar: jar,
headers: {
'x-csrf-token': body.csrf_token,
},
}, function (err, response, body) {
callback(err, response, body, jar);
});
});
}
function registerUser(email, username, password, callback) {
var jar = request.jar();
request({
url: nconf.get('url') + '/api/config',
json: true,
jar: jar,
}, function (err, response, body) {
if (err) {
return callback(err);
}
request.post(nconf.get('url') + '/register', {
form: {
email: email,
username: username,
password: password,
},
json: true,
jar: jar,
headers: {
'x-csrf-token': body.csrf_token,
},
}, function (err, response, body) {
callback(err, response, body, jar);
});
});
}
var jar = request.jar();
var regularUid;
before(function (done) {
@ -89,43 +145,24 @@ describe('authentication', function () {
});
it('should login a user', function (done) {
var jar = request.jar();
request({
url: nconf.get('url') + '/api/config',
json: true,
jar: jar,
}, function (err, response, body) {
loginUser('regular', 'regularpwd', function (err, response, body, jar) {
assert.ifError(err);
assert(body);
request.post(nconf.get('url') + '/login', {
form: {
username: 'regular',
password: 'regularpwd',
},
request({
url: nconf.get('url') + '/api/me',
json: true,
jar: jar,
headers: {
'x-csrf-token': body.csrf_token,
},
}, function (err, response, body) {
assert.ifError(err);
assert(body);
request({
url: nconf.get('url') + '/api/me',
json: true,
jar: jar,
}, function (err, response, body) {
assert.equal(body.username, 'regular');
assert.equal(body.email, 'regular@nodebb.org');
db.getObject('uid:' + regularUid + ':sessionUUID:sessionId', function (err, sessions) {
assert.ifError(err);
assert(body);
assert.equal(body.username, 'regular');
assert.equal(body.email, 'regular@nodebb.org');
db.getObject('uid:' + regularUid + ':sessionUUID:sessionId', function (err, sessions) {
assert.ifError(err);
assert(sessions);
assert(Object.keys(sessions).length > 0);
done();
});
assert(sessions);
assert(Object.keys(sessions).length > 0);
done();
});
});
});
@ -147,6 +184,119 @@ describe('authentication', function () {
});
});
it('should fail to login if user does not exist', function (done) {
loginUser('doesnotexist', 'nopassword', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 403);
assert.equal(body, '[[error:invalid-login-credentials]]');
done();
});
});
it('should fail to login if username is empty', function (done) {
loginUser('', 'some password', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 403);
assert.equal(body, '[[error:invalid-username-or-password]]');
done();
});
});
it('should fail to login if password is empty', function (done) {
loginUser('someuser', '', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 403);
assert.equal(body, '[[error:invalid-username-or-password]]');
done();
});
});
it('should fail to login if username and password are empty', function (done) {
loginUser('', '', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 403);
assert.equal(body, '[[error:invalid-username-or-password]]');
done();
});
});
it('should fail to login if password is longer than 4096', function (done) {
var longPassword;
for (var i = 0; i < 5000; i++) {
longPassword += 'a';
}
loginUser('someuser', longPassword, function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 403);
assert.equal(body, '[[error:password-too-long]]');
done();
});
});
it('should fail to login if local login is disabled', function (done) {
meta.config.allowLocalLogin = 0;
loginUser('someuser', 'somepass', function (err, response, body) {
meta.config.allowLocalLogin = 1;
assert.ifError(err);
assert.equal(response.statusCode, 403);
assert.equal(body, '[[error:local-login-disabled]]');
done();
});
});
it('should fail to register if registraton is disabled', function (done) {
meta.config.registrationType = 'disabled';
registerUser('some@user.com', 'someuser', 'somepassword', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 403);
assert.equal(body, 'Forbidden');
done();
});
});
it('should return error if invitation is not valid', function (done) {
meta.config.registrationType = 'invite-only';
registerUser('some@user.com', 'someuser', 'somepassword', function (err, response, body) {
meta.config.registrationType = 'normal';
assert.ifError(err);
assert.equal(response.statusCode, 400);
assert.equal(body, '[[error:invalid-data]]');
done();
});
});
it('should fail to register if email is falsy', function (done) {
registerUser('', 'someuser', 'somepassword', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 400);
assert.equal(body, '[[error:invalid-email]]');
done();
});
});
it('should fail to register if username is falsy or too short', function (done) {
registerUser('some@user.com', '', 'somepassword', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 400);
assert.equal(body, '[[error:username-too-short]]');
registerUser('some@user.com', 'a', 'somepassword', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 400);
assert.equal(body, '[[error:username-too-short]]');
done();
});
});
});
it('should fail to register if username is too long', function (done) {
registerUser('some@user.com', 'thisisareallylongusername', '123456', function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 400);
assert.equal(body, '[[error:username-too-long]]');
done();
});
});
after(function (done) {
db.emptydb(done);

@ -1,19 +1,199 @@
'use strict';
var string = require('string');
var path = require('path');
var fs = require('fs');
var assert = require('assert');
var mkdirp = require('mkdirp');
var rimraf = require('rimraf');
var async = require('async');
var db = require('./mocks/databasemock');
var file = require('../src/file');
describe('Build', function () {
describe('minifier', function () {
before(function (done) {
db.setupMockDefaults(done);
mkdirp(path.join(__dirname, '../build/test'), done);
});
it('should build all assets', function (done) {
this.timeout(50000);
var build = require('../src/meta/build');
build.buildAll(function (err) {
var minifier = require('../src/meta/minifier');
var scripts = [
path.resolve(__dirname, './files/1.js'),
path.resolve(__dirname, './files/2.js'),
];
it('.js.bundle() should concat scripts', function (done) {
minifier.js.bundle(scripts, false, false, function (err, bundle) {
assert.ifError(err);
assert.strictEqual(
bundle.code,
'(function (window, document) {' +
'\n\twindow.doStuff = function () {' +
'\n\t\tdocument.body.innerHTML = \'Stuff has been done\';' +
'\n\t};' +
'\n})(window, document);' +
'\n' +
'\n;function foo(name, age) {' +
'\n\treturn \'The person known as "\' + name + \'" is \' + age + \' years old\';' +
'\n}' +
'\n'
);
done();
});
});
it('.js.bundle() should minify scripts', function (done) {
minifier.js.bundle(scripts, true, false, function (err, bundle) {
assert.ifError(err);
assert.strictEqual(
bundle.code,
'(function(n,o){n.doStuff=function(){o.body.innerHTML="Stuff has been done"}})(window,document);function foo(n,o){return\'The person known as "\'+n+\'" is \'+o+" years old"}'
);
done();
});
});
it('.js.minifyBatch() should minify each script', function (done) {
var s = scripts.map(function (script) {
return {
srcPath: script,
destPath: path.resolve(__dirname, '../build/test', path.basename(script)),
};
});
minifier.js.minifyBatch(s, false, function (err) {
assert.ifError(err);
assert(file.existsSync(s[0].destPath));
assert(file.existsSync(s[1].destPath));
fs.readFile(s[0].destPath, function (err, buffer) {
assert.ifError(err);
assert.strictEqual(
buffer.toString(),
'(function(n,o){n.doStuff=function(){o.body.innerHTML="Stuff has been done"}})(window,document);'
);
done();
});
});
});
var styles = [
'@import (inline) "./1.css";',
'@import "./2.less";',
].join('\n');
var paths = [
path.resolve(__dirname, './files'),
];
it('.css.bundle() should concat styles', function (done) {
minifier.css.bundle(styles, paths, false, false, function (err, bundle) {
assert.ifError(err);
assert.strictEqual(bundle.code, '.help { margin: 10px; } .yellow { background: yellow; }\n.help {\n display: block;\n}\n.help .blue {\n background: blue;\n}\n');
done();
});
});
it('.css.bundle() should minify styles', function (done) {
minifier.css.bundle(styles, paths, true, false, function (err, bundle) {
assert.ifError(err);
assert.strictEqual(bundle.code, '.help{margin:10px;display:block}.yellow{background:#ff0}.help .blue{background:#00f}');
done();
});
});
});
describe('Build', function (done) {
var build = require('../src/meta/build');
before(function (done) {
async.parallel([
async.apply(rimraf, path.join(__dirname, '../build/public')),
db.setupMockDefaults,
], done);
});
it('should build plugin static dirs', function (done) {
build.build(['plugin static dirs'], function (err) {
assert.ifError(err);
assert(file.existsSync(path.join(__dirname, '../build/public/plugins/nodebb-plugin-dbsearch/dbsearch')));
done();
});
});
it('should build requirejs modules', function (done) {
build.build(['requirejs modules'], function (err) {
assert.ifError(err);
var filename = path.join(__dirname, '../build/public/src/modules/Chart.js');
assert(file.existsSync(filename));
assert(fs.readFileSync(filename).toString().startsWith('/*!\n * Chart.js'));
done();
});
});
it('should build client js bundle', function (done) {
build.build(['client js bundle'], function (err) {
assert.ifError(err);
var filename = path.join(__dirname, '../build/public/nodebb.min.js');
assert(file.existsSync(filename));
assert(fs.readFileSync(filename).length > 1000);
done();
});
});
it('should build admin js bundle', function (done) {
build.build(['admin js bundle'], function (err) {
assert.ifError(err);
var filename = path.join(__dirname, '../build/public/acp.min.js');
assert(file.existsSync(filename));
assert(fs.readFileSync(filename).length > 1000);
done();
});
});
it('should build client side styles', function (done) {
build.build(['client side styles'], function (err) {
assert.ifError(err);
var filename = path.join(__dirname, '../build/public/stylesheet.css');
assert(file.existsSync(filename));
assert(fs.readFileSync(filename).toString().startsWith('/*! normalize.css'));
done();
});
});
it('should build admin control panel styles', function (done) {
build.build(['admin control panel styles'], function (err) {
assert.ifError(err);
var filename = path.join(__dirname, '../build/public/admin.css');
assert(file.existsSync(filename));
assert(fs.readFileSync(filename).toString().startsWith('@charset "UTF-8";'));
done();
});
});
it('should build templates', function (done) {
build.build(['templates'], function (err) {
assert.ifError(err);
var filename = path.join(__dirname, '../build/public/templates/admin/header.tpl');
assert(file.existsSync(filename));
assert(fs.readFileSync(filename).toString().startsWith('<!DOCTYPE html>'));
done();
});
});
it('should build languages', function (done) {
build.build(['languages'], function (err) {
assert.ifError(err);
var filename = path.join(__dirname, '../build/public/language/en-GB/global.json');
assert(file.existsSync(filename));
var global = fs.readFileSync(filename).toString();
assert.strictEqual(JSON.parse(global).home, 'Home');
done();
});
});
it('should build sounds', function (done) {
build.build(['sounds'], function (err) {
assert.ifError(err);
var filename = path.join(__dirname, '../build/public/sounds/fileMap.json');
assert(file.existsSync(filename));
done();
});
});

@ -2,6 +2,7 @@
var assert = require('assert');
var nconf = require('nconf');
var db = require('./mocks/databasemock');
@ -12,14 +13,46 @@ describe('Test database', function () {
});
});
it('should return info about database', function (done) {
db.info(db.client, function (err, info) {
assert.ifError(err);
assert(info);
done();
describe('info', function () {
it('should return info about database', function (done) {
db.info(db.client, function (err, info) {
assert.ifError(err);
assert(info);
done();
});
});
it('should not error and return null if client is falsy', function (done) {
db.info(null, function (err, info) {
assert.ifError(err);
assert.equal(info, null);
done();
});
});
});
describe('checkCompatibility', function () {
it('should not throw', function (done) {
db.checkCompatibility(done);
});
it('should return error with a too low version', function (done) {
var dbName = nconf.get('database');
if (dbName === 'redis') {
db.checkCompatibilityVersion('2.4.0', function (err) {
assert.equal(err.message, 'Your Redis version is not new enough to support NodeBB, please upgrade Redis to v2.8.9 or higher.');
done();
});
} else if (dbName === 'mongo') {
db.checkCompatibilityVersion('1.8.0', function (err) {
assert.equal(err.message, 'The `mongodb` package is out-of-date, please run `./nodebb setup` again.');
done();
});
}
});
});
require('./database/keys');
require('./database/list');
require('./database/sets');

@ -190,6 +190,15 @@ describe('Hash methods', function () {
done();
});
});
it('should return undefined for all fields if object does not exist', function (done) {
db.getObjectsFields(['doesnotexist1', 'doesnotexist2'], ['name', 'age'], function (err, data) {
assert.ifError(err);
assert.equal(data.name, null);
assert.equal(data.age, null);
done();
});
});
});
describe('getObjectKeys()', function () {

@ -102,6 +102,37 @@ describe('Key methods', function () {
});
});
it('should delete all sorted set elements', function (done) {
async.parallel([
function (next) {
db.sortedSetAdd('deletezset', 1, 'value1', next);
},
function (next) {
db.sortedSetAdd('deletezset', 2, 'value2', next);
},
], function (err) {
if (err) {
return done(err);
}
db.delete('deletezset', function (err) {
assert.ifError(err);
async.parallel({
key1exists: function (next) {
db.isSortedSetMember('deletezset', 'value1', next);
},
key2exists: function (next) {
db.isSortedSetMember('deletezset', 'value2', next);
},
}, function (err, results) {
assert.equal(err, null);
assert.equal(results.key1exists, false);
assert.equal(results.key2exists, false);
done();
});
});
});
});
describe('increment', function () {
it('should initialize key to 1', function (done) {
db.increment('keyToIncrement', function (err, value) {

@ -9,11 +9,18 @@ describe('List methods', function () {
describe('listAppend()', function () {
it('should append to a list', function (done) {
db.listAppend('testList1', 5, function (err) {
assert.equal(err, null);
assert.ifError(err);
assert.equal(arguments.length, 1);
done();
});
});
it('should not add anyhing if key is falsy', function (done) {
db.listAppend(null, 3, function (err) {
assert.ifError(err);
done();
});
});
});
describe('listPrepend()', function () {
@ -38,6 +45,13 @@ describe('List methods', function () {
done();
});
});
it('should not add anyhing if key is falsy', function (done) {
db.listPrepend(null, 3, function (err) {
assert.ifError(err);
done();
});
});
});
describe('getListRange()', function () {
@ -83,6 +97,14 @@ describe('List methods', function () {
done();
});
});
it('should not get anything if key is falsy', function (done) {
db.getListRange(null, 0, -1, function (err, data) {
assert.ifError(err);
assert.equal(data, undefined);
done();
});
});
});
describe('listRemoveLast()', function () {
@ -105,6 +127,13 @@ describe('List methods', function () {
done();
});
});
it('should not remove anyhing if key is falsy', function (done) {
db.listRemoveLast(null, function (err) {
assert.ifError(err);
done();
});
});
});
describe('listRemoveAll()', function () {
@ -132,6 +161,13 @@ describe('List methods', function () {
});
});
});
it('should not remove anyhing if key is falsy', function (done) {
db.listRemoveAll(null, 3, function (err) {
assert.ifError(err);
done();
});
});
});
describe('listTrim()', function () {
@ -156,6 +192,13 @@ describe('List methods', function () {
});
});
});
it('should not add anyhing if key is falsy', function (done) {
db.listTrim(null, 0, 3, function (err) {
assert.ifError(err);
done();
});
});
});

@ -303,6 +303,7 @@ describe('Sorted Set methods', function () {
assert.equal(err, null);
assert.equal(arguments.length, 2);
assert.equal(!!score, false);
assert.strictEqual(score, null);
done();
});
});
@ -312,6 +313,7 @@ describe('Sorted Set methods', function () {
assert.equal(err, null);
assert.equal(arguments.length, 2);
assert.equal(!!score, false);
assert.strictEqual(score, null);
done();
});
});

@ -0,0 +1,120 @@
'use strict';
var assert = require('assert');
var async = require('async');
var request = require('request');
var nconf = require('nconf');
var db = require('./mocks/databasemock');
var topics = require('../src/topics');
var categories = require('../src/categories');
var groups = require('../src/groups');
var user = require('../src/user');
var meta = require('../src/meta');
var privileges = require('../src/privileges');
describe('feeds', function () {
var tid;
var pid;
var fooUid;
var cid;
before(function (done) {
groups.resetCache();
meta.config['feeds:disableRSS'] = 1;
async.series({
category: function (next) {
categories.create({
name: 'Test Category',
description: 'Test category created by testing script',
}, next);
},
user: function (next) {
user.create({ username: 'foo', password: 'barbar', email: 'foo@test.com' }, next);
},
}, function (err, results) {
if (err) {
return done(err);
}
cid = results.category.cid;
fooUid = results.user;
topics.post({ uid: results.user, title: 'test topic title', content: 'test topic content', cid: results.category.cid }, function (err, result) {
tid = result.topicData.tid;
pid = result.postData.pid;
done(err);
});
});
});
it('should 404', function (done) {
var feedUrls = [
nconf.get('url') + '/topic/' + tid + '.rss',
nconf.get('url') + '/category/' + cid + '.rss',
nconf.get('url') + '/recent.rss',
nconf.get('url') + '/popular.rss',
nconf.get('url') + '/popular/day.rss',
nconf.get('url') + '/recentposts.rss',
nconf.get('url') + '/category/' + cid + '/recentposts.rss',
nconf.get('url') + '/user/foo/topics.rss',
nconf.get('url') + '/tags/nodebb.rss',
];
async.eachSeries(feedUrls, function (url, next) {
request(url, function (err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 404);
next();
});
}, function (err) {
assert.ifError(err);
meta.config['feeds:disableRSS'] = 0;
done();
});
});
it('should 404 if topic does not exist', function (done) {
request(nconf.get('url') + '/topic/' + 1000 + '.rss', function (err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 404);
done();
});
});
it('should redirect if we do not have read privilege', function (done) {
privileges.categories.rescind(['topics:read'], cid, 'guests', function (err) {
assert.ifError(err);
request(nconf.get('url') + '/topic/' + tid + '.rss', function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
assert(body.indexOf('Login to your account') !== -1);
privileges.categories.give(['topics:read'], cid, 'guests', done);
});
});
});
it('should 404 if user is not found', function (done) {
request(nconf.get('url') + '/user/doesnotexist/topics.rss', function (err, res) {
assert.ifError(err);
assert.equal(res.statusCode, 404);
done();
});
});
it('should redirect if we do not have read privilege', function (done) {
privileges.categories.rescind(['read'], cid, 'guests', function (err) {
assert.ifError(err);
request(nconf.get('url') + '/category/' + cid + '.rss', function (err, res, body) {
assert.ifError(err);
assert.equal(res.statusCode, 200);
assert(body);
assert(body.indexOf('Login to your account') !== -1);
privileges.categories.give(['read'], cid, 'guests', done);
});
});
});
after(function (done) {
db.emptydb(done);
});
});

@ -0,0 +1 @@
.help { margin: 10px; } .yellow { background: yellow; }

@ -0,0 +1,5 @@
(function (window, document) {
window.doStuff = function () {
document.body.innerHTML = 'Stuff has been done';
};
})(window, document);

@ -0,0 +1,3 @@
function foo(name, age) {
return 'The person known as "' + name + '" is ' + age + ' years old';
}

@ -0,0 +1 @@
.help { display: block; .blue { background: blue; } }

@ -11,7 +11,7 @@ var User = require('../src/user');
var Groups = require('../src/groups');
var Messaging = require('../src/messaging');
var helpers = require('./helpers');
var socketModules = require('../src/socket.io/modules');
describe('Messaging Library', function () {
var fooUid;
@ -55,7 +55,10 @@ describe('Messaging Library', function () {
assert.ifError(err);
Messaging.canMessageUser(herpUid, bazUid, function (err) {
assert.strictEqual(err.message, '[[error:chat-restricted]]');
done();
socketModules.chats.addUserToRoom({ uid: herpUid }, { roomId: 1, username: 'baz' }, function (err) {
assert.equal(err.message, '[[error:chat-restricted]]');
done();
});
});
});
});
@ -78,23 +81,93 @@ describe('Messaging Library', function () {
});
describe('rooms', function () {
var socketModules = require('../src/socket.io/modules');
it('should fail to create a new chat room with invalid data', function (done) {
socketModules.chats.newRoom({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
it('should return rate limit error on second try', function (done) {
var socketMock = { uid: fooUid };
socketModules.chats.newRoom(socketMock, { touid: bazUid }, function (err) {
assert.ifError(err);
socketModules.chats.newRoom(socketMock, { touid: bazUid }, function (err) {
assert.equal(err.message, '[[error:too-many-messages]]');
done();
});
});
});
it('should create a new chat room', function (done) {
socketModules.chats.newRoom({ uid: fooUid }, { touid: bazUid }, function (err, _roomId) {
roomId = _roomId;
assert.ifError(err);
assert(roomId);
done();
socketModules.chats.canMessage({ uid: fooUid }, _roomId, function (err) {
assert.ifError(err);
done();
});
});
});
it('should fail to add user to room with invalid data', function (done) {
socketModules.chats.addUserToRoom({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
});
it('should add a user to room', function (done) {
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) {
assert.ifError(err);
Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) {
assert.ifError(err);
assert(isInRoom);
done();
});
});
});
it('should fail to add users to room if max is reached', function (done) {
meta.config.maximumUsersInChatRoom = 2;
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'test' }, function (err) {
assert.equal(err.message, '[[error:cant-add-more-users-to-chat-room]]');
meta.config.maximumUsersInChatRoom = 0;
done();
});
});
it('should fail to add users to room if user does not exist', function (done) {
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'doesnotexist' }, function (err) {
assert.equal(err.message, '[[error:no-user]]');
done();
});
});
it('should fail to add self to room', function (done) {
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'foo' }, function (err) {
assert.equal(err.message, '[[error:cant-add-self-to-chat-room]]');
done();
});
});
it('should fail to leave room with invalid data', function (done) {
socketModules.chats.leave({ uid: null }, roomId, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.leave({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
it('should leave the chat room', function (done) {
socketModules.chats.leave({ uid: bazUid }, roomId, function (err) {
assert.ifError(err);
@ -106,6 +179,60 @@ describe('Messaging Library', function () {
});
});
it('should fail to remove user from room', function (done) {
socketModules.chats.removeUserFromRoom({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.removeUserFromRoom({ uid: fooUid }, {}, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
it('should fail to remove user from room if user does not exist', function (done) {
socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'doesnotexist' }, function (err) {
assert.equal(err.message, '[[error:no-user]]');
done();
});
});
it('should remove user from room', function (done) {
socketModules.chats.newRoom({ uid: fooUid }, { touid: herpUid }, function (err, roomId) {
assert.ifError(err);
Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) {
assert.ifError(err);
assert(isInRoom);
socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) {
assert.equal(err.message, '[[error:cant-remove-last-user]]');
socketModules.chats.addUserToRoom({ uid: fooUid }, { roomId: roomId, username: 'baz' }, function (err) {
assert.ifError(err);
socketModules.chats.removeUserFromRoom({ uid: fooUid }, { roomId: roomId, username: 'herp' }, function (err) {
assert.ifError(err);
Messaging.isUserInRoom(herpUid, roomId, function (err, isInRoom) {
assert.ifError(err);
assert(!isInRoom);
done();
});
});
});
});
});
});
});
it('should fail to send a message to room with invalid data', function (done) {
socketModules.chats.send({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.send({ uid: fooUid }, { roomId: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.send({ uid: null }, { roomId: 1 }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
});
it('should send a message to a room', function (done) {
socketModules.chats.send({ uid: fooUid }, { roomId: roomId, message: 'first chat message' }, function (err, messageData) {
assert.ifError(err);
@ -116,11 +243,39 @@ describe('Messaging Library', function () {
socketModules.chats.getRaw({ uid: fooUid }, { roomId: roomId, mid: messageData.mid }, function (err, raw) {
assert.ifError(err);
assert.equal(raw, 'first chat message');
setTimeout(done, 300);
});
});
});
it('should fail to send second message due to rate limit', function (done) {
var socketMock = { uid: fooUid };
socketModules.chats.send(socketMock, { roomId: roomId, message: 'first chat message' }, function (err) {
assert.ifError(err);
socketModules.chats.send(socketMock, { roomId: roomId, message: 'first chat message' }, function (err) {
assert.equal(err.message, '[[error:too-many-messages]]');
done();
});
});
});
it('should return invalid-data error', function (done) {
socketModules.chats.getRaw({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.getRaw({ uid: fooUid }, { }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
it('should return not in room error', function (done) {
socketModules.chats.getRaw({ uid: 0 }, { roomId: roomId, mid: 1 }, function (err) {
assert.equal(err.message, '[[error:not-allowed]]');
done();
});
});
it('should notify offline users of message', function (done) {
Messaging.notificationSendDelay = 100;
@ -143,6 +298,22 @@ describe('Messaging Library', function () {
});
});
it('should fail to get messages from room with invalid data', function (done) {
socketModules.chats.getMessages({ uid: null }, null, function (err, messages) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.getMessages({ uid: fooUid }, null, function (err, messages) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.getMessages({ uid: fooUid }, { uid: null }, function (err, messages) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.getMessages({ uid: fooUid }, { uid: 1, roomId: null }, function (err, messages) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
});
});
it('should get messages from room', function (done) {
socketModules.chats.getMessages({ uid: fooUid }, {
uid: fooUid,
@ -157,6 +328,23 @@ describe('Messaging Library', function () {
});
});
it('should fail to mark read with invalid data', function (done) {
socketModules.chats.markRead({ uid: null }, roomId, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.markRead({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
it('should not error if user is not in room', function (done) {
socketModules.chats.markRead({ uid: herpUid }, 10, function (err) {
assert.ifError(err);
done();
});
});
it('should mark room read', function (done) {
socketModules.chats.markRead({ uid: fooUid }, roomId, function (err) {
assert.ifError(err);
@ -171,10 +359,39 @@ describe('Messaging Library', function () {
});
});
it('should fail to rename room with invalid data', function (done) {
socketModules.chats.renameRoom({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.renameRoom({ uid: fooUid }, { roomId: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.renameRoom({ uid: fooUid }, { roomId: roomId, newName: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
});
it('should rename room', function (done) {
socketModules.chats.renameRoom({ uid: fooUid }, { roomId: roomId, newName: 'new room name' }, function (err) {
assert.ifError(err);
done();
});
});
it('should fail to load room with invalid-data', function (done) {
socketModules.chats.loadRoom({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.loadRoom({ uid: fooUid }, { roomId: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
it('should fail to load room if user is not in', function (done) {
socketModules.chats.loadRoom({ uid: 0 }, { roomId: roomId }, function (err) {
assert.equal(err.message, '[[error:not-allowed]]');
done();
});
});
@ -198,6 +415,45 @@ describe('Messaging Library', function () {
});
});
});
it('should fail to load recent chats with invalid data', function (done) {
socketModules.chats.getRecentChats({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.getRecentChats({ uid: fooUid }, { after: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.getRecentChats({ uid: fooUid }, { after: 0, uid: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
});
it('should load recent chats of user', function (done) {
socketModules.chats.getRecentChats({ uid: fooUid }, { after: 0, uid: fooUid }, function (err, data) {
assert.ifError(err);
assert(Array.isArray(data.rooms));
done();
});
});
it('should fail to check if user has private chat with invalid data', function (done) {
socketModules.chats.hasPrivateChat({ uid: null }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.hasPrivateChat({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
it('should check if user has private chat with another uid', function (done) {
socketModules.chats.hasPrivateChat({ uid: fooUid }, herpUid, function (err, roomId) {
assert.ifError(err);
assert(roomId);
done();
});
});
});
describe('edit/delete', function () {
@ -211,6 +467,26 @@ describe('Messaging Library', function () {
});
});
it('should fail to edit message with invalid data', function (done) {
socketModules.chats.edit({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.edit({ uid: fooUid }, { roomId: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.edit({ uid: fooUid }, { roomId: 1, message: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
});
it('should fail to edit message if not own message', function (done) {
socketModules.chats.edit({ uid: herpUid }, { mid: 5, roomId: roomId, message: 'message edited' }, function (err) {
assert.equal(err.message, '[[error:cant-edit-chat-message]]');
done();
});
});
it('should edit message', function (done) {
socketModules.chats.edit({ uid: fooUid }, { mid: mid, roomId: roomId, message: 'message edited' }, function (err) {
assert.ifError(err);
@ -222,6 +498,27 @@ describe('Messaging Library', function () {
});
});
it('should fail to delete message with invalid data', function (done) {
socketModules.chats.delete({ uid: fooUid }, null, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.delete({ uid: fooUid }, { roomId: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
socketModules.chats.delete({ uid: fooUid }, { roomId: 1, messageId: null }, function (err) {
assert.equal(err.message, '[[error:invalid-data]]');
done();
});
});
});
});
it('should fail to delete message if not owner', function (done) {
socketModules.chats.delete({ uid: herpUid }, { messageId: mid, roomId: roomId }, function (err) {
assert.equal(err.message, '[[error:cant-delete-chat-message]]');
done();
});
});
it('should delete message', function (done) {
socketModules.chats.delete({ uid: fooUid }, { messageId: mid, roomId: roomId }, function (err) {
assert.ifError(err);

@ -262,6 +262,21 @@ describe('meta', function () {
});
describe('sounds', function () {
var socketModules = require('../src/socket.io/modules');
it('should getUserMap', function (done) {
socketModules.sounds.getUserSoundMap({ uid: 1 }, null, function (err, data) {
assert.ifError(err);
assert(data.hasOwnProperty('chat-incoming'));
assert(data.hasOwnProperty('chat-outgoing'));
assert(data.hasOwnProperty('notification'));
done();
});
});
});
after(function (done) {
db.emptydb(done);
});

@ -5,185 +5,182 @@
* ATTENTION: testing db is flushed before every use!
*/
(function (module) {
var async = require('async');
var winston = require('winston');
var path = require('path');
var nconf = require('nconf');
var url = require('url');
var errorText;
nconf.file({ file: path.join(__dirname, '../../config.json') });
nconf.defaults({
base_dir: path.join(__dirname, '../..'),
themes_path: path.join(__dirname, '../../node_modules'),
upload_path: 'public/uploads',
views_dir: path.join(__dirname, '../../build/public/templates'),
relative_path: '',
});
if (!nconf.get('isCluster')) {
nconf.set('isPrimary', 'true');
nconf.set('isCluster', 'false');
}
var dbType = nconf.get('database');
var testDbConfig = nconf.get('test_database');
var productionDbConfig = nconf.get(dbType);
if (!testDbConfig) {
errorText = 'test_database is not defined';
winston.info(
'\n===========================================================\n' +
'Please, add parameters for test database in config.json\n' +
'For example (redis):\n' +
'"test_database": {\n' +
' "host": "127.0.0.1",\n' +
' "port": "6379",\n' +
' "password": "",\n' +
' "database": "1"\n' +
'}\n' +
' or (mongo):\n' +
'"test_database": {\n' +
' "host": "127.0.0.1",\n' +
' "port": "27017",\n' +
' "password": "",\n' +
' "database": "1\n' +
'}\n' +
' or (mongo) in a replicaset\n' +
'"test_database": {\n' +
' "host": "127.0.0.1,127.0.0.1,127.0.0.1",\n' +
' "port": "27017,27018,27019",\n' +
' "username": "",\n' +
' "password": "",\n' +
' "database": "nodebb_test"\n' +
'}\n' +
'==========================================================='
);
winston.error(errorText);
throw new Error(errorText);
}
if (testDbConfig.database === productionDbConfig.database &&
testDbConfig.host === productionDbConfig.host &&
testDbConfig.port === productionDbConfig.port) {
errorText = 'test_database has the same config as production db';
winston.error(errorText);
throw new Error(errorText);
}
nconf.set(dbType, testDbConfig);
winston.info('database config');
winston.info(dbType);
winston.info(testDbConfig);
var db = require('../../src/database');
before(function (done) {
this.timeout(30000);
async.series([
function (next) {
db.init(next);
},
function (next) {
setupMockDefaults(next);
},
function (next) {
db.initSessionStore(next);
},
function (next) {
var meta = require('../../src/meta');
// nconf defaults, if not set in config
if (!nconf.get('sessionKey')) {
nconf.set('sessionKey', 'express.sid');
}
// Parse out the relative_url and other goodies from the configured URL
var urlObject = url.parse(nconf.get('url'));
var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
nconf.set('secure', urlObject.protocol === 'https:');
nconf.set('use_port', !!urlObject.port);
nconf.set('relative_path', relativePath);
nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567);
nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path')));
nconf.set('core_templates_path', path.join(__dirname, '../../src/views'));
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates'));
nconf.set('theme_templates_path', meta.config['theme:templates'] ? path.join(nconf.get('themes_path'), meta.config['theme:id'], meta.config['theme:templates']) : nconf.get('base_templates_path'));
nconf.set('theme_config', path.join(nconf.get('themes_path'), 'nodebb-theme-persona', 'theme.json'));
nconf.set('bcrypt_rounds', 1);
next();
},
function (next) {
var webserver = require('../../src/webserver');
var sockets = require('../../src/socket.io');
sockets.init(webserver.server);
require('../../src/notifications').startJobs();
require('../../src/user').startJobs();
webserver.listen(next);
},
], done);
});
function setupMockDefaults(callback) {
var meta = require('../../src/meta');
async.series([
function (next) {
db.emptydb(next);
},
function (next) {
winston.info('test_database flushed');
setupDefaultConfigs(meta, next);
},
function (next) {
meta.configs.init(next);
},
function (next) {
meta.dependencies.check(next);
},
function (next) {
meta.config.postDelay = 0;
meta.config.initialPostDelay = 0;
meta.config.newbiePostDelay = 0;
enableDefaultPlugins(next);
},
function (next) {
meta.themes.set({
type: 'local',
id: 'nodebb-theme-persona',
}, next);
},
], callback);
}
db.setupMockDefaults = setupMockDefaults;
function setupDefaultConfigs(meta, next) {
winston.info('Populating database with default configs, if not already set...\n');
var defaults = require(path.join(nconf.get('base_dir'), 'install/data/defaults.json'));
meta.configs.setOnEmpty(defaults, next);
}
function enableDefaultPlugins(callback) {
winston.info('Enabling default plugins\n');
var defaultEnabled = [
'nodebb-plugin-dbsearch',
];
winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled);
db.sortedSetAdd('plugins:active', [0], defaultEnabled, callback);
}
module.exports = db;
}(module));
var async = require('async');
var winston = require('winston');
var path = require('path');
var nconf = require('nconf');
var url = require('url');
var errorText;
nconf.file({ file: path.join(__dirname, '../../config.json') });
nconf.defaults({
base_dir: path.join(__dirname, '../..'),
themes_path: path.join(__dirname, '../../node_modules'),
upload_path: 'public/uploads',
views_dir: path.join(__dirname, '../../build/public/templates'),
relative_path: '',
});
if (!nconf.get('isCluster')) {
nconf.set('isPrimary', 'true');
nconf.set('isCluster', 'false');
}
var dbType = nconf.get('database');
var testDbConfig = nconf.get('test_database');
var productionDbConfig = nconf.get(dbType);
if (!testDbConfig) {
errorText = 'test_database is not defined';
winston.info(
'\n===========================================================\n' +
'Please, add parameters for test database in config.json\n' +
'For example (redis):\n' +
'"test_database": {\n' +
' "host": "127.0.0.1",\n' +
' "port": "6379",\n' +
' "password": "",\n' +
' "database": "1"\n' +
'}\n' +
' or (mongo):\n' +
'"test_database": {\n' +
' "host": "127.0.0.1",\n' +
' "port": "27017",\n' +
' "password": "",\n' +
' "database": "1\n' +
'}\n' +
' or (mongo) in a replicaset\n' +
'"test_database": {\n' +
' "host": "127.0.0.1,127.0.0.1,127.0.0.1",\n' +
' "port": "27017,27018,27019",\n' +
' "username": "",\n' +
' "password": "",\n' +
' "database": "nodebb_test"\n' +
'}\n' +
'==========================================================='
);
winston.error(errorText);
throw new Error(errorText);
}
if (testDbConfig.database === productionDbConfig.database &&
testDbConfig.host === productionDbConfig.host &&
testDbConfig.port === productionDbConfig.port) {
errorText = 'test_database has the same config as production db';
winston.error(errorText);
throw new Error(errorText);
}
nconf.set(dbType, testDbConfig);
winston.info('database config');
winston.info(dbType);
winston.info(testDbConfig);
var db = require('../../src/database');
module.exports = db;
before(function (done) {
this.timeout(30000);
async.series([
function (next) {
db.init(next);
},
function (next) {
setupMockDefaults(next);
},
function (next) {
db.initSessionStore(next);
},
function (next) {
var meta = require('../../src/meta');
// nconf defaults, if not set in config
if (!nconf.get('sessionKey')) {
nconf.set('sessionKey', 'express.sid');
}
// Parse out the relative_url and other goodies from the configured URL
var urlObject = url.parse(nconf.get('url'));
var relativePath = urlObject.pathname !== '/' ? urlObject.pathname : '';
nconf.set('base_url', urlObject.protocol + '//' + urlObject.host);
nconf.set('secure', urlObject.protocol === 'https:');
nconf.set('use_port', !!urlObject.port);
nconf.set('relative_path', relativePath);
nconf.set('port', urlObject.port || nconf.get('port') || nconf.get('PORT') || (nconf.get('PORT_ENV_VAR') ? nconf.get(nconf.get('PORT_ENV_VAR')) : false) || 4567);
nconf.set('upload_path', path.join(nconf.get('base_dir'), nconf.get('upload_path')));
nconf.set('core_templates_path', path.join(__dirname, '../../src/views'));
nconf.set('base_templates_path', path.join(nconf.get('themes_path'), 'nodebb-theme-persona/templates'));
nconf.set('theme_templates_path', meta.config['theme:templates'] ? path.join(nconf.get('themes_path'), meta.config['theme:id'], meta.config['theme:templates']) : nconf.get('base_templates_path'));
nconf.set('theme_config', path.join(nconf.get('themes_path'), 'nodebb-theme-persona', 'theme.json'));
nconf.set('bcrypt_rounds', 1);
next();
},
function (next) {
var webserver = require('../../src/webserver');
var sockets = require('../../src/socket.io');
sockets.init(webserver.server);
require('../../src/notifications').startJobs();
require('../../src/user').startJobs();
webserver.listen(next);
},
], done);
});
function setupMockDefaults(callback) {
var meta = require('../../src/meta');
async.series([
function (next) {
db.emptydb(next);
},
function (next) {
winston.info('test_database flushed');
setupDefaultConfigs(meta, next);
},
function (next) {
meta.configs.init(next);
},
function (next) {
meta.dependencies.check(next);
},
function (next) {
meta.config.postDelay = 0;
meta.config.initialPostDelay = 0;
meta.config.newbiePostDelay = 0;
enableDefaultPlugins(next);
},
function (next) {
meta.themes.set({
type: 'local',
id: 'nodebb-theme-persona',
}, next);
},
], callback);
}
db.setupMockDefaults = setupMockDefaults;
function setupDefaultConfigs(meta, next) {
winston.info('Populating database with default configs, if not already set...\n');
var defaults = require(path.join(nconf.get('base_dir'), 'install/data/defaults.json'));
meta.configs.setOnEmpty(defaults, next);
}
function enableDefaultPlugins(callback) {
winston.info('Enabling default plugins\n');
var defaultEnabled = [
'nodebb-plugin-dbsearch',
];
winston.info('[install/enableDefaultPlugins] activating default plugins', defaultEnabled);
db.sortedSetAdd('plugins:active', [0], defaultEnabled, callback);
}

@ -5,7 +5,11 @@ var assert = require('assert');
var async = require('async');
var db = require('./mocks/databasemock');
var meta = require('../src/meta');
var user = require('../src/user');
var topics = require('../src/topics');
var categories = require('../src/categories');
var groups = require('../src/groups');
var notifications = require('../src/notifications');
var socketNotifications = require('../src/socket.io/notifications');
@ -14,6 +18,7 @@ describe('Notifications', function () {
var notification;
before(function (done) {
groups.resetCache();
user.create({ username: 'poster' }, function (err, _uid) {
if (err) {
return done(err);
@ -24,11 +29,19 @@ describe('Notifications', function () {
});
});
it('should fail to create notification without a nid', function (done) {
notifications.create({}, function (err) {
assert.equal(err.message, '[[error:no-notification-id]]');
done();
});
});
it('should create a notification', function (done) {
notifications.create({
bodyShort: 'bodyShort',
nid: 'notification_id',
path: '/notification/path',
pid: 1,
}, function (err, _notification) {
notification = _notification;
assert.ifError(err);
@ -45,6 +58,29 @@ describe('Notifications', function () {
});
});
it('should return null if pid is same and importance is lower', function (done) {
notifications.create({
bodyShort: 'bodyShort',
nid: 'notification_id',
path: '/notification/path',
pid: 1,
importance: 1,
}, function (err, notification) {
assert.ifError(err);
assert.strictEqual(notification, null);
done();
});
});
it('should get empty array', function (done) {
notifications.getMultiple(null, function (err, data) {
assert.ifError(err);
assert(Array.isArray(data));
assert.equal(data.length, 0);
done();
});
});
it('should get notifications', function (done) {
notifications.getMultiple([notification.nid], function (err, notificationsData) {
assert.ifError(err);
@ -55,6 +91,19 @@ describe('Notifications', function () {
});
});
it('should do nothing', function (done) {
notifications.push(null, [], function (err) {
assert.ifError(err);
notifications.push({ nid: null }, [], function (err) {
assert.ifError(err);
notifications.push(notification, [], function (err) {
assert.ifError(err);
done();
});
});
});
});
it('should push a notification to uid', function (done) {
notifications.push(notification, [uid], function (err) {
assert.ifError(err);
@ -94,6 +143,16 @@ describe('Notifications', function () {
});
});
it('should not mark anything with invalid uid or nid', function (done) {
socketNotifications.markRead({ uid: null }, null, function (err) {
assert.ifError(err);
socketNotifications.markRead({ uid: uid }, null, function (err) {
assert.ifError(err);
done();
});
});
});
it('should mark a notification read', function (done) {
socketNotifications.markRead({ uid: uid }, notification.nid, function (err) {
assert.ifError(err);
@ -109,6 +168,23 @@ describe('Notifications', function () {
});
});
it('should not mark anything with invalid uid or nid', function (done) {
socketNotifications.markUnread({ uid: null }, null, function (err) {
assert.ifError(err);
socketNotifications.markUnread({ uid: uid }, null, function (err) {
assert.ifError(err);
done();
});
});
});
it('should error if notification does not exist', function (done) {
socketNotifications.markUnread({ uid: uid }, 123123, function (err) {
assert.equal(err.message, '[[error:no-notification]]');
done();
});
});
it('should mark a notification unread', function (done) {
socketNotifications.markUnread({ uid: uid }, notification.nid, function (err) {
assert.ifError(err);
@ -143,6 +219,13 @@ describe('Notifications', function () {
});
});
it('should not do anything', function (done) {
socketNotifications.markAllRead({ uid: 1000 }, null, function (err) {
assert.ifError(err);
done();
});
});
it('should link to the first unread post in a watched topic', function (done) {
var categories = require('../src/categories');
var topics = require('../src/topics');
@ -251,14 +334,145 @@ describe('Notifications', function () {
});
});
it('should return empty with falsy uid', function (done) {
user.notifications.get(0, function (err, data) {
assert.ifError(err);
assert.equal(data.read.length, 0);
assert.equal(data.unread.length, 0);
done();
});
});
it('should get all notifications and filter', function (done) {
var nid = 'willbefiltered';
notifications.create({
bodyShort: 'bodyShort',
nid: nid,
path: '/notification/path',
type: 'post',
}, function (err, notification) {
assert.ifError(err);
notifications.push(notification, [uid], function (err) {
assert.ifError(err);
setTimeout(function () {
user.notifications.getAll(uid, 'post', function (err, nids) {
assert.ifError(err);
assert.notEqual(nids.indexOf(nid), -1);
done();
});
}, 1500);
});
});
});
it('should not get anything if notifications does not exist', function (done) {
user.notifications.getNotifications(['doesnotexistnid1', 'doesnotexistnid2'], uid, function (err, data) {
assert.ifError(err);
assert.deepEqual(data, []);
done();
});
});
it('should get daily notifications', function (done) {
user.notifications.getDailyUnread(uid, function (err, data) {
assert.ifError(err);
assert.equal(data[0].nid, 'willbefiltered');
done();
});
});
it('should return 0 for falsy uid', function (done) {
user.notifications.getUnreadCount(0, function (err, count) {
assert.ifError(err);
assert.equal(count, 0);
done();
});
});
it('should not do anything if uid is falsy', function (done) {
user.notifications.deleteAll(0, function (err) {
assert.ifError(err);
done();
});
});
it('should send notification to followers of user when he posts', function (done) {
var followerUid;
async.waterfall([
function (next) {
user.create({ username: 'follower' }, next);
},
function (_followerUid, next) {
followerUid = _followerUid;
user.follow(followerUid, uid, next);
},
function (next) {
categories.create({
name: 'Test Category',
description: 'Test category created by testing script',
}, next);
},
function (category, next) {
topics.post({
uid: uid,
cid: category.cid,
title: 'Test Topic Title',
content: 'The content of test topic',
}, next);
},
function (data, next) {
setTimeout(next, 1100);
},
function (next) {
user.notifications.getAll(followerUid, '', next);
},
], function (err, data) {
assert.ifError(err);
assert(data);
done();
});
});
it('should send welcome notification', function (done) {
meta.config.welcomeNotification = 'welcome to the forums';
user.notifications.sendWelcomeNotification(uid, function (err) {
assert.ifError(err);
user.notifications.sendWelcomeNotification(uid, function (err) {
assert.ifError(err);
setTimeout(function () {
user.notifications.getAll(uid, '', function (err, data) {
meta.config.welcomeNotification = '';
assert.ifError(err);
assert.notEqual(data.indexOf('welcome_' + uid), -1);
done();
});
}, 1100);
});
});
});
it('should prune notifications', function (done) {
notifications.create({
bodyShort: 'bodyShort',
nid: 'tobedeleted',
path: '/notification/path',
}, function (err) {
}, function (err, notification) {
assert.ifError(err);
notifications.prune(done);
notifications.prune(function (err) {
assert.ifError(err);
var week = 604800000;
db.sortedSetAdd('notifications', Date.now() - (2 * week), notification.nid, function (err) {
assert.ifError(err);
notifications.prune(function (err) {
assert.ifError(err);
notifications.get(notification.nid, function (err, data) {
assert.ifError(err);
assert(!data);
done();
});
});
});
});
});
});

@ -760,13 +760,13 @@ describe('Topic\'s', function () {
it('should 401 if not allowed to read as guest', function (done) {
var privileges = require('../src/privileges');
privileges.categories.rescind(['read'], topicData.cid, 'guests', function (err) {
privileges.categories.rescind(['topics:read'], topicData.cid, 'guests', function (err) {
assert.ifError(err);
request(nconf.get('url') + '/api/topic/' + topicData.slug, function (err, response, body) {
assert.ifError(err);
assert.equal(response.statusCode, 401);
assert(body);
privileges.categories.give(['read'], topicData.cid, 'guests', done);
privileges.categories.give(['topics:read'], topicData.cid, 'guests', done);
});
});
});
@ -1551,7 +1551,7 @@ describe('Topic\'s', function () {
it('should return empty array if first param is empty', function (done) {
topics.getTeasers([], function (err, teasers) {
topics.getTeasers([], 1, function (err, teasers) {
assert.ifError(err);
assert.equal(0, teasers.length);
done();
@ -1559,7 +1559,7 @@ describe('Topic\'s', function () {
});
it('should get teasers with 2 params', function (done) {
topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) {
topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) {
assert.ifError(err);
assert.deepEqual([undefined, undefined], teasers);
done();
@ -1568,7 +1568,7 @@ describe('Topic\'s', function () {
it('should get teasers with first posts', function (done) {
meta.config.teaserPost = 'first';
topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) {
topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) {
assert.ifError(err);
assert.equal(2, teasers.length);
assert(teasers[0]);
@ -1581,7 +1581,7 @@ describe('Topic\'s', function () {
});
it('should get teasers even if one topic is falsy', function (done) {
topics.getTeasers([null, topic2.topicData], function (err, teasers) {
topics.getTeasers([null, topic2.topicData], 1, function (err, teasers) {
assert.ifError(err);
assert.equal(2, teasers.length);
assert.equal(undefined, teasers[0]);
@ -1598,7 +1598,7 @@ describe('Topic\'s', function () {
topics.reply({ uid: adminUid, content: 'reply 1 content', tid: topic1.topicData.tid }, function (err, result) {
assert.ifError(err);
topic1.topicData.teaserPid = result.pid;
topics.getTeasers([topic1.topicData, topic2.topicData], function (err, teasers) {
topics.getTeasers([topic1.topicData, topic2.topicData], 1, function (err, teasers) {
assert.ifError(err);
assert(teasers[0]);
assert(teasers[1]);
@ -1610,7 +1610,7 @@ describe('Topic\'s', function () {
});
it('should get teasers by tids', function (done) {
topics.getTeasersByTids([topic2.topicData.tid, topic1.topicData.tid], function (err, teasers) {
topics.getTeasersByTids([topic2.topicData.tid, topic1.topicData.tid], 1, function (err, teasers) {
assert.ifError(err);
assert(2, teasers.length);
assert.equal(teasers[1].content, 'reply 1 content');
@ -1619,7 +1619,7 @@ describe('Topic\'s', function () {
});
it('should return empty array ', function (done) {
topics.getTeasersByTids([], function (err, teasers) {
topics.getTeasersByTids([], 1, function (err, teasers) {
assert.ifError(err);
assert.equal(0, teasers.length);
done();
@ -1627,7 +1627,7 @@ describe('Topic\'s', function () {
});
it('should get teaser by tid', function (done) {
topics.getTeaser(topic2.topicData.tid, function (err, teaser) {
topics.getTeaser(topic2.topicData.tid, 1, function (err, teaser) {
assert.ifError(err);
assert(teaser);
assert.equal(teaser.content, 'content 2');

@ -237,6 +237,76 @@ describe('User', function () {
done();
});
});
it('should search users by ip', function (done) {
User.create({ username: 'ipsearch' }, function (err, uid) {
assert.ifError(err);
db.sortedSetAdd('ip:1.1.1.1:uid', [1, 1], [testUid, uid], function (err) {
assert.ifError(err);
socketUser.search({ uid: testUid }, { query: '1.1.1.1', searchBy: 'ip' }, function (err, data) {
assert.ifError(err);
assert(Array.isArray(data.users));
assert.equal(data.users.length, 2);
done();
});
});
});
});
it('should return empty array if query is empty', function (done) {
socketUser.search({ uid: testUid }, { query: '' }, function (err, data) {
assert.ifError(err);
assert.equal(data.users.length, 0);
done();
});
});
it('should filter users', function (done) {
User.create({ username: 'ipsearch_filter' }, function (err, uid) {
assert.ifError(err);
User.setUserFields(uid, { banned: 1, flags: 10 }, function (err) {
assert.ifError(err);
socketUser.search({ uid: testUid }, {
query: 'ipsearch',
onlineOnly: true,
bannedOnly: true,
flaggedOnly: true,
}, function (err, data) {
assert.ifError(err);
assert.equal(data.users[0].username, 'ipsearch_filter');
done();
});
});
});
});
it('should sort results by username', function (done) {
async.waterfall([
function (next) {
User.create({ username: 'brian' }, next);
},
function (uid, next) {
User.create({ username: 'baris' }, next);
},
function (uid, next) {
User.create({ username: 'bzari' }, next);
},
function (uid, next) {
User.search({
uid: testUid,
query: 'b',
sortBy: 'username',
paginate: false,
}, next);
},
], function (err, data) {
assert.ifError(err);
assert.equal(data.users[0].username, 'baris');
assert.equal(data.users[1].username, 'brian');
assert.equal(data.users[2].username, 'bzari');
done();
});
});
});
describe('.delete()', function () {

Loading…
Cancel
Save