41 Commits

Author SHA1 Message Date
9bd96ad876 version bump 2024-04-13 11:09:30 +02:00
ed517e7b03 Fix 'Welcome Tour'
Reviewed-on: https://gitea.augustin64.fr/piair/MsRewards-Reborn/pulls/11
2024-04-13 11:07:04 +02:00
01416c0e11 Check before scrolling, but should be good to merge ! 2024-04-13 11:01:25 +02:00
19cf77c6bd make an actual choice 2024-04-13 10:57:32 +02:00
feb7834d1f sleep a bit 2024-04-13 10:26:53 +02:00
1ea1ff776e wait until visible 2024-04-13 10:26:21 +02:00
ae023688a4 welcome-tour: URL check 2024-04-13 10:15:26 +02:00
fcb40537dc Use newer method 2024-04-13 10:06:21 +02:00
d7d2f49a3f pas la seule occurrence évidemment 2024-04-12 23:46:26 +02:00
68b5de32d6 fix de la popup "explorer le programme"
peu satisfaisant, consiste juste à fermer la popup à chaque fois qu'elle apparaît sans s'en débarrasser définitivement
2024-04-12 23:28:46 +02:00
c5e9fb1267 version bump ? 2024-04-12 20:01:53 +02:00
6ade73617a Make console resizable 2024-04-12 20:01:34 +02:00
37e8f6f61b Fix fake_popen 2024-04-12 16:20:25 +02:00
db6fa9b6b0 Add env variables arguments to flask app
- NO_SUBPROCESS to fake subprocesses calls
- APP_ROOT to use the app outside of Docker
2024-04-12 15:55:14 +02:00
d6988c03b4 Oops.. fixed nginx config 2024-04-12 15:51:20 +02:00
449d2da410 Use a different profile for mobile browser 2024-04-12 09:31:50 +02:00
3eb193eca3 Disable stream buffering on upstream nginx server 2024-04-10 14:24:57 +02:00
f566b2eeda Potentially fix no-discord 2024-04-10 12:15:14 +02:00
52e88f81b9 More checks on TFA 2024-04-10 12:14:41 +02:00
1a8137783c Even faster cached build 2024-04-10 11:43:54 +02:00
6f13b2532d Add clean.sh 2024-04-10 11:36:33 +02:00
3978c44bbc Use chrome profile to change language 2024-04-10 11:35:46 +02:00
49dc53ed32 Fix 2FA (language setting is not always working) 2024-04-10 11:04:54 +02:00
ba66a96c65 Check if creds are not empty 2024-04-10 10:29:53 +02:00
db157771de Merge flags 2024-04-08 16:51:43 +02:00
cbd1ad93a6 version bump 2024-04-08 16:38:16 +02:00
afabd94f0d Re-implement cookie login
with chrome profiles
2024-04-08 16:36:38 +02:00
81deaf05b0 Update chrome.deb URL 2024-04-08 16:34:36 +02:00
9af0f4aadb build.sh: check permissions 2024-04-03 15:42:14 +02:00
1d16294c04 Add more logs (custom.txt..) to logs view 2024-04-03 15:34:46 +02:00
fae2033061 Simplify Dockerfile to cache building layers 2024-04-03 15:26:02 +02:00
50c4036c73 Merge pull request 'Fix ANSI code formatting' (#9) from augustin64/MsRewards-Reborn:master into master
Reviewed-on: https://gitea.augustin64.fr/piair/MsRewards-Reborn/pulls/9
2024-03-31 12:05:17 +02:00
c683472895 Fix ANSI code formatting 2024-03-29 16:53:59 +01:00
178f2d472a Merge pull request 'Mise en forme des codes ANSI dans les logs' (#8) from augustin64/MsRewards-Reborn:augustin64-ansi-up into master
Reviewed-on: https://gitea.augustin64.fr/piair/MsRewards-Reborn/pulls/8
2024-03-28 23:56:33 +01:00
d3137f858a Mise en forme des codes ANSI dans les logs 2024-03-25 10:41:34 +01:00
d2ad467d4e Oops, pas push le bon fichier 2024-03-24 20:30:09 +01:00
b45e9e549f mise à jour automatique de chrome 2024-03-24 20:27:40 +01:00
200b0d8a86 Trying to fix an issue when an account fail 2024-03-05 21:18:39 +01:00
4a5af6455d Fixed 2FA issue + added better logs 2024-03-01 17:29:15 +01:00
49b691d736 not tested enough apparently 2024-02-28 18:22:15 +01:00
9549a6dea3 not tested enough apparently 2024-02-28 18:12:32 +01:00
23 changed files with 436 additions and 1624 deletions

2
.dockerignore Normal file
View File

@ -0,0 +1,2 @@
**/.venv
user_data/*

4
.gitignore vendored
View File

@ -3,18 +3,16 @@ geckodriver.log
.vscode/ .vscode/
.idea .idea
venv venv
**/.venv
/Git /Git
page.html page.html
screenshot.png screenshot.png
login.csv
data data
**/__pycache__ **/__pycache__
user_data/* user_data/*
install.sh install.sh
nohup.out nohup.out
points.csv
file.png file.png
user_data/configs.json
*.ts *.ts
LICENSE LICENSE
README.md README.md

View File

@ -1,21 +1,42 @@
FROM python:3.10 FROM python:3.10
ENV DEBIAN_FRONTEND noninteractive ENV DEBIAN_FRONTEND noninteractive
WORKDIR /app/ WORKDIR /app/
RUN apt update \
&& wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_amd64.deb \ # Initial apt install
&& dpkg -i libssl1.1_1.1.0g-2ubuntu4_amd64.deb \ RUN apt update
&& apt install redis libgtk-4-1 libvulkan1 libxdamage1 -y \ RUN apt install -y libgtk-4-1 libvulkan1 libxdamage1 \
&& curl -sSLO https://nc.piair.xyz/s/BKLsBWoZkTdYjfq/download/chrome.deb \ novnc websockify xvfb nginx nano tzdata \
&& ln -fs /usr/share/zoneinfo/Europe/Paris /etc/localtime \ sqlite3 apt-transport-https software-properties-common \
&& git clone https://gitea.augustin64.fr/piair/MsRewards-Reborn \ wget wfrench tigervnc-standalone-server libasound2 \
&& python3 -m pip install -r MsRewards-Reborn/requirements.txt \ libatk-bridge2.0-0 libnss3 libnspr4 xvfb libgbm1 libatk1.0-0 \
&& wget -q -O /usr/share/keyrings/grafana.key https://apt.grafana.com/gpg.key \ libu2f-udev libatspi2.0-0 libcups2 libxkbcommon0 libxrandr2 \
&& curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg \ libdbus-1-3 xdg-utils fonts-liberation libdrm2
&& echo "deb [signed-by=/usr/share/keyrings/grafana.key] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list \
&& apt update \ # Additional repos and packages
&& apt install novnc websockify grafana xvfb nginx nano tzdata sqlite3 apt-transport-https software-properties-common wget wfrench tigervnc-standalone-server libasound2 libatk-bridge2.0-0 libnss3 libnspr4 xvfb libgbm1 libatk1.0-0 libu2f-udev libatspi2.0-0 libcups2 libxkbcommon0 libxrandr2 libdbus-1-3 xdg-utils fonts-liberation libdrm2 -y \ RUN wget http://security.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.0g-2ubuntu4_amd64.deb \
&& bash MsRewards-Reborn/config/config.sh \ && dpkg -i libssl1.1_1.1.0g-2ubuntu4_amd64.deb
RUN curl -sSL http://mirror.cs.uchicago.edu/google-chrome/pool/main/g/google-chrome-stable/google-chrome-stable_123.0.6312.86-1_amd64.deb -o chrome.deb \
&& dpkg -i chrome.deb && dpkg -i chrome.deb
RUN ln -fs /usr/share/zoneinfo/Europe/Paris /etc/localtime
RUN wget -q -O /usr/share/keyrings/grafana.key https://apt.grafana.com/gpg.key \
&& echo "deb [signed-by=/usr/share/keyrings/grafana.key] https://apt.grafana.com stable main" | tee -a /etc/apt/sources.list.d/grafana.list
RUN curl -fsSL https://packages.redis.io/gpg | gpg --dearmor -o /usr/share/keyrings/redis-archive-keyring.gpg
# Install from new repo
RUN apt update \
&& apt install -y redis grafana
# Configure Grafana
RUN grafana-cli plugins install frser-sqlite-datasource
COPY requirements.txt /app/requirements.txt
RUN python3 -m pip install -r requirements.txt
# Setup app
RUN git clone https://gitea.augustin64.fr/piair/MsRewards-Reborn
# Use this instead when developping locally:
# COPY . /app/MsRewards-Reborn
RUN bash MsRewards-Reborn/config/config.sh
ENV TZ="Europe/Paris" ENV TZ="Europe/Paris"
WORKDIR /app/MsRewards-Reborn/Flask/ WORKDIR /app/MsRewards-Reborn/Flask/

View File

@ -13,11 +13,27 @@ import re
from requests import get from requests import get
import redis import redis
APP_ROOT = os.getenv("APP_ROOT")
if APP_ROOT is None:
APP_ROOT = "/app/MsRewards-Reborn/"
NO_SUBPROCESS = os.getenv("NO_SUBPROCESS")
if NO_SUBPROCESS is not None:
def fake_popen(*args, **kwargs):
print("Calling subprocess.Popen with", args, kwargs)
subprocess.Popen = fake_popen
print("Faking subprocess calls")
# redis part for live update # redis part for live update
pool = redis.ConnectionPool(host='localhost', port=6379, db=0) pool = redis.ConnectionPool(host='localhost', port=6379, db=0)
r = redis.Redis(connection_pool=pool) r = redis.Redis(connection_pool=pool)
def get_path(path):
return os.path.join(APP_ROOT, path)
def generate_output(): def generate_output():
pubsub = r.pubsub() pubsub = r.pubsub()
pubsub.subscribe('console') pubsub.subscribe('console')
@ -32,7 +48,7 @@ def generate_output():
# the end # the end
global password global password
with open("/app/MsRewards-Reborn/user_data/flask.json", "r") as inFile: with open(get_path("user_data/flask.json"), "r") as inFile:
data = json.load(inFile) data = json.load(inFile)
password = data["password"] password = data["password"]
@ -40,7 +56,7 @@ secret = data["secret"]
if secret == "": if secret == "":
import secrets import secrets
secret = secrets.token_hex() secret = secrets.token_hex()
with open("/app/MsRewards-Reborn/user_data/flask.json", "w") as inFile: with open(get_path("user_data/flask.json"), "w") as inFile:
data = { data = {
"password": password, "password": password,
"secret": secret "secret": secret
@ -70,14 +86,14 @@ scheduler.add_job( # on relance le job
def start_ms(i): def start_ms(i):
print("\033[32m" + f"Starting config {i}" + "\033[0m") print("\033[32m" + f"Starting config {i}" + "\033[0m")
log = open(f"/app/MsRewards-Reborn/Flask/static/logs/{i}.txt", 'a') # so that data written to it will be appended log = open(get_path(f"Flask/static/logs/{i}.txt"), 'a') # so that data written to it will be appended
subprocess.Popen([f"python3 -u /app/MsRewards-Reborn/V6.py -c {i}"], stdout=log, stderr=log, shell=True) subprocess.Popen([f"python3 -u {get_path('V6.py')} -c {i}"], stdout=log, stderr=log, shell=True)
log.close() log.close()
TriggerDict = {} TriggerDict = {}
def update_jobs(): def update_jobs():
with open("/app/MsRewards-Reborn/user_data/configs.json", "r") as inFile: with open(get_path("user_data/configs.json"), "r") as inFile:
configs = json.load(inFile) configs = json.load(inFile)
for i in configs: for i in configs:
try : try :
@ -120,7 +136,7 @@ app = Flask(__name__)
@app.context_processor @app.context_processor
def inject_default_variables(): def inject_default_variables():
with open("/app/MsRewards-Reborn/version", "r") as f: with open(get_path("version"), "r") as f:
version = f.readline().replace("\n", '') version = f.readline().replace("\n", '')
return dict(version=version) return dict(version=version)
""" """
@ -175,7 +191,7 @@ def change_password():
if request.method == 'POST': if request.method == 'POST':
password = request.form["password"] password = request.form["password"]
subprocess.Popen(["grafana-cli", "admin", "reset-admin-password", password]) subprocess.Popen(["grafana-cli", "admin", "reset-admin-password", password])
with open("/app/MsRewards-Reborn/user_data/flask.json", "w") as inFile: with open(get_path("user_data/flask.json"), "w") as inFile:
data = { data = {
"password": password, "password": password,
"secret": secret "secret": secret
@ -201,21 +217,21 @@ def load_user(userid):
@app.route("/") @app.route("/")
def main(): def main():
with open("/app/MsRewards-Reborn/user_data/configs.json", "r") as inFile: with open(get_path("user_data/configs.json"), "r") as inFile:
configs = json.load(inFile) configs = json.load(inFile)
return(render_template("schedule.html", data=configs)) return(render_template("schedule.html", data=configs))
@app.route("/discord/") @app.route("/discord/")
def discord_get(): def discord_get():
with open("/app/MsRewards-Reborn/user_data/discord.json", "r") as inFile: with open(get_path("user_data/discord.json"), "r") as inFile:
data = json.load(inFile) data = json.load(inFile)
return(render_template("discord.html", data=data, len=maxi(data))) return(render_template("discord.html", data=data, len=maxi(data)))
@app.route("/discord/", methods=["post"]) @app.route("/discord/", methods=["post"])
def discord_post(): def discord_post():
with open("/app/MsRewards-Reborn/user_data/discord.json", "r") as inFile: with open(get_path("user_data/discord.json"), "r") as inFile:
data = json.load(inFile) data = json.load(inFile)
action = request.form action = request.form
if action['DISCORD'] == "delete" : if action['DISCORD'] == "delete" :
@ -237,7 +253,7 @@ def discord_post():
name = action["name"] if action["name"] else f"unnamed{action['select']}" name = action["name"] if action["name"] else f"unnamed{action['select']}"
data[config] = {"errorsL" : errorsL, "errorsT": errorsT, "successT": successT, "successL": successL, "name": name} data[config] = {"errorsL" : errorsL, "errorsT": errorsT, "successT": successT, "successL": successL, "name": name}
with open("/app/MsRewards-Reborn/user_data/discord.json", "w") as outFile: with open(get_path("user_data/discord.json"), "w") as outFile:
json.dump(data, outFile) json.dump(data, outFile)
return(render_template("discord.html", data=data, len=maxi(data))) return(render_template("discord.html", data=data, len=maxi(data)))
@ -249,7 +265,7 @@ def dev2():
@app.route("/settings/") @app.route("/settings/")
def settings_get(): def settings_get():
with open("/app/MsRewards-Reborn/user_data/settings.json", "r") as inFile: with open(get_path("user_data/settings.json"), "r") as inFile:
settings = json.load(inFile) settings = json.load(inFile)
return(render_template("settings.html", data=settings)) return(render_template("settings.html", data=settings))
@ -259,21 +275,21 @@ def settings_post():
settings = {} settings = {}
action = request.form action = request.form
settings['avatarlink'] = action["avatarlink"] settings['avatarlink'] = action["avatarlink"]
with open("/app/MsRewards-Reborn/user_data/settings.json", "w") as inFile: with open(get_path("user_data/settings.json"), "w") as inFile:
json.dump(settings, inFile) json.dump(settings, inFile)
return(render_template("settings.html", data=settings)) return(render_template("settings.html", data=settings))
@app.route("/proxy/") @app.route("/proxy/")
def proxy_get(): def proxy_get():
with open("/app/MsRewards-Reborn/user_data/proxy.json", "r") as inFile: with open(get_path("user_data/proxy.json"), "r") as inFile:
j = json.load(inFile) j = json.load(inFile)
return(render_template("proxy.html", data=j, len=maxi(j))) return(render_template("proxy.html", data=j, len=maxi(j)))
@app.route("/proxy/", methods=["post"]) @app.route("/proxy/", methods=["post"])
def proxy_post(): def proxy_post():
with open("/app/MsRewards-Reborn/user_data/proxy.json", "r") as inFile: with open(get_path("user_data/proxy.json"), "r") as inFile:
data = json.load(inFile) data = json.load(inFile)
action = request.form action = request.form
print(action) print(action)
@ -290,21 +306,21 @@ def proxy_post():
except : except :
print("error : probably bad config") print("error : probably bad config")
with open("/app/MsRewards-Reborn/user_data/proxy.json", "w") as outFile: with open(get_path("user_data/proxy.json"), "w") as outFile:
json.dump(data, outFile) json.dump(data, outFile)
return(render_template("proxy.html", data=data, len=maxi(data))) return(render_template("proxy.html", data=data, len=maxi(data)))
@app.route("/schedule/") @app.route("/schedule/")
def schedule_get(): def schedule_get():
with open("/app/MsRewards-Reborn/user_data/configs.json", "r") as inFile: with open(get_path("user_data/configs.json"), "r") as inFile:
configs = json.load(inFile) configs = json.load(inFile)
return(render_template("schedule.html", data=configs)) return(render_template("schedule.html", data=configs))
@app.route("/schedule/", methods=["post"]) @app.route("/schedule/", methods=["post"])
def schedule_post(): def schedule_post():
with open("/app/MsRewards-Reborn/user_data/configs.json", "r") as inFile: with open(get_path("user_data/configs.json"), "r") as inFile:
configs = json.load(inFile) configs = json.load(inFile)
data = dict(request.form) data = dict(request.form)
@ -318,7 +334,7 @@ def schedule_post():
configs[i]["time"] = data[f"time{i}"] configs[i]["time"] = data[f"time{i}"]
configs[i]["enabled"] = data[f"switch{i}"] == "on" configs[i]["enabled"] = data[f"switch{i}"] == "on"
with open("/app/MsRewards-Reborn/user_data/configs.json", "w") as inFile: with open(get_path("user_data/configs.json"), "w") as inFile:
json.dump(configs, inFile) json.dump(configs, inFile)
update_jobs() update_jobs()
return(render_template("schedule.html", data=configs)) return(render_template("schedule.html", data=configs))
@ -326,11 +342,11 @@ def schedule_post():
@app.route("/config/") @app.route("/config/")
def config_get(): def config_get():
with open("/app/MsRewards-Reborn/user_data/proxy.json", "r") as inFile: with open(get_path("user_data/proxy.json"), "r") as inFile:
proxys = json.load(inFile) proxys = json.load(inFile)
with open("/app/MsRewards-Reborn/user_data/discord.json", "r") as inFile: with open(get_path("user_data/discord.json"), "r") as inFile:
discords = json.load(inFile) discords = json.load(inFile)
with open("/app/MsRewards-Reborn/user_data/configs.json", "r") as inFile: with open(get_path("user_data/configs.json"), "r") as inFile:
configs = json.load(inFile) configs = json.load(inFile)
return(render_template("config.html", data=configs, discords=discords, proxys=proxys, configs=configs, len=maxi(configs))) return(render_template("config.html", data=configs, discords=discords, proxys=proxys, configs=configs, len=maxi(configs)))
@ -338,11 +354,11 @@ def config_get():
@app.route("/config/", methods=["POST"]) @app.route("/config/", methods=["POST"])
def config_post(): def config_post():
action = request.form action = request.form
with open("/app/MsRewards-Reborn/user_data/proxy.json", "r") as inFile: with open(get_path("user_data/proxy.json"), "r") as inFile:
proxys = json.load(inFile) proxys = json.load(inFile)
with open("/app/MsRewards-Reborn/user_data/discord.json", "r") as inFile: with open(get_path("user_data/discord.json"), "r") as inFile:
discords = json.load(inFile) discords = json.load(inFile)
with open("/app/MsRewards-Reborn/user_data/configs.json", "r") as inFile: with open(get_path("user_data/configs.json"), "r") as inFile:
configs = json.load(inFile) configs = json.load(inFile)
if action["data"] == "delete": if action["data"] == "delete":
@ -365,16 +381,26 @@ def config_post():
"enabled":"False", "enabled":"False",
"accounts": comptes "accounts": comptes
} }
with open("/app/MsRewards-Reborn/user_data/configs.json", "w") as outFile: with open(get_path("user_data/configs.json"), "w") as outFile:
json.dump(configs, outFile) json.dump(configs, outFile)
return(render_template("config.html", data=configs, discords=discords, proxys=proxys, configs=configs, len=maxi(configs))) return(render_template("config.html", data=configs, discords=discords, proxys=proxys, configs=configs, len=maxi(configs)))
@app.route("/logs/", methods=["GET", "POST"]) @app.route("/logs/", methods=["GET", "POST"])
def logs(): def logs():
with open("/app/MsRewards-Reborn/user_data/configs.json", "r") as inFile: with open(get_path("user_data/configs.json"), "r") as inFile:
configs = json.load(inFile) configs = json.load(inFile)
print(configs)
return(render_template("logs.html", data=configs)) files = [(configs[i]["name"], i) for i in configs]
config_files = [i[1] for i in files]
for f in os.listdir(get_path("Flask/static/logs")):
fid = ".".join(f.split(".")[:-1]) # filename without .txt
if f != ".gitignore" and fid not in config_files:
files.append((f, fid))
return render_template(
"logs.html",
files=files
)
@app.route("/stats/", methods=["GET", "POST"]) @app.route("/stats/", methods=["GET", "POST"])
@ -385,21 +411,21 @@ def stats():
@app.route("/override/", methods=["POST"]) @app.route("/override/", methods=["POST"])
def override_post(): def override_post():
json = request.form.to_dict(flat=False) json = request.form.to_dict(flat=False)
log = open(f"/app/MsRewards-Reborn/Flask/static/logs/custom.txt", 'w') # so that data written to it will be appended log = open(get_path("Flask/static/logs/custom.txt"), 'w') # so that data written to it will be appended
subprocess.Popen([f"python3 -u /app/MsRewards-Reborn/V6.py -c {json['config'][0]} --json \"{json}\""], stdout=log, stderr=log, shell=True) subprocess.Popen([f"python3 -u {get_path('V6.py')} -c {json['config'][0]} --json \"{json}\""], stdout=log, stderr=log, shell=True)
log.close() log.close()
return(render_template("vnc_post.html")) return(render_template("vnc_post.html"))
@app.route("/override/", methods=["GET"]) @app.route("/override/", methods=["GET"])
def override_get(): def override_get():
with open("/app/MsRewards-Reborn/user_data/configs.json", "r") as inFile: with open(get_path("user_data/configs.json"), "r") as inFile:
configs = json.load(inFile) configs = json.load(inFile)
return(render_template("vnc_get.html", configs=configs)) return(render_template("vnc_get.html", configs=configs))
@app.route('/download/<path:filename>', methods=['GET', 'POST']) @app.route('/download/<path:filename>', methods=['GET', 'POST'])
@login_required @login_required
def download(filename): def download(filename):
return send_from_directory(directory='/app/MsRewards-Reborn/user_data/', path=filename, as_attachment=True) return send_from_directory(directory=get_path("user_data/"), path=filename, as_attachment=True)
def allowed_file(filename): def allowed_file(filename):
@ -420,8 +446,8 @@ def upload_file():
elif file and allowed_file(file.filename): elif file and allowed_file(file.filename):
filename = secure_filename(file.filename) filename = secure_filename(file.filename)
print(os.path.join('/app/MsRewards-Reborn/user_data/', filename)) print(os.path.join(get_path("user_data/"), filename))
file.save(os.path.join('/app/MsRewards-Reborn/user_data/', filename)) file.save(os.path.join(get_path("user_data/"), filename))
i += 1 i += 1
print(i) print(i)
@ -439,4 +465,7 @@ def maxi(dict):
update_jobs() update_jobs()
subprocess.Popen(["bash",'/app/MsRewards-Reborn/config/request.sh']) subprocess.Popen(["bash", get_path("config/request.sh")])
if __name__ == "__main__":
app.run()

View File

@ -0,0 +1,17 @@
#console {
height: 100%;
width: 20%;
float: left;
resize: horizontal;
overflow: auto;
}
#vnc-container {
height: 100%;
width: 80%;
float: left;
}
.container {
height: 100%;
}

View File

@ -8,14 +8,37 @@
<select name="select" onchange="change_logs(this.value)"> <select name="select" onchange="change_logs(this.value)">
<option id="null" value="0">Choisir une config</option> <option id="null" value="0">Choisir une config</option>
{% for i in data %} {% for file in files %}
<option id="{{data[i]['name']}}" value="{{i}}">{{data[i]['name']}}</option> <option id="{{ file[0] }}" value="{{ file[1] }}">{{ file[0] }}</option>
{% endfor %} {% endfor %}
</select> </select>
<br><br> <br><br>
<iframe type="text/html" src="{{url_for('static', filename='logs/1.txt')}}" width="100%" height="85%" id="embed"> <iframe type="text/html" src="{{url_for('static', filename='logs/1.txt')}}" width="100%" height="85%" id="embed"></iframe>
<script defer>
const iframe = document.getElementsByTagName("iframe")[0];
var script = document.createElement('script');
// Wait until ansi_up load
script.onload = function () {
// Wait until iframe load
iframe.onload = function() {
const subdoc = iframe.contentWindow.document;
const subBody = subdoc.getElementsByTagName("body")[0]
let ansiOutput = subBody;
// Depending on the content encoding (and maybe on the browser)
// a <pre> is added around the content of the file
if (subBody.getElementsByTagName("pre").length > 0) {
ansiOutput = subBody.getElementsByTagName("pre")[0];
}
const ansi_up = new AnsiUp();
ansiOutput.innerHTML = ansi_up.ansi_to_html(ansiOutput.innerText);
}
};
script.src = "https://cdn.jsdelivr.net/npm/ansi_up@4.0.4/ansi_up.js";
document.head.appendChild(script);
</script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -6,19 +6,19 @@
{% if not current_user.is_authenticated %} {% if not current_user.is_authenticated %}
<button class="unselected" onclick="location.href = '/login';">login</button> <button class="unselected" onclick="location.href = '/login';">login</button>
{% else %} {% else %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/vnc-post.css') }}"/>
<link rel="stylesheet" href="{{ url_for('static', filename='node_modules/xterm/css/xterm.css') }}"/> <link rel="stylesheet" href="{{ url_for('static', filename='node_modules/xterm/css/xterm.css') }}"/>
<script src="{{ url_for('static', filename='node_modules/xterm/lib/xterm.js') }}"></script> <script src="{{ url_for('static', filename='node_modules/xterm/lib/xterm.js') }}"></script>
<script src="{{ url_for('static', filename='node_modules/xterm-addon-fit/lib/xterm-addon-fit.js') }}"></script> <script src="{{ url_for('static', filename='node_modules/xterm-addon-fit/lib/xterm-addon-fit.js') }}"></script>
<script> <script>
document.getElementsByClassName("content")[0].style.padding = "0 0" document.getElementsByClassName("content")[0].style.padding = "0 0"
</script> </script>
<div style="height: 100%"> <div class="container">
<div id="console"></div>
<div id="console" style="height: 100%; width: 20%; float:left;"></div> <div id="vnc-container">
<div style="height: 100%; width: 80%; float:left;">
<iframe src="/novnc/vnc.html?resize=scale&path=novnc/websockify&autoconnect=true&view_only" <iframe src="/novnc/vnc.html?resize=scale&path=novnc/websockify&autoconnect=true&view_only"
width="100%" height="100%" frameborder="0"></iframe> width="100%" height="100%" frameborder="0">
</iframe>
</div> </div>
</div> </div>

125
V6.py
View File

@ -1,4 +1,4 @@
#!/usr/bin/python3.10 #!/usr/bin/python3
from modules.Classes.Config import Config from modules.Classes.Config import Config
from modules.Classes.DiscordLogger import DiscordLogger from modules.Classes.DiscordLogger import DiscordLogger
from modules.Classes.UserCredentials import UserCredentials from modules.Classes.UserCredentials import UserCredentials
@ -9,6 +9,8 @@ from modules.db import add_to_database
from modules.driver_tools import * from modules.driver_tools import *
from modules.error import * from modules.error import *
import random
# create a webdriver # create a webdriver
def create_driver(mobile=False): def create_driver(mobile=False):
@ -22,16 +24,33 @@ def create_driver(mobile=False):
"AppleWebKit/537.36 (KHTML, like Gecko)" "AppleWebKit/537.36 (KHTML, like Gecko)"
"Chrome/22 Mobile Safari/537.36" "Chrome/22 Mobile Safari/537.36"
) )
chrome_options = webdriver.ChromeOptions()
chrome_profile_dir = init_profile(config.UserCredentials.get_mail(), mobile=mobile)
# Full list on https://github.com/GoogleChrome/chrome-launcher/blob/main/docs/chrome-flags-for-tools.md
arguments = [
"--no-first-run",
"--ash-no-nudges",
"--no-default-browser-check",
"--disable-features=PrivacySandboxSettings4,Translate",
"--disable-search-engine-choice-screen",
f"--user-data-dir={chrome_profile_dir}/"
]
if mobile: if mobile:
chrome_options.add_argument(f"--user-agent={mobile_user_agent}") arguments.append(f"--user-agent={mobile_user_agent}")
else: else:
chrome_options.add_argument(f"--user-agent={pc_user_agent}") arguments.append(f"--user-agent={pc_user_agent}")
# disabled as it may cause detection # disabled as it may cause detection
if config.proxy.is_enabled(): if config.proxy.is_enabled():
chrome_options.add_argument(f'--proxy-server={config.proxy.ip}:{config.proxy.port}') arguments.append(f'--proxy-server={config.proxy.ip}:{config.proxy.port}')
chrome_options = webdriver.ChromeOptions()
for arg in arguments:
chrome_options.add_argument(arg)
driver = uc.Chrome(options=chrome_options) driver = uc.Chrome(options=chrome_options)
set_language(driver)
return driver return driver
@ -152,7 +171,7 @@ def play_quiz4(override: int = None):
except Exception as e: except Exception as e:
log_error(e) log_error(e)
raise ValueError(e) raise ValueError(e)
info("Quiz 8 done.") info("Quiz 4 done.")
custom_sleep(3) custom_sleep(3)
@ -165,6 +184,8 @@ def do_poll():
try: try:
answer_elem.click() answer_elem.click()
except exceptions.ElementNotInteractableException: except exceptions.ElementNotInteractableException:
warning("element not clickable. Waiting a bit and retrying.")
custom_sleep(uniform(2, 2.5))
driver.execute_script("arguments[0].click();", answer_elem) driver.execute_script("arguments[0].click();", answer_elem)
custom_sleep(uniform(2, 2.5)) custom_sleep(uniform(2, 2.5))
except Exception as err: except Exception as err:
@ -177,6 +198,27 @@ def do_poll():
# Find each playable card and tries to click on it to earn points # Find each playable card and tries to click on it to earn points
def all_cards(): def all_cards():
driver = config.WebDriver.driver driver = config.WebDriver.driver
def check_welcome_tour() -> bool:
if "rewards.bing.com/welcometour" not in driver.current_url:
return False
info("Popup 'Explorer le programme' reçue")
wait_until_visible(By.ID, "welcome-tour", timeout=5, browser=driver)
custom_sleep(1.5)
welcome_tour = driver.find_element(By.ID, "welcome-tour")
interest_button_box = welcome_tour.find_element(By.CLASS_NAME, "interest-buttons")
interests = interest_button_box.find_elements(By.CLASS_NAME, "ng-binding")
debug("Got the following interests: "+str(interests))
random.choice(interests).click() # Choose interest
custom_sleep(1.5)
claim_button = welcome_tour.find_element(By.ID, "claim-button")
claim_button.click() # submit
custom_sleep(1.5)
return True
driver.get("https://rewards.bing.com") driver.get("https://rewards.bing.com")
wait_until_visible(By.CLASS_NAME, "c-card-content", 10, driver) wait_until_visible(By.CLASS_NAME, "c-card-content", 10, driver)
@ -190,7 +232,7 @@ def all_cards():
info("no promo card") info("no promo card")
if len(card_list) < 10: # most likely an error during loading if len(card_list) < 10: # most likely an error during loading
if "suspendu" in driver.page_source: if "suspendu" in driver.page_source or "suspended" in driver.page_source:
raise Banned() raise Banned()
driver.refresh() driver.refresh()
card_list = driver.find_elements(By.CLASS_NAME, "c-card-content") card_list = driver.find_elements(By.CLASS_NAME, "c-card-content")
@ -220,8 +262,11 @@ def all_cards():
if i == len(card_list) and i > 15: if i == len(card_list) and i > 15:
checked = False checked = False
if checked: if not checked:
continue
custom_sleep(1.5) custom_sleep(1.5)
check_welcome_tour()
driver.execute_script("arguments[0].scrollIntoView();", card_list[i]) driver.execute_script("arguments[0].scrollIntoView();", card_list[i])
custom_sleep(1.5) custom_sleep(1.5)
card_list[i].click() card_list[i].click()
@ -235,9 +280,13 @@ def all_cards():
driver.refresh() driver.refresh()
card_list = driver.find_elements(By.CLASS_NAME, "c-card-content") card_list = driver.find_elements(By.CLASS_NAME, "c-card-content")
if "mee-icon-AddMedium" in card_list[i].get_attribute("innerHTML"): if "mee-icon-AddMedium" not in card_list[i].get_attribute("innerHTML"):
continue
check_welcome_tour()
driver.execute_script("arguments[0].scrollIntoView();", card_list[i]) driver.execute_script("arguments[0].scrollIntoView();", card_list[i])
card_list[i].click() card_list[i].click()
driver.switch_to.window(driver.window_handles[1]) driver.switch_to.window(driver.window_handles[1])
custom_sleep(10) custom_sleep(10)
log_error(f"Card {i} Can't be completed. Why MS ?") log_error(f"Card {i} Can't be completed. Why MS ?")
@ -250,6 +299,7 @@ def all_cards():
except Exception as e: except Exception as e:
debug(e) debug(e)
driver.get("https://rewards.bing.com") driver.get("https://rewards.bing.com")
except Exception as err: except Exception as err:
log_error(err) log_error(err)
custom_sleep(3) custom_sleep(3)
@ -360,20 +410,33 @@ def login_part_1():
driver = config.WebDriver.driver driver = config.WebDriver.driver
driver.get("https://login.live.com") driver.get("https://login.live.com")
wait_until_visible(By.ID, "i0116", browser=driver) wait_until_visible(By.ID, "i0116", browser=driver)
mail_elem = driver.find_element(By.ID, "i0116") send_wait_and_confirm(
send_keys_wait(mail_elem, config.UserCredentials.get_mail()) driver.find_element(By.ID, "i0116"),
mail_elem.send_keys(Keys.ENTER) config.UserCredentials.get_mail()
)
wait_until_visible(By.ID, "i0118", browser=driver) wait_until_visible(By.ID, "i0118", browser=driver)
pwd_elem = driver.find_element(By.ID, "i0118") send_wait_and_confirm(
send_keys_wait(pwd_elem, config.UserCredentials.get_password()) driver.find_element(By.ID, "i0118"),
pwd_elem.send_keys(Keys.ENTER) config.UserCredentials.get_password()
custom_sleep(2) )
# 2FA # 2FA
if "Entrez le code de sécurité" in driver.page_source:
try: try:
a2f_elem = driver.find_element(By.ID, "idTxtBx_SAOTCC_OTC") if not wait_until_visible(By.ID, "idTxtBx_SAOTCC_OTC", browser=driver, timeout=5, raise_error=False):
a2f_elem.send_keys(config.UserCredentials.get_tfa().now()) custom_sleep(2)
a2f_elem.send_keys(Keys.ENTER) return
tfa = config.UserCredentials.get_tfa()
if tfa is None:
error("2FA needed but no code available for this account, sending error")
raise ValueError("2FA needed but no code available for this account")
else:
a2f_code = tfa.now()
info(f"Need 2FA, I have code: {a2f_code}")
send_wait_and_confirm(
driver.find_element(By.ID, "idTxtBx_SAOTCC_OTC"),
a2f_code
)
except Exception as err: except Exception as err:
log_error(err) log_error(err)
@ -410,8 +473,16 @@ def login_part_2():
# login() tries to login to your Microsoft account. # login() tries to login to your Microsoft account.
# it uses global variable g._mail and g._password to login # it uses global variable g._mail and g._password to login
def login(): def login():
def logged_in():
driver.get("https://login.live.com")
custom_sleep(10)
if get_domain(driver) == "account.microsoft.com":
return True
return False
driver = config.WebDriver.driver driver = config.WebDriver.driver
try: try:
if not logged_in():
login_part_1() login_part_1()
login_part_2() login_part_2()
driver.get("https://rewards.bing.com/") driver.get("https://rewards.bing.com/")
@ -610,9 +681,11 @@ def daily_routine(cred: UserCredentials, custom=False):
try: try:
if not custom: # custom already is logged in if not custom: # custom already is logged in
login() login()
except Banned: except Banned:
log_error("This account is locked.") log_error("This account is locked.")
return raise Banned()
except Identity: except Identity:
log_error("This account has an issue.") log_error("This account has an issue.")
return return
@ -666,7 +739,7 @@ def json_start(json_entry, cred: UserCredentials):
config.WebDriver.switch_to_driver("PC") config.WebDriver.switch_to_driver("PC")
driver = config.WebDriver.driver driver = config.WebDriver.driver
try: try:
if str(account_id) in json_entry["unban"]: if "unban" in json_entry and str(account_id) in json_entry["unban"]:
login_part_1() login_part_1()
info("\nGO TO example.com TO PROCEED or wait 1200 secs.") info("\nGO TO example.com TO PROCEED or wait 1200 secs.")
for _ in range(1200): for _ in range(1200):
@ -680,7 +753,7 @@ def json_start(json_entry, cred: UserCredentials):
login() login()
try: try:
if str(account_id) in json_entry["tout"]: if str(account_id) in json_entry["tout"]:
daily_routine(cred) daily_routine(cred, True)
except KeyError: except KeyError:
pass pass
else: else:
@ -744,12 +817,16 @@ def default_start():
config.WebDriver.pc_driver.quit() config.WebDriver.pc_driver.quit()
config.display.stop() config.display.stop()
break break
except Banned:
warning("this account is banned. Switching to next account")
except Exception as e: except Exception as e:
log_error(f"Error not caught. Skipping this account. " + format_error(e)) log_error(f"Error not caught. Skipping this account. " + format_error(e))
critical(f"Error not caught. Skipping this account. {e}") critical(f"Error not caught. Skipping this account. {e}")
config.WebDriver.pc_driver.quit() config.WebDriver.pc_driver.quit()
finally:
config.UserCredentials.next_account() config.UserCredentials.next_account()
config.display.stop() config.display.stop()

View File

@ -1 +1,12 @@
sudo docker build --no-cache --network host -t msrewards . && sudo docker run -d --restart unless-stopped -p 1234:1234 -p 2345:2345 -ti --shm-size=2gb --name MsRewards msrewards #!/bin/bash
docker-do () { # Check if sudo needs to be used
if id -nG "$(whoami)" | grep -qw "docker"; then
docker $@
else
sudo docker $@
fi
}
docker-do build --network host -t msrewards .
docker-do run -d --restart unless-stopped -p 1234:1234 -p 2345:2345 -ti --shm-size=2gb --name MsRewards msrewards

13
clean.sh Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
docker-do () { # Check if sudo needs to be used
if id -nG "$(whoami)" | grep -qw "docker"; then
docker $@
else
sudo docker $@
fi
}
docker-do stop MsRewards
docker-do rm MsRewards
docker-do image rm msrewards

View File

@ -46,6 +46,7 @@ server {
proxy_pass "http://127.0.0.1:6666"; proxy_pass "http://127.0.0.1:6666";
chunked_transfer_encoding off; chunked_transfer_encoding off;
proxy_buffering off; proxy_buffering off;
add_header X-Accel-Buffering no;
} }
} }
@ -59,7 +60,6 @@ sqlite3 /app/MsRewards-Reborn/MsRewards.db "CREATE TABLE comptes (id INTEGER PRI
printf "\nconfigurating grafana\n" printf "\nconfigurating grafana\n"
cp /app/MsRewards-Reborn/config/grafana.ini /etc/grafana/ cp /app/MsRewards-Reborn/config/grafana.ini /etc/grafana/
grafana-cli plugins install frser-sqlite-datasource
printf "setting up default dashboard" printf "setting up default dashboard"
cp /app/MsRewards-Reborn/config/Stats-dashbord.json /usr/share/grafana/public/dashboards/home.json cp /app/MsRewards-Reborn/config/Stats-dashbord.json /usr/share/grafana/public/dashboards/home.json

File diff suppressed because it is too large Load Diff

View File

@ -50,9 +50,14 @@ class Config:
""" """
self.discord = DiscordConfig() self.discord = DiscordConfig()
self.discord.avatar_url = settings["avatarlink"] self.discord.avatar_url = settings["avatarlink"]
self.discord.wh_link = discord[config[args.config]["discord"]]["errorsL"]
if self.discord.wh_link != "": if (
"discord" in config[args.config]
and config[args.config]["discord"] in discord
and "errorsL" in discord[config[args.config]["discord"]]
and discord[config[args.config]["discord"]]["errorsL"] != ""
):
self.discord.wh_link = discord[config[args.config]["discord"]]["errorsL"]
self.discord.wh = Webhook.from_url(self.discord.wh_link, adapter=RequestsWebhookAdapter()) self.discord.wh = Webhook.from_url(self.discord.wh_link, adapter=RequestsWebhookAdapter())
else: else:
self.discord.wh = FakeWebHook() self.discord.wh = FakeWebHook()

View File

@ -9,6 +9,6 @@ class DiscordConfig:
class FakeWebHook: class FakeWebHook:
def send(self, *args): def send(self, *args, **kwargs):
debug(f"Used a webhook call without webhook url with {args}") debug(f"Used a webhook call without webhook url with {args} {kwargs}")

View File

@ -30,7 +30,7 @@ class DiscordLogger:
) )
file = File("screenshot.png") file = File("screenshot.png")
embed.set_image(url="attachment://screenshot.png") embed.set_image(url="attachment://screenshot.png")
embed.set_footer(text=self.config.UserCredentials.creds.get_mail()) embed.set_footer(text=self.config.UserCredentials.get_mail() + " - " + self.config.WebDriver.current_driver())
self.config.discord.wh.send(embed=embed, username="error", file=file, avatar_url=self.config.discord.avatar_url) self.config.discord.wh.send(embed=embed, username="error", file=file, avatar_url=self.config.discord.avatar_url)
self.config.discord.wh.send(username="error", file=File("page.html"), avatar_url=self.config.discord.avatar_url) self.config.discord.wh.send(username="error", file=File("page.html"), avatar_url=self.config.discord.avatar_url)

View File

@ -11,12 +11,15 @@ class Driver:
self.mobile_driver = mobile_driver self.mobile_driver = mobile_driver
def switch_to_driver(self, driver: str): def switch_to_driver(self, driver: str):
match driver: match driver.lower():
case "pc" | "PC" | "Pc": case "pc":
self.driver = self.pc_driver self.driver = self.pc_driver
case "mobile" | "Mobile": case "mobile":
self.driver = self.mobile_driver self.driver = self.mobile_driver
case _: case _:
raise ValueError("The driver must be either pc or mobile") raise ValueError("The driver must be either pc or mobile")
def current_driver(self):
return "PC" if self.pc_driver == self.driver else "Mobile"

View File

@ -30,8 +30,9 @@ class UserCredentials:
def get_tfa(self): def get_tfa(self):
if not self.tfa_enable(): if not self.tfa_enable():
warning("Warning: TFA is not enabled. Calling get_tfa is an expected behaviour.") warning("Warning: TFA is not enabled. Can't get a TFA code.")
return TOTP(self.data[self.current]["tfa"]) return None
return TOTP(self.data[self.current]["2fa"])
def next_account(self): def next_account(self):
self.current += 1 self.current += 1
@ -41,4 +42,5 @@ class UserCredentials:
debug("No new credentials.") debug("No new credentials.")
def is_valid(self): def is_valid(self):
return self.current < self.total return (self.current < self.total
and self.get_mail() != "" and self.get_mail is not None)

View File

@ -0,0 +1,8 @@
import undetected_chromedriver as uc
from pyvirtualdisplay.smartdisplay import SmartDisplay
display = SmartDisplay(size=(1920, 1080))
display.start()
driver = uc.Chrome()
driver.close()
driver.close()

View File

@ -30,7 +30,7 @@ class ColoredFormatter(logging.Formatter):
# Set up the root logger # Set up the root logger
root_logger = logging.getLogger(__name__) root_logger = logging.getLogger(__name__)
root_logger.setLevel(logging.DEBUG) root_logger.setLevel(logging.INFO)
# Create a console handler and set the formatter # Create a console handler and set the formatter
ch = logging.StreamHandler() ch = logging.StreamHandler()

View File

@ -10,7 +10,7 @@ def get_domain(driver):
def custom_sleep(temps): def custom_sleep(temps):
try: try:
if True: # todo: change this awful condition if False: # todo: change this awful condition
points = ["", "", "", "", "", "", "", ""] points = ["", "", "", "", "", "", "", ""]
passe = 0 passe = 0
for _ in range(int(temps)): for _ in range(int(temps)):

View File

@ -0,0 +1,48 @@
import requests
import re
from packaging import version
import subprocess
from logger import critical, info, error
errorMessage = subprocess.run(['python3', 'generate_error.py'], check=False, stdout=subprocess.PIPE,
stderr=subprocess.PIPE).stderr.decode("utf-8")
versionPattern = "This version of ChromeDriver only supports Chrome version ([0-9]+)"
try:
versionN = re.search(versionPattern, errorMessage)[1]
except Exception as e:
critical("Can't get version number from error")
error(e)
exit(0)
info(f"Needed version : '{versionN}'")
downloadUrl = "http://mirror.cs.uchicago.edu/google-chrome/pool/main/g/google-chrome-stable/"
r = requests.get(downloadUrl)
content = r.text
exactVersionList = re.findall(f"(google-chrome-stable_({versionN}.[0-9.]+)[^<^>^\"]+)", content)
try:
best = exactVersionList[0]
except Exception as e:
critical("No version matches required version")
error(e)
exit(0)
for i in exactVersionList:
if version.parse(i[1]) > version.parse(best[1]):
best = i
chromeDebURL = f"http://mirror.cs.uchicago.edu/google-chrome/pool/main/g/google-chrome-stable/{best[0]}"
info(f"chrome deb URL : {chromeDebURL}")
info("downloading chrome")
subprocess.call(['wget', "-O", "/tmp/chrome.deb", chromeDebURL])
info("Chrome deb downloaded. Installing chrome")
subprocess.call(["dpkg", "-i", "/tmp/chrome.deb"])
info("Chrome installed")

View File

@ -1,3 +1,5 @@
import os
import json
from random import uniform from random import uniform
from selenium.common import TimeoutException from selenium.common import TimeoutException
@ -10,33 +12,40 @@ from modules.Tools.logger import debug
from modules.Tools.tools import * from modules.Tools.tools import *
def set_language(ldriver): def init_profile(mail, mobile=False):
ldriver.get("chrome://settings/languages") if not mobile:
action = ActionChains(ldriver) chrome_profile_dir = "/app/MsRewards-Reborn/user_data/profile/"+mail
action.reset_actions() else:
# select language chrome_profile_dir = "/app/MsRewards-Reborn/user_data/profile/mobile-"+mail
x_coord = 1200
y_coord = 150 os.makedirs(chrome_profile_dir, exist_ok=True)
action.move_by_offset(x_coord, y_coord).click().perform()
sleep(0.5) preferences_file = os.path.join(chrome_profile_dir, "Default", "Preferences")
# scroll down if not os.path.exists(preferences_file):
action.reset_actions() os.makedirs(os.path.join(chrome_profile_dir, "Default"), exist_ok=True)
elm = ldriver.find_element(By.XPATH, "/html/body") with open(preferences_file, "w") as f:
ActionChains(ldriver) \ json.dump(
.send_keys("french") \ {
.pause(0.5) \ "intl": {
.send_keys(Keys.TAB + Keys.TAB + Keys.ENTER + Keys.TAB + Keys.TAB + Keys.ENTER) \ "accept_languages": "fr-FR,en-US,en",
.perform() "selected_languages": "fr-FR,en-US,en"
x_coord = 1163 }
y_coord = 717 }, f
action.move_by_offset(x_coord, y_coord).click().perform() )
# put to the top else:
sleep(0.5) with open(preferences_file, "r") as f:
action.reset_actions() settings = json.load(f)
x_coord = 1257
y_coord = 328 if "intl" not in settings:
action.move_by_offset(x_coord, y_coord).click().perform() settings["intl"] = {}
action.click().perform()
settings["intl"]["accept_languages"] = "fr-FR,en-US,en"
settings["intl"]["selected_languages"] = "fr-FR,en-US,en"
with open(preferences_file, "w") as f:
json.dump(settings, f)
return chrome_profile_dir
# Deal with RGPD popup as well as some random popup like 'are you satisfied' one # Deal with RGPD popup as well as some random popup like 'are you satisfied' one
@ -60,13 +69,18 @@ def send_keys_wait(element, keys: str) -> None:
element.send_keys(i) element.send_keys(i)
sleep(uniform(0.1, 0.3)) sleep(uniform(0.1, 0.3))
def send_wait_and_confirm(element, keys: str) -> None:
send_keys_wait(element, keys)
element.send_keys(Keys.ENTER)
# Wait for the presence of the element identifier or [timeout]s # Wait for the presence of the element identifier or [timeout]s
def wait_until_visible(search_by: str, identifier: str, timeout: int = 20, browser=None) -> bool: def wait_until_visible(search_by: str, identifier: str, timeout: int = 20, browser=None, raise_error=True) -> bool:
try: try:
WebDriverWait(browser, timeout).until( WebDriverWait(browser, timeout).until(
expected_conditions.visibility_of_element_located((search_by, identifier)), "element not found") expected_conditions.visibility_of_element_located((search_by, identifier)), "element not found")
return True return True
except TimeoutException as e: except TimeoutException as e:
if raise_error:
error(f"element {identifier} not found after {timeout}s") error(f"element {identifier} not found after {timeout}s")
return False return False

View File

@ -1 +1 @@
v6.8.38 v6.8.48