├── .gitignore ├── README.md ├── alembic.ini ├── config.toml ├── main.py ├── migrations ├── README ├── env.py ├── script.py.mako └── versions │ └── __init__.py ├── models.py ├── platforms ├── __init__.py ├── medium.py └── pentesterland.py ├── requirements.txt └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .toml 3 | config.toml 4 | *.toml 5 | database.db 6 | __pycache__/ 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # writeup-app 2 | 3 | ## information 4 | Writeup-app is a simple program for sending notifications of new bug bounty write-ups in Discord. 5 | 6 | ### built with 7 | * [![Python][Python]][Python-Url] 8 | * [![SqlAlchemy][SqlAlchemy]][SqlAlchemy-Url] 9 | * [![Alembic][Alembic]][Alembic] 10 | * [![Toml][Toml]][Toml-Url] 11 | 12 | 13 | # installation 14 | Edit config.toml file and put your discord webhooks in it. 15 | ```commandline 16 | git clone https://github.com/0xuf/writeup-app.git 17 | cd writeup-app 18 | pip install -r requirements.txt 19 | alembic revision --autogenerate -m "Create Writeup models" 20 | alembic upgrade head 21 | python main.py 22 | ``` 23 | 24 | ## You can set the script to run once every time by adding crontab 25 | ## example for run every 1 hour 26 | ```commandline 27 | crontab -e 28 | 0 * * * * /usr/bin/python3 /path/to/main.py 29 | ``` 30 | 31 | ## You can also scrap more posts from the medium.com by adding more tags to the config.toml 32 | 33 | # License 34 | ``` 35 | This project is licensed under MIT License. 36 | ``` 37 | 38 | # Author 39 | Discord: NotAvailable#7600 40 | 41 | [Instagram](https://instagram.com/n0t.4vailable) 42 | 43 | [Python]: https://img.shields.io/badge/python-000000?style=for-the-badge&logo=python&logoColor=blue 44 | [Python-Url]: https://python.org 45 | [Toml]: https://img.shields.io/badge/toml-35495E?style=for-the-badge 46 | [Toml-Url]: https://toml.io 47 | [SqlAlchemy]: https://img.shields.io/badge/SqlALchemy-0769AD?style=for-the-badge 48 | [SqlAlchemy-Url]: https://www.sqlalchemy.org/ 49 | [Alembic]: https://img.shields.io/badge/alembic-20232A?style=for-the-badge 50 | [Alembic-Url]: https://pypi.org/project/alembic/ -------------------------------------------------------------------------------- /alembic.ini: -------------------------------------------------------------------------------- 1 | # A generic, single database configuration. 2 | 3 | [alembic] 4 | # path to migration scripts 5 | script_location = migrations 6 | 7 | # template used to generate migration file names; The default value is %%(rev)s_%%(slug)s 8 | # Uncomment the line below if you want the files to be prepended with date and time 9 | # see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file 10 | # for all available tokens 11 | # file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s 12 | 13 | # sys.path path, will be prepended to sys.path if present. 14 | # defaults to the current working directory. 15 | prepend_sys_path = . 16 | 17 | # timezone to use when rendering the date within the migration file 18 | # as well as the filename. 19 | # If specified, requires the python-dateutil library that can be 20 | # installed by adding `alembic[tz]` to the pip requirements 21 | # string value is passed to dateutil.tz.gettz() 22 | # leave blank for localtime 23 | # timezone = 24 | 25 | # max length of characters to apply to the 26 | # "slug" field 27 | # truncate_slug_length = 40 28 | 29 | # set to 'true' to run the environment during 30 | # the 'revision' command, regardless of autogenerate 31 | # revision_environment = false 32 | 33 | # set to 'true' to allow .pyc and .pyo files without 34 | # a source .py file to be detected as revisions in the 35 | # versions/ directory 36 | # sourceless = false 37 | 38 | # version location specification; This defaults 39 | # to migrations/versions. When using multiple version 40 | # directories, initial revisions must be specified with --version-path. 41 | # The path separator used here should be the separator specified by "version_path_separator" below. 42 | # version_locations = %(here)s/bar:%(here)s/bat:migrations/versions 43 | 44 | # version path separator; As mentioned above, this is the character used to split 45 | # version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. 46 | # If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. 47 | # Valid values for version_path_separator are: 48 | # 49 | # version_path_separator = : 50 | # version_path_separator = ; 51 | # version_path_separator = space 52 | version_path_separator = os # Use os.pathsep. Default configuration used for new projects. 53 | 54 | # the output encoding used when revision files 55 | # are written from script.py.mako 56 | # output_encoding = utf-8 57 | 58 | sqlalchemy.url = sqlite:///database.db 59 | 60 | 61 | [post_write_hooks] 62 | # post_write_hooks defines scripts or Python functions that are run 63 | # on newly generated revision scripts. See the documentation for further 64 | # detail and examples 65 | 66 | # format using "black" - use the console_scripts runner, against the "black" entrypoint 67 | # hooks = black 68 | # black.type = console_scripts 69 | # black.entrypoint = black 70 | # black.options = -l 79 REVISION_SCRIPT_FILENAME 71 | 72 | # Logging configuration 73 | [loggers] 74 | keys = root,sqlalchemy,alembic 75 | 76 | [handlers] 77 | keys = console 78 | 79 | [formatters] 80 | keys = generic 81 | 82 | [logger_root] 83 | level = WARN 84 | handlers = console 85 | qualname = 86 | 87 | [logger_sqlalchemy] 88 | level = WARN 89 | handlers = 90 | qualname = sqlalchemy.engine 91 | 92 | [logger_alembic] 93 | level = INFO 94 | handlers = 95 | qualname = alembic 96 | 97 | [handler_console] 98 | class = StreamHandler 99 | args = (sys.stderr,) 100 | level = NOTSET 101 | formatter = generic 102 | 103 | [formatter_generic] 104 | format = %(levelname)-5.5s [%(name)s] %(message)s 105 | datefmt = %H:%M:%S 106 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | [discord] 2 | writeup_notif_webhook = "" 3 | script_working_alert_webhook = "" 4 | 5 | 6 | [medium] 7 | tags = ["bug-bounty-writeup", "bugbounty-writeup"] 8 | 9 | [database] 10 | uri = "sqlite:///database.db" -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from requests.exceptions import MissingSchema 3 | from platforms import ( 4 | PentesterlandScrapper, MediumScrapper 5 | ) 6 | from utils import ( 7 | cleanup, ascii_art, notify_script_launch, notify_writeup 8 | ) 9 | from sqlalchemy import create_engine 10 | from sqlalchemy.orm import sessionmaker 11 | from models import ( 12 | Log, App 13 | ) 14 | from rich.logging import RichHandler 15 | from tomli import load 16 | 17 | FORMAT = "%(message)s" 18 | logging.basicConfig( 19 | level="INFO", format=FORMAT, datefmt="[%X]", handlers=[RichHandler()] 20 | ) 21 | log = logging.getLogger("rich") 22 | 23 | 24 | class WriteupApp: 25 | """ 26 | Main class of Writeup App 27 | """ 28 | first_launch = None 29 | 30 | def __init__(self) -> None: 31 | """ 32 | Constructor method 33 | """ 34 | 35 | # Read config.toml file 36 | with open("config.toml", mode="rb") as _config_file: 37 | self.config = load(_config_file) 38 | _config_file.close() 39 | 40 | # Session of the sqlite database 41 | self.Session = sessionmaker( 42 | bind=create_engine( 43 | self.config['database']['uri'] 44 | ) 45 | ) 46 | 47 | log.info("Sent launch log in discord.") 48 | notify_script_launch(notify_webhook=self.config["discord"]["script_working_alert_webhook"]) 49 | 50 | # Check that the program is running for the first time or not 51 | with self.Session() as session: 52 | app_table = session.query(App).all() 53 | 54 | if len(app_table) < 1: 55 | self.first_launch = True 56 | session.add( 57 | App( 58 | id=1, 59 | first_launch=False 60 | ) 61 | ) 62 | session.commit() 63 | 64 | else: 65 | self.first_launch = False 66 | 67 | def medium_writeup(self) -> list: 68 | """ 69 | This method will receive the dictionary of medium write-ups 70 | :return: medium write-ups 71 | :rtype: list 72 | """ 73 | output = [] 74 | # Read tags from config.toml file 75 | tags = self.config['medium']['tags'] 76 | 77 | log.info("Requesting to medium to get new writeups.") 78 | 79 | for tag in tags: 80 | _instance = MediumScrapper(tag=tag) 81 | output.append(_instance.get_response()) 82 | 83 | return output 84 | 85 | @staticmethod 86 | def pentesterland_writeup() -> list: 87 | """ 88 | This method will receive the dictionary of pentesterlab write-ups 89 | :return: pentesterlab write-ups 90 | :type: list 91 | """ 92 | log.info("Requesting to pentesterland to get new writeups.") 93 | _instance = PentesterlandScrapper() 94 | return _instance.get_response() 95 | 96 | def insert_log(self, data) -> None: 97 | """ 98 | This method will insert data to database 99 | :param data: write-up data 100 | :return: Nothing 101 | :type: None 102 | """ 103 | with self.Session() as session: 104 | session.add( 105 | Log( 106 | url=data.get("post")["link"], 107 | title=data.get("post")["title"], 108 | author=data.get("author")["name"], 109 | author_url=data.get("author")["username"] 110 | ) 111 | ) 112 | session.commit() 113 | session.close() 114 | 115 | def main(self) -> None: 116 | """ 117 | This is the main method of the class and when it is called, it checks whether a new write-up has been added 118 | or not, if it has been added, it adds it to the database and announces it in Discord. 119 | 120 | :return: Nothing 121 | :type: None 122 | """ 123 | 124 | # Get all write-ups from database 125 | with self.Session() as session: 126 | database_writeups = session.query(Log).all() 127 | session.close() 128 | 129 | medium_writeups = self.medium_writeup() 130 | pentesterland_writeups = self.pentesterland_writeup() 131 | 132 | titles = [output.title for output in database_writeups] 133 | log.info("Checking medium writeups with the database") 134 | 135 | # Loop into medium write-ups 136 | for writeups in medium_writeups: 137 | for writeup in writeups: 138 | if writeup.get("post")["title"] not in titles: 139 | if not self.first_launch: 140 | log.info("Sent new writeup in discord.") 141 | # Notify the write-up in discord 142 | notify_writeup(notify_webhook=self.config["discord"]["writeup_notif_webhook"], data=writeup) 143 | self.insert_log(writeup) 144 | 145 | log.info("Checking pentesterland writeups with the database") 146 | for writeup in pentesterland_writeups: 147 | if writeup.get("post")["title"] not in titles: 148 | if not self.first_launch: 149 | log.info("Sent new writeup in discord.") 150 | # Notify the write-up in discord 151 | notify_writeup(notify_webhook=self.config["discord"]["writeup_notif_webhook"], data=writeup) 152 | self.insert_log(writeup) 153 | 154 | 155 | if __name__ == "__main__": 156 | cleanup() 157 | ascii_art() 158 | try: 159 | instance = WriteupApp() 160 | instance.main() 161 | except MissingSchema: 162 | log.error("Add Discord webhooks into config.toml file.") 163 | -------------------------------------------------------------------------------- /migrations/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /migrations/env.py: -------------------------------------------------------------------------------- 1 | from logging.config import fileConfig 2 | 3 | from sqlalchemy import engine_from_config 4 | from sqlalchemy import pool 5 | 6 | from alembic import context 7 | 8 | # this is the Alembic Config object, which provides 9 | # access to the values within the .ini file in use. 10 | config = context.config 11 | 12 | # Interpret the config file for Python logging. 13 | # This line sets up loggers basically. 14 | if config.config_file_name is not None: 15 | fileConfig(config.config_file_name) 16 | 17 | # add your model's MetaData object here 18 | # for 'autogenerate' support 19 | from models import Base # noqa 20 | 21 | target_metadata = Base.metadata 22 | 23 | 24 | # target_metadata = None 25 | 26 | # other values from the config, defined by the needs of env.py, 27 | # can be acquired: 28 | # my_important_option = config.get_main_option("my_important_option") 29 | # ... etc. 30 | 31 | 32 | def run_migrations_offline() -> None: 33 | """Run migrations in 'offline' mode. 34 | 35 | This configures the context with just a URL 36 | and not an Engine, though an Engine is acceptable 37 | here as well. By skipping the Engine creation 38 | we don't even need a DBAPI to be available. 39 | 40 | Calls to context.execute() here emit the given string to the 41 | script output. 42 | 43 | """ 44 | url = config.get_main_option("sqlalchemy.url") 45 | context.configure( 46 | url=url, 47 | target_metadata=target_metadata, 48 | literal_binds=True, 49 | dialect_opts={"paramstyle": "named"}, 50 | ) 51 | 52 | with context.begin_transaction(): 53 | context.run_migrations() 54 | 55 | 56 | def run_migrations_online() -> None: 57 | """Run migrations in 'online' mode. 58 | 59 | In this scenario we need to create an Engine 60 | and associate a connection with the context. 61 | 62 | """ 63 | connectable = engine_from_config( 64 | config.get_section(config.config_ini_section), 65 | prefix="sqlalchemy.", 66 | poolclass=pool.NullPool, 67 | ) 68 | 69 | with connectable.connect() as connection: 70 | context.configure( 71 | connection=connection, target_metadata=target_metadata 72 | ) 73 | 74 | with context.begin_transaction(): 75 | context.run_migrations() 76 | 77 | 78 | if context.is_offline_mode(): 79 | run_migrations_offline() 80 | else: 81 | run_migrations_online() 82 | -------------------------------------------------------------------------------- /migrations/script.py.mako: -------------------------------------------------------------------------------- 1 | """${message} 2 | 3 | Revision ID: ${up_revision} 4 | Revises: ${down_revision | comma,n} 5 | Create Date: ${create_date} 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | ${imports if imports else ""} 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = ${repr(up_revision)} 14 | down_revision = ${repr(down_revision)} 15 | branch_labels = ${repr(branch_labels)} 16 | depends_on = ${repr(depends_on)} 17 | 18 | 19 | def upgrade() -> None: 20 | ${upgrades if upgrades else "pass"} 21 | 22 | 23 | def downgrade() -> None: 24 | ${downgrades if downgrades else "pass"} 25 | -------------------------------------------------------------------------------- /migrations/versions/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xuf/writeup-app/15f443e4839b8180341bcedf10ddbb91aaf9cab9/migrations/versions/__init__.py -------------------------------------------------------------------------------- /models.py: -------------------------------------------------------------------------------- 1 | from sqlalchemy import ( 2 | Column, Integer, String, Boolean 3 | ) 4 | from sqlalchemy.ext.declarative import declarative_base 5 | 6 | from tomli import load 7 | 8 | with open("config.toml", mode="rb") as _config_file: 9 | _config = load(_config_file) 10 | _config_file.close() 11 | 12 | Base = declarative_base() 13 | 14 | 15 | class Log(Base): 16 | __tablename__ = 'log' 17 | 18 | id = Column(Integer, primary_key=True) 19 | url = Column(String) 20 | title = Column(String) 21 | author = Column(String) 22 | author_url = Column(String) 23 | 24 | 25 | class App(Base): 26 | __tablename__ = 'app' 27 | 28 | id = Column(Integer, primary_key=True) 29 | first_launch = Column(Boolean) 30 | -------------------------------------------------------------------------------- /platforms/__init__.py: -------------------------------------------------------------------------------- 1 | from platforms.medium import MediumScrapper 2 | from platforms.pentesterland import PentesterlandScrapper 3 | -------------------------------------------------------------------------------- /platforms/medium.py: -------------------------------------------------------------------------------- 1 | from requests import get 2 | from bs4 import BeautifulSoup 3 | 4 | 5 | class MediumScrapper: 6 | """ 7 | This class will scrap the medium.com 8 | """ 9 | medium_url = "https://medium.com/{}" 10 | output_data = [] 11 | BASE_URL = "https://medium.com/tag/{}/latest" 12 | headers = { 13 | 'Host': 'medium.com', 14 | 'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0', 15 | 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8', 16 | 'Accept-Language': 'en-US,en;q=0.5', 17 | 'Upgrade-Insecure-Requests': '1', 18 | 'Sec-Fetch-Dest': 'document', 19 | 'Sec-Fetch-Mode': 'navigate', 20 | 'Sec-Fetch-Site': 'cross-site', 21 | } 22 | 23 | def __init__(self, tag: str) -> None: 24 | """ 25 | Constructor method 26 | :param tag: receives the information based on the tag parameter 27 | :return: Nothing 28 | :rtype: None 29 | """ 30 | self.tag = tag 31 | self.BASE_URL = self.BASE_URL.format(tag) 32 | 33 | def get_response(self) -> list: 34 | """ 35 | This method receives the posts and outputs them as a list 36 | :return: all_posts 37 | :type: list 38 | """ 39 | _resp = get(url=self.BASE_URL, headers=self.headers) 40 | # Parse the html with BS4 41 | bs = BeautifulSoup(_resp.content.decode('utf-8'), 'html.parser') 42 | 43 | all_posts = bs.findAll("div", class_="kj kk kl l") 44 | 45 | # Extract data from medium.com posts 46 | for post in all_posts: 47 | _author = dict( 48 | name=post.findNext( 49 | "p", 50 | class_="bn b bo bp fm kr hd he hf hg fy hh gg" 51 | ).text, 52 | username=self.medium_url.format(post.findNext( 53 | "a", 54 | class_="au av aw ax ay az ba bb bc bd be bf bg bh bi" 55 | ).attrs['href'].split("?")[0].replace("/", "") 56 | )) 57 | _post = dict( 58 | title=post.findNext( 59 | "h2", 60 | class_="bn gi lf lg lh li gm lj lk ll lm gq ln lo lp lq gu lr ls " 61 | "lt lu gy lv lw lx ly hc fm hd he hg hh gg" 62 | ).text, 63 | link=self.medium_url.format(post.findNext( 64 | "a", 65 | {"aria-label": "Post Preview Title"} 66 | ).attrs['href'].split("?")[0])) 67 | 68 | self.output_data.append( 69 | { 70 | 'author': _author, 71 | 'post': _post 72 | } 73 | ) 74 | 75 | return self.output_data 76 | -------------------------------------------------------------------------------- /platforms/pentesterland.py: -------------------------------------------------------------------------------- 1 | from requests import get 2 | from bs4 import BeautifulSoup 3 | 4 | 5 | class PentesterlandScrapper: 6 | """ 7 | This class will Scrap the pentester.land 8 | """ 9 | BASE_URL = "https://pentester.land/list-of-bug-bounty-writeups.html" 10 | output_data = [] 11 | headers = { 12 | "Host": "pentester.land", 13 | "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:103.0) Gecko/20100101 Firefox/103.0", 14 | "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", 15 | "Accept-Language": "en-US,en;q=0.5", 16 | "Upgrade-Insecure-Requests": "1", 17 | "Sec-Fetch-Dest": "document", 18 | "Sec-Fetch-Mode": "navigate", 19 | "Sec-Fetch-Site": "none", 20 | "Sec-Fetch-User": "?1", 21 | "Connection": "close", 22 | } 23 | 24 | def get_response(self) -> list: 25 | """ 26 | This method receives all the write-ups of this year and outputs them as a list. 27 | :return: pentester.lab write-ups 28 | :rtype: list 29 | """ 30 | _resp = get(url=self.BASE_URL, headers=self.headers) 31 | # Parse HTML with BS4 32 | bs = BeautifulSoup(_resp.content.decode("utf-8"), "html.parser") 33 | find_table = bs.findAll("table")[0] 34 | 35 | # Extract data from pentester.land 36 | for row in find_table.findAll("tr"): 37 | 38 | # Check if author has link 39 | try: 40 | _author_row = row.findAll("a")[1] 41 | _post_row = row.findAll("a")[0] 42 | except IndexError as e: 43 | get_row = row.findAll("td") 44 | try: 45 | _author_row = get_row[1] 46 | _post_row = get_row[0].findAll("a")[0] 47 | except IndexError: 48 | continue 49 | 50 | # Put Author link None author doesn't have link 51 | try: 52 | _author = dict( 53 | name=_author_row.text, 54 | username=_author_row.attrs["href"] 55 | ) 56 | except KeyError: 57 | _author = dict( 58 | name=_author_row.text, 59 | username=None 60 | ) 61 | 62 | _post = dict( 63 | title=_post_row.text, 64 | link=_post_row.attrs["href"] 65 | ) 66 | self.output_data.append( 67 | { 68 | "author": _author, 69 | "post": _post 70 | } 71 | ) 72 | 73 | return self.output_data 74 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | alembic==1.8.1 2 | async-timeout==4.0.2 3 | attrs==22.1.0 4 | beautifulsoup4==4.11.1 5 | bs4==0.0.1 6 | cchardet==2.1.7 7 | certifi==2022.6.15 8 | cffi==1.15.1 9 | charset-normalizer==2.1.0 10 | click==8.1.3 11 | commonmark==0.9.1 12 | elastic-transport==8.1.2 13 | elasticsearch==8.3.3 14 | fake-useragent==0.1.11 15 | frozenlist==1.3.1 16 | geographiclib==1.52 17 | geopy==2.2.0 18 | googletransx==2.4.2 19 | greenlet==1.1.2 20 | idna==3.3 21 | Mako==1.2.1 22 | MarkupSafe==2.1.1 23 | multidict==6.0.2 24 | pycares==4.2.2 25 | pycparser==2.21 26 | Pygments==2.12.0 27 | PySocks==1.7.1 28 | python-dateutil==2.8.2 29 | python-socks==2.0.3 30 | pytz==2022.2.1 31 | requests==2.28.1 32 | rich==12.5.1 33 | schedule==1.1.0 34 | six==1.16.0 35 | soupsieve==2.3.2.post1 36 | SQLAlchemy==1.4.40 37 | tomli==2.0.1 38 | urllib3==1.26.11 39 | yarl==1.8.1 40 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | from os import ( 2 | name, system 3 | ) 4 | from sys import stdout 5 | from random import choice 6 | from time import sleep 7 | from requests import post 8 | from datetime import datetime 9 | 10 | 11 | def cleanup(): 12 | """ 13 | This function will clean the command line 14 | """ 15 | system("cls") if name == "nt" else system("clear") 16 | 17 | 18 | def ascii_art(): 19 | """ 20 | This method will print ASCII art of writeup app 21 | """ 22 | clear = "\x1b[0m" 23 | colors = [36, 32, 34, 35, 31, 37] 24 | 25 | x = r""" 26 | $$\ $$\ $$\ $$\ $$$$$$\ 27 | $$ | $\ $$ | \__| $$ | $$ __$$\ 28 | $$ |$$$\ $$ | $$$$$$\ $$\ $$$$$$\ $$$$$$\ $$\ $$\ $$$$$$\ $$ / $$ | $$$$$$\ $$$$$$\ 29 | $$ $$ $$\$$ |$$ __$$\ $$ |\_$$ _| $$ __$$\ $$ | $$ |$$ __$$\ $$$$$$$$ |$$ __$$\ $$ __$$\ 30 | $$$$ _$$$$ |$$ | \__|$$ | $$ | $$$$$$$$ |$$ | $$ |$$ / $$ | $$ __$$ |$$ / $$ |$$ / $$ | 31 | $$$ / \$$$ |$$ | $$ | $$ |$$\ $$ ____|$$ | $$ |$$ | $$ | $$ | $$ |$$ | $$ |$$ | $$ | 32 | $$ / \$$ |$$ | $$ | \$$$$ |\$$$$$$$\ \$$$$$$ |$$$$$$$ | $$ | $$ |$$$$$$$ |$$$$$$$ | 33 | \__/ \__|\__| \__| \____/ \_______| \______/ $$ ____/ \__| \__|$$ ____/ $$ ____/ 34 | $$ | $$ | $$ | 35 | github.com/0xuf $$ | $$ | $$ | 36 | v1.0 \__| \__| \__| 37 | """ 38 | for N, line in enumerate(x.split("\n")): 39 | stdout.write("\x1b[1;%dm%s%s\n" % (choice(colors), line, clear)) 40 | sleep(0.05) 41 | 42 | 43 | def notify_script_launch(notify_webhook: str) -> bool: 44 | """ 45 | This function will notify you when Script launched. 46 | :param notify_webhook: get webhook_url to send message 47 | :rtype: bool 48 | """ 49 | data = dict( 50 | content=f"Writeup app launched at {datetime.now()}" 51 | ) 52 | _resp = post(url=notify_webhook, json=data) 53 | if _resp.status_code == 204: 54 | return True 55 | 56 | return False 57 | 58 | 59 | def notify_writeup(notify_webhook: str, data) -> bool: 60 | """ 61 | This function will notify you when it finds a new write-up on the Discord server 62 | :param notify_webhook: get webhook_url to send message 63 | :param data: get writeup-data to post it on discord chat 64 | :rtype: bool 65 | """ 66 | author = data.get("author") 67 | _post = data.get("post") 68 | 69 | # Check Author username is None or not 70 | if author["username"] is None: 71 | parse_data = f"🟢 New WriteUp !\n\n" \ 72 | f"🖇 WriteUp Link: ||[{_post['title']}]({_post['link']})||\n\n" \ 73 | f"🧑‍💻 Writeup Author: ||{author['name']}||\n" 74 | else: 75 | parse_data = f"🟢 New WriteUp !\n\n" \ 76 | f"🖇 WriteUp Link: ||[{_post['title']}]({_post['link']})||\n\n" \ 77 | f"🧑‍💻 Writeup Author: ||[{author['name']}]({author['username']})||\n" 78 | 79 | data = dict( 80 | content=parse_data 81 | ) 82 | _resp = post(url=notify_webhook, json=data) 83 | if _resp.status_code == 204: 84 | return True 85 | 86 | return False 87 | --------------------------------------------------------------------------------