diff --git a/bootstrap.py b/bootstrap.py index e969ca1..b79859c 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -119,6 +119,9 @@ if __name__ == '__main__': client_group.add_argument('-kt', '--auth-ttl', action='store', type=int, default=3000, help='Auth key TTL (seconds)') + membership_group = parser.add_argument_group('membership') + membership_group.add_argument('--expire-membership', action='store_true', help='Should membership expire?') + args = parser.parse_args() args.port = args.port if args.port else 9875 if args.type == 'world' else 6112 diff --git a/houdini.sql b/houdini.sql index 46d9b6d..4325d62 100644 --- a/houdini.sql +++ b/houdini.sql @@ -1371,13 +1371,17 @@ COMMENT ON TABLE penguin_stamp IS 'Penguin earned stamps'; COMMENT ON COLUMN penguin_stamp.penguin_id IS 'Stamp penguin ID'; COMMENT ON COLUMN penguin_stamp.stamp_id IS 'Stamp ID'; COMMENT ON COLUMN penguin_stamp.recent IS 'Is recently earned?'; +COMMENT ON COLUMN penguin_stamp.recent IS 'Is recently earned?'; DROP TABLE IF EXISTS penguin_membership; CREATE TABLE penguin_membership ( penguin_id INT NOT NULL, start TIMESTAMP NOT NULL, - expires TIMESTAMP NOT NULL, - PRIMARY KEY(penguin_id, start, expires), + expires TIMESTAMP DEFAULT NULL, + start_aware BOOLEAN DEFAULT FALSE, + expires_aware BOOLEAN DEFAULT FALSE, + expired_aware BOOLEAN DEFAULT FALSE, + PRIMARY KEY(penguin_id, start), CONSTRAINT penguin_membership_ibfk_1 FOREIGN KEY (penguin_id) REFERENCES penguin (id) ON DELETE RESTRICT ON UPDATE CASCADE ); diff --git a/houdini/__init__.py b/houdini/__init__.py index f1211e6..92c5378 100644 --- a/houdini/__init__.py +++ b/houdini/__init__.py @@ -84,7 +84,7 @@ class PenguinStringCompiler(OrderedDict): 'Y': PenguinStringCompiler.attribute_by_name('y'), 'Frame': PenguinStringCompiler.attribute_by_name('frame'), 'Member': PenguinStringCompiler.attribute_by_name('member'), - 'MemberDays': PenguinStringCompiler.attribute_by_name('membership_days'), + 'MemberDays': PenguinStringCompiler.attribute_by_name('membership_days_total'), 'Avatar': PenguinStringCompiler.attribute_by_name('avatar'), 'PenguinState': PenguinStringCompiler.attribute_by_name('penguin_state'), 'PartyState': PenguinStringCompiler.attribute_by_name('party_state'), diff --git a/houdini/data/penguin.py b/houdini/data/penguin.py index bd2f566..9440cae 100644 --- a/houdini/data/penguin.py +++ b/houdini/data/penguin.py @@ -148,7 +148,10 @@ class PenguinMembership(db.Model): penguin_id = db.Column(db.ForeignKey('penguin.id', ondelete='CASCADE', onupdate='CASCADE'), primary_key=True, nullable=False) start = db.Column(db.DateTime, primary_key=True, nullable=False) - expires = db.Column(db.DateTime, primary_key=True, nullable=False) + expires = db.Column(db.DateTime) + start_aware = db.Column(db.Boolean, server_default=db.text("false")) + expires_aware = db.Column(db.Boolean, server_default=db.text("false")) + expired_aware = db.Column(db.Boolean, server_default=db.text("false")) class Login(db.Model): diff --git a/houdini/handlers/play/navigation.py b/houdini/handlers/play/navigation.py index 38cd846..35b9f71 100644 --- a/houdini/handlers/play/navigation.py +++ b/houdini/handlers/play/navigation.py @@ -43,7 +43,7 @@ async def handle_join_server(p, penguin_id: int, login_key: str): await p.send_xt('lp', await p.string, p.coins, int(p.safe_chat), p.egg_timer_minutes, penguin_standard_time, p.age, 0, p.minutes_played, - "membership_days", server_time_offset, int(p.opened_playercard), + p.membership_days_remain, server_time_offset, int(p.opened_playercard), p.map_category, p.status_field) spawn = random.choice(p.server.spawn_rooms) diff --git a/houdini/handlers/play/player.py b/houdini/handlers/play/player.py index a0bdf53..49a55d1 100644 --- a/houdini/handlers/play/player.py +++ b/houdini/handlers/play/player.py @@ -1,11 +1,14 @@ from houdini import handlers from houdini.converters import SeparatorConverter from houdini.handlers import XTPacket -from houdini.data.penguin import Penguin +from houdini.handlers.play.navigation import handle_join_server +from houdini.data import db +from houdini.data.penguin import Penguin, PenguinMembership +from houdini.data.mail import PenguinPostcard from houdini.constants import ClientType from aiocache import cached -from datetime import datetime +from datetime import datetime, timedelta import random import asyncio import time @@ -77,6 +80,78 @@ async def server_egg_timer(server): await p.send_error_and_disconnect(910) +MemberWarningDaysToExpiry = 14 +MemberWarningPostcardsVanilla = [122, 123] +MemberWarningPostcardsLegacy = [163] +MemberExpiredPostcard = 124 +MemberStartPostcardVanilla = 121 +MemberStartPostcardLegacy = 164 + + +@handlers.handler(XTPacket('j', 'js'), pre_login=True, before=handle_join_server) +@handlers.allow_once +async def handle_setup_membership(p): + if not p.server.config.expire_membership or p.moderator or p.character: + p.is_member = True + p.membership_days_total = p.age + return + + membership_history = PenguinMembership.query.where(PenguinMembership.penguin_id == p.id) + current_timestamp = datetime.now() + postcards = [] + + warning_postcards = MemberWarningPostcardsVanilla if p.is_vanilla_client else MemberWarningPostcardsLegacy + start_postcard = MemberStartPostcardVanilla if p.is_vanilla_client else MemberStartPostcardLegacy + + async with db.transaction(): + async for membership_record in membership_history.gino.iterate(): + membership_recurring = membership_record.expires is None + membership_active = membership_recurring or membership_record.expires >= current_timestamp + + if membership_record.start < current_timestamp: + if membership_active: + p.is_member = True + + if not membership_recurring: + days_to_expiry = (membership_record.expires.date() - datetime.now().date()).days + p.membership_days_remain = days_to_expiry + + if days_to_expiry <= MemberWarningDaysToExpiry and not membership_record.expires_aware: + postcards.append(dict( + penguin_id=p.id, + postcard_id=random.choice(warning_postcards), + send_date=membership_record.expires - timedelta(days=MemberWarningDaysToExpiry) + )) + await membership_record.update(expires_aware=True).apply() + else: + if p.membership_days_remain < 0: + days_since_expiry = (membership_record.expires.date() - datetime.now().date()).days + p.membership_days_remain = min(p.membership_days_remain, days_since_expiry) + + if not membership_record.expired_aware: + if p.is_vanilla_client: + postcards.append(dict( + penguin_id=p.id, + postcard_id=MemberExpiredPostcard, + send_date=membership_record.expires + )) + await membership_record.update(expired_aware=True).apply() + + if not membership_record.start_aware: + postcards.append(dict( + penguin_id=p.id, + postcard_id=start_postcard, + send_date=membership_record.start + )) + await membership_record.update(start_aware=True).apply() + + membership_end_date = current_timestamp if membership_active else membership_record.expires + p.membership_days_total += (membership_end_date - membership_record.start).days + + if postcards: + await PenguinPostcard.insert().values(postcards).gino.status() + + @handlers.handler(XTPacket('u', 'h')) @handlers.cooldown(59) async def handle_heartbeat(p): diff --git a/houdini/penguin.py b/houdini/penguin.py index 4bf9ce8..98e64be 100644 --- a/houdini/penguin.py +++ b/houdini/penguin.py @@ -7,9 +7,9 @@ from houdini.data import penguin class Penguin(Spheniscidae, penguin.Penguin): __slots__ = ['x', 'y', 'frame', 'toy', 'room', 'waddle', 'table', - 'data', 'muted', 'login_key', 'member', 'membership_days', - 'avatar', 'walking_puffle', 'permissions', 'active_quests', - 'buddy_requests', 'heartbeat', 'login_timestamp', + 'data', 'muted', 'login_key', 'is_member', 'membership_days_total', + 'membership_days_remain', 'avatar', 'walking_puffle', 'permissions', + 'active_quests', 'legacy_buddy_requests', 'heartbeat', 'login_timestamp', 'egg_timer_minutes'] def __init__(self, *args): @@ -25,8 +25,9 @@ class Penguin(Spheniscidae, penguin.Penguin): self.login_key = None - self.member = 1 - self.membership_days = 1 + self.is_member = False + self.membership_days_total = 0 + self.membership_days_remain = -1 self.avatar = None self.walking_puffle = None @@ -58,6 +59,10 @@ class Penguin(Spheniscidae, penguin.Penguin): def safe_name(self): return self.safe_nickname(self.server.config.lang) + @property + def member(self): + return int(self.is_member) + async def join_room(self, room): await room.add_penguin(self)