package in.partake.daemon.impl;
import in.partake.app.PartakeApp;
import in.partake.base.DateTime;
import in.partake.base.PartakeException;
import in.partake.base.TimeUtil;
import in.partake.base.Util;
import in.partake.daemon.IPartakeDaemonTask;
import in.partake.model.IPartakeDAOs;
import in.partake.model.UserEx;
import in.partake.model.access.Transaction;
import in.partake.model.dao.DAOException;
import in.partake.model.dao.DataIterator;
import in.partake.model.dao.PartakeConnection;
import in.partake.model.dao.access.IUserTwitterLinkAccess;
import in.partake.model.daofacade.EventDAOFacade;
import in.partake.model.daofacade.UserDAOFacade;
import in.partake.model.dto.Event;
import in.partake.model.dto.EventTicket;
import in.partake.model.dto.Message;
import in.partake.model.dto.MessageEnvelope;
import in.partake.model.dto.TwitterMessage;
import in.partake.model.dto.UserNotification;
import in.partake.model.dto.UserPreference;
import in.partake.model.dto.UserReceivedMessage;
import in.partake.model.dto.UserTwitterLink;
import in.partake.model.dto.auxiliary.MessageDelivery;
import in.partake.view.util.Helper;
import java.util.UUID;
import javax.servlet.http.HttpServletResponse;
import play.Logger;
import twitter4j.TwitterException;
class SendMessageEnvelopeTask extends Transaction<Void> implements IPartakeDaemonTask {
@Override
public String getName() {
return "SendMessageEnvelopeTask";
}
@Override
public void run() throws Exception {
this.execute();
}
@Override
protected Void doExecute(PartakeConnection con, IPartakeDAOs daos) throws DAOException, PartakeException {
DataIterator<MessageEnvelope> it = daos.getMessageEnvelopeAccess().getIterator(con);
try {
while (it.hasNext()) {
MessageEnvelope envelope = it.next();
if (envelope == null) {
it.remove();
continue;
}
// InvalidAfter 後であれば、message を update して envelope を消去
// TODO: Refine this code!
if (envelope.getInvalidAfter() != null && envelope.getInvalidAfter().isBefore(TimeUtil.getCurrentDateTime())) {
Logger.warn("run : envelope id " + envelope.getId() + " could not be sent : Time out.");
if (envelope.getUserMessageId() != null) {
UserReceivedMessage userMessage = daos.getUserReceivedMessageAccess().find(con, UUID.fromString(envelope.getUserMessageId()));
if (userMessage != null) {
UserReceivedMessage message = new UserReceivedMessage(userMessage);
message.setDelivery(MessageDelivery.FAIL);
message.setModifiedAt(TimeUtil.getCurrentDateTime());
daos.getUserReceivedMessageAccess().put(con, message);
}
}
if (envelope.getTwitterMessageId() != null) {
TwitterMessage twitterMessage = daos.getTwitterMessageAccess().find(con, envelope.getTwitterMessageId());
if (twitterMessage != null) {
TwitterMessage message = new TwitterMessage(twitterMessage);
message.setDelivery(MessageDelivery.FAIL);
message.setModifiedAt(TimeUtil.getCurrentDateTime());
daos.getTwitterMessageAccess().put(con, message);
}
}
if (envelope.getUserNotificationId() != null) {
UserNotification notification = daos.getUserNotificationAccess().find(con, envelope.getUserNotificationId());
if (notification != null) {
UserNotification message = new UserNotification(notification);
message.setDelivery(MessageDelivery.FAIL);
message.setModifiedAt(TimeUtil.getCurrentDateTime());
daos.getUserNotificationAccess().put(con, message);
}
}
it.remove();
continue;
}
// tryAfter 前であれば送らない。
if (envelope.getTryAfter() != null && !envelope.getTryAfter().isBefore(TimeUtil.getCurrentDateTime())) {
Logger.debug("run : envelope id " + envelope.getId() + " should be sent after " + envelope.getTryAfter());
continue;
}
if (envelope.getTwitterMessageId() != null)
sendTwitterMessage(con, daos, it, envelope);
else if (envelope.getUserMessageId() != null)
sendUserMessage(con, daos, it, envelope);
else if (envelope.getUserNotificationId() != null)
sendUserNotification(con, daos, it, envelope);
else {
// Hmm... shouldn't happen.
Logger.error("Shouldn't happen");
assert false;
it.remove();
}
}
} finally {
it.close();
}
return null;
}
/**
* Envelope を送信する。true を返すと送ることができた / もうこれ以上送ってはいけないという意味になる。
* @param envelope
* @return
*/
private void sendUserMessage(PartakeConnection con, IPartakeDAOs daos, DataIterator<MessageEnvelope> it, MessageEnvelope envelope) throws DAOException {
UserReceivedMessage userMessage = daos.getUserReceivedMessageAccess().find(con, UUID.fromString(envelope.getUserMessageId()));
if (userMessage == null) {
didSendUserMessage(con, daos, it, envelope, userMessage, MessageDelivery.FAIL);
return;
}
UserEx receiver = UserDAOFacade.getUserEx(con, daos, userMessage.getReceiverId());
if (receiver == null) {
didSendUserMessage(con, daos, it, envelope, userMessage, MessageDelivery.FAIL);
return;
}
UserPreference pref = daos.getUserPreferenceAccess().find(con, receiver.getId());
if (pref == null)
pref = UserPreference.getDefaultPreference(receiver.getId());
// twitter message を受け取らない設定になっていれば送らない。
if (!pref.isReceivingTwitterMessage()) {
didSendUserMessage(con, daos, it, envelope, userMessage, MessageDelivery.NOT_DELIVERED);
return;
}
UserTwitterLink twitterLinkage = receiver.getTwitterLinkage();
if (twitterLinkage == null || !twitterLinkage.isAuthorized()) {
Logger.warn("sendDirectMessage : envelope id " + envelope.getId() + " could not be sent : No access token");
didSendUserMessage(con, daos, it, envelope, userMessage, MessageDelivery.FAIL);
return;
}
try {
Message message = daos.getMessageAccess().find(con, UUID.fromString(userMessage.getMessageId()));
long twitterId = twitterLinkage.getTwitterId();
Event event = null;
if (userMessage.getEventId() != null)
event = daos.getEventAccess().find(con, userMessage.getEventId());
String url = "http://partake.in/messages/" + userMessage.getId();
String messageBody;
if (event != null) {
int rest = 140;
String format = "[PRTK] %s 「%s」に関する新着メッセージがあります。 : %s";
rest -= Util.codePointCount(format);
rest -= EventDAOFacade.URL_LENGTH;
String title = Util.shorten(event.getTitle(), 30);
rest -= Util.codePointCount(title);
String body = Util.shorten(message.getSubject(), rest);
messageBody = String.format(format, url, title, body);
} else {
int rest = 140;
String format = "[PRTK] %s 新着メッセージがあります。: %s";
rest -= Util.codePointCount(format);
rest -= EventDAOFacade.URL_LENGTH;
String subject = Util.shorten(message.getSubject(), rest);
messageBody = String.format(format, url, subject);
}
PartakeApp.getTwitterService().sendDirectMesage(
twitterLinkage.getAccessToken(), twitterLinkage.getAccessTokenSecret(), twitterId, messageBody);
didSendUserMessage(con, daos, it, envelope, userMessage, MessageDelivery.SUCCESS);
Logger.info("sendDirectMessage : direct message has been sent to " + twitterLinkage.getScreenName());
} catch (NumberFormatException e) {
Logger.error("twitterId has not a number.", e);
didSendUserMessage(con, daos, it, envelope, userMessage, MessageDelivery.FAIL);
} catch (TwitterException e) {
if (updateEnvelopeByTwitterException(con, daos, receiver, envelope, it, e))
didSendUserMessage(con, daos, it, envelope, userMessage, MessageDelivery.FAIL);
}
}
private void didSendUserMessage(PartakeConnection con, IPartakeDAOs daos,
DataIterator<MessageEnvelope> it, MessageEnvelope envelope, UserReceivedMessage message, MessageDelivery delivery) throws DAOException {
UserReceivedMessage userMessage = new UserReceivedMessage(message);
userMessage.setDelivery(delivery);
userMessage.setModifiedAt(TimeUtil.getCurrentDateTime());
daos.getUserReceivedMessageAccess().put(con, userMessage);
it.remove();
}
private void sendTwitterMessage(PartakeConnection con, IPartakeDAOs daos, DataIterator<MessageEnvelope> it, MessageEnvelope envelope) throws DAOException {
TwitterMessage message = daos.getTwitterMessageAccess().find(con, envelope.getTwitterMessageId());
if (message == null) {
Logger.warn("SendMessageEnvelopeTask.sendTwitterMessage : message was null.");
// Since the message was null, we cannot update the message status. So we silently remove this MessageEnvelope.
it.remove();
return;
}
UserEx sender = UserDAOFacade.getUserEx(con, daos, message.getUserId());
if (sender == null) {
Logger.warn("sendTwitterMessage : sender is null.");
failedSendingTwitterMessage(con, daos, it, envelope, message);
return;
}
UserTwitterLink twitterLinkage = sender.getTwitterLinkage();
if (twitterLinkage == null || !twitterLinkage.isAuthorized()) {
Logger.warn("sendTwitterMessage : envelope id " + envelope.getId() + " could not be sent : No access token");
failedSendingTwitterMessage(con, daos, it, envelope, message);
return;
}
try {
PartakeApp.getTwitterService().updateStatus(twitterLinkage.getAccessToken(), twitterLinkage.getAccessTokenSecret(), message.getMessage());
succeededSendingTwitterMessage(con, daos, it, envelope, message);
return;
} catch (TwitterException e) {
if (updateEnvelopeByTwitterException(con, daos, sender, envelope, it, e))
failedSendingTwitterMessage(con, daos, it, envelope, message);
}
}
private void succeededSendingTwitterMessage(PartakeConnection con, IPartakeDAOs daos, DataIterator<MessageEnvelope> it, MessageEnvelope envelope, TwitterMessage message) throws DAOException {
TwitterMessage twitterMessage = new TwitterMessage(message);
twitterMessage.setDelivery(MessageDelivery.SUCCESS);
twitterMessage.setModifiedAt(TimeUtil.getCurrentDateTime());
daos.getTwitterMessageAccess().put(con, twitterMessage);
it.remove();
}
private void failedSendingTwitterMessage(PartakeConnection con, IPartakeDAOs daos, DataIterator<MessageEnvelope> it, MessageEnvelope envelope, TwitterMessage message) throws DAOException {
TwitterMessage twitterMessage = new TwitterMessage(message);
twitterMessage.setDelivery(MessageDelivery.FAIL);
twitterMessage.setModifiedAt(TimeUtil.getCurrentDateTime());
daos.getTwitterMessageAccess().put(con, twitterMessage);
it.remove();
}
private void sendUserNotification(PartakeConnection con, IPartakeDAOs daos, DataIterator<MessageEnvelope> it, MessageEnvelope envelope) throws DAOException {
UserNotification notification = daos.getUserNotificationAccess().find(con, envelope.getUserNotificationId());
if (notification == null) {
failedSendingUserNotification(con, daos, it, envelope, notification);
return;
}
UserEx sender = UserDAOFacade.getUserEx(con, daos, notification.getUserId());
if (sender == null) {
Logger.warn("sendTwitterMessage : sender is null.");
failedSendingUserNotification(con, daos, it, envelope, notification);
return;
}
UserTwitterLink twitterLinkage = sender.getTwitterLinkage();
if (twitterLinkage == null || !twitterLinkage.isAuthorized()) {
Logger.warn("sendTwitterMessage : envelope id " + envelope.getId() + " could not be sent : No access token");
failedSendingUserNotification(con, daos, it, envelope, notification);
return;
}
EventTicket ticket = daos.getEventTicketAccess().find(con, notification.getTicketId());
if (ticket == null) {
failedSendingUserNotification(con, daos, it, envelope, notification);
return;
}
Event event = daos.getEventAccess().find(con, ticket.getEventId());
if (event == null) {
failedSendingUserNotification(con, daos, it, envelope, notification);
return;
}
String messageBody = buildUserNotificationMessageBody(notification, event, ticket);
assert messageBody != null;
if (messageBody == null) {
failedSendingUserNotification(con, daos, it, envelope, notification);
return;
}
try {
PartakeApp.getTwitterService().sendDirectMesage(
twitterLinkage.getAccessToken(), twitterLinkage.getAccessTokenSecret(), twitterLinkage.getTwitterId(), messageBody);
succeededSendingUserNotification(con, daos, it, envelope, notification);
return;
} catch (TwitterException e) {
if (updateEnvelopeByTwitterException(con, daos, sender, envelope, it, e))
failedSendingUserNotification(con, daos, it, envelope, notification);
}
}
static String buildUserNotificationMessageBody(UserNotification notification, Event event, EventTicket ticket) {
switch (notification.getNotificationType()) {
case EVENT_ONEDAY_BEFORE_REMINDER: {
int rest = 140;
String format = "[PRTK] 「%s」は%sに開始です。あなたの参加は確定しています。 %s";
rest -= Util.codePointCount(format);
String beginDate = Helper.readableDate(event.getBeginDate());
rest -= Util.codePointCount(beginDate);
String url = event.getEventURL();
rest -= EventDAOFacade.URL_LENGTH;
String title = Util.shorten(event.getTitle(), rest);
return String.format(format, title, beginDate, url);
}
case HALF_DAY_BEFORE_REMINDER_FOR_RESERVATION: {
int rest = 140;
String format = "[PRTK] 「%s」の締め切りは%sです。参加・不参加を確定してください。 %s";
rest -= Util.codePointCount(format);
String deadline = Helper.readableDate(ticket.acceptsTill(event));
rest -= Util.codePointCount(deadline);
String url = event.getEventURL();
rest -= EventDAOFacade.URL_LENGTH;
String title = Util.shorten(event.getTitle(), rest);
return String.format(format, title, deadline, url);
}
case ONE_DAY_BEFORE_REMINDER_FOR_RESERVATION: {
int rest = 140;
String format = "[PRTK] 「%s」の締め切りは%sです。参加・不参加を確定してください。 %s";
rest -= Util.codePointCount(format);
String deadline = Helper.readableDate(ticket.acceptsTill(event));
rest -= Util.codePointCount(deadline);
String url = event.getEventURL();
rest -= EventDAOFacade.URL_LENGTH;
String title = Util.shorten(event.getTitle(), rest);
return String.format(format, title, deadline, url);
}
case BECAME_TO_BE_CANCELLED: {
int rest = 140;
String format = "[PRTK] 「%s」で参加者から補欠へ繰り下がりました。 %s";
rest -= Util.codePointCount(format);
String url = event.getEventURL();
rest -= EventDAOFacade.URL_LENGTH;
String title = Util.shorten(event.getTitle(), rest);
return String.format(format, title, url);
}
case BECAME_TO_BE_ENROLLED: {
int rest = 140;
String format = "[PRTK] 「%s」で補欠から参加者へ繰り上がりました。 %s";
rest -= Util.codePointCount(format);
String url = event.getEventURL();
rest -= EventDAOFacade.URL_LENGTH;
String title = Util.shorten(event.getTitle(), rest);
return String.format(format, title, url);
}
default:
assert false;
return null;
}
}
private void succeededSendingUserNotification(PartakeConnection con, IPartakeDAOs daos, DataIterator<MessageEnvelope> it, MessageEnvelope envelope, UserNotification notification) throws DAOException {
UserNotification userNotification = new UserNotification(notification);
userNotification.setDelivery(MessageDelivery.SUCCESS);
userNotification.setModifiedAt(TimeUtil.getCurrentDateTime());
daos.getUserNotificationAccess().put(con, userNotification);
it.remove();
}
private void failedSendingUserNotification(PartakeConnection con, IPartakeDAOs daos, DataIterator<MessageEnvelope> it, MessageEnvelope envelope, UserNotification notification) throws DAOException {
UserNotification userNotification = new UserNotification(notification);
userNotification.setDelivery(MessageDelivery.FAIL);
userNotification.setModifiedAt(TimeUtil.getCurrentDateTime());
daos.getUserNotificationAccess().put(con, userNotification);
it.remove();
}
/**
* @return true if <code>it</code> was removed.
*/
private boolean updateEnvelopeByTwitterException(PartakeConnection con, IPartakeDAOs daos,
UserEx user, MessageEnvelope envelope, DataIterator<MessageEnvelope> it, TwitterException e) throws DAOException {
if (e.isCausedByNetworkIssue()) {
Logger.warn("Twitter Unreachable?", e);
// Retry after 10 minutes later.
DateTime retryAfter = new DateTime(TimeUtil.getCurrentTime() + 600 * 1000);
MessageEnvelope newEnvelope = new MessageEnvelope(envelope);
newEnvelope.updateForSendingFailure(retryAfter);
it.update(newEnvelope);
return false;
}
if (e.exceededRateLimitation()) {
Logger.warn("Twitter Rate Limination : " + envelope.getId() + " was failed to deliver.", e);
DateTime retryAfter = new DateTime(TimeUtil.getCurrentTime() + e.getRetryAfter() * 1000);
MessageEnvelope newEnvelope = new MessageEnvelope(envelope);
newEnvelope.updateForSendingFailure(retryAfter);
it.update(newEnvelope);
return false;
}
if (e.getStatusCode() == HttpServletResponse.SC_UNAUTHORIZED) {
markAsUnauthorizedUser(con, daos, user);
Logger.info("Unauthorized User : " + envelope.getId() + " was failed to deliver.", e);
// We cannot send envelopes to unauthorized user.
it.remove();
return true;
}
// Unknown error. Retry.
Logger.warn("Unknown Error : " + envelope.getId() + " was failed to deliver.", e);
// Retry after 5 minutes later.
DateTime retryAfter = new DateTime(TimeUtil.getCurrentTime() + 600 * 1000);
MessageEnvelope newEnvelope = new MessageEnvelope(envelope);
newEnvelope.updateForSendingFailure(retryAfter);
it.update(newEnvelope);
return false;
}
private void markAsUnauthorizedUser(PartakeConnection con, IPartakeDAOs daos, UserEx user) {
IUserTwitterLinkAccess access = daos.getTwitterLinkageAccess();
UserTwitterLink linkage = new UserTwitterLink(user.getTwitterLinkage());
linkage.markAsUnauthorized();
try {
// TODO UserExが参照するTwitterLinkageが更新されたため、UserExのキャッシュを破棄あるいは更新する必要がある
access.put(con, linkage);
} catch (DAOException ignore) {
Logger.warn("DAOException is thrown but it's ignored.", ignore);
}
}
}