Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/**/js/node_modules
/**/js/antibot-simulation.js
/**/tmp
/**/.DS_Store
.vscode
docker-compose.yml
entrypoint.sh
Expand Down
7 changes: 7 additions & 0 deletions apps/pushnotification/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from ...secret import MAILER_PW

port = 465 # For starttls
smtp_server = "smtp.gmail.com"
sender_email = "[email protected]"
subject = "Message from Ticketmaster Ticket Tracker"
app_password = MAILER_PW
100 changes: 100 additions & 0 deletions apps/pushnotification/msg_formatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
from datetime import datetime
from ..ticketscraping.models.pick import Pick

def price_formatter(price):
price_str = ''
if type(price) is float or int:
price_str = "$" + "{:.2f}".format(price)
elif type(price) is str:
price_str = price
return price_str

def decimal_to_percent(num: float):
return "{:.2f}".format(num*100) + "%"

def format_date(date: datetime):
return date.isoformat()

def default_formatter(s: str):
return s

def format_seat_columns(cols):
if type(cols) is str:
return cols
elif type(cols) is list:
return "(" + ",".join(cols) + ")"
return '-'

def apply_format(s, formatter)->str:
return formatter(s)

def apply(values: list, formatters: list, delimiter="\t"):
if len(values) != len(formatters):
raise Exception('values and formatters must have the same length')
s = []
for i in range(len(values)):
s.append(apply_format(values[i], formatters[i]))
return delimiter.join(s)

def format_full_seat(seat: dict, delimiter="\t"):
price = seat.get("price", "n/a")
section = seat.get("section", "n/a")
row = seat.get("row", "n/a")
seat_columns = seat.get("seat_columns", "n/a")
last_modified = seat.get("last_modified", "n/a")
return apply(
[price, section, row, seat_columns, last_modified],
[price_formatter, default_formatter, default_formatter,
format_seat_columns, format_date],
delimiter)

def format_price_only_seat(seat: dict, delimiter="\t"):
price = seat.get("price", "n/a")
last_modified = seat.get("last_modified", "n/a")
return apply([price, last_modified], [price_formatter, format_date], delimiter)

def format_seat(seat: dict, price_only=False, delimiter="\t"):
if price_only:
return format_price_only_seat(seat, delimiter)
else:
return format_full_seat(seat, delimiter)

def format_seats(seats: list, price_only=False, delimiter="\t"):
return "\n".join([format_seat(seat, price_only, delimiter) for seat in seats])


def format_entire_mail(pick: Pick, target_price: int, percentile: float, rank: int, num_total: int, top_history_seats: list, same_seats: list):
"""
structure of message:
1. greetings
2. attributes of new seats
3. top 3 comparable history seats
4. exact same seats if possible
5. signature
"""
p1 = (
f"Hi!"
)
p2 = (
f"Congratulations! Ticket tracker reminds you that your ticket subscription request with target price {price_formatter(target_price)} "
f"found better budget seats (price, section, row, seats) at ({format_full_seat(vars(pick), delimiter=', ')}). "
f"{decimal_to_percent(percentile)} of all comparable seats in the history are better than the newly found seats, that is, "
f"they rank no.{rank} out of {num_total} comparable seats in the history."
)
p3 = (
f"You can compare to history seats that are better than the newly found seats:"
f"{chr(10)}"
f"{format_seats(top_history_seats, price_only=False)}"
) if len(top_history_seats) > 0 else ""
p4 = (
f"The newly found seats have history prices:"
f"{chr(10)}"
f"{format_seats(same_seats, price_only=True)}"
) if len(same_seats) > 0 else ""
p5 = (
f"Bests,"
f"{chr(10)}"
f"Ticketmaster Ticket Tracker"
)
paras = list(filter(lambda p: len(p) > 0, [p1, p2, p3, p4, p5]))
return "\n\n".join(paras)
44 changes: 44 additions & 0 deletions apps/pushnotification/smtp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from smtplib import SMTP_SSL
from ssl import create_default_context
from email.message import EmailMessage
from . import constants


def init_server():
context = create_default_context()
server = SMTP_SSL(constants.smtp_server, constants.port, context=context)
return server


def server_login(server: SMTP_SSL, password: str):
return server.login(constants.sender_email, password)


def server_send_email(server: SMTP_SSL, receiver_emails: list[str], message: str):
em = EmailMessage()
em['From'] = constants.sender_email
em['To'] = receiver_emails
em['subject'] = constants.subject

em.set_content(message)
return server.sendmail(constants.sender_email, receiver_emails, em.as_string())


def send_email(receiver_emails: list[str], messages: list[str]):
if len(messages) == 0:
return
# print(messages[0])
try:
err = server_send_email(server, receiver_emails, messages[0])
if err is not None:
raise Exception('could not send email to the receiver')
except Exception as ex:
print(ex)


server = init_server()


def auth_server():
global server
server_login(server, constants.app_password)
33 changes: 29 additions & 4 deletions apps/startup/apps.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,39 @@
from django.apps import AppConfig
from ..ticketscraping.scraping import start
from datetime import datetime
from threading import Thread
from multiprocessing import Process


def run_prepare():
# import module inside the child process to prevent execution in the parent process
print(
f"ticket scraping service started at {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}")

# start sender socket
from apps.ticketscraping.schedulers.async_tasks_scheduler import async_tasks_scheduler
conn_thread = Thread(target=async_tasks_scheduler.connect)
conn_thread.start()
# wait for async tasks handler to connect
conn_thread.join()

# start itself (scraping)
from apps.ticketscraping.scraping import start
start()


def run():
# starter
p = Process(target=run_prepare, daemon=True)
p.start()
# start receiver socket
from apps.ticketscraping.connection.asyn_tasks_receiver import run
conn_process = Process(target=run)
conn_process.start()


class MyAppConfig(AppConfig):
name = "apps.startup"
verbose_name = "start tmtracker"

def ready(self):
print(
f"server started at {datetime.now().strftime('%d/%m/%Y %H:%M:%S')}")
Thread(target=start).start()
run()
10 changes: 8 additions & 2 deletions apps/storage/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,18 @@ def find_one_and_update__(coll: collection.Collection, filter: dict, update=dict
def find_one_and_delete__(coll: collection.Collection, filter: dict):
return coll.find_one_and_delete(filter)

def find_one__(coll: collection.Collection, filter: dict, projection):
return coll.find_one(filter=filter, projection=projection)
def find_one__(coll: collection.Collection, filter: dict, projection, **kwargs):
return coll.find_one(filter=filter, projection=projection, **kwargs)

def find_many__(coll: collection.Collection, filter: dict, projection, **kwargs):
return coll.find(filter=filter, projection=projection, **kwargs)

def count_docs__(coll: collection.Collection, filter: dict):
return coll.count_documents(filter=filter)

def estimated_count_docs__(coll: collection.Collection):
return coll.estimated_document_count()

def watch__(coll: collection.Collection, **kwargs):
return coll.watch(**kwargs)

Expand Down
16 changes: 16 additions & 0 deletions apps/storage/query.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from .storage import *
import pymongo

# find the max value in a collection
def find_max(collection_name, filter: dict, sort_key: str, db_name="tickets"):
sort_seq = [(sort_key, pymongo.DESCENDING)]
return find_one(collection_name, filter, db_name=db_name, sort=sort_seq)

# find the min value in a collection
def find_min(collection_name, filter: dict, sort_key: str, db_name="tickets"):
sort_seq = [(sort_key, pymongo.ASCENDING)]
return find_one(collection_name, filter, db_name=db_name, sort=sort_seq)

def find_many_ascending_order(collection_name, filter: dict, sort_key: str, db_name="tickets"):
sort_seq = [(sort_key, pymongo.ASCENDING)]
return find_many(collection_name, filter, db_name=db_name, sort=sort_seq)
16 changes: 14 additions & 2 deletions apps/storage/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,29 @@ def find_one_and_delete(collection_name, filter: dict, db_name="tickets"):
return find_one_and_delete__(coll, filter)

# find one
def find_one(collection_name, filter: dict, projection=None, db_name="tickets"):
def find_one(collection_name, filter: dict, projection=None, db_name="tickets", **kwargs):
db = get_db_handle(db_name)
coll = db[collection_name]
return find_one__(coll, filter, projection)
return find_one__(coll, filter, projection, **kwargs)

# find many
def find_many(collection_name, filter: dict, projection=None, db_name="tickets", **kwargs):
db = get_db_handle(db_name)
coll = db[collection_name]
return list(find_many__(coll, filter, projection, **kwargs))

# count with filter
def count_docs(collection_name, filter: dict, db_name="tickets"):
db = get_db_handle(db_name)
coll = db[collection_name]
return count_docs__(coll, filter)

# count all docs in a collection
def estimated_count_docs(collection_name, db_name="tickets"):
db = get_db_handle(db_name)
coll = db[collection_name]
return estimated_count_docs__(coll)

# watch changes
def watch(collection_name, db_name="tickets", **kwargs):
db = get_db_handle(db_name)
Expand Down
35 changes: 35 additions & 0 deletions apps/ticketscraping/connection/asyn_tasks_receiver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# start sockets
from threading import Thread
from multiprocessing import Process
from apps.ticketscraping.connection.receiver_process import ReceiverProcess
from apps.ticketscraping.constants import SERVICE_LOCALHOST, ASYNC_TASKS_RECEIVER_PORT


def run_prepare():
# start receiver socket
from apps.ticketscraping.connection.mail_receiver import run
conn_process = Process(target=run, daemon=True)
conn_process.start()

# start sender socket
from apps.ticketscraping.schedulers.mail_scheduler import mail_scheduler
conn_thread = Thread(target=mail_scheduler.connect)
conn_thread.start()
# wait for mailer to connect
conn_thread.join()

# start itself
from apps.ticketscraping.tasks.asynchronous import run_async_tasks
receiver = ReceiverProcess(run_async_tasks, SERVICE_LOCALHOST, ASYNC_TASKS_RECEIVER_PORT)
receiver.connect()
receiver.serve_forever()


def run():
# starter
p = Process(target=run_prepare)
p.start()


if __name__ == '__main__':
run()
13 changes: 13 additions & 0 deletions apps/ticketscraping/connection/mail_receiver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
from apps.ticketscraping.connection.receiver_process import ReceiverProcess
from apps.pushnotification.smtp import send_email, auth_server
from apps.ticketscraping.constants import SERVICE_LOCALHOST, MAIL_RECEIVER_PORT

def run():
# start itself
auth_server()
receiver = ReceiverProcess(send_email, SERVICE_LOCALHOST, MAIL_RECEIVER_PORT)
receiver.connect()
receiver.serve_forever()

if __name__ == '__main__':
run()
24 changes: 24 additions & 0 deletions apps/ticketscraping/connection/receiver.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
from multiprocessing.connection import Client
from threading import Semaphore

class Receiver:
def __init__(self, hostname: str, port: int):
self.lock = Semaphore(1)
self.hostname = hostname
self.port = port
self.conn = None

def connect(self):
self.conn = Client(address=(self.hostname, self.port,))

def recv(self):
if self.conn is None:
raise Exception('connection is not established')
self.lock.acquire()
res = self.conn.recv()
self.lock.release()
return res

def __del__(self):
if self.conn is not None: self.conn.close()

11 changes: 11 additions & 0 deletions apps/ticketscraping/connection/receiver_process.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from .receiver import Receiver

class ReceiverProcess(Receiver):
def __init__(self, action, hostname: str, port: int):
super().__init__(hostname, port)
self.action = action

def serve_forever(self):
while True:
res = self.recv()
self.action(*res)
26 changes: 26 additions & 0 deletions apps/ticketscraping/connection/sender.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from multiprocessing.connection import Listener
from threading import Semaphore

class Sender:
def __init__(self, hostname: str, port: int):
self.lock = Semaphore(1)
self.hostname = hostname
self.port = port
self.conn = None

def connect(self):
listener = Listener(address=(self.hostname, self.port))
self.conn = listener.accept()
print("conn accepted ", self.port)

def send(self, *args):
if self.conn is None:
raise Exception('connection is not established')
self.lock.acquire()
self.conn.send(args)
self.lock.release()
return True

def __del__(self):
if self.conn is not None:
self.conn.close()
Loading