github-monitor 1.9rc1__py3-none-any.whl → 2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {github_monitor-1.9rc1.dist-info → github_monitor-2.3.dist-info}/METADATA +81 -12
- github_monitor-2.3.dist-info/RECORD +7 -0
- {github_monitor-1.9rc1.dist-info → github_monitor-2.3.dist-info}/WHEEL +1 -1
- github_monitor.py +785 -109
- github_monitor-1.9rc1.dist-info/RECORD +0 -7
- {github_monitor-1.9rc1.dist-info → github_monitor-2.3.dist-info}/entry_points.txt +0 -0
- {github_monitor-1.9rc1.dist-info → github_monitor-2.3.dist-info}/licenses/LICENSE +0 -0
- {github_monitor-1.9rc1.dist-info → github_monitor-2.3.dist-info}/top_level.txt +0 -0
github_monitor.py
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
3
|
Author: Michal Szymanski <misiektoja-github@rm-rf.ninja>
|
|
4
|
-
|
|
4
|
+
v2.3
|
|
5
5
|
|
|
6
|
-
OSINT tool implementing real-time tracking of
|
|
6
|
+
OSINT tool implementing real-time tracking of GitHub users activities including profile and repositories changes:
|
|
7
7
|
https://github.com/misiektoja/github_monitor/
|
|
8
8
|
|
|
9
9
|
Python pip3 requirements:
|
|
@@ -16,14 +16,14 @@ tzlocal (optional)
|
|
|
16
16
|
python-dotenv (optional)
|
|
17
17
|
"""
|
|
18
18
|
|
|
19
|
-
VERSION = "
|
|
19
|
+
VERSION = "2.3"
|
|
20
20
|
|
|
21
21
|
# ---------------------------
|
|
22
22
|
# CONFIGURATION SECTION START
|
|
23
23
|
# ---------------------------
|
|
24
24
|
|
|
25
25
|
CONFIG_BLOCK = """
|
|
26
|
-
# Get your
|
|
26
|
+
# Get your GitHub personal access token (classic) by visiting:
|
|
27
27
|
# https://github.com/settings/apps
|
|
28
28
|
#
|
|
29
29
|
# Then go to: Personal access tokens -> Tokens (classic) -> Generate new token (classic)
|
|
@@ -32,18 +32,24 @@ CONFIG_BLOCK = """
|
|
|
32
32
|
# - Pass it at runtime with -t / --github-token
|
|
33
33
|
# - Set it as an environment variable (e.g. export GITHUB_TOKEN=...)
|
|
34
34
|
# - Add it to ".env" file (GITHUB_TOKEN=...) for persistent use
|
|
35
|
-
# Fallback:
|
|
36
|
-
# - Hard-code it in the code or config file
|
|
35
|
+
# - Fallback: hard-code it in the code or config file
|
|
37
36
|
GITHUB_TOKEN = "your_github_classic_personal_access_token"
|
|
38
37
|
|
|
39
|
-
# The URL of the
|
|
38
|
+
# The URL of the GitHub API
|
|
40
39
|
#
|
|
41
|
-
# For Public Web
|
|
42
|
-
# For
|
|
40
|
+
# For Public Web GitHub use the default: https://api.github.com
|
|
41
|
+
# For GitHub Enterprise change to: https://{your_hostname}/api/v3
|
|
43
42
|
#
|
|
44
43
|
# Can also be set using the -x flag
|
|
45
44
|
GITHUB_API_URL = "https://api.github.com"
|
|
46
45
|
|
|
46
|
+
# The base URL of the GitHub web interface
|
|
47
|
+
# Required to check if the profile is public or private
|
|
48
|
+
#
|
|
49
|
+
# For public GitHub use the default: https://github.com
|
|
50
|
+
# For GitHub Enterprise change to: https://{your_hostname}
|
|
51
|
+
GITHUB_HTML_URL = "https://github.com"
|
|
52
|
+
|
|
47
53
|
# SMTP settings for sending email notifications
|
|
48
54
|
# If left as-is, no notifications will be sent
|
|
49
55
|
#
|
|
@@ -70,6 +76,7 @@ EVENT_NOTIFICATION = False
|
|
|
70
76
|
|
|
71
77
|
# Whether to send an email when user's repositories change (stargazers, watchers, forks, issues,
|
|
72
78
|
# PRs, description etc., except for update date)
|
|
79
|
+
# Requires TRACK_REPOS_CHANGES to be enabled
|
|
73
80
|
# Can also be enabled via the -q flag
|
|
74
81
|
REPO_NOTIFICATION = False
|
|
75
82
|
|
|
@@ -77,6 +84,11 @@ REPO_NOTIFICATION = False
|
|
|
77
84
|
# Can also be enabled via the -u flag
|
|
78
85
|
REPO_UPDATE_DATE_NOTIFICATION = False
|
|
79
86
|
|
|
87
|
+
# Whether to send an email when user's daily contributions count changes
|
|
88
|
+
# Requires TRACK_CONTRIB_CHANGES to be enabled
|
|
89
|
+
# Can also be enabled via the -y flag
|
|
90
|
+
CONTRIB_NOTIFICATION = False
|
|
91
|
+
|
|
80
92
|
# Whether to send an email on errors
|
|
81
93
|
# Can also be disabled via the -e flag
|
|
82
94
|
ERROR_NOTIFICATION = True
|
|
@@ -85,7 +97,7 @@ ERROR_NOTIFICATION = True
|
|
|
85
97
|
# Can also be set using the -c flag
|
|
86
98
|
GITHUB_CHECK_INTERVAL = 1800 # 30 mins
|
|
87
99
|
|
|
88
|
-
# Set your local time zone so that
|
|
100
|
+
# Set your local time zone so that GitHub API timestamps are converted accordingly (e.g. 'Europe/Warsaw')
|
|
89
101
|
# Use this command to list all time zones supported by pytz:
|
|
90
102
|
# python3 -c "import pytz; print('\\n'.join(pytz.all_timezones))"
|
|
91
103
|
# If set to 'Auto', the tool will try to detect your local time zone automatically (requires tzlocal)
|
|
@@ -120,6 +132,36 @@ EVENTS_TO_MONITOR = [
|
|
|
120
132
|
# any events older than the most recent EVENTS_NUMBER will be missed
|
|
121
133
|
EVENTS_NUMBER = 30 # 1 page
|
|
122
134
|
|
|
135
|
+
# If True, track user's repository changes (changed stargazers, watchers, forks, description, update date etc.)
|
|
136
|
+
# Can also be enabled using the -j flag
|
|
137
|
+
TRACK_REPOS_CHANGES = False
|
|
138
|
+
|
|
139
|
+
# Repositories to monitor when TRACK_REPOS_CHANGES is enabled
|
|
140
|
+
# Use 'ALL' to monitor all repositories (default behavior)
|
|
141
|
+
# Use 'user/repo_name' format to monitor specific repositories for specific users
|
|
142
|
+
# If the current user matches the user in the list, that repository will be monitored
|
|
143
|
+
# Example: ['user1/repo1', 'user2/repo2', 'user1/repo3']
|
|
144
|
+
# Can also be set using the --repos flag (comma-separated repo names only, without user prefix)
|
|
145
|
+
# Example: --repos "repo1,repo2,repo3"
|
|
146
|
+
# Note: When using a specific list (not 'ALL'), newly created repositories will NOT be
|
|
147
|
+
# automatically monitored - only repositories explicitly listed here will be monitored.
|
|
148
|
+
REPOS_TO_MONITOR = ['ALL']
|
|
149
|
+
|
|
150
|
+
# If True, disable event monitoring
|
|
151
|
+
# Can also be disabled using the -k flag
|
|
152
|
+
DO_NOT_MONITOR_GITHUB_EVENTS = False
|
|
153
|
+
|
|
154
|
+
# If True, fetch all user repos (owned, forks, collaborations); otherwise, fetch only owned repos
|
|
155
|
+
GET_ALL_REPOS = False
|
|
156
|
+
|
|
157
|
+
# Alert about blocked (403 - TOS violation and 451 - DMCA block) repos in the console output (in monitoring mode)
|
|
158
|
+
# In listing mode (-r), blocked repos are always shown
|
|
159
|
+
BLOCKED_REPOS = False
|
|
160
|
+
|
|
161
|
+
# If True, track and log user's daily contributions count changes
|
|
162
|
+
# Can also be enabled using the -m flag
|
|
163
|
+
TRACK_CONTRIB_CHANGES = False
|
|
164
|
+
|
|
123
165
|
# How often to print a "liveness check" message to the output; in seconds
|
|
124
166
|
# Set to 0 to disable
|
|
125
167
|
LIVENESS_CHECK_INTERVAL = 43200 # 12 hours
|
|
@@ -148,10 +190,10 @@ GITHUB_LOGFILE = "github_monitor"
|
|
|
148
190
|
# Can also be disabled via the -d flag
|
|
149
191
|
DISABLE_LOGGING = False
|
|
150
192
|
|
|
151
|
-
# Width of main horizontal line
|
|
193
|
+
# Width of main horizontal line
|
|
152
194
|
HORIZONTAL_LINE1 = 105
|
|
153
195
|
|
|
154
|
-
# Width of horizontal line for repositories list output
|
|
196
|
+
# Width of horizontal line for repositories list output
|
|
155
197
|
HORIZONTAL_LINE2 = 80
|
|
156
198
|
|
|
157
199
|
# Whether to clear the terminal screen after starting the tool
|
|
@@ -175,6 +217,7 @@ GITHUB_CHECK_SIGNAL_VALUE = 60 # 1 minute
|
|
|
175
217
|
# Do not change values below - modify them in the configuration section or config file instead
|
|
176
218
|
GITHUB_TOKEN = ""
|
|
177
219
|
GITHUB_API_URL = ""
|
|
220
|
+
GITHUB_HTML_URL = ""
|
|
178
221
|
SMTP_HOST = ""
|
|
179
222
|
SMTP_PORT = 0
|
|
180
223
|
SMTP_USER = ""
|
|
@@ -186,11 +229,18 @@ PROFILE_NOTIFICATION = False
|
|
|
186
229
|
EVENT_NOTIFICATION = False
|
|
187
230
|
REPO_NOTIFICATION = False
|
|
188
231
|
REPO_UPDATE_DATE_NOTIFICATION = False
|
|
232
|
+
CONTRIB_NOTIFICATION = False
|
|
189
233
|
ERROR_NOTIFICATION = False
|
|
190
234
|
GITHUB_CHECK_INTERVAL = 0
|
|
191
235
|
LOCAL_TIMEZONE = ""
|
|
192
236
|
EVENTS_TO_MONITOR = []
|
|
193
237
|
EVENTS_NUMBER = 0
|
|
238
|
+
TRACK_REPOS_CHANGES = False
|
|
239
|
+
REPOS_TO_MONITOR = []
|
|
240
|
+
DO_NOT_MONITOR_GITHUB_EVENTS = False
|
|
241
|
+
GET_ALL_REPOS = False
|
|
242
|
+
BLOCKED_REPOS = False
|
|
243
|
+
TRACK_CONTRIB_CHANGES = False
|
|
194
244
|
LIVENESS_CHECK_INTERVAL = 0
|
|
195
245
|
CHECK_INTERNET_URL = ""
|
|
196
246
|
CHECK_INTERNET_TIMEOUT = 0
|
|
@@ -218,9 +268,6 @@ LIVENESS_CHECK_COUNTER = LIVENESS_CHECK_INTERVAL / GITHUB_CHECK_INTERVAL
|
|
|
218
268
|
stdout_bck = None
|
|
219
269
|
csvfieldnames = ['Date', 'Type', 'Name', 'Old', 'New']
|
|
220
270
|
|
|
221
|
-
TRACK_REPOS_CHANGES = False
|
|
222
|
-
DO_NOT_MONITOR_GITHUB_EVENTS = False
|
|
223
|
-
|
|
224
271
|
CLI_CONFIG_PATH = None
|
|
225
272
|
|
|
226
273
|
# to solve the issue: 'SyntaxError: f-string expression part cannot include a backslash'
|
|
@@ -236,7 +283,7 @@ if sys.version_info < (3, 10):
|
|
|
236
283
|
import time
|
|
237
284
|
import string
|
|
238
285
|
import os
|
|
239
|
-
from datetime import datetime, timezone
|
|
286
|
+
from datetime import datetime, timezone, date
|
|
240
287
|
from dateutil import relativedelta
|
|
241
288
|
from dateutil.parser import isoparse
|
|
242
289
|
import calendar
|
|
@@ -262,6 +309,7 @@ import re
|
|
|
262
309
|
import ipaddress
|
|
263
310
|
try:
|
|
264
311
|
from github import Github, Auth, GithubException, UnknownObjectException
|
|
312
|
+
from github.GithubException import RateLimitExceededException
|
|
265
313
|
from github.GithubException import BadCredentialsException
|
|
266
314
|
except ModuleNotFoundError:
|
|
267
315
|
raise SystemExit("Error: Couldn't find the PyGitHub library !\n\nTo install it, run:\n pip3 install PyGithub\n\nOnce installed, re-run this tool. For more help, visit:\nhttps://github.com/PyGithub/PyGithub")
|
|
@@ -272,7 +320,9 @@ import socket
|
|
|
272
320
|
from typing import Any, Callable
|
|
273
321
|
import shutil
|
|
274
322
|
from pathlib import Path
|
|
275
|
-
|
|
323
|
+
from typing import Optional
|
|
324
|
+
import datetime as dt
|
|
325
|
+
import requests
|
|
276
326
|
|
|
277
327
|
NET_ERRORS = (
|
|
278
328
|
req.exceptions.RequestException,
|
|
@@ -550,6 +600,11 @@ def now_local_naive():
|
|
|
550
600
|
return datetime.now(pytz.timezone(LOCAL_TIMEZONE)).replace(microsecond=0, tzinfo=None)
|
|
551
601
|
|
|
552
602
|
|
|
603
|
+
# Returns today's date in LOCAL_TIMEZONE (naive date)
|
|
604
|
+
def today_local() -> dt.date:
|
|
605
|
+
return now_local_naive().date()
|
|
606
|
+
|
|
607
|
+
|
|
553
608
|
# Returns the current date/time in human readable format; eg. Sun 21 Apr 2024, 15:08:45
|
|
554
609
|
def get_cur_ts(ts_str=""):
|
|
555
610
|
return (f'{ts_str}{calendar.day_abbr[(now_local_naive()).weekday()]}, {now_local_naive().strftime("%d %b %Y, %H:%M:%S")}')
|
|
@@ -619,6 +674,11 @@ def get_short_date_from_ts(ts, show_year=False, show_hour=True, show_weekday=Tru
|
|
|
619
674
|
ts_rounded = int(round(ts))
|
|
620
675
|
ts_new = datetime.fromtimestamp(ts_rounded, tz)
|
|
621
676
|
|
|
677
|
+
elif isinstance(ts, date):
|
|
678
|
+
ts = datetime.combine(ts, datetime.min.time())
|
|
679
|
+
ts = pytz.utc.localize(ts)
|
|
680
|
+
ts_new = ts.astimezone(tz)
|
|
681
|
+
|
|
622
682
|
else:
|
|
623
683
|
return ""
|
|
624
684
|
|
|
@@ -721,7 +781,7 @@ def toggle_profile_changes_notifications_signal_handler(sig, frame):
|
|
|
721
781
|
PROFILE_NOTIFICATION = not PROFILE_NOTIFICATION
|
|
722
782
|
sig_name = signal.Signals(sig).name
|
|
723
783
|
print(f"* Signal {sig_name} received")
|
|
724
|
-
print(f"* Email notifications
|
|
784
|
+
print(f"* Email notifications:\t\t[profile changes = {PROFILE_NOTIFICATION}]")
|
|
725
785
|
print_cur_ts("Timestamp:\t\t\t")
|
|
726
786
|
|
|
727
787
|
|
|
@@ -731,7 +791,7 @@ def toggle_new_events_notifications_signal_handler(sig, frame):
|
|
|
731
791
|
EVENT_NOTIFICATION = not EVENT_NOTIFICATION
|
|
732
792
|
sig_name = signal.Signals(sig).name
|
|
733
793
|
print(f"* Signal {sig_name} received")
|
|
734
|
-
print(f"* Email notifications
|
|
794
|
+
print(f"* Email notifications:\t\t[new events = {EVENT_NOTIFICATION}]")
|
|
735
795
|
print_cur_ts("Timestamp:\t\t\t")
|
|
736
796
|
|
|
737
797
|
|
|
@@ -741,7 +801,7 @@ def toggle_repo_changes_notifications_signal_handler(sig, frame):
|
|
|
741
801
|
REPO_NOTIFICATION = not REPO_NOTIFICATION
|
|
742
802
|
sig_name = signal.Signals(sig).name
|
|
743
803
|
print(f"* Signal {sig_name} received")
|
|
744
|
-
print(f"* Email notifications
|
|
804
|
+
print(f"* Email notifications:\t\t[repos changes = {REPO_NOTIFICATION}]")
|
|
745
805
|
print_cur_ts("Timestamp:\t\t\t")
|
|
746
806
|
|
|
747
807
|
|
|
@@ -751,7 +811,17 @@ def toggle_repo_update_date_changes_notifications_signal_handler(sig, frame):
|
|
|
751
811
|
REPO_UPDATE_DATE_NOTIFICATION = not REPO_UPDATE_DATE_NOTIFICATION
|
|
752
812
|
sig_name = signal.Signals(sig).name
|
|
753
813
|
print(f"* Signal {sig_name} received")
|
|
754
|
-
print(f"* Email notifications
|
|
814
|
+
print(f"* Email notifications:\t\t[repos update date = {REPO_UPDATE_DATE_NOTIFICATION}]")
|
|
815
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
# Signal handler for SIGURG allowing to switch email notifications for user's daily contributions changes
|
|
819
|
+
def toggle_contrib_changes_notifications_signal_handler(sig, frame):
|
|
820
|
+
global CONTRIB_NOTIFICATION
|
|
821
|
+
CONTRIB_NOTIFICATION = not CONTRIB_NOTIFICATION
|
|
822
|
+
sig_name = signal.Signals(sig).name
|
|
823
|
+
print(f"* Signal {sig_name} received")
|
|
824
|
+
print(f"* Email notifications:\t\t[contrib changes = {CONTRIB_NOTIFICATION}]")
|
|
755
825
|
print_cur_ts("Timestamp:\t\t\t")
|
|
756
826
|
|
|
757
827
|
|
|
@@ -761,7 +831,7 @@ def increase_check_signal_handler(sig, frame):
|
|
|
761
831
|
GITHUB_CHECK_INTERVAL = GITHUB_CHECK_INTERVAL + GITHUB_CHECK_SIGNAL_VALUE
|
|
762
832
|
sig_name = signal.Signals(sig).name
|
|
763
833
|
print(f"* Signal {sig_name} received")
|
|
764
|
-
print(f"*
|
|
834
|
+
print(f"* GitHub polling interval:\t[ {display_time(GITHUB_CHECK_INTERVAL)} ]")
|
|
765
835
|
print_cur_ts("Timestamp:\t\t\t")
|
|
766
836
|
|
|
767
837
|
|
|
@@ -772,7 +842,7 @@ def decrease_check_signal_handler(sig, frame):
|
|
|
772
842
|
GITHUB_CHECK_INTERVAL = GITHUB_CHECK_INTERVAL - GITHUB_CHECK_SIGNAL_VALUE
|
|
773
843
|
sig_name = signal.Signals(sig).name
|
|
774
844
|
print(f"* Signal {sig_name} received")
|
|
775
|
-
print(f"*
|
|
845
|
+
print(f"* GitHub polling interval:\t[ {display_time(GITHUB_CHECK_INTERVAL)} ]")
|
|
776
846
|
print_cur_ts("Timestamp:\t\t\t")
|
|
777
847
|
|
|
778
848
|
|
|
@@ -824,6 +894,34 @@ def gh_call(fn: Callable[..., Any], retries=NET_MAX_RETRIES, backoff=NET_BASE_BA
|
|
|
824
894
|
for i in range(1, retries + 1):
|
|
825
895
|
try:
|
|
826
896
|
return fn(*args, **kwargs)
|
|
897
|
+
except RateLimitExceededException as e:
|
|
898
|
+
headers = getattr(e, "headers", None)
|
|
899
|
+
|
|
900
|
+
reset_str = None
|
|
901
|
+
if headers:
|
|
902
|
+
val = headers.get("X-RateLimit-Reset")
|
|
903
|
+
if isinstance(val, str):
|
|
904
|
+
reset_str = val
|
|
905
|
+
|
|
906
|
+
sleep_for: int
|
|
907
|
+
if reset_str is not None and reset_str.isdigit():
|
|
908
|
+
reset_epoch = int(reset_str)
|
|
909
|
+
sleep_for = max(0, reset_epoch - int(time.time()) + 1)
|
|
910
|
+
else:
|
|
911
|
+
retry_after_str = None
|
|
912
|
+
if headers:
|
|
913
|
+
ra = headers.get("Retry-After")
|
|
914
|
+
if isinstance(ra, str):
|
|
915
|
+
retry_after_str = ra
|
|
916
|
+
if retry_after_str is not None and retry_after_str.isdigit():
|
|
917
|
+
sleep_for = int(retry_after_str)
|
|
918
|
+
else:
|
|
919
|
+
sleep_for = int(backoff * i)
|
|
920
|
+
|
|
921
|
+
print(f"* {fn.__name__} rate limited, sleeping {sleep_for}s (retry {i}/{retries})")
|
|
922
|
+
time.sleep(sleep_for)
|
|
923
|
+
continue
|
|
924
|
+
|
|
827
925
|
except NET_ERRORS as e:
|
|
828
926
|
print(f"* {fn.__name__} error: {e} (retry {i}/{retries})")
|
|
829
927
|
time.sleep(backoff * i)
|
|
@@ -865,7 +963,7 @@ def github_print_followers_and_followings(user):
|
|
|
865
963
|
|
|
866
964
|
print(f"\nUsername:\t\t{user_name_str}")
|
|
867
965
|
print(f"User URL:\t\t{user_url}/")
|
|
868
|
-
print(f"
|
|
966
|
+
print(f"GitHub API URL:\t\t{GITHUB_API_URL}")
|
|
869
967
|
print(f"Local timezone:\t\t{LOCAL_TIMEZONE}")
|
|
870
968
|
|
|
871
969
|
print(f"\nFollowers:\t\t{followers_count}")
|
|
@@ -903,6 +1001,7 @@ def github_print_followers_and_followings(user):
|
|
|
903
1001
|
|
|
904
1002
|
# Processes items from all passed repositories and returns a list of dictionaries
|
|
905
1003
|
def github_process_repos(repos_list):
|
|
1004
|
+
import logging
|
|
906
1005
|
list_of_repos = []
|
|
907
1006
|
stargazers_list = []
|
|
908
1007
|
subscribers_list = []
|
|
@@ -913,9 +1012,24 @@ def github_process_repos(repos_list):
|
|
|
913
1012
|
try:
|
|
914
1013
|
repo_created_date = repo.created_at
|
|
915
1014
|
repo_updated_date = repo.updated_at
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1015
|
+
|
|
1016
|
+
github_logger = logging.getLogger('github')
|
|
1017
|
+
original_level = github_logger.level
|
|
1018
|
+
github_logger.setLevel(logging.ERROR)
|
|
1019
|
+
|
|
1020
|
+
try:
|
|
1021
|
+
stargazers_list = [star.login for star in repo.get_stargazers()]
|
|
1022
|
+
subscribers_list = [subscriber.login for subscriber in repo.get_subscribers()]
|
|
1023
|
+
forked_repos = [fork.full_name for fork in repo.get_forks()]
|
|
1024
|
+
except GithubException as e:
|
|
1025
|
+
if e.status in [403, 451]:
|
|
1026
|
+
if BLOCKED_REPOS:
|
|
1027
|
+
print(f"* Repo '{repo.name}' is blocked, skipping for now: {e}")
|
|
1028
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1029
|
+
continue
|
|
1030
|
+
raise
|
|
1031
|
+
finally:
|
|
1032
|
+
github_logger.setLevel(original_level)
|
|
919
1033
|
|
|
920
1034
|
issues = list(repo.get_issues(state='open'))
|
|
921
1035
|
pulls = list(repo.get_pulls(state='open'))
|
|
@@ -928,8 +1042,20 @@ def github_process_repos(repos_list):
|
|
|
928
1042
|
pr_list = [f"#{pr.number} {pr.title} ({pr.user.login}) [ {pr.html_url} ]" for pr in pulls]
|
|
929
1043
|
|
|
930
1044
|
list_of_repos.append({"name": repo.name, "descr": repo.description, "is_fork": repo.fork, "forks": repo.forks_count, "stars": repo.stargazers_count, "subscribers": repo.subscribers_count, "url": repo.html_url, "language": repo.language, "date": repo_created_date, "update_date": repo_updated_date, "stargazers_list": stargazers_list, "forked_repos": forked_repos, "subscribers_list": subscribers_list, "issues": issue_count, "pulls": pr_count, "issues_list": issues_list, "pulls_list": pr_list})
|
|
1045
|
+
|
|
1046
|
+
except GithubException as e:
|
|
1047
|
+
# Skip TOS-blocked (403) and legally blocked (451) repositories
|
|
1048
|
+
if e.status in [403, 451]:
|
|
1049
|
+
if BLOCKED_REPOS:
|
|
1050
|
+
print(f"* Repo '{repo.name}' is blocked, skipping for now: {e}")
|
|
1051
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1052
|
+
continue
|
|
1053
|
+
else:
|
|
1054
|
+
print(f"* Cannot process repo '{repo.name}', skipping for now: {e}")
|
|
1055
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
1056
|
+
continue
|
|
931
1057
|
except Exception as e:
|
|
932
|
-
print(f"*
|
|
1058
|
+
print(f"* Cannot process repo '{repo.name}', skipping for now: {e}")
|
|
933
1059
|
print_cur_ts("Timestamp:\t\t\t")
|
|
934
1060
|
continue
|
|
935
1061
|
|
|
@@ -938,6 +1064,7 @@ def github_process_repos(repos_list):
|
|
|
938
1064
|
|
|
939
1065
|
# Prints a list of public repositories for a GitHub user (-r)
|
|
940
1066
|
def github_print_repos(user):
|
|
1067
|
+
import logging
|
|
941
1068
|
user_name_str = user
|
|
942
1069
|
user_url = "-"
|
|
943
1070
|
repos_count = 0
|
|
@@ -954,8 +1081,12 @@ def github_print_repos(user):
|
|
|
954
1081
|
user_name = g_user.name
|
|
955
1082
|
user_url = g_user.html_url
|
|
956
1083
|
|
|
957
|
-
|
|
958
|
-
|
|
1084
|
+
if GET_ALL_REPOS:
|
|
1085
|
+
repos_list = g_user.get_repos()
|
|
1086
|
+
repos_count = g_user.public_repos
|
|
1087
|
+
else:
|
|
1088
|
+
repos_list = [repo for repo in g_user.get_repos(type='owner') if not repo.fork and repo.owner.login == user_login]
|
|
1089
|
+
repos_count = len(repos_list)
|
|
959
1090
|
|
|
960
1091
|
user_name_str = user_login
|
|
961
1092
|
if user_name:
|
|
@@ -965,7 +1096,8 @@ def github_print_repos(user):
|
|
|
965
1096
|
|
|
966
1097
|
print(f"\nUsername:\t\t{user_name_str}")
|
|
967
1098
|
print(f"User URL:\t\t{user_url}/")
|
|
968
|
-
print(f"
|
|
1099
|
+
print(f"GitHub API URL:\t\t{GITHUB_API_URL}")
|
|
1100
|
+
print(f"Owned repos only:\t{not GET_ALL_REPOS}")
|
|
969
1101
|
print(f"Local timezone:\t\t{LOCAL_TIMEZONE}")
|
|
970
1102
|
|
|
971
1103
|
print(f"\nRepositories:\t\t{repos_count}\n")
|
|
@@ -976,6 +1108,10 @@ def github_print_repos(user):
|
|
|
976
1108
|
for repo in repos_list:
|
|
977
1109
|
print(f"🔸 {repo.name} {'(fork)' if repo.fork else ''} \n")
|
|
978
1110
|
|
|
1111
|
+
github_logger = logging.getLogger('github')
|
|
1112
|
+
original_level = github_logger.level
|
|
1113
|
+
github_logger.setLevel(logging.ERROR)
|
|
1114
|
+
|
|
979
1115
|
try:
|
|
980
1116
|
pr_count = repo.get_pulls(state='open').totalCount
|
|
981
1117
|
issue_count = repo.open_issues_count - pr_count
|
|
@@ -983,26 +1119,36 @@ def github_print_repos(user):
|
|
|
983
1119
|
pr_count = "?"
|
|
984
1120
|
issue_count = "?"
|
|
985
1121
|
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1122
|
+
try:
|
|
1123
|
+
print(f" - 🌐 URL:\t\t{repo.html_url}")
|
|
1124
|
+
print(f" - 💻 Language:\t\t{repo.language}")
|
|
1125
|
+
|
|
1126
|
+
print(f"\n - ⭐ Stars:\t\t{repo.stargazers_count}")
|
|
1127
|
+
print(f" - 🍴 Forks:\t\t{repo.forks_count}")
|
|
1128
|
+
print(f" - 👓 Watchers:\t\t{repo.subscribers_count}")
|
|
1129
|
+
|
|
1130
|
+
# print(f" - 🐞 Issues+PRs:\t{repo.open_issues_count}")
|
|
1131
|
+
print(f" - 🐞 Issues:\t\t{issue_count}")
|
|
1132
|
+
print(f" - 📬 PRs:\t\t{pr_count}")
|
|
1133
|
+
|
|
1134
|
+
print(f"\n - 📝 License:\t\t{repo.license.name if repo.license else 'None'}")
|
|
1135
|
+
print(f" - 🌿 Branch (default):\t{repo.default_branch}")
|
|
1136
|
+
|
|
1137
|
+
print(f"\n - 📅 Created:\t\t{get_date_from_ts(repo.created_at)} ({calculate_timespan(int(time.time()), repo.created_at, granularity=2)} ago)")
|
|
1138
|
+
print(f" - 🔄 Updated:\t\t{get_date_from_ts(repo.updated_at)} ({calculate_timespan(int(time.time()), repo.updated_at, granularity=2)} ago)")
|
|
1139
|
+
print(f" - 🔃 Last push:\t{get_date_from_ts(repo.pushed_at)} ({calculate_timespan(int(time.time()), repo.pushed_at, granularity=2)} ago)")
|
|
1140
|
+
|
|
1141
|
+
if repo.description:
|
|
1142
|
+
print(f"\n - 📝 Desc:\t\t{repo.description}")
|
|
1143
|
+
except GithubException as e:
|
|
1144
|
+
# Inform about TOS-blocked (403) and legally blocked (451) repositories
|
|
1145
|
+
if e.status in [403, 451]:
|
|
1146
|
+
print(f"\n* Repo '{repo.name}' is blocked: {e}")
|
|
1147
|
+
print("─" * HORIZONTAL_LINE2)
|
|
1148
|
+
continue
|
|
1149
|
+
finally:
|
|
1150
|
+
github_logger.setLevel(original_level)
|
|
1003
1151
|
|
|
1004
|
-
if repo.description:
|
|
1005
|
-
print(f"\n - 📝 Desc:\t\t{repo.description}")
|
|
1006
1152
|
print("─" * HORIZONTAL_LINE2)
|
|
1007
1153
|
except Exception as e:
|
|
1008
1154
|
raise RuntimeError(f"Cannot fetch user's repositories list: {e}")
|
|
@@ -1039,7 +1185,7 @@ def github_print_starred_repos(user):
|
|
|
1039
1185
|
|
|
1040
1186
|
print(f"\nUsername:\t\t{user_name_str}")
|
|
1041
1187
|
print(f"User URL:\t\t{user_url}/")
|
|
1042
|
-
print(f"
|
|
1188
|
+
print(f"GitHub API URL:\t\t{GITHUB_API_URL}")
|
|
1043
1189
|
print(f"Local timezone:\t\t{LOCAL_TIMEZONE}")
|
|
1044
1190
|
|
|
1045
1191
|
print(f"\nRepos starred by user:\t{starred_count}")
|
|
@@ -1074,6 +1220,13 @@ def format_body_block(content, indent=" "):
|
|
|
1074
1220
|
return f"\n{indented}"
|
|
1075
1221
|
|
|
1076
1222
|
|
|
1223
|
+
# Returns the base web URL for GitHub or GHE (e.g. https://github.com or https://ghe.example.com)
|
|
1224
|
+
def github_web_base() -> str:
|
|
1225
|
+
if "api.github.com" in GITHUB_API_URL:
|
|
1226
|
+
return "https://github.com"
|
|
1227
|
+
return GITHUB_API_URL.replace("/api/v3", "").rstrip("/")
|
|
1228
|
+
|
|
1229
|
+
|
|
1077
1230
|
# Prints details about passed GitHub event
|
|
1078
1231
|
def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
1079
1232
|
|
|
@@ -1094,19 +1247,33 @@ def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
|
1094
1247
|
st += print_v(f"Event type:\t\t\t{event.type}")
|
|
1095
1248
|
|
|
1096
1249
|
if event.repo.id:
|
|
1097
|
-
repo_name = event.repo.name
|
|
1098
|
-
repo_url = event.repo.url.replace("https://api.github.com/repos/", "https://github.com/")
|
|
1099
|
-
st += print_v(f"\nRepo name:\t\t\t{repo_name}")
|
|
1100
|
-
st += print_v(f"Repo URL:\t\t\t{repo_url}")
|
|
1101
|
-
|
|
1102
1250
|
try:
|
|
1103
1251
|
desc_len = 80
|
|
1104
1252
|
repo = g.get_repo(event.repo.name)
|
|
1105
|
-
|
|
1253
|
+
|
|
1254
|
+
# For ForkEvent, prefer the source repo if available
|
|
1255
|
+
if event.type == "ForkEvent" and repo is not None:
|
|
1256
|
+
try:
|
|
1257
|
+
parent = gh_call(lambda: getattr(repo, "parent", None))()
|
|
1258
|
+
if parent:
|
|
1259
|
+
repo = parent
|
|
1260
|
+
except Exception:
|
|
1261
|
+
pass
|
|
1262
|
+
|
|
1263
|
+
repo_name = getattr(repo, "full_name", event.repo.name)
|
|
1264
|
+
|
|
1265
|
+
api_prefix = GITHUB_API_URL.rstrip("/") + "/repos/"
|
|
1266
|
+
repo_url = getattr(repo, "html_url", event.repo.url.replace(api_prefix, github_web_base() + "/"))
|
|
1267
|
+
|
|
1268
|
+
st += print_v(f"\nRepo name:\t\t\t{repo_name}")
|
|
1269
|
+
st += print_v(f"Repo URL:\t\t\t{repo_url}")
|
|
1270
|
+
|
|
1271
|
+
desc = (repo.description or "") if repo else ""
|
|
1106
1272
|
cleaned = desc.replace('\n', ' ')
|
|
1107
1273
|
short_desc = cleaned[:desc_len] + '...' if len(cleaned) > desc_len else cleaned
|
|
1108
1274
|
if short_desc:
|
|
1109
1275
|
st += print_v(f"Repo description:\t\t{short_desc}")
|
|
1276
|
+
|
|
1110
1277
|
except UnknownObjectException:
|
|
1111
1278
|
repo = None
|
|
1112
1279
|
st += print_v("\nRepository not found or has been removed")
|
|
@@ -1134,6 +1301,7 @@ def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
|
1134
1301
|
if event.payload.get("action"):
|
|
1135
1302
|
st += print_v(f"\nAction:\t\t\t\t{event.payload.get('action')}")
|
|
1136
1303
|
|
|
1304
|
+
# Prefer commits from payload when present (older API behavior)
|
|
1137
1305
|
if event.payload.get("commits"):
|
|
1138
1306
|
commits = event.payload["commits"]
|
|
1139
1307
|
commits_total = len(commits)
|
|
@@ -1144,7 +1312,7 @@ def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
|
1144
1312
|
|
|
1145
1313
|
commit_details = None
|
|
1146
1314
|
if repo:
|
|
1147
|
-
commit_details = repo.get_commit(commit["sha"])
|
|
1315
|
+
commit_details = gh_call(lambda: repo.get_commit(commit["sha"]))()
|
|
1148
1316
|
|
|
1149
1317
|
if commit_details:
|
|
1150
1318
|
commit_date = commit_details.commit.author.date
|
|
@@ -1180,6 +1348,81 @@ def github_print_event(event, g, time_passed=False, ts: datetime | None = None):
|
|
|
1180
1348
|
st += print_v(f"\n - Commit message:\t\t'{commit['message']}'")
|
|
1181
1349
|
st += print_v("." * HORIZONTAL_LINE1)
|
|
1182
1350
|
|
|
1351
|
+
# Fallback for new Events API where PushEvent no longer includes commit summaries
|
|
1352
|
+
elif event.type == "PushEvent" and repo:
|
|
1353
|
+
before_sha = event.payload.get("before")
|
|
1354
|
+
head_sha = event.payload.get("head") or event.payload.get("after")
|
|
1355
|
+
size_hint = event.payload.get("size")
|
|
1356
|
+
|
|
1357
|
+
# Debug when payload has no commits
|
|
1358
|
+
# st += print_v("\n[debug] PushEvent payload has no 'commits' array; using compare API")
|
|
1359
|
+
# st += print_v(f"[debug] before:\t\t\t{before_sha}")
|
|
1360
|
+
# st += print_v(f"[debug] head/after:\t\t{head_sha}")
|
|
1361
|
+
# if size_hint is not None:
|
|
1362
|
+
# st += print_v(f"[debug] size (hint):\t\t{size_hint}")
|
|
1363
|
+
|
|
1364
|
+
if before_sha and head_sha and before_sha != head_sha:
|
|
1365
|
+
try:
|
|
1366
|
+
compare = gh_call(lambda: repo.compare(before_sha, head_sha))()
|
|
1367
|
+
except Exception as e:
|
|
1368
|
+
compare = None
|
|
1369
|
+
st += print_v(f"* Error using compare({before_sha[:12]}...{head_sha[:12]}): {e}")
|
|
1370
|
+
|
|
1371
|
+
if compare:
|
|
1372
|
+
commits = list(compare.commits)
|
|
1373
|
+
commits_total = len(commits)
|
|
1374
|
+
short_repo = getattr(repo, "full_name", repo_name)
|
|
1375
|
+
compare_url = f"{github_web_base()}/{short_repo}/compare/{before_sha[:12]}...{head_sha[:12]}"
|
|
1376
|
+
st += print_v(f"\nNumber of commits:\t\t{commits_total}")
|
|
1377
|
+
st += print_v(f"Compare URL:\t\t\t{compare_url}")
|
|
1378
|
+
|
|
1379
|
+
for commit_count, c in enumerate(commits, start=1):
|
|
1380
|
+
st += print_v(f"\n=== Commit {commit_count}/{commits_total} ===")
|
|
1381
|
+
st += print_v("." * HORIZONTAL_LINE1)
|
|
1382
|
+
|
|
1383
|
+
commit_sha = getattr(c, "sha", None) or getattr(c, "id", None)
|
|
1384
|
+
commit_details = gh_call(lambda: repo.get_commit(commit_sha))() if (repo and commit_sha) else None
|
|
1385
|
+
|
|
1386
|
+
if commit_details:
|
|
1387
|
+
commit_date = commit_details.commit.author.date
|
|
1388
|
+
st += print_v(f" - Commit date:\t\t\t{get_date_from_ts(commit_date)}")
|
|
1389
|
+
|
|
1390
|
+
if commit_sha:
|
|
1391
|
+
st += print_v(f" - Commit SHA:\t\t\t{commit_sha}")
|
|
1392
|
+
|
|
1393
|
+
author_name = None
|
|
1394
|
+
if commit_details and commit_details.commit and commit_details.commit.author:
|
|
1395
|
+
author_name = commit_details.commit.author.name
|
|
1396
|
+
st += print_v(f" - Commit author:\t\t{author_name or 'N/A'}")
|
|
1397
|
+
|
|
1398
|
+
if commit_details and commit_details.author:
|
|
1399
|
+
st += print_v(f" - Commit author URL:\t\t{commit_details.author.html_url}")
|
|
1400
|
+
|
|
1401
|
+
if commit_details:
|
|
1402
|
+
st += print_v(f" - Commit URL:\t\t\t{commit_details.html_url}")
|
|
1403
|
+
st += print_v(f" - Commit raw patch URL:\t{commit_details.html_url}.patch")
|
|
1404
|
+
|
|
1405
|
+
stats = getattr(commit_details, "stats", None)
|
|
1406
|
+
additions = stats.additions if stats else 0
|
|
1407
|
+
deletions = stats.deletions if stats else 0
|
|
1408
|
+
stats_total = stats.total if stats else 0
|
|
1409
|
+
st += print_v(f"\n - Additions/Deletions:\t\t+{additions} / -{deletions} ({stats_total})")
|
|
1410
|
+
|
|
1411
|
+
try:
|
|
1412
|
+
file_count = sum(1 for _ in commit_details.files)
|
|
1413
|
+
except Exception:
|
|
1414
|
+
file_count = "N/A"
|
|
1415
|
+
st += print_v(f" - Files changed:\t\t{file_count}")
|
|
1416
|
+
if file_count and file_count != "N/A":
|
|
1417
|
+
st += print_v(" - Changed files list:")
|
|
1418
|
+
for f in commit_details.files:
|
|
1419
|
+
st += print_v(f" • '{f.filename}' - {f.status} (+{f.additions} / -{f.deletions})")
|
|
1420
|
+
|
|
1421
|
+
st += print_v(f"\n - Commit message:\t\t'{commit_details.commit.message}'")
|
|
1422
|
+
st += print_v("." * HORIZONTAL_LINE1)
|
|
1423
|
+
else:
|
|
1424
|
+
st += print_v("\nNo compare range available (forced push, tag push, or identical before/after)")
|
|
1425
|
+
|
|
1183
1426
|
if event.payload.get("commits") == []:
|
|
1184
1427
|
st += print_v("\nNo new commits (forced push, tag push, branch reset or other ref update)")
|
|
1185
1428
|
|
|
@@ -1557,7 +1800,7 @@ def github_list_events(user, number, csv_file_name):
|
|
|
1557
1800
|
|
|
1558
1801
|
print(f"Username:\t\t\t{user_name_str}")
|
|
1559
1802
|
print(f"User URL:\t\t\t{user_url}/")
|
|
1560
|
-
print(f"
|
|
1803
|
+
print(f"GitHub API URL:\t\t\t{GITHUB_API_URL}")
|
|
1561
1804
|
if csv_file_name:
|
|
1562
1805
|
print(f"CSV export enabled:\t\t{bool(csv_file_name)}" + (f" ({csv_file_name})" if csv_file_name else ""))
|
|
1563
1806
|
print(f"Local timezone:\t\t\t{LOCAL_TIMEZONE}")
|
|
@@ -1636,9 +1879,11 @@ def handle_profile_change(label, count_old, count_new, list_old, raw_list, user,
|
|
|
1636
1879
|
if removed_items:
|
|
1637
1880
|
print(f"Removed {label.lower()}:\n")
|
|
1638
1881
|
removed_mbody = f"\nRemoved {label.lower()}:\n\n"
|
|
1882
|
+
web_base = github_web_base()
|
|
1639
1883
|
for item in removed_items:
|
|
1640
|
-
item_url = (f"
|
|
1641
|
-
else f"
|
|
1884
|
+
item_url = (f"{web_base}/{item}/" if label.lower() in ["followers", "followings", "starred repos"]
|
|
1885
|
+
else f"{web_base}/{user}/{item}/")
|
|
1886
|
+
|
|
1642
1887
|
print(f"- {item} [ {item_url} ]")
|
|
1643
1888
|
removed_list_str += f"- {item} [ {item_url} ]\n"
|
|
1644
1889
|
try:
|
|
@@ -1651,9 +1896,10 @@ def handle_profile_change(label, count_old, count_new, list_old, raw_list, user,
|
|
|
1651
1896
|
if added_items:
|
|
1652
1897
|
print(f"Added {label.lower()}:\n")
|
|
1653
1898
|
added_mbody = f"\nAdded {label.lower()}:\n\n"
|
|
1899
|
+
web_base = github_web_base()
|
|
1654
1900
|
for item in added_items:
|
|
1655
|
-
item_url = (f"
|
|
1656
|
-
else f"
|
|
1901
|
+
item_url = (f"{web_base}/{item}/" if label.lower() in ["followers", "followings", "starred repos"]
|
|
1902
|
+
else f"{web_base}/{user}/{item}/")
|
|
1657
1903
|
print(f"- {item} [ {item_url} ]")
|
|
1658
1904
|
added_list_str += f"- {item} [ {item_url} ]\n"
|
|
1659
1905
|
try:
|
|
@@ -1664,12 +1910,12 @@ def handle_profile_change(label, count_old, count_new, list_old, raw_list, user,
|
|
|
1664
1910
|
print()
|
|
1665
1911
|
|
|
1666
1912
|
if diff == 0:
|
|
1667
|
-
m_subject = f"
|
|
1913
|
+
m_subject = f"GitHub user {user} {label.lower()} list changed"
|
|
1668
1914
|
m_body = (f"{label} list changed {label_context} user {user}\n"
|
|
1669
1915
|
f"{removed_mbody}{removed_list_str}{added_mbody}{added_list_str}\n"
|
|
1670
1916
|
f"Check interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}")
|
|
1671
1917
|
else:
|
|
1672
|
-
m_subject = f"
|
|
1918
|
+
m_subject = f"GitHub user {user} {label.lower()} number has changed! ({diff_str}, {old_count} -> {new_count})"
|
|
1673
1919
|
m_body = (f"{label} number changed {label_context} user {user} from {old_count} to {new_count} ({diff_str})\n"
|
|
1674
1920
|
f"{removed_mbody}{removed_list_str}{added_mbody}{added_list_str}\n"
|
|
1675
1921
|
f"Check interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}")
|
|
@@ -1725,7 +1971,7 @@ def check_repo_list_changes(count_old, count_new, list_old, list_new, label, rep
|
|
|
1725
1971
|
print(f"{removal_text} {label.lower()}:\n")
|
|
1726
1972
|
removed_mbody = f"\n{removal_text} {label.lower()}:\n\n"
|
|
1727
1973
|
for item in removed_items:
|
|
1728
|
-
item_line = f"- {item} [
|
|
1974
|
+
item_line = f"- {item} [ {github_web_base()}/{item}/ ]" if label.lower() in ["stargazers", "watchers", "forks"] else f"- {item}"
|
|
1729
1975
|
print(item_line)
|
|
1730
1976
|
removed_list_str += item_line + "\n"
|
|
1731
1977
|
try:
|
|
@@ -1740,7 +1986,7 @@ def check_repo_list_changes(count_old, count_new, list_old, list_new, label, rep
|
|
|
1740
1986
|
print(f"Added {label.lower()}:\n")
|
|
1741
1987
|
added_mbody = f"\nAdded {label.lower()}:\n\n"
|
|
1742
1988
|
for item in added_items:
|
|
1743
|
-
item_line = f"- {item} [
|
|
1989
|
+
item_line = f"- {item} [ {github_web_base()}/{item}/ ]" if label.lower() in ["stargazers", "watchers", "forks"] else f"- {item}"
|
|
1744
1990
|
print(item_line)
|
|
1745
1991
|
added_list_str += item_line + "\n"
|
|
1746
1992
|
try:
|
|
@@ -1752,12 +1998,12 @@ def check_repo_list_changes(count_old, count_new, list_old, list_new, label, rep
|
|
|
1752
1998
|
print()
|
|
1753
1999
|
|
|
1754
2000
|
if diff == 0:
|
|
1755
|
-
m_subject = f"
|
|
2001
|
+
m_subject = f"GitHub user {user} {label.lower()} list changed for repo '{repo_name}'!"
|
|
1756
2002
|
m_body = (f"* Repo '{repo_name}': {label.lower()} list changed\n"
|
|
1757
2003
|
f"* Repo URL: {repo_url}\n{removed_mbody}{removed_list_str}{added_mbody}{added_list_str}\n"
|
|
1758
2004
|
f"Check interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}")
|
|
1759
2005
|
else:
|
|
1760
|
-
m_subject = f"
|
|
2006
|
+
m_subject = f"GitHub user {user} number of {label.lower()} for repo '{repo_name}' has changed! ({diff_str}, {old_count} -> {new_count})"
|
|
1761
2007
|
m_body = (f"* Repo '{repo_name}': number of {label.lower()} changed from {old_count} to {new_count} ({diff_str})\n"
|
|
1762
2008
|
f"* Repo URL: {repo_url}\n{removed_mbody}{removed_list_str}{added_mbody}{added_list_str}\n"
|
|
1763
2009
|
f"Check interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}")
|
|
@@ -1807,7 +2053,215 @@ def resolve_executable(path):
|
|
|
1807
2053
|
raise FileNotFoundError(f"Could not find executable '{path}'")
|
|
1808
2054
|
|
|
1809
2055
|
|
|
1810
|
-
#
|
|
2056
|
+
# Checks if the authenticated user (token's owner) is blocked by user
|
|
2057
|
+
def is_blocked_by(user):
|
|
2058
|
+
try:
|
|
2059
|
+
|
|
2060
|
+
headers = {
|
|
2061
|
+
"Authorization": f"Bearer {GITHUB_TOKEN}",
|
|
2062
|
+
"Accept": "application/vnd.github+json",
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
response = req.get(f"{GITHUB_API_URL}/user", headers=headers, timeout=15)
|
|
2066
|
+
if response.status_code != 200:
|
|
2067
|
+
return False
|
|
2068
|
+
me_login = response.json().get("login", "").lower()
|
|
2069
|
+
if user.lower() == me_login:
|
|
2070
|
+
return False
|
|
2071
|
+
|
|
2072
|
+
graphql_endpoint = GITHUB_API_URL.rstrip("/") + "/graphql"
|
|
2073
|
+
query = """
|
|
2074
|
+
query($login: String!) {
|
|
2075
|
+
user(login: $login) {
|
|
2076
|
+
viewerCanFollow
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
"""
|
|
2080
|
+
payload = {"query": query, "variables": {"login": user}}
|
|
2081
|
+
response_graphql = req.post(graphql_endpoint, json=payload, headers=headers, timeout=15)
|
|
2082
|
+
|
|
2083
|
+
if response_graphql.status_code == 404:
|
|
2084
|
+
return False
|
|
2085
|
+
|
|
2086
|
+
if not response_graphql.ok:
|
|
2087
|
+
return False
|
|
2088
|
+
|
|
2089
|
+
data = response_graphql.json()
|
|
2090
|
+
can_follow = (data.get("data", {}).get("user", {}).get("viewerCanFollow", True))
|
|
2091
|
+
return not bool(can_follow)
|
|
2092
|
+
|
|
2093
|
+
except Exception:
|
|
2094
|
+
return False
|
|
2095
|
+
|
|
2096
|
+
|
|
2097
|
+
# Return the total number of repositories the user has starred (faster than via PyGithub)
|
|
2098
|
+
def get_starred_count(user):
|
|
2099
|
+
try:
|
|
2100
|
+
|
|
2101
|
+
headers = {
|
|
2102
|
+
"Authorization": f"Bearer {GITHUB_TOKEN}",
|
|
2103
|
+
"Accept": "application/vnd.github+json",
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
graphql_endpoint = f"{GITHUB_API_URL.rstrip('/')}/graphql"
|
|
2107
|
+
query = """
|
|
2108
|
+
query($login:String!){
|
|
2109
|
+
user(login:$login){
|
|
2110
|
+
starredRepositories{
|
|
2111
|
+
totalCount
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
"""
|
|
2116
|
+
payload = {"query": query, "variables": {"login": user}}
|
|
2117
|
+
response = req.post(graphql_endpoint, json=payload, headers=headers, timeout=15)
|
|
2118
|
+
|
|
2119
|
+
if not response.ok:
|
|
2120
|
+
return 0
|
|
2121
|
+
|
|
2122
|
+
data = response.json()
|
|
2123
|
+
|
|
2124
|
+
return (data.get("data", {}).get("user", {}).get("starredRepositories", {}).get("totalCount", 0))
|
|
2125
|
+
|
|
2126
|
+
except Exception:
|
|
2127
|
+
return 0
|
|
2128
|
+
|
|
2129
|
+
|
|
2130
|
+
# Returns True if the user's GitHub page shows "activity is private"
|
|
2131
|
+
def has_private_banner(user):
|
|
2132
|
+
try:
|
|
2133
|
+
url = f"{GITHUB_HTML_URL.rstrip('/')}/{user}"
|
|
2134
|
+
r = req.get(url, timeout=15)
|
|
2135
|
+
return r.ok and "activity is private" in r.text.lower()
|
|
2136
|
+
except Exception:
|
|
2137
|
+
return False
|
|
2138
|
+
|
|
2139
|
+
|
|
2140
|
+
# Returns True if the user's GitHub profile is public
|
|
2141
|
+
def is_profile_public(g: Github, user, new_account_days=30):
|
|
2142
|
+
|
|
2143
|
+
if has_private_banner(user):
|
|
2144
|
+
return False
|
|
2145
|
+
|
|
2146
|
+
try:
|
|
2147
|
+
u = g.get_user(user)
|
|
2148
|
+
|
|
2149
|
+
if any([
|
|
2150
|
+
u.followers > 0,
|
|
2151
|
+
u.following > 0,
|
|
2152
|
+
get_starred_count(user) > 0,
|
|
2153
|
+
]):
|
|
2154
|
+
return True
|
|
2155
|
+
|
|
2156
|
+
try:
|
|
2157
|
+
events_iter = iter(u.get_events())
|
|
2158
|
+
next(events_iter)
|
|
2159
|
+
return True
|
|
2160
|
+
except (StopIteration, GithubException):
|
|
2161
|
+
pass
|
|
2162
|
+
|
|
2163
|
+
except GithubException:
|
|
2164
|
+
pass
|
|
2165
|
+
|
|
2166
|
+
return False
|
|
2167
|
+
|
|
2168
|
+
|
|
2169
|
+
# Returns a dict mapping 'YYYY-MM-DD' -> int contribution count for the range
|
|
2170
|
+
def get_daily_contributions(username: str, start: Optional[dt.date] = None, end: Optional[dt.date] = None, token: Optional[str] = None) -> dict:
|
|
2171
|
+
if token is None:
|
|
2172
|
+
raise ValueError("GitHub token is required")
|
|
2173
|
+
|
|
2174
|
+
today = dt.date.today()
|
|
2175
|
+
if start is None:
|
|
2176
|
+
start = today
|
|
2177
|
+
if end is None:
|
|
2178
|
+
end = today
|
|
2179
|
+
|
|
2180
|
+
url = GITHUB_API_URL.rstrip("/") + "/graphql"
|
|
2181
|
+
headers = {
|
|
2182
|
+
"Authorization": f"Bearer {token}",
|
|
2183
|
+
"Time-Zone": LOCAL_TIMEZONE,
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
tz = pytz.timezone(LOCAL_TIMEZONE)
|
|
2187
|
+
start_w = start - dt.timedelta(days=1)
|
|
2188
|
+
end_w_exclusive = end + dt.timedelta(days=2)
|
|
2189
|
+
start_iso = tz.localize(dt.datetime.combine(start_w, dt.time.min)).isoformat()
|
|
2190
|
+
end_iso = tz.localize(dt.datetime.combine(end_w_exclusive, dt.time.min)).isoformat()
|
|
2191
|
+
|
|
2192
|
+
query = """
|
|
2193
|
+
query($login: String!, $from: DateTime!, $to: DateTime!) {
|
|
2194
|
+
user(login: $login) {
|
|
2195
|
+
contributionsCollection(from: $from, to: $to) {
|
|
2196
|
+
contributionCalendar {
|
|
2197
|
+
weeks {
|
|
2198
|
+
contributionDays {
|
|
2199
|
+
date
|
|
2200
|
+
contributionCount
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
}"""
|
|
2207
|
+
|
|
2208
|
+
variables = {"login": username, "from": start_iso, "to": end_iso}
|
|
2209
|
+
r = requests.post(url, json={"query": query, "variables": variables}, headers=headers, timeout=30)
|
|
2210
|
+
r.raise_for_status()
|
|
2211
|
+
data = r.json()
|
|
2212
|
+
|
|
2213
|
+
days = data["data"]["user"]["contributionsCollection"]["contributionCalendar"]["weeks"]
|
|
2214
|
+
out = {}
|
|
2215
|
+
for w in days:
|
|
2216
|
+
for d in w["contributionDays"]:
|
|
2217
|
+
date = d["date"]
|
|
2218
|
+
if start <= dt.date.fromisoformat(date) <= end:
|
|
2219
|
+
out[date] = d["contributionCount"]
|
|
2220
|
+
return out
|
|
2221
|
+
|
|
2222
|
+
|
|
2223
|
+
# Return contribution count for a single day
|
|
2224
|
+
def get_daily_contributions_count(username: str, day: dt.date, token: str) -> int:
|
|
2225
|
+
data = get_daily_contributions(username, day, day, token)
|
|
2226
|
+
return next(iter(data.values()), 0)
|
|
2227
|
+
|
|
2228
|
+
|
|
2229
|
+
# Checks count for today and decides whether to notify based on stored state.
|
|
2230
|
+
def check_daily_contribs(username: str, token: str, state: dict, min_delta: int = 1, fail_threshold: int = 3) -> tuple[bool, int, bool]:
|
|
2231
|
+
day = today_local()
|
|
2232
|
+
|
|
2233
|
+
try:
|
|
2234
|
+
curr = get_daily_contributions_count(username, day, token=token)
|
|
2235
|
+
state["consecutive_failures"] = 0
|
|
2236
|
+
state["last_error"] = None
|
|
2237
|
+
except Exception as e:
|
|
2238
|
+
state["consecutive_failures"] = state.get("consecutive_failures", 0) + 1
|
|
2239
|
+
state["last_error"] = f"{type(e).__name__}: {e}"
|
|
2240
|
+
error_notify = state["consecutive_failures"] >= fail_threshold
|
|
2241
|
+
return False, state.get("count", 0), error_notify
|
|
2242
|
+
|
|
2243
|
+
prev_day = state.get("day")
|
|
2244
|
+
prev_cnt = state.get("count")
|
|
2245
|
+
|
|
2246
|
+
# New day -> reset baseline silently
|
|
2247
|
+
if prev_day != day:
|
|
2248
|
+
state["day"] = day
|
|
2249
|
+
state["count"] = curr
|
|
2250
|
+
state["prev_count"] = curr
|
|
2251
|
+
return False, curr, False # no notify on rollover
|
|
2252
|
+
|
|
2253
|
+
# Same day -> notify if change >= threshold
|
|
2254
|
+
if prev_cnt is not None and abs(curr - prev_cnt) >= min_delta:
|
|
2255
|
+
state["prev_count"] = prev_cnt
|
|
2256
|
+
state["count"] = curr
|
|
2257
|
+
return True, curr, False
|
|
2258
|
+
|
|
2259
|
+
# No change
|
|
2260
|
+
state["count"] = curr
|
|
2261
|
+
return False, curr, False
|
|
2262
|
+
|
|
2263
|
+
|
|
2264
|
+
# Monitors activity of the specified GitHub user
|
|
1811
2265
|
def github_monitor_user(user, csv_file_name):
|
|
1812
2266
|
|
|
1813
2267
|
try:
|
|
@@ -1824,6 +2278,12 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1824
2278
|
events = []
|
|
1825
2279
|
repos_list = []
|
|
1826
2280
|
event_date: datetime | None = None
|
|
2281
|
+
blocked = None
|
|
2282
|
+
public = False
|
|
2283
|
+
contrib_state = {}
|
|
2284
|
+
contrib_curr = 0
|
|
2285
|
+
|
|
2286
|
+
print("Sneaking into GitHub like a ninja ...")
|
|
1827
2287
|
|
|
1828
2288
|
try:
|
|
1829
2289
|
auth = Auth.Token(GITHUB_TOKEN)
|
|
@@ -1847,21 +2307,37 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1847
2307
|
|
|
1848
2308
|
followers_count = g_user.followers
|
|
1849
2309
|
followings_count = g_user.following
|
|
1850
|
-
repos_count = g_user.public_repos
|
|
1851
2310
|
|
|
1852
2311
|
followers_list = g_user.get_followers()
|
|
1853
2312
|
followings_list = g_user.get_following()
|
|
1854
|
-
|
|
2313
|
+
|
|
2314
|
+
if GET_ALL_REPOS:
|
|
2315
|
+
repos_list = g_user.get_repos()
|
|
2316
|
+
repos_count = g_user.public_repos
|
|
2317
|
+
else:
|
|
2318
|
+
repos_list = [repo for repo in g_user.get_repos(type='owner') if not repo.fork and repo.owner.login == user_login]
|
|
2319
|
+
repos_count = len(repos_list)
|
|
1855
2320
|
|
|
1856
2321
|
starred_list = g_user.get_starred()
|
|
1857
2322
|
starred_count = starred_list.totalCount
|
|
1858
2323
|
|
|
2324
|
+
public = is_profile_public(g, user)
|
|
2325
|
+
blocked = is_blocked_by(user) if public else None
|
|
2326
|
+
|
|
2327
|
+
if TRACK_CONTRIB_CHANGES:
|
|
2328
|
+
contrib_curr = get_daily_contributions_count(user, today_local(), token=GITHUB_TOKEN)
|
|
2329
|
+
contrib_state = {
|
|
2330
|
+
"day": today_local(),
|
|
2331
|
+
"count": contrib_curr,
|
|
2332
|
+
"prev_count": contrib_curr
|
|
2333
|
+
}
|
|
2334
|
+
|
|
1859
2335
|
if not DO_NOT_MONITOR_GITHUB_EVENTS:
|
|
1860
2336
|
events = list(islice(g_user.get_events(), EVENTS_NUMBER))
|
|
1861
2337
|
available_events = len(events)
|
|
1862
2338
|
|
|
1863
2339
|
except Exception as e:
|
|
1864
|
-
print(f"* Error: {e}")
|
|
2340
|
+
print(f"\n* Error: {e}")
|
|
1865
2341
|
sys.exit(1)
|
|
1866
2342
|
|
|
1867
2343
|
last_event_id = 0
|
|
@@ -1879,7 +2355,7 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1879
2355
|
if last_event_id:
|
|
1880
2356
|
last_event_ts = newest.created_at
|
|
1881
2357
|
except Exception as e:
|
|
1882
|
-
print(f"* Cannot get event IDs / timestamps: {e}\n")
|
|
2358
|
+
print(f"\n* Cannot get event IDs / timestamps: {e}\n")
|
|
1883
2359
|
pass
|
|
1884
2360
|
|
|
1885
2361
|
followers_old_count = followers_count
|
|
@@ -1893,6 +2369,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1893
2369
|
company_old = company
|
|
1894
2370
|
email_old = email
|
|
1895
2371
|
blog_old = blog
|
|
2372
|
+
blocked_old = blocked
|
|
2373
|
+
public_old = public
|
|
1896
2374
|
|
|
1897
2375
|
last_event_id_old = last_event_id
|
|
1898
2376
|
last_event_ts_old = last_event_ts
|
|
@@ -1902,7 +2380,7 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1902
2380
|
if user_myself_name:
|
|
1903
2381
|
user_myself_name_str += f" ({user_myself_name})"
|
|
1904
2382
|
|
|
1905
|
-
print(f"
|
|
2383
|
+
print(f"\nToken belongs to:\t\t{user_myself_name_str}" + f"\n\t\t\t\t[ {user_myself_url} ]" if user_myself_url else "")
|
|
1906
2384
|
|
|
1907
2385
|
user_name_str = user_login
|
|
1908
2386
|
if user_name:
|
|
@@ -1923,6 +2401,9 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1923
2401
|
if blog:
|
|
1924
2402
|
print(f"Blog URL:\t\t\t{blog}")
|
|
1925
2403
|
|
|
2404
|
+
print(f"\nPublic profile:\t\t\t{'Yes' if public else 'No'}")
|
|
2405
|
+
print(f"Blocked by the user:\t\t{'Unknown' if blocked is None else ('Yes' if blocked else 'No')}")
|
|
2406
|
+
|
|
1926
2407
|
print(f"\nAccount creation date:\t\t{get_date_from_ts(account_created_date)} ({calculate_timespan(int(time.time()), account_created_date, show_seconds=False)} ago)")
|
|
1927
2408
|
print(f"Account updated date:\t\t{get_date_from_ts(account_updated_date)} ({calculate_timespan(int(time.time()), account_updated_date, show_seconds=False)} ago)")
|
|
1928
2409
|
account_updated_date_old = account_updated_date
|
|
@@ -1931,6 +2412,9 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1931
2412
|
print(f"Followings:\t\t\t{followings_count}")
|
|
1932
2413
|
print(f"Repositories:\t\t\t{repos_count}")
|
|
1933
2414
|
print(f"Starred repos:\t\t\t{starred_count}")
|
|
2415
|
+
if TRACK_CONTRIB_CHANGES:
|
|
2416
|
+
print(f"Today's contributions:\t\t{contrib_curr}")
|
|
2417
|
+
|
|
1934
2418
|
if not DO_NOT_MONITOR_GITHUB_EVENTS:
|
|
1935
2419
|
print(f"Available events:\t\t{available_events}{'+' if available_events == EVENTS_NUMBER else ''}")
|
|
1936
2420
|
|
|
@@ -1941,9 +2425,31 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1941
2425
|
|
|
1942
2426
|
list_of_repos = []
|
|
1943
2427
|
if repos_list and TRACK_REPOS_CHANGES:
|
|
2428
|
+
# Filter repos for detailed monitoring only (keep full repos_list for profile change detection)
|
|
2429
|
+
repos_list_filtered = repos_list
|
|
2430
|
+
if 'ALL' not in REPOS_TO_MONITOR:
|
|
2431
|
+
repos_list_filtered = []
|
|
2432
|
+
for repo in repos_list:
|
|
2433
|
+
# Check if repo matches any entry in REPOS_TO_MONITOR
|
|
2434
|
+
should_monitor = False
|
|
2435
|
+
for monitor_entry in REPOS_TO_MONITOR:
|
|
2436
|
+
if '/' in monitor_entry:
|
|
2437
|
+
# Format: 'user/repo_name' - check if user matches and repo matches
|
|
2438
|
+
monitor_user, monitor_repo = monitor_entry.split('/', 1)
|
|
2439
|
+
if monitor_user == user_login and monitor_repo == repo.name:
|
|
2440
|
+
should_monitor = True
|
|
2441
|
+
break
|
|
2442
|
+
else:
|
|
2443
|
+
# Format: just 'repo_name' (from CLI) - check if repo name matches for current user
|
|
2444
|
+
if monitor_entry == repo.name and repo.owner.login == user_login:
|
|
2445
|
+
should_monitor = True
|
|
2446
|
+
break
|
|
2447
|
+
if should_monitor:
|
|
2448
|
+
repos_list_filtered.append(repo)
|
|
2449
|
+
|
|
1944
2450
|
print("Processing list of public repositories (be patient, it might take a while) ...")
|
|
1945
2451
|
try:
|
|
1946
|
-
list_of_repos = github_process_repos(
|
|
2452
|
+
list_of_repos = github_process_repos(repos_list_filtered)
|
|
1947
2453
|
except Exception as e:
|
|
1948
2454
|
print(f"* Cannot process list of public repositories: {e}")
|
|
1949
2455
|
print_cur_ts("\nTimestamp:\t\t\t")
|
|
@@ -1983,7 +2489,7 @@ def github_monitor_user(user, csv_file_name):
|
|
|
1983
2489
|
alive_counter = 0
|
|
1984
2490
|
email_sent = False
|
|
1985
2491
|
|
|
1986
|
-
#
|
|
2492
|
+
# Primary loop
|
|
1987
2493
|
while True:
|
|
1988
2494
|
|
|
1989
2495
|
try:
|
|
@@ -2031,8 +2537,13 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2031
2537
|
followers_old, followers_old_count = handle_profile_change("Followers", followers_old_count, followers_count, followers_old, followers_raw, user, csv_file_name, field="login")
|
|
2032
2538
|
|
|
2033
2539
|
# Changed public repositories
|
|
2034
|
-
|
|
2035
|
-
|
|
2540
|
+
if GET_ALL_REPOS:
|
|
2541
|
+
repos_raw = list(gh_call(g_user.get_repos)())
|
|
2542
|
+
repos_count = gh_call(lambda: g_user.public_repos)()
|
|
2543
|
+
else:
|
|
2544
|
+
repos_raw = list(gh_call(lambda: [repo for repo in g_user.get_repos(type='owner') if not repo.fork and repo.owner.login == user_login])())
|
|
2545
|
+
repos_count = len(repos_raw)
|
|
2546
|
+
|
|
2036
2547
|
if repos_raw is not None and repos_count is not None:
|
|
2037
2548
|
repos_old, repos_old_count = handle_profile_change("Repos", repos_old_count, repos_count, repos_old, repos_raw, user, csv_file_name, field="name")
|
|
2038
2549
|
|
|
@@ -2043,6 +2554,36 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2043
2554
|
starred_count = starred_raw.totalCount
|
|
2044
2555
|
starred_old, starred_old_count = handle_profile_change("Starred Repos", starred_old_count, starred_count, starred_old, starred_list, user, csv_file_name, field="full_name")
|
|
2045
2556
|
|
|
2557
|
+
# Changed contributions in a day
|
|
2558
|
+
if TRACK_CONTRIB_CHANGES:
|
|
2559
|
+
contrib_notify, contrib_curr, contrib_error_notify = check_daily_contribs(user, GITHUB_TOKEN, contrib_state, min_delta=1, fail_threshold=3)
|
|
2560
|
+
if contrib_error_notify and ERROR_NOTIFICATION:
|
|
2561
|
+
failures = contrib_state.get("consecutive_failures", 0)
|
|
2562
|
+
last_err = contrib_state.get("last_error", "Unknown error")
|
|
2563
|
+
err_msg = f"Error: GitHub daily contributions check failed {failures} times. Last error: {last_err}\n"
|
|
2564
|
+
print(err_msg)
|
|
2565
|
+
send_email(f"GitHub monitor errors for {user}", err_msg + get_cur_ts(nl_ch + "Timestamp: "), "", SMTP_SSL)
|
|
2566
|
+
|
|
2567
|
+
if contrib_notify:
|
|
2568
|
+
contrib_old = contrib_state.get("prev_count")
|
|
2569
|
+
print(f"* Daily contributions changed for user {user} on {get_short_date_from_ts(contrib_state['day'], show_hour=False)} from {contrib_old} to {contrib_curr}!\n")
|
|
2570
|
+
|
|
2571
|
+
try:
|
|
2572
|
+
if csv_file_name:
|
|
2573
|
+
write_csv_entry(csv_file_name, now_local_naive(), "Daily Contribs", user, contrib_old, contrib_curr)
|
|
2574
|
+
except Exception as e:
|
|
2575
|
+
print(f"* Error: {e}")
|
|
2576
|
+
|
|
2577
|
+
m_subject = f"GitHub user {user} daily contributions changed from {contrib_old} to {contrib_curr}!"
|
|
2578
|
+
m_body = (f"GitHub user {user} daily contributions changed on {get_short_date_from_ts(contrib_state['day'], show_hour=False)} from {contrib_old} to {contrib_curr}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}")
|
|
2579
|
+
|
|
2580
|
+
if CONTRIB_NOTIFICATION:
|
|
2581
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2582
|
+
send_email(m_subject, m_body, "", SMTP_SSL)
|
|
2583
|
+
|
|
2584
|
+
print(f"Check interval:\t\t\t{display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)})")
|
|
2585
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
2586
|
+
|
|
2046
2587
|
# Changed bio
|
|
2047
2588
|
bio = gh_call(lambda: g_user.bio)()
|
|
2048
2589
|
if bio is not None and bio != bio_old:
|
|
@@ -2056,8 +2597,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2056
2597
|
except Exception as e:
|
|
2057
2598
|
print(f"* Error: {e}")
|
|
2058
2599
|
|
|
2059
|
-
m_subject = f"
|
|
2060
|
-
m_body = f"
|
|
2600
|
+
m_subject = f"GitHub user {user} bio has changed!"
|
|
2601
|
+
m_body = f"GitHub user {user} bio has changed\n\nOld bio:\n\n{bio_old}\n\nNew bio:\n\n{bio}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2061
2602
|
|
|
2062
2603
|
if PROFILE_NOTIFICATION:
|
|
2063
2604
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2080,8 +2621,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2080
2621
|
except Exception as e:
|
|
2081
2622
|
print(f"* Error: {e}")
|
|
2082
2623
|
|
|
2083
|
-
m_subject = f"
|
|
2084
|
-
m_body = f"
|
|
2624
|
+
m_subject = f"GitHub user {user} location has changed!"
|
|
2625
|
+
m_body = f"GitHub user {user} location has changed\n\nOld location: {location_old}\n\nNew location: {location}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2085
2626
|
|
|
2086
2627
|
if PROFILE_NOTIFICATION:
|
|
2087
2628
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2104,8 +2645,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2104
2645
|
except Exception as e:
|
|
2105
2646
|
print(f"* Error: {e}")
|
|
2106
2647
|
|
|
2107
|
-
m_subject = f"
|
|
2108
|
-
m_body = f"
|
|
2648
|
+
m_subject = f"GitHub user {user} name has changed!"
|
|
2649
|
+
m_body = f"GitHub user {user} name has changed\n\nOld user name: {user_name_old}\n\nNew user name: {user_name}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2109
2650
|
|
|
2110
2651
|
if PROFILE_NOTIFICATION:
|
|
2111
2652
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2128,8 +2669,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2128
2669
|
except Exception as e:
|
|
2129
2670
|
print(f"* Error: {e}")
|
|
2130
2671
|
|
|
2131
|
-
m_subject = f"
|
|
2132
|
-
m_body = f"
|
|
2672
|
+
m_subject = f"GitHub user {user} company has changed!"
|
|
2673
|
+
m_body = f"GitHub user {user} company has changed\n\nOld company: {company_old}\n\nNew company: {company}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2133
2674
|
|
|
2134
2675
|
if PROFILE_NOTIFICATION:
|
|
2135
2676
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2152,8 +2693,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2152
2693
|
except Exception as e:
|
|
2153
2694
|
print(f"* Error: {e}")
|
|
2154
2695
|
|
|
2155
|
-
m_subject = f"
|
|
2156
|
-
m_body = f"
|
|
2696
|
+
m_subject = f"GitHub user {user} email has changed!"
|
|
2697
|
+
m_body = f"GitHub user {user} email has changed\n\nOld email: {email_old}\n\nNew email: {email}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2157
2698
|
|
|
2158
2699
|
if PROFILE_NOTIFICATION:
|
|
2159
2700
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2176,8 +2717,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2176
2717
|
except Exception as e:
|
|
2177
2718
|
print(f"* Error: {e}")
|
|
2178
2719
|
|
|
2179
|
-
m_subject = f"
|
|
2180
|
-
m_body = f"
|
|
2720
|
+
m_subject = f"GitHub user {user} blog URL has changed!"
|
|
2721
|
+
m_body = f"GitHub user {user} blog URL has changed\n\nOld blog URL: {blog_old}\n\nNew blog URL: {blog}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2181
2722
|
|
|
2182
2723
|
if PROFILE_NOTIFICATION:
|
|
2183
2724
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2200,8 +2741,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2200
2741
|
except Exception as e:
|
|
2201
2742
|
print(f"* Error: {e}")
|
|
2202
2743
|
|
|
2203
|
-
m_subject = f"
|
|
2204
|
-
m_body = f"
|
|
2744
|
+
m_subject = f"GitHub user {user} account has been updated! (after {calculate_timespan(account_updated_date, account_updated_date_old, show_seconds=False, granularity=2)})"
|
|
2745
|
+
m_body = f"GitHub user {user} account has been updated (after {calculate_timespan(account_updated_date, account_updated_date_old, show_seconds=False, granularity=2)})\n\nOld account update date: {get_date_from_ts(account_updated_date_old)}\n\nNew account update date: {get_date_from_ts(account_updated_date)}\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2205
2746
|
|
|
2206
2747
|
if PROFILE_NOTIFICATION:
|
|
2207
2748
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2211,14 +2752,97 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2211
2752
|
print(f"Check interval:\t\t\t{display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)})")
|
|
2212
2753
|
print_cur_ts("Timestamp:\t\t\t")
|
|
2213
2754
|
|
|
2755
|
+
# Profile visibility changed
|
|
2756
|
+
public = is_profile_public(g, user)
|
|
2757
|
+
if public != public_old:
|
|
2758
|
+
|
|
2759
|
+
def _get_profile_status(public):
|
|
2760
|
+
return "public" if public else "private"
|
|
2761
|
+
|
|
2762
|
+
print(f"* User {user} has changed profile visibility to '{_get_profile_status(public)}' !\n")
|
|
2763
|
+
|
|
2764
|
+
try:
|
|
2765
|
+
if csv_file_name:
|
|
2766
|
+
write_csv_entry(csv_file_name, now_local_naive(), "Profile Visibility", user, _get_profile_status(public_old), _get_profile_status(public))
|
|
2767
|
+
except Exception as e:
|
|
2768
|
+
print(f"* Error: {e}")
|
|
2769
|
+
|
|
2770
|
+
m_subject = f"GitHub user {user} has changed profile visibility to '{_get_profile_status(public)}' !"
|
|
2771
|
+
m_body = f"GitHub user {user} has changed profile visibility to '{_get_profile_status(public)}' !\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2772
|
+
|
|
2773
|
+
if PROFILE_NOTIFICATION:
|
|
2774
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2775
|
+
send_email(m_subject, m_body, "", SMTP_SSL)
|
|
2776
|
+
|
|
2777
|
+
public_old = public
|
|
2778
|
+
print(f"Check interval:\t\t\t{display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)})")
|
|
2779
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
2780
|
+
|
|
2781
|
+
# Blocked status changed
|
|
2782
|
+
blocked = is_blocked_by(user) if public else None
|
|
2783
|
+
|
|
2784
|
+
if blocked is not None and blocked_old is None:
|
|
2785
|
+
blocked_old = blocked
|
|
2786
|
+
|
|
2787
|
+
elif None not in (blocked_old, blocked) and blocked != blocked_old:
|
|
2788
|
+
|
|
2789
|
+
def _get_blocked_status(blocked, public):
|
|
2790
|
+
return 'Unknown' if blocked is None else ('Yes' if blocked else 'No')
|
|
2791
|
+
|
|
2792
|
+
print(f"* User {user} has {'blocked' if blocked else 'unblocked'} you!\n")
|
|
2793
|
+
|
|
2794
|
+
try:
|
|
2795
|
+
if csv_file_name:
|
|
2796
|
+
write_csv_entry(csv_file_name, now_local_naive(), "Block Status", user, _get_blocked_status(blocked_old, public), _get_blocked_status(blocked, public))
|
|
2797
|
+
except Exception as e:
|
|
2798
|
+
print(f"* Error: {e}")
|
|
2799
|
+
|
|
2800
|
+
m_subject = f"GitHub user {user} has {'blocked' if blocked else 'unblocked'} you!"
|
|
2801
|
+
m_body = f"GitHub user {user} has {'blocked' if blocked else 'unblocked'} you!\n\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2802
|
+
|
|
2803
|
+
if PROFILE_NOTIFICATION:
|
|
2804
|
+
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
2805
|
+
send_email(m_subject, m_body, "", SMTP_SSL)
|
|
2806
|
+
|
|
2807
|
+
blocked_old = blocked
|
|
2808
|
+
print(f"Check interval:\t\t\t{display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)})")
|
|
2809
|
+
print_cur_ts("Timestamp:\t\t\t")
|
|
2810
|
+
|
|
2214
2811
|
list_of_repos = []
|
|
2215
2812
|
|
|
2216
2813
|
# Changed repos details
|
|
2217
2814
|
if TRACK_REPOS_CHANGES:
|
|
2218
|
-
|
|
2219
|
-
if
|
|
2815
|
+
|
|
2816
|
+
if GET_ALL_REPOS:
|
|
2817
|
+
repos_list = gh_call(g_user.get_repos)()
|
|
2818
|
+
else:
|
|
2819
|
+
repos_list = gh_call(lambda: [repo for repo in g_user.get_repos(type='owner') if not repo.fork and repo.owner.login == user_login])()
|
|
2820
|
+
|
|
2821
|
+
# Filter repos for detailed monitoring only (keep full repos_list for profile change detection)
|
|
2822
|
+
repos_list_filtered = repos_list
|
|
2823
|
+
if repos_list is not None and 'ALL' not in REPOS_TO_MONITOR:
|
|
2824
|
+
repos_list_filtered = []
|
|
2825
|
+
for repo in repos_list:
|
|
2826
|
+
# Check if repo matches any entry in REPOS_TO_MONITOR
|
|
2827
|
+
should_monitor = False
|
|
2828
|
+
for monitor_entry in REPOS_TO_MONITOR:
|
|
2829
|
+
if '/' in monitor_entry:
|
|
2830
|
+
# Format: 'user/repo_name' - check if user matches and repo matches
|
|
2831
|
+
monitor_user, monitor_repo = monitor_entry.split('/', 1)
|
|
2832
|
+
if monitor_user == user_login and monitor_repo == repo.name:
|
|
2833
|
+
should_monitor = True
|
|
2834
|
+
break
|
|
2835
|
+
else:
|
|
2836
|
+
# Format: just 'repo_name' (from CLI) - check if repo name matches for current user
|
|
2837
|
+
if monitor_entry == repo.name and repo.owner.login == user_login:
|
|
2838
|
+
should_monitor = True
|
|
2839
|
+
break
|
|
2840
|
+
if should_monitor:
|
|
2841
|
+
repos_list_filtered.append(repo)
|
|
2842
|
+
|
|
2843
|
+
if repos_list_filtered is not None:
|
|
2220
2844
|
try:
|
|
2221
|
-
list_of_repos = github_process_repos(
|
|
2845
|
+
list_of_repos = github_process_repos(repos_list_filtered)
|
|
2222
2846
|
list_of_repos_ok = True
|
|
2223
2847
|
except Exception as e:
|
|
2224
2848
|
list_of_repos = list_of_repos_old
|
|
@@ -2269,7 +2893,7 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2269
2893
|
write_csv_entry(csv_file_name, now_local_naive(), "Repo Update Date", r_name, convert_to_local_naive(r_update_old), convert_to_local_naive(r_update))
|
|
2270
2894
|
except Exception as e:
|
|
2271
2895
|
print(f"* Error: {e}")
|
|
2272
|
-
m_subject = f"
|
|
2896
|
+
m_subject = f"GitHub user {user} repo '{r_name}' update date has changed ! (after {calculate_timespan(r_update, r_update_old, show_seconds=False, granularity=2)})"
|
|
2273
2897
|
m_body = f"{r_message}\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2274
2898
|
if REPO_UPDATE_DATE_NOTIFICATION:
|
|
2275
2899
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2301,7 +2925,7 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2301
2925
|
write_csv_entry(csv_file_name, now_local_naive(), "Repo Description", r_name, r_descr_old, r_descr)
|
|
2302
2926
|
except Exception as e:
|
|
2303
2927
|
print(f"* Error: {e}")
|
|
2304
|
-
m_subject = f"
|
|
2928
|
+
m_subject = f"GitHub user {user} repo '{r_name}' description has changed !"
|
|
2305
2929
|
m_body = f"{r_message}\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2306
2930
|
if REPO_NOTIFICATION:
|
|
2307
2931
|
print(f"Sending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2311,7 +2935,7 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2311
2935
|
|
|
2312
2936
|
list_of_repos_old = list_of_repos
|
|
2313
2937
|
|
|
2314
|
-
# New
|
|
2938
|
+
# New GitHub events
|
|
2315
2939
|
if not DO_NOT_MONITOR_GITHUB_EVENTS:
|
|
2316
2940
|
events = list(gh_call(lambda: list(islice(g_user.get_events(), EVENTS_NUMBER)))())
|
|
2317
2941
|
if events is not None:
|
|
@@ -2366,8 +2990,8 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2366
2990
|
except Exception as e:
|
|
2367
2991
|
print(f"* Error: {e}")
|
|
2368
2992
|
|
|
2369
|
-
m_subject = f"
|
|
2370
|
-
m_body = f"
|
|
2993
|
+
m_subject = f"GitHub user {user} has new {event.type} (repo: {repo_name})"
|
|
2994
|
+
m_body = f"GitHub user {user} has new {event.type} event\n\n{event_text}\nCheck interval: {display_time(GITHUB_CHECK_INTERVAL)} ({get_range_of_dates_from_tss(int(time.time()) - GITHUB_CHECK_INTERVAL, int(time.time()), short=True)}){get_cur_ts(nl_ch + 'Timestamp: ')}"
|
|
2371
2995
|
|
|
2372
2996
|
if EVENT_NOTIFICATION:
|
|
2373
2997
|
print(f"\nSending email notification to {RECEIVER_EMAIL}")
|
|
@@ -2392,7 +3016,7 @@ def github_monitor_user(user, csv_file_name):
|
|
|
2392
3016
|
|
|
2393
3017
|
|
|
2394
3018
|
def main():
|
|
2395
|
-
global CLI_CONFIG_PATH, DOTENV_FILE, LOCAL_TIMEZONE, LIVENESS_CHECK_COUNTER, GITHUB_TOKEN, GITHUB_API_URL, CSV_FILE, DISABLE_LOGGING, GITHUB_LOGFILE, PROFILE_NOTIFICATION, EVENT_NOTIFICATION, REPO_NOTIFICATION, REPO_UPDATE_DATE_NOTIFICATION, ERROR_NOTIFICATION, GITHUB_CHECK_INTERVAL, SMTP_PASSWORD, stdout_bck, DO_NOT_MONITOR_GITHUB_EVENTS, TRACK_REPOS_CHANGES
|
|
3019
|
+
global CLI_CONFIG_PATH, DOTENV_FILE, LOCAL_TIMEZONE, LIVENESS_CHECK_COUNTER, GITHUB_TOKEN, GITHUB_API_URL, CSV_FILE, DISABLE_LOGGING, GITHUB_LOGFILE, PROFILE_NOTIFICATION, EVENT_NOTIFICATION, REPO_NOTIFICATION, REPO_UPDATE_DATE_NOTIFICATION, ERROR_NOTIFICATION, GITHUB_CHECK_INTERVAL, SMTP_PASSWORD, stdout_bck, DO_NOT_MONITOR_GITHUB_EVENTS, TRACK_REPOS_CHANGES, REPOS_TO_MONITOR, GET_ALL_REPOS, CONTRIB_NOTIFICATION, TRACK_CONTRIB_CHANGES
|
|
2396
3020
|
|
|
2397
3021
|
if "--generate-config" in sys.argv:
|
|
2398
3022
|
print(CONFIG_BLOCK.strip("\n"))
|
|
@@ -2409,7 +3033,7 @@ def main():
|
|
|
2409
3033
|
|
|
2410
3034
|
clear_screen(CLEAR_SCREEN)
|
|
2411
3035
|
|
|
2412
|
-
print(f"
|
|
3036
|
+
print(f"GitHub Monitoring Tool v{VERSION}\n")
|
|
2413
3037
|
|
|
2414
3038
|
parser = argparse.ArgumentParser(
|
|
2415
3039
|
prog="github_monitor",
|
|
@@ -2499,6 +3123,13 @@ def main():
|
|
|
2499
3123
|
default=None,
|
|
2500
3124
|
help="Email when user's repositories update date changes"
|
|
2501
3125
|
)
|
|
3126
|
+
notify.add_argument(
|
|
3127
|
+
"-y", "--notify-daily-contribs",
|
|
3128
|
+
dest="notify_daily_contribs",
|
|
3129
|
+
action="store_true",
|
|
3130
|
+
default=None,
|
|
3131
|
+
help="Email when user's daily contributions count changes"
|
|
3132
|
+
)
|
|
2502
3133
|
notify.add_argument(
|
|
2503
3134
|
"-e", "--no-error-notify",
|
|
2504
3135
|
dest="notify_errors",
|
|
@@ -2577,6 +3208,13 @@ def main():
|
|
|
2577
3208
|
default=None,
|
|
2578
3209
|
help="Disable event monitoring"
|
|
2579
3210
|
)
|
|
3211
|
+
opts.add_argument(
|
|
3212
|
+
"-a", "--get-all-repos",
|
|
3213
|
+
dest="get_all_repos",
|
|
3214
|
+
action="store_true",
|
|
3215
|
+
default=None,
|
|
3216
|
+
help="Fetch all user repos (owned, forks, collaborations)"
|
|
3217
|
+
)
|
|
2580
3218
|
opts.add_argument(
|
|
2581
3219
|
"-b", "--csv-file",
|
|
2582
3220
|
dest="csv_file",
|
|
@@ -2591,6 +3229,20 @@ def main():
|
|
|
2591
3229
|
default=None,
|
|
2592
3230
|
help="Disable logging to github_monitor_<username>.log"
|
|
2593
3231
|
)
|
|
3232
|
+
opts.add_argument(
|
|
3233
|
+
"-m", "--track-contribs-changes",
|
|
3234
|
+
dest="track_contribs_changes",
|
|
3235
|
+
action="store_true",
|
|
3236
|
+
default=None,
|
|
3237
|
+
help="Track user's daily contributions count and log changes"
|
|
3238
|
+
)
|
|
3239
|
+
opts.add_argument(
|
|
3240
|
+
"--repos",
|
|
3241
|
+
dest="repos",
|
|
3242
|
+
metavar="REPO_LIST",
|
|
3243
|
+
type=str,
|
|
3244
|
+
help="Comma-separated list of repository names to monitor (only when -j/--track-repos-changes is enabled). Overrides REPOS_TO_MONITOR config. Example: --repos \"repo1,repo2,repo3\""
|
|
3245
|
+
)
|
|
2594
3246
|
|
|
2595
3247
|
args = parser.parse_args()
|
|
2596
3248
|
|
|
@@ -2694,6 +3346,9 @@ def main():
|
|
|
2694
3346
|
print("* Error: GITHUB_API_URL (-x / --github_url) value is empty")
|
|
2695
3347
|
sys.exit(1)
|
|
2696
3348
|
|
|
3349
|
+
if args.get_all_repos is True:
|
|
3350
|
+
GET_ALL_REPOS = True
|
|
3351
|
+
|
|
2697
3352
|
if args.list_followers_and_followings:
|
|
2698
3353
|
try:
|
|
2699
3354
|
github_print_followers_and_followings(args.username)
|
|
@@ -2777,12 +3432,25 @@ def main():
|
|
|
2777
3432
|
if args.notify_repo_update_date is True:
|
|
2778
3433
|
REPO_UPDATE_DATE_NOTIFICATION = True
|
|
2779
3434
|
|
|
3435
|
+
if args.notify_daily_contribs is True:
|
|
3436
|
+
CONTRIB_NOTIFICATION = True
|
|
3437
|
+
|
|
2780
3438
|
if args.notify_errors is False:
|
|
2781
3439
|
ERROR_NOTIFICATION = False
|
|
2782
3440
|
|
|
2783
3441
|
if args.track_repos_changes is True:
|
|
2784
3442
|
TRACK_REPOS_CHANGES = True
|
|
2785
3443
|
|
|
3444
|
+
if args.repos is not None:
|
|
3445
|
+
if not TRACK_REPOS_CHANGES:
|
|
3446
|
+
print("* Error: --repos requires -j/--track-repos-changes to be enabled")
|
|
3447
|
+
sys.exit(1)
|
|
3448
|
+
# Split comma-separated repo names and strip whitespace
|
|
3449
|
+
REPOS_TO_MONITOR = [repo.strip() for repo in args.repos.split(',') if repo.strip()]
|
|
3450
|
+
|
|
3451
|
+
if args.track_contribs_changes is True:
|
|
3452
|
+
TRACK_CONTRIB_CHANGES = True
|
|
3453
|
+
|
|
2786
3454
|
if args.no_monitor_events is True:
|
|
2787
3455
|
DO_NOT_MONITOR_GITHUB_EVENTS = True
|
|
2788
3456
|
|
|
@@ -2790,6 +3458,9 @@ def main():
|
|
|
2790
3458
|
REPO_NOTIFICATION = False
|
|
2791
3459
|
REPO_UPDATE_DATE_NOTIFICATION = False
|
|
2792
3460
|
|
|
3461
|
+
if not TRACK_CONTRIB_CHANGES:
|
|
3462
|
+
CONTRIB_NOTIFICATION = False
|
|
3463
|
+
|
|
2793
3464
|
if DO_NOT_MONITOR_GITHUB_EVENTS:
|
|
2794
3465
|
EVENT_NOTIFICATION = False
|
|
2795
3466
|
|
|
@@ -2798,13 +3469,16 @@ def main():
|
|
|
2798
3469
|
PROFILE_NOTIFICATION = False
|
|
2799
3470
|
REPO_NOTIFICATION = False
|
|
2800
3471
|
REPO_UPDATE_DATE_NOTIFICATION = False
|
|
3472
|
+
CONTRIB_NOTIFICATION = False
|
|
2801
3473
|
ERROR_NOTIFICATION = False
|
|
2802
3474
|
|
|
2803
|
-
print(f"*
|
|
2804
|
-
print(f"* Email notifications:\t\t[profile changes = {PROFILE_NOTIFICATION}] [new events = {EVENT_NOTIFICATION}]\n*\t\t\t\t[repos changes = {REPO_NOTIFICATION}] [repos update date = {REPO_UPDATE_DATE_NOTIFICATION}]\n*\t\t\t\t[errors = {ERROR_NOTIFICATION}]")
|
|
2805
|
-
print(f"*
|
|
3475
|
+
print(f"* GitHub polling interval:\t[ {display_time(GITHUB_CHECK_INTERVAL)} ]")
|
|
3476
|
+
print(f"* Email notifications:\t\t[profile changes = {PROFILE_NOTIFICATION}] [new events = {EVENT_NOTIFICATION}]\n*\t\t\t\t[repos changes = {REPO_NOTIFICATION}] [repos update date = {REPO_UPDATE_DATE_NOTIFICATION}]\n*\t\t\t\t[contrib changes = {CONTRIB_NOTIFICATION}] [errors = {ERROR_NOTIFICATION}]")
|
|
3477
|
+
print(f"* GitHub API URL:\t\t{GITHUB_API_URL}")
|
|
2806
3478
|
print(f"* Track repos changes:\t\t{TRACK_REPOS_CHANGES}")
|
|
2807
|
-
print(f"*
|
|
3479
|
+
print(f"* Track contrib changes:\t{TRACK_CONTRIB_CHANGES}")
|
|
3480
|
+
print(f"* Monitor GitHub events:\t{not DO_NOT_MONITOR_GITHUB_EVENTS}")
|
|
3481
|
+
print(f"* Get owned repos only:\t\t{not GET_ALL_REPOS}")
|
|
2808
3482
|
print(f"* Liveness check:\t\t{bool(LIVENESS_CHECK_INTERVAL)}" + (f" ({display_time(LIVENESS_CHECK_INTERVAL)})" if LIVENESS_CHECK_INTERVAL else ""))
|
|
2809
3483
|
print(f"* CSV logging enabled:\t\t{bool(CSV_FILE)}" + (f" ({CSV_FILE})" if CSV_FILE else ""))
|
|
2810
3484
|
print(f"* Output logging enabled:\t{not DISABLE_LOGGING}" + (f" ({FINAL_LOG_PATH})" if not DISABLE_LOGGING else ""))
|
|
@@ -2812,9 +3486,10 @@ def main():
|
|
|
2812
3486
|
print(f"* Dotenv file:\t\t\t{env_path or 'None'}")
|
|
2813
3487
|
print(f"* Local timezone:\t\t{LOCAL_TIMEZONE}")
|
|
2814
3488
|
|
|
2815
|
-
out = f"\nMonitoring
|
|
3489
|
+
out = f"\nMonitoring GitHub user {args.username}"
|
|
2816
3490
|
print(out)
|
|
2817
|
-
print("-" * len(out))
|
|
3491
|
+
# print("-" * len(out))
|
|
3492
|
+
print("─" * HORIZONTAL_LINE1)
|
|
2818
3493
|
|
|
2819
3494
|
# We define signal handlers only for Linux, Unix & MacOS since Windows has limited number of signals supported
|
|
2820
3495
|
if platform.system() != 'Windows':
|
|
@@ -2822,6 +3497,7 @@ def main():
|
|
|
2822
3497
|
signal.signal(signal.SIGUSR2, toggle_new_events_notifications_signal_handler)
|
|
2823
3498
|
signal.signal(signal.SIGCONT, toggle_repo_changes_notifications_signal_handler)
|
|
2824
3499
|
signal.signal(signal.SIGPIPE, toggle_repo_update_date_changes_notifications_signal_handler)
|
|
3500
|
+
signal.signal(signal.SIGURG, toggle_contrib_changes_notifications_signal_handler)
|
|
2825
3501
|
signal.signal(signal.SIGTRAP, increase_check_signal_handler)
|
|
2826
3502
|
signal.signal(signal.SIGABRT, decrease_check_signal_handler)
|
|
2827
3503
|
signal.signal(signal.SIGHUP, reload_secrets_signal_handler)
|