antenna-fyi 0.5.0
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.
- package/README.md +89 -0
- package/bin/antenna.js +53 -0
- package/install.js +19 -0
- package/lib/cli.js +221 -0
- package/lib/core.js +247 -0
- package/lib/mcp.js +148 -0
- package/lib/plugin-template/index.ts +674 -0
- package/lib/plugin-template/openclaw.plugin.json +48 -0
- package/lib/plugin-template/package.json +11 -0
- package/package.json +28 -0
- package/skill/SKILL.md +183 -0
package/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# ๐ก Antenna
|
|
2
|
+
|
|
3
|
+
Nearby people discovery โ find interesting people around you.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g antenna-fyi
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## CLI Usage
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Create your profile card
|
|
15
|
+
antenna setup --id telegram:123
|
|
16
|
+
|
|
17
|
+
# Scan for nearby people
|
|
18
|
+
antenna scan --lat 39.99 --lng 116.48 --radius 500 --id telegram:123
|
|
19
|
+
|
|
20
|
+
# Check in at a location
|
|
21
|
+
antenna checkin --id telegram:123 --lat 39.99 --lng 116.48
|
|
22
|
+
|
|
23
|
+
# View/edit your profile
|
|
24
|
+
antenna profile --id telegram:123
|
|
25
|
+
antenna profile --id telegram:123 --name Yi --emoji ๐ฆฆ --line1 'Product Designer'
|
|
26
|
+
|
|
27
|
+
# Accept a match
|
|
28
|
+
antenna accept --id telegram:123 --target telegram:789 --contact 'WeChat: yi'
|
|
29
|
+
|
|
30
|
+
# Check match status
|
|
31
|
+
antenna matches --id telegram:123
|
|
32
|
+
|
|
33
|
+
# Show status
|
|
34
|
+
antenna status --id telegram:123
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## MCP Server
|
|
38
|
+
|
|
39
|
+
Start the MCP server for AI agent integration:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
antenna serve
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
This starts a stdio-based MCP server with tools:
|
|
46
|
+
- `antenna_scan` โ Scan for nearby people
|
|
47
|
+
- `antenna_profile` โ Get/set profile card
|
|
48
|
+
- `antenna_checkin` โ Check in at a location
|
|
49
|
+
- `antenna_accept` โ Accept a match
|
|
50
|
+
- `antenna_check_matches` โ Check match status
|
|
51
|
+
|
|
52
|
+
## OpenClaw Integration
|
|
53
|
+
|
|
54
|
+
### Install Skill (recommended)
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
antenna install-skill
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Copies the SKILL.md to `~/.openclaw/skills/antenna/` so your agent knows how to use Antenna.
|
|
61
|
+
|
|
62
|
+
### Install Plugin (advanced)
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mkdir my-antenna-plugin && cd my-antenna-plugin
|
|
66
|
+
antenna install-plugin
|
|
67
|
+
npm install
|
|
68
|
+
openclaw plugins install .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The plugin adds automatic location-triggered scanning, match polling, and real-time notifications.
|
|
72
|
+
|
|
73
|
+
## Environment Variables
|
|
74
|
+
|
|
75
|
+
| Variable | Description | Default |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `ANTENNA_SUPABASE_URL` | Supabase project URL | Built-in |
|
|
78
|
+
| `ANTENNA_SUPABASE_KEY` | Supabase anon key | Built-in |
|
|
79
|
+
|
|
80
|
+
## How It Works
|
|
81
|
+
|
|
82
|
+
1. **Create a profile card** โ emoji, name, 3 lines about you
|
|
83
|
+
2. **Scan nearby** โ find people within radius at your location
|
|
84
|
+
3. **Accept matches** โ if both sides accept, exchange contact info
|
|
85
|
+
4. **Everything expires in 24h** โ ephemeral by design
|
|
86
|
+
|
|
87
|
+
## License
|
|
88
|
+
|
|
89
|
+
MIT
|
package/bin/antenna.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
parseFlags,
|
|
5
|
+
handleScan,
|
|
6
|
+
handleProfile,
|
|
7
|
+
handleAccept,
|
|
8
|
+
handleCheckin,
|
|
9
|
+
handleMatches,
|
|
10
|
+
handleSetup,
|
|
11
|
+
handleStatus,
|
|
12
|
+
handleInstallSkill,
|
|
13
|
+
handleInstallPlugin,
|
|
14
|
+
printHelp,
|
|
15
|
+
} from "../lib/cli.js";
|
|
16
|
+
|
|
17
|
+
const [,, cmd, ...args] = process.argv;
|
|
18
|
+
const f = parseFlags(args);
|
|
19
|
+
|
|
20
|
+
async function main() {
|
|
21
|
+
switch (cmd) {
|
|
22
|
+
case "scan":
|
|
23
|
+
return handleScan(f);
|
|
24
|
+
case "profile":
|
|
25
|
+
return handleProfile(f);
|
|
26
|
+
case "accept":
|
|
27
|
+
return handleAccept(f);
|
|
28
|
+
case "checkin":
|
|
29
|
+
return handleCheckin(f);
|
|
30
|
+
case "matches":
|
|
31
|
+
return handleMatches(f);
|
|
32
|
+
case "serve": {
|
|
33
|
+
const { startMcpServer } = await import("../lib/mcp.js");
|
|
34
|
+
return startMcpServer();
|
|
35
|
+
}
|
|
36
|
+
case "setup":
|
|
37
|
+
return handleSetup(f);
|
|
38
|
+
case "status":
|
|
39
|
+
return handleStatus(f);
|
|
40
|
+
case "install-skill":
|
|
41
|
+
return handleInstallSkill();
|
|
42
|
+
case "install-plugin":
|
|
43
|
+
return handleInstallPlugin();
|
|
44
|
+
case "help":
|
|
45
|
+
default:
|
|
46
|
+
return printHelp();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
main().catch((e) => {
|
|
51
|
+
console.error(e.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
});
|
package/install.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// postinstall โ welcome banner
|
|
2
|
+
|
|
3
|
+
console.log(`
|
|
4
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
5
|
+
โ ๐ก Antenna โ Nearby People Discovery โ
|
|
6
|
+
โ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฃ
|
|
7
|
+
โ โ
|
|
8
|
+
โ Quick start: โ
|
|
9
|
+
โ antenna setup Create your card โ
|
|
10
|
+
โ antenna scan --lat โฆ --lng โฆ Find people โ
|
|
11
|
+
โ antenna serve Start MCP server โ
|
|
12
|
+
โ โ
|
|
13
|
+
โ OpenClaw integration: โ
|
|
14
|
+
โ antenna install-skill Install SKILL.md โ
|
|
15
|
+
โ antenna install-plugin Get plugin files โ
|
|
16
|
+
โ โ
|
|
17
|
+
โ antenna help Full usage info โ
|
|
18
|
+
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
19
|
+
`);
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// antenna CLI command handlers
|
|
2
|
+
|
|
3
|
+
import { scan, getProfile, setProfile, accept, checkMatches, checkin } from "./core.js";
|
|
4
|
+
import { createInterface } from "readline";
|
|
5
|
+
import { existsSync, mkdirSync, copyFileSync } from "fs";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { homedir } from "os";
|
|
9
|
+
|
|
10
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
11
|
+
const __dirname = dirname(__filename);
|
|
12
|
+
|
|
13
|
+
export function parseFlags(args) {
|
|
14
|
+
const flags = {};
|
|
15
|
+
for (let i = 0; i < args.length; i++) {
|
|
16
|
+
if (args[i].startsWith("--")) {
|
|
17
|
+
const key = args[i].slice(2);
|
|
18
|
+
flags[key] = args[i + 1] || true;
|
|
19
|
+
i++;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return flags;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function handleScan(f) {
|
|
26
|
+
if (!f.lat || !f.lng) return console.error("Usage: antenna scan --lat 39.99 --lng 116.48 [--radius 500] [--id telegram:123]");
|
|
27
|
+
const result = await scan({
|
|
28
|
+
lat: +f.lat,
|
|
29
|
+
lng: +f.lng,
|
|
30
|
+
radius_m: +(f.radius || 500),
|
|
31
|
+
device_id: f.id || null,
|
|
32
|
+
});
|
|
33
|
+
if (result.count === 0) return console.log("๐ก No one nearby within " + result.radius_m + "m");
|
|
34
|
+
console.log(`๐ก ${result.count} people within ${result.radius_m}m:\n`);
|
|
35
|
+
result.profiles.forEach((p) => {
|
|
36
|
+
console.log(` ${p.emoji} ${p.name}${p.distance_m != null ? ` (${Math.round(p.distance_m)}m)` : ""}`);
|
|
37
|
+
if (p.line1) console.log(` ${p.line1}`);
|
|
38
|
+
if (p.line2) console.log(` ${p.line2}`);
|
|
39
|
+
if (p.line3) console.log(` ${p.line3}`);
|
|
40
|
+
console.log(` id: ${p.device_id}\n`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function handleProfile(f) {
|
|
45
|
+
if (!f.id) return console.error("Usage: antenna profile --id telegram:123 [--name Yi --emoji ๐ฆฆ --line1 '...' --line2 '...' --line3 '...']");
|
|
46
|
+
if (f.name || f.line1 || f.line2 || f.line3) {
|
|
47
|
+
const data = await setProfile({
|
|
48
|
+
device_id: f.id,
|
|
49
|
+
display_name: f.name,
|
|
50
|
+
emoji: f.emoji || "๐ค",
|
|
51
|
+
line1: f.line1,
|
|
52
|
+
line2: f.line2,
|
|
53
|
+
line3: f.line3,
|
|
54
|
+
});
|
|
55
|
+
console.log("โ
Profile saved");
|
|
56
|
+
console.log(JSON.stringify(data, null, 2));
|
|
57
|
+
} else {
|
|
58
|
+
const data = await getProfile({ device_id: f.id });
|
|
59
|
+
if (!data) return console.log("No profile yet. Create one with --name and --line1/2/3");
|
|
60
|
+
console.log(`${data.emoji || "๐ค"} ${data.display_name || "Anonymous"}`);
|
|
61
|
+
if (data.line1) console.log(` ${data.line1}`);
|
|
62
|
+
if (data.line2) console.log(` ${data.line2}`);
|
|
63
|
+
if (data.line3) console.log(` ${data.line3}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function handleAccept(f) {
|
|
68
|
+
if (!f.id || !f.target) return console.error("Usage: antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']");
|
|
69
|
+
const result = await accept({
|
|
70
|
+
device_id: f.id,
|
|
71
|
+
target_device_id: f.target,
|
|
72
|
+
contact_info: f.contact,
|
|
73
|
+
});
|
|
74
|
+
console.log("โ
" + result.message);
|
|
75
|
+
if (result.mutual && result.their_contact) console.log("๐ Their contact: " + result.their_contact);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function handleCheckin(f) {
|
|
79
|
+
if (!f.id || !f.lat || !f.lng) return console.error("Usage: antenna checkin --id telegram:123 --lat 39.99 --lng 116.48 [--place 'ไธ้ๅฑฏ']");
|
|
80
|
+
const result = await checkin({
|
|
81
|
+
lat: +f.lat,
|
|
82
|
+
lng: +f.lng,
|
|
83
|
+
device_id: f.id,
|
|
84
|
+
});
|
|
85
|
+
console.log(result.checked_in ? "โ
" + result.message : "โ " + result.message);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function handleMatches(f) {
|
|
89
|
+
if (!f.id) return console.error("Usage: antenna matches --id telegram:123");
|
|
90
|
+
const result = await checkMatches({ device_id: f.id });
|
|
91
|
+
if (!result.mutual_matches.length && !result.incoming_accepts.length) {
|
|
92
|
+
return console.log(result.message);
|
|
93
|
+
}
|
|
94
|
+
for (const m of result.mutual_matches) {
|
|
95
|
+
console.log(`๐ MUTUAL: ${m.emoji} ${m.name}`);
|
|
96
|
+
if (m.their_contact) console.log(` Their contact: ${m.their_contact}`);
|
|
97
|
+
if (m.you_shared) console.log(` You shared: ${m.you_shared}`);
|
|
98
|
+
console.log();
|
|
99
|
+
}
|
|
100
|
+
for (const m of result.incoming_accepts) {
|
|
101
|
+
console.log(`๐ฉ WANTS TO MEET YOU: ${m.emoji} ${m.name}`);
|
|
102
|
+
if (m.line1) console.log(` ${m.line1}`);
|
|
103
|
+
console.log(` Accept: antenna accept --id ${f.id} --target ${m.device_id}`);
|
|
104
|
+
console.log();
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export async function handleSetup(f) {
|
|
109
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
110
|
+
const ask = (q) => new Promise((resolve) => rl.question(q, resolve));
|
|
111
|
+
|
|
112
|
+
console.log("\n๐ก Antenna Setup โ ๅๅปบไฝ ็ๅ็\n");
|
|
113
|
+
|
|
114
|
+
const id = f.id || await ask("Your device ID (e.g. telegram:123): ");
|
|
115
|
+
if (!id) { rl.close(); return console.error("Device ID is required."); }
|
|
116
|
+
|
|
117
|
+
const name = await ask("Display name: ");
|
|
118
|
+
const emoji = (await ask("Emoji (default ๐ค): ")) || "๐ค";
|
|
119
|
+
const line1 = await ask("Line 1 โ who you are / what you do: ");
|
|
120
|
+
const line2 = await ask("Line 2 โ what you're into: ");
|
|
121
|
+
const line3 = await ask("Line 3 โ what you're looking for: ");
|
|
122
|
+
|
|
123
|
+
rl.close();
|
|
124
|
+
|
|
125
|
+
const data = await setProfile({
|
|
126
|
+
device_id: id,
|
|
127
|
+
display_name: name || null,
|
|
128
|
+
emoji,
|
|
129
|
+
line1: line1 || null,
|
|
130
|
+
line2: line2 || null,
|
|
131
|
+
line3: line3 || null,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
console.log("\nโ
Profile saved!\n");
|
|
135
|
+
console.log(` ${emoji} ${name || "Anonymous"}`);
|
|
136
|
+
if (line1) console.log(` ${line1}`);
|
|
137
|
+
if (line2) console.log(` ${line2}`);
|
|
138
|
+
if (line3) console.log(` ${line3}`);
|
|
139
|
+
console.log();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function handleStatus(f) {
|
|
143
|
+
const supabaseUrl = process.env.ANTENNA_SUPABASE_URL || process.env.ANTENNA_URL || "https://bcudjloikmpcqwcptuyd.supabase.co";
|
|
144
|
+
console.log("๐ก Antenna Status\n");
|
|
145
|
+
console.log(` Supabase URL: ${supabaseUrl}`);
|
|
146
|
+
|
|
147
|
+
if (f.id) {
|
|
148
|
+
const profile = await getProfile({ device_id: f.id });
|
|
149
|
+
if (profile) {
|
|
150
|
+
console.log(` Profile: โ
${profile.emoji || "๐ค"} ${profile.display_name || "Anonymous"}`);
|
|
151
|
+
} else {
|
|
152
|
+
console.log(" Profile: โ Not created yet");
|
|
153
|
+
}
|
|
154
|
+
const matches = await checkMatches({ device_id: f.id });
|
|
155
|
+
console.log(` Mutual matches: ${matches.mutual_matches.length}`);
|
|
156
|
+
console.log(` Incoming accepts: ${matches.incoming_accepts.length}`);
|
|
157
|
+
} else {
|
|
158
|
+
console.log(" Profile: (pass --id to check)");
|
|
159
|
+
console.log(" Matches: (pass --id to check)");
|
|
160
|
+
}
|
|
161
|
+
console.log();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function handleInstallSkill() {
|
|
165
|
+
const skillDir = join(homedir(), ".openclaw", "skills", "antenna");
|
|
166
|
+
const skillSrc = join(__dirname, "..", "skill", "SKILL.md");
|
|
167
|
+
|
|
168
|
+
if (!existsSync(skillDir)) {
|
|
169
|
+
mkdirSync(skillDir, { recursive: true });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
copyFileSync(skillSrc, join(skillDir, "SKILL.md"));
|
|
173
|
+
console.log("โ
SKILL.md installed to ~/.openclaw/skills/antenna/");
|
|
174
|
+
console.log(" Restart OpenClaw to pick it up.");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export function handleInstallPlugin() {
|
|
178
|
+
const templateDir = join(__dirname, "plugin-template");
|
|
179
|
+
const files = ["index.ts", "openclaw.plugin.json", "package.json"];
|
|
180
|
+
|
|
181
|
+
for (const file of files) {
|
|
182
|
+
const src = join(templateDir, file);
|
|
183
|
+
const dest = join(process.cwd(), file);
|
|
184
|
+
if (existsSync(dest)) {
|
|
185
|
+
console.log(`โ ๏ธ ${file} already exists, skipping.`);
|
|
186
|
+
} else {
|
|
187
|
+
copyFileSync(src, dest);
|
|
188
|
+
console.log(`๐ Copied ${file}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
console.log("\nโ
Plugin template files copied to current directory.");
|
|
193
|
+
console.log(" Next steps:");
|
|
194
|
+
console.log(" 1. Run: npm install");
|
|
195
|
+
console.log(" 2. Run: openclaw plugins install .");
|
|
196
|
+
console.log();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function printHelp() {
|
|
200
|
+
console.log(`๐ก Antenna โ nearby people discovery
|
|
201
|
+
|
|
202
|
+
Usage:
|
|
203
|
+
antenna scan --lat 39.99 --lng 116.48 [--radius 500] [--id telegram:123]
|
|
204
|
+
antenna checkin --id telegram:123 --lat 39.99 --lng 116.48
|
|
205
|
+
antenna profile --id telegram:123 [--name Yi --emoji ๐ฆฆ --line1 '...']
|
|
206
|
+
antenna accept --id telegram:123 --target telegram:789 [--contact 'WeChat: yi']
|
|
207
|
+
antenna matches --id telegram:123
|
|
208
|
+
antenna serve Start MCP server (stdio transport)
|
|
209
|
+
antenna setup Interactive profile setup [--id telegram:123]
|
|
210
|
+
antenna status Show config & status [--id telegram:123]
|
|
211
|
+
antenna install-skill Install SKILL.md to ~/.openclaw/skills/antenna/
|
|
212
|
+
antenna install-plugin Copy plugin template to current directory
|
|
213
|
+
antenna help Show this help
|
|
214
|
+
|
|
215
|
+
Environment:
|
|
216
|
+
ANTENNA_SUPABASE_URL Supabase project URL (optional, has default)
|
|
217
|
+
ANTENNA_SUPABASE_KEY Supabase anon key (optional, has default)
|
|
218
|
+
|
|
219
|
+
Install: npm install -g antenna-fyi
|
|
220
|
+
Or: npx antenna-fyi scan --lat 39.99 --lng 116.48`);
|
|
221
|
+
}
|
package/lib/core.js
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// antenna-core โ shared logic for CLI, MCP, and Plugin
|
|
2
|
+
// All three import this instead of duplicating Supabase calls.
|
|
3
|
+
|
|
4
|
+
import { createClient } from "@supabase/supabase-js";
|
|
5
|
+
|
|
6
|
+
const DEFAULT_URL = "https://bcudjloikmpcqwcptuyd.supabase.co";
|
|
7
|
+
const DEFAULT_KEY =
|
|
8
|
+
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImJjdWRqbG9pa21wY3F3Y3B0dXlkIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzQ0MTg1NDgsImV4cCI6MjA4OTk5NDU0OH0.FaoC3QfpfHP1npNGjRchJAoAp2PdZtQe_WhP-t-GN1o";
|
|
9
|
+
|
|
10
|
+
let _client = null;
|
|
11
|
+
let _url = null;
|
|
12
|
+
|
|
13
|
+
export function getClient(url, key) {
|
|
14
|
+
const u = url || process.env.ANTENNA_SUPABASE_URL || process.env.ANTENNA_URL || DEFAULT_URL;
|
|
15
|
+
const k = key || process.env.ANTENNA_SUPABASE_KEY || process.env.ANTENNA_KEY || DEFAULT_KEY;
|
|
16
|
+
if (!_client || _url !== u) {
|
|
17
|
+
_client = createClient(u, k);
|
|
18
|
+
_url = u;
|
|
19
|
+
}
|
|
20
|
+
return _client;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function deriveDeviceId(senderId, channel) {
|
|
24
|
+
return `${channel}:${senderId}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function fuzzyCoord(lat, lng) {
|
|
28
|
+
return {
|
|
29
|
+
lat: Math.round(lat * 1000) / 1000,
|
|
30
|
+
lng: Math.round(lng * 1000) / 1000,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// โโโ scan โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
35
|
+
|
|
36
|
+
export async function scan({ lat, lng, radius_m = 500, device_id, supabaseUrl, supabaseKey }) {
|
|
37
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
38
|
+
const fuzzy = fuzzyCoord(lat, lng);
|
|
39
|
+
|
|
40
|
+
if (device_id) {
|
|
41
|
+
await sb.rpc("upsert_profile_location", {
|
|
42
|
+
p_device_id: device_id,
|
|
43
|
+
p_lng: fuzzy.lng,
|
|
44
|
+
p_lat: fuzzy.lat,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { data, error } = await sb.rpc("nearby_profiles", {
|
|
49
|
+
p_lat: fuzzy.lat,
|
|
50
|
+
p_lng: fuzzy.lng,
|
|
51
|
+
p_radius_m: radius_m,
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (error) throw new Error(error.message);
|
|
55
|
+
|
|
56
|
+
const others = device_id
|
|
57
|
+
? (data || []).filter((p) => p.device_id !== device_id)
|
|
58
|
+
: data || [];
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
count: others.length,
|
|
62
|
+
radius_m,
|
|
63
|
+
profiles: others.map((p) => ({
|
|
64
|
+
device_id: p.device_id,
|
|
65
|
+
name: p.display_name || "ๅฟๅ",
|
|
66
|
+
emoji: p.emoji || "๐ค",
|
|
67
|
+
line1: p.line1,
|
|
68
|
+
line2: p.line2,
|
|
69
|
+
line3: p.line3,
|
|
70
|
+
distance_m: p.distance_m ?? p.dist_meters ?? null,
|
|
71
|
+
})),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// โโโ getProfile โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
76
|
+
|
|
77
|
+
export async function getProfile({ device_id, supabaseUrl, supabaseKey }) {
|
|
78
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
79
|
+
const { data, error } = await sb.rpc("get_profile", { p_device_id: device_id });
|
|
80
|
+
if (error) throw new Error(error.message);
|
|
81
|
+
return data || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// โโโ setProfile โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
85
|
+
|
|
86
|
+
export async function setProfile({
|
|
87
|
+
device_id,
|
|
88
|
+
display_name,
|
|
89
|
+
emoji = "๐ค",
|
|
90
|
+
line1,
|
|
91
|
+
line2,
|
|
92
|
+
line3,
|
|
93
|
+
visible = true,
|
|
94
|
+
supabaseUrl,
|
|
95
|
+
supabaseKey,
|
|
96
|
+
}) {
|
|
97
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
98
|
+
const { data, error } = await sb.rpc("upsert_profile", {
|
|
99
|
+
p_device_id: device_id,
|
|
100
|
+
p_display_name: display_name || null,
|
|
101
|
+
p_emoji: emoji,
|
|
102
|
+
p_line1: line1 || null,
|
|
103
|
+
p_line2: line2 || null,
|
|
104
|
+
p_line3: line3 || null,
|
|
105
|
+
p_visible: visible,
|
|
106
|
+
});
|
|
107
|
+
if (error) throw new Error(error.message);
|
|
108
|
+
return data;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// โโโ accept โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
112
|
+
|
|
113
|
+
export async function accept({
|
|
114
|
+
device_id,
|
|
115
|
+
target_device_id,
|
|
116
|
+
contact_info,
|
|
117
|
+
supabaseUrl,
|
|
118
|
+
supabaseKey,
|
|
119
|
+
}) {
|
|
120
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
121
|
+
|
|
122
|
+
const { error } = await sb.rpc("upsert_match", {
|
|
123
|
+
p_device_id_a: device_id,
|
|
124
|
+
p_device_id_b: target_device_id,
|
|
125
|
+
p_reason: "",
|
|
126
|
+
p_score: 0,
|
|
127
|
+
p_status: "accepted",
|
|
128
|
+
p_contact_info: contact_info || null,
|
|
129
|
+
p_expires_hours: 24,
|
|
130
|
+
});
|
|
131
|
+
if (error) throw new Error(error.message);
|
|
132
|
+
|
|
133
|
+
// Check mutual
|
|
134
|
+
const { data: reverse } = await sb
|
|
135
|
+
.from("matches")
|
|
136
|
+
.select("status, contact_info_a")
|
|
137
|
+
.eq("device_id_a", target_device_id)
|
|
138
|
+
.eq("device_id_b", device_id)
|
|
139
|
+
.eq("status", "accepted")
|
|
140
|
+
.single();
|
|
141
|
+
|
|
142
|
+
const mutual = !!reverse;
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
accepted: true,
|
|
146
|
+
mutual,
|
|
147
|
+
their_contact: mutual ? reverse?.contact_info_a || null : null,
|
|
148
|
+
message: mutual
|
|
149
|
+
? "ๅๅๅน้
ๆๅ๏ผ๐"
|
|
150
|
+
: "ๅทฒๆฅๅใ็ญๅฏนๆนไนๆฅๅๅ๏ผไฝ ไปฌๅฐฑๅฏไปฅไบคๆข่็ณปๆนๅผไบใ",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// โโโ checkin โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
155
|
+
|
|
156
|
+
export async function checkin({ lat, lng, device_id, supabaseUrl, supabaseKey }) {
|
|
157
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
158
|
+
const fuzzy = fuzzyCoord(lat, lng);
|
|
159
|
+
|
|
160
|
+
// Check profile exists
|
|
161
|
+
const profile = await getProfile({ device_id, supabaseUrl, supabaseKey });
|
|
162
|
+
if (!profile) {
|
|
163
|
+
return {
|
|
164
|
+
checked_in: false,
|
|
165
|
+
message: "ไฝ ่ฟๆฒกๆๅ็๏ผๅ
ๅๅปบไธไธชๅงใ",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const { error } = await sb.rpc("upsert_profile_location", {
|
|
170
|
+
p_device_id: device_id,
|
|
171
|
+
p_lng: fuzzy.lng,
|
|
172
|
+
p_lat: fuzzy.lat,
|
|
173
|
+
});
|
|
174
|
+
if (error) throw new Error(error.message);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
checked_in: true,
|
|
178
|
+
message: "ๅทฒ็ญพๅฐ ๐ ็ฐๅจ้่ฟ็ไบบๆซๆๅฐฑ่ฝ็ๅฐไฝ ไบใ",
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// โโโ checkMatches โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
183
|
+
|
|
184
|
+
export async function checkMatches({ device_id, supabaseUrl, supabaseKey }) {
|
|
185
|
+
const sb = getClient(supabaseUrl, supabaseKey);
|
|
186
|
+
|
|
187
|
+
const { data: allMatches, error } = await sb.rpc("get_my_matches", { p_device_id: device_id });
|
|
188
|
+
if (error) throw new Error(error.message);
|
|
189
|
+
|
|
190
|
+
if (!allMatches?.length) {
|
|
191
|
+
return {
|
|
192
|
+
mutual_matches: [],
|
|
193
|
+
incoming_accepts: [],
|
|
194
|
+
message: "็ฎๅๆฒกๆ่ฟ่กไธญ็ๅน้
ใ",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const myMatches = allMatches.filter((m) => m.device_id_a === device_id);
|
|
199
|
+
const incomingMatches = allMatches.filter((m) => m.device_id_b === device_id);
|
|
200
|
+
|
|
201
|
+
// Mutual
|
|
202
|
+
const mutualMatches = [];
|
|
203
|
+
for (const match of myMatches) {
|
|
204
|
+
const reverse = incomingMatches.find((m) => m.device_id_a === match.device_id_b);
|
|
205
|
+
if (reverse) {
|
|
206
|
+
const profile = await getProfile({ device_id: match.device_id_b, supabaseUrl, supabaseKey });
|
|
207
|
+
mutualMatches.push({
|
|
208
|
+
device_id: match.device_id_b,
|
|
209
|
+
name: profile?.display_name || "ๅฟๅ",
|
|
210
|
+
emoji: profile?.emoji || "๐ค",
|
|
211
|
+
line1: profile?.line1,
|
|
212
|
+
line2: profile?.line2,
|
|
213
|
+
line3: profile?.line3,
|
|
214
|
+
their_contact: reverse.contact_info_a || null,
|
|
215
|
+
you_shared: match.contact_info_a || null,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Incoming only
|
|
221
|
+
const incomingAccepts = [];
|
|
222
|
+
for (const match of incomingMatches) {
|
|
223
|
+
const iAccepted = myMatches.find((m) => m.device_id_b === match.device_id_a);
|
|
224
|
+
if (!iAccepted) {
|
|
225
|
+
const profile = await getProfile({ device_id: match.device_id_a, supabaseUrl, supabaseKey });
|
|
226
|
+
incomingAccepts.push({
|
|
227
|
+
device_id: match.device_id_a,
|
|
228
|
+
name: profile?.display_name || "ๅฟๅ",
|
|
229
|
+
emoji: profile?.emoji || "๐ค",
|
|
230
|
+
line1: profile?.line1,
|
|
231
|
+
line2: profile?.line2,
|
|
232
|
+
line3: profile?.line3,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const messages = [];
|
|
238
|
+
if (mutualMatches.length > 0) messages.push(`${mutualMatches.length} ไธชๅๅๅน้
๏ผๅฏไปฅไบคๆข่็ณปๆนๅผไบ`);
|
|
239
|
+
if (incomingAccepts.length > 0) messages.push(`${incomingAccepts.length} ไธชไบบๆณ่ฎค่ฏไฝ ๏ผ็ญไฝ ๅๅบ`);
|
|
240
|
+
if (messages.length === 0) messages.push("ไฝ ๆฅๅไบไธไบๅน้
๏ผไฝๅฏนๆน่ฟๆฒกๆๅๅบใ่ๅฟ็ญ็ญ โณ");
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
mutual_matches: mutualMatches,
|
|
244
|
+
incoming_accepts: incomingAccepts,
|
|
245
|
+
message: messages.join("๏ผ"),
|
|
246
|
+
};
|
|
247
|
+
}
|