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")