package fr.ippon.tatami.service;
import fr.ippon.tatami.config.Constants;
import fr.ippon.tatami.domain.Group;
import fr.ippon.tatami.domain.User;
import fr.ippon.tatami.domain.status.*;
import fr.ippon.tatami.repository.*;
import fr.ippon.tatami.security.AuthenticationService;
import fr.ippon.tatami.service.dto.StatusDTO;
import fr.ippon.tatami.service.exception.ArchivedGroupException;
import fr.ippon.tatami.service.exception.ReplyStatusException;
import fr.ippon.tatami.service.util.DomainUtil;
import org.apache.camel.util.Time;
import org.apache.commons.lang.StringEscapeUtils;
import org.apache.openjpa.jdbc.kernel.exps.Abs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.inject.Inject;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Service
public class StatusUpdateService {
private static final Logger log = LoggerFactory.getLogger(StatusUpdateService.class);
private static final Pattern PATTERN_LOGIN = Pattern.compile("@[^\\s,\\p{Punct}]+");
private static final Pattern PATTERN_HASHTAG = Pattern.compile("(^|\\s)#([^\\s !\"#$%&\'()*+,./:;<=>?@\\\\\\[\\]^_`{|}~-]+)");
@Inject
private FollowerRepository followerRepository;
@Inject
private TagFollowerRepository tagFollowerRepository;
@Inject
private AuthenticationService authenticationService;
@Inject
private DaylineRepository daylineRepository;
@Inject
private StatusRepository statusRepository;
@Inject
private TimelineRepository timelineRepository;
@Inject
private MentionService mentionService;
@Inject
private UserlineRepository userlineRepository;
@Inject
private TaglineRepository taglineRepository;
@Inject
private TagCounterRepository tagCounterRepository;
@Inject
private UserGroupRepository userGroupRepository;
@Inject
private GrouplineRepository grouplineRepository;
@Inject
private GroupMembersRepository groupMembersRepository;
@Inject
private GroupService groupService;
@Inject
private TrendRepository trendsRepository;
@Inject
private UserTrendRepository userTrendRepository;
@Inject
private DiscussionRepository discussionRepository;
@Inject
private CounterRepository counterRepository;
@Inject
private SearchService searchService;
@Inject
private DomainlineRepository domainlineRepository;
@Inject
private StatusAttachmentRepository statusAttachmentRepository;
@Inject
private AtmosphereService atmosphereService;
@Inject
private MailService mailService;
@Inject
private StatusReportRepository statusReportRepository;
@Inject
private BlockRepository blockRepository;
@Inject
private TimelineService timelineService;
public void postStatus(String content, boolean statusPrivate, Collection<String> attachmentIds, String geoLocalization) {
createStatus(content, statusPrivate, null, "", "", "", attachmentIds, null, geoLocalization);
}
public void postStatus(String content, boolean statusPrivate, Collection<String> attachmentIds) {
createStatus(content, statusPrivate, null, "", "", "", attachmentIds);
}
public void postStatusToGroup(String content, Group group, Collection<String> attachmentIds, String geoLocalization) {
createStatus(content, false, group, "", "", "", attachmentIds, null, geoLocalization);
}
public void postStatusAsUser(String content, User user) {
createStatus(content, false, null, "", "", "", null, user, null);
}
public void replyToStatus(String content, String replyTo, Collection<String> attachmentIds) throws ArchivedGroupException, ReplyStatusException {
AbstractStatus abstractStatus = statusRepository.findStatusById(replyTo);
if (abstractStatus != null &&
!abstractStatus.getType().equals(StatusType.STATUS) &&
!abstractStatus.getType().equals(StatusType.SHARE) &&
!abstractStatus.getType().equals(StatusType.ANNOUNCEMENT)) {
log.debug("Cannot reply to a status of this type");
throw new ReplyStatusException();
}
if (abstractStatus != null &&
abstractStatus.getType().equals(StatusType.SHARE)) {
log.debug("Replacing the share by the original status");
Share share = (Share) abstractStatus;
AbstractStatus abstractRealStatus = statusRepository.findStatusById(share.getOriginalStatusId());
abstractStatus = abstractRealStatus;
} else if (abstractStatus != null &&
abstractStatus.getType().equals(StatusType.ANNOUNCEMENT)) {
log.debug("Replacing the announcement by the original status");
Announcement announcement = (Announcement) abstractStatus;
AbstractStatus abstractRealStatus = statusRepository.findStatusById(announcement.getOriginalStatusId());
abstractStatus = abstractRealStatus;
}
Status status = (Status) abstractStatus;
Group group = null;
if (status.getGroupId() != null) {
group = groupService.getGroupById(status.getDomain(), status.getGroupId());
if (group.isArchivedGroup()) {
throw new ArchivedGroupException();
}
}
if (!status.getReplyTo().equals("")) {
log.debug("Replacing the status by the status at the origin of the discussion");
// Original status is also a reply, replying to the real original status instead
AbstractStatus abstractRealOriginalStatus = statusRepository.findStatusById(status.getDiscussionId());
if (abstractRealOriginalStatus == null ||
!abstractRealOriginalStatus.getType().equals(StatusType.STATUS)) {
throw new ReplyStatusException();
}
Status realOriginalStatus = (Status) abstractRealOriginalStatus;
Status replyStatus = createStatus(
content,
realOriginalStatus.getStatusPrivate(),
group,
realOriginalStatus.getStatusId(),
status.getStatusId(),
status.getUsername(),
attachmentIds);
discussionRepository.addReplyToDiscussion(realOriginalStatus.getStatusId(), replyStatus.getStatusId());
} else {
log.debug("Replying directly to the status at the origin of the disucssion");
// The original status of the discussion is the one we reply to
Status replyStatus =
createStatus(content,
status.getStatusPrivate(),
group,
status.getStatusId(),
status.getStatusId(),
status.getUsername(),
attachmentIds);
discussionRepository.addReplyToDiscussion(status.getStatusId(), replyStatus.getStatusId());
}
}
private Status createStatus(String content,
boolean statusPrivate,
Group group,
String discussionId,
String replyTo,
String replyToUsername,
Collection<String> attachmentIds) {
return createStatus(
content,
statusPrivate,
group,
discussionId,
replyTo,
replyToUsername,
attachmentIds,
null, null);
}
private Status createStatus(String content,
boolean statusPrivate,
Group group,
String discussionId,
String replyTo,
String replyToUsername,
Collection<String> attachmentIds,
User user,
String geoLocalization) {
content = StringEscapeUtils.unescapeHtml(content);
long startTime = 0;
if (log.isInfoEnabled()) {
startTime = Calendar.getInstance().getTimeInMillis();
log.debug("Creating new status : {}", content);
}
String currentLogin;
if (user == null) {
currentLogin = authenticationService.getCurrentUser().getLogin();
} else {
currentLogin = user.getLogin();
}
String domain = DomainUtil.getDomainFromLogin(currentLogin);
Status status =
statusRepository.createStatus(currentLogin,
statusPrivate,
group,
attachmentIds,
content,
discussionId,
replyTo,
replyToUsername,
geoLocalization);
if (attachmentIds != null && attachmentIds.size() > 0) {
for (String attachmentId : attachmentIds) {
statusAttachmentRepository.addAttachmentId(status.getStatusId(),
attachmentId);
}
}
// add status to the timeline
addStatusToTimelineAndNotify(currentLogin, status);
if (status.getStatusPrivate()) { // Private status
// add status to the mentioned users' timeline
manageMentions(status, null, currentLogin, domain, new ArrayList<String>());
} else { // Public status
Collection<String> followersForUser = followerRepository.findFollowersForUser(currentLogin);
// add status to the dayline, userline
String day = StatsService.DAYLINE_KEY_FORMAT.format(status.getStatusDate());
daylineRepository.addStatusToDayline(status, day);
userlineRepository.addStatusToUserline(status.getLogin(), status.getStatusId());
// add the status to the group line and group followers
manageGroups(status, group, followersForUser);
// tag managgement
manageStatusTags(status, group);
// add status to the mentioned users' timeline
manageMentions(status, group, currentLogin, domain, followersForUser);
// Increment status count for the current user
counterRepository.incrementStatusCounter(currentLogin);
// Add to the searchStatus engine
searchService.addStatus(status);
// add status to the company wall
addToCompanyWall(status, group);
}
if (log.isInfoEnabled()) {
long finishTime = Calendar.getInstance().getTimeInMillis();
log.info("Status created in " + (finishTime - startTime) + "ms.");
}
return status;
}
private void manageGroups(Status status, Group group, Collection<String> followersForUser) {
if (group != null) {
grouplineRepository.addStatusToGroupline(group.getGroupId(), status.getStatusId());
Collection<String> groupMemberLogins = groupMembersRepository.findMembers(group.getGroupId()).keySet();
// For all people following the group
for (String groupMemberLogin : groupMemberLogins) {
addStatusToTimelineAndNotify(groupMemberLogin, status);
}
if (isPublicGroup(group)) { // for people not following the group but following the user
for (String followerLogin : followersForUser) {
if (!groupMemberLogins.contains(followerLogin)) {
addStatusToTimelineAndNotify(followerLogin, status);
}
}
}
} else { // only people following the user
for (String followerLogin : followersForUser) {
addStatusToTimelineAndNotify(followerLogin, status);
}
}
}
private void addToCompanyWall(Status status, Group group) {
if (isPublicGroup(group)) {
domainlineRepository.addStatusToDomainline(status.getDomain(), status.getStatusId());
}
}
/**
* Parses the status to find tags, and add those tags to the TagLine and the Trends.
* <p/>
* The Tatami Bot is a specific use case : as it sends a lot of statuses, it may pollute the global trends,
* so it is excluded from it.
*/
private void manageStatusTags(Status status, Group group) {
Matcher m = PATTERN_HASHTAG.matcher(status.getContent());
while (m.find()) {
String tag = m.group(2);
if (tag != null && !tag.isEmpty() && !tag.contains("#")) {
log.debug("Found tag : {}", tag);
taglineRepository.addStatusToTagline(tag, status);
tagCounterRepository.incrementTagCounter(status.getDomain(), tag);
//Excludes the Tatami Bot from the global trend
if (!status.getUsername().equals(Constants.TATAMIBOT_NAME)) {
trendsRepository.addTag(status.getDomain(), tag);
}
userTrendRepository.addTag(status.getLogin(), tag);
// Add the status to all users following this tag
addStatusToTagFollowers(status, group, tag);
}
}
}
private void manageMentions(Status status, Group group, String currentLogin, String domain, Collection<String> followersForUser) {
Matcher m = PATTERN_LOGIN.matcher(status.getContent());
while (m.find()) {
String mentionedUsername = extractUsernameWithoutAt(m.group());
if (mentionedUsername != null &&
!mentionedUsername.equals(currentLogin) &&
!followersForUser.contains(mentionedUsername)) {
log.debug("Mentionning : {}", mentionedUsername);
String mentionedLogin =
DomainUtil.getLoginFromUsernameAndDomain(mentionedUsername, domain);
// If this is a private group, and if the mentioned user is not in the group, he will not see the status
if (!isPublicGroup(group)) {
Collection<String> groupIds = userGroupRepository.findGroups(mentionedLogin);
if (groupIds.contains(group.getGroupId())) { // The user is part of the private group
mentionUser(mentionedLogin, status);
}
} else { // This is a public status
mentionUser(mentionedLogin, status);
}
}
}
}
private void addStatusToTagFollowers(Status status, Group group, String tag) {
Collection<String> followersForTag =
tagFollowerRepository.findFollowers(status.getDomain(), tag);
if (isPublicGroup(group)) { // This is a public status
for (String followerLogin : followersForTag) {
addStatusToTimelineAndNotify(followerLogin, status);
}
} else { // This is a private status
for (String followerLogin : followersForTag) {
Collection<String> groupIds = userGroupRepository.findGroups(followerLogin);
if (groupIds.contains(group.getGroupId())) { // The user is part of the private group
addStatusToTimelineAndNotify(followerLogin, status);
}
}
}
}
/**
* A status that mentions a user is put in the user's mentionline and in his timeline.
* The mentioned user can also be notified by email or iOS push.
*/
private void mentionUser(String mentionedLogin, Status status) {
addStatusToTimelineAndNotify(mentionedLogin, status);
mentionService.mentionUser(mentionedLogin, status);
}
private String extractUsernameWithoutAt(String dest) {
return dest.substring(1, dest.length());
}
private boolean isPublicGroup(Group group) {
return group == null || group.isPublicGroup();
}
/**
* Adds the status to the timeline and notifies the user with Atmosphere.
*/
private void addStatusToTimelineAndNotify(String login, Status status) {
timelineRepository.addStatusToTimeline(login, status.getStatusId());
atmosphereService.notifyUser(login, status);
}
public void reportStatus(String reportingLogin, String statusId) {
log.debug("Reported Status: ", statusId);
String domain = DomainUtil.getDomainFromLogin(reportingLogin);
statusReportRepository.reportStatus(domain, statusId, reportingLogin);
mailService.sendReportedStatusEmail(reportingLogin, statusRepository.findStatusById(statusId));
}
private List<String> getAllReportedStatuses(String domain){
return statusReportRepository.findReportedStatuses(domain);
}
public Collection<StatusDTO> findReportedStatuses (){
List<String> reportedStatusId = getAllReportedStatuses(authenticationService.getCurrentUser().getDomain());
return timelineService.buildStatusList(reportedStatusId);
}
public void approveReportedStatus(String statusId){
if(authenticationService.hasAuthenticatedUser() && authenticationService.isCurrentUserInRole("ROLE_ADMIN")){
log.debug("Admin approving reported status {}", statusId );
User currentUser = authenticationService.getCurrentUser();
statusReportRepository.unreportStatus(currentUser.getDomain(), statusId);
} else {
log.warn("Attempt to approve reported status {} but is not admin", statusId);
}
}
public void deleteReportedStatus(String statusId){
if(authenticationService.hasAuthenticatedUser() && authenticationService.isCurrentUserInRole("ROLE_ADMIN")){
log.debug("Admin deleting reported status {}", statusId );
User currentUser = authenticationService.getCurrentUser();
statusReportRepository.unreportStatus(currentUser.getDomain(), statusId);
timelineService.removeStatus(statusId);
} else {
log.warn("Attempt to delete reported status {} but is not admin", statusId);
}
}
}