示例#1
0
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):
示例#2
0
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
示例#3
0
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
示例#4
0
# 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 
示例#5
0
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)
示例#6
0
文件: vote.py 项目: wizzwizz4/saidit
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')
示例#7
0
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",
示例#8
0
    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):
示例#9
0
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
示例#10
0
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
示例#11
0
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(),
        }
示例#12
0
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
示例#13
0
)
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,
}