HackaProfile 0.1.0__tar.gz

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.
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: HackaProfile
3
+ Version: 0.1.0
4
+ Summary: A simple tool to automatically update Slack user profile based on their Hackatime/Wakatime heartbeat!
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: annotated-doc==0.0.4
8
+ Requires-Dist: blinker==1.9.0
9
+ Requires-Dist: certifi==2026.6.17
10
+ Requires-Dist: charset-normalizer==3.4.7
11
+ Requires-Dist: click==8.4.1
12
+ Requires-Dist: dotenv==0.9.9
13
+ Requires-Dist: Flask==3.1.3
14
+ Requires-Dist: idna==3.18
15
+ Requires-Dist: itsdangerous==2.2.0
16
+ Requires-Dist: jaraco.classes==3.4.0
17
+ Requires-Dist: jaraco.context==6.1.2
18
+ Requires-Dist: jaraco.functools==4.5.0
19
+ Requires-Dist: Jinja2==3.1.6
20
+ Requires-Dist: keyring==25.7.0
21
+ Requires-Dist: markdown-it-py==4.2.0
22
+ Requires-Dist: MarkupSafe==3.0.3
23
+ Requires-Dist: mdurl==0.1.2
24
+ Requires-Dist: more-itertools==11.1.0
25
+ Requires-Dist: prompt_toolkit==3.0.52
26
+ Requires-Dist: psutil==7.2.2
27
+ Requires-Dist: Pygments==2.20.0
28
+ Requires-Dist: pyperclip==1.11.0
29
+ Requires-Dist: python-dotenv==1.2.2
30
+ Requires-Dist: questionary==2.1.1
31
+ Requires-Dist: requests==2.34.2
32
+ Requires-Dist: rich==15.0.0
33
+ Requires-Dist: shellingham==1.5.4
34
+ Requires-Dist: typer==0.26.7
35
+ Requires-Dist: urllib3==2.7.0
36
+ Requires-Dist: wcwidth==0.8.1
37
+ Requires-Dist: Werkzeug==3.1.8
38
+ Requires-Dist: platformdirs==4.10.0
39
+
40
+ # HackaProfile
41
+ <img src="./assets/icon.png" width=30% alt="HackaProfile logo"></img>
42
+
43
+ ### A simple tool to automatically update Slack (more platforms *SOON*) user profile based on their Hackatime/Wakatime heartbeat!
44
+
45
+ ## Features
46
+ - FULLY local (no data leaves your device except for the API calls to update profile)
47
+ - FULLY customizable (see the Place holder variables section)
48
+ - More features #TODO ~~(I don't wanna write README.md >:3)~~
49
+
50
+ ## installation
51
+ > [!TIP]
52
+ > One line install does not work yet!
53
+ ### MacOS
54
+ `brew install hackaprofile`
55
+ ### Linux
56
+ Debian/Ubuntu:
57
+ `apt install hackaprofile`
58
+
59
+ ## Placeholder Variables
60
+ A key feature of HackaProfile is that it allows you to customise your profile however you like (just like how you would change it on Slack/other platforms) BUT it **also allows you to use dynamic values** (i.e. Placeholder variables).
61
+
62
+ Example
63
+ `I am typing {{language}} in {{project}} project` becomes `I am typing Python in HackaProfile project`
64
+
65
+ -`{{id}}` Some sort of Hackatime id, might be unique?
66
+ - `{{created_at}}` When was the Hackatime data last fetched
67
+ - `{{time}}` Current time in Unix Timestamp
68
+ - `{{category}}`: Category of the Hackatime action
69
+ - communicating
70
+ - ai coding
71
+ - coding
72
+ - writing docs
73
+ - `{{project}}` Hackatime project name
74
+ - `{{language}}` Shows the current language that that you are working on
75
+ - `{{editor}}` Current IDE/Editor
76
+ - `{{operating_system}}` The current OS (N.B for MacOS, it shows `darwin`)
77
+ - `{{entity}}` Path to the file workng on
78
+ - `{{machine}}` Current machine's hostname
79
+
@@ -0,0 +1,19 @@
1
+ README.md
2
+ pyproject.toml
3
+ HackaProfile.egg-info/PKG-INFO
4
+ HackaProfile.egg-info/SOURCES.txt
5
+ HackaProfile.egg-info/dependency_links.txt
6
+ HackaProfile.egg-info/entry_points.txt
7
+ HackaProfile.egg-info/requires.txt
8
+ HackaProfile.egg-info/top_level.txt
9
+ src/__init__.py
10
+ src/agent.py
11
+ src/backend.py
12
+ src/frontend.py
13
+ src/hackatimeOA.py
14
+ src/slackOA.py
15
+ src/configTemplate/agent.pid
16
+ src/configTemplate/hackaprofile.conf
17
+ src/configTemplate/hackatime.hackaprofile.conf
18
+ src/configTemplate/slack.hackaprofile.conf
19
+ src/logTemplate/agent.log
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ hackaprofile = src.frontend:app
@@ -0,0 +1,32 @@
1
+ annotated-doc==0.0.4
2
+ blinker==1.9.0
3
+ certifi==2026.6.17
4
+ charset-normalizer==3.4.7
5
+ click==8.4.1
6
+ dotenv==0.9.9
7
+ Flask==3.1.3
8
+ idna==3.18
9
+ itsdangerous==2.2.0
10
+ jaraco.classes==3.4.0
11
+ jaraco.context==6.1.2
12
+ jaraco.functools==4.5.0
13
+ Jinja2==3.1.6
14
+ keyring==25.7.0
15
+ markdown-it-py==4.2.0
16
+ MarkupSafe==3.0.3
17
+ mdurl==0.1.2
18
+ more-itertools==11.1.0
19
+ prompt_toolkit==3.0.52
20
+ psutil==7.2.2
21
+ Pygments==2.20.0
22
+ pyperclip==1.11.0
23
+ python-dotenv==1.2.2
24
+ questionary==2.1.1
25
+ requests==2.34.2
26
+ rich==15.0.0
27
+ shellingham==1.5.4
28
+ typer==0.26.7
29
+ urllib3==2.7.0
30
+ wcwidth==0.8.1
31
+ Werkzeug==3.1.8
32
+ platformdirs==4.10.0
@@ -0,0 +1,79 @@
1
+ Metadata-Version: 2.4
2
+ Name: HackaProfile
3
+ Version: 0.1.0
4
+ Summary: A simple tool to automatically update Slack user profile based on their Hackatime/Wakatime heartbeat!
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: annotated-doc==0.0.4
8
+ Requires-Dist: blinker==1.9.0
9
+ Requires-Dist: certifi==2026.6.17
10
+ Requires-Dist: charset-normalizer==3.4.7
11
+ Requires-Dist: click==8.4.1
12
+ Requires-Dist: dotenv==0.9.9
13
+ Requires-Dist: Flask==3.1.3
14
+ Requires-Dist: idna==3.18
15
+ Requires-Dist: itsdangerous==2.2.0
16
+ Requires-Dist: jaraco.classes==3.4.0
17
+ Requires-Dist: jaraco.context==6.1.2
18
+ Requires-Dist: jaraco.functools==4.5.0
19
+ Requires-Dist: Jinja2==3.1.6
20
+ Requires-Dist: keyring==25.7.0
21
+ Requires-Dist: markdown-it-py==4.2.0
22
+ Requires-Dist: MarkupSafe==3.0.3
23
+ Requires-Dist: mdurl==0.1.2
24
+ Requires-Dist: more-itertools==11.1.0
25
+ Requires-Dist: prompt_toolkit==3.0.52
26
+ Requires-Dist: psutil==7.2.2
27
+ Requires-Dist: Pygments==2.20.0
28
+ Requires-Dist: pyperclip==1.11.0
29
+ Requires-Dist: python-dotenv==1.2.2
30
+ Requires-Dist: questionary==2.1.1
31
+ Requires-Dist: requests==2.34.2
32
+ Requires-Dist: rich==15.0.0
33
+ Requires-Dist: shellingham==1.5.4
34
+ Requires-Dist: typer==0.26.7
35
+ Requires-Dist: urllib3==2.7.0
36
+ Requires-Dist: wcwidth==0.8.1
37
+ Requires-Dist: Werkzeug==3.1.8
38
+ Requires-Dist: platformdirs==4.10.0
39
+
40
+ # HackaProfile
41
+ <img src="./assets/icon.png" width=30% alt="HackaProfile logo"></img>
42
+
43
+ ### A simple tool to automatically update Slack (more platforms *SOON*) user profile based on their Hackatime/Wakatime heartbeat!
44
+
45
+ ## Features
46
+ - FULLY local (no data leaves your device except for the API calls to update profile)
47
+ - FULLY customizable (see the Place holder variables section)
48
+ - More features #TODO ~~(I don't wanna write README.md >:3)~~
49
+
50
+ ## installation
51
+ > [!TIP]
52
+ > One line install does not work yet!
53
+ ### MacOS
54
+ `brew install hackaprofile`
55
+ ### Linux
56
+ Debian/Ubuntu:
57
+ `apt install hackaprofile`
58
+
59
+ ## Placeholder Variables
60
+ A key feature of HackaProfile is that it allows you to customise your profile however you like (just like how you would change it on Slack/other platforms) BUT it **also allows you to use dynamic values** (i.e. Placeholder variables).
61
+
62
+ Example
63
+ `I am typing {{language}} in {{project}} project` becomes `I am typing Python in HackaProfile project`
64
+
65
+ -`{{id}}` Some sort of Hackatime id, might be unique?
66
+ - `{{created_at}}` When was the Hackatime data last fetched
67
+ - `{{time}}` Current time in Unix Timestamp
68
+ - `{{category}}`: Category of the Hackatime action
69
+ - communicating
70
+ - ai coding
71
+ - coding
72
+ - writing docs
73
+ - `{{project}}` Hackatime project name
74
+ - `{{language}}` Shows the current language that that you are working on
75
+ - `{{editor}}` Current IDE/Editor
76
+ - `{{operating_system}}` The current OS (N.B for MacOS, it shows `darwin`)
77
+ - `{{entity}}` Path to the file workng on
78
+ - `{{machine}}` Current machine's hostname
79
+
@@ -0,0 +1,40 @@
1
+ # HackaProfile
2
+ <img src="./assets/icon.png" width=30% alt="HackaProfile logo"></img>
3
+
4
+ ### A simple tool to automatically update Slack (more platforms *SOON*) user profile based on their Hackatime/Wakatime heartbeat!
5
+
6
+ ## Features
7
+ - FULLY local (no data leaves your device except for the API calls to update profile)
8
+ - FULLY customizable (see the Place holder variables section)
9
+ - More features #TODO ~~(I don't wanna write README.md >:3)~~
10
+
11
+ ## installation
12
+ > [!TIP]
13
+ > One line install does not work yet!
14
+ ### MacOS
15
+ `brew install hackaprofile`
16
+ ### Linux
17
+ Debian/Ubuntu:
18
+ `apt install hackaprofile`
19
+
20
+ ## Placeholder Variables
21
+ A key feature of HackaProfile is that it allows you to customise your profile however you like (just like how you would change it on Slack/other platforms) BUT it **also allows you to use dynamic values** (i.e. Placeholder variables).
22
+
23
+ Example
24
+ `I am typing {{language}} in {{project}} project` becomes `I am typing Python in HackaProfile project`
25
+
26
+ -`{{id}}` Some sort of Hackatime id, might be unique?
27
+ - `{{created_at}}` When was the Hackatime data last fetched
28
+ - `{{time}}` Current time in Unix Timestamp
29
+ - `{{category}}`: Category of the Hackatime action
30
+ - communicating
31
+ - ai coding
32
+ - coding
33
+ - writing docs
34
+ - `{{project}}` Hackatime project name
35
+ - `{{language}}` Shows the current language that that you are working on
36
+ - `{{editor}}` Current IDE/Editor
37
+ - `{{operating_system}}` The current OS (N.B for MacOS, it shows `darwin`)
38
+ - `{{entity}}` Path to the file workng on
39
+ - `{{machine}}` Current machine's hostname
40
+
@@ -0,0 +1,53 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "HackaProfile"
7
+ version = "0.1.0"
8
+ description = "A simple tool to automatically update Slack user profile based on their Hackatime/Wakatime heartbeat!"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = [
12
+ "annotated-doc==0.0.4",
13
+ "blinker==1.9.0",
14
+ "certifi==2026.6.17",
15
+ "charset-normalizer==3.4.7",
16
+ "click==8.4.1",
17
+ "dotenv==0.9.9",
18
+ "Flask==3.1.3",
19
+ "idna==3.18",
20
+ "itsdangerous==2.2.0",
21
+ "jaraco.classes==3.4.0",
22
+ "jaraco.context==6.1.2",
23
+ "jaraco.functools==4.5.0",
24
+ "Jinja2==3.1.6",
25
+ "keyring==25.7.0",
26
+ "markdown-it-py==4.2.0",
27
+ "MarkupSafe==3.0.3",
28
+ "mdurl==0.1.2",
29
+ "more-itertools==11.1.0",
30
+ "prompt_toolkit==3.0.52",
31
+ "psutil==7.2.2",
32
+ "Pygments==2.20.0",
33
+ "pyperclip==1.11.0",
34
+ "python-dotenv==1.2.2",
35
+ "questionary==2.1.1",
36
+ "requests==2.34.2",
37
+ "rich==15.0.0",
38
+ "shellingham==1.5.4",
39
+ "typer==0.26.7",
40
+ "urllib3==2.7.0",
41
+ "wcwidth==0.8.1",
42
+ "Werkzeug==3.1.8",
43
+ "platformdirs==4.10.0",
44
+ ]
45
+
46
+ [project.scripts]
47
+ hackaprofile = "src.frontend:app"
48
+
49
+ [tool.setuptools]
50
+ packages = ["src"]
51
+
52
+ [tool.setuptools.package-data]
53
+ src = ["configTemplate/*", "logTemplate/*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,3 @@
1
+ """HackaProfile - Automatically update Slack profile based on Hackatime/Wakatime heartbeat"""
2
+
3
+ __version__ = "0.0.1"
@@ -0,0 +1,106 @@
1
+ import time
2
+ try:
3
+ from . import backend
4
+ except ImportError:
5
+ import backend
6
+ import dotenv
7
+ import os
8
+ from pathlib import Path
9
+ import platformdirs
10
+
11
+ hackatime = backend.hackatime()
12
+ slack = backend.slack()
13
+
14
+
15
+ active_services = ["slack"]
16
+ interval = 20
17
+ HOME = Path.home()
18
+ CONFIG_DIR = Path(platformdirs.user_config_dir("hackaprofile"))
19
+ LOG_DIR = Path(platformdirs.user_log_dir("hackaprofile"))
20
+
21
+
22
+ # langauge = ""
23
+
24
+ pid = os.getpid()
25
+ with open(CONFIG_DIR / "agent.pid", "w") as f:
26
+ f.write(str(pid))
27
+ f.close()
28
+
29
+ def clean_value(value: str) -> str:
30
+ """
31
+ Makes the values clean for slack (Value passed for `name` contained unallowed special characters)
32
+ """
33
+ return value.replace("<", "").replace(">", "")
34
+
35
+ def parse_config(config: dict, map):
36
+ parsed_config = {}
37
+ for _, field in enumerate(config):
38
+ key = field
39
+ template: str = config[field]
40
+ # Replace {{key}} placeholders directly from the map.
41
+ for map_key, map_value in map.items():
42
+ template = template.replace(map_key, str(map_value))
43
+ try:
44
+ parsed_value = clean_value(template.format_map(map))
45
+
46
+ # if the config field is empty, don't append it
47
+ if parsed_value != "":
48
+ parsed_config[key] = parsed_value
49
+ except KeyError:
50
+ pass
51
+
52
+ # print(parsed)
53
+
54
+ return parsed_config
55
+
56
+ while True:
57
+ json = hackatime.fetch_hb()
58
+
59
+ # for _, field in enumerate(json):
60
+ # key = field
61
+ # value = json.get(field, "")
62
+
63
+ # project =
64
+
65
+
66
+ map = {
67
+ "{{id}}": json.get("id", ""),
68
+ "{{created_at}}": json.get("created_at", ""),
69
+ "{{time}}": json.get("time", ""),
70
+ "{{category}}": json.get("category", ""),
71
+ "{{project}}": json.get("project", ""),
72
+ "{{language}}": json.get("language", ""),
73
+ "{{editor}}": json.get("editor", ""),
74
+ "{{operating_system}}": json.get("operating_system", ""),
75
+ "{{machine}}": json.get("machine", ""),
76
+ "{{entity}}": json.get("entity", "")
77
+ }
78
+
79
+
80
+
81
+ print(f'Language: {map["{{language}}"]}')
82
+ print(json)
83
+
84
+ if "slack" in active_services:
85
+ print(slack.fetch_config())
86
+ config = parse_config(slack.fetch_config(), map)
87
+ print(config)
88
+
89
+ res = slack.set_profile(config)
90
+ print(res)
91
+ # for _, field in enumerate(slack_config):
92
+ # print((field, slack_config[field]))
93
+
94
+
95
+ # slack.set_profile(
96
+ # profile={
97
+ # "status_text": "test status text",
98
+ # "status_emoji": ":67:",
99
+ # "status_expiration": 0
100
+ # })
101
+ print("---")
102
+ time.sleep(interval)
103
+
104
+
105
+
106
+ #
@@ -0,0 +1,171 @@
1
+ try:
2
+ from . import hackatimeOA
3
+ from . import slackOA
4
+ except ImportError:
5
+ import hackatimeOA
6
+ import slackOA
7
+ import keyring
8
+ import requests
9
+ import dotenv
10
+ from pathlib import Path
11
+ import platformdirs
12
+
13
+ service_name = "HackaProfile"
14
+ CONFIG_DIR = Path(platformdirs.user_config_dir("hackaprofile"))
15
+ LOG_DIR = Path(platformdirs.user_log_dir("hackaprofile"))
16
+
17
+ # config = dotenv.dotenv_values("../config/.conf")
18
+
19
+
20
+ class hackatime():
21
+ def __init__(self) -> None:
22
+ self.hb_url = "https://hackatime.hackclub.com/api/v1/authenticated/heartbeats/latest"
23
+ self.username = "hackatime_token"
24
+ self.config_path = CONFIG_DIR / "hackatime.hackaprofile.conf"
25
+ # self.client_id = self.load_config()["client_id"]
26
+ # Authorize hackatime
27
+ def authorize(self) -> tuple:
28
+ token = hackatimeOA.authenticate()
29
+ # If token is given:
30
+ if type(token) == str and token:
31
+ self.store_token(token)
32
+ return (True, token)
33
+ else:
34
+ return (False, token)
35
+
36
+ def revoke(self):
37
+ url = "https://hackatime.hackclub.com/oauth/revoke"
38
+ print(self.fetch_config()["client_id"])
39
+ headers = {
40
+ "Authorization": f"Bearer {self.get_token()}"
41
+ }
42
+ data = {
43
+ "token": self.get_token(),
44
+ "client_id": self.fetch_config()["client_id"],
45
+ }
46
+
47
+ # dataJson = json.dumps(data)
48
+ # print(dataJson)
49
+ return requests.post(url=url, data=data).json()
50
+
51
+ def get_token(self) -> str:
52
+ token = keyring.get_password(service_name, self.username)
53
+ if type(token) != str:
54
+ token = ""
55
+ return token
56
+
57
+ def store_token(self, token: str) -> None:
58
+ keyring.set_password(service_name, self.username, token)
59
+
60
+ def fetch_hb(self):
61
+ url = self.hb_url
62
+ headers = {
63
+ "Authorization": f"Bearer {self.get_token()}"
64
+ }
65
+ try:
66
+ res = requests.get(url=url, headers=headers)
67
+ json = res.json()
68
+ except:
69
+ json = {"ok": False}
70
+ return json
71
+
72
+ def fetch_config(self) -> dict:
73
+ return dotenv.dotenv_values(self.config_path)
74
+
75
+ def status(self):
76
+ json = self.fetch_hb()
77
+ # print(json)
78
+
79
+ # Hackatime does not return ok if its ok. Weird...
80
+ ok = json.get("ok", True)
81
+ if ok == False:
82
+ error = json.get("error", "")
83
+ else:
84
+ error = ""
85
+ return {"ok": ok, "error": error}
86
+ def load_config(self) -> dict:
87
+ return dotenv.dotenv_values(self.config_path)
88
+
89
+
90
+ class slack():
91
+ def __init__(self) -> None:
92
+ self.base_url = "https://slack.com/api"
93
+ self.username = "slack_token"
94
+ self.config_path = CONFIG_DIR / "slack.hackaprofile.conf"
95
+ # self.client_id = self.load_config()["client_id"]
96
+ # Authorize hackatime
97
+ def authorize(self) -> tuple:
98
+ token = slackOA.authenticate()
99
+ # If token is given:
100
+ if type(token) == str and token:
101
+ self.store_token(token)
102
+ return (True, token)
103
+ else:
104
+ return (False, token)
105
+
106
+ def get_token(self) -> str:
107
+ token = keyring.get_password(service_name, self.username)
108
+ if type(token) != str:
109
+ token = ""
110
+ return token
111
+
112
+ def store_token(self, token: str) -> None:
113
+ keyring.set_password(service_name, self.username, token)
114
+
115
+ def set_profile(self, profile: dict) -> dict:
116
+ url = f"{self.base_url}/users.profile.set"
117
+ headers = {
118
+ "Authorization": f"Bearer {self.get_token()}"
119
+ }
120
+ data = {
121
+ "profile": profile
122
+ }
123
+
124
+ # dataJson = json.dumps(data)
125
+ # print(dataJson)
126
+ return requests.post(url=url, headers=headers, json=data).json()
127
+
128
+ def get_profile(self):
129
+ url = f"{self.base_url}/users.profile.get"
130
+ headers = {
131
+ "Authorization": f"Bearer {self.get_token()}"
132
+ }
133
+
134
+ # dataJson = json.dumps(data)
135
+ # print(dataJson)
136
+ return requests.get(url=url, headers=headers).json()
137
+
138
+ # Get the field id <-> label pairs
139
+ def get_team_profile(self):
140
+ url = f"{self.base_url}/team.profile.get"
141
+ headers = {
142
+ "Authorization": f"Bearer {self.get_token()}"
143
+ }
144
+
145
+ # dataJson = json.dumps(data)
146
+ # print(dataJson)
147
+ return requests.get(url=url, headers=headers).json()
148
+
149
+ def fetch_config(self) -> dict:
150
+ return dotenv.dotenv_values(self.config_path)
151
+
152
+ def status(self):
153
+ pass
154
+ # json = self.fetch_hb()
155
+ # # print(json)
156
+
157
+ # # Hackatime does not return ok if its ok. Weird...
158
+ # ok = json.get("ok", True)
159
+ # if ok == False:
160
+ # error = json.get("error", "")
161
+ # else:
162
+ # error = ""
163
+ # return {"ok": ok, "error": error}
164
+
165
+
166
+ def load_config(self) -> dict:
167
+ return dotenv.dotenv_values(self.config_path)
168
+
169
+ platfroms = {
170
+ "slack": slack,
171
+ }
File without changes
@@ -0,0 +1,3 @@
1
+ # WARNING: DO NOT CHANGE UNLESS YOU KNOW WHAT YOU ARE DOING
2
+ hackatime_client_id = "WAlWRi9-IfFv2F88Iv6O75iZD7MoSVpCQtwrCj8xvrY"
3
+ slack_client_id = "2210535565.11339190985360"
@@ -0,0 +1,2 @@
1
+ # Don't change this plz :3 (it will break...)
2
+ client_id = "WAlWRi9-IfFv2F88Iv6O75iZD7MoSVpCQtwrCj8xvrY"
@@ -0,0 +1,11 @@
1
+ # Don't change this plz (it will break...)
2
+ client_id = "2210535565.11339190985360"
3
+
4
+ title =
5
+ phone =
6
+ real_name =
7
+ display_name =
8
+ status_text =
9
+ status_emoji =
10
+
11
+ # Check README.md
@@ -0,0 +1,360 @@
1
+ import time
2
+ from rich import print as rprint
3
+ from rich.panel import Panel
4
+ from rich.console import Console, Group
5
+ from rich.text import Text
6
+ from rich.pretty import Pretty, pprint
7
+ from rich.prompt import Confirm
8
+ from rich.table import Table
9
+ from rich import box
10
+ from rich.live import Live
11
+ from rich.highlighter import Highlighter
12
+ # from rich.terminal_theme import MONOKAI
13
+
14
+ import typer
15
+ from typing import Annotated
16
+
17
+ try:
18
+ from . import backend
19
+ except ImportError:
20
+ import backend
21
+
22
+ import questionary
23
+
24
+ import os
25
+ import shutil
26
+ from pathlib import Path
27
+ import dotenv
28
+ import pyperclip
29
+
30
+ import subprocess as sp
31
+
32
+ import sys
33
+ import psutil
34
+ import signal
35
+ import platformdirs
36
+
37
+ console = Console()
38
+
39
+ app = typer.Typer(no_args_is_help=True)
40
+ hackatime = backend.hackatime()
41
+ slack = backend.slack()
42
+
43
+ CONFIG_DIR = Path(platformdirs.user_config_dir("hackaprofile"))
44
+ LOG_DIR = Path(platformdirs.user_log_dir("hackaprofile"))
45
+
46
+ @app.callback()
47
+ def callback():
48
+ """
49
+ Useful tool to automatically update online profiles based on Hackatime status
50
+ """
51
+
52
+ # Auto completion
53
+ def complete_platform(incomplete: str) -> list:
54
+ valid_names = ["HackaTime", "Slack"]
55
+ completion = []
56
+ for name in valid_names:
57
+ if name.startswith(incomplete):
58
+ completion.append(name)
59
+ return completion
60
+
61
+ @app.command()
62
+ def status():
63
+ """
64
+ View the status of HackaProfile
65
+ """
66
+ parsed_hackatime_status = {}
67
+ parsed_slack_status = {}
68
+
69
+ with console.status("Fetching status...", spinner="dots"):
70
+ hackatime_status: dict = hackatime.fetch_hb()
71
+
72
+ temp = slack.get_profile()
73
+ # rprint(temp)
74
+ try:
75
+ slack_status: dict = temp["profile"]
76
+ except KeyError:
77
+ # If there is an error
78
+ slack_status: dict = temp
79
+ # rprint(slack_status)
80
+
81
+ if hackatime_status.get("ok", True) == False:
82
+ parsed_hackatime_status["Authorization"] = "❌ Unauthorized"
83
+ else:
84
+ parsed_hackatime_status["Authorization"] = "[bold green]✓[/bold green] Authorized"
85
+ parsed_hackatime_status["Current Language"] = hackatime_status.get("language", "Unknown")
86
+ parsed_hackatime_status["Current Project"] = hackatime_status.get("project", "Unknown")
87
+
88
+ # print(slack_status)
89
+ if slack_status.get("ok") == False:
90
+ parsed_slack_status["Authorization"] = f"❌ {slack_status.get("error", "Unknown error")}"
91
+ else:
92
+ parsed_slack_status["Authorization"] = "[bold green]✓[/bold green] Authorized"
93
+ parsed_slack_status["Display Name"] = slack_status.get("display_name", "Unknown")
94
+ parsed_slack_status["Status Text"] = slack_status.get("status_text", "Unknown")
95
+ parsed_slack_status["Status Emoji"] = slack_status.get("status_emoji", "Unknown")
96
+
97
+
98
+ hackatime_table = Table("Field", "Value", title="Hackatime", box=box.ROUNDED, expand=True)
99
+ slack_table = Table("Field", "Value", title="Slack", box=box.ROUNDED, expand=True)
100
+
101
+ for _, field in enumerate(parsed_hackatime_status):
102
+ key = field
103
+ value = parsed_hackatime_status[field]
104
+ hackatime_table.add_row(key, value)
105
+
106
+ for _, field in enumerate(parsed_slack_status):
107
+ key = field
108
+ value = parsed_slack_status[field]
109
+ slack_table.add_row(key, value)
110
+
111
+ grid = Table.grid(expand=True, padding=10)
112
+ grid.add_column()
113
+ grid.add_column(justify="right")
114
+ grid.add_row(hackatime_table, slack_table)
115
+ rprint(
116
+ "",
117
+ grid
118
+ )
119
+
120
+
121
+ # [bold green]✓[/bold green]
122
+ def authorizeHA():
123
+ ok, hatoken = hackatime.authorize()
124
+ # If token is given:
125
+ if ok:
126
+ rprint("[bold green]✓[/bold green] Hackatime authorized!")
127
+ return hatoken
128
+ else:
129
+ rprint(f"[bold red]err: {str(hatoken)}")
130
+ hatoken = authorizeHA()
131
+
132
+ return hatoken
133
+
134
+ @app.command()
135
+ def setup(force: Annotated[bool, typer.Option("--force")] = False):
136
+ """
137
+ Guided setup of HackaProfile
138
+ """
139
+ console.clear()
140
+ # console.rule("HackaProfile")
141
+ rprint(Panel(Text("Welcome to HackaProfile\nyou will be guided on an easy setup of the tool!", justify="center")))
142
+ console.rule()
143
+ # rprint(force)
144
+
145
+ # Copy log files
146
+ try:
147
+ # Allow overwrite if set to force
148
+ shutil.copytree(Path(__file__).resolve().parent / "logTemplate", LOG_DIR, dirs_exist_ok=force)
149
+ rprint("[bold green]✓[/bold green] Log file setup done!")
150
+ except FileExistsError:
151
+ rprint("[bold green]✓[/bold green] Log file already exists!")
152
+ except Exception as e:
153
+ rprint(f"❌ Failed to setup log files: {e}")
154
+
155
+ # Copy config files
156
+ try:
157
+ # Allow overwrite if set to force
158
+ shutil.copytree(Path(__file__).resolve().parent / "configTemplate", CONFIG_DIR, dirs_exist_ok=force)
159
+ rprint("[bold green]✓[/bold green] Config file setup done!")
160
+ except FileExistsError:
161
+ rprint("[bold green]✓[/bold green] Config file already exists!")
162
+ except Exception as e:
163
+ rprint(f"❌ Failed to setup config files: {e}")
164
+
165
+
166
+ # If no token stored
167
+ if force or not hackatime.status()["ok"]:
168
+ # hackatimeConfirm = Confirm.ask("[bold cyan]Do you want to authorize Hackatime (This will redirect you to OAuth page)", default=True)
169
+ hackatimeConfirm = questionary.confirm("Do you want to authorize Hackatime (This will redirect you to OAuth page)").ask()
170
+ if hackatimeConfirm:
171
+ with console.status("Authorizing Hackatime", spinner="dots"):
172
+ ok, hackatime_token = hackatime.authorize()
173
+
174
+ if ok:
175
+ rprint("[bold green]✓[/bold green] Hackatime authorized!")
176
+
177
+ else :
178
+ rprint(f"[bold red]err: {str(hackatime_token)}")
179
+ else:
180
+ rprint("[bold red]err: HackaProfile could not function without Hackatime.")
181
+ typer.Abort()
182
+ # If already stored
183
+ else:
184
+ rprint("[bold green]✓[/bold green] Hackatime already authorized!\n")
185
+
186
+ platforms = questionary.checkbox(
187
+ message="Please choose the platforms you want to link to",
188
+ choices=[
189
+ "Slack",
190
+ "Github"
191
+ ]
192
+ ).ask()
193
+
194
+ # Authorize the platforms
195
+
196
+ for platform in platforms:
197
+ # print("test")
198
+ cls = backend.platfroms.get(platform.lower())
199
+ if cls:
200
+ instance = cls()
201
+ with console.status(f"Authorizing {platform}", spinner="dots"):
202
+ ok, platform_token = instance.authorize()
203
+ if ok:
204
+ rprint(f"[bold green]✓[/bold green] {platform} authorized!")
205
+
206
+ else:
207
+ rprint(f"[bold red]err: {str(platform_token)}")
208
+
209
+ else:
210
+ rprint("[bold red]err: Platform unsupported")
211
+
212
+
213
+ rprint("[bold green]✓[/bold green] Setup complete!\n\n1.Run [bold green]hackaprofile config \\[platform name][/bold green] to configure\n2.Run [bold green]hackaprofile start[/bold green] to start updating your profile automatically!")
214
+
215
+ class placeholderHighlighter(Highlighter):
216
+ def highlight(self, text) -> None:
217
+ text.highlight_regex(r"\{\{.*?\}\}", "bold yellow")
218
+
219
+ @app.command()
220
+ def config(platform: Annotated[str, typer.Argument]):
221
+ """
222
+ Shows a structured preview of the config files for each of the platforms
223
+ """
224
+
225
+ table = Table(
226
+ "Field",
227
+ "Value",
228
+ title="Preview Slack",
229
+ box=box.ROUNDED,
230
+ expand=True,
231
+ )
232
+
233
+ hlt = placeholderHighlighter()
234
+ slack_config_keys = list(slack.load_config().keys())
235
+ slack_config_values = list(slack.load_config().values())
236
+
237
+ for i in range(len(slack_config_keys)):
238
+ field = slack_config_keys[i]
239
+ value = slack_config_values[i]
240
+ if not value:
241
+ value = "Not set"
242
+ table.add_row(field, hlt(value))
243
+
244
+
245
+ rprint(table)
246
+ option = questionary.select(
247
+ "What do you want to do?",
248
+ choices = [
249
+ "Edit",
250
+ "Exit"
251
+ ]
252
+ ).ask()
253
+
254
+ if option == "Exit":
255
+ typer.Exit()
256
+ elif option == "Edit":
257
+ path = str(CONFIG_DIR/ f"{platform}.hackaprofile.conf")
258
+ rprint(f"\n[bold green]Open[/bold green] {path} [bold green]in your preferred editor!")
259
+
260
+ cbConfirm = questionary.confirm(
261
+ "Do you want to copy path to clipboard?",
262
+ default=False
263
+ ).ask()
264
+ if cbConfirm:
265
+ pyperclip.copy(path)
266
+ rprint("[bold green]✓[/bold green] Copied!")
267
+
268
+ # with Live(table, refresh_per_second=4):
269
+ # console.clear()Path(__file__).resolve().parent.parent / "config" / f"{platform}.hackaprofile.conf"
270
+ # while True:
271
+ # table.add_row("Status", "I love coding")
272
+ # # table.add_row("Status")
273
+ # time.sleep(0.5)
274
+
275
+ @app.command(deprecated=True)
276
+ def auth(platform: Annotated[str, typer.Option(prompt=False, help="Which platform you want to authenticate", autocompletion=complete_platform)] = ""):
277
+ """
278
+ Using OAuth to bind HackaProfile to your account
279
+ """
280
+
281
+ rprint(f"Please use [bold green]hackaprofile setup[/bold green] and select the platforms you want to authorize, use [bold green]--force[/bold green] to re-auth Hackatime")
282
+
283
+ @app.command()
284
+ def revoke(platform: str, all: Annotated[bool, typer.Option("--all")] = False):
285
+
286
+ # TODO
287
+ if all:
288
+ pass
289
+ else:
290
+ if platform == "hackatime":
291
+ rprint(hackatime.revoke())
292
+
293
+
294
+ def get_agent_pid() -> int:
295
+ """
296
+ Retruns PID of the agent process based on agent.pid
297
+ """
298
+
299
+ with open(CONFIG_DIR / "agent.pid", "r") as f:
300
+ pid = f.readline()
301
+ f.close()
302
+ return int(pid)
303
+
304
+ def is_agent_alive(pid: int) -> bool:
305
+ return psutil.pid_exists(pid)
306
+
307
+ @app.command()
308
+ def start():
309
+ log = open(LOG_DIR / "agent.log", "a")
310
+ error = "Unknown error"
311
+ try:
312
+ sp.Popen(
313
+ [sys.executable, "-u", Path(__file__).resolve().parent / "agent.py"],
314
+ stdout=log,
315
+ stderr=sp.STDOUT,
316
+ stdin=sp.DEVNULL,
317
+ start_new_session=True
318
+
319
+ )
320
+ except Exception as e:
321
+ error = e
322
+ time.sleep(0.5)
323
+ pid = get_agent_pid()
324
+ if is_agent_alive(pid):
325
+ rprint(f"[bold green]✓[/bold green] Started background worker!")
326
+ else:
327
+ rprint(f"❌ Worker not started. {error}")
328
+
329
+ @app.command()
330
+ def stop():
331
+ pid = get_agent_pid()
332
+ error = ""
333
+
334
+ try:
335
+ os.kill(pid, signal.SIGTERM)
336
+ except Exception as e:
337
+ error = " " + str(e)
338
+
339
+ time.sleep(0.5)
340
+ if not is_agent_alive(pid) and not error:
341
+ rprint(f"[bold green]✓[/bold green] Stopped background worker!")
342
+ else:
343
+ rprint(f"❌ Worker not stopped{error}. Retry or manually kill process {pid}")
344
+
345
+ @app.command()
346
+ def restart():
347
+ stop()
348
+ start()
349
+
350
+ @app.command()
351
+ def debug():
352
+ """
353
+ Development purpose only: Uhh, don't worry about it...
354
+ """
355
+ print(CONFIG_DIR)
356
+ print(LOG_DIR)
357
+
358
+
359
+ if __name__ == "__main__":
360
+ app()
@@ -0,0 +1,116 @@
1
+ from flask import Flask, request
2
+ import requests
3
+ import secrets
4
+ import hashlib
5
+ import base64
6
+ import webbrowser
7
+ from werkzeug.serving import make_server
8
+ import threading
9
+ import time
10
+ import logging
11
+ import dotenv
12
+ from pathlib import Path
13
+ import platformdirs
14
+
15
+ log = logging.getLogger('werkzeug')
16
+
17
+ log.setLevel(logging.ERROR)
18
+
19
+ stateRNG = secrets.token_urlsafe(32)
20
+ baseUrl = "https://hackatime.hackclub.com/oauth/authorize"
21
+ exchangeBaseUrl = "https://hackatime.hackclub.com/oauth/token"
22
+ redirection_uri = "http://localhost:32767/auth/hackatime/callback"
23
+
24
+ CONFIG_DIR = Path(platformdirs.user_config_dir("hackaprofile"))
25
+ LOG_DIR = Path(platformdirs.user_log_dir("hackaprofile"))
26
+
27
+ # print(client_id)
28
+ token = ""
29
+ code_verifier = ""
30
+ client_id = ""
31
+
32
+ token_event = threading.Event()
33
+ # Redirect to the auth page
34
+ def redirection(state):
35
+ # print("debug")
36
+ global code_verifier, client_id
37
+
38
+ # Just an unique id, not secret lol
39
+ client_id = dotenv.dotenv_values(CONFIG_DIR / "hackatime.hackaprofile.conf")["client_id"]
40
+
41
+
42
+ code_verifier = secrets.token_urlsafe(664)
43
+ digest = hashlib.sha256(code_verifier.encode()).digest()
44
+ code_challenge = base64.urlsafe_b64encode(digest).decode().rstrip('=')
45
+ args = {
46
+ "client_id": client_id,
47
+ "redirect_uri": redirection_uri,
48
+ "response_type": "code",
49
+ "state": state,
50
+ "code_challenge": code_challenge,
51
+ "code_challenge_method": "S256",
52
+ "state": state,
53
+ }
54
+ url = f"{baseUrl}/?client_id={args['client_id']}&redirect_uri={args["redirect_uri"]}&response_type=code&state={args["state"]}&code_challenge={args["code_challenge"]}&code_challenge_method={args["code_challenge_method"]}&state={args["state"]}"
55
+
56
+ webbrowser.open(url)
57
+
58
+
59
+ # Exchange API key
60
+ def exchange(code: str):
61
+ global token_event
62
+ url = exchangeBaseUrl
63
+ data = {
64
+ "client_id": client_id,
65
+ "code": code,
66
+ "redirect_uri": redirection_uri,
67
+ "grant_type": "authorization_code",
68
+ "code_verifier": code_verifier,
69
+
70
+ }
71
+ res = requests.post(url=url, data=data)
72
+ json = res.json()
73
+ token_event.set()
74
+ if not json.get("error"):
75
+ token = json.get("access_token")
76
+ # print(token)
77
+ return token
78
+ else:
79
+ return {"error": json.get("error")}
80
+ # Handle callback
81
+ app = Flask(__name__)
82
+
83
+ @app.route("/auth/hackatime/callback")
84
+ def hackatimeCallback():
85
+ global token
86
+ code = request.args.get("code")
87
+ error = request.args.get("error")
88
+ state2 = request.args.get("state")
89
+ # print(code)
90
+
91
+ if not error and code and state2 == stateRNG:
92
+ token = exchange(code)
93
+ # print(token)
94
+ return "<p>Authorization completed, you can close this tab now.</p>"
95
+ else:
96
+ return "<p>You have denied authorization, or an error has occured.</p><p>If you did not deny authorization, please close this tab and submit a issue on Github</p>"
97
+
98
+
99
+ def authenticate():
100
+ redirection(stateRNG)
101
+ server = make_server("127.0.0.1", 32767, app)
102
+ thread = threading.Thread(target=server.serve_forever)
103
+ thread.start()
104
+
105
+ token_event.wait(timeout=120)
106
+
107
+
108
+ server.shutdown()
109
+ thread.join()
110
+ return token
111
+
112
+
113
+
114
+ # Debug
115
+ if __name__ == "__main__":
116
+ authenticate()
File without changes
@@ -0,0 +1,114 @@
1
+ from flask import Flask, request
2
+ import requests
3
+ import secrets
4
+ import hashlib
5
+ import base64
6
+ import webbrowser
7
+ from werkzeug.serving import make_server
8
+ import threading
9
+ import time
10
+ import logging
11
+ import dotenv
12
+ from pathlib import Path
13
+ import platformdirs
14
+
15
+ log = logging.getLogger('werkzeug')
16
+
17
+ log.setLevel(logging.ERROR)
18
+
19
+ stateRNG = secrets.token_urlsafe(32)
20
+ baseUrl = "https://slack.com/oauth/v2/authorize"
21
+ exchangeBaseUrl = "https://slack.com/api/oauth.v2.access"
22
+ redirection_uri = "http://localhost:32767/auth/slack/callback"
23
+ CONFIG_DIR = Path(platformdirs.user_config_dir("hackaprofile"))
24
+ LOG_DIR = Path(platformdirs.user_log_dir("hackaprofile"))
25
+
26
+ token = ""
27
+ code_verifier = ""
28
+
29
+ token_event = threading.Event()
30
+ client_id = ""
31
+ # Redirect to the auth page
32
+ def redirection(state):
33
+ # print("debug")
34
+ global code_verifier, client_id
35
+
36
+ # Just an unique id, not secret lol
37
+ client_id = dotenv.dotenv_values(CONFIG_DIR / "slack.hackaprofile.conf")["client_id"]
38
+
39
+
40
+ code_verifier = secrets.token_urlsafe(664)
41
+ digest = hashlib.sha256(code_verifier.encode()).digest()
42
+ code_challenge = base64.urlsafe_b64encode(digest).decode().rstrip('=')
43
+ args = {
44
+ "client_id": client_id,
45
+ "redirect_uri": redirection_uri,
46
+ "response_type": "code",
47
+ "state": state,
48
+ "code_challenge": code_challenge,
49
+ "code_challenge_method": "S256",
50
+ "scope": "",
51
+ "user_scope": "users.profile:read,users.profile:write"
52
+ }
53
+ url = f"{baseUrl}/?client_id={args['client_id']}&redirect_uri={args["redirect_uri"]}&scope={args["scope"]}&user_scope={args["user_scope"]}&state={args["state"]}&code_challenge={args["code_challenge"]}&code_challenge_method={args["code_challenge_method"]}"
54
+
55
+ webbrowser.open(url)
56
+
57
+
58
+ # Exchange API key
59
+ def exchange(code: str):
60
+ global token_event
61
+
62
+ url = exchangeBaseUrl
63
+ data = {
64
+ "client_id": client_id,
65
+ "code": code,
66
+ "redirect_uri": redirection_uri,
67
+ "grant_type": "authorization_code",
68
+ "code_verifier": code_verifier,
69
+
70
+ }
71
+ res = requests.post(url=url, data=data)
72
+ json = res.json()
73
+ token_event.set()
74
+ if json.get("ok") == True:
75
+ token = json.get("authed_user", {}).get("access_token", None)
76
+ # print(token)
77
+ return token
78
+ else:
79
+ return {"error": json.get("error")}
80
+ # Handle callback
81
+ app = Flask(__name__)
82
+
83
+ @app.route("/auth/slack/callback")
84
+ def hackatimeCallback():
85
+ global token
86
+ code = request.args.get("code")
87
+ error = request.args.get("error")
88
+ state2 = request.args.get("state")
89
+ # print(code)
90
+ if not error and code and state2 == stateRNG:
91
+ token = exchange(code)
92
+ # print(token)
93
+ return "<p>Authorization completed, you can close this tab now.</p>"
94
+ else:
95
+ return "<p>You have denied authorization, or an error has occured.</p><p>If you did not deny authorization, please close this tab and submit a issue on Github</p>"
96
+
97
+
98
+ def authenticate():
99
+ redirection(stateRNG)
100
+ server = make_server("127.0.0.1", 32767, app)
101
+ thread = threading.Thread(target=server.serve_forever)
102
+ thread.start()
103
+
104
+ token_event.wait(timeout=120)
105
+
106
+ server.shutdown()
107
+ thread.join()
108
+ return token
109
+
110
+
111
+
112
+ # Debug
113
+ if __name__ == "__main__":
114
+ authenticate()