Skip to content

Commit df1642a

Browse files
committed
Merge remote-tracking branch 'upstream/main' into feature/send-schedule-review-fixes
# Conflicts: # web/pages/settings/index.vue
2 parents f80acce + 14b718e commit df1642a

54 files changed

Lines changed: 1114 additions & 267 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/copilot-instructions.md

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
# Copilot Instructions for httpSMS
2+
3+
httpSMS is a service that turns an Android phone into an SMS gateway via an HTTP API. This is a monorepo with three components:
4+
5+
- **`api/`** — Go backend (Fiber, GORM, PostgreSQL)
6+
- **`web/`** — Nuxt 2 frontend (Vue 2, Vuetify 2, TypeScript)
7+
- **`android/`** — Native Android app (Kotlin)
8+
9+
## Build, Test, and Lint Commands
10+
11+
### API (Go)
12+
13+
```bash
14+
cd api
15+
16+
# Development with hot-reload
17+
air
18+
19+
# Build
20+
go build -o ./tmp/main.exe .
21+
22+
# Run tests
23+
go test ./...
24+
25+
# Run a single test
26+
go test ./pkg/services/ -run TestMessageService
27+
28+
# Generate Swagger docs (required after changing API annotations)
29+
swag init --requiredByDefault --parseDependency --parseInternal
30+
31+
# Pre-commit hooks run: go-fumpt, go-imports, go-lint, go-mod-tidy
32+
```
33+
34+
### Web (Nuxt/Vue)
35+
36+
```bash
37+
cd web
38+
39+
# Install dependencies
40+
pnpm install
41+
42+
# Development server (port 3000)
43+
pnpm dev
44+
45+
# Lint (eslint + stylelint + prettier)
46+
pnpm lint
47+
48+
# Auto-fix lint issues
49+
pnpm lintfix
50+
51+
# Run tests (Jest)
52+
pnpm test
53+
54+
# Static site generation (production build)
55+
pnpm run generate
56+
57+
# Regenerate TypeScript API models from Swagger
58+
pnpm api:models
59+
```
60+
61+
### Android (Kotlin)
62+
63+
```bash
64+
cd android
65+
66+
# Build
67+
./gradlew build
68+
69+
# Debug APK
70+
./gradlew assembleDebug
71+
72+
# Release APK
73+
./gradlew assembleRelease
74+
```
75+
76+
### Docker (full stack)
77+
78+
```bash
79+
# Start all services (PostgreSQL, Redis, API, Web)
80+
docker compose up --build
81+
# API at localhost:8000, Web at localhost:3000
82+
```
83+
84+
## Architecture
85+
86+
### API — Layered Architecture with Event-Driven Processing
87+
88+
The API uses a **DI container** (`pkg/di/container.go`) that lazily initializes all services as singletons. The layered architecture flows as:
89+
90+
**Handlers → Services → Repositories → GORM/PostgreSQL**
91+
92+
- **Handlers** (`pkg/handlers/`) — Fiber HTTP handlers. Each has a `RegisterRoutes()` method and embeds a base `handler` struct with standardized response methods (`responseBadRequest`, `responseNotFound`, etc.).
93+
- **Services** (`pkg/services/`) — Business logic. Orchestrate repositories and dispatch events.
94+
- **Repositories** (`pkg/repositories/`) — Data access via GORM. Interfaces defined alongside GORM implementations (prefixed `gorm*`).
95+
- **Validators** (`pkg/validators/`) — One validator per handler, return `url.Values` for field errors.
96+
- **Entities** (`pkg/entities/`) — Domain models, auto-migrated by GORM.
97+
98+
**Event system**: Uses CloudEvents spec (`cloudevents/sdk-go`). Events defined in `pkg/events/` (31 event types). Listeners in `pkg/listeners/` process events either synchronously or via Google Cloud Tasks queue (emulator mode for local dev).
99+
100+
**Entry point**: `main.go` loads `.env` in local mode, creates the DI container, and starts Fiber on `APP_PORT`.
101+
102+
### Web — Nuxt 2 Static SPA
103+
104+
- **State management**: Single Vuex store (`store/index.ts`) — actions make API calls via Axios, mutations update state, getters expose computed values.
105+
- **Components**: Use `vue-property-decorator` class syntax with `@Component`, `@Prop`, `@Watch` decorators.
106+
- **API client**: Axios configured in `plugins/axios.ts` with Firebase bearer token auth and `x-api-key` header support.
107+
- **API models**: TypeScript types in `models/` are auto-generated from the Swagger spec via `swagger-typescript-api`.
108+
- **Auth**: Firebase Authentication (Email/Password, Google, GitHub) with `auth` and `guest` middleware for route guards.
109+
- **Real-time**: Pusher.js for live message updates.
110+
111+
### Android — Task-Oriented, Event-Driven
112+
113+
- **No MVVM/Clean Architecture** — uses a flat package structure with Activities, Services, BroadcastReceivers, and WorkManager tasks.
114+
- **FCM integration**: `MyFirebaseMessagingService` receives push notifications → schedules `SendSmsWorker` via WorkManager → fetches message from API → sends SMS.
115+
- **Dual SIM support**: Independent settings per SIM via `Settings` singleton (SharedPreferences).
116+
- **HTTP client**: OkHttp with `x-api-key` authentication against the API.
117+
- **Encryption**: AES-256/CFB with SHA-256 key derivation (`Encrypter.kt`).
118+
119+
## Key Conventions
120+
121+
### API (Go)
122+
123+
- **Error handling**: Use `github.com/palantir/stacktrace` — wrap errors with `stacktrace.Propagate(err, "context")` or `stacktrace.PropagateWithCode()`. Never return bare errors.
124+
- **Database queries**: Always use GORM query builder with context propagation (`repository.db.WithContext(ctx)`). No raw SQL.
125+
- **Route registration**: Each handler defines `RegisterRoutes()` called from the DI container. Routes follow REST conventions under `/v1/`.
126+
- **Middleware chain**: HTTP Logger → OpenTelemetry → CORS → Request Logger → Bearer Auth → API Key Auth.
127+
- **Observability**: All layers are instrumented with OpenTelemetry (Fiber, GORM, Redis). Pass `logger` and `tracer` to constructors.
128+
- **Code formatting**: `go-fumpt` (not `gofmt`), enforced via pre-commit hooks.
129+
130+
### Web (Vue/TypeScript)
131+
132+
- **Formatting**: No semicolons, single quotes, 2-space indentation (Prettier + ESLint).
133+
- **Component style**: Class-based with `vue-property-decorator`, not Options API (though some pages use `Vue.extend()`).
134+
- **Store pattern**: Actions handle async API calls and commit mutations. Access store from components via `this.$store`.
135+
136+
### Android (Kotlin)
137+
138+
- **API calls**: Use `HttpSmsApiService` singleton (static `create()` factory). OkHttp client with `x-api-key` header.
139+
- **Background work**: Use WorkManager for tasks that must survive process death. Direct `Thread { }` for lightweight background ops.
140+
- **State**: `Settings` object (SharedPreferences singleton) for all persistent state.
141+
- **Phone number formatting**: Use `libphonenumber` for E.164 format validation.

.mcp.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"mcpServers": {
3+
"playwright": {
4+
"type": "stdio",
5+
"command": "npx",
6+
"args": [
7+
"-y",
8+
"@modelcontextprotocol/server-playwright",
9+
"--base-url",
10+
"http://localhost:3000"
11+
],
12+
"env": {
13+
"BROWSER": "chromium"
14+
}
15+
},
16+
"context7": {
17+
"type": "stdio",
18+
"command": "npx",
19+
"args": ["@upstash/context7-mcp@latest"]
20+
}
21+
}
22+
}

android/app/build.gradle

Lines changed: 0 additions & 73 deletions
This file was deleted.

android/app/build.gradle.kts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
plugins {
2+
id("com.android.application")
3+
id("com.google.gms.google-services")
4+
id("io.sentry.android.gradle") version "6.2.0"
5+
}
6+
7+
val gitHash = providers.exec {
8+
commandLine("git", "rev-parse", "--short", "HEAD")
9+
}.standardOutput.asText.map { it.trim() }
10+
11+
android {
12+
compileSdk = 36
13+
14+
defaultConfig {
15+
applicationId = "com.httpsms"
16+
minSdk = 28
17+
targetSdk = 36
18+
versionCode = 1
19+
versionName = gitHash.getOrElse("unknown")
20+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
21+
}
22+
23+
buildTypes {
24+
getByName("debug") {
25+
manifestPlaceholders["sentryEnvironment"] = "development"
26+
}
27+
getByName("release") {
28+
manifestPlaceholders["sentryEnvironment"] = "production"
29+
isMinifyEnabled = false
30+
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
31+
}
32+
}
33+
compileOptions {
34+
sourceCompatibility = JavaVersion.VERSION_1_8
35+
targetCompatibility = JavaVersion.VERSION_1_8
36+
}
37+
namespace = "com.httpsms"
38+
39+
buildFeatures {
40+
buildConfig = true
41+
}
42+
}
43+
44+
dependencies {
45+
implementation(platform("com.google.firebase:firebase-bom:34.11.0"))
46+
implementation("com.journeyapps:zxing-android-embedded:4.3.0")
47+
implementation("com.google.firebase:firebase-analytics")
48+
implementation("com.google.firebase:firebase-messaging")
49+
implementation("com.squareup.okhttp3:okhttp:5.3.2")
50+
implementation("com.jakewharton.timber:timber:5.0.1")
51+
implementation("androidx.preference:preference-ktx:1.2.1")
52+
implementation("androidx.work:work-runtime-ktx:2.11.1")
53+
implementation("androidx.core:core-ktx:1.18.0")
54+
implementation("androidx.cardview:cardview:1.0.0")
55+
implementation("com.beust:klaxon:5.6")
56+
implementation("androidx.appcompat:appcompat:1.7.1")
57+
implementation("org.apache.commons:commons-text:1.15.0")
58+
implementation("com.google.android.material:material:1.13.0")
59+
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
60+
implementation("com.googlecode.libphonenumber:libphonenumber:9.0.26")
61+
implementation("com.klinkerapps:android-smsmms:5.2.6")
62+
testImplementation("junit:junit:4.13.2")
63+
androidTestImplementation("androidx.test.ext:junit:1.3.0")
64+
androidTestImplementation("androidx.test.espresso:espresso-core:3.7.0")
65+
}

android/app/src/main/AndroidManifest.xml

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<uses-permission android:name="android.permission.READ_SMS" />
1313
<uses-permission android:name="android.permission.SEND_SMS" />
1414
<uses-permission android:name="android.permission.RECEIVE_SMS" />
15+
<uses-permission android:name="android.permission.RECEIVE_MMS"/>
1516
<uses-permission android:name="android.permission.INTERNET" />
1617
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
1718
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
@@ -30,7 +31,7 @@
3031
android:roundIcon="@mipmap/ic_launcher_round"
3132
android:supportsRtl="true"
3233
android:theme="@style/Theme.HttpSMS"
33-
tools:targetApi="31">
34+
tools:targetApi="36">
3435
<activity
3536
android:name=".MainActivity"
3637
android:screenOrientation="portrait"
@@ -47,10 +48,10 @@
4748
<activity android:name=".SettingsActivity" android:screenOrientation="portrait"
4849
tools:ignore="LockedOrientationActivity" />
4950
<activity
50-
android:name="com.journeyapps.barcodescanner.CaptureActivity"
51-
android:screenOrientation="fullSensor"
52-
tools:replace="screenOrientation"
53-
tools:ignore="DiscouragedApi" />
51+
android:name="com.journeyapps.barcodescanner.CaptureActivity"
52+
android:screenOrientation="fullSensor"
53+
tools:replace="screenOrientation"
54+
tools:ignore="DiscouragedApi" />
5455

5556
<service
5657
android:name=".services.StickyNotificationService"
@@ -73,6 +74,10 @@
7374
<intent-filter android:priority="999">
7475
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
7576
</intent-filter>
77+
<intent-filter android:priority="999">
78+
<action android:name="android.provider.Telephony.WAP_PUSH_RECEIVED" />
79+
<data android:mimeType="application/vnd.wap.mms-message" />
80+
</intent-filter>
7681
</receiver>
7782

7883
<receiver android:enabled="true" android:exported="true" android:name=".receivers.PhoneStateReceiver" android:permission="android.permission.READ_PHONE_STATE">
@@ -90,6 +95,17 @@
9095
</intent-filter>
9196
</receiver>
9297

98+
<!-- Need this to share the attachment images with native mms service -->
99+
<provider
100+
android:name="androidx.core.content.FileProvider"
101+
android:authorities="${applicationId}.fileprovider"
102+
android:exported="false"
103+
android:grantUriPermissions="true">
104+
<meta-data
105+
android:name="android.support.FILE_PROVIDER_PATHS"
106+
android:resource="@xml/file_paths" />
107+
</provider>
108+
93109
<meta-data
94110
android:name="com.google.firebase.messaging.default_notification_channel_id"
95111
android:value="@string/notification_channel_default" />

android/app/src/main/java/com/httpsms/Constants.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ class Constants {
1010
const val KEY_MESSAGE_TIMESTAMP = "KEY_MESSAGE_TIMESTAMP"
1111
const val KEY_MESSAGE_REASON = "KEY_MESSAGE_REASON"
1212
const val KEY_MESSAGE_ENCRYPTED = "KEY_MESSAGE_ENCRYPTED"
13+
const val KEY_MESSAGE_ATTACHMENTS = "KEY_MESSAGE_ATTACHMENTS"
1314

1415

1516
const val KEY_HEARTBEAT_ID = "KEY_HEARTBEAT_ID"
@@ -18,5 +19,7 @@ class Constants {
1819
const val SIM2 = "SIM2"
1920

2021
const val TIMESTAMP_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS'000000'ZZZZZ"
22+
23+
const val MAX_MMS_ATTACHMENT_SIZE: Long = (3L * 1024 * 1024) / 2
2124
}
2225
}

0 commit comments

Comments
 (0)