diff --git a/config.json.example b/config.json.example index 74e2f0822c..e34d187838 100644 --- a/config.json.example +++ b/config.json.example @@ -21,6 +21,7 @@ "NEW_RELIC_APPLICATION_ID":"NEW_RELIC_APPLICATION_ID", "NEW_RELIC_API_KEY":"NEW_RELIC_API_KEY", "GA_ID": "GA_ID", + "FLAG_REPORT_EMAIL": "email@mod.com", "EMAIL_SERVER": { "url": "http://example.com", "authUser": "user", diff --git a/public/css/index.styl b/public/css/index.styl index 07e2829d0b..381ab03082 100644 --- a/public/css/index.styl +++ b/public/css/index.styl @@ -135,6 +135,9 @@ a a span.glyphicon color: #333 // Takes base color from bootstrap + &.text-danger + color: #a94442 // Allows glyphicon to use text-danger class + a.label color: #fff diff --git a/public/js/controllers/groupsCtrl.js b/public/js/controllers/groupsCtrl.js index a9f744839c..8e4b8695dd 100644 --- a/public/js/controllers/groupsCtrl.js +++ b/public/js/controllers/groupsCtrl.js @@ -108,8 +108,8 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' } ]) - .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', - function($scope, $rootScope, Members, Shared, $http, Notification) { + .controller("MemberModalCtrl", ['$scope', '$rootScope', 'Members', 'Shared', '$http', 'Notification', 'Groups', + function($scope, $rootScope, Members, Shared, $http, Notification, Groups) { $scope.timestamp = function(timestamp){ return moment(timestamp).format('MM/DD/YYYY'); } @@ -139,6 +139,20 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' $scope.$close(); }) } + $scope.reportAbuse = function(reporter, message, groupId) { + message.flags[reporter._id] = true; + Groups.Group.flagChatMessage({gid: groupId, messageId: message.id}, undefined, function(data){ + Notification.text(window.env.t('abuseReported')); + $scope.$close(); + }); + } + $scope.clearFlagCount = function(message, groupId) { + Groups.Group.clearFlagCount({gid: groupId, messageId: message.id}, undefined, function(data){ + message.flagCount = 0; + Notification.text("Flags cleared"); + $scope.$close(); + }); + } } ]) @@ -197,7 +211,7 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' }); }]) - .controller('ChatCtrl', ['$scope', 'Groups', 'User', '$http', 'ApiUrlService', 'Notification', function($scope, Groups, User, $http, ApiUrlService, Notification){ + .controller('ChatCtrl', ['$scope', 'Groups', 'User', '$http', 'ApiUrlService', 'Notification', 'Members', '$rootScope', function($scope, Groups, User, $http, ApiUrlService, Notification, Members, $rootScope){ $scope.message = {content:''}; $scope._sending = false; @@ -262,6 +276,22 @@ habitrpg.controller("GroupsCtrl", ['$scope', '$rootScope', 'Shared', 'Groups', ' $http.post(ApiUrlService.get() + '/api/v2/groups/' + group._id + '/chat/' + message.id + '/like'); } + $scope.flagChatMessage = function(groupId,message) { + if(!message.flags) message.flags = {}; + if(message.flags[User.user._id]) + Notification.text(window.env.t('abuseAlreadyReported')); + else { + $scope.abuseObject = message; + $scope.groupId = groupId; + Members.selectMember(message.uuid, function(){ + $rootScope.openModal('abuse-flag',{ + controller:'MemberModalCtrl', + scope: $scope + }); + }); + } + } + $scope.sync = function(group){ group.$get(); } diff --git a/public/js/services/groupServices.js b/public/js/services/groupServices.js index 42dd787846..e07f21b15c 100644 --- a/public/js/services/groupServices.js +++ b/public/js/services/groupServices.js @@ -13,6 +13,8 @@ angular.module('groupServices', ['ngResource']). //query: {method: "GET", isArray:false}, postChat: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat'}, deleteChatMessage: {method: "DELETE", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId'}, + flagChatMessage: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId/flag'}, + clearFlagCount: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/chat/:messageId/clearflags'}, join: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/join'}, leave: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/leave'}, invite: {method: "POST", url: ApiUrlService.get() + '/api/v2/groups/:gid/invite'}, diff --git a/src/controllers/groups.js b/src/controllers/groups.js index 1d365d7d8b..05cc7f1c7a 100644 --- a/src/controllers/groups.js +++ b/src/controllers/groups.js @@ -7,10 +7,12 @@ function clone(a) { var _ = require('lodash'); var nconf = require('nconf'); var async = require('async'); +var utils = require('./../utils'); var shared = require('habitrpg-shared'); var User = require('./../models/user').model; var Group = require('./../models/group').model; var Challenge = require('./../models/challenge').model; +var isProd = nconf.get('NODE_ENV') === 'production'; var api = module.exports; /* @@ -256,6 +258,82 @@ api.deleteChatMessage = function(req, res, next){ }); } +api.flagChatMessage = function(req, res, next){ + var user = res.locals.user + var group = res.locals.group; + var message = _.find(group.chat, {id: req.params.mid}); + + if(!message) return res.json(404, {err: "Message not found!"}); + if(message.uuid == user._id) return res.json(401, {err: "Can't report your own message."}); + + User.findOne({_id: message.uuid}, {auth: 1}, function(err, author){ + if(err) return next(err); + + // Log user ids that have flagged the message + if(!message.flags) message.flags = {}; + if(message.flags[user._id] && !user.contributor.admin) return res.json(401, {err: "You have already reported this message"}); + message.flags[user._id] = true; + + // Log total number of flags (publicly viewable) + if(!message.flagCount) message.flagCount = 0; + if(user.contributor.admin){ + // Arbitraty amount, higher than 2 + message.flagCount = 5; + } else { + message.flagCount++ + } + + group.markModified('chat'); + group.save(function(err,_saved){ + if(err) return next(err); + if (isProd){ + utils.txnEmail({email: nconf.get('FLAG_REPORT_EMAIL')}, 'flag-report-to-mods', [ + {name: "MESSAGE_TIME", content: message.timestamp}, + {name: "MESSAGE_TEXT", content: message.text}, + + {name: "REPORTER_USERNAME", content: user.profile.name}, + {name: "REPORTER_UUID", content: user._id}, + {name: "REPORTER_EMAIL", content: user.auth.local ? user.auth.local.email : ((user.auth.facebook && user.auth.facebook.emails && user.auth.facebook.emails[0]) ? user.auth.facebook.emails[0].value : null)}, + {name: "REPORTER_MODAL_URL", content: "https://habitrpg.com/static/front/#?memberId=" + user._id}, + + {name: "AUTHOR_USERNAME", content: message.user}, + {name: "AUTHOR_UUID", content: message.uuid}, + {name: "AUTHOR_EMAIL", content: author.auth.local ? author.auth.local.email : ((author.auth.facebook && author.auth.facebook.emails && author.auth.facebook.emails[0]) ? author.auth.facebook.emails[0].value : null)}, + {name: "AUTHOR_MODAL_URL", content: "https://habitrpg.com/static/front/#?memberId=" + message.uuid}, + + {name: "GROUP_NAME", content: group.name}, + {name: "GROUP_TYPE", content: group.type}, + {name: "GROUP_ID", content: group._id}, + {name: "GROUP_URL", content: group._id == 'habitrpg' ? (nconf.get('BASE_URL') + '/#/options/groups/tavern') : (group.type === 'guild' ? (nconf.get('BASE_URL')+ '/#/options/groups/guilds/' + group._id) : 'party')}, + ]); + } + return res.send(204); + }); + }); + +} + +api.clearFlagCount = function(req, res, next){ + var user = res.locals.user + var group = res.locals.group; + var message = _.find(group.chat, {id: req.params.mid}); + + if(!message) return res.json(404, {err: "Message not found!"}); + + if(user.contributor.admin){ + message.flagCount = 0; + + group.markModified('chat'); + group.save(function(err,_saved){ + if(err) return next(err); + return res.send(204); + }); + }else{ + return res.json(401, {err: "Only an admin can clear the flag count!"}) + } + +} + api.seenMessage = function(req,res,next){ // Skip the auth step, we want this to be fast. If !found with uuid/token, then it just doesn't save // Check for req.params.gid to exist diff --git a/src/models/group.js b/src/models/group.js index 85653670dc..2997fd4cfe 100644 --- a/src/models/group.js +++ b/src/models/group.js @@ -101,7 +101,9 @@ var chatDefaults = module.exports.chatDefaults = function(msg,user){ id: shared.uuid(), text: msg, timestamp: +new Date, - likes: {} + likes: {}, + flags: {}, + flagCount: 0 }; if (user) { _.defaults(message, { @@ -319,6 +321,18 @@ GroupSchema.statics.bossQuest = function(user, progress, cb) { }) } +GroupSchema.methods.toJSON = function() { + var doc = this.toObject(); + if(doc.chat){ + doc.chat.forEach(function(msg){ + msg.flags = {}; + }); + } + + return doc; +}; + + module.exports.schema = GroupSchema; var Group = module.exports.model = mongoose.model("Group", GroupSchema); @@ -333,4 +347,4 @@ Group.count({_id:'habitrpg'},function(err,ct){ type: 'guild', privacy:'public' }).save(); -}) \ No newline at end of file +}) diff --git a/src/routes/apiv2.coffee b/src/routes/apiv2.coffee index 40b0adf15d..6c93e3ba42 100644 --- a/src/routes/apiv2.coffee +++ b/src/routes/apiv2.coffee @@ -577,6 +577,28 @@ module.exports = (swagger, v2) -> middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] action: groups.likeChatMessage + "/groups/{gid}/chat/{mid}/flag": + spec: + method: 'POST' + description: "Flag a chat message" + parameters: [ + path 'gid','Group id','string' + path 'mid','Message id','string' + ] + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] + action: groups.flagChatMessage + + "/groups/{gid}/chat/{mid}/clearflags": + spec: + method: 'POST' + description: "Clear flag count from message and unhide it" + parameters: [ + path 'gid','Group id','string' + path 'mid','Message id','string' + ] + middleware: [auth.auth, i18n.getUserLanguage, groups.attachGroup] + action: groups.clearFlagCount + # --------------------------------- # Members # --------------------------------- diff --git a/views/options/social/chat-message.jade b/views/options/social/chat-message.jade index 89ebc181e6..e93f304b7d 100644 --- a/views/options/social/chat-message.jade +++ b/views/options/social/chat-message.jade @@ -1,8 +1,10 @@ mixin chatMessages(inbox) ul.list-unstyled.tavern-chat - var ngRepeat = inbox ? 'message in user.inbox.messages | toArray:true | orderBy:"sort":true' : 'message in group.chat track by message.id' - li.chat-message(ng-repeat=ngRepeat, ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}', style='{{::message.sent ? "opacity:0.5" : ""}}') - .scrollable-message + li.chat-message(ng-repeat=ngRepeat, ng-class=':: {highlight: isUserMentioned(user,message) || message.uuid=="system", "own-message": user._id == message.uuid}', ng-if="!message.flagCount || message.flagCount < 2 || user.contributor.admin") + span.pull-right.text-danger(ng-if="user.contributor.admin && message.flagCount > 0") + | {{message.flagCount > 1 ? "Message Hidden" : "1 flag"}} + .scrollable-message(ng-class='{"transparent": message.sent || message.flags[user._id] || (user.contributor.admin && message.flagCount > 1)}') span(ng-if='::message.user') a.label.label-default.chat-message.hidden-label span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') @@ -23,9 +25,12 @@ mixin chatMessages(inbox) if inbox a(ng-click="quickReply(message.uuid)") span.glyphicon.glyphicon-share-alt(tooltip=env.t('pm-reply')) - |         + |     a(ng-click='#{inbox? "user.ops.deletePM({params:{id:message.$key}})" : "deleteChatMessage(group, message)"}', ng-if='#{inbox ? "true" : ":: user.contributor.admin || message.uuid == user.id"}') span.glyphicon.glyphicon-trash(tooltip=env.t('delete')) + |     + a(ng-click="flagChatMessage(group._id, message)", ng-if=':: user.contributor.admin || (!message.sent && user.flags.communityGuidelinesAccepted && message.uuid != user.id && message.uuid != "system")') + span.glyphicon.glyphicon-flag(tooltip="{{message.flags[user._id] ? env.t('abuseAlreadyReported') : env.t('abuseFlag')}}" ng-class='message.flags[user._id] ? "text-danger" : ""') span.float-label(ng-class='::contribText(message.contributor, message.backer).length > 30 ? "long-title" : ""') a.label.label-default.chat-message(ng-if=':: message.user', ng-class='::userLevelStyleFromLevel(message.contributor.level, message.backer.npc, style)', ng-click='clickMember(message.uuid, true)') span.glyphicon.glyphicon-arrow-right(ng-if='::message.sent') diff --git a/views/shared/modals/members.jade b/views/shared/modals/members.jade index 595929c819..0255ab8e6d 100644 --- a/views/shared/modals/members.jade +++ b/views/shared/modals/members.jade @@ -88,4 +88,17 @@ script(type='text/ng-template', id='modals/send-gift.html') button.btn.btn-primary(ng-show=fromBal, ng-click='sendGift(profile._id, gift)')=env.t("send") a.btn.btn-primary(ng-hide=fromBal, ng-click='Payments.showStripe({gift:gift, uuid:profile._id})')=env.t('card') a.btn.btn-warning(ng-hide=fromBal, href='/paypal/checkout?_id={{::user._id}}&apiToken={{::user.apiToken}}&gift={{Payments.encodeGift(profile._id, gift)}}') PayPal - button.btn.btn-default(ng-click='$close()')=env.t('cancel') \ No newline at end of file + button.btn.btn-default(ng-click='$close()')=env.t('cancel') + +script(type='text/ng-template', id='modals/abuse-flag.html') + .modal-header + h4!=env.t('abuseFlagModalHeading', {name: "{{profile.profile.name}}"}) + .modal-body + blockquote + markdown(ng-model="abuseObject.text") + p!=env.t('abuseFlagModalBody', {firstLinkStart: "", secondLinkStart: "", linkEnd: ""}) + .modal-footer + button.pull-left.btn.btn-danger(ng-click='clearFlagCount(abuseObject, groupId)', ng-if='user.contributor.admin && abuseObject.flagCount >= 2') + | Reset Flag Count + button.btn.btn-primary(ng-click='$close()')=env.t('cancel') + button.btn.btn-default(ng-click='reportAbuse(user, abuseObject, groupId)')=env.t("abuseFlagModalButton")