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.
- hackaprofile-0.1.0/HackaProfile.egg-info/PKG-INFO +79 -0
- hackaprofile-0.1.0/HackaProfile.egg-info/SOURCES.txt +19 -0
- hackaprofile-0.1.0/HackaProfile.egg-info/dependency_links.txt +1 -0
- hackaprofile-0.1.0/HackaProfile.egg-info/entry_points.txt +2 -0
- hackaprofile-0.1.0/HackaProfile.egg-info/requires.txt +32 -0
- hackaprofile-0.1.0/HackaProfile.egg-info/top_level.txt +1 -0
- hackaprofile-0.1.0/PKG-INFO +79 -0
- hackaprofile-0.1.0/README.md +40 -0
- hackaprofile-0.1.0/pyproject.toml +53 -0
- hackaprofile-0.1.0/setup.cfg +4 -0
- hackaprofile-0.1.0/src/__init__.py +3 -0
- hackaprofile-0.1.0/src/agent.py +106 -0
- hackaprofile-0.1.0/src/backend.py +171 -0
- hackaprofile-0.1.0/src/configTemplate/agent.pid +0 -0
- hackaprofile-0.1.0/src/configTemplate/hackaprofile.conf +3 -0
- hackaprofile-0.1.0/src/configTemplate/hackatime.hackaprofile.conf +2 -0
- hackaprofile-0.1.0/src/configTemplate/slack.hackaprofile.conf +11 -0
- hackaprofile-0.1.0/src/frontend.py +360 -0
- hackaprofile-0.1.0/src/hackatimeOA.py +116 -0
- hackaprofile-0.1.0/src/logTemplate/agent.log +0 -0
- hackaprofile-0.1.0/src/slackOA.py +114 -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,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 @@
|
|
|
1
|
+
|
|
@@ -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 @@
|
|
|
1
|
+
src
|
|
@@ -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,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,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()
|