tranquillity 0.1.0__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.
- ctOS.py +43 -0
- docker_launcher.py +37 -0
- settings/settings.py +5 -0
- src/core/__init__.py +0 -0
- src/core/gemini.py +244 -0
- src/ctOS/help.py +21 -0
- src/ctOS/light/light.py +15 -0
- src/ctOS/light/toggle_light.py +18 -0
- src/ctOS/music/get_id_device.py +17 -0
- src/ctOS/music/get_token_tmp.py +115 -0
- src/ctOS/music/init_token.py +34 -0
- src/ctOS/music/music.py +26 -0
- src/ctOS/music/pause.py +15 -0
- src/ctOS/music/resume.py +15 -0
- src/ctOS/start/init.py +125 -0
- src/ctOS/start/start.py +61 -0
- src/subsystem/autonome_sys/autonome.py +8 -0
- src/subsystem/create_event.py +47 -0
- src/subsystem/get_creds.py +30 -0
- src/subsystem/get_email.py +101 -0
- src/subsystem/list_event.py +65 -0
- src/subsystem/notification_system.py +17 -0
- src/subsystem/search_web.py +48 -0
- src/tranquillity.py +10 -0
- tranquillity-0.1.0.dist-info/METADATA +23 -0
- tranquillity-0.1.0.dist-info/RECORD +29 -0
- tranquillity-0.1.0.dist-info/WHEEL +5 -0
- tranquillity-0.1.0.dist-info/entry_points.txt +2 -0
- tranquillity-0.1.0.dist-info/top_level.txt +4 -0
ctOS.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from src.ctOS.start.init import demo
|
|
2
|
+
from src.ctOS.start.start import start
|
|
3
|
+
from src.ctOS.help import help
|
|
4
|
+
import sys
|
|
5
|
+
import asyncio
|
|
6
|
+
from src.subsystem.notification_system import send_discord_message
|
|
7
|
+
from dotenv import load_dotenv
|
|
8
|
+
from src.tranquillity import init as tranquillity
|
|
9
|
+
from src.ctOS.music.music import music_command
|
|
10
|
+
from src.ctOS.music.init_token import init
|
|
11
|
+
load_dotenv()
|
|
12
|
+
|
|
13
|
+
def main():
|
|
14
|
+
nb = len(sys.argv)
|
|
15
|
+
|
|
16
|
+
if nb <= 1:
|
|
17
|
+
help()
|
|
18
|
+
return
|
|
19
|
+
if sys.argv[1] == "--demo":
|
|
20
|
+
demo()
|
|
21
|
+
start()
|
|
22
|
+
return
|
|
23
|
+
if sys.argv[1] == "--music":
|
|
24
|
+
music_command()
|
|
25
|
+
return
|
|
26
|
+
if sys.argv[1] == "--send":
|
|
27
|
+
if sys.argv[2] == None:
|
|
28
|
+
return 84
|
|
29
|
+
asyncio.run(send_discord_message(sys.argv[2]))
|
|
30
|
+
return
|
|
31
|
+
if sys.argv[1] == "--init":
|
|
32
|
+
init()
|
|
33
|
+
return
|
|
34
|
+
if sys.argv[1] == "--start":
|
|
35
|
+
tranquillity()
|
|
36
|
+
return
|
|
37
|
+
if sys.argv[1] == "--help" or "-h":
|
|
38
|
+
help()
|
|
39
|
+
else:
|
|
40
|
+
return 84
|
|
41
|
+
|
|
42
|
+
if __name__ == "__main__":
|
|
43
|
+
main()
|
docker_launcher.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
IMAGE_ENV = "TRANQUILLITY_IMAGE"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> int:
|
|
11
|
+
image = os.environ.get(IMAGE_ENV)
|
|
12
|
+
if not image:
|
|
13
|
+
print(
|
|
14
|
+
f"{IMAGE_ENV} n'est pas defini. Exemple: "
|
|
15
|
+
"export TRANQUILLITY_IMAGE=ghcr.io/mon-compte/tranquillity:latest",
|
|
16
|
+
file=sys.stderr,
|
|
17
|
+
)
|
|
18
|
+
return 2
|
|
19
|
+
|
|
20
|
+
if shutil.which("docker") is None:
|
|
21
|
+
print("Docker est requis mais la commande docker est introuvable.", file=sys.stderr)
|
|
22
|
+
return 127
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
subprocess.run(["docker", "info"], check=True)
|
|
26
|
+
subprocess.run(["docker", "pull", image], check=True)
|
|
27
|
+
return subprocess.run(
|
|
28
|
+
["docker", "run", "--rm", "-it", "--name", "tranquillity", image, *sys.argv[1:]]
|
|
29
|
+
).returncode
|
|
30
|
+
except subprocess.CalledProcessError as error:
|
|
31
|
+
return error.returncode
|
|
32
|
+
except KeyboardInterrupt:
|
|
33
|
+
return 130
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == "__main__":
|
|
37
|
+
raise SystemExit(main())
|
settings/settings.py
ADDED
src/core/__init__.py
ADDED
|
File without changes
|
src/core/gemini.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import sounddevice as sd
|
|
4
|
+
import queue
|
|
5
|
+
import subprocess
|
|
6
|
+
import tempfile
|
|
7
|
+
import wave
|
|
8
|
+
import threading
|
|
9
|
+
import shlex
|
|
10
|
+
from piper import PiperVoice
|
|
11
|
+
from settings.settings import IA_NAME, VOICE_PATH, MODEL_PATH, SAMPLE_RATE, BLOCK_SIZE
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
import re
|
|
14
|
+
from vosk import Model, KaldiRecognizer, SetLogLevel
|
|
15
|
+
import json
|
|
16
|
+
import requests
|
|
17
|
+
from src.subsystem.search_web import search_web
|
|
18
|
+
from src.subsystem.get_email import get_email
|
|
19
|
+
from google import genai
|
|
20
|
+
from google.genai import types
|
|
21
|
+
from src.subsystem.create_event import create_event
|
|
22
|
+
from src.subsystem.list_event import list_calendar
|
|
23
|
+
|
|
24
|
+
load_dotenv()
|
|
25
|
+
|
|
26
|
+
recording_enabled = True
|
|
27
|
+
audio_queue = queue.Queue()
|
|
28
|
+
|
|
29
|
+
def callback(indata, frames, time, status):
|
|
30
|
+
global recording_enabled
|
|
31
|
+
if status:
|
|
32
|
+
print(status, file=sys.stderr)
|
|
33
|
+
if recording_enabled:
|
|
34
|
+
audio_queue.put(bytes(indata))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def clean_for_tts(text: str) -> str:
|
|
38
|
+
text = re.sub(r'\*\*(.+?)\*\*', r'\1', text)
|
|
39
|
+
text = re.sub(r'\*(.+?)\*', r'\1', text)
|
|
40
|
+
text = re.sub(r'[*#`_]', '', text)
|
|
41
|
+
return text
|
|
42
|
+
|
|
43
|
+
def player_worker(q: "queue.Queue[str | None]"):
|
|
44
|
+
global recording_enabled
|
|
45
|
+
while True:
|
|
46
|
+
path = q.get()
|
|
47
|
+
if path is None:
|
|
48
|
+
return
|
|
49
|
+
recording_enabled = False
|
|
50
|
+
subprocess.run(["mpv", "--no-terminal", "--really-quiet", path])
|
|
51
|
+
recording_enabled = True
|
|
52
|
+
try:
|
|
53
|
+
os.remove(path)
|
|
54
|
+
except OSError:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def clear_screen():
|
|
59
|
+
os.system('cls' if os.name == 'nt' else 'clear')
|
|
60
|
+
|
|
61
|
+
def get_api_key():
|
|
62
|
+
api_key = os.getenv("GEMINI_API_KEY")
|
|
63
|
+
if api_key:
|
|
64
|
+
return api_key
|
|
65
|
+
|
|
66
|
+
print("=== Configuration de la clé API Gemini ===")
|
|
67
|
+
print("Vous pouvez également définir la variable d'environnement GEMINI_API_KEY pour éviter cette étape.\n")
|
|
68
|
+
while True:
|
|
69
|
+
api_key = input("Veuillez entrer votre clé API Google Gemini : ").strip()
|
|
70
|
+
if api_key:
|
|
71
|
+
return api_key
|
|
72
|
+
print("La clé ne peut pas être vide. Veuillez réessayer.")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def tts_worker(text_q, out_q, voice):
|
|
76
|
+
while True:
|
|
77
|
+
fragment = text_q.get()
|
|
78
|
+
if fragment is None:
|
|
79
|
+
out_q.put(None)
|
|
80
|
+
return
|
|
81
|
+
fd, path = tempfile.mkstemp(prefix="tts_", suffix=".wav")
|
|
82
|
+
os.close(fd)
|
|
83
|
+
with wave.open(path, "wb") as wav_file:
|
|
84
|
+
voice.synthesize_wav(fragment, wav_file)
|
|
85
|
+
out_q.put(path)
|
|
86
|
+
|
|
87
|
+
def detect_command(response_text):
|
|
88
|
+
resultat = re.search(r"(?mi)^command:\s*(.+)$", response_text)
|
|
89
|
+
|
|
90
|
+
if resultat:
|
|
91
|
+
commande = resultat.group(1).strip()
|
|
92
|
+
return resultat.start(), commande
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
def execute_command(commande_raw):
|
|
96
|
+
if commande_raw:
|
|
97
|
+
try:
|
|
98
|
+
try:
|
|
99
|
+
args = shlex.split(commande_raw)
|
|
100
|
+
subprocess.run(args, shell=False)
|
|
101
|
+
except ValueError:
|
|
102
|
+
subprocess.run(commande_raw, shell=True)
|
|
103
|
+
except Exception as e:
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
def gemini():
|
|
107
|
+
clear_screen()
|
|
108
|
+
print("Initialisation Tranquillity")
|
|
109
|
+
|
|
110
|
+
api_key = get_api_key()
|
|
111
|
+
model_name = IA_NAME
|
|
112
|
+
|
|
113
|
+
print("Initialisation de la personalité")
|
|
114
|
+
|
|
115
|
+
with open("settings/personality.txt", "r", encoding="utf-8") as f:
|
|
116
|
+
content = f.read()
|
|
117
|
+
system_instruction = content
|
|
118
|
+
if not system_instruction:
|
|
119
|
+
system_instruction = "Tu es un assistant virtuel utile, amical et précis. Réponds toujours en français."
|
|
120
|
+
print(f"-> Contexte par défaut appliqué : \"{system_instruction}\"")
|
|
121
|
+
else:
|
|
122
|
+
print(f"-> Contexte personnalisé appliqué avec succès !")
|
|
123
|
+
|
|
124
|
+
print("\nInitialisation du modèle...")
|
|
125
|
+
|
|
126
|
+
client = genai.Client(api_key=api_key)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
chat = client.chats.create(
|
|
130
|
+
model=model_name,
|
|
131
|
+
config=types.GenerateContentConfig(
|
|
132
|
+
system_instruction=system_instruction,
|
|
133
|
+
tools=[search_web, get_email, list_calendar, create_event]
|
|
134
|
+
),
|
|
135
|
+
history=[]
|
|
136
|
+
)
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"Erreur lors de l'initialisation du modèle : {e}")
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
voice = PiperVoice.load(VOICE_PATH)
|
|
142
|
+
print("voice init success")
|
|
143
|
+
|
|
144
|
+
print("Initialisation VOSK")
|
|
145
|
+
print("VOSK load")
|
|
146
|
+
SetLogLevel(-1)
|
|
147
|
+
|
|
148
|
+
model = Model(MODEL_PATH)
|
|
149
|
+
rec = KaldiRecognizer(model, SAMPLE_RATE)
|
|
150
|
+
|
|
151
|
+
clear_screen()
|
|
152
|
+
print("==================================================")
|
|
153
|
+
print(" 💬 CHAT INITIALISÉ AVEC SUCCÈS ")
|
|
154
|
+
print("==================================================")
|
|
155
|
+
print(f"Modèle utilisé : {model_name}")
|
|
156
|
+
print("--------------------------------------------------")
|
|
157
|
+
print("Instructions :")
|
|
158
|
+
print(" - Tapez votre message et appuyez sur Entrée.")
|
|
159
|
+
print(" - Écrivez 'exit', 'quit' ou 'quitter' pour fermer le chat.")
|
|
160
|
+
print(" - Écrivez 'clear' pour vider l'écran du terminal.")
|
|
161
|
+
print("==================================================\n")
|
|
162
|
+
|
|
163
|
+
audio_q: "queue.Queue[str | None]" = queue.Queue()
|
|
164
|
+
tts_text_q: "queue.Queue[str | None]" = queue.Queue()
|
|
165
|
+
|
|
166
|
+
t_tts = threading.Thread(target=tts_worker, args=(tts_text_q, audio_q, voice), daemon=True)
|
|
167
|
+
t_tts.start()
|
|
168
|
+
t = threading.Thread(target=player_worker, args=(audio_q,), daemon=True)
|
|
169
|
+
t.start()
|
|
170
|
+
|
|
171
|
+
one_use = False
|
|
172
|
+
print("En écoute...")
|
|
173
|
+
with sd.RawInputStream(
|
|
174
|
+
samplerate=SAMPLE_RATE,
|
|
175
|
+
blocksize=BLOCK_SIZE,
|
|
176
|
+
dtype="int16",
|
|
177
|
+
channels=1,
|
|
178
|
+
callback=callback,
|
|
179
|
+
):
|
|
180
|
+
while True:
|
|
181
|
+
try:
|
|
182
|
+
data = audio_queue.get()
|
|
183
|
+
audio_q: "queue.Queue[str | None]" = queue.Queue()
|
|
184
|
+
if rec.AcceptWaveform(data) and one_use == False:
|
|
185
|
+
one_use = True
|
|
186
|
+
result = json.loads(rec.Result())
|
|
187
|
+
user_input = result.get("text", "").strip()
|
|
188
|
+
if not user_input:
|
|
189
|
+
one_use = False
|
|
190
|
+
continue
|
|
191
|
+
if user_input.lower() in ['exit', 'quit', 'quitter']:
|
|
192
|
+
print("\n Assistant : Au revoir ! Passez une excellente journée.")
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if user_input.lower() == 'clear':
|
|
196
|
+
clear_screen()
|
|
197
|
+
print("==================================================")
|
|
198
|
+
print(" CONVERSATION EN COURS ")
|
|
199
|
+
print("================================================--\n")
|
|
200
|
+
one_use = False
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
print("Vous", user_input)
|
|
204
|
+
print(" Assistant : ", end="", flush=True)
|
|
205
|
+
response = chat.send_message(user_input)
|
|
206
|
+
|
|
207
|
+
streamed_text = response.text
|
|
208
|
+
print(streamed_text, flush=True)
|
|
209
|
+
|
|
210
|
+
command_start = None
|
|
211
|
+
command_match = detect_command(streamed_text)
|
|
212
|
+
if command_match:
|
|
213
|
+
command_start, _ = command_match
|
|
214
|
+
spoken_text = streamed_text[:command_start]
|
|
215
|
+
else:
|
|
216
|
+
spoken_text = streamed_text
|
|
217
|
+
|
|
218
|
+
clean_tts = clean_for_tts(spoken_text)
|
|
219
|
+
if clean_tts.strip():
|
|
220
|
+
tts_text_q.put(clean_tts)
|
|
221
|
+
|
|
222
|
+
print("\n")
|
|
223
|
+
|
|
224
|
+
if command_start is not None:
|
|
225
|
+
_, commande = detect_command(streamed_text)
|
|
226
|
+
if commande:
|
|
227
|
+
print(f"Commande détectée : {commande}")
|
|
228
|
+
run_command = threading.Thread(target=execute_command, args=(commande,), daemon=True)
|
|
229
|
+
run_command.start()
|
|
230
|
+
one_use = False
|
|
231
|
+
|
|
232
|
+
except KeyboardInterrupt:
|
|
233
|
+
print("\n\n Assistant : Chat interrompu. Au revoir !")
|
|
234
|
+
audio_q.put(None)
|
|
235
|
+
t.join()
|
|
236
|
+
break
|
|
237
|
+
except Exception as e:
|
|
238
|
+
print(f"\nUne erreur est survenue lors de la communication avec l'API : {e}\n")
|
|
239
|
+
audio_q.put(None)
|
|
240
|
+
t.join()
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
gemini()
|
src/ctOS/help.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
def help():
|
|
2
|
+
print("==================================================")
|
|
3
|
+
print(" Bienvenue dans ctOS - CLI Tranquillity")
|
|
4
|
+
print("==================================================\n")
|
|
5
|
+
print("USAGE :")
|
|
6
|
+
print(" ctOS [commande]")
|
|
7
|
+
print(" ctOS [option]\n")
|
|
8
|
+
print("COMMANDES PRINCIPALES :\n")
|
|
9
|
+
print(" --start Lance le programme principal Tranquillity")
|
|
10
|
+
print("GESTION MUSICALE :\n")
|
|
11
|
+
print(" --music pause Met en pause la musique en cours")
|
|
12
|
+
print(" --music resume Reprend la musique en pause")
|
|
13
|
+
print(" --music device Affiche et gere les appareils audio connectes\n")
|
|
14
|
+
print("CONTROLE DOMOTIQUE :\n")
|
|
15
|
+
print(" --light toggle Active/desactive les lumieres connectees\n")
|
|
16
|
+
print("OUTILS ET CONFIGURATION :\n")
|
|
17
|
+
print(" --init Initialise le token Spotify et vérification")
|
|
18
|
+
print(" --send [message] Envoie un message via Discord\n")
|
|
19
|
+
print("AUTRES :\n")
|
|
20
|
+
print(" --help, -h Affiche ce message d'aide\n")
|
|
21
|
+
print("==================================================\n")
|
src/ctOS/light/light.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from src.ctOS.light.toggle_light import toggle_light
|
|
4
|
+
|
|
5
|
+
def light_command():
|
|
6
|
+
nb = len(sys.argv)
|
|
7
|
+
|
|
8
|
+
if nb <= 2:
|
|
9
|
+
print("no argument give. Please give word")
|
|
10
|
+
return
|
|
11
|
+
if sys.argv[2] == "toggle":
|
|
12
|
+
toggle_light()
|
|
13
|
+
return
|
|
14
|
+
else:
|
|
15
|
+
print("no argument give. Please give word")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import subprocess
|
|
3
|
+
|
|
4
|
+
FIFO_PATH = "/tmp/kitty_shell_input"
|
|
5
|
+
CMD_LIGHT = "./ecc-api --toggle"
|
|
6
|
+
|
|
7
|
+
def toggle_light():
|
|
8
|
+
if not os.path.exists(FIFO_PATH):
|
|
9
|
+
print(f"Sub-Agent: FIFO introuvable: {FIFO_PATH}")
|
|
10
|
+
return
|
|
11
|
+
with open(FIFO_PATH, "w") as fifo:
|
|
12
|
+
fifo.write(CMD_LIGHT + "\n")
|
|
13
|
+
subprocess.run(CMD_LIGHT)
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
def toggle_light():
|
|
17
|
+
subprocess.run(CMD_LIGHT)
|
|
18
|
+
return
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from src.ctOS.music.get_token_tmp import get_token
|
|
5
|
+
import requests
|
|
6
|
+
|
|
7
|
+
def get_id_device():
|
|
8
|
+
access_token = get_token()
|
|
9
|
+
url = "https://api.spotify.com/v1/me/player/devices"
|
|
10
|
+
header = {"Authorization": "Bearer " + access_token}
|
|
11
|
+
|
|
12
|
+
result = requests.get(url, headers=header)
|
|
13
|
+
try:
|
|
14
|
+
body = result.json()
|
|
15
|
+
except Exception:
|
|
16
|
+
body = result.text
|
|
17
|
+
print(body)
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
import webbrowser
|
|
5
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
6
|
+
from urllib.parse import urlencode, urlparse, parse_qs
|
|
7
|
+
from requests import post
|
|
8
|
+
from dotenv import load_dotenv
|
|
9
|
+
|
|
10
|
+
load_dotenv()
|
|
11
|
+
|
|
12
|
+
CLIENT_ID = os.getenv("TOKEN_PUBLIC")
|
|
13
|
+
CLIENT_SECRET = os.getenv("TOKEN_PRIVATE")
|
|
14
|
+
REDIRECT_URI = "http://127.0.0.1:8888/callback"
|
|
15
|
+
SCOPES = "user-read-playback-state user-modify-playback-state"
|
|
16
|
+
TOKEN_FILE = os.path.join(os.path.dirname(__file__), ".spotify_token.json")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _basic_auth_header():
|
|
20
|
+
auth_string = f"{CLIENT_ID}:{CLIENT_SECRET}".encode("utf-8")
|
|
21
|
+
return "Basic " + base64.b64encode(auth_string).decode("utf-8")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class _CallbackHandler(BaseHTTPRequestHandler):
|
|
25
|
+
code = None
|
|
26
|
+
|
|
27
|
+
def do_GET(self):
|
|
28
|
+
query = parse_qs(urlparse(self.path).query)
|
|
29
|
+
_CallbackHandler.code = query.get("code", [None])[0]
|
|
30
|
+
self.send_response(200)
|
|
31
|
+
self.send_header("Content-type", "text/html")
|
|
32
|
+
self.end_headers()
|
|
33
|
+
self.wfile.write(b"<h1>You can close this.</h1>")
|
|
34
|
+
|
|
35
|
+
def log_message(self, format, *args):
|
|
36
|
+
pass
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _request_authorization_code():
|
|
40
|
+
params = {
|
|
41
|
+
"client_id": CLIENT_ID,
|
|
42
|
+
"response_type": "code",
|
|
43
|
+
"redirect_uri": REDIRECT_URI,
|
|
44
|
+
"scope": SCOPES,
|
|
45
|
+
}
|
|
46
|
+
auth_url = "https://accounts.spotify.com/authorize?" + urlencode(params)
|
|
47
|
+
webbrowser.open(auth_url)
|
|
48
|
+
|
|
49
|
+
server = HTTPServer(("127.0.0.1", 8888), _CallbackHandler)
|
|
50
|
+
server.handle_request()
|
|
51
|
+
return _CallbackHandler.code
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _exchange_code_for_tokens(code):
|
|
55
|
+
url = "https://accounts.spotify.com/api/token"
|
|
56
|
+
headers = {
|
|
57
|
+
"Authorization": _basic_auth_header(),
|
|
58
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
59
|
+
}
|
|
60
|
+
data = {
|
|
61
|
+
"grant_type": "authorization_code",
|
|
62
|
+
"code": code,
|
|
63
|
+
"redirect_uri": REDIRECT_URI,
|
|
64
|
+
}
|
|
65
|
+
result = post(url, headers=headers, data=data)
|
|
66
|
+
result.raise_for_status()
|
|
67
|
+
return result.json()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _refresh_access_token(refresh_token):
|
|
71
|
+
url = "https://accounts.spotify.com/api/token"
|
|
72
|
+
headers = {
|
|
73
|
+
"Authorization": _basic_auth_header(),
|
|
74
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
75
|
+
}
|
|
76
|
+
data = {
|
|
77
|
+
"grant_type": "refresh_token",
|
|
78
|
+
"refresh_token": refresh_token,
|
|
79
|
+
}
|
|
80
|
+
result = post(url, headers=headers, data=data)
|
|
81
|
+
result.raise_for_status()
|
|
82
|
+
return result.json()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _save_tokens(tokens):
|
|
86
|
+
with open(TOKEN_FILE, "w") as f:
|
|
87
|
+
json.dump(tokens, f)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load_tokens():
|
|
91
|
+
if not os.path.exists(TOKEN_FILE):
|
|
92
|
+
return None
|
|
93
|
+
with open(TOKEN_FILE) as f:
|
|
94
|
+
return json.load(f)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def get_token():
|
|
98
|
+
if not CLIENT_ID or not CLIENT_SECRET:
|
|
99
|
+
raise RuntimeError("TOKEN_PUBLIC et TOKEN_PRIVATE doivent etre definis dans l'environnement")
|
|
100
|
+
|
|
101
|
+
tokens = _load_tokens()
|
|
102
|
+
|
|
103
|
+
if tokens and "refresh_token" in tokens:
|
|
104
|
+
new_tokens = _refresh_access_token(tokens["refresh_token"])
|
|
105
|
+
new_tokens.setdefault("refresh_token", tokens["refresh_token"])
|
|
106
|
+
_save_tokens(new_tokens)
|
|
107
|
+
return new_tokens["access_token"]
|
|
108
|
+
|
|
109
|
+
code = _request_authorization_code()
|
|
110
|
+
if not code:
|
|
111
|
+
raise RuntimeError("Aucun code recu, autorisation echouee")
|
|
112
|
+
|
|
113
|
+
tokens = _exchange_code_for_tokens(code)
|
|
114
|
+
_save_tokens(tokens)
|
|
115
|
+
return tokens["access_token"]
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from src.ctOS.music.get_token_tmp import get_token
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
PATH_TOKEN = "src/music/.spotify_token.json"
|
|
7
|
+
|
|
8
|
+
def init():
|
|
9
|
+
p = Path(".env")
|
|
10
|
+
if not p.exists():
|
|
11
|
+
print("Pas de .env detecter. Veuillez en créer un")
|
|
12
|
+
return 84
|
|
13
|
+
|
|
14
|
+
token_public = os.getenv("TOKEN_PUBLIC")
|
|
15
|
+
token_private = os.getenv("TOKEN_PRIVATE")
|
|
16
|
+
if not token_public or not token_private:
|
|
17
|
+
print("Pas de token detecter. Veuillez vous referer a la doc pour pouvoir en obtenir")
|
|
18
|
+
time.sleep(2)
|
|
19
|
+
return 84
|
|
20
|
+
|
|
21
|
+
p = Path(PATH_TOKEN)
|
|
22
|
+
if not p.exists():
|
|
23
|
+
print("Premier utilisation detecter. Redirection vers la connection spotify")
|
|
24
|
+
time.sleep(2)
|
|
25
|
+
get_token()
|
|
26
|
+
|
|
27
|
+
token_discord = os.getenv("API_KEY")
|
|
28
|
+
if not token_discord:
|
|
29
|
+
print("Pas de Clef API pour connecter Tranquility au bot discord. Si vous souhaitez l'utiliser inscrivez vous au bot discord et mettez la clef api dans le .env")
|
|
30
|
+
else:
|
|
31
|
+
print("Clef API est détécter. L'integration discord est donc disponible.")
|
|
32
|
+
time.sleep(2)
|
|
33
|
+
print("Tous est désormais initialisé. Veuillez faire ctOS --start")
|
|
34
|
+
time.sleep(2)
|
src/ctOS/music/music.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import json
|
|
3
|
+
import sys
|
|
4
|
+
from src.ctOS.music.get_token_tmp import get_token
|
|
5
|
+
import requests
|
|
6
|
+
from src.ctOS.music.get_id_device import get_id_device
|
|
7
|
+
from src.ctOS.music.pause import pause
|
|
8
|
+
from src.ctOS.music.resume import resume
|
|
9
|
+
|
|
10
|
+
def music_command():
|
|
11
|
+
nb = len(sys.argv)
|
|
12
|
+
|
|
13
|
+
if nb <= 2:
|
|
14
|
+
print("no argument give. Please give word")
|
|
15
|
+
return
|
|
16
|
+
if sys.argv[2] == "device":
|
|
17
|
+
get_id_device()
|
|
18
|
+
return
|
|
19
|
+
if sys.argv[2] == "pause":
|
|
20
|
+
pause()
|
|
21
|
+
return
|
|
22
|
+
if sys.argv[2] == "resume":
|
|
23
|
+
resume()
|
|
24
|
+
return
|
|
25
|
+
else:
|
|
26
|
+
print("no argument give. Please give word")
|
src/ctOS/music/pause.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from src.ctOS.music.get_token_tmp import get_token
|
|
3
|
+
|
|
4
|
+
def pause():
|
|
5
|
+
access_token = get_token()
|
|
6
|
+
url = "https://api.spotify.com/v1/me/player/pause"
|
|
7
|
+
header = {"Authorization": "Bearer " + access_token}
|
|
8
|
+
|
|
9
|
+
result = requests.put(url, headers=header)
|
|
10
|
+
try:
|
|
11
|
+
body = result.json()
|
|
12
|
+
except Exception:
|
|
13
|
+
body = result.text
|
|
14
|
+
print(body)
|
|
15
|
+
print(result.status_code)
|
src/ctOS/music/resume.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from src.ctOS.music.get_token_tmp import get_token
|
|
3
|
+
|
|
4
|
+
def resume():
|
|
5
|
+
access_token = get_token()
|
|
6
|
+
url = "https://api.spotify.com/v1/me/player/play"
|
|
7
|
+
header = {"Authorization": "Bearer " + access_token, "Content-Type": "application/json"}
|
|
8
|
+
data = {}
|
|
9
|
+
result = requests.put(url, headers=header, json=data)
|
|
10
|
+
try:
|
|
11
|
+
body = result.json()
|
|
12
|
+
except Exception:
|
|
13
|
+
body = result.text
|
|
14
|
+
print(body)
|
|
15
|
+
print(result.status_code)
|
src/ctOS/start/init.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import random
|
|
3
|
+
from collections import deque
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.layout import Layout
|
|
6
|
+
from rich.live import Live
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich.align import Align
|
|
9
|
+
from rich.panel import Panel
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
import os
|
|
12
|
+
from dotenv import load_dotenv
|
|
13
|
+
|
|
14
|
+
load_dotenv()
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
GREEN = "bold spring_green2"
|
|
18
|
+
DIM_GREEN = "spring_green4"
|
|
19
|
+
|
|
20
|
+
BOOT_LINES = [
|
|
21
|
+
"[INIT] Check env is created",
|
|
22
|
+
"[OK ] Env check",
|
|
23
|
+
"[INIT] Check env variable",
|
|
24
|
+
"[OK ] Variable good",
|
|
25
|
+
"[INIT] Check spotify section",
|
|
26
|
+
"[OK ] Token is good",
|
|
27
|
+
"[INIT] Check other command",
|
|
28
|
+
"[OK ] Other command good",
|
|
29
|
+
"[INIT] Start cli...",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
SPACE_LINE = [
|
|
33
|
+
" "
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
WAITING_LINE = [
|
|
37
|
+
"."
|
|
38
|
+
".."
|
|
39
|
+
"..."
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
START_LINE = [
|
|
43
|
+
"Hello from ctOS"
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
ERROR_LINE = "Error detected. First time to use ? Use ctOS --init"
|
|
47
|
+
|
|
48
|
+
def make_layout() -> Layout:
|
|
49
|
+
layout = Layout()
|
|
50
|
+
layout.split_column(
|
|
51
|
+
Layout(name="main", ratio=2),
|
|
52
|
+
Layout(name="bottom", size=9),
|
|
53
|
+
)
|
|
54
|
+
layout["bottom"].split_row(
|
|
55
|
+
Layout(name="bottom_left"),
|
|
56
|
+
)
|
|
57
|
+
return layout
|
|
58
|
+
|
|
59
|
+
def render_center(percent: int, width: int = 40) -> Align:
|
|
60
|
+
filled = int(width * percent / 100)
|
|
61
|
+
empty = width - filled
|
|
62
|
+
bar_str = "█" * filled + "░" * empty
|
|
63
|
+
text = Text(f"{bar_str} {percent:3d}%", style=GREEN, justify="center")
|
|
64
|
+
return Align.center(text, vertical="middle")
|
|
65
|
+
|
|
66
|
+
def render_status(history: deque) -> Panel:
|
|
67
|
+
body = Text("\n".join(history), style=DIM_GREEN)
|
|
68
|
+
return Panel(body, border_style=DIM_GREEN)
|
|
69
|
+
|
|
70
|
+
def check_file_env():
|
|
71
|
+
p = Path(".env")
|
|
72
|
+
if p.exists():
|
|
73
|
+
return 0
|
|
74
|
+
else:
|
|
75
|
+
return 84
|
|
76
|
+
|
|
77
|
+
def check_token():
|
|
78
|
+
p = Path("src/music/.spotify_token.json")
|
|
79
|
+
if p.exists():
|
|
80
|
+
return 0
|
|
81
|
+
else:
|
|
82
|
+
return 84
|
|
83
|
+
|
|
84
|
+
def check_variable():
|
|
85
|
+
token_public = os.getenv("TOKEN_PUBLIC")
|
|
86
|
+
token_private = os.getenv("TOKEN_PRIVATE")
|
|
87
|
+
if not token_public or not token_private:
|
|
88
|
+
return 84
|
|
89
|
+
else:
|
|
90
|
+
return 0
|
|
91
|
+
|
|
92
|
+
STEPS = [
|
|
93
|
+
("[INIT] Check env is created", None),
|
|
94
|
+
("[OK ] Env check", check_file_env),
|
|
95
|
+
("[INIT] Check env variable", None),
|
|
96
|
+
("[OK ] Variable good", check_variable),
|
|
97
|
+
("[INIT] Check spotify section", None),
|
|
98
|
+
("[OK ] Token is good", check_token),
|
|
99
|
+
("[INIT] Check other command", None),
|
|
100
|
+
("[OK ] Other command good", None),
|
|
101
|
+
("[INIT] Start cli...", None),
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
def demo():
|
|
105
|
+
layout = make_layout()
|
|
106
|
+
history = deque(maxlen=6)
|
|
107
|
+
|
|
108
|
+
with Live(layout, console=console, refresh_per_second=20, screen=True):
|
|
109
|
+
for i, (label, check) in enumerate(STEPS):
|
|
110
|
+
history.append(label)
|
|
111
|
+
|
|
112
|
+
if check is not None:
|
|
113
|
+
r = check()
|
|
114
|
+
if r == 84:
|
|
115
|
+
history.append(ERROR_LINE)
|
|
116
|
+
time.sleep(2)
|
|
117
|
+
return 84
|
|
118
|
+
|
|
119
|
+
percent = ((i + 1) * 100) // len(STEPS)
|
|
120
|
+
layout["main"].update(render_center(percent))
|
|
121
|
+
layout["bottom_left"].update(render_status(history))
|
|
122
|
+
time.sleep(1)
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
demo()
|
src/ctOS/start/start.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
|
|
2
|
+
import requests
|
|
3
|
+
import time
|
|
4
|
+
import random
|
|
5
|
+
from collections import deque
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.layout import Layout
|
|
8
|
+
from rich.live import Live
|
|
9
|
+
from rich.text import Text
|
|
10
|
+
from rich.align import Align
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
GREEN = "bold spring_green2"
|
|
15
|
+
DIM_GREEN = "spring_green4"
|
|
16
|
+
|
|
17
|
+
BOOT_LINES = [
|
|
18
|
+
"[INIT] Loading all module...",
|
|
19
|
+
"[NET ] Establishing uplink to backbone...",
|
|
20
|
+
"[OK ] All systems are onlines.",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
SPACE_LINE = [
|
|
24
|
+
" "
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
WAITING_LINE = [
|
|
28
|
+
"."
|
|
29
|
+
".."
|
|
30
|
+
"..."
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
START_LINE = [
|
|
34
|
+
"Hello from ctOS"
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
def make_layout() -> Layout:
|
|
38
|
+
layout = Layout()
|
|
39
|
+
layout.split_column(
|
|
40
|
+
Layout(name="main", ratio=2),
|
|
41
|
+
)
|
|
42
|
+
return layout
|
|
43
|
+
|
|
44
|
+
def render_status(history: deque) -> Panel:
|
|
45
|
+
body = Text("\n".join(history), style=DIM_GREEN)
|
|
46
|
+
return Panel(body, border_style=DIM_GREEN)
|
|
47
|
+
|
|
48
|
+
def start():
|
|
49
|
+
layout = make_layout()
|
|
50
|
+
history = deque(maxlen=6)
|
|
51
|
+
messages = BOOT_LINES
|
|
52
|
+
start = START_LINE
|
|
53
|
+
wait = WAITING_LINE
|
|
54
|
+
is_finish = False
|
|
55
|
+
|
|
56
|
+
with Live(layout, console=console, refresh_per_second=20, screen=True):
|
|
57
|
+
while is_finish is False:
|
|
58
|
+
history = None
|
|
59
|
+
history = deque(maxlen=6)
|
|
60
|
+
history.append(start[0])
|
|
61
|
+
layout["main"].update(render_status(history))
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# Refer to the Python quickstart on how to setup the environment:
|
|
2
|
+
# https://developers.google.com/workspace/calendar/quickstart/python
|
|
3
|
+
# Change the scope to 'https://www.googleapis.com/auth/calendar' and delete any
|
|
4
|
+
# stored credentials.
|
|
5
|
+
from googleapiclient.discovery import build
|
|
6
|
+
from src.subsystem.get_creds import get_creds
|
|
7
|
+
|
|
8
|
+
def create_event(title: str, description: str, startTime: str, endTime: str):
|
|
9
|
+
"""
|
|
10
|
+
Summary:
|
|
11
|
+
Create a event in your google calendar
|
|
12
|
+
Args:
|
|
13
|
+
title: just a title
|
|
14
|
+
description: just a description
|
|
15
|
+
startTime: start of your event like this '2026-09-09T09:00:00-07:00'
|
|
16
|
+
endTime: same type of startTime
|
|
17
|
+
"""
|
|
18
|
+
creds = get_creds()
|
|
19
|
+
service = build("calendar", "v3", credentials=creds)
|
|
20
|
+
event = {
|
|
21
|
+
'summary': title,
|
|
22
|
+
'location': 'None',
|
|
23
|
+
'description': description,
|
|
24
|
+
'start': {
|
|
25
|
+
'dateTime': startTime,
|
|
26
|
+
'timeZone': 'Europe/Paris',
|
|
27
|
+
},
|
|
28
|
+
'end': {
|
|
29
|
+
'dateTime': endTime,
|
|
30
|
+
'timeZone': 'Europe/Paris',
|
|
31
|
+
},
|
|
32
|
+
'recurrence': [
|
|
33
|
+
'RRULE:FREQ=DAILY;COUNT=1'
|
|
34
|
+
],
|
|
35
|
+
'reminders': {
|
|
36
|
+
'useDefault': False,
|
|
37
|
+
'overrides': [
|
|
38
|
+
{'method': 'email', 'minutes': 24 * 60},
|
|
39
|
+
{'method': 'popup', 'minutes': 10},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
event = service.events().insert(calendarId='primary', body=event).execute()
|
|
45
|
+
print('Event created: %s' % (event.get('htmlLink')))
|
|
46
|
+
return "'Event created: %s' % (event.get('htmlLink'))"
|
|
47
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from google.auth.transport.requests import Request
|
|
3
|
+
from google.oauth2.credentials import Credentials
|
|
4
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
5
|
+
from googleapiclient.discovery import build
|
|
6
|
+
from googleapiclient.errors import HttpError
|
|
7
|
+
|
|
8
|
+
SCOPES = ["https://www.googleapis.com/auth/calendar", "https://www.googleapis.com/auth/gmail.modify",]
|
|
9
|
+
|
|
10
|
+
def get_creds():
|
|
11
|
+
"""
|
|
12
|
+
Summary:
|
|
13
|
+
Create or check your actual token. If not exist, Tranquillity create it
|
|
14
|
+
"""
|
|
15
|
+
creds = None
|
|
16
|
+
if os.path.exists("token.json"):
|
|
17
|
+
creds = Credentials.from_authorized_user_file("token.json", SCOPES)
|
|
18
|
+
# If there are no (valid) credentials available, let the user log in.
|
|
19
|
+
if not creds or not creds.valid:
|
|
20
|
+
if creds and creds.expired and creds.refresh_token:
|
|
21
|
+
creds.refresh(Request())
|
|
22
|
+
else:
|
|
23
|
+
flow = InstalledAppFlow.from_client_secrets_file(
|
|
24
|
+
"./settings/credentials.json", SCOPES
|
|
25
|
+
)
|
|
26
|
+
creds = flow.run_local_server(port=0)
|
|
27
|
+
# Save the credentials for the next run
|
|
28
|
+
with open("token.json", "w") as token:
|
|
29
|
+
token.write(creds.to_json())
|
|
30
|
+
return creds
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import os
|
|
3
|
+
from google.auth.transport.requests import Request
|
|
4
|
+
from google.oauth2.credentials import Credentials
|
|
5
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
6
|
+
from googleapiclient.discovery import build
|
|
7
|
+
from googleapiclient.errors import HttpError
|
|
8
|
+
import base64
|
|
9
|
+
import html2text
|
|
10
|
+
from src.subsystem.get_creds import get_creds
|
|
11
|
+
|
|
12
|
+
def get_header(payload, name):
|
|
13
|
+
for h in payload["headers"]:
|
|
14
|
+
if h["name"].lower() == name.lower():
|
|
15
|
+
return h["value"]
|
|
16
|
+
return ""
|
|
17
|
+
|
|
18
|
+
def html_to_text(html_content: str) -> str:
|
|
19
|
+
h = html2text.HTML2Text()
|
|
20
|
+
h.ignore_links = True
|
|
21
|
+
h.ignore_images = True
|
|
22
|
+
h.body_width = 0
|
|
23
|
+
return h.handle(html_content).strip()
|
|
24
|
+
|
|
25
|
+
def get_body(payload):
|
|
26
|
+
plain_text = None
|
|
27
|
+
html_text = None
|
|
28
|
+
|
|
29
|
+
if "parts" in payload:
|
|
30
|
+
for part in payload["parts"]:
|
|
31
|
+
# gère aussi les parts imbriquées (multipart/alternative dans multipart/mixed)
|
|
32
|
+
if part.get("parts"):
|
|
33
|
+
nested = get_body(part)
|
|
34
|
+
if nested:
|
|
35
|
+
return nested
|
|
36
|
+
if part["mimeType"] == "text/plain" and not plain_text:
|
|
37
|
+
data = part["body"].get("data")
|
|
38
|
+
if data:
|
|
39
|
+
plain_text = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
|
|
40
|
+
elif part["mimeType"] == "text/html" and not html_text:
|
|
41
|
+
data = part["body"].get("data")
|
|
42
|
+
if data:
|
|
43
|
+
html_text = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
|
|
44
|
+
else:
|
|
45
|
+
data = payload.get("body", {}).get("data")
|
|
46
|
+
if data:
|
|
47
|
+
decoded = base64.urlsafe_b64decode(data).decode("utf-8", errors="replace")
|
|
48
|
+
if payload.get("mimeType") == "text/html":
|
|
49
|
+
html_text = decoded
|
|
50
|
+
else:
|
|
51
|
+
plain_text = decoded
|
|
52
|
+
|
|
53
|
+
if plain_text and plain_text.strip():
|
|
54
|
+
return plain_text.strip()
|
|
55
|
+
if html_text:
|
|
56
|
+
return html_to_text(html_text)
|
|
57
|
+
return ""
|
|
58
|
+
|
|
59
|
+
def get_email(number: int):
|
|
60
|
+
"""Shows basic usage of the Gmail API.
|
|
61
|
+
Lists the user's Gmail labels.
|
|
62
|
+
"""
|
|
63
|
+
creds = get_creds()
|
|
64
|
+
try:
|
|
65
|
+
# Call the Gmail API and limit the result to the requested number of emails.
|
|
66
|
+
service = build("gmail", "v1", credentials=creds)
|
|
67
|
+
results = (
|
|
68
|
+
service.users().messages().list(
|
|
69
|
+
userId="me", labelIds=["INBOX"], maxResults=number
|
|
70
|
+
).execute()
|
|
71
|
+
)
|
|
72
|
+
messages = results.get("messages", [])
|
|
73
|
+
|
|
74
|
+
if not messages:
|
|
75
|
+
print("No messages found.")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
print("Messages:")
|
|
79
|
+
content = []
|
|
80
|
+
for message in messages:
|
|
81
|
+
print(f'Message ID: {message["id"]}')
|
|
82
|
+
msg = (
|
|
83
|
+
service.users().messages().get(
|
|
84
|
+
userId="me", id=message["id"], format="full"
|
|
85
|
+
).execute()
|
|
86
|
+
)
|
|
87
|
+
subject = get_header(msg["payload"], "Subject")
|
|
88
|
+
date = get_header(msg["payload"], "Date")
|
|
89
|
+
sender = get_header(msg["payload"], "From")
|
|
90
|
+
body = get_body(msg["payload"])
|
|
91
|
+
print(sender)
|
|
92
|
+
print(date)
|
|
93
|
+
print(subject)
|
|
94
|
+
print(body)
|
|
95
|
+
content.append(f"Sender : {sender}.\n Date : {date}.\n Subject: {subject}.\n Body: {body}\n")
|
|
96
|
+
return "\n".join(content)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
except HttpError as error:
|
|
100
|
+
# TODO(developer) - Handle errors from gmail API.
|
|
101
|
+
print(f"An error occurred: {error}")
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
from google.auth.transport.requests import Request
|
|
3
|
+
from google.oauth2.credentials import Credentials
|
|
4
|
+
from google_auth_oauthlib.flow import InstalledAppFlow
|
|
5
|
+
from googleapiclient.discovery import build
|
|
6
|
+
from googleapiclient.errors import HttpError
|
|
7
|
+
from src.subsystem.get_creds import get_creds
|
|
8
|
+
from datetime import datetime as normaldatetime
|
|
9
|
+
from zoneinfo import ZoneInfo
|
|
10
|
+
|
|
11
|
+
# If modifying these scopes, delete the file token.json.
|
|
12
|
+
SCOPES = ["https://www.googleapis.com/auth/calendar.readonly", "https://www.googleapis.com/auth/gmail.readonly",]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_event_time(iso_string: str) -> str:
|
|
16
|
+
if "T" not in iso_string:
|
|
17
|
+
# événement toute la journée, pas d'heure
|
|
18
|
+
return iso_string
|
|
19
|
+
dt_utc = normaldatetime.fromisoformat(iso_string.replace("Z", "+00:00"))
|
|
20
|
+
dt_paris = dt_utc.astimezone(ZoneInfo("Europe/Paris"))
|
|
21
|
+
return dt_paris.strftime("%Y-%m-%d %H:%M")
|
|
22
|
+
|
|
23
|
+
def list_calendar(number: int):
|
|
24
|
+
"""Shows basic usage of the Google Calendar API.
|
|
25
|
+
Prints the start and name of the next 10 events on the user's calendar.
|
|
26
|
+
"""
|
|
27
|
+
creds = get_creds()
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
service = build("calendar", "v3", credentials=creds)
|
|
31
|
+
|
|
32
|
+
# Call the Calendar API
|
|
33
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc).isoformat()
|
|
34
|
+
print("Getting the upcoming 10 events")
|
|
35
|
+
events_result = (
|
|
36
|
+
service.events()
|
|
37
|
+
.list(
|
|
38
|
+
calendarId="primary",
|
|
39
|
+
timeMin=now,
|
|
40
|
+
maxResults=number,
|
|
41
|
+
singleEvents=True,
|
|
42
|
+
orderBy="startTime",
|
|
43
|
+
)
|
|
44
|
+
.execute()
|
|
45
|
+
)
|
|
46
|
+
events = events_result.get("items", [])
|
|
47
|
+
|
|
48
|
+
if not events:
|
|
49
|
+
print("No upcoming events found.")
|
|
50
|
+
return ""
|
|
51
|
+
|
|
52
|
+
content = []
|
|
53
|
+
# Prints the start and name of the next 10 events
|
|
54
|
+
for event in events:
|
|
55
|
+
start = event["start"].get("dateTime", event["start"].get("date"))
|
|
56
|
+
print(format_event_time(start), event["summary"])
|
|
57
|
+
temp_content = f"{start} - {event["summary"]}"
|
|
58
|
+
content.append(temp_content)
|
|
59
|
+
return "\n".join(content)
|
|
60
|
+
|
|
61
|
+
except HttpError as error:
|
|
62
|
+
print(f"An error occurred: {error}")
|
|
63
|
+
|
|
64
|
+
if __name__ == "__main__":
|
|
65
|
+
list_calendar(5)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import requests
|
|
4
|
+
import secrets
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
import asyncio
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
async def send_discord_message(text: str):
|
|
10
|
+
async with httpx.AsyncClient() as client:
|
|
11
|
+
r = await client.post(
|
|
12
|
+
"https://gla-dos-prod.up.railway.app/discord/say",
|
|
13
|
+
json={"text": text},
|
|
14
|
+
headers={"X-Glados-Key": os.getenv("API_KEY")},
|
|
15
|
+
)
|
|
16
|
+
r.raise_for_status()
|
|
17
|
+
return r.json()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import requests
|
|
3
|
+
|
|
4
|
+
def search_web(query: str) -> str:
|
|
5
|
+
api_key = os.getenv("NEWSAPI_KEY")
|
|
6
|
+
if not api_key:
|
|
7
|
+
return "Erreur : clé NEWSAPI_KEY manquante dans les variables d'environnement."
|
|
8
|
+
|
|
9
|
+
mots_generaux = ["actualité", "actualités", "info", "infos", "news", "dernières", "nouvelles"]
|
|
10
|
+
is_general = any(mot in query.lower() for mot in mots_generaux)
|
|
11
|
+
q = "France" if is_general else query
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
resp = requests.get(
|
|
15
|
+
"https://newsapi.org/v2/everything",
|
|
16
|
+
params={
|
|
17
|
+
"q": q,
|
|
18
|
+
"language": "fr",
|
|
19
|
+
"sortBy": "publishedAt",
|
|
20
|
+
"pageSize": 5,
|
|
21
|
+
"apiKey": api_key,
|
|
22
|
+
},
|
|
23
|
+
timeout=8,
|
|
24
|
+
)
|
|
25
|
+
data = resp.json()
|
|
26
|
+
|
|
27
|
+
if data.get("status") != "ok":
|
|
28
|
+
return f"Erreur API actualités : {data.get('message', 'inconnue')}"
|
|
29
|
+
|
|
30
|
+
articles = data.get("articles", [])
|
|
31
|
+
if not articles:
|
|
32
|
+
return "Aucun résultat trouvé pour cette recherche."
|
|
33
|
+
|
|
34
|
+
lignes = []
|
|
35
|
+
for a in articles:
|
|
36
|
+
titre = a.get("title", "").strip()
|
|
37
|
+
source = a.get("source", {}).get("name", "")
|
|
38
|
+
description = (a.get("description") or "").strip()
|
|
39
|
+
if titre:
|
|
40
|
+
ligne = f"- {titre} ({source})"
|
|
41
|
+
if description:
|
|
42
|
+
ligne += f" : {description}"
|
|
43
|
+
lignes.append(ligne)
|
|
44
|
+
|
|
45
|
+
return "\n".join(lignes) if lignes else "Aucun résultat exploitable trouvé."
|
|
46
|
+
|
|
47
|
+
except requests.exceptions.RequestException as e:
|
|
48
|
+
return f"Erreur réseau lors de la recherche : {e}"
|
src/tranquillity.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from threading import Thread
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from src.core.gemini import gemini
|
|
5
|
+
from src.subsystem.autonome_sys.autonome import autonome_chain
|
|
6
|
+
|
|
7
|
+
def init():
|
|
8
|
+
autonome = Thread(target=autonome_chain, daemon=True)
|
|
9
|
+
autonome.start()
|
|
10
|
+
gemini()
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: tranquillity
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Provides-Extra: app
|
|
5
|
+
Requires-Dist: opencv-contrib-python; extra == "app"
|
|
6
|
+
Requires-Dist: requests; extra == "app"
|
|
7
|
+
Requires-Dist: ollama; extra == "app"
|
|
8
|
+
Requires-Dist: piper-tts; extra == "app"
|
|
9
|
+
Requires-Dist: vosk; extra == "app"
|
|
10
|
+
Requires-Dist: rapidfuzz; extra == "app"
|
|
11
|
+
Requires-Dist: sounddevice; extra == "app"
|
|
12
|
+
Requires-Dist: pyyaml; extra == "app"
|
|
13
|
+
Requires-Dist: spacy; extra == "app"
|
|
14
|
+
Requires-Dist: rich; extra == "app"
|
|
15
|
+
Requires-Dist: python-dotenv; extra == "app"
|
|
16
|
+
Requires-Dist: click; extra == "app"
|
|
17
|
+
Requires-Dist: google-generativeai; extra == "app"
|
|
18
|
+
Requires-Dist: google-genai; extra == "app"
|
|
19
|
+
Requires-Dist: google-api-python-client; extra == "app"
|
|
20
|
+
Requires-Dist: google-auth-httplib2; extra == "app"
|
|
21
|
+
Requires-Dist: google-auth-oauthlib; extra == "app"
|
|
22
|
+
Requires-Dist: html2text; extra == "app"
|
|
23
|
+
Requires-Dist: feedparser; extra == "app"
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
ctOS.py,sha256=5852cYA5lVpGJrEGc_SjoJEUCwnpbEzidbeGgFIOW_M,1032
|
|
2
|
+
docker_launcher.py,sha256=HosJFPTQi31-F16bSkybnNIezSQ4oczd0jURKpBQ9Nk,985
|
|
3
|
+
settings/settings.py,sha256=SBKpmH8fffXVM_RGimK5v7smRzOHJ2OYFjbWU1tzjZc,193
|
|
4
|
+
src/tranquillity.py,sha256=pPZSyQxZaQcXYTmmpJnk37yYbHUNB6IreJcqHxW9zco,267
|
|
5
|
+
src/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
|
+
src/core/gemini.py,sha256=osnpQTR2GiDLqu9Qcf43TfVVgO3UuPZ3qQB2GxShEW8,8301
|
|
7
|
+
src/ctOS/help.py,sha256=TwRGZJIzUaoDqtPMbjYHGc60fRDTyxE-N-xj9Qgb4o8,1112
|
|
8
|
+
src/ctOS/light/light.py,sha256=ENIpnAXPbhkngL-EaLUwxGRf9kkVOLE0DNVu3q53kdM,335
|
|
9
|
+
src/ctOS/light/toggle_light.py,sha256=iG78Cs8WLRjC8mYyhHwbUZNUf8odZOIWGhHt5VgxplA,410
|
|
10
|
+
src/ctOS/music/get_id_device.py,sha256=osRZqpmZpO80sTAUvr-ZlpgIDItcekupFhcSuakGY-o,424
|
|
11
|
+
src/ctOS/music/get_token_tmp.py,sha256=vZj9VeDWryg4oOrIM4lA0deHpm9l4TqXItS-30rpzcA,3316
|
|
12
|
+
src/ctOS/music/init_token.py,sha256=GF1K0785vT1cnxsTHtcxQH8JIp8gLFcy4RjYhnlINJ4,1181
|
|
13
|
+
src/ctOS/music/music.py,sha256=8Q38Zt6AW7o2RLq6XUHVwwHXazVJRcPnf9huRzfQjVc,630
|
|
14
|
+
src/ctOS/music/pause.py,sha256=mVilxUK8ykWZhwj6wUkrm7SQNW52vAoRvJ-ciYzGOJU,404
|
|
15
|
+
src/ctOS/music/resume.py,sha256=vuBsDPEvIIDzm1Z1HQDdFQGUv5ec7T8hIlkyT2EoyiM,464
|
|
16
|
+
src/ctOS/start/init.py,sha256=f7hCI4-r0T7x_QSegNpm-F4aaSm8MmYcWqjUAHqAuNg,3142
|
|
17
|
+
src/ctOS/start/start.py,sha256=TPQpgpqYM4nxy-GJkdVbEfr7hBzZvDszAZ_kqB5f0Bc,1302
|
|
18
|
+
src/subsystem/create_event.py,sha256=DcQMIWDgAyE5fZ6W3ARyymMIpYTXslp7d4J-MQVqtJQ,1499
|
|
19
|
+
src/subsystem/get_creds.py,sha256=abSccAwzhYNT7DINRXM2cvOD358i1QdSoSr9lzQanfs,1158
|
|
20
|
+
src/subsystem/get_email.py,sha256=vUXQjIb6NveiU78QvzTOIRqXjls2mnnZgvYMxUM7F24,3345
|
|
21
|
+
src/subsystem/list_event.py,sha256=mJXiG54LBTUZ4lGDYdNJcGk6frYimlMCCYpAhzpyoAM,2127
|
|
22
|
+
src/subsystem/notification_system.py,sha256=r1MyBn5S0EJnabGmcnOYfd-aF5GgyOOYQWWFL_hRvLs,446
|
|
23
|
+
src/subsystem/search_web.py,sha256=-64AejTzF0B1G8XVsLwgCcroOSSOSrWVzMpRfRi3ecI,1635
|
|
24
|
+
src/subsystem/autonome_sys/autonome.py,sha256=1wxQqIspCuF0w5_gOAsKDn9y8aQ-Bt5g_9QIfV4Oj00,83
|
|
25
|
+
tranquillity-0.1.0.dist-info/METADATA,sha256=I6H2xRytqQMnnW4VwHDQjDOw49qD04Xw-GJBjbdqtsw,899
|
|
26
|
+
tranquillity-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
27
|
+
tranquillity-0.1.0.dist-info/entry_points.txt,sha256=v9iaIcdDzmX2dsMrhd-Hmyf8vejSH7wRrTS_hHbdmKI,54
|
|
28
|
+
tranquillity-0.1.0.dist-info/top_level.txt,sha256=YxzuiHTGqHg_QHX9c0dchVO2jyu-gNFySLi324i9ZEo,34
|
|
29
|
+
tranquillity-0.1.0.dist-info/RECORD,,
|