posix4e commited on
Commit
516b1f5
·
1 Parent(s): dc7f995

Actually send the puppet reads

Browse files

Deploy to hugging face
Add history
Add commands

.github/workflows/cd.yml ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+ on:
3
+ push:
4
+ branches: [main]
5
+
6
+ # to run this workflow manually from the Actions tab
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ sync-to-hub:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v3
14
+ with:
15
+ fetch-depth: 0
16
+ lfs: true
17
+ - name: Push to hub
18
+ env:
19
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
20
+ run:
21
+ git push https://posix4e:$HF_TOKEN@huggingface.co/spaces/posix4e/puppet main
.github/workflows/release_ci.yml CHANGED
@@ -32,14 +32,6 @@ jobs:
32
  distribution: 'temurin'
33
  cache: gradle
34
 
35
- - name: Create file
36
- run: cat /home/runner/work/puppet/puppet/puppet/app/google-services.json | base64
37
-
38
- - name: Putting data
39
- env:
40
- DATA: ${{ secrets.GOOGLE_SERVICES_JSON }}
41
- run: echo $DATA > /home/runner/work/puppet/puppet/puppet/app/google-services.json
42
-
43
  - name: Grant execute permission for gradlew
44
  run: chmod +x gradlew
45
 
 
32
  distribution: 'temurin'
33
  cache: gradle
34
 
 
 
 
 
 
 
 
 
35
  - name: Grant execute permission for gradlew
36
  run: chmod +x gradlew
37
 
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ **/*.swp
2
+ .vscode
README.md CHANGED
@@ -1 +1,23 @@
1
- # puppet
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: puppet
3
+ colorTo: indigo
4
+ app_file: backend/backend.py
5
+ sdk: gradio
6
+ sdk_version: 3.36.1
7
+ python_version: 3.11.2
8
+ pinned: false
9
+ license: mit
10
+ ---
11
+
12
+ Fun!
13
+
14
+ ## MVP
15
+ - [ ] switch to only rpcing every minute
16
+ - [ ] Run commands on android
17
+ - [x] Fix assist on android
18
+ - [ ] Testing
19
+ - [ ] Docs
20
+
21
+ ## Todo Soon
22
+
23
+ - [ ] Other clients (browser extension/watch)
backend/backend.py CHANGED
@@ -1,144 +1,386 @@
1
- from fastapi import FastAPI, HTTPException
2
- from pydantic import BaseModel
3
- from firebase_admin import credentials, messaging, initialize_app, auth
4
- import openai
5
- import asyncio
6
  import gradio as gr
7
- from fastapi.middleware.wsgi import WSGIMiddleware
8
- from fastapi.staticfiles import StaticFiles
9
- import uvicorn
10
  from dotenv import load_dotenv
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
12
  load_dotenv()
13
 
 
 
 
14
 
15
- app = FastAPI()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
- # User credentials will be stored in this dictionary
18
- user_data = {}
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
 
21
  class RegisterItem(BaseModel):
22
- apiKey: str
23
- authDomain: str
24
- databaseURL: str
25
- storageBucket: str
26
 
27
 
28
  @app.post("/register")
29
  async def register(item: RegisterItem):
30
- # Firebase initialization with user-specific credentials
31
- cred = credentials.Certificate(
32
- {
33
- "apiKey": item.apiKey,
34
- "authDomain": item.authDomain,
35
- "databaseURL": item.databaseURL,
36
- "storageBucket": item.storageBucket,
37
- }
38
- )
39
- firebase_app = initialize_app(cred, name=str(len(user_data)))
40
- # Add the Firebase app and auth details to the user_data dictionary
41
- user_data[str(len(user_data))] = {
42
- "firebase_app": firebase_app,
43
- "authDomain": item.authDomain,
44
- }
45
- return {"uid": str(len(user_data) - 1)} # Return the user ID
46
 
47
 
48
- class ProcessItem(BaseModel):
49
  uid: str
50
  prompt: str
 
51
 
52
 
53
- @app.post("/process_request")
54
- async def process_request(item: ProcessItem):
55
- # Get the user's Firebase app from the user_data dictionary
56
- firebase_app = user_data.get(item.uid, {}).get("firebase_app", None)
57
- authDomain = user_data.get(item.uid, {}).get("authDomain", None)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
 
59
- if not firebase_app or not authDomain:
 
 
 
 
 
60
  raise HTTPException(status_code=400, detail="Invalid uid")
61
 
62
  # Call OpenAI
63
- response = openai.Completion.create(
64
- engine="text-davinci-002", prompt=item.prompt, max_tokens=150
 
 
 
 
 
 
 
65
  )
66
 
67
- # The message data that will be sent to the client
68
- message = messaging.Message(
69
- data={
70
- "message": response.choices[0].text.strip(),
71
- },
72
- topic="updates",
73
- app=firebase_app, # Use the user-specific Firebase app
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  )
 
75
 
76
- # Send the message asynchronously
77
- asyncio.run(send_notification(message))
78
 
79
- return {"message": "Notification sent"}
 
 
 
 
 
80
 
81
 
82
- def send_notification(message):
83
- # Send a message to the devices subscribed to the provided topic.
84
- response = messaging.send(message)
85
- print("Successfully sent message:", response)
 
 
 
86
 
87
 
88
- def gradio_interface():
89
- def register(apiKey, authDomain, databaseURL, storageBucket):
90
- response = requests.post(
91
- "http://localhost:8000/register",
92
- json={
93
- "apiKey": apiKey,
94
- "authDomain": authDomain,
95
- "databaseURL": databaseURL,
96
- "storageBucket": storageBucket,
97
- },
98
- )
99
- return response.json()
100
 
101
- def process_request(uid, prompt):
102
- response = requests.post(
103
- "http://localhost:8000/process_request", json={"uid": uid, "prompt": prompt}
104
- )
105
- return response.json()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
106
 
107
- demo = gr.Interface(
108
- fn=[register, process_request],
109
  inputs=[
110
- [
111
- gr.inputs.Textbox(label="apiKey"),
112
- gr.inputs.Textbox(label="authDomain"),
113
- gr.inputs.Textbox(label="databaseURL"),
114
- gr.inputs.Textbox(label="storageBucket"),
115
- ],
116
- [gr.inputs.Textbox(label="uid"), gr.inputs.Textbox(label="prompt")],
117
  ],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
118
  outputs="json",
119
- title="API Explorer",
120
- description="Use this tool to make requests to the Register and Process Request APIs",
121
  )
122
- return demo
123
 
124
 
125
- def process_request_interface(uid, prompt):
126
- item = ProcessItem(uid=uid, prompt=prompt)
127
- response = process_request(item)
128
- return response
 
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- def get_gradle_interface():
 
132
  return gr.Interface(
133
- fn=process_request_interface,
134
  inputs=[
135
  gr.inputs.Textbox(label="UID", type="text"),
136
- gr.inputs.Textbox(label="Prompt", type="text"),
137
  ],
138
- outputs="text",
139
- title="OpenAI Text Generation",
140
- description="Generate text using OpenAI's GPT-3 model.",
141
  )
142
 
143
 
144
- app = gr.mount_gradio_app(app, get_gradle_interface(), path="/")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import uuid
3
+ from datetime import datetime
4
+
 
5
  import gradio as gr
6
+ import mistune
7
+ import openai
 
8
  from dotenv import load_dotenv
9
+ from fastapi import FastAPI, HTTPException
10
+ from fastapi.testclient import TestClient
11
+ from pydantic import BaseModel
12
+ from pygments import highlight
13
+ from pygments.formatters import html
14
+ from pygments.lexers import get_lexer_by_name
15
+ from sqlalchemy import JSON, Column, Integer, String, create_engine
16
+ from sqlalchemy.ext.declarative import declarative_base
17
+ from sqlalchemy.orm import Session, sessionmaker
18
+ from sqlalchemy.sql import insert, select, text
19
+ from uvicorn import Config, Server
20
+ from fastapi.middleware.gzip import GZipMiddleware
21
+
22
+ LANGS = [
23
+ "text-davinci-002:100",
24
+ "text-davinci-003:1500",
25
+ "gpt-3.5-turbo:4000",
26
+ "gpt-4:6000",
27
+ ]
28
+
29
+ Base = declarative_base()
30
+
31
+
32
+ class User(Base):
33
+ __tablename__ = "user_data"
34
+
35
+ id = Column(Integer, primary_key=True, autoincrement=True)
36
+ uid = Column(String, nullable=False)
37
+ openai_key = Column(String)
38
+
39
+ def __repr__(self):
40
+ return f"User(id={self.id}, uid={self.uid}"
41
+
42
+
43
+ class History(Base):
44
+ __tablename__ = "history"
45
+
46
+ id = Column(Integer, primary_key=True, autoincrement=True)
47
+ uid = Column(String, nullable=False)
48
+ question = Column(String, nullable=False)
49
+ answer = Column(JSON, nullable=False)
50
+
51
+ def __repr__(self):
52
+ return f"History(id={self.id}, uid={self.uid}, question={self.question}, answer={self.answer}"
53
+
54
+
55
+ # Add a new table to store the commands
56
+ class Command(Base):
57
+ __tablename__ = "commands"
58
+
59
+ id = Column(Integer, primary_key=True, autoincrement=True)
60
+ uid = Column(String, nullable=False)
61
+ command = Column(String, nullable=False)
62
+ status = Column(String, nullable=False, default="queued")
63
+
64
+ def __repr__(self):
65
+ return f"self.command"
66
+
67
+
68
+ engine = create_engine("sqlite:///puppet.db")
69
+ Base.metadata.create_all(bind=engine)
70
+ SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
71
 
72
  load_dotenv()
73
 
74
+ app = FastAPI(debug=True)
75
+ app.add_middleware(GZipMiddleware, minimum_size=1000)
76
+
77
 
78
+ # Add a new API endpoint to add commands to the queue
79
+ class CommandItem(BaseModel):
80
+ uid: str
81
+ command: str
82
+
83
+
84
+ @app.post("/add_command")
85
+ async def add_command(item: CommandItem):
86
+ db: Session = SessionLocal()
87
+ new_command = Command(uid=item.uid, command=item.command)
88
+ db.add(new_command)
89
+ db.commit()
90
+ db.refresh(new_command)
91
+ return {"message": "Command added"}
92
+
93
+
94
+ class EventItem(BaseModel):
95
+ uid: str
96
+ event: str
97
+
98
+
99
+ @app.post("/send_event")
100
+ async def send_event(item: EventItem):
101
+ print(f"Received event from {item.uid}:\n{item.event}")
102
+
103
+ with open(f"{item.uid}_events.txt", "a") as f:
104
+ f.write(f"{datetime.now()} - {item.event}\n")
105
+
106
+ db: Session = SessionLocal()
107
+ user = db.query(User).filter(User.uid == item.uid).first()
108
+ if not user:
109
+ raise HTTPException(status_code=400, detail="Invalid uid")
110
+
111
+ # Update the last time send_event was called and increment the number of events
112
+ user.last_event = datetime.now()
113
+ db.commit()
114
 
115
+ # Get all the queued commands for this user
116
+ commands = (
117
+ db.query(Command)
118
+ .filter(Command.uid == item.uid, Command.status == "queued")
119
+ .all()
120
+ )
121
+ for command in commands:
122
+ command.status = "running"
123
+ db.commit()
124
+
125
+ return {
126
+ "message": "Event received",
127
+ "commands": [command.command for command in commands],
128
+ }
129
 
130
 
131
  class RegisterItem(BaseModel):
132
+ openai_key: str
 
 
 
133
 
134
 
135
  @app.post("/register")
136
  async def register(item: RegisterItem):
137
+ db: Session = SessionLocal()
138
+ new_user = User(uid=str(uuid.uuid4()), openai_key=item.openai_key)
139
+ db.add(new_user)
140
+ db.commit()
141
+ db.refresh(new_user)
142
+ return {"uid": new_user.uid}
 
 
 
 
 
 
 
 
 
 
143
 
144
 
145
+ class AssistItem(BaseModel):
146
  uid: str
147
  prompt: str
148
+ version: str
149
 
150
 
151
+ def generate_quick_completion(prompt, model):
152
+ dropdrown = model.split(":")
153
+ engine = dropdrown[0]
154
+ max_tokens = int(dropdrown[1])
155
+ if "gpt" in model:
156
+ message = [{"role": "user", "content": prompt}]
157
+ response = openai.ChatCompletion.create(
158
+ model=engine,
159
+ messages=message,
160
+ temperature=0.2,
161
+ max_tokens=max_tokens,
162
+ frequency_penalty=0.0,
163
+ )
164
+ elif "davinci" in model:
165
+ response = openai.Completion.create(
166
+ engine=engine, prompt=prompt, max_tokens=max_tokens
167
+ )
168
+ else:
169
+ raise Exception("Unknown model")
170
+ return response
171
 
172
+
173
+ @app.post("/assist")
174
+ async def assist(item: AssistItem):
175
+ db: Session = SessionLocal()
176
+ user = db.query(User).filter(User.uid == item.uid).first()
177
+ if not user:
178
  raise HTTPException(status_code=400, detail="Invalid uid")
179
 
180
  # Call OpenAI
181
+ openai.api_key = user.openai_key
182
+ response = generate_quick_completion(item.prompt, item.version)
183
+
184
+ # Update the last time assist was called
185
+ user.last_assist = datetime.now()
186
+
187
+ # Store the history
188
+ new_history = History(
189
+ uid=item.uid, question=item.prompt, answer=json.loads(str(response))
190
  )
191
 
192
+ db.add(new_history)
193
+ db.commit()
194
+
195
+ return response
196
+
197
+
198
+ @app.get("/get_history/{uid}")
199
+ async def get_history(uid: str):
200
+ db: Session = SessionLocal()
201
+ history = db.query(History).filter(History.uid == uid).all()
202
+ commands = db.query(Command).filter(Command.uid == uid).all()
203
+ if not history:
204
+ raise HTTPException(status_code=400, detail="No history found for this uid")
205
+ ret = {"history": [h.__dict__ for h in history]}
206
+
207
+ if commands and len(commands) > 0:
208
+ try:
209
+ with open(f"{uid}_events.txt", "r") as f:
210
+ events = f.read().split()
211
+ except FileNotFoundError:
212
+ events = None
213
+
214
+ return {
215
+ "events": events,
216
+ "history": [h.__dict__ for h in history],
217
+ "commands": [c.__dict__ for c in commands],
218
+ }
219
+
220
+ else:
221
+ return {"history": [h.__dict__ for h in history]}
222
+
223
+
224
+ def assist_interface(uid, prompt, gpt_version):
225
+ client = TestClient(app)
226
+
227
+ response = client.post(
228
+ "/assist",
229
+ json={"uid": uid, "prompt": prompt, "version": gpt_version},
230
  )
231
+ return display_json(response.text)
232
 
 
 
233
 
234
+ def get_user_interface(uid):
235
+ db: Session = SessionLocal()
236
+ user = db.query(User).filter(User.uid == uid).first()
237
+ if not user:
238
+ return {"message": "No user with this uid found"}
239
+ return str(user)
240
 
241
 
242
+ class HighlightRenderer(mistune.HTMLRenderer):
243
+ def block_code(self, code, info=None):
244
+ if info:
245
+ lexer = get_lexer_by_name(info, stripall=True)
246
+ formatter = html.HtmlFormatter()
247
+ return highlight(code, lexer, formatter)
248
+ return "<pre><code>" + mistune.escape(code) + "</code></pre>"
249
 
250
 
251
+ def display_json(data):
252
+ html_output = "<html>"
253
+ json_data = json.loads(data)
 
 
 
 
 
 
 
 
 
254
 
255
+ id = json_data["id"]
256
+ object = json_data["object"]
257
+ created = json_data["created"]
258
+ model = json_data["model"]
259
+ choices = json_data["choices"]
260
+
261
+ html_output += f"<h2>ID: {id}</h2>"
262
+ html_output += f"<p>Object: {object}</p>"
263
+ html_output += f"<p>Created: {created}</p>"
264
+ html_output += f"<p>Model: {model}</p>"
265
+
266
+ html_output += "<h3>Choices:</h3>"
267
+ if "davinci" in model:
268
+ for choice in choices:
269
+ text = choice["text"]
270
+ html_output += f"<p>Text: {text}</p>"
271
+ elif "gpt" in model:
272
+ for choice in choices:
273
+ try:
274
+ markdown = mistune.create_markdown(renderer=HighlightRenderer())
275
+ text = markdown(choice["message"]["content"])
276
+ html_output += f"<p>Text: {text}</p>"
277
+ except:
278
+ markdown = mistune.create_markdown()
279
+ text = markdown(choice["message"]["content"])
280
+ html_output += f"<p>Text: {text}</p>"
281
+
282
+ html_output += "</html>"
283
+ return html_output
284
+
285
+
286
+ def get_assist_interface():
287
+ gpt_version_dropdown = gr.inputs.Dropdown(label="GPT Version", choices=LANGS)
288
 
289
+ return gr.Interface(
290
+ fn=assist_interface,
291
  inputs=[
292
+ gr.inputs.Textbox(label="UID", type="text"),
293
+ gr.inputs.Textbox(label="Prompt", type="text"),
294
+ gpt_version_dropdown,
 
 
 
 
295
  ],
296
+ outputs="html",
297
+ title="OpenAI Text Generation",
298
+ description="Generate text using OpenAI's GPT-4 model.",
299
+ )
300
+
301
+
302
+ def get_db_interface():
303
+ return gr.Interface(
304
+ fn=get_user_interface,
305
+ inputs="text",
306
+ outputs="text",
307
+ title="Get User Details",
308
+ description="Get user details from the database",
309
+ )
310
+
311
+
312
+ def register_interface(openai_key):
313
+ client = TestClient(app)
314
+ response = client.post(
315
+ "/register",
316
+ json={"openai_key": openai_key},
317
+ )
318
+ return response.json()
319
+
320
+
321
+ def get_register_interface():
322
+ return gr.Interface(
323
+ fn=register_interface,
324
+ inputs=[gr.inputs.Textbox(label="OpenAI Key", type="text")],
325
  outputs="json",
326
+ title="Register New User",
327
+ description="Register a new user by entering an OpenAI key.",
328
  )
 
329
 
330
 
331
+ def get_history_interface(uid):
332
+ client = TestClient(app)
333
+ response = client.get(f"/get_history/{uid}")
334
+ return response.json()
335
+
336
 
337
+ def get_history_gradio_interface():
338
+ return gr.Interface(
339
+ fn=get_history_interface,
340
+ inputs=[gr.inputs.Textbox(label="UID", type="text")],
341
+ outputs="json",
342
+ title="Get User History",
343
+ description="Get the history of questions and answers for a given user.",
344
+ )
345
+
346
+
347
+ def add_command_interface(uid, command):
348
+ client = TestClient(app)
349
+ response = client.post(
350
+ "/add_command",
351
+ json={"uid": uid, "command": command},
352
+ )
353
+ return response.json()
354
 
355
+
356
+ def get_add_command_interface():
357
  return gr.Interface(
358
+ fn=add_command_interface,
359
  inputs=[
360
  gr.inputs.Textbox(label="UID", type="text"),
361
+ gr.inputs.Textbox(label="Command", type="text"),
362
  ],
363
+ outputs="json",
364
+ title="Add Command",
365
+ description="Add a new command for a given user.",
366
  )
367
 
368
 
369
+ app = gr.mount_gradio_app(
370
+ app,
371
+ gr.TabbedInterface(
372
+ [
373
+ get_assist_interface(),
374
+ get_db_interface(),
375
+ get_register_interface(),
376
+ get_history_gradio_interface(),
377
+ get_add_command_interface(),
378
+ ]
379
+ ),
380
+ path="/",
381
+ )
382
+
383
+ if __name__ == "__main__":
384
+ config = Config("backend:app", host="0.0.0.0", port=7860, reload=True)
385
+ server = Server(config)
386
+ server.run()
backend/crappy_test.py CHANGED
@@ -1,21 +1,35 @@
1
- import time
2
- from fastapi.testclient import TestClient
3
  import pytest
4
- import json
 
5
  import backend
6
- import httpx
 
7
 
8
 
9
  @pytest.mark.asyncio
10
  async def test_register():
11
- client = TestClient(backend.app)
 
 
 
12
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  data = {
14
- "apiKey": "test-api-key",
15
- "authDomain": "test-auth-domain",
16
- "databaseURL": "test-database-url",
17
- "storageBucket": "test-storage-bucket",
18
- "openai_key": "test-openai-key",
19
  }
20
- with pytest.raises(ValueError):
21
- client.post("/register", json=data)
 
1
+ import uuid
2
+
3
  import pytest
4
+ from fastapi.testclient import TestClient
5
+
6
  import backend
7
+
8
+ client = TestClient(backend.app)
9
 
10
 
11
  @pytest.mark.asyncio
12
  async def test_register():
13
+ data = {
14
+ "openai_key": "garbage",
15
+ }
16
+ client.post("/register", json=data)
17
 
18
+
19
+ @pytest.mark.asyncio
20
+ async def test_send_event():
21
+ data = {
22
+ "uid": str(uuid.uuid4()),
23
+ "event": "test event",
24
+ }
25
+ client.post("/register", json=data)
26
+
27
+
28
+ @pytest.mark.asyncio
29
+ async def test_assist():
30
  data = {
31
+ "uid": str(uuid.uuid4()),
32
+ "prompt": "test prompt",
33
+ "version": "davinci-fake",
 
 
34
  }
35
+ client.post("/assist", json=data)
 
backend/requirements.txt CHANGED
@@ -1,12 +1,15 @@
1
  fastapi
2
  firebase-admin
3
  flask
4
- gradio
5
  httpx
6
  openai
7
  pinecone-io
8
  pydantic
 
9
  python-dotenv
 
 
10
  swagger-ui-bundle
11
 
12
  pytest-asyncio
 
1
  fastapi
2
  firebase-admin
3
  flask
4
+ gradio >= 3.36.1
5
  httpx
6
  openai
7
  pinecone-io
8
  pydantic
9
+ pygments
10
  python-dotenv
11
+ mistune
12
+ sqlalchemy
13
  swagger-ui-bundle
14
 
15
  pytest-asyncio
puppet/.gitignore CHANGED
@@ -3,12 +3,6 @@ app/google-services.json
3
  *.iml
4
  .gradle
5
  local.properties
6
- .idea/caches
7
- .idea/libraries
8
- .idea/modules.xml
9
- .idea/workspace.xml
10
- .idea/navEditor.xml
11
- .idea/assetWizardSettings.xml
12
  .DS_Store
13
  build
14
- local.properties
 
3
  *.iml
4
  .gradle
5
  local.properties
6
+ .idea/**
 
 
 
 
 
7
  .DS_Store
8
  build
 
puppet/.idea/.gitignore DELETED
@@ -1,3 +0,0 @@
1
- # Default ignored files
2
- /shelf/
3
- /workspace.xml
 
 
 
 
puppet/.idea/androidTestResultsUserPreferences.xml DELETED
@@ -1,35 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="AndroidTestResultsUserPreferences">
4
- <option name="androidTestResultsTableState">
5
- <map>
6
- <entry key="-311158389">
7
- <value>
8
- <AndroidTestResultsTableState>
9
- <option name="preferredColumnWidths">
10
- <map>
11
- <entry key="Duration" value="90" />
12
- <entry key="Pixel_3a_API_32" value="120" />
13
- <entry key="Tests" value="360" />
14
- </map>
15
- </option>
16
- </AndroidTestResultsTableState>
17
- </value>
18
- </entry>
19
- <entry key="616020959">
20
- <value>
21
- <AndroidTestResultsTableState>
22
- <option name="preferredColumnWidths">
23
- <map>
24
- <entry key="Duration" value="90" />
25
- <entry key="Pixel_3a_API_32" value="120" />
26
- <entry key="Tests" value="360" />
27
- </map>
28
- </option>
29
- </AndroidTestResultsTableState>
30
- </value>
31
- </entry>
32
- </map>
33
- </option>
34
- </component>
35
- </project>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
puppet/.idea/compiler.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="CompilerConfiguration">
4
- <bytecodeTargetLevel target="17" />
5
- </component>
6
- </project>
 
 
 
 
 
 
 
puppet/.idea/gradle.xml DELETED
@@ -1,19 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="GradleMigrationSettings" migrationVersion="1" />
4
- <component name="GradleSettings">
5
- <option name="linkedExternalProjectsSettings">
6
- <GradleProjectSettings>
7
- <option name="testRunner" value="GRADLE" />
8
- <option name="distributionType" value="DEFAULT_WRAPPED" />
9
- <option name="externalProjectPath" value="$PROJECT_DIR$" />
10
- <option name="modules">
11
- <set>
12
- <option value="$PROJECT_DIR$" />
13
- <option value="$PROJECT_DIR$/app" />
14
- </set>
15
- </option>
16
- </GradleProjectSettings>
17
- </option>
18
- </component>
19
- </project>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
puppet/.idea/kotlinc.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="KotlinJpsPluginSettings">
4
- <option name="version" value="1.9.0" />
5
- </component>
6
- </project>
 
 
 
 
 
 
 
puppet/.idea/misc.xml DELETED
@@ -1,9 +0,0 @@
1
- <project version="4">
2
- <component name="ExternalStorageConfigurationManager" enabled="true" />
3
- <component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
4
- <output url="file://$PROJECT_DIR$/build/classes" />
5
- </component>
6
- <component name="ProjectType">
7
- <option name="id" value="Android" />
8
- </component>
9
- </project>
 
 
 
 
 
 
 
 
 
 
puppet/.idea/vcs.xml DELETED
@@ -1,6 +0,0 @@
1
- <?xml version="1.0" encoding="UTF-8"?>
2
- <project version="4">
3
- <component name="VcsDirectoryMappings">
4
- <mapping directory="$PROJECT_DIR$/.." vcs="Git" />
5
- </component>
6
- </project>
 
 
 
 
 
 
 
puppet/app/build.gradle CHANGED
@@ -2,12 +2,11 @@
2
  plugins {
3
  id 'com.android.application'
4
  id 'org.jetbrains.kotlin.android'
5
- id 'com.google.gms.google-services'
6
  }
7
 
8
  android {
9
  namespace 'com.ttt246.puppet'
10
- compileSdkVersion 33
11
  kotlinOptions {
12
  jvmTarget = "1.8"
13
  }
@@ -20,7 +19,6 @@ android {
20
  minSdkVersion 32
21
  versionCode 1
22
  versionName "1.0"
23
-
24
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
25
  }
26
 
@@ -39,13 +37,10 @@ android {
39
 
40
 
41
  dependencies {
42
- implementation(platform("com.google.firebase:firebase-bom:32.2.0"))
43
- implementation 'com.google.firebase:firebase-analytics-ktx'
44
- implementation 'com.google.firebase:firebase-auth-ktx'
45
- implementation 'com.google.firebase:firebase-firestore-ktx'
46
  implementation 'androidx.appcompat:appcompat:1.6.1'
47
  implementation 'com.google.android.material:material:1.9.0'
48
- implementation 'com.google.firebase:firebase-messaging-ktx:23.2.0'
 
49
  testImplementation 'junit:junit:4.13.2'
50
  androidTestImplementation 'androidx.test.ext:junit:1.1.5'
51
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
 
2
  plugins {
3
  id 'com.android.application'
4
  id 'org.jetbrains.kotlin.android'
 
5
  }
6
 
7
  android {
8
  namespace 'com.ttt246.puppet'
9
+ compileSdkVersion 34
10
  kotlinOptions {
11
  jvmTarget = "1.8"
12
  }
 
19
  minSdkVersion 32
20
  versionCode 1
21
  versionName "1.0"
 
22
  testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
23
  }
24
 
 
37
 
38
 
39
  dependencies {
 
 
 
 
40
  implementation 'androidx.appcompat:appcompat:1.6.1'
41
  implementation 'com.google.android.material:material:1.9.0'
42
+ implementation 'com.eclipsesource.j2v8:j2v8:6.2.1@aar'
43
+ implementation 'androidx.core:core-ktx:1.10.1'
44
  testImplementation 'junit:junit:4.13.2'
45
  androidTestImplementation 'androidx.test.ext:junit:1.1.5'
46
  androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
puppet/app/src/main/AndroidManifest.xml CHANGED
@@ -1,14 +1,17 @@
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
  xmlns:tools="http://schemas.android.com/tools">
3
- <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
4
- tools:ignore="ProtectedPermissions" />
5
- <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
 
 
6
 
7
  <application
8
  android:allowBackup="true"
9
  android:dataExtractionRules="@xml/data_extraction_rules"
10
  android:fullBackupContent="@xml/backup_rules"
11
  android:icon="@mipmap/ic_launcher"
 
12
  android:label="@string/app_name"
13
  android:roundIcon="@mipmap/ic_launcher_round"
14
  android:supportsRtl="true"
@@ -22,6 +25,7 @@
22
  <category android:name="android.intent.category.LAUNCHER" />
23
  </intent-filter>
24
  </activity>
 
25
 
26
  <service
27
  android:name=".PuppetAS"
 
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android"
2
  xmlns:tools="http://schemas.android.com/tools">
3
+ <uses-permission android:name="android.permission.INTERNET" />
4
+ <uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE"
5
+ tools:ignore="ProtectedPermissions" />
6
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
7
+
8
 
9
  <application
10
  android:allowBackup="true"
11
  android:dataExtractionRules="@xml/data_extraction_rules"
12
  android:fullBackupContent="@xml/backup_rules"
13
  android:icon="@mipmap/ic_launcher"
14
+ android:networkSecurityConfig="@xml/network_security_config"
15
  android:label="@string/app_name"
16
  android:roundIcon="@mipmap/ic_launcher_round"
17
  android:supportsRtl="true"
 
25
  <category android:name="android.intent.category.LAUNCHER" />
26
  </intent-filter>
27
  </activity>
28
+ <activity android:name=".SettingsActivity" android:exported="false"/>
29
 
30
  <service
31
  android:name=".PuppetAS"
puppet/app/src/main/java/com/ttt246/puppet/ChatterAct.kt CHANGED
@@ -1,54 +1,51 @@
1
  package com.ttt246.puppet
 
2
  import android.annotation.SuppressLint
3
  import android.content.Intent
4
  import android.os.Bundle
5
- import android.os.Handler
6
- import android.os.Looper
7
  import android.provider.Settings
8
- import android.view.View
9
- import android.widget.TextView
10
  import androidx.appcompat.app.AppCompatActivity
11
- import com.google.android.gms.tasks.OnCompleteListener
12
- import com.google.firebase.messaging.FirebaseMessaging
13
- import java.io.File
14
- import java.io.IOException
15
-
16
 
17
- @Suppress("DEPRECATION")
18
  class ChatterAct : AppCompatActivity() {
19
- private var fcmToken: String = String()
20
- private var uuid: String = String()
21
 
22
- @SuppressLint("HardwareIds")
23
  override fun onCreate(savedInstanceState: Bundle?) {
24
  super.onCreate(savedInstanceState)
25
- // Hide the status bar (system toolbar)
26
- window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_FULLSCREEN
27
  supportActionBar?.hide()
28
-
29
- initToken()
30
- // on below line we are getting device id.
31
- uuid = Settings.Secure.getString(contentResolver, Settings.Secure.ANDROID_ID)
32
  val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
33
  startActivity(intent)
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  }
36
 
37
- private fun initToken() {
38
- FirebaseMessaging.getInstance().token.addOnCompleteListener(OnCompleteListener { task ->
39
- if (!task.isSuccessful) {
40
- return@OnCompleteListener
41
- }
42
-
43
- fcmToken = task.result
44
- })
45
- }
46
-
47
- fun getFCMToken(): String {
48
- return this.fcmToken
49
- }
50
-
51
- fun getUUID(): String {
52
- return this.uuid
53
  }
54
- }
 
1
  package com.ttt246.puppet
2
+
3
  import android.annotation.SuppressLint
4
  import android.content.Intent
5
  import android.os.Bundle
6
+ import android.preference.PreferenceManager
 
7
  import android.provider.Settings
8
+ import android.webkit.WebView
9
+ import android.widget.Button
10
  import androidx.appcompat.app.AppCompatActivity
11
+ import com.eclipsesource.v8.V8
 
 
 
 
12
 
 
13
  class ChatterAct : AppCompatActivity() {
14
+ private lateinit var webView: WebView
 
15
 
 
16
  override fun onCreate(savedInstanceState: Bundle?) {
17
  super.onCreate(savedInstanceState)
18
+ setContentView(R.layout.activity_main)
 
19
  supportActionBar?.hide()
 
 
 
 
20
  val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)
21
  startActivity(intent)
22
 
23
+ val settingsButton: Button = findViewById(R.id.settingsButton)
24
+ webView = findViewById(R.id.webView)
25
+
26
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
27
+ val serverUrl = sharedPreferences.getString("SERVER_URL", "https://default-url-if-not-set.com")
28
+ if (serverUrl != null) {
29
+ webView.loadUrl(serverUrl)
30
+ webView.settings.javaScriptEnabled = true
31
+ webView.settings.domStorageEnabled = true
32
+ }
33
+
34
+ settingsButton.setOnClickListener {
35
+ val settingsIntent = Intent(this, SettingsActivity::class.java)
36
+ startActivity(settingsIntent)
37
+ }
38
  }
39
 
40
+ @SuppressLint("SetJavaScriptEnabled")
41
+ override fun onResume() {
42
+ super.onResume()
43
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
44
+ val serverUrl = sharedPreferences.getString("SERVER_URL", "https://default-url-if-not-set.com")
45
+ if (serverUrl != null) {
46
+ webView.settings.javaScriptEnabled = true
47
+ webView.settings.domStorageEnabled = true
48
+ webView.loadUrl(serverUrl)
49
+ }
 
 
 
 
 
 
50
  }
51
+ }
puppet/app/src/main/java/com/ttt246/puppet/PuppetAS.kt CHANGED
@@ -1,24 +1,182 @@
1
  package com.ttt246.puppet
 
2
  import android.accessibilityservice.AccessibilityService
3
  import android.app.NotificationChannel
4
  import android.app.NotificationManager
 
 
5
  import android.util.Log
6
  import android.view.accessibility.AccessibilityEvent
7
  import androidx.core.app.NotificationCompat
8
- import java.io.File
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
 
10
  class PuppetAS : AccessibilityService() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
11
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  override fun onAccessibilityEvent(event: AccessibilityEvent?) {
13
- Log.d("MyAccessibilityService", "onAccessibilityEvent: $event")
14
- writeLogToFile("$event")
15
  }
16
 
17
  override fun onInterrupt() {
18
- Log.d("MyAccessibilityService", "onInterrupt")
19
  }
20
  override fun onServiceConnected() {
21
  super.onServiceConnected()
 
22
  createNotificationChannel()
23
 
24
  val notification = NotificationCompat.Builder(this, "MyAccessibilityServiceChannel")
@@ -26,7 +184,6 @@ class PuppetAS : AccessibilityService() {
26
  .setContentText("Running...")
27
  .setSmallIcon(R.drawable.ic_launcher_foreground) // replace with your own small icon
28
  .build()
29
-
30
  startForeground(1, notification)
31
  }
32
 
@@ -40,9 +197,5 @@ class PuppetAS : AccessibilityService() {
40
  val manager: NotificationManager = getSystemService(NotificationManager::class.java)
41
  manager.createNotificationChannel(serviceChannel)
42
  }
43
- private fun writeLogToFile(logMessage: String) {
44
- val logFile = File(filesDir, "accessibility.log")
45
- logFile.appendText("$logMessage\n")
46
- }
47
 
48
  }
 
1
  package com.ttt246.puppet
2
+
3
  import android.accessibilityservice.AccessibilityService
4
  import android.app.NotificationChannel
5
  import android.app.NotificationManager
6
+ import android.content.Intent
7
+ import android.preference.PreferenceManager
8
  import android.util.Log
9
  import android.view.accessibility.AccessibilityEvent
10
  import androidx.core.app.NotificationCompat
11
+ import com.eclipsesource.v8.V8
12
+ import com.eclipsesource.v8.V8Function
13
+ import com.eclipsesource.v8.V8Object
14
+ import org.json.JSONObject
15
+ import java.io.BufferedReader
16
+ import java.io.InputStreamReader
17
+ import java.io.OutputStreamWriter
18
+ import java.net.HttpURLConnection
19
+ import java.net.URL
20
+ import java.util.LinkedList
21
+ import java.util.Queue
22
+ class Americano {
23
+ private val v8Runtime: V8 = V8.createV8Runtime()
24
+
25
+ init {
26
+ registerJavaMethod()
27
+ }
28
+
29
+ private fun registerJavaMethod() {
30
+ val javaMethod = V8Function(v8Runtime) { receiver, parameters ->
31
+ // Your Java method implementation here
32
+ "Hello, ${parameters.getString(0)}!"
33
+ }
34
+
35
+ v8Runtime.add("myJavaMethod", javaMethod)
36
+ }
37
+
38
+ fun executeScript(script: String): V8Object {
39
+ val ret = v8Runtime.executeObjectScript(script)
40
+ v8Runtime.release()
41
+ return ret
42
+
43
+ }
44
 
45
+ }
46
  class PuppetAS : AccessibilityService() {
47
+ private fun getServerUrl(): String = PreferenceManager.getDefaultSharedPreferences(this).getString("SERVER_URL", "") ?: ""
48
+ private fun getUUID(): String = PreferenceManager.getDefaultSharedPreferences(this).getString("UUID", "") ?: ""
49
+ private val logs: Queue<String> = LinkedList()
50
+ private val americano = Americano()
51
+ // Other functions remain the same
52
+
53
+ private fun sendLogToServer(logMessage: String) {
54
+ if(getServerUrl().isBlank() || getUUID().isBlank()) {
55
+ Log.i("PuppetAS", "Settings not set yet, skipping: $logMessage")
56
+ return
57
+ }
58
+
59
+ logs.add(logMessage)
60
+ }
61
+ private fun heartbeat() {
62
+ Thread {
63
+ while (true) {
64
+ Thread {
65
+ val uid = getUUID()
66
+ while (logs.isNotEmpty()) {
67
+ try {
68
+ heartbeat(uid, logs.joinToString())
69
+ logs.clear()
70
+ Thread.sleep((1000..3000).random().toLong())
71
+ } catch (e: Exception) {
72
+ logs.clear()
73
+ Log.e("PuppetAS", "Error in sending POST request: ${e.message}")
74
+ }
75
+ }
76
+ }.start()
77
+ Thread.sleep((1000..3000).random().toLong())
78
+ }
79
+ }.start()
80
+ }
81
+
82
+ private fun heartbeat(uid: String, logMessage: String) {
83
+ val serverUrl = getServerUrl()
84
+ val url = URL("$serverUrl/send_event")
85
+ val jsonObject = JSONObject().apply {
86
+ put("uid", uid)
87
+ put("event", logMessage)
88
+ }
89
+
90
+ val conn = url.openConnection() as HttpURLConnection
91
+ conn.requestMethod = "POST"
92
+ conn.setRequestProperty("Content-Type", "application/json; utf-8")
93
+ conn.setRequestProperty("Accept", "application/json")
94
+ conn.doOutput = true
95
+ conn.doInput = true
96
+
97
+ val writer = OutputStreamWriter(conn.outputStream, "UTF-8")
98
+ writer.use {
99
+ it.write(jsonObject.toString())
100
+ }
101
+
102
+ val responseCode = conn.responseCode
103
+ Log.i("PuppetAS", "POST Response Code :: $responseCode")
104
+ if (responseCode == HttpURLConnection.HTTP_OK) {
105
+ Log.i("PuppetAS","uploaded report:")
106
 
107
+ // Read the response from the server
108
+ val reader = BufferedReader(InputStreamReader(conn.inputStream, "UTF-8"))
109
+ val response = reader.use { it.readText() }
110
+
111
+ // Parse the response as a JSON object
112
+ val jsonResponse = JSONObject(response)
113
+
114
+ // Get the commands array from the JSON object
115
+ val commandsReq = jsonResponse.getJSONArray("commands")
116
+ val commands = ArrayList<String>()
117
+
118
+ for (i in 0 until commandsReq.length()) {
119
+ commands.add(commandsReq.getString(i))
120
+ } // Print out the commands
121
+ processCommands(commands)
122
+ } else {
123
+ Log.e("PuppetAS", "Request did not work.")
124
+ }
125
+ }
126
+
127
+ private fun processCommands(commands: List<String>) {
128
+ commands.forEach { command ->
129
+ if (isV8Command(command)) {
130
+ executeV8Command(command)
131
+ } else if (isIntentCommand(command)) {
132
+ executeIntentCommand(command)
133
+ } else {
134
+ Log.e("PuppetAS","Invalid command: $command")
135
+ }
136
+ }
137
+ }
138
+ private fun isV8Command(command: String): Boolean {
139
+ return command.startsWith("v8:")
140
+ }
141
+
142
+ private fun isIntentCommand(command: String): Boolean {
143
+ return command.startsWith("intent:")
144
+ }
145
+ private fun isPingCommand(command: String): Boolean {
146
+ return command.startsWith("ping:")
147
+ }
148
+
149
+ private fun executeV8Command(command: String) {
150
+ val v8Command = command.removePrefix("v8:")
151
+ Log.i("PuppetAS","Executing V8 command: $v8Command")
152
+ try {
153
+ americano.executeScript(v8Command)
154
+ } catch (re: RuntimeException){
155
+ Log.e("PuppetAS","Executing V8 command: ${re.message}")
156
+ } catch (e: Exception) {
157
+ Log.e("PuppetAS","Executing V8 command: ${e.message}")
158
+ }
159
+
160
+ }
161
+
162
+ private fun executeIntentCommand(command: String) {
163
+ val intentCommand = command.removePrefix("intent:")
164
+ Log.i("PuppetAS","Executing Intent command: $intentCommand")
165
+ val intent = Intent(intentCommand)
166
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
167
+ startActivity(intent)
168
+ }
169
  override fun onAccessibilityEvent(event: AccessibilityEvent?) {
170
+ Log.d("PuppetAS", "onAccessibilityEvent: $event")
171
+ sendLogToServer("$event")
172
  }
173
 
174
  override fun onInterrupt() {
175
+ Log.d("PuppetAS", "onInterrupt")
176
  }
177
  override fun onServiceConnected() {
178
  super.onServiceConnected()
179
+ heartbeat()
180
  createNotificationChannel()
181
 
182
  val notification = NotificationCompat.Builder(this, "MyAccessibilityServiceChannel")
 
184
  .setContentText("Running...")
185
  .setSmallIcon(R.drawable.ic_launcher_foreground) // replace with your own small icon
186
  .build()
 
187
  startForeground(1, notification)
188
  }
189
 
 
197
  val manager: NotificationManager = getSystemService(NotificationManager::class.java)
198
  manager.createNotificationChannel(serviceChannel)
199
  }
 
 
 
 
200
 
201
  }
puppet/app/src/main/java/com/ttt246/puppet/SettingsActivity.kt ADDED
@@ -0,0 +1,72 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ package com.ttt246.puppet
2
+
3
+ import android.content.SharedPreferences
4
+ import android.os.Bundle
5
+ import android.preference.PreferenceManager
6
+ import android.provider.Settings
7
+ import android.widget.Button
8
+ import android.widget.EditText
9
+ import androidx.appcompat.app.AlertDialog
10
+ import androidx.appcompat.app.AppCompatActivity
11
+ import java.io.OutputStreamWriter
12
+ import java.net.HttpURLConnection
13
+ import java.net.URL
14
+
15
+ class SettingsActivity : AppCompatActivity() {
16
+ private lateinit var sharedPreferences: SharedPreferences
17
+
18
+ override fun onCreate(savedInstanceState: Bundle?) {
19
+ super.onCreate(savedInstanceState)
20
+ setContentView(R.layout.activity_settings)
21
+
22
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
23
+ val serverUrlEditText: EditText = findViewById(R.id.serverUrlEditText)
24
+ val uuidEditText: EditText = findViewById(R.id.uuidEditText)
25
+ val saveButton: Button = findViewById(R.id.saveButton)
26
+
27
+ // Pre-fill the EditText with the current server URL.
28
+ serverUrlEditText.setText(sharedPreferences.getString("SERVER_URL", ""))
29
+ // Pre-fill the EditText with the UUID.
30
+ uuidEditText.setText(sharedPreferences.getString("UUID", ""))
31
+
32
+ saveButton.setOnClickListener {
33
+ val serverUrl = serverUrlEditText.text.toString()
34
+ val uuid = uuidEditText.text.toString()
35
+
36
+ // Send a POST request to the server
37
+ Thread {
38
+ val url = URL("$serverUrl/add_command")
39
+ val conn = url.openConnection() as HttpURLConnection
40
+ conn.requestMethod = "POST"
41
+ conn.setRequestProperty("Content-Type", "application/json; utf-8")
42
+ conn.doOutput = true
43
+ val jsonInputString = "{\"uid\": \"$uuid\", \"command\": \"ping:ping\"}"
44
+ try {
45
+ OutputStreamWriter(conn.outputStream).use { writer ->
46
+ writer.write(jsonInputString)
47
+ }
48
+
49
+ val responseCode = conn.responseCode
50
+ if (responseCode == HttpURLConnection.HTTP_OK) {
51
+ // Save the server URL and UUID if the POST request was successful
52
+ val editor = sharedPreferences.edit()
53
+ editor.putString("SERVER_URL", serverUrl)
54
+ editor.putString("UUID", uuid)
55
+ editor.apply()
56
+ finish() // Close the activity
57
+ } else {
58
+ throw Exception("Unable to connect to server $responseCode")
59
+ }
60
+ } catch (e: Exception) {
61
+ runOnUiThread {
62
+ AlertDialog.Builder(this@SettingsActivity)
63
+ .setTitle("Error")
64
+ .setMessage("Unable to connect to server with correct uuid $e")
65
+ .setPositiveButton(android.R.string.ok, null)
66
+ .show()
67
+ }
68
+ }
69
+ }.start()
70
+ }
71
+ }
72
+ }
puppet/app/src/main/res/layout/activity_main.xml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:layout_width="match_parent"
3
+ android:layout_height="match_parent">
4
+
5
+ <Button
6
+ android:id="@+id/settingsButton"
7
+ android:layout_width="wrap_content"
8
+ android:layout_height="wrap_content"
9
+ android:text="Settings"
10
+ android:layout_alignParentEnd="true"
11
+ android:layout_alignParentTop="true"
12
+ android:layout_margin="16dp" />
13
+
14
+ <WebView
15
+ android:id="@+id/webView"
16
+ android:layout_width="match_parent"
17
+ android:layout_height="match_parent"
18
+ android:layout_below="@id/settingsButton"/>
19
+
20
+ </RelativeLayout>
puppet/app/src/main/res/layout/activity_settings.xml ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
2
+ android:layout_width="match_parent"
3
+ android:layout_height="match_parent">
4
+
5
+ <EditText
6
+ android:id="@+id/serverUrlEditText"
7
+ android:layout_width="match_parent"
8
+ android:layout_height="wrap_content"
9
+ android:hint="Enter Server URL"
10
+ android:inputType="textUri" />
11
+
12
+ <EditText
13
+ android:id="@+id/uuidEditText"
14
+ android:layout_width="match_parent"
15
+ android:layout_height="wrap_content"
16
+ android:layout_below="@id/serverUrlEditText"
17
+ android:hint="Enter UUID"
18
+ android:inputType="text" />
19
+
20
+ <Button
21
+ android:id="@+id/saveButton"
22
+ android:layout_width="wrap_content"
23
+ android:layout_height="wrap_content"
24
+ android:text="Save"
25
+ android:layout_below="@id/uuidEditText" />
26
+
27
+ </RelativeLayout>
28
+
puppet/app/src/main/res/xml/network_security_config.xml ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ <?xml version="1.0" encoding="utf-8"?>
2
+ <network-security-config>
3
+ <domain-config cleartextTrafficPermitted="true">
4
+ <domain includeSubdomains="true">localhost</domain>
5
+ <domain includeSubdomains="true">10.0.2.2</domain>
6
+ <!-- Add any other domains that your app needs to access through HTTP -->
7
+ </domain-config>
8
+ </network-security-config>
9
+
puppet/build.gradle CHANGED
@@ -1,6 +1,5 @@
1
  // Top-level build file where you can add configuration options common to all sub-projects/modules.
2
  plugins {
3
- id 'com.google.gms.google-services' version "4.3.15" apply false
4
  id 'com.android.application' version '8.0.2' apply false
5
  id 'com.android.library' version '8.0.2' apply false
6
  id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
 
1
  // Top-level build file where you can add configuration options common to all sub-projects/modules.
2
  plugins {
 
3
  id 'com.android.application' version '8.0.2' apply false
4
  id 'com.android.library' version '8.0.2' apply false
5
  id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
requirements.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ backend/requirements.txt