Skip to content

Commit

Permalink
Merge branch 'master' into python312
Browse files Browse the repository at this point in the history
  • Loading branch information
dgtlmoon committed May 21, 2024
2 parents bd84f6c + 59cefe5 commit e721872
Show file tree
Hide file tree
Showing 23 changed files with 325 additions and 164 deletions.
1 change: 1 addition & 0 deletions changedetectionio/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ def hide_referrer(response):
# proxy_set_header Host "localhost";
# proxy_set_header X-Forwarded-Prefix /app;


if os.getenv('USE_X_SETTINGS'):
logger.info("USE_X_SETTINGS is ENABLED")
from werkzeug.middleware.proxy_fix import ProxyFix
Expand Down
31 changes: 27 additions & 4 deletions changedetectionio/content_fetchers/puppeteer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
from changedetectionio.content_fetchers.base import Fetcher, manage_user_agent
from changedetectionio.content_fetchers.exceptions import PageUnloadable, Non200ErrorCodeReceived, EmptyReply, BrowserFetchTimedOut, BrowserConnectError


class fetcher(Fetcher):
fetcher_description = "Puppeteer/direct {}/Javascript".format(
os.getenv("PLAYWRIGHT_BROWSER_TYPE", 'chromium').capitalize()
Expand Down Expand Up @@ -93,15 +92,39 @@ async def fetch_page(self,
ignoreHTTPSErrors=True
)
except websockets.exceptions.InvalidStatusCode as e:
raise BrowserConnectError(msg=f"Error while trying to connect the browser, Code {e.status_code} (check your access)")
raise BrowserConnectError(msg=f"Error while trying to connect the browser, Code {e.status_code} (check your access, whitelist IP, password etc)")
except websockets.exceptions.InvalidURI:
raise BrowserConnectError(msg=f"Error connecting to the browser, check your browser connection address (should be ws:// or wss://")
except Exception as e:
raise BrowserConnectError(msg=f"Error connecting to the browser {str(e)}")

# Better is to launch chrome with the URL as arg
# non-headless - newPage() will launch an extra tab/window, .browser should already contain 1 page/tab
# headless - ask a new page
self.page = (pages := await browser.pages) and len(pages) or await browser.newPage()

try:
from pyppeteerstealth import inject_evasions_into_page
except ImportError:
logger.debug("pyppeteerstealth module not available, skipping")
pass
else:
self.page = await browser.newPage()
# I tried hooking events via self.page.on(Events.Page.DOMContentLoaded, inject_evasions_requiring_obj_to_page)
# But I could never get it to fire reliably, so we just inject it straight after
await inject_evasions_into_page(self.page)

await self.page.setUserAgent(manage_user_agent(headers=request_headers, current_ua=await self.page.evaluate('navigator.userAgent')))
# This user agent is similar to what was used when tweaking the evasions in inject_evasions_into_page(..)
user_agent = None
if request_headers:
user_agent = next((value for key, value in request_headers.items() if key.lower().strip() == 'user-agent'), None)
if user_agent:
await self.page.setUserAgent(user_agent)
# Remove it so it's not sent again with headers after
[request_headers.pop(key) for key in list(request_headers) if key.lower().strip() == 'user-agent'.lower().strip()]

if not user_agent:
# Attempt to strip 'HeadlessChrome' etc
await self.page.setUserAgent(manage_user_agent(headers=request_headers, current_ua=await self.page.evaluate('navigator.userAgent')))

await self.page.setBypassCSP(True)
if request_headers:
Expand Down
5 changes: 0 additions & 5 deletions changedetectionio/content_fetchers/requests.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,6 @@ def run(self,
if self.browser_steps_get_valid_steps():
raise BrowserStepsInUnsupportedFetcher(url=url)

# Make requests use a more modern looking user-agent
if not {k.lower(): v for k, v in request_headers.items()}.get('user-agent', None):
request_headers['User-Agent'] = os.getenv("DEFAULT_SETTINGS_HEADERS_USERAGENT",
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36')

proxies = {}

# Allows override the proxy on a per-request basis
Expand Down
94 changes: 49 additions & 45 deletions changedetectionio/flask_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -338,8 +338,11 @@ def rss():

# @todo needs a .itemsWithTag() or something - then we can use that in Jinaj2 and throw this away
for uuid, watch in datastore.data['watching'].items():
# @todo tag notification_muted skip also (improve Watch model)
if watch.get('notification_muted'):
continue
if limit_tag and not limit_tag in watch['tags']:
continue
continue
watch['uuid'] = uuid
sorted_watches.append(watch)

Expand Down Expand Up @@ -768,7 +771,7 @@ def edit_page(uuid):
jq_support=jq_support,
playwright_enabled=os.getenv('PLAYWRIGHT_DRIVER_URL', False),
settings_application=datastore.data['settings']['application'],
using_global_webdriver_wait=default['webdriver_delay'] is None,
using_global_webdriver_wait=not default['webdriver_delay'],
uuid=uuid,
visualselector_enabled=visualselector_enabled,
watch=watch
Expand Down Expand Up @@ -1063,6 +1066,8 @@ def preview_page(uuid):
content = []
ignored_line_numbers = []
trigger_line_numbers = []
versions = []
timestamp = None

# More for testing, possible to return the first/only
if uuid == 'first':
Expand All @@ -1082,57 +1087,53 @@ def preview_page(uuid):
if (watch.get('fetch_backend') == 'system' and system_uses_webdriver) or watch.get('fetch_backend') == 'html_webdriver' or watch.get('fetch_backend', '').startswith('extra_browser_'):
is_html_webdriver = True

# Never requested successfully, but we detected a fetch error
if datastore.data['watching'][uuid].history_n == 0 and (watch.get_error_text() or watch.get_error_snapshot()):
flash("Preview unavailable - No fetch/check completed or triggers not reached", "error")
output = render_template("preview.html",
content=content,
history_n=watch.history_n,
extra_stylesheets=extra_stylesheets,
# current_diff_url=watch['url'],
watch=watch,
uuid=uuid,
is_html_webdriver=is_html_webdriver,
last_error=watch['last_error'],
last_error_text=watch.get_error_text(),
last_error_screenshot=watch.get_error_snapshot())
return output
else:
# So prepare the latest preview or not
preferred_version = request.args.get('version')
versions = list(watch.history.keys())
timestamp = versions[-1]
if preferred_version and preferred_version in versions:
timestamp = preferred_version

timestamp = list(watch.history.keys())[-1]
try:
tmp = watch.get_history_snapshot(timestamp).splitlines()

# Get what needs to be highlighted
ignore_rules = watch.get('ignore_text', []) + datastore.data['settings']['application']['global_ignore_text']

# .readlines will keep the \n, but we will parse it here again, in the future tidy this up
ignored_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
wordlist=ignore_rules,
mode='line numbers'
)

trigger_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
wordlist=watch['trigger_text'],
mode='line numbers'
)
# Prepare the classes and lines used in the template
i=0
for l in tmp:
classes=[]
i+=1
if i in ignored_line_numbers:
classes.append('ignored')
if i in trigger_line_numbers:
classes.append('triggered')
content.append({'line': l, 'classes': ' '.join(classes)})
try:
versions = list(watch.history.keys())
tmp = watch.get_history_snapshot(timestamp).splitlines()

# Get what needs to be highlighted
ignore_rules = watch.get('ignore_text', []) + datastore.data['settings']['application']['global_ignore_text']

# .readlines will keep the \n, but we will parse it here again, in the future tidy this up
ignored_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
wordlist=ignore_rules,
mode='line numbers'
)

trigger_line_numbers = html_tools.strip_ignore_text(content="\n".join(tmp),
wordlist=watch['trigger_text'],
mode='line numbers'
)
# Prepare the classes and lines used in the template
i=0
for l in tmp:
classes=[]
i+=1
if i in ignored_line_numbers:
classes.append('ignored')
if i in trigger_line_numbers:
classes.append('triggered')
content.append({'line': l, 'classes': ' '.join(classes)})

except Exception as e:
content.append({'line': f"File doesnt exist or unable to read timestamp {timestamp}", 'classes': ''})
except Exception as e:
content.append({'line': f"File doesnt exist or unable to read timestamp {timestamp}", 'classes': ''})

output = render_template("preview.html",
content=content,
current_version=timestamp,
history_n=watch.history_n,
extra_stylesheets=extra_stylesheets,
extra_title=f" - Diff - {watch.label} @ {timestamp}",
ignored_line_numbers=ignored_line_numbers,
triggered_line_numbers=trigger_line_numbers,
current_diff_url=watch['url'],
Expand All @@ -1142,7 +1143,10 @@ def preview_page(uuid):
is_html_webdriver=is_html_webdriver,
last_error=watch['last_error'],
last_error_text=watch.get_error_text(),
last_error_screenshot=watch.get_error_snapshot())
last_error_screenshot=watch.get_error_snapshot(),
versions=versions
)


return output

Expand Down
6 changes: 6 additions & 0 deletions changedetectionio/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,10 @@ class SingleExtraBrowser(Form):
browser_connection_url = StringField('Browser connection URL', [validators.Optional()], render_kw={"placeholder": "wss://brightdata... wss://oxylabs etc", "size":50})
# @todo do the validation here instead

class DefaultUAInputForm(Form):
html_requests = StringField('Plaintext requests', validators=[validators.Optional()], render_kw={"placeholder": "<default>"})
if os.getenv("PLAYWRIGHT_DRIVER_URL") or os.getenv("WEBDRIVER_URL"):
html_webdriver = StringField('Chrome requests', validators=[validators.Optional()], render_kw={"placeholder": "<default>"})

# datastore.data['settings']['requests']..
class globalSettingsRequestForm(Form):
Expand All @@ -537,6 +541,8 @@ class globalSettingsRequestForm(Form):
extra_proxies = FieldList(FormField(SingleExtraProxy), min_entries=5)
extra_browsers = FieldList(FormField(SingleExtraBrowser), min_entries=5)

default_ua = FormField(DefaultUAInputForm, label="Default User-Agent overrides")

def validate_extra_proxies(self, extra_validators=None):
for e in self.data['extra_proxies']:
if e.get('proxy_name') or e.get('proxy_url'):
Expand Down
5 changes: 5 additions & 0 deletions changedetectionio/model/App.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
)

_FILTER_FAILURE_THRESHOLD_ATTEMPTS_DEFAULT = 6
DEFAULT_SETTINGS_HEADERS_USERAGENT='Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.66 Safari/537.36'

class model(dict):
base_config = {
Expand All @@ -22,6 +23,10 @@ class model(dict):
'time_between_check': {'weeks': None, 'days': None, 'hours': 3, 'minutes': None, 'seconds': None},
'timeout': int(getenv("DEFAULT_SETTINGS_REQUESTS_TIMEOUT", "45")), # Default 45 seconds
'workers': int(getenv("DEFAULT_SETTINGS_REQUESTS_WORKERS", "10")), # Number of threads, lower is better for slow connections
'default_ua': {
'html_requests': getenv("DEFAULT_SETTINGS_HEADERS_USERAGENT", DEFAULT_SETTINGS_HEADERS_USERAGENT),
'html_webdriver': None,
}
},
'application': {
# Custom notification content
Expand Down
9 changes: 5 additions & 4 deletions changedetectionio/notification.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,10 +122,6 @@ def process_notification(n_object, datastore):
# Insert variables into the notification content
notification_parameters = create_notification_parameters(n_object, datastore)

# Get the notification body from datastore
n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)

n_format = valid_notification_formats.get(
n_object.get('notification_format', default_notification_format),
valid_notification_formats[default_notification_format],
Expand All @@ -151,6 +147,11 @@ def process_notification(n_object, datastore):

with apprise.LogCapture(level=apprise.logging.DEBUG) as logs:
for url in n_object['notification_urls']:

# Get the notification body from datastore
n_body = jinja_render(template_str=n_object.get('notification_body', ''), **notification_parameters)
n_title = jinja_render(template_str=n_object.get('notification_title', ''), **notification_parameters)

url = url.strip()
if not url:
logger.warning(f"Process Notification: skipping empty notification URL.")
Expand Down
4 changes: 4 additions & 0 deletions changedetectionio/processors/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ def call_browser(self):
request_headers.update(self.datastore.get_all_base_headers())
request_headers.update(self.datastore.get_all_headers_in_textfile_for_watch(uuid=self.watch.get('uuid')))

ua = self.datastore.data['settings']['requests'].get('default_ua')
if ua and ua.get(prefer_fetch_backend):
request_headers.update({'User-Agent': ua.get(prefer_fetch_backend)})

# https://github.com/psf/requests/issues/4525
# Requests doesnt yet support brotli encoding, so don't put 'br' here, be totally sure that the user cannot
# do this by accident.
Expand Down
Binary file removed changedetectionio/static/images/gradient-border.png
Binary file not shown.
7 changes: 7 additions & 0 deletions changedetectionio/static/js/diff-overview.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ $(document).ready(function () {
}
})

$('.needs-localtime').each(function () {
for (var option of this.options) {
var dateObject = new Date(option.value * 1000);
option.label = dateObject.toLocaleString(undefined, {dateStyle: "full", timeStyle: "medium"});
}
});

// Load it when the #screenshot tab is in use, so we dont give a slow experience when waiting for the text diff to load
window.addEventListener('hashchange', function (e) {
toggle(location.hash);
Expand Down
7 changes: 1 addition & 6 deletions changedetectionio/static/js/diff-render.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,7 @@ $(document).ready(function () {
$('#jump-next-diff').click();
}

$('.needs-localtime').each(function () {
for (var option of this.options) {
var dateObject = new Date(option.value * 1000);
option.label = dateObject.toLocaleString(undefined, {dateStyle: "full", timeStyle: "medium"});
}
})

onDiffTypeChange(
document.querySelector('#settings [name="diff_type"]:checked'),
);
Expand Down
49 changes: 49 additions & 0 deletions changedetectionio/static/js/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
function redirect_to_version(version) {
var currentUrl = window.location.href;
var baseUrl = currentUrl.split('?')[0]; // Base URL without query parameters
var anchor = '';

// Check if there is an anchor
if (baseUrl.indexOf('#') !== -1) {
anchor = baseUrl.substring(baseUrl.indexOf('#'));
baseUrl = baseUrl.substring(0, baseUrl.indexOf('#'));
}
window.location.href = baseUrl + '?version=' + version + anchor;
}

document.addEventListener('keydown', function (event) {
var selectElement = document.getElementById('preview-version');
if (selectElement) {
var selectedOption = selectElement.querySelector('option:checked');
if (selectedOption) {
if (event.key === 'ArrowLeft') {
if (selectedOption.previousElementSibling) {
redirect_to_version(selectedOption.previousElementSibling.value);
}
} else if (event.key === 'ArrowRight') {
if (selectedOption.nextElementSibling) {
redirect_to_version(selectedOption.nextElementSibling.value);
}
}
}
}
});


document.getElementById('preview-version').addEventListener('change', function () {
redirect_to_version(this.value);
});

var selectElement = document.getElementById('preview-version');
if (selectElement) {
var selectedOption = selectElement.querySelector('option:checked');
if (selectedOption) {
if (selectedOption.previousElementSibling) {
document.getElementById('btn-previous').href = "?version=" + selectedOption.previousElementSibling.value;
}
if (selectedOption.nextElementSibling) {
document.getElementById('btn-next').href = "?version=" + selectedOption.nextElementSibling.value;
}

}
}
4 changes: 3 additions & 1 deletion changedetectionio/static/styles/scss/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,6 @@ body::after {
body::before {
// background-image set in base.html so it works with reverse proxies etc
content: "";
background-size: cover
}

body:after,
Expand Down Expand Up @@ -1083,6 +1082,9 @@ ul {
li {
list-style: none;
font-size: 0.8rem;
> * {
display: inline-block;
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions changedetectionio/static/styles/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -574,8 +574,7 @@ body::after {
opacity: 0.91; }

body::before {
content: "";
background-size: cover; }
content: ""; }

body:after,
body:before {
Expand Down Expand Up @@ -1173,6 +1172,8 @@ ul {
#quick-watch-processor-type ul li {
list-style: none;
font-size: 0.8rem; }
#quick-watch-processor-type ul li > * {
display: inline-block; }

.restock-label {
padding: 3px;
Expand Down
Loading

0 comments on commit e721872

Please sign in to comment.