from uuid import uuid1 from pycassa.types import CompositeType from pylons import tmpl_context as c from pylons import app_globals as g from pylons.i18n import _, N_ from r2.lib.unicode import _force_unicode from r2.lib.db import tdb_cassandra from r2.lib.db.thing import Thing, NotFound from r2.lib.memoize import memoize from r2.lib.utils import Enum, to_datetime, to_date from r2.models.subreddit import Subreddit, Frontpage PROMOTE_STATUS = Enum("unpaid", "unseen", "accepted", "rejected", "pending", "promoted", "finished") class PriorityLevel(object): name = '' _text = N_('') _description = N_('') value = 1 # Values are from 1 (highest) to 100 (lowest) default = False inventory_override = False cpm = True # Non-cpm is percentage, will fill unsold impressions def __repr__(self): return "<PriorityLevel %s: %s>" % (self.name, self.value) @property def text(self):
class Email(object): handler = EmailHandler() Kind = Enum( "SHARE", "FEEDBACK", "ADVERTISE", "OPTOUT", "OPTIN", "VERIFY_EMAIL", "RESET_PASSWORD", "BID_PROMO", "ACCEPT_PROMO", "REJECT_PROMO", "QUEUED_PROMO", "LIVE_PROMO", "FINISHED_PROMO", "NEW_PROMO", "NERDMAIL", "GOLDMAIL", "PASSWORD_CHANGE", "EMAIL_CHANGE", "REFUNDED_PROMO", "VOID_PAYMENT", "GOLD_GIFT_CODE", ) subjects = { Kind.SHARE: _("[reddit] %(user)s has shared a link with you"), Kind.FEEDBACK: _("[feedback] feedback from '%(user)s'"), Kind.ADVERTISE: _("[ad_inq] feedback from '%(user)s'"), Kind.OPTOUT: _("[reddit] email removal notice"), Kind.OPTIN: _("[reddit] email addition notice"), Kind.RESET_PASSWORD: _("[reddit] reset your password"), Kind.VERIFY_EMAIL: _("[reddit] verify your email address"), Kind.BID_PROMO: _("[reddit] your bid has been accepted"), Kind.ACCEPT_PROMO: _("[reddit] your promotion has been accepted"), Kind.REJECT_PROMO: _("[reddit] your promotion has been rejected"), Kind.QUEUED_PROMO: _("[reddit] your promotion has been charged"), Kind.LIVE_PROMO: _("[reddit] your promotion is now live"), Kind.FINISHED_PROMO: _("[reddit] your promotion has finished"), Kind.NEW_PROMO: _("[reddit] your promotion has been created"), Kind.NERDMAIL: _("[reddit] hey, nerd!"), Kind.GOLDMAIL: _("[reddit] reddit gold activation link"), Kind.PASSWORD_CHANGE: _("[reddit] your password has been changed"), Kind.EMAIL_CHANGE: _("[reddit] your email address has been changed"), Kind.REFUNDED_PROMO: _("[reddit] your campaign didn't get enough impressions"), Kind.VOID_PAYMENT: _("[reddit] your payment has been voided"), Kind.GOLD_GIFT_CODE: _("[reddit] your reddit gold gift code"), } def __init__(self, user, thing, email, from_name, date, ip, banned_ip, kind, msg_hash, body='', from_addr='', reply_to=''): self.user = user self.thing = thing self.to_addr = email self.fr_addr = from_addr self._from_name = from_name self.date = date self.ip = ip self.banned_ip = banned_ip self.kind = kind self.sent = False self.body = body self.msg_hash = msg_hash self.reply_to = reply_to self.subject = self.subjects.get(kind, "") try: self.subject = self.subject % dict(user=self.from_name()) except UnicodeDecodeError: self.subject = self.subject % dict(user="******") def from_name(self): if not self.user: name = "%(name)s" elif self._from_name != self.user.name: name = "%(name)s (%(uname)s)" else: name = "%(uname)s" return name % dict(name=self._from_name, uname=self.user.name if self.user else '') @classmethod def get_unsent(cls, max_date, batch_limit=50, kind=None): for e in cls.handler.from_queue(max_date, batch_limit=batch_limit, kind=kind): yield cls(*e) def should_queue(self): return (not self.user or not self.user._spam) and \ (not self.thing or not self.thing._spam) and \ not self.banned_ip and \ (self.kind == self.Kind.OPTOUT or not has_opted_out(self.to_addr)) def set_sent(self, date=None, rejected=False): if not self.sent: self.date = date or datetime.datetime.now(g.tz) t = self.handler.reject_table if rejected else self.handler.track_table try: t.insert().values({ t.c.account_id: self.user._id if self.user else 0, t.c.to_addr: self.to_addr, t.c.fr_addr: self.fr_addr, t.c.reply_to: self.reply_to, t.c.ip: self.ip, t.c.fullname: self.thing._fullname if self.thing else "", t.c.date: self.date, t.c.kind: self.kind, t.c.msg_hash: self.msg_hash, }).execute() except: print "failed to send message" self.sent = True def to_MIMEText(self): def utf8(s, reject_newlines=True): if reject_newlines and '\n' in s: raise HeaderParseError( 'header value contains unexpected newline: {!r}'.format(s)) return s.encode('utf8') if isinstance(s, unicode) else s fr = '"%s" <%s>' % ( self.from_name().replace('"', ''), self.fr_addr.replace('>', ''), ) if not fr.startswith('-') and not self.to_addr.startswith( '-'): # security msg = MIMEText(utf8(self.body, reject_newlines=False)) msg.set_charset('utf8') msg['To'] = utf8(self.to_addr) msg['From'] = utf8(fr) msg['Subject'] = utf8(self.subject) if self.user: msg['X-Reddit-username'] = utf8(self.user.name) msg['X-Reddit-ID'] = self.msg_hash if self.reply_to: msg['Reply-To'] = utf8(self.reply_to) return msg return None
class Email(object): handler = EmailHandler() # Do not modify in any way other than appending new items! # Database tables storing mail stuff use an int column as an index into # this Enum, so anything other than appending new items breaks mail history. Kind = Enum( "SHARE", "FEEDBACK", "ADVERTISE", "OPTOUT", "OPTIN", "VERIFY_EMAIL", "RESET_PASSWORD", "BID_PROMO", "ACCEPT_PROMO", "REJECT_PROMO", "QUEUED_PROMO", "LIVE_PROMO", "FINISHED_PROMO", "NEW_PROMO", "NERDMAIL", "GOLDMAIL", "PASSWORD_CHANGE", "EMAIL_CHANGE", "REFUNDED_PROMO", "VOID_PAYMENT", "GOLD_GIFT_CODE", "SUSPICIOUS_PAYMENT", "FRAUD_ALERT", "USER_FRAUD", "MESSAGE_NOTIFICATION", "ADS_ALERT", "EDITED_LIVE_PROMO", ) # Do not remove anything from this dictionary! See above comment. subjects = { Kind.SHARE: _("[reddit] %(user)s has shared a link with you"), Kind.FEEDBACK: _("[feedback] feedback from '%(user)s'"), Kind.ADVERTISE: _("[advertising] feedback from '%(user)s'"), Kind.OPTOUT: _("[reddit] email removal notice"), Kind.OPTIN: _("[reddit] email addition notice"), Kind.RESET_PASSWORD: _("[reddit] reset your password"), Kind.VERIFY_EMAIL: _("[reddit] verify your email address"), Kind.BID_PROMO: _("[reddit] your budget has been accepted"), Kind.ACCEPT_PROMO: _("[reddit] your promotion has been accepted"), Kind.REJECT_PROMO: _("[reddit] your promotion has been rejected"), Kind.QUEUED_PROMO: _("[reddit] your promotion has been charged"), Kind.LIVE_PROMO: _("[reddit] your promotion is now live"), Kind.FINISHED_PROMO: _("[reddit] your promotion has finished"), Kind.NEW_PROMO: _("[reddit] your promotion has been created"), Kind.EDITED_LIVE_PROMO: _("[reddit] your promotion edit is being approved"), Kind.NERDMAIL: _("[reddit] hey, nerd!"), Kind.GOLDMAIL: _("[reddit] reddit gold activation link"), Kind.PASSWORD_CHANGE: _("[reddit] your password has been changed"), Kind.EMAIL_CHANGE: _("[reddit] your email address has been changed"), Kind.REFUNDED_PROMO: _("[reddit] your campaign didn't get enough impressions"), Kind.VOID_PAYMENT: _("[reddit] your payment has been voided"), Kind.GOLD_GIFT_CODE: _("[reddit] your reddit gold gift code"), Kind.SUSPICIOUS_PAYMENT: _("[selfserve] suspicious payment alert"), Kind.FRAUD_ALERT: _("[selfserve] fraud alert"), Kind.USER_FRAUD: _("[selfserve] a user has committed fraud"), Kind.MESSAGE_NOTIFICATION: _("[reddit] message notification"), Kind.ADS_ALERT: _("[reddit] Ads Alert"), } def __init__(self, user, thing, email, from_name, date, ip, kind, msg_hash, body='', from_addr='', reply_to=''): self.user = user self.thing = thing self.to_addr = email self.fr_addr = from_addr self._from_name = from_name self.date = date self.ip = ip self.kind = kind self.sent = False self.body = body self.msg_hash = msg_hash self.reply_to = reply_to self.subject = self.subjects.get(kind, "") try: self.subject = self.subject % dict(user=self.from_name()) except UnicodeDecodeError: self.subject = self.subject % dict(user="******") def from_name(self): if not self.user: name = "%(name)s" elif self._from_name != self.user.name: name = "%(name)s (%(uname)s)" else: name = "%(uname)s" return name % dict(name=self._from_name, uname=self.user.name if self.user else '') @classmethod def get_unsent(cls, max_date, batch_limit=50, kind=None): for e in cls.handler.from_queue(max_date, batch_limit=batch_limit, kind=kind): yield cls(*e) def should_queue(self): return (not self.user or not self.user._spam) and \ (not self.thing or not self.thing._spam) and \ (self.kind == self.Kind.OPTOUT or not has_opted_out(self.to_addr)) def set_sent(self, date=None, rejected=False): if not self.sent: self.date = date or datetime.datetime.now(g.tz) t = self.handler.reject_table if rejected else self.handler.track_table try: t.insert().values({ t.c.account_id: self.user._id if self.user else 0, t.c.to_addr: self.to_addr, t.c.fr_addr: self.fr_addr, t.c.reply_to: self.reply_to, t.c.ip: self.ip, t.c.fullname: self.thing._fullname if self.thing else "", t.c.date: self.date, t.c.kind: self.kind, t.c.msg_hash: self.msg_hash, }).execute() except: print "failed to send message" self.sent = True def to_MIMEText(self): def utf8(s, reject_newlines=True): if reject_newlines and '\n' in s: raise HeaderParseError( 'header value contains unexpected newline: {!r}'.format(s)) return s.encode('utf8') if isinstance(s, unicode) else s fr = '"%s" <%s>' % ( self.from_name().replace('"', ''), self.fr_addr.replace('>', ''), ) # Addresses that start with a dash could confuse poorly-written # software's argument parsers, and thus are disallowed by default in # Postfix: http://www.postfix.org/postconf.5.html#allow_min_user if not fr.startswith('-') and not self.to_addr.startswith('-'): msg = MIMEText(utf8(self.body, reject_newlines=False)) msg.set_charset('utf8') msg['To'] = utf8(self.to_addr) msg['From'] = utf8(fr) msg['Subject'] = utf8(self.subject) timestamp = time.mktime(self.date.timetuple()) msg['Date'] = utf8(email.utils.formatdate(timestamp)) if self.user: msg['X-Reddit-username'] = utf8(self.user.name) msg['X-Reddit-ID'] = self.msg_hash if self.reply_to: msg['Reply-To'] = utf8(self.reply_to) return msg return None
# WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License for # the specific language governing rights and limitations under the License. # # The Original Code is reddit. # # The Original Developer is the Initial Developer. The Initial Developer of the # Original Code is reddit. # # All portions of the code written by reddit are Copyright (c) 2006-2012 # reddit, Inc. All Rights Reserved. ################################################################################ from r2.lib.db.thing import Thing, NotFound from r2.lib.utils import Enum from r2.models import Link PaymentState = Enum('UNPAID', 'PAID', 'FREEBIE') TransactionCode = Enum('NEW', 'FREEBIE') class PromoCampaign(Thing): _defaults = dict(link_id=None, sr_name='', owner_id=None, payment_state=PaymentState.UNPAID, trans_id=TransactionCode.NEW, trans_error=None, bid=None, start_date=None, end_date=None) @classmethod
class Bid(Sessionized, Base): __tablename__ = "bids" STATUS = Enum("AUTH", "CHARGE", "REFUND", "VOID") # will be unique from authorize transaction = Column(BigInteger, primary_key = True, autoincrement = False) # identifying characteristics account_id = Column(BigInteger, index = True, nullable = False) pay_id = Column(BigInteger, index = True, nullable = False) thing_id = Column(BigInteger, index = True, nullable = False) # breadcrumbs ip = Column(Inet) date = Column(DateTime(timezone = True), default = safunc.now(), nullable = False) # bid information: bid = Column(Float, nullable = False) charge = Column(Float) status = Column(Integer, nullable = False, default = STATUS.AUTH) # make this a primary key as well so that we can have more than # one freebie per campaign campaign = Column(Integer, default = 0, primary_key = True) @classmethod def _new(cls, trans_id, user, pay_id, thing_id, bid, campaign = 0): bid = Bid(trans_id, user, pay_id, thing_id, getattr(request, 'ip', '0.0.0.0'), bid = bid, campaign = campaign) bid._commit() return bid # @classmethod # def for_transactions(cls, transids): # transids = filter(lambda x: x != 0, transids) # if transids: # q = cls.query() # q = q.filter(or_(*[cls.transaction == i for i in transids])) # return dict((p.transaction, p) for p in q) # return {} def set_status(self, status): if self.status != status: self.status = status self._commit() def auth(self): self.set_status(self.STATUS.AUTH) def is_auth(self): return (self.status == self.STATUS.AUTH) def void(self): self.set_status(self.STATUS.VOID) def is_void(self): return (self.status == self.STATUS.VOID) def charged(self): self.charge = self.bid self.set_status(self.STATUS.CHARGE) self._commit() def is_charged(self): """ Returns True if transaction has been charged with authorize.net or is a freebie with "charged" status. """ return (self.status == self.STATUS.CHARGE) def refund(self, amount): current_charge = self.charge or self.bid # needed if charged() not # setting charge attr self.charge = current_charge - amount self.set_status(self.STATUS.REFUND) self._commit() def is_refund(self): return (self.status == self.STATUS.REFUND)
class Vote(object): # CUSTOM: voting model DIRECTIONS = Enum("up", "down", "unup", "undown", "onon", "onoff", "offon", "offoff", "unvote") SERIALIZED_DIRECTIONS = { # user vote directions DIRECTIONS.up: 1, DIRECTIONS.down: -1, DIRECTIONS.unvote: 0, DIRECTIONS.unup: 11, DIRECTIONS.undown: -11, # state DIRECTIONS.onon: 3, DIRECTIONS.onoff: 4, DIRECTIONS.offon: 5, DIRECTIONS.offoff: 6, } DESERIALIZED_DIRECTIONS = { v: k for k, v in SERIALIZED_DIRECTIONS.iteritems() } def __init__(self, user, thing, direction, date, data=None, effects=None, get_previous_vote=True, event_data=None, vote_direction=None): if not thing.is_votable: raise TypeError("Can't create vote on unvotable thing %s" % thing) if direction not in self.DIRECTIONS: raise ValueError("Invalid vote direction: %s" % direction) self.user = user self.thing = thing self.direction = direction # CUSTOM: voting model self.vote_direction = vote_direction self.date = date.replace(tzinfo=g.tz) self.data = data self.event_data = event_data # see if the user has voted on this thing before if get_previous_vote: self.previous_vote = VoteDetailsByThing.get_vote(user, thing) if self.previous_vote: # XXX: why do we keep the old date? self.date = self.previous_vote.date.replace(tzinfo=g.tz) else: self.previous_vote = None self.effects = VoteEffects(self, effects) def __eq__(self, other): return (self.user == other.user and self.thing == other.thing and self.direction == other.direction) def __ne__(self, other): return not self == other @classmethod def serialize_direction(cls, direction): """Convert the DIRECTIONS enum to values used when storing.""" if direction not in cls.DIRECTIONS: raise ValueError("Invalid vote direction: %s" % direction) return cls.SERIALIZED_DIRECTIONS[direction] @classmethod def deserialize_direction(cls, direction): """Convert stored vote direction value back to DIRECTIONS enum.""" direction = int(direction) if direction not in cls.DESERIALIZED_DIRECTIONS: raise ValueError("Invalid vote direction: %s" % direction) return cls.DESERIALIZED_DIRECTIONS[direction] @property def _id(self): return "%s_%s" % (self.user._id36, self.thing._id36) @property def affected_thing_attr(self): """The attr on the thing this vote will increment.""" if not self.effects.affects_score: return None if self.is_upvote: return "_ups" elif self.is_downvote: return "_downs" # CUSTOM: voting model @property def is_upvote(self): # backward compatibility return self.vote_direction == self.DIRECTIONS.up or self.direction == self.DIRECTIONS.up @property def is_downvote(self): # backward compatibility return self.vote_direction == self.DIRECTIONS.down or self.direction == self.DIRECTIONS.down @property def is_unupvote(self): return self.vote_direction == self.DIRECTIONS.unup @property def is_undownvote(self): return self.vote_direction == self.DIRECTIONS.undown # vote directions as state @property def is_ononvote(self): return self.direction == self.DIRECTIONS.onon @property def is_onoffvote(self): return self.direction == self.DIRECTIONS.onoff @property def is_offonvote(self): return self.direction == self.DIRECTIONS.offon @property def is_offoffvote(self): return self.direction == self.DIRECTIONS.offoff @property def is_self_vote(self): """Whether the voter is also the author of the thing voted on.""" return self.user._id == self.thing.author_id @property def is_automatic_initial_vote(self): """Whether this is the automatic vote cast on things when posted.""" return self.is_self_vote and not self.previous_vote @property def delay(self): """How long after the thing was posted that the vote was cast.""" if self.is_automatic_initial_vote: return timedelta(0) return self.date - self.thing._date def apply_effects(self): # CUSTOM: voting model """Apply the effects of the vote to the thing that was voted on.""" # remove the old vote if self.previous_vote and self.is_unupvote and self.effects.affects_score: g.log.warning("!!! apply_effects() decrementing _ups") self.thing._incr("_ups", -1) elif self.previous_vote and self.is_undownvote and self.effects.affects_score: g.log.warning("!!! apply_effects() decrementing _downs") self.thing._incr("_downs", -1) # add the new vote if self.affected_thing_attr: g.log.warning("!!! apply_effects() incrementing %s" % self.affected_thing_attr) self.thing._incr(self.affected_thing_attr, 1) if self.effects.affects_karma: change = self.effects.karma_change # CUSTOM: voting model, previous_vote accounted for in affects_karma # if self.previous_vote: # change -= self.previous_vote.effects.karma_change if change: self.thing.author_slow.incr_karma( kind=self.thing.affects_karma_type, sr=self.thing.subreddit_slow, amt=change, ) hooks.get_hook("vote.apply_effects").call(vote=self) def commit(self): """Apply the vote's effects and persist it.""" if self.previous_vote and self == self.previous_vote: return self.apply_effects() VotesByAccount.write_vote(self) # Always update the search index if the thing has fewer than 20 votes. # When the thing has more votes queue an update less often. if self.thing.num_votes < 20 or self.thing.num_votes % 10 == 0: self.thing.update_search_index(boost_only=True) if self.event_data: g.events.vote_event(self) g.stats.simple_event('vote.total')
from r2.lib.memoize import memoize from r2.lib.template_helpers import get_domain from r2.lib.utils import Enum, UniqueIterator from organic import keep_fresh_links from pylons import g, c from datetime import datetime, timedelta from r2.lib.db.queries import make_results, db_sort, add_queries, merge_results import itertools import random promoted_memo_lifetime = 30 promoted_memo_key = 'cached_promoted_links2' promoted_lock_key = 'cached_promoted_links_lock2' STATUS = Enum("unpaid", "unseen", "accepted", "rejected", "pending", "promoted", "finished") CAMPAIGN = Enum("start", "end", "bid", "sr", "trans_id") @memoize("get_promote_srid") def get_promote_srid(name='promos'): try: sr = Subreddit._by_name(name) except NotFound: sr = Subreddit._new( name=name, title="promoted links", # negative author_ids make this unlisable author_id=-1, type="public",
RandomNSFW, RandomSubscription, Sub, Subreddit, valid_admin_cookie, valid_feed, valid_otp_cookie, ) from r2.lib.db import tdb_cassandra NEVER = datetime(2037, 12, 31, 23, 59, 59) DELETE = datetime(1970, 01, 01, 0, 0, 1) PAGECACHE_POLICY = Enum( # logged in users may use the pagecache as well. "LOGGEDIN_AND_LOGGEDOUT", # only attempt to use pagecache if the current user is not logged in. "LOGGEDOUT_ONLY", # do not use pagecache. "NEVER", ) def pagecache_policy(policy): """Decorate a controller method to specify desired pagecache behaviour. If not specified, the policy will default to LOGGEDOUT_ONLY. """ assert policy in PAGECACHE_POLICY def pagecache_decorator(fn):
from datetime import datetime from uuid import uuid1 from pycassa.system_manager import INT_TYPE, TIME_UUID_TYPE, UTF8_TYPE from pylons import tmpl_context as c from pylons import app_globals as g from pylons.i18n import _, N_ from r2.config import feature from r2.lib.unicode import _force_unicode from r2.lib.db import tdb_cassandra from r2.lib.db.thing import Thing from r2.lib.utils import Enum, to_datetime from r2.models.subreddit import Subreddit, Frontpage PROMOTE_STATUS = Enum("unpaid", "unseen", "accepted", "rejected", "pending", "promoted", "finished", "edited_live") PROMOTE_COST_BASIS = Enum( 'fixed_cpm', 'cpm', 'cpc', ) class PriorityLevel(object): name = '' _text = N_('') _description = N_('') default = False inventory_override = False
class ModmailConversation(Base): """An overall conversation/ticket, potentially multiple messages. owner_fullname - The fullname of the "owner" of this conversation. For modmail, this is a subreddit's fullname. subject - The overall conversation's subject. state - The state of the conversation (new, etc.) num_messages - The total number of messages in the conversation. last_user_update - The last datetime a user made any interaction with the conversation last_mod_update - The last datetime a mod made any interaction with the conversation last_updated - Last time that this conversation had a significant update (new message). Can be combined with the read-state table to determine if a conversation should be considered unread for a user. This is the max of the two values last_user_update and last_mod_update and is a hybrid property on the model. is_internal - Whether the conversation is internal-only. If true, it means that it can only be viewed and interacted with by someone with overall access to the conversations (for example, a moderator in the case of modmail). If the user stops having overall access, they will also lose access to all internal conversations, regardless of whether they participated in them or not. is_highlighted - this field will be true if a conversation has been 'highlighted' and false if the conversation is not 'highlighted' legacy_first_message_id - the ID for the first Message object in this conversation in the legacy messaging system (if any). """ __tablename__ = "modmail_conversations" id = Column(BigInteger, primary_key=True) owner_fullname = Column(String(100), nullable=False, index=True) subject = Column(String(100), nullable=False) state = Column(Integer, index=True, default=0) num_messages = Column(Integer, nullable=False, default=0) last_user_update = Column( DateTime(timezone=True), nullable=False, index=True, default=datetime.min) last_mod_update = Column( DateTime(timezone=True), nullable=False, index=True, default=datetime.min) is_internal = Column(Boolean, nullable=False, default=False) is_auto = Column(Boolean, nullable=False, default=False) is_highlighted = Column(Boolean, nullable=False, default=False) legacy_first_message_id = Column(BigInteger, index=True) messages = relationship( "ModmailMessage", order_by="ModmailMessage.date.desc()", lazy="joined") mod_actions = relationship( "ModmailConversationAction", order_by="ModmailConversationAction.date.desc()", lazy="joined") # DO NOT REARRANGE THE ITEMS IN THIS ENUM - only append new items at bottom # Pseudo-states: mod (is_internal), notification (is_auto), these # pseudo-states act as a conversation type to denote mod only convos and # automoderator generated convos STATE = Enum( "new", "inprogress", "archived", ) @property def author_ids(self): return [message.author_id for message in self.messages] @property def mod_action_account_ids(self): return [mod_action.account_id for mod_action in self.mod_actions] @property def ordered_msg_and_action_ids(self): order_elements = self.messages + self.mod_actions ordered_elements = sorted(order_elements, key=lambda x: x.date) ordered_id_array = [] for element in ordered_elements: key = 'messages' if isinstance(element, ModmailConversationAction): key = 'modActions' ordered_id_array.append({ 'key': key, 'id': to36(element.id) }) return ordered_id_array @property def id36(self): return to36(self.id) @hybrid_property def last_updated(self): if self.last_user_update is None and self.last_mod_update: return self.last_mod_update elif self.last_mod_update is None and self.last_user_update: return self.last_user_update return max(self.last_user_update, self.last_mod_update) @last_updated.expression def last_updated(cls): return func.greatest(cls.last_mod_update, cls.last_user_update) def __init__(self, owner, author, subject, body, is_author_hidden=False, to=None, legacy_first_message_id=None, is_auto=False): self.owner_fullname = owner._fullname self.subject = subject self.legacy_first_message_id = legacy_first_message_id self.num_messages = 0 self.is_internal = False self.is_auto = is_auto participant_id = None if owner.is_moderator_with_perms(author, 'mail'): # check if moderator has addressed the new convo to someone # if they have make the convo not internal and add the 'to' user # as the participant of the conversation. If the 'to' user is also # a moderator of the subreddit convert the conversation to an # internal conversation (i.e. mod discussion). Auto conversations # can never be internal conversations. if to and not owner.is_moderator_with_perms(to, 'mail'): participant_id = to._id elif not is_auto: self.is_internal = True else: participant_id = author._id if is_author_hidden: raise MustBeAModError( 'Must be a mod to hide the message author.') Session.add(self) if participant_id: self.add_participant(participant_id) self.add_message(author, body, is_author_hidden=is_author_hidden) @classmethod def unread_convo_count(cls, user): """Returns a dict by conversation state with a count of all the unread conversations for the passed user. Returns the following dict: { 'new': <count>, 'inprogress': <count>, 'mod': <count>, 'notifications': <count>, 'highlighted': <count>, 'archived': <count> } """ users_modded_subs = user.moderated_subreddits('mail') sr_fullnames = [sr._fullname for sr in users_modded_subs] # Build subquery to select all conversations with an unread # record for the passed user, this will preselect the records # that need to be counted as well as limit the number of # rows that will have to be counted in the main query subquery = Session.query(cls) subquery = subquery.outerjoin( ModmailConversationUnreadState, and_(ModmailConversationUnreadState.account_id == user._id, ModmailConversationUnreadState.conversation_id == cls.id, ModmailConversationUnreadState.active.is_(True))) subquery = subquery.filter( cls.owner_fullname.in_(sr_fullnames), ModmailConversationUnreadState.date.isnot(None)) subquery = subquery.subquery() # Pass the subquery to the count query to retrieve a tuple of # counts for each conversation state query = (Session.query( cls.state, label('internal', func.count(case( [(cls.is_internal, cls.id)], else_=literal_column('NULL')))), label('auto', func.count(case( [(cls.is_auto, cls.id)], else_=literal_column('NULL')))), label('highlighted', func.count(case( [(cls.is_highlighted, cls.id)], else_=literal_column('NULL')))), label('total', func.count(cls.id)),) .select_from(subquery) .group_by(cls.state)) convo_counts = query.all() # initialize result dict so all keys are present result = {state: 0 for state in ModmailConversation.STATE.name} result.update({ 'notifications': 0, 'mod': 0, 'highlighted': 0 }) if not convo_counts: return result for convo_count in convo_counts: (state, internal_count, auto_count, highlighted_count, total_count) = convo_count num_convos = total_count - internal_count result['mod'] += internal_count result['highlighted'] += highlighted_count # Only add count to notifications and higlighted for 'new' # conversations, ignore 'inprogress' and 'archived' conversations if state == ModmailConversation.STATE.new: result['notifications'] += auto_count # Do not double count notification messages that are 'new' num_convos -= auto_count if state in ModmailConversation.STATE: result[ModmailConversation.STATE.name[state]] += num_convos return result @classmethod def _byID(cls, ids, current_user=None): """Return conversation(s) looked up by ID. Additional logic has been added to deal with the case when the current user is passed into the method. When a current_user is passed query.one() returns a keyedtuple, whereas, when a current_user is not passed it returns a single object. """ ids = tup(ids) query = Session.query(cls).filter(cls.id.in_(ids)) if current_user: query = query.add_columns( ModmailConversationUnreadState.date.label("last_unread")) query = query.outerjoin( ModmailConversationUnreadState, and_(ModmailConversationUnreadState.account_id == current_user._id, ModmailConversationUnreadState.conversation_id.in_(ids), ModmailConversationUnreadState.active.is_(True)) ) if len(ids) == 1: try: if not current_user: return query.one() conversation, last_unread = query.one() conversation.last_unread = last_unread return conversation except NoResultFound: raise NotFound results = [] for row in query.all(): if current_user: conversation = row[0] conversation.last_unread = row[1] results.append(conversation) else: results.append(row) return results @classmethod def _by_legacy_message(cls, legacy_message): """Return conversation associated with a legacy message.""" if legacy_message.first_message: legacy_id = legacy_message.first_message else: legacy_id = legacy_message._id query = Session.query(cls).filter_by(legacy_first_message_id=legacy_id) try: return query.one() except NoResultFound: raise NotFound @classmethod def get_mod_conversations(cls, owners, viewer=None, limit=None, after=None, sort='recent', state='all'): """Get the list of conversations for a specific owner or list of owners. The optional `viewer` argument should be an Account that is viewing the listing. It will attach the unread-state data for that viewer. """ if not owners: return [] owners = tup(owners) query = Session.query(cls) fullnames = [owner._fullname for owner in owners] query = query.filter(cls.owner_fullname.in_(fullnames)) # Filter messages based on passed state, all means that # that messages should not be filtered by state and returned # respecting the sort order that has been passed in. The # mod state is a special state which will filter # out conversations that are not internal. The other special # state is the notification state which denotes a convo created # by automoderator if state == 'mod': query = query.filter(cls.is_internal.is_(True)) elif state == 'notifications': query = query.filter(cls.is_auto.is_(True), cls.state == cls.STATE['new']) elif state == 'highlighted': query = query.filter(cls.is_highlighted.is_(True)) elif state != 'all': query = (query.filter_by(state=cls.STATE[state]) .filter(cls.is_internal.is_(False))) if state == 'new': query = query.filter(cls.is_auto.is_(False)) # If viewer context is not passed just return the results # without adding the last_read attribute if not viewer: results = [] for row in query.all(): results.append(row.ModmailConversation) return results # look up the last time they read each conversation query = query.add_columns( ModmailConversationUnreadState.date.label("last_unread")) query = query.outerjoin( ModmailConversationUnreadState, and_(ModmailConversationUnreadState.account_id == viewer._id, ModmailConversationUnreadState.conversation_id == cls.id, ModmailConversationUnreadState.active.is_(True)) ) if after: if sort == 'mod': query = (query.filter(cls.last_mod_update <= after.last_mod_update) .filter(cls.id != after.id)) elif sort == 'user': query = (query.filter(cls.last_user_update <= after.last_user_update) .filter(cls.id != after.id)) else: query = (query.filter(cls.last_updated <= after.last_updated) .filter(cls.id != after.id)) if sort == 'mod': query = query.order_by(sql.desc(cls.last_mod_update)) elif sort == 'user': query = query.order_by(sql.desc(cls.last_user_update)) else: query = query.order_by(sql.desc(cls.last_updated)) if limit: query = query.limit(limit) results = [] # attach the last_read data to the objects for row in query.all(): result = row.ModmailConversation result.last_unread = row.last_unread results.append(result) return results @classmethod def get_recent_convo_by_sr(cls, srs): if not srs: return sr_fullnames = [sr._fullname for sr in srs] query = (Session.query(cls.owner_fullname, func.max(cls.last_updated)) .filter(cls.owner_fullname.in_(sr_fullnames)) .group_by(cls.owner_fullname) .order_by(func.max(cls.last_updated))) return {row[0]: row[1].isoformat() for row in query.all()} def make_permalink(self): return '{}mail/perma/{}'.format(g.modmail_base_url, self.id36) def get_participant_account(self): if self.is_internal: return None try: convo_participant = ModmailConversationParticipant.get_participant( self.id) participant = Account._byID(convo_participant.account_id) except NotFound: if not self.is_auto: raise return None if participant._deleted: raise NotFound return participant def add_participant(self, participant_id): participant = ModmailConversationParticipant(self, participant_id) Session.add(participant) def add_message(self, author, body, is_author_hidden=False, is_internal=False): """Add a new message to the conversation.""" sr = Subreddit._by_fullname(self.owner_fullname) # if the conversation is internal, make the message # an internal message if self.is_internal: is_internal = True message = ModmailMessage( self, author, body, is_author_hidden, is_internal) Session.add(message) self.num_messages += 1 is_first_message = (self.num_messages == 1) if sr.is_moderator_with_perms(author, 'mail'): # Check if a mod who is not the original author of the # conversation is responding and if so change the state # of the conversation to 'inprogress'. Internal # conversations also should not change state to 'inprogress'. # Lastly if a conversation is 'archived' change the state # to 'inprogress' regardless if the mod has participated or not if (not self.is_internal and not is_first_message and (author._id not in self.author_ids or self.state == self.STATE['archived'])): self.state = self.STATE['inprogress'] self.last_mod_update = message.date else: # Set the state to 'inprogress' only if a mod has responded # with a message in the conversation already and the conversation # is not already in an 'inprogress' state. if (self.last_mod_update is not None and (self.last_mod_update != datetime.min.replace(tzinfo=g.tz)) and self.state != self.STATE['inprogress']): self.state = self.STATE['inprogress'] self.last_user_update = message.date try: Session.commit() except Exception as e: g.log.error('Failed to save message: {}'.format(e)) Session.rollback() raise update_sr_mods_modmail_icon(sr) # create unread records for all except the author of # the newly created message ModmailConversationUnreadState.create_unreads( self.id, list((set(sr.moderators) | set(self.author_ids)) - set([author._id])) ) return message def get_participant(self): try: if not self.is_internal: return ModmailConversationParticipant.get_participant( self.id) except NotFound: pass return None def add_action(self, account, action_type_name, commit=False): """Add an action message to a conversation""" try: convo_action = ModmailConversationAction( self, account, action_type_name) Session.add(convo_action) except ValueError: raise except Exception as e: g.log.error('Failed to save mod action: {}'.format(e)) Session.rollback() raise if commit: Session.commit() def set_state(self, state): """Set the state of this conversation.""" try: self.state = self.STATE[state] except KeyError: Session.rollback() raise ValueError("invalid state") Session.commit() def set_legacy_first_message_id(self, message_id): """Set the legacy_first_message_id for this conversation.""" self.legacy_first_message_id = message_id Session.commit() def mark_read(self, user): """Mark this conversation read for a user.""" ModmailConversationUnreadState.mark_read(user, [self.id]) def mark_unread(self, user): """Mark this conversation unread for a user.""" ModmailConversationUnreadState.create_unreads(self.id, [user._id]) def add_highlight(self): """Add a highlight to this conversation.""" self.is_highlighted = True Session.commit() def remove_highlight(self): """Remove the highlight from this conversation.""" self.is_highlighted = False Session.commit() @classmethod def set_states(cls, convo_ids, state): """Set state for multiple conversations""" convo_ids = tup(convo_ids) (Session.query(cls) .filter(cls.id.in_(convo_ids)) .update({"state": state}, synchronize_session='fetch')) Session.commit() def to_serializable(self, authors_dict=None, entity=None, all_messages=False, current_user=None): # Lookup authors if they are not passed if not authors_dict: from r2.models import Account authors_dict = Account._byID( set(self.author_ids) | set(self.mod_action_account_ids), return_dict=True) # Lookup entity if it is not passed if not entity: entity = Subreddit._by_fullname(self.owner_fullname) serializable_authors = [] for message in self.messages: author = authors_dict.get(message.author_id) serializable_authors.append( to_serializable_author(author, entity, current_user, is_hidden=message.is_author_hidden) ) last_unread = getattr(self, 'last_unread', None) if last_unread is not None: last_unread = last_unread.isoformat() parsed_last_user_update = None parsed_last_mod_update = None min_tz_aware_datetime = datetime.min.replace(tzinfo=g.tz) if self.last_user_update != min_tz_aware_datetime: parsed_last_user_update = self.last_user_update.isoformat() if self.last_mod_update != min_tz_aware_datetime: parsed_last_mod_update = self.last_mod_update.isoformat() result_dict = { 'state': self.state, 'lastUpdated': self.last_updated.isoformat(), 'lastUserUpdate': parsed_last_user_update, 'lastModUpdate': parsed_last_mod_update, 'lastUnread': last_unread, 'isInternal': self.is_internal, 'isAuto': self.is_auto, 'numMessages': self.num_messages, 'owner': { 'id': self.owner_fullname, 'type': entity._type_name, 'displayName': entity.name }, 'isHighlighted': self.is_highlighted, 'id': to36(self.id), 'subject': self.subject, 'authors': serializable_authors, } if all_messages: for mod_action in self.mod_actions: author = authors_dict.get(mod_action.account_id) serializable_authors.append( to_serializable_author(author, entity, current_user, is_hidden=False) ) result_dict.update({ 'objIds': self.ordered_msg_and_action_ids, 'messages': { to36(message.id): message.to_serializable( entity, authors_dict.get(message.author_id), current_user ) for message in self.messages }, 'modActions': { to36(mod_action.id): mod_action.to_serializable() for mod_action in self.mod_actions } }) else: result_dict.update({ 'objIds': [ {'key': 'messages', 'id': to36(self.messages[0].id)} ] }) return result_dict
class ModmailConversationAction(Base): """Mapping table which will map a particular users action to its associated conversation This will track which actions have been applied by whom for each conversation. """ __tablename__ = 'modmail_conversation_actions' id = Column(Integer, primary_key=True) conversation_id = Column( BigInteger, ForeignKey(ModmailConversation.id), nullable=False, index=True) account_id = Column(Integer, index=True) action_type_id = Column(Integer, index=True) date = Column(DateTime(timezone=True), nullable=False, index=True) # DO NOT REARRANGE ORDERING, APPEND NEW TYPES TO THE END ACTION_TYPES = Enum( 'highlighted', 'unhighlighted', 'archived', 'unarchived', 'reported_to_admins', 'muted', 'unmuted', 'banned', 'unbanned', ) def __init__(self, conversation, account, action_type_name): self.conversation_id = conversation.id self.account_id = account._id self.date = datetime.now(g.tz) try: self.action_type_id = self.ACTION_TYPES[action_type_name] except: raise ValueError('Incorrect action_type_name.') @property def id36(self): return to36(self.id) @classmethod def add_actions(cls, conversations, account, action_type_name): conversations = tup(conversations) try: for conversation in conversations: convo_action = ModmailConversationAction( conversation, account, action_type_name) Session.add(convo_action) except Exception as e: g.log.error('Failed bulk action creation: {}'.format(e)) Session.rollback() raise Session.commit() def to_serializable(self, author=None): if not author: from r2.models import Account author = Account._byID(self.account_id) name = author.name author_id = author._id if author._deleted: name = '[deleted]' author_id = None return { 'id': to36(self.id), 'author': { 'id': author_id, 'name': name, 'isAdmin': author.employee, 'isMod': True, 'isHidden': False, 'isDeleted': author._deleted }, 'actionTypeId': self.action_type_id, 'date': self.date.isoformat(), }
from r2.lib.utils import Enum from r2.models.promo import Location from r2.models.promo_metrics import LocationPromoMetrics from r2.models.subreddit import Frontpage from reddit_adzerk.adzerkads import FRONTPAGE_NAME from reddit_adzerk import adzerk_api # https://github.com/adzerk/adzerk-api/wiki/Reporting-API URL_BASE = "https://api.adzerk.net/v1" HEADERS = { 'X-Adzerk-ApiKey': g.secrets['az_ads_key'], 'Content-Type': 'application/x-www-form-urlencoded', } STATUS = Enum(None, "PENDING", "COMPLETE", "ERROR") ReportItem = namedtuple('ReportItem', ['start', 'end', 'impressions', 'clicks']) ReportTuple = namedtuple('ReportTuple', ['date', 'impressions', 'clicks']) AZ_DATE_FORMAT = '%Y-%m-%dT%H:%M:%SZ' class ReportPendingException(Exception): pass class ReportFailedException(Exception): pass
) from r2.models import promo from reddit_adzerk.lib.cache import PromoCampaignByFlightIdCache hooks = HookRegistrar() LEADERBOARD_AD_TYPE = 4 ADZERK_IMPRESSION_BUMP = 500 # add extra impressions to the number we # request from adzerk in case their count # is lower than our internal traffic tracking DELCHARS = ''.join(c for c in map(chr, range(256)) if not (c.isalnum() or c.isspace())) FREQ_CAP_TYPE = Enum(None, "hour", "day") EVENT_TYPE_UPVOTE = 10 EVENT_TYPE_DOWNVOTE = 11 RATE_TYPE_BY_COST_BASIS = { promo.PROMOTE_COST_BASIS.fixed_cpm: 2, promo.PROMOTE_COST_BASIS.cpm: 2, promo.PROMOTE_COST_BASIS.cpc: 3, } GOAL_TYPE_BY_COST_BASIS = { promo.PROMOTE_COST_BASIS.fixed_cpm: 1, promo.PROMOTE_COST_BASIS.cpm: 2, promo.PROMOTE_COST_BASIS.cpc: 2, }