├── assets ├── step_1.png └── step_2.png ├── .gitignore ├── requirements.txt ├── Makefile ├── README.md ├── LICENSE.md ├── cli.py └── slotter.py /assets/step_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awbx/slotter/HEAD/assets/step_1.png -------------------------------------------------------------------------------- /assets/step_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/awbx/slotter/HEAD/assets/step_2.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore .env file 2 | .env 3 | # ignore env folder 4 | env 5 | # ignore __pycache__ folder 6 | __pycache__ 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | beautifulsoup4==4.11.1 2 | python-dotenv==0.21.0 3 | requests==2.28.1 4 | click==8.1.3 5 | bs4==0.0.1 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | install: 5 | touch .env && echo "INTRA_SESSION_ID=YOUR INTRA SESSION ID" > .env # TODO check if .env file exists to avoid overwriting. 6 | python3.9 -m pip install -r requirements.txt -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slotter 2 | This's a slotter CLI, that will fill/remove the available slots. 3 | 4 | PS: this is a beta version 5 | 6 | ## installation 7 | 8 | ```bash 9 | # using Makefile 10 | make install 11 | 12 | # manually install 13 | python3.9 -m pip install -r requirements.txt 14 | ``` 15 | 16 | ## usage 17 | -First of all, you should create a .env file that will contain the following content: 18 | ``` 19 | INTRA_SESSION_ID=YOUR INTRA SESSION ID 20 | ``` 21 | -Follow the steps in the screenshots to get your session id. 22 | * Step1 -> go to Cookies 23 | Step 1 24 | 25 | * Step2 -> Copy the `_intra_42_session_production` content 26 | Step 2 27 | 28 | 29 | * take slots 30 | - `python3.9 cli.py take-slots` 31 | * delete slots 32 | - `python3.9 cli.py delete-slots` 33 | 34 | ## TODO 35 | 36 | - Add User-Agent to act like a browser 37 | - Add more options(features) to `take-slots` command 38 | - Add day range feature to `take-slots` command 39 | 40 | ## License 41 | [MIT](./LICENSE.md) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Abdelhadi Sabani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | from dotenv import load_dotenv 2 | from slotter import Slotter 3 | import click, sys, os 4 | 5 | # load the .env variables 6 | load_dotenv() 7 | 8 | session_id = os.environ.get("INTRA_SESSION_ID") 9 | 10 | if not session_id: 11 | print("Please add your INTRA session id!", file=sys.stderr) 12 | sys.exit(1) 13 | 14 | # create an instance from Slotter class 15 | 16 | slotter = Slotter(session_id) 17 | 18 | # login with session intra and check if valid 19 | 20 | if not slotter.login(): 21 | print("Please add a valid INTRA session id!", file=sys.stderr) 22 | sys.exit(1) 23 | 24 | 25 | @click.group() 26 | def cli(): 27 | "Slotter Command line interface." 28 | 29 | def validate_duration(ctx, param, value): 30 | if value < 30: 31 | raise click.BadParameter('duration should be greate than 30.') 32 | return value 33 | 34 | 35 | @cli.command 36 | @click.option("--duration", "-d", type=int, callback=validate_duration, default=30) 37 | def take_slots(duration): 38 | """take the untaken slots""" 39 | slotter.take_slots(duration) 40 | 41 | 42 | 43 | @cli.command 44 | def delete_slots(): 45 | """delete the taken slots""" 46 | slotter.delete_slots() 47 | 48 | if __name__ == "__main__": 49 | cli(prog_name='slotter') -------------------------------------------------------------------------------- /slotter.py: -------------------------------------------------------------------------------- 1 | from bs4 import BeautifulSoup 2 | from requests import session 3 | import datetime 4 | 5 | BASE_URL = "https://profile.intra.42.fr" 6 | 7 | 8 | class Slotter: 9 | def __init__(self, session_id): 10 | self.sess = session() 11 | self.session_id = session_id 12 | 13 | def login(self): 14 | self.sess.cookies.update( 15 | {"_intra_42_session_production": self.session_id}) 16 | resp = self.sess.get( 17 | f"{BASE_URL}/", allow_redirects=False) 18 | if resp.is_redirect: 19 | return False 20 | soup = BeautifulSoup(resp.text, "html.parser") 21 | csrf_meta = soup.find("meta", attrs={"name": "csrf-token"}) 22 | self.sess.headers['X-Csrf-Token'] = csrf_meta.get("content") 23 | return True 24 | 25 | def take_slots(self, duration=30): 26 | start = datetime.datetime.now() + datetime.timedelta(minutes=60) 27 | while True: 28 | data = { 29 | "slot[begin_at]": start.strftime("%Y-%m-%dT%H:%M:%S"), 30 | "slot[end_at]": (start + datetime.timedelta(minutes=duration)).strftime("%Y-%m-%dT%H:%M:%S"), 31 | } 32 | resp = self.sess.post( 33 | f"{BASE_URL}/slots.json", data=data) 34 | payload = resp.json() 35 | if payload['message'].startswith('Has') or payload['message'].startswith('No overlapping!'): 36 | start += datetime.timedelta(minutes=30) 37 | continue 38 | elif payload['message'].startswith('Ending') and payload['status'] != 200: 39 | break 40 | start = datetime.datetime.strptime(payload.get( 41 | 'data')['end'][:-6], '%Y-%m-%dT%H:%M:%S.%f') 42 | start += datetime.timedelta(minutes=30) 43 | print("Taking slots...", end='\r') 44 | 45 | def delete_slots(self): 46 | start = datetime.datetime.now() 47 | end = start + datetime.timedelta(minutes=60 * 24 * 30) 48 | resp = self.sess.get(f'{BASE_URL}/slots.json', params={ 49 | "start": start.strftime("%Y-%m-%d"), 50 | "end": end.strftime("%Y-%m-%d")}) 51 | slots = resp.json() 52 | if not slots: 53 | print("There's no slots to delete!") 54 | return 55 | for slot in slots: 56 | resp = self.sess.post(f"{BASE_URL}/slots/{slot['id']}.json", data={ 57 | "ids": slot['ids'], 58 | "_method": "delete", 59 | "confirm": False 60 | }) 61 | print("Deleting Slots...", end="\r") 62 | --------------------------------------------------------------------------------