Merge commit 'f70d54a397b4eeb7a64abfb70cf2c0769734de07' into weekly

This commit is contained in:
NodeBB Misty
2016-07-04 16:00:19 -04:00
58 changed files with 812 additions and 517 deletions

View File

@@ -54,11 +54,11 @@
"nodebb-plugin-markdown": "6.0.0",
"nodebb-plugin-mentions": "1.1.2",
"nodebb-plugin-soundpack-default": "0.1.6",
"nodebb-plugin-spam-be-gone": "0.4.8",
"nodebb-plugin-spam-be-gone": "0.4.9",
"nodebb-rewards-essentials": "0.0.8",
"nodebb-theme-lavender": "3.0.13",
"nodebb-theme-persona": "4.1.3",
"nodebb-theme-vanilla": "5.1.0",
"nodebb-theme-persona": "4.1.4",
"nodebb-theme-vanilla": "5.1.1",
"nodebb-widget-essentials": "2.0.9",
"nodemailer": "2.0.0",
"nodemailer-sendmail-transport": "1.0.0",
@@ -117,4 +117,4 @@
"url": "https://github.com/barisusakli"
}
]
}
}

View File

@@ -4,7 +4,7 @@
"invite": "Pozvánka od %1",
"greeting_no_name": "Dobrý den",
"greeting_with_name": "Dobrý den %1",
"welcome.text1": "Děkujeme vám za registraci s %1!",
"welcome.text1": "Děkujeme vám za registraci na %1!",
"welcome.text2": "Pro úplnou aktivaci vašeho účtu potřebujeme ověřit vaší emailovou adresu.",
"welcome.text3": "Administrátor právě potvrdil vaší registraci. Nyní se můžete přihlásit jménem a heslem.",
"welcome.cta": "Klikněte zde pro potvrzení vaší emailové adresy",

View File

@@ -29,13 +29,13 @@
"header.popular": "Populární",
"header.users": "Uživatelé",
"header.groups": "Skupiny",
"header.chats": "Chats",
"header.chats": "Chaty",
"header.notifications": "Oznámení",
"header.search": "Hledat",
"header.profile": "Můj profil",
"header.navigation": "Navigace",
"notifications.loading": "Načítání upozornění",
"chats.loading": "Načítání grafů",
"chats.loading": "Načítání chatů",
"motd.welcome": "Vítejte na NodeBB, diskusní platforma buducnosti.",
"previouspage": "Předchozí stránka",
"nextpage": "Další stránka",

View File

@@ -1,6 +1,6 @@
{
"username-email": "Username / Email",
"username": "Username",
"username-email": "Uživatel / Email",
"username": "Uživatel",
"email": "Email",
"remember_me": "Zapamatovat si mě?",
"forgot_password": "Zapomněli jste heslo?",

View File

@@ -4,41 +4,41 @@
"chat.send": "Odeslat",
"chat.no_active": "Nemáte žádné aktivní konverzace.",
"chat.user_typing": "%1 píše ...",
"chat.user_has_messaged_you": "%1 has messaged you.",
"chat.see_all": "See all chats",
"chat.mark_all_read": "Mark all chats read",
"chat.no-messages": "Please select a recipient to view chat message history",
"chat.no-users-in-room": "No users in this room",
"chat.recent-chats": "Recent Chats",
"chat.user_has_messaged_you": "%1 Vám napsal.",
"chat.see_all": "Prohlédnout všechny chaty",
"chat.mark_all_read": "Označit vše jako přečtené",
"chat.no-messages": "Prosím vyberte příjemce k prohlédnutí historie zpráv.",
"chat.no-users-in-room": "Žádní uživatelé v místnosti.",
"chat.recent-chats": "Aktuální chaty",
"chat.contacts": "Kontakty",
"chat.message-history": "Historie zpráv",
"chat.pop-out": "Pop out chat",
"chat.pop-out": "Skrýt chat",
"chat.maximize": "Maximalizovat",
"chat.seven_days": "7 dní",
"chat.thirty_days": "30 dní",
"chat.three_months": "3 měsíce",
"chat.delete_message_confirm": "Jste si jisti že chcete odstranit tuto zprávu?",
"chat.roomname": "Chat Room %1",
"chat.add-users-to-room": "Add users to room",
"composer.compose": "Compose",
"composer.show_preview": "Show Preview",
"composer.hide_preview": "Hide Preview",
"composer.user_said_in": "%1 said in %2:",
"composer.user_said": "%1 said:",
"composer.discard": "Are you sure you wish to discard this post?",
"composer.submit_and_lock": "Submit and Lock",
"chat.roomname": "Místnost %1",
"chat.add-users-to-room": "Přidat uživatele do místnosti",
"composer.compose": "Napsat",
"composer.show_preview": "Ukázat náhled",
"composer.hide_preview": "Skrýt náhled",
"composer.user_said_in": "%1 řekl v %2:",
"composer.user_said": "%1 řekl:",
"composer.discard": "Jste si jisti, že chcete zrušit tento příspěvek?",
"composer.submit_and_lock": "Potvrdit a uzamknout",
"composer.toggle_dropdown": "Toggle Dropdown",
"composer.uploading": "Uploading %1",
"composer.formatting.bold": "Bold",
"composer.formatting.italic": "Italic",
"composer.formatting.list": "List",
"composer.formatting.strikethrough": "Strikethrough",
"composer.uploading": "Odesílám %1",
"composer.formatting.bold": "Tučné",
"composer.formatting.italic": "Kurzíva",
"composer.formatting.list": "Seznam",
"composer.formatting.strikethrough": "Přeškrtnutí",
"composer.formatting.link": "Odkaz",
"composer.formatting.picture": "Obrázek",
"composer.upload-picture": "Nahrát obrázek",
"composer.upload-file": "Nahrát soubor",
"bootbox.ok": "OK",
"bootbox.cancel": "Cancel",
"bootbox.cancel": "Zrušit",
"bootbox.confirm": "Potvrdit",
"cover.dragging_title": "Cover Photo Positioning",
"cover.dragging_message": "Drag the cover photo to the desired position and click \"Save\"",

View File

@@ -1,16 +1,16 @@
{
"title": "Upozornění",
"no_notifs": "You have no new notifications",
"see_all": "See all notifications",
"mark_all_read": "Mark all notifications read",
"back_to_home": "Back to %1",
"no_notifs": "Nemáte žádná nová upozornění.",
"see_all": "Zobrazit všechna upozornění",
"mark_all_read": "Označit všechna upozornění jako přečtená",
"back_to_home": "Zpět na %1",
"outgoing_link": "Odkaz mimo fórum",
"outgoing_link_message": "You are now leaving %1",
"continue_to": "Continue to %1",
"return_to": "Return to %1",
"new_notification": "New Notification",
"you_have_unread_notifications": "You have unread notifications.",
"new_message_from": "New message from <strong>%1</strong>",
"outgoing_link_message": "Opouštíte %1",
"continue_to": "Pokračovat na %1",
"return_to": "Vrátit na %1",
"new_notification": "Nové upozornění",
"you_have_unread_notifications": "Máte nepřečtená upozornění.",
"new_message_from": "Nová zpráva od <strong>%1</strong>",
"upvoted_your_post_in": "<strong>%1</strong> has upvoted your post in <strong>%2</strong>.",
"upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.",
"upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.",
@@ -31,8 +31,8 @@
"user_started_following_you_multiple": "<strong>%1</strong> and %2 others started following you.",
"new_register": "<strong>%1</strong> sent a registration request.",
"new_register_multiple": "There are <strong>%1</strong> registration requests awaiting review.",
"email-confirmed": "Email Confirmed",
"email-confirmed-message": "Thank you for validating your email. Your account is now fully activated.",
"email-confirm-error-message": "There was a problem validating your email address. Perhaps the code was invalid or has expired.",
"email-confirm-sent": "Confirmation email sent."
"email-confirmed": "Email potvrzen",
"email-confirmed-message": "Děkujeme za ověření Vaší emailové adresy. Váš účet je nyní aktivován.",
"email-confirm-error-message": "Nastal problém s ověřením Vaší emailové adresy. Pravděpodobně neplatný nebo expirovaný kód.",
"email-confirm-sent": "Ověřovací email odeslán."
}

View File

@@ -1,19 +1,19 @@
{
"home": "Home",
"unread": "Unread Topics",
"home": "Domů",
"unread": "Nepřečtená témata",
"popular-day": "Dnešní oblíbená témata",
"popular-week": "Oblíbená témata pro tento týden",
"popular-month": "Oblíbená témata pro tento měsíc",
"popular-alltime": "Oblíbená témata za celou dobu",
"recent": "Recent Topics",
"recent": "Aktuální témata",
"flagged-posts": "Označené příspěvky",
"users/online": "Uživatelé online",
"users/latest": "Nejnovější uživatelé",
"users/sort-posts": "Uživatelé s nejvíce příspěvky",
"users/sort-reputation": "Uživatelé s nejlepší reputací",
"users/banned": "Banned Users",
"users/banned": "Zabanovaní uživatelé",
"users/search": "Hledání uživatele",
"notifications": "Notifications",
"notifications": "Oznámení",
"tags": "Tagy",
"tag": "Téma označeno pod \"%1\"",
"register": "Zaregistrovat účet",
@@ -22,8 +22,8 @@
"categories": "Kategorie",
"groups": "Skupiny",
"group": "%1 skupina",
"chats": "Chats",
"chat": "Chatting with %1",
"chats": "Chaty",
"chat": "Chatovat s %1",
"account/edit": "Editing \"%1\"",
"account/edit/password": "Editing password of \"%1\"",
"account/edit/username": "Editing username of \"%1\"",

View File

@@ -11,7 +11,7 @@
"enter_email_address": "Zadejte emailovou adresu",
"password_reset_sent": "Obnova hesla odeslána",
"invalid_email": "Špatný email / Email neexistuje!",
"password_too_short": "The password entered is too short, please pick a different password.",
"passwords_do_not_match": "The two passwords you've entered do not match.",
"password_expired": "Your password has expired, please choose a new password"
"password_too_short": "Zadané heslo je příliš krátké, zvolte si prosím jiné.",
"passwords_do_not_match": "Vámi zadaná hesla se neshodují.",
"password_expired": "Platnost Vašeho hesla vypršela, zvolte si prosím nové."
}

View File

@@ -1,7 +1,7 @@
{
"no_tag_topics": "Není zde žádné téma s tímto tagem.",
"tags": "Tagy",
"enter_tags_here": "Enter tags here, between %1 and %2 characters each.",
"enter_tags_here": "Zde vložte tagy, každý o délce %1 až %2 znaků.",
"enter_tags_here_short": "Vložte tagy ...",
"no_tags": "Zatím tu není žádný tag."
}

View File

@@ -35,8 +35,8 @@
"ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.",
"login_to_subscribe": "Please register or log in in order to subscribe to this topic.",
"markAsUnreadForAll.success": "Topic marked as unread for all.",
"mark_unread": "Mark unread",
"mark_unread.success": "Topic marked as unread.",
"mark_unread": "Označ za nepřečtené",
"mark_unread.success": "Téma označeno jako nepřečtené",
"watch": "Sledovat",
"unwatch": "Unwatch",
"watch.title": "Be notified of new replies in this topic",
@@ -48,18 +48,18 @@
"watching.description": "Notify me of new replies.<br/>Show topic in unread.",
"not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.",
"ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.",
"thread_tools.title": "Topic Tools",
"thread_tools.title": "Správa tématu",
"thread_tools.markAsUnreadForAll": "Označit jako nepřečtené",
"thread_tools.pin": "Pin Topic",
"thread_tools.unpin": "Unpin Topic",
"thread_tools.lock": "Lock Topic",
"thread_tools.pin": "Připnout téma",
"thread_tools.unpin": "Odepnout téma",
"thread_tools.lock": "Zamknout téma",
"thread_tools.unlock": "Odemknout téma",
"thread_tools.move": "Přesunout téma",
"thread_tools.move_all": "Přesunout vše",
"thread_tools.fork": "Fork Topic",
"thread_tools.fork": "Větvit téma",
"thread_tools.delete": "Odstranit téma",
"thread_tools.delete-posts": "Odstranit přispěvky",
"thread_tools.delete_confirm": "Are you sure you want to delete this topic?",
"thread_tools.delete_confirm": "Opravdu chcete smazat toto téma.",
"thread_tools.restore": "Obnovit téma",
"thread_tools.restore_confirm": "Are you sure you want to restore this topic?",
"thread_tools.purge": "Purge Topic",
@@ -74,7 +74,7 @@
"confirm_fork": "Rozdělit",
"favourite": "Záložka",
"favourites": "Záložky",
"favourites.has_no_favourites": "You haven't bookmarked any posts yet.",
"favourites.has_no_favourites": "Zatím jste do záložek nepřidal žádné příspěvky.",
"loading_more_posts": "Načítání více příspěvků",
"move_topic": "Přesunout téma",
"move_topics": "Přesunout témata",
@@ -88,16 +88,16 @@
"delete_posts_instruction": "Click the posts you want to delete/purge",
"composer.title_placeholder": "Zadejte název tématu...",
"composer.handle_placeholder": "Jméno",
"composer.discard": "Discard",
"composer.discard": "Zrušit",
"composer.submit": "Odeslat",
"composer.replying_to": "Replying to %1",
"composer.new_topic": "Nové téma",
"composer.uploading": "nahrávání...",
"composer.thumb_url_label": "Paste a topic thumbnail URL",
"composer.thumb_title": "Add a thumbnail to this topic",
"composer.thumb_url_label": "Vložit URL náhled tématu",
"composer.thumb_title": "Přidat k tématu náhled",
"composer.thumb_url_placeholder": "http://example.com/thumb.png",
"composer.thumb_file_label": "Nebo nahrajte soubor",
"composer.thumb_remove": "Clear fields",
"composer.thumb_remove": "Vymazat pole",
"composer.drag_and_drop_images": "Drag and Drop Images Here",
"more_users_and_guests": "%1 more user(s) and %2 guest(s)",
"more_users": "%1 more user(s)",
@@ -107,13 +107,13 @@
"oldest_to_newest": "Od nejstarších po nejnovější",
"newest_to_oldest": "Od nejnovějších po nejstarší",
"most_votes": "Nejvíce hlasů",
"most_posts": "Most posts",
"stale.title": "Create new topic instead?",
"stale.warning": "The topic you are replying to is quite old. Would you like to create a new topic instead, and reference this one in your reply?",
"stale.create": "Create a new topic",
"stale.reply_anyway": "Reply to this topic anyway",
"most_posts": "Nejvíce příspěvků",
"stale.title": "Přesto vytvořit nové téma",
"stale.warning": "Reagujete na starší téma. Nechcete raději vytvořit téma nové a na původní v něm odkázat?",
"stale.create": "Vytvořit nové téma",
"stale.reply_anyway": "Přesto reagovat na toto téma",
"link_back": "Re: [%1](%2)",
"spam": "Spam",
"offensive": "Offensive",
"custom-flag-reason": "Enter a flagging reason"
"offensive": "Urážlivé",
"custom-flag-reason": "Vložte důvod oznámení"
}

View File

@@ -2,10 +2,10 @@
"title": "Nepřečtené",
"no_unread_topics": "Nejsou zde žádné nepřečtené témata.",
"load_more": "Načíst další",
"mark_as_read": "Označit jako přeštené",
"mark_as_read": "Označit jako přečtené",
"selected": "Vybrané",
"all": "Vše",
"all_categories": "All categories",
"all_categories": "Všechny kategorie",
"topics_marked_as_read.success": "Téma bylo označeno jako přečtené!",
"all-topics": "Všechna témata",
"new-topics": "Nová témata",

View File

@@ -30,7 +30,7 @@
"signature": "Podpis",
"birthday": "Datum narození",
"chat": "Chat",
"chat_with": "Chat with %1",
"chat_with": "Chatovat s %1",
"follow": "Sledovat",
"unfollow": "Nesledovat",
"more": "Více",
@@ -64,7 +64,7 @@
"settings": "Nastavení",
"show_email": "Zobrazovat můj email v profilu",
"show_fullname": "Zobrazovat celé jméno",
"restrict_chats": "Only allow chat messages from users I follow",
"restrict_chats": "Povolit chatovací zprávy pouze od uživatelů, které sleduji.",
"digest_label": "Odebírat přehled",
"digest_description": "Subscribe to email updates for this forum (new notifications and topics) according to a set schedule",
"digest_off": "Vypnuto",

View File

@@ -5,7 +5,7 @@
"search": "Vyhledávat",
"enter_username": "Zadej uživatelské jméno k hledání",
"load_more": "Načíst další",
"users-found-search-took": "%1 user(s) found! Search took %2 seconds.",
"users-found-search-took": "Nalezeno %1 uživatel(ů) za %2 vteřiny.",
"filter-by": "Filtrovat dle",
"online-only": "Pouze online",
"invite": "Pozvat",

View File

@@ -37,6 +37,7 @@
"user-banned": "User banned",
"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",
"no-category": "Category does not exist",
"no-topic": "Topic does not exist",
@@ -57,6 +58,13 @@
"post-edit-duration-expired-days": "You are only allowed to edit posts for %1 day(s) after posting",
"post-edit-duration-expired-days-hours": "You are only allowed to edit posts for %1 day(s) %2 hour(s) after posting",
"post-delete-duration-expired": "You are only allowed to delete posts for %1 second(s) after posting",
"post-delete-duration-expired-minutes": "You are only allowed to delete posts for %1 minute(s) after posting",
"post-delete-duration-expired-minutes-seconds": "You are only allowed to delete posts for %1 minute(s) %2 second(s) after posting",
"post-delete-duration-expired-hours": "You are only allowed to delete posts for %1 hour(s) after posting",
"post-delete-duration-expired-hours-minutes": "You are only allowed to delete posts for %1 hour(s) %2 minute(s) after posting",
"post-delete-duration-expired-days": "You are only allowed to delete posts for %1 day(s) after posting",
"post-delete-duration-expired-days-hours": "You are only allowed to delete posts for %1 day(s) %2 hour(s) after posting",
"content-too-short": "Please enter a longer post. Posts should contain at least %1 character(s).",
"content-too-long": "Please enter a shorter post. Posts can't be longer than %1 character(s).",

View File

@@ -13,6 +13,7 @@
"users/sort-posts": "Users with the most posts",
"users/sort-reputation": "Users with the most reputation",
"users/banned": "Banned Users",
"users/most-flags": "Most flagged users",
"users/search": "User Search",
"notifications": "Notifications",

View File

@@ -2,6 +2,7 @@
"latest_users": "Latest Users",
"top_posters": "Top Posters",
"most_reputation": "Most Reputation",
"most_flags": "Most Flags",
"search": "Search",
"enter_username": "Enter a username to search",
"load_more": "Load More",

View File

@@ -92,7 +92,7 @@
"invalid-chat-message": "پیام نامعتبر",
"chat-message-too-long": "پیام طولانی تر از حد مجاز است",
"cant-edit-chat-message": "شما اجازه ی ویرایش این پیام را ندارید",
"cant-remove-last-user": "You can't remove the last user",
"cant-remove-last-user": "شما نمی توانید آخرین کاربر را حذف کنید",
"cant-delete-chat-message": "شما اجازه حذف این پیام را ندارید.",
"already-voting-for-this-post": "شما قبلا به این پست رای داده اید.",
"reputation-system-disabled": "سیستم اعتبار غیر فعال شده است",
@@ -106,7 +106,7 @@
"wrong-login-type-email": "لطفا از ایمیل خود برای ورود استفاده کنید",
"wrong-login-type-username": "لطفا از نام کاربری خود برای ورود استفاده کنید",
"invite-maximum-met": "You have invited the maximum amount of people (%1 out of %2).",
"no-session-found": "No login session found!",
"no-session-found": "هیچ سشن ورودی یافت نشد!",
"not-in-room": "هیچ کاربری در این گفتگو نیست",
"no-users-in-room": "هیچ کاربری در این گفتگو نیست",
"cant-kick-self": "شما نمی توانید خودتان را از گروه کیک کنید"

View File

@@ -50,8 +50,8 @@
"topics": "موضوع ها",
"posts": "دیدگاه‌ها",
"best": "بهترین",
"upvoted": "Upvoted",
"downvoted": "Downvoted",
"upvoted": "رای مثبت",
"downvoted": "رای منفی",
"views": "بازدیدها",
"reputation": "اعتبار",
"read_more": "بیشتر بخوانید",

View File

@@ -12,8 +12,8 @@
"you_have_unread_notifications": "شما اطلاعیه های نخوانده دارید.",
"new_message_from": "پیام تازه از <strong>%1</strong>",
"upvoted_your_post_in": "<strong>%1</strong> امتیاز مثبت به پست شما در <strong>%2</strong> داده",
"upvoted_your_post_in_dual": "<strong>%1</strong> and <strong>%2</strong> have upvoted your post in <strong>%3</strong>.",
"upvoted_your_post_in_multiple": "<strong>%1</strong> and %2 others have upvoted your post in <strong>%3</strong>.",
"upvoted_your_post_in_dual": "<strong>%1</strong> و <strong>%2</strong> رای مثبت به پست شما در\n <strong>%3</strong>.",
"upvoted_your_post_in_multiple": "<strong>%1</strong>و %2 دیگران به پست شما رای مثبت دادن در <strong>%3</strong>.",
"moved_your_post": "<strong>%1</strong> پست شما را به <strong>%2</strong> انتقال داده است",
"moved_your_topic": "<strong>%2</strong> <strong>%1</strong> را منتقل کرده است",
"favourited_your_post_in": "<strong>%1</strong> has bookmarked your post in <strong>%2</strong>.",

View File

@@ -6,7 +6,7 @@
"popular-month": "موضوعات پربازدید این ماه",
"popular-alltime": "پربازدیدترین موضوعات",
"recent": "جستارهای تازه",
"flagged-posts": "Flagged Posts",
"flagged-posts": "پست نشانه گذاری شده",
"users/online": "کاربران آنلاین",
"users/latest": "آخرین کاربران",
"users/sort-posts": "کاربران با بیش‌ترین پست",
@@ -33,12 +33,12 @@
"account/posts": "پست‌های %1",
"account/topics": "موضوع های %1",
"account/groups": "گروه‌های %1",
"account/favourites": "%1's Bookmarked Posts",
"account/favourites": "%1 پست های به علاقمندی اضافه شدن",
"account/settings": "تنظیمات کاربر",
"account/watched": "موضوع های دیده شده توسط \"%1\"",
"account/upvoted": "Posts upvoted by %1",
"account/downvoted": "Posts downvoted by %1",
"account/best": "Best posts made by %1",
"account/upvoted": "رای مثبت داده شده به پست ها توسط %1",
"account/downvoted": "رای منفی داده شده به پست ها توسط %1",
"account/best": "بهترین پست های ارسال شده توسط %1",
"confirm": "ایمیل تایید شد",
"maintenance.text": "%1 در حال حاضر تحت تعمیر و نگهدارییست. لطفا زمان دیگری مراجعه کنید.",
"maintenance.messageIntro": "علاوه بر این، مدیر این پیام را گذاشته است:",

View File

@@ -26,28 +26,28 @@
"tools": "ابزارها",
"flag": "پرچم",
"locked": "قفل شده است",
"bookmark_instructions": "Click here to return to the last read post in this thread.",
"bookmark_instructions": "برای بازگشت به آخرین پست در این موضوع اینجا را کلیک کنید.",
"flag_title": "پرچم‌گذاری این موضوع برای بررسی ناظران",
"flag_success": "این موضوع برای بررسی ناظران پرچم گذاشته شد.",
"deleted_message": "این موضوع پاک شده است. تنها کاربرانِ با حق مدیریت موضوع می‌توانند آن را ببینند.",
"following_topic.message": "از این پس اگر کسی در این موضوع پست بگذارد، شما آگاه خواهید شد.",
"not_following_topic.message": "شما دیگر اطلاعیه های این موضوع را دریافت نخواهید کرد.",
"ignoring_topic.message": "You will no longer see this topic in the unread topics list. You will be notified when you are mentioned or your post is up voted.",
"ignoring_topic.message": "شما دیگر نمی خواهید این تاپیک را در فهرست عنوان های خوانده نشده ببینید. به شما اطلاع داده خواهد شد زمانی که به پست شما کسی رای بدهد.",
"login_to_subscribe": "برای دنبال کردن این موضوع، لطفا ثبت نام کنید و یا با نام کاربری خود وارد شوید",
"markAsUnreadForAll.success": "موضوع برای همگان نخوانده در نظر گرفته شد.",
"mark_unread": "Mark unread",
"mark_unread.success": "Topic marked as unread.",
"mark_unread": "علامت بزن به عنوان خوانده نشده",
"mark_unread.success": "تاپیک را علامت بزن به عنوان خوانده نشده",
"watch": "پیگیری",
"unwatch": "عدم پیگیری",
"watch.title": "از پاسخ‌های تازه به این موضوع آگاه شوید.",
"unwatch.title": "توقف پیگیری این موضوع",
"share_this_post": "به اشتراک‌گذاری این موضوع",
"watching": "Watching",
"not-watching": "Not Watching",
"ignoring": "Ignoring",
"watching.description": "Notify me of new replies.<br/>Show topic in unread.",
"not-watching.description": "Do not notify me of new replies.<br/>Show topic in unread if category is not ignored.",
"ignoring.description": "Do not notify me of new replies.<br/>Do not show topic in unread.",
"watching": "درحال پیگیری",
"not-watching": "درحال پیگیری نیستید",
"ignoring": "نادیده گرفتن",
"watching.description": "به من اطلاع بده برای پاسخ های جدید.<br/>نشان بده تاپیک های خوانده نشده را.",
"not-watching.description": "به من پس از ارسال هر پاسخی جدیدی اطلاع نده.<br/>تاپیک به صورت خوانده نشده قرار بگیرد ولی نادیده گرفته نشود.",
"ignoring.description": "به من پس از ارسال هر پاسخی جدیدی اطلاع نده.<br/>دیگر تاپیک را به صورت خوانده نشده نشان نده.",
"thread_tools.title": "ابزارهای موضوع",
"thread_tools.markAsUnreadForAll": "نخوانده بگیر",
"thread_tools.pin": "سنجاق زدن موضوع",
@@ -72,9 +72,9 @@
"disabled_categories_note": "دسته‌های از کار افتاده به رنگ خاکستری در می‌آیند",
"confirm_move": "جابه‌جا کردن",
"confirm_fork": "شاخه ساختن",
"favourite": "Bookmark",
"favourite": "علاقمندی",
"favourites": "علاقمندی ها",
"favourites.has_no_favourites": "You haven't bookmarked any posts yet.",
"favourites.has_no_favourites": "شما هیچ پستی را به صورت علاقمندی ندارید.",
"loading_more_posts": "بارگذاری پست‌های بیش‌تر",
"move_topic": "جابه‌جایی موضوع",
"move_topics": "انتقال موضوع",
@@ -85,7 +85,7 @@
"fork_topic_instruction": "پست‌هایی را که می‌خواهید به موضوع تازه ببرید، انتخاب کنید",
"fork_no_pids": "هیچ پستی انتخاب نشده!",
"fork_success": "موضوع با موفقیت منشعب شد! برای رفتن به موضوع انشعابی اینجا را کلیک کنید.",
"delete_posts_instruction": "Click the posts you want to delete/purge",
"delete_posts_instruction": "با کلیک بر روی پست شما می خواهید به حذف/پاکسازی",
"composer.title_placeholder": "عنوان موضوعتان را اینجا بنویسید...",
"composer.handle_placeholder": "نام",
"composer.discard": "دور بیانداز",

View File

@@ -80,7 +80,7 @@
"has_no_topics": "این کاربر تا به حال هیچ موضوعی ارسال نکرده است",
"has_no_watched_topics": "این کاربر تا به حال هیچ موضوعی را پیگیری نکرده است",
"has_no_upvoted_posts": "این کاربر به هیچ پستی امتیاز نداده است.",
"has_no_downvoted_posts": "This user hasn't downvoted any posts yet.",
"has_no_downvoted_posts": "این کاربر به هیچ پستی رای منفی نداده است.",
"has_no_voted_posts": "این کاربر به پست رای نداده است",
"email_hidden": "ایمیل پنهان شده",
"hidden": "پنهان",
@@ -93,7 +93,7 @@
"enable_topic_searching": "فعال کردن جستجوی داخل-موضوع",
"topic_search_help": "If enabled, in-topic searching will override the browser's default page search behaviour and allow you to search through the entire topic, instead of what is only shown on screen",
"delay_image_loading": "تاخیر در حال بارگذاری عکس",
"image_load_delay_help": "If enabled, images in topics will not load until they are scrolled into view",
"image_load_delay_help": "اگر فعال باشد، تصاویر در تاپیک ها بارگذاری نمی شود تا زمانی که از طریق اسکرول روی آن بروید قابل مشاهده است",
"scroll_to_my_post": "پس از ارسال پست، اولین پست جدید نشان بده",
"follow_topics_you_reply_to": "تاپیک هایی که پاسخ داده ای را دنبال کن",
"follow_topics_you_create": "موضوع هایی که ایجاد کرده ای را دنبال کن",

View File

@@ -16,5 +16,5 @@
"unread_topics": "موضوع های خوانده نشده",
"categories": "دسته ها",
"tags": "برچسب‌ها",
"no-users-found": "No users found!"
"no-users-found": "کاربری پیدا نشد!"
}

View File

@@ -63,7 +63,7 @@
"already-unfavourited": "您已取消了此贴的书签",
"cant-ban-other-admins": "您不能封禁其他管理员!",
"cant-remove-last-admin": "您是唯一的管理员。在删除您的管理员权限前,请添加另一个管理员。",
"cant-delete-admin": "Remove administrator privileges from this account before attempting to delete it.",
"cant-delete-admin": "在删除之前请你先把管理员的权限从这个账号移除。",
"invalid-image-type": "无效的图像类型。允许的类型有:%1",
"invalid-image-extension": "无效的图像扩展",
"invalid-file-type": "无效文件格式,允许的格式有:%1",

View File

@@ -1,11 +1,12 @@
"use strict";
/*global define, socket, app, admin, utils, bootbox, RELATIVE_PATH*/
/*global define, socket, app, utils, bootbox, ajaxify*/
define('admin/manage/flags', [
'forum/infinitescroll',
'admin/modules/selectable',
'autocomplete'
], function(infinitescroll, selectable, autocomplete) {
'autocomplete',
'Chart'
], function(infinitescroll, selectable, autocomplete, Chart) {
var Flags = {};
@@ -20,6 +21,7 @@ define('admin/manage/flags', [
handleDismissAll();
handleDelete();
handleInfiniteScroll();
handleGraphs();
};
function handleDismiss() {
@@ -101,5 +103,42 @@ define('admin/manage/flags', [
});
}
function handleGraphs() {
var dailyCanvas = document.getElementById('flags:daily');
var dailyLabels = utils.getDaysArray().map(function(text, idx) {
return idx % 3 ? '' : text;
});
if (utils.isMobile()) {
Chart.defaults.global.showTooltips = false;
}
var data = {
'flags:daily': {
labels: dailyLabels,
datasets: [
{
label: "",
fillColor: "rgba(151,187,205,0.2)",
strokeColor: "rgba(151,187,205,1)",
pointColor: "rgba(151,187,205,1)",
pointStrokeColor: "#fff",
pointHighlightFill: "#fff",
pointHighlightStroke: "rgba(151,187,205,1)",
data: ajaxify.data.analytics
}
]
}
};
dailyCanvas.width = $(dailyCanvas).parent().width();
new Chart(dailyCanvas.getContext('2d')).Line(data['flags:daily'], {
responsive: true,
animation: false
});
}
return Flags;
});

View File

@@ -96,12 +96,14 @@ define('forum/topic/events', [
if (!data || !data.post) {
return;
}
var editedPostEl = components.get('post/content', data.post.pid),
editorEl = $('[data-pid="' + data.post.pid + '"] [component="post/editor"]'),
topicTitle = components.get('topic/title'),
breadCrumb = components.get('breadcrumb/current');
var editedPostEl = components.get('post/content', data.post.pid);
var editorEl = $('[data-pid="' + data.post.pid + '"] [component="post/editor"]');
var topicTitle = components.get('topic/title');
var navbarTitle = components.get('navbar/title').find('span');
var breadCrumb = components.get('breadcrumb/current');
if (topicTitle.length && data.topic.title && topicTitle.html() !== data.topic.title) {
ajaxify.data.title = data.topic.title;
var newUrl = 'topic/' + data.topic.slug + (window.location.search ? window.location.search : '');
history.replaceState({url: newUrl}, null, window.location.protocol + '//' + window.location.host + config.relative_path + '/' + newUrl);
@@ -111,6 +113,9 @@ define('forum/topic/events', [
breadCrumb.fadeOut(250, function() {
breadCrumb.html(data.topic.title).fadeIn(250);
});
navbarTitle.fadeOut(250, function() {
navbarTitle.html(data.topic.title).fadeIn(250);
});
}
editedPostEl.fadeOut(250, function() {

View File

@@ -6,7 +6,11 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
var PostTools = {};
var staleReplyAnyway = false;
PostTools.init = function(tid) {
staleReplyAnyway = false;
renderMenu();
addPostHandlers(tid);
@@ -171,43 +175,55 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
var timestamp = parseInt(getData(btn, 'data-timestamp'), 10);
var postEditDuration = parseInt(ajaxify.data.postEditDuration, 10);
if (!ajaxify.data.privileges.isAdminOrMod && postEditDuration && Date.now() - timestamp > postEditDuration * 1000) {
var numDays = Math.floor(postEditDuration / 86400);
var numHours = Math.floor((postEditDuration % 86400) / 3600);
var numMinutes = Math.floor(((postEditDuration % 86400) % 3600) / 60);
var numSeconds = ((postEditDuration % 86400) % 3600) % 60;
var msg = '[[error:post-edit-duration-expired, ' + postEditDuration + ']]';
if (numDays) {
if (numHours) {
msg = '[[error:post-edit-duration-expired-days-hours, ' + numDays + ', ' + numHours + ']]';
} else {
msg = '[[error:post-edit-duration-expired-days, ' + numDays + ']]';
}
} else if (numHours) {
if (numMinutes) {
msg = '[[error:post-edit-duration-expired-hours-minutes, ' + numHours + ', ' + numMinutes + ']]';
} else {
msg = '[[error:post-edit-duration-expired-hours, ' + numHours + ']]';
}
} else if (numMinutes) {
if (numSeconds) {
msg = '[[error:post-edit-duration-expired-minutes-seconds, ' + numMinutes + ', ' + numSeconds + ']]';
} else {
msg = '[[error:post-edit-duration-expired-minutes, ' + numMinutes + ']]';
}
}
return app.alertError(msg);
}
$(window).trigger('action:composer.post.edit', {
pid: getData(btn, 'data-pid')
});
if (checkDuration(postEditDuration, timestamp, 'post-edit-duration-expired')) {
$(window).trigger('action:composer.post.edit', {
pid: getData(btn, 'data-pid')
});
}
});
postContainer.on('click', '[component="post/delete"]', function() {
togglePostDelete($(this), tid);
var btn = $(this);
var timestamp = parseInt(getData(btn, 'data-timestamp'), 10);
var postDeleteDuration = parseInt(ajaxify.data.postDeleteDuration, 10);
if (checkDuration(postDeleteDuration, timestamp, 'post-delete-duration-expired')) {
togglePostDelete($(this), tid);
}
});
function checkDuration(duration, postTimestamp, languageKey) {
if (!ajaxify.data.privileges.isAdminOrMod && duration && Date.now() - postTimestamp > duration * 1000) {
var numDays = Math.floor(duration / 86400);
var numHours = Math.floor((duration % 86400) / 3600);
var numMinutes = Math.floor(((duration % 86400) % 3600) / 60);
var numSeconds = ((duration % 86400) % 3600) % 60;
var msg = '[[error:' + languageKey + ', ' + duration + ']]';
if (numDays) {
if (numHours) {
msg = '[[error:' + languageKey + '-days-hours, ' + numDays + ', ' + numHours + ']]';
} else {
msg = '[[error:' + languageKey + '-days, ' + numDays + ']]';
}
} else if (numHours) {
if (numMinutes) {
msg = '[[error:' + languageKey + '-hours-minutes, ' + numHours + ', ' + numMinutes + ']]';
} else {
msg = '[[error:' + languageKey + '-hours, ' + numHours + ']]';
}
} else if (numMinutes) {
if (numSeconds) {
msg = '[[error:' + languageKey + '-minutes-seconds, ' + numMinutes + ', ' + numSeconds + ']]';
} else {
msg = '[[error:' + languageKey + '-minutes, ' + numMinutes + ']]';
}
}
app.alertError(msg);
return false;
}
return true;
}
postContainer.on('click', '[component="post/restore"]', function() {
togglePostDelete($(this), tid);
});
@@ -226,9 +242,9 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
}
function onReplyClicked(button, tid) {
showStaleWarning(function() {
var selectedText = getSelectedText(button);
var selectedText = getSelectedText(button);
showStaleWarning(function() {
var username = getUserName(button);
if (getData(button, 'data-uid') === '0' || !getData(button, 'data-userslug')) {
username = '';
@@ -258,6 +274,8 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
}
function onQuoteClicked(button, tid) {
var selectedText = getSelectedText(button);
showStaleWarning(function() {
function quote(text) {
@@ -274,7 +292,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
var username = getUserName(button);
var pid = getData(button, 'data-pid');
var selectedText = getSelectedText(button);
if (selectedText) {
return quote(selectedText);
}
@@ -394,9 +412,9 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
}
function togglePostDelete(button, tid) {
var pid = getData(button, 'data-pid'),
postEl = components.get('post', 'pid', pid),
action = !postEl.hasClass('deleted') ? 'delete' : 'restore';
var pid = getData(button, 'data-pid');
var postEl = components.get('post', 'pid', pid);
var action = !postEl.hasClass('deleted') ? 'delete' : 'restore';
postAction(action, pid, tid);
}
@@ -494,7 +512,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
}
function showStaleWarning(callback) {
if (ajaxify.data.lastposttime >= (Date.now() - (1000 * 60 * 60 * 24 * ajaxify.data.topicStaleDays))) {
if (staleReplyAnyway || ajaxify.data.lastposttime >= (Date.now() - (1000 * 60 * 60 * 24 * ajaxify.data.topicStaleDays))) {
return callback();
}
@@ -507,6 +525,7 @@ define('forum/topic/postTools', ['share', 'navigator', 'components', 'translator
label: '[[topic:stale.reply_anyway]]',
className: 'btn-link',
callback: function() {
staleReplyAnyway = true;
callback();
}
},

View File

@@ -67,6 +67,7 @@ define('forum/users', ['translator'], function(translator) {
translated = $(translated);
$('#users-container').append(translated);
translated.find('span.timeago').timeago();
utils.addCommasToNumbers(translated.find('.formatted-number'));
$('#users-container .anon-user').appendTo($('#users-container'));
});
});
@@ -101,14 +102,15 @@ define('forum/users', ['translator'], function(translator) {
if (!username) {
return loadPage(page);
}
var activeSection = getActiveSection();
socket.emit('user.search', {
query: username,
page: page,
searchBy: 'username',
sortBy: $('.search select').val() || getSortBy(),
onlineOnly: $('.search .online-only').is(':checked') || (getActiveSection() === 'online'),
bannedOnly: getActiveSection() === 'banned'
onlineOnly: $('.search .online-only').is(':checked') || (activeSection === 'online'),
bannedOnly: activeSection === 'banned',
flaggedOnly: activeSection === 'flagged'
}, function(err, data) {
if (err) {
return app.alertError(err.message);
@@ -167,8 +169,8 @@ define('forum/users', ['translator'], function(translator) {
}
function getActiveSection() {
var url = window.location.href,
parts = url.split('/');
var url = window.location.href;
var parts = url.split('/');
return parts[parts.length - 1];
}

View File

@@ -2,6 +2,7 @@
var async = require('async');
var posts = require('../../posts');
var analytics = require('../../analytics');
var flagsController = {};
@@ -13,20 +14,28 @@ flagsController.get = function(req, res, next) {
async.waterfall([
function (next) {
if (byUsername) {
posts.getUserFlags(byUsername, sortBy, req.uid, start, stop, next);
} else {
var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged';
posts.getFlags(set, req.uid, start, stop, next);
}
async.parallel({
posts: function(next) {
if (byUsername) {
posts.getUserFlags(byUsername, sortBy, req.uid, start, stop, next);
} else {
var set = sortBy === 'count' ? 'posts:flags:count' : 'posts:flagged';
posts.getFlags(set, req.uid, start, stop, next);
}
},
analytics: function(next) {
analytics.getDailyStatsForSet('analytics:flags', Date.now(), 30, next);
}
}, next);
}
], function (err, posts) {
], function (err, results) {
if (err) {
return next(err);
}
var data = {
posts: posts,
next: stop + 1,
posts: results.posts,
analytics: results.analytics,
next: stop + 1,
byUsername: byUsername,
title: '[[pages:flagged-posts]]'
};

View File

@@ -28,6 +28,10 @@ usersController.noPosts = function(req, res, next) {
getUsersByScore('users:postcount', 'noposts', 0, 0, req, res, next);
};
usersController.flagged = function(req, res, next) {
getUsersByScore('users:flags', 'mostflags', 1, '+inf', req, res, next);
};
usersController.inactive = function(req, res, next) {
var timeRange = 1000 * 60 * 60 * 24 * 30 * (parseInt(req.query.months, 10) || 3);
var cutoff = Date.now() - timeRange;

View File

@@ -129,47 +129,93 @@ apiController.renderWidgets = function(req, res, next) {
});
};
apiController.getObject = function(req, res, next) {
apiController.getObjectByType(req.uid, req.params.type, req.params.id, function(err, results) {
if (err) {
return next(err);
apiController.getPostData = function(pid, uid, callback) {
async.parallel({
privileges: function(next) {
privileges.posts.get([pid], uid, next);
},
post: function(next) {
posts.getPostData(pid, next);
}
}, function(err, results) {
if (err || !results.post) {
return callback(err);
}
res.json(results);
var post = results.post;
var privileges = results.privileges[0];
if (!privileges.read || !privileges['topics:read']) {
return callback();
}
post.ip = privileges.isAdminOrMod ? post.ip : undefined;
var selfPost = uid && uid === parseInt(post.uid, 10);
if (post.deleted && !(privileges.isAdminOrMod || selfPost)) {
post.content = '[[topic:post_is_deleted]]';
}
callback(null, post);
});
};
apiController.getObjectByType = function(uid, type, id, callback) {
apiController.getTopicData = function(tid, uid, callback) {
async.parallel({
privileges: function(next) {
privileges.topics.get(tid, uid, next);
},
topic: function(next) {
topics.getTopicData(tid, next);
}
}, function(err, results) {
if (err || !results.topic) {
return callback(err);
}
if (!results.privileges.read || !results.privileges['topics:read'] || (parseInt(results.topic.deleted, 10) && !results.privileges.view_deleted)) {
return callback();
}
callback(null, results.topic);
});
};
apiController.getCategoryData = function(cid, uid, callback) {
async.parallel({
privileges: function(next) {
privileges.categories.get(cid, uid, next);
},
category: function(next) {
categories.getCategoryData(cid, next);
}
}, function(err, results) {
if (err || !results.category) {
return callback(err);
}
if (!results.privileges.read) {
return callback();
}
callback(null, results.category);
});
};
apiController.getObject = function(req, res, next) {
var methods = {
post: {
canRead: privileges.posts.can,
data: posts.getPostData
},
topic: {
canRead: privileges.topics.can,
data: topics.getTopicData
},
category: {
canRead: privileges.categories.can,
data: categories.getCategoryData
}
post: apiController.getPostData,
topic: apiController.getTopicData,
category: apiController.getCategoryData
};
if (!methods[type]) {
return callback();
var method = methods[req.params.type];
if (!method) {
return next();
}
async.waterfall([
function (next) {
methods[type].canRead('read', id, uid, next);
},
function (canRead, next) {
if (!canRead) {
return next(new Error('[[error:no-privileges]]'));
}
methods[type].data(id, next);
method(req.params.id, req.uid, function(err, result) {
if (err || !result) {
return next(err);
}
], callback);
res.json(result);
});
};
apiController.getUserByUID = function(req, res, next) {

View File

@@ -334,10 +334,13 @@ authenticationController.localLogin = function(req, username, password, next) {
function (next) {
async.parallel({
userData: function(next) {
db.getObjectFields('user:' + uid, ['password', 'banned', 'passwordExpiry'], next);
db.getObjectFields('user:' + uid, ['password', 'passwordExpiry'], next);
},
isAdmin: function(next) {
user.isAdministrator(uid, next);
},
banned: function(next) {
user.isBanned(uid, next);
}
}, next);
},
@@ -349,13 +352,13 @@ authenticationController.localLogin = function(req, username, password, next) {
if (!result.isAdmin && parseInt(meta.config.allowLocalLogin, 10) === 0) {
return next(new Error('[[error:local-login-disabled]]'));
}
if (!userData || !userData.password) {
return next(new Error('[[error:invalid-user-data]]'));
}
if (userData.banned && parseInt(userData.banned, 10) === 1) {
if (result.banned) {
return next(new Error('[[error:user-banned]]'));
}
Password.compare(password, userData.password, next);
},
function (passwordMatch, next) {

View File

@@ -264,7 +264,8 @@ topicsController.get = function(req, res, callback) {
data['downvote:disabled'] = parseInt(meta.config['downvote:disabled'], 10) === 1;
data['feeds:disableRSS'] = parseInt(meta.config['feeds:disableRSS'], 10) === 1;
data.bookmarkThreshold = parseInt(meta.config.bookmarkThreshold, 10) || 5;
data.postEditDuration = parseInt(meta.config.postEditDuration, 10);
data.postEditDuration = parseInt(meta.config.postEditDuration, 10) || 0;
data.postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10) || 0;
data.scrollToMyPost = settings.scrollToMyPost;
data.rssFeedUrl = nconf.get('relative_path') + '/topic/' + data.tid + '.rss';
data.pagination = pagination.create(currentPage, pageCount);

View File

@@ -69,6 +69,20 @@ usersController.getBannedUsers = function(req, res, next) {
});
};
usersController.getFlaggedUsers = function(req, res, next) {
usersController.getUsers('users:flags', req.uid, req.query.page, function(err, userData) {
if (err) {
return next(err);
}
if (!userData.isAdminOrGlobalMod) {
return next();
}
render(req, res, userData, next);
});
};
usersController.renderUsersPage = function(set, req, res, next) {
usersController.getUsers(set, req.uid, req.query.page, function(err, userData) {
if (err) {
@@ -79,23 +93,16 @@ usersController.renderUsersPage = function(set, req, res, next) {
};
usersController.getUsers = function(set, uid, page, callback) {
var setToTitles = {
'users:postcount': '[[pages:users/sort-posts]]',
'users:reputation': '[[pages:users/sort-reputation]]',
'users:joindate': '[[pages:users/latest]]',
'users:online': '[[pages:users/online]]',
'users:banned': '[[pages:users/banned]]'
var setToData = {
'users:postcount': {title: '[[pages:users/sort-posts]]', crumb: '[[users:top_posters]]'},
'users:reputation': {title: '[[pages:users/sort-reputation]]', crumb: '[[users:most_reputation]]'},
'users:joindate': {title: '[[pages:users/latest]]', crumb: '[[global:users]]'},
'users:online': {title: '[[pages:users/online]]', crumb: '[[global:online]]'},
'users:banned': {title: '[[pages:users/banned]]', crumb: '[[user:banned]]'},
'users:flags': {title: '[[pages:users/most-flags]]', crumb: '[[users:most_flags]]'},
};
var setToCrumbs = {
'users:postcount': '[[users:top_posters]]',
'users:reputation': '[[users:most_reputation]]',
'users:joindate': '[[global:users]]',
'users:online': '[[global:online]]',
'users:banned': '[[user:banned]]'
};
var breadcrumbs = [{text: setToCrumbs[set]}];
var breadcrumbs = [{text: setToData[set].crumb}];
if (set !== 'users:joindate') {
breadcrumbs.unshift({text: '[[global:users]]', url: '/users'});
@@ -127,7 +134,7 @@ usersController.getUsers = function(set, uid, page, callback) {
users: results.usersData.users,
pagination: pagination.create(page, pageCount),
userCount: results.usersData.count,
title: setToTitles[set] || '[[pages:users/latest]]',
title: setToData[set].title || '[[pages:users/latest]]',
breadcrumbs: helpers.buildBreadcrumbs(breadcrumbs),
setName: set,
isAdminOrGlobalMod: results.isAdministrator || results.isGlobalMod
@@ -148,6 +155,8 @@ usersController.getUsersAndCount = function(set, uid, start, stop, callback) {
db.sortedSetCount('users:online', now - 300000, '+inf', next);
} else if (set === 'users:banned') {
db.sortedSetCard('users:banned', next);
} else if (set === 'users:flags') {
db.sortedSetCard('users:flags', next);
} else {
db.getObjectField('global', 'userCount', next);
}

View File

@@ -39,7 +39,9 @@ module.exports = function(Groups) {
}, next);
});
}
], callback);
], function(err) {
callback(err);
});
});
};
};

View File

@@ -13,76 +13,76 @@ var db = require('./../database');
module.exports = function(Groups) {
Groups.join = function(groupName, uid, callback) {
function join() {
var tasks = [
async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid),
async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount')
];
async.waterfall([
function(next) {
async.parallel({
isAdmin: function(next) {
user.isAdministrator(uid, next);
},
isHidden: function(next) {
Groups.isHidden(groupName, next);
}
}, next);
},
function(results, next) {
if (results.isAdmin) {
tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid));
}
if (!results.isHidden) {
tasks.push(async.apply(db.sortedSetIncrBy, 'groups:visible:memberCount', 1, groupName));
}
async.parallel(tasks, next);
},
function(results, next) {
setGroupTitleIfNotSet(groupName, uid, next);
},
function(next) {
plugins.fireHook('action:group.join', {
groupName: groupName,
uid: uid
});
next();
}
], callback);
}
callback = callback || function() {};
if (!groupName) {
return callback(new Error('[[error:invalid-data]]'));
}
Groups.exists(groupName, function(err, exists) {
if (err) {
return callback(err);
}
if (exists) {
return join();
}
Groups.create({
name: groupName,
description: '',
hidden: 1
}, function(err) {
if (err && err.message !== '[[error:group-already-exists]]') {
winston.error('[groups.join] Could not create new hidden group: ' + err.message);
return callback(err);
async.waterfall([
function(next) {
Groups.isMember(uid, groupName, next);
},
function(isMember, next) {
if (isMember) {
return callback();
}
join();
});
});
Groups.exists(groupName, next);
},
function(exists, next) {
if (exists) {
return next();
}
Groups.create({
name: groupName,
description: '',
hidden: 1
}, function(err) {
if (err && err.message !== '[[error:group-already-exists]]') {
winston.error('[groups.join] Could not create new hidden group: ' + err.message);
return callback(err);
}
next();
});
},
function(next) {
async.parallel({
isAdmin: function(next) {
user.isAdministrator(uid, next);
},
isHidden: function(next) {
Groups.isHidden(groupName, next);
}
}, next);
},
function(results, next) {
var tasks = [
async.apply(db.sortedSetAdd, 'group:' + groupName + ':members', Date.now(), uid),
async.apply(db.incrObjectField, 'group:' + groupName, 'memberCount')
];
if (results.isAdmin) {
tasks.push(async.apply(db.setAdd, 'group:' + groupName + ':owners', uid));
}
if (!results.isHidden) {
tasks.push(async.apply(db.sortedSetIncrBy, 'groups:visible:memberCount', 1, groupName));
}
async.parallel(tasks, next);
},
function(results, next) {
setGroupTitleIfNotSet(groupName, uid, next);
},
function(next) {
plugins.fireHook('action:group.join', {
groupName: groupName,
uid: uid
});
next();
}
], callback);
};
function setGroupTitleIfNotSet(groupName, uid, callback) {
if (groupName === 'registered-users') {
if (groupName === 'registered-users' || Groups.isPrivilegeGroup(groupName)) {
return callback();
}
@@ -204,38 +204,52 @@ module.exports = function(Groups) {
Groups.leave = function(groupName, uid, callback) {
callback = callback || function() {};
var tasks = [
async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid),
async.apply(db.setRemove, 'group:' + groupName + ':owners', uid),
async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount')
];
async.parallel(tasks, function(err) {
if (err) {
return callback(err);
}
plugins.fireHook('action:group.leave', {
groupName: groupName,
uid: uid
});
Groups.getGroupFields(groupName, ['hidden', 'memberCount'], function(err, groupData) {
if (err || !groupData) {
return callback(err);
async.waterfall([
function(next) {
Groups.isMember(uid, groupName, next);
},
function(isMember, next) {
if (!isMember) {
return callback();
}
Groups.exists(groupName, next);
},
function(exists, next) {
if (!exists) {
return callback();
}
async.parallel([
async.apply(db.sortedSetRemove, 'group:' + groupName + ':members', uid),
async.apply(db.setRemove, 'group:' + groupName + ':owners', uid),
async.apply(db.decrObjectField, 'group:' + groupName, 'memberCount')
], next);
},
function(results, next) {
Groups.getGroupFields(groupName, ['hidden', 'memberCount'], next);
},
function(groupData, next) {
if (!groupData) {
return callback();
}
if (parseInt(groupData.hidden, 10) === 1 && parseInt(groupData.memberCount, 10) === 0) {
Groups.destroy(groupName, callback);
Groups.destroy(groupName, next);
} else {
if (parseInt(groupData.hidden, 10) !== 1) {
db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, callback);
db.sortedSetAdd('groups:visible:memberCount', groupData.memberCount, groupName, next);
} else {
callback();
next();
}
}
});
});
},
function(next) {
plugins.fireHook('action:group.leave', {
groupName: groupName,
uid: uid
});
next();
}
], callback);
};
Groups.leaveAllGroups = function(uid, callback) {

View File

@@ -61,14 +61,14 @@ var async = require('async'),
plugins.fireHook('static:app.reload', {}, next);
},
async.apply(plugins.clearRequireCache),
async.apply(plugins.reload),
async.apply(plugins.reloadRoutes),
async.apply(Meta.css.minify),
async.apply(Meta.js.minify, 'nodebb.min.js'),
async.apply(Meta.js.minify, 'acp.min.js'),
async.apply(Meta.sounds.init),
async.apply(languages.init),
async.apply(Meta.templates.compile),
async.apply(plugins.reload),
async.apply(plugins.reloadRoutes),
async.apply(auth.reloadRoutes),
function(next) {
Meta.config['cache-buster'] = utils.generateUUID();

View File

@@ -2,10 +2,10 @@
'use strict';
var async = require('async'),
db = require('../database'),
user = require('../user');
var async = require('async');
var db = require('../database');
var user = require('../user');
var analytics = require('../analytics');
module.exports = function(Posts) {
@@ -13,52 +13,59 @@ module.exports = function(Posts) {
if (!parseInt(uid, 10) || !reason) {
return callback();
}
async.parallel({
hasFlagged: async.apply(hasFlagged, post.pid, uid),
exists: async.apply(Posts.exists, post.pid)
}, function(err, results) {
if (err || !results.exists) {
return callback(err || new Error('[[error:no-post]]'));
}
if (results.hasFlagged) {
return callback(new Error('[[error:already-flagged]]'));
}
var now = Date.now();
async.parallel([
function(next) {
db.sortedSetAdd('posts:flagged', now, post.pid, next);
},
function(next) {
db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
},
function(next) {
db.incrObjectField('post:' + post.pid, 'flags', next);
},
function(next) {
db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
},
function(next) {
db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
},
function(next) {
if (parseInt(post.uid, 10)) {
db.sortedSetAdd('uid:' + post.uid + ':flag:pids', now, post.pid, next);
} else {
next();
}
},
function(next) {
if (parseInt(post.uid, 10)) {
db.setAdd('uid:' + post.uid + ':flagged_by', uid, next);
} else {
next();
}
async.waterfall([
function(next) {
async.parallel({
hasFlagged: async.apply(hasFlagged, post.pid, uid),
exists: async.apply(Posts.exists, post.pid)
}, next);
},
function(results, next) {
if (!results.exists) {
return next(new Error('[[error:no-post]]'));
}
], function(err) {
callback(err);
});
if (results.hasFlagged) {
return next(new Error('[[error:already-flagged]]'));
}
var now = Date.now();
async.parallel([
function(next) {
db.sortedSetAdd('posts:flagged', now, post.pid, next);
},
function(next) {
db.sortedSetIncrBy('posts:flags:count', 1, post.pid, next);
},
function(next) {
db.incrObjectField('post:' + post.pid, 'flags', next);
},
function(next) {
db.sortedSetAdd('pid:' + post.pid + ':flag:uids', now, uid, next);
},
function(next) {
db.sortedSetAdd('pid:' + post.pid + ':flag:uid:reason', 0, uid + ':' + reason, next);
},
function(next) {
if (parseInt(post.uid, 10)) {
async.parallel([
async.apply(db.sortedSetIncrBy, 'users:flags', 1, post.uid),
async.apply(db.incrObjectField, 'user:' + post.uid, 'flags'),
async.apply(db.sortedSetAdd, 'uid:' + post.uid + ':flag:pids', now, post.pid)
], next);
} else {
next();
}
}
], next);
}
], function(err) {
if (err) {
return callback(err);
}
analytics.increment('flags');
callback();
});
};
@@ -67,45 +74,58 @@ module.exports = function(Posts) {
}
Posts.dismissFlag = function(pid, callback) {
var uid;
async.parallel([
async.waterfall([
function(next) {
db.getObjectField('post:' + pid, 'uid', function(err, _uid) {
if (err) {
return next(err);
}
uid = _uid;
db.sortedSetsRemove([
'posts:flagged',
'posts:flags:count',
'uid:' + uid + ':flag:pids'
], pid, next);
});
db.getObjectFields('post:' + pid, ['pid', 'uid', 'flags'], next);
},
function(next) {
async.series([
function(postData, next) {
if (!postData.pid) {
return callback();
}
async.parallel([
function(next) {
db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function(err, uids) {
async.each(uids, function(uid, next) {
var nid = 'post_flag:' + pid + ':uid:' + uid;
if (parseInt(postData.uid, 10)) {
if (parseInt(postData.flags, 10) > 0) {
async.parallel([
async.apply(db.delete, 'notifications:' + nid),
async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid)
async.apply(db.sortedSetIncrBy, 'users:flags', -postData.flags, postData.uid),
async.apply(db.incrObjectFieldBy, 'user:' + postData.uid, 'flags', -postData.flags)
], next);
}, next);
});
} else {
next();
}
}
},
async.apply(db.delete, 'pid:' + pid + ':flag:uids')
function(next) {
db.sortedSetsRemove([
'posts:flagged',
'posts:flags:count',
'uid:' + postData.uid + ':flag:pids'
], pid, next);
},
function(next) {
async.series([
function(next) {
db.getSortedSetRange('pid:' + pid + ':flag:uids', 0, -1, function(err, uids) {
async.each(uids, function(uid, next) {
var nid = 'post_flag:' + pid + ':uid:' + uid;
async.parallel([
async.apply(db.delete, 'notifications:' + nid),
async.apply(db.sortedSetRemove, 'notifications', 'post_flag:' + pid + ':uid:' + uid)
], next);
}, next);
});
},
async.apply(db.delete, 'pid:' + pid + ':flag:uids')
], next);
},
async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason')
], next);
},
async.apply(db.deleteObjectField, 'post:' + pid, 'flags'),
async.apply(db.delete, 'pid:' + pid + ':flag:uid:reason')
], function(err) {
callback(err);
});
function(results, next) {
db.sortedSetsRemoveRangeByScore(['users:flags'], '-inf', 0, next);
}
], callback);
};
Posts.dismissAllFlags = function(callback) {
@@ -113,7 +133,16 @@ module.exports = function(Posts) {
if (err) {
return callback(err);
}
async.eachLimit(pids, 50, Posts.dismissFlag, callback);
async.eachSeries(pids, Posts.dismissFlag, callback);
});
};
Posts.dismissUserFlags = function(uid, callback) {
db.getSortedSetRange('uid:' + uid + ':flag:pids', 0, -1, function(err, pids) {
if (err) {
return callback(err);
}
async.eachSeries(pids, Posts.dismissFlag, callback);
});
};

View File

@@ -31,7 +31,6 @@ module.exports = function(Posts) {
}
data.postData.content = translator.escape(data.postData.content);
data.postData.content = Posts.relativeToAbsolute(data.postData.content);
if (global.env === 'production' && data.postData.pid) {
cache.set(data.postData.pid, data.postData.content);
@@ -67,8 +66,8 @@ module.exports = function(Posts) {
content = content.slice(0, current.index + 6) + absolute + content.slice(current.index + 6 + current[1].length);
}
} catch(err) {
winston.verbose(err.messsage);
}
winston.verbose(err.messsage);
}
}
}

View File

@@ -34,10 +34,10 @@ module.exports = function(Posts) {
return next(new Error('[[error:post-already-restored]]'));
}
privileges.posts.canEdit(pid, uid, next);
privileges.posts.canDelete(pid, uid, next);
},
function (canEdit, next) {
if (!canEdit) {
function (canDelete, next) {
if (!canDelete) {
return next(new Error('[[error:no-privileges]]'));
}

View File

@@ -19,17 +19,20 @@ module.exports = function(privileges) {
return callback(null, []);
}
async.parallel({
isAdmin: function(next){
user.isAdministrator(uid, next);
async.waterfall([
function(next) {
posts.getCidsByPids(pids, next);
},
isModerator: function(next) {
posts.isModerator(pids, uid, next);
},
isOwner: function(next) {
posts.isOwner(pids, uid, next);
function(cids, next) {
async.parallel({
isAdmin: async.apply(user.isAdministrator, uid),
isModerator: async.apply(posts.isModerator, pids, uid),
isOwner: async.apply(posts.isOwner, pids, uid),
'topics:read': async.apply(helpers.isUserAllowedTo, 'topics:read', uid, cids),
read: async.apply(helpers.isUserAllowedTo, 'read', uid, cids),
}, next);
}
}, function(err, results) {
], function(err, results) {
if (err) {
return callback(err);
}
@@ -37,11 +40,16 @@ module.exports = function(privileges) {
var privileges = [];
for (var i=0; i<pids.length; ++i) {
var editable = results.isAdmin || results.isModerator[i] || results.isOwner[i];
var isAdminOrMod = results.isAdmin || results.isModerator[i];
var editable = isAdminOrMod || results.isOwner[i];
privileges.push({
editable: editable,
view_deleted: editable,
move: results.isAdmin || results.isModerator[i]
move: isAdminOrMod,
isAdminOrMod: isAdminOrMod,
'topics:read': results['topics:read'][i] || isAdminOrMod,
read: results.read[i] || isAdminOrMod
});
}
@@ -150,6 +158,38 @@ module.exports = function(privileges) {
});
};
privileges.posts.canDelete = function(pid, uid, callback) {
var postData;
async.waterfall([
function(next) {
posts.getPostFields(pid, ['tid', 'timestamp'], next);
},
function(_postData, next) {
postData = _postData;
async.parallel({
isAdminOrMod: async.apply(isAdminOrMod, pid, uid),
isLocked: async.apply(topics.isLocked, postData.tid),
isOwner: async.apply(posts.isOwner, pid, uid)
}, next);
}
], function(err, results) {
if (err) {
return callback(err);
}
if (results.isAdminOrMod) {
return callback(null, true);
}
if (results.isLocked) {
return callback(new Error('[[error:topic-locked]]'));
}
var postDeleteDuration = parseInt(meta.config.postDeleteDuration, 10);
if (postDeleteDuration && (Date.now() - parseInt(postData.timestamp, 10) > postDeleteDuration * 1000)) {
return callback(new Error('[[error:post-delete-duration-expired, ' + meta.config.postDeleteDuration + ']]'));
}
callback(null, results.isOwner);
});
};
privileges.posts.canMove = function(pid, uid, callback) {
posts.isMain(pid, function(err, isMain) {
if (err || isMain) {

View File

@@ -65,6 +65,7 @@ function addRoutes(router, middleware, controllers) {
router.get('/manage/users/not-validated', middlewares, controllers.admin.users.notValidated);
router.get('/manage/users/no-posts', middlewares, controllers.admin.users.noPosts);
router.get('/manage/users/inactive', middlewares, controllers.admin.users.inactive);
router.get('/manage/users/flagged', middlewares, controllers.admin.users.flagged);
router.get('/manage/users/banned', middlewares, controllers.admin.users.banned);
router.get('/manage/registration', middlewares, controllers.admin.users.registrationQueue);

View File

@@ -75,6 +75,7 @@ function userRoutes(app, middleware, controllers) {
setupPageRoute(app, '/users/sort-posts', middleware, middlewares, controllers.users.getUsersSortedByPosts);
setupPageRoute(app, '/users/sort-reputation', middleware, middlewares, controllers.users.getUsersSortedByReputation);
setupPageRoute(app, '/users/banned', middleware, middlewares, controllers.users.getBannedUsers);
setupPageRoute(app, '/users/flagged', middleware, middlewares, controllers.users.getFlaggedUsers);
}
function groupRoutes(app, middleware, controllers) {

View File

@@ -185,25 +185,15 @@ User.search = function(socket, data, callback) {
return user && user.uid;
});
async.parallel({
users: function(next) {
user.getUsersFields(uids, ['email'], next);
},
flagCounts: function(next) {
var sets = uids.map(function(uid) {
return 'uid:' + uid + ':flagged_by';
});
db.setsCount(sets, next);
}
}, function(err, results) {
user.getUsersFields(uids, ['email', 'flags'], function(err, userInfo) {
if (err) {
return callback(err);
}
userData.forEach(function(user, index) {
if (user) {
user.email = (results.users[index] && results.users[index].email) || '';
user.flags = results.flagCounts[index] || 0;
if (user && userInfo[index]) {
user.email = userInfo[index].email || '';
user.flags = userInfo[index].flags || 0;
}
});

View File

@@ -193,7 +193,7 @@ SocketCategories.isModerator = function(socket, cid, callback) {
};
SocketCategories.getCategory = function(socket, cid, callback) {
apiController.getObjectByType(socket.uid, 'category', cid, callback);
apiController.getCategoryData(cid, socket.uid, callback);
};
module.exports = SocketCategories;

View File

@@ -73,17 +73,7 @@ SocketPosts.getRawPost = function(socket, pid, callback) {
};
SocketPosts.getPost = function(socket, pid, callback) {
async.waterfall([
function(next) {
apiController.getObjectByType(socket.uid, 'post', pid, next);
},
function(postData, next) {
if (parseInt(postData.deleted, 10) === 1) {
return next(new Error('[[error:no-post]]'));
}
next(null, postData);
}
], callback);
apiController.getPostData(pid, socket.uid, callback);
};
SocketPosts.loadMoreFavourites = function(socket, data, callback) {

View File

@@ -23,8 +23,8 @@ module.exports = function(SocketPosts) {
return callback(new Error('[[error:invalid-data]]'));
}
var flaggingUser = {},
post;
var flaggingUser = {};
var post;
async.waterfall([
function (next) {
@@ -40,9 +40,7 @@ module.exports = function(SocketPosts) {
},
function (topicData, next) {
post.topic = topicData;
next();
},
function (next) {
async.parallel({
isAdminOrMod: function(next) {
privileges.categories.isAdminOrMod(post.topic.cid, socket.uid, next);

View File

@@ -111,17 +111,7 @@ SocketTopics.isModerator = function(socket, tid, callback) {
};
SocketTopics.getTopic = function (socket, tid, callback) {
async.waterfall([
function (next) {
apiController.getObjectByType(socket.uid, 'topic', tid, next);
},
function (topicData, next) {
if (parseInt(topicData.deleted, 10) === 1) {
return next(new Error('[[error:no-topic]]'));
}
next(null, topicData);
}
], callback);
apiController.getTopicData(tid, socket.uid, callback);
};
module.exports = SocketTopics;

View File

@@ -7,12 +7,20 @@ var events = require('../../events');
module.exports = function(SocketUser) {
SocketUser.banUsers = function(socket, uids, callback) {
toggleBan(socket.uid, uids, SocketUser.banUser, function(err) {
SocketUser.banUsers = function(socket, data, callback) {
// Backwards compatibility
if (Array.isArray(data)) {
data = {
uids: data,
until: 0
}
}
toggleBan(socket.uid, data.uids, banUser.bind(null, data.until || 0), function(err) {
if (err) {
return callback(err);
}
async.each(uids, function(uid, next) {
async.each(data.uids, function(uid, next) {
events.log({
type: 'user-ban',
uid: socket.uid,
@@ -45,7 +53,7 @@ module.exports = function(SocketUser) {
], callback);
}
SocketUser.banUser = function(uid, callback) {
function banUser(until, uid, callback) {
async.waterfall([
function (next) {
user.isAdministrator(uid, next);
@@ -54,7 +62,7 @@ module.exports = function(SocketUser) {
if (isAdmin) {
return next(new Error('[[error:cant-ban-other-admins]]'));
}
user.ban(uid, next);
user.ban(uid, until, next);
},
function (next) {
websockets.in('uid_' + uid).emit('event:banned');

View File

@@ -20,6 +20,7 @@ module.exports = function(SocketUser) {
sortBy: data.sortBy,
onlineOnly: data.onlineOnly,
bannedOnly: data.bannedOnly,
flaggedOnly: data.flaggedOnly,
uid: socket.uid
}, function(err, result) {
if (err) {

View File

@@ -8,6 +8,7 @@ var winston = require('winston');
var db = require('../database');
var user = require('../user');
var posts = require('../posts');
var notifications = require('../notifications');
var privileges = require('../privileges');
var meta = require('../meta');
@@ -194,6 +195,8 @@ module.exports = function(Topics) {
titleEscaped = title.replace(/%/g, '&#37;').replace(/,/g, '&#44;');
}
postData.content = posts.relativeToAbsolute(postData.content);
notifications.create({
bodyShort: '[[notifications:user_posted_to, ' + postData.user.username + ', ' + titleEscaped + ']]',
bodyLong: postData.content,
@@ -223,6 +226,7 @@ module.exports = function(Topics) {
if (err) {
return next(err);
}
if (data.userSettings.sendPostNotifications) {
emailer.send('notif_post', toUid, {
pid: postData.pid,

View File

@@ -89,7 +89,8 @@ var utils = require('../public/src/utils');
};
User.getUsers = function(uids, uid, callback) {
var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'banned', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline'];
var fields = ['uid', 'username', 'userslug', 'picture', 'status', 'flags',
'banned', 'joindate', 'postcount', 'reputation', 'email:confirmed', 'lastonline'];
async.waterfall([
function (next) {
@@ -256,6 +257,35 @@ var utils = require('../public/src/utils');
});
};
User.isBanned = function(uid, callback) {
async.waterfall([
async.apply(User.getUserField, uid, 'banned'),
function(banned, next) {
banned = parseInt(banned, 10) === 1;
if (!banned) {
return next(null, banned);
} else {
// If they are banned, see if the ban has expired
db.sortedSetScore('users:banned:expire', uid, function(err, score) {
var stillBanned = Date.now() < score;
if (!stillBanned) {
async.parallel([
async.apply(db.sortedSetRemove.bind(db), 'users:banned:expire', uid),
async.apply(db.sortedSetRemove.bind(db), 'users:banned', uid),
async.apply(User.setUserField, uid, 'banned', 0)
], function(err) {
next(err, false);
});
} else {
next(err, true);
}
});
}
}
], callback);
};
User.addInterstitials = function(callback) {
plugins.registerHook('core', {
hook: 'filter:register.interstitial',

View File

@@ -3,6 +3,7 @@
var async = require('async');
var db = require('../database');
var posts = require('../posts');
var plugins = require('../plugins');
module.exports = function(User) {
@@ -52,23 +53,43 @@ module.exports = function(User) {
], callback);
};
User.ban = function(uid, callback) {
async.waterfall([
function (next) {
User.setUserField(uid, 'banned', 1, next);
},
function (next) {
db.sortedSetAdd('users:banned', Date.now(), uid, next);
},
function (next) {
plugins.fireHook('action:user.banned', {uid: uid});
next();
User.ban = function(uid, until, callback) {
// "until" (optional) is unix timestamp in milliseconds
if (!callback && typeof until === 'function') {
callback = until;
until = 0;
}
until = parseInt(until, 10);
if (isNaN(until)) {
return callback(new Error('[[error:ban-expiry-missing]]'));
}
var tasks = [
async.apply(User.setUserField, uid, 'banned', 1),
async.apply(db.sortedSetAdd, 'users:banned', Date.now(), uid),
];
if (until > 0 && Date.now() < until) {
tasks.push(async.apply(db.sortedSetAdd, 'users:banned:expire', until, uid));
} else {
until = 0;
}
async.series(tasks, function (err) {
if (err) {
return callback(err);
}
], callback);
plugins.fireHook('action:user.banned', {
uid: uid,
until: until > 0 ? until : undefined
});
callback();
});
};
User.unban = function(uid, callback) {
db.delete('uid:' + uid + ':flagged_by');
async.waterfall([
function (next) {
User.setUserField(uid, 'banned', 0, next);
@@ -87,9 +108,9 @@ module.exports = function(User) {
if (!Array.isArray(uids) || !uids.length) {
return callback();
}
var keys = uids.map(function(uid) {
return 'uid:' + uid + ':flagged_by';
});
db.deleteAll(keys, callback);
async.eachSeries(uids, function(uid, next) {
posts.dismissUserFlags(uid, next);
}, callback);
};
};

View File

@@ -7,6 +7,7 @@ var S = require('string');
var utils = require('../../public/src/utils');
var meta = require('../meta');
var db = require('../database');
var groups = require('../groups');
var plugins = require('../plugins');
module.exports = function(User) {
@@ -100,7 +101,21 @@ module.exports = function(User) {
});
}
async.series([isAboutMeValid, isSignatureValid, isEmailAvailable, isUsernameAvailable], function(err) {
function isGroupTitleValid(next) {
if (data.groupTitle === 'registered-users' || groups.isPrivilegeGroup(data.groupTitle)) {
next(new Error('[[error:invalid-group-title]]'));
} else {
next();
}
}
async.series([
isAboutMeValid,
isSignatureValid,
isEmailAvailable,
isUsernameAvailable,
isGroupTitleValid
], function(err) {
if (err) {
return callback(err);
}

View File

@@ -1,10 +1,10 @@
'use strict';
var async = require('async'),
meta = require('../meta'),
plugins = require('../plugins'),
db = require('../database');
var async = require('async');
var meta = require('../meta');
var plugins = require('../plugins');
var db = require('../database');
module.exports = function(User) {
@@ -84,7 +84,7 @@ module.exports = function(User) {
function filterAndSortUids(uids, data, callback) {
var sortBy = data.sortBy || 'joindate';
var fields = ['uid', 'status', 'lastonline', 'banned', sortBy];
var fields = ['uid', 'status', 'lastonline', 'banned', 'flags', sortBy];
User.getUsersFields(uids, fields, function(err, userData) {
if (err) {
@@ -96,13 +96,19 @@ module.exports = function(User) {
return user && user.status !== 'offline' && (Date.now() - parseInt(user.lastonline, 10) < 300000);
});
}
if(data.bannedOnly) {
if (data.bannedOnly) {
userData = userData.filter(function(user) {
return user && user.banned;
});
}
if (data.flaggedOnly) {
userData = userData.filter(function(user) {
return user && parseInt(user.flags, 10) > 0;
});
}
sortUsers(userData, sortBy);
uids = userData.map(function(user) {

View File

@@ -1,32 +1,48 @@
<div class="flags">
<div class="col-lg-9">
<div class="col-lg-12">
<div class="text-center">
<div class="panel panel-default">
<div class="panel-body">
<div><canvas id="flags:daily" height="250"></canvas></div>
<p>
</p>
</div>
<div class="panel-footer"><small>Daily flags</small></div>
</div>
</div>
<form id="flag-search" method="GET" action="flags">
<div class="form-group">
<div>
<div>
<label>Flags by user</label>
<input type="text" class="form-control" id="byUsername" placeholder="Search flagged posts by username" name="byUsername" value="{byUsername}">
</div>
</div>
</div>
<div class="form-group">
<label>Sort By</label>
<div>
<div>
<select id="flag-sort-by" class="form-control" name="sortBy">
<option value="count">Most Flags</option>
<option value="time">Most Recent</option>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Search</button>
<button class="btn btn-primary" id="dismissAll">Dismiss All</button>
</form>
<hr/>
<div data-next="{next}">
<form id="flag-search" method="GET" action="flags">
<div class="form-group">
<div class="row">
<div class="col-md-6">
<label>Flags by user</label>
<input type="text" class="form-control" id="byUsername" placeholder="Search flagged posts by username" name="byUsername" value="{byUsername}">
</div>
</div>
</div>
<div class="form-group">
<label>Sort By</label>
<div class="row">
<div class="col-md-6">
<select id="flag-sort-by" class="form-control" name="sortBy">
<option value="count">Most Flags</option>
<option value="time">Most Recent</option>
</select>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary">Search</button>
</form>
<br />
<hr/>
<div class="post-container" data-next="{next}">
<!-- IF !posts.length -->
@@ -53,7 +69,6 @@
</a>
<div class="content">
<p>{posts.content}</p>
<p class="fade-out"></p>
</div>
<small>
<span class="pull-right">
@@ -92,15 +107,4 @@
</div>
</div>
</div>
<div class="col-lg-3 acp-sidebar">
<div class="panel panel-default">
<div class="panel-heading">Flags Control Panel</div>
<div class="panel-body">
<div>
<button class="btn btn-primary" id="dismissAll">Dismiss All</button>
</div>
</div>
</div>
</div>
</div>

View File

@@ -8,6 +8,7 @@
<li><a href='{config.relative_path}/admin/manage/users/not-validated'>Not validated</a></li>
<li><a href='{config.relative_path}/admin/manage/users/no-posts'>No Posts</a></li>
<li><a href='{config.relative_path}/admin/manage/users/inactive'>Inactive</a></li>
<li><a href='{config.relative_path}/admin/manage/users/flagged'>Most Flags</a></li>
<li><a href='{config.relative_path}/admin/manage/users/banned'>Banned</a></li>
<li><a href='{config.relative_path}/admin/manage/users/search'>User Search</a></li>
@@ -84,7 +85,7 @@
posts {users.postcount}
<!-- IF users.flags -->
<div><small><span><i class="fa fa-flag"></i> {users.flags}</span></small></div>
<div><small><span><i class="fa fa-flag"></i> <a href="{config.relative_path}/admin/manage/flags?byUsername={users.username}">{users.flags}</a></span></small></div>
<!-- ENDIF users.flags -->
</div>
<!-- END users -->

View File

@@ -48,6 +48,10 @@
<label for="postEditDuration">Number of seconds users are allowed to edit posts after posting. (0 disabled)</label>
<input id="postEditDuration" type="text" class="form-control" value="0" data-field="postEditDuration">
</div>
<div class="form-group">
<label for="postDeleteDuration">Number of seconds users are allowed to delete posts after posting. (0 disabled)</label>
<input id="postDeleteDuration" type="text" class="form-control" value="0" data-field="postDeleteDuration">
</div>
<div class="form-group">
<label for="minimumTitleLength">Minimum Title Length</label>
<input id="minimumTitleLength" type="text" class="form-control" value="3" data-field="minimumTitleLength">