Skip to content

Commit c50ea6f

Browse files
author
Jordan Hotmann
committed
MQTT and HTTP can both be used at same time
1 parent 1c86470 commit c50ea6f

File tree

11 files changed

+124
-37
lines changed

11 files changed

+124
-37
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The recommended and supported installation method is using [Docker](https://www.
2020
| Environmental Variable | Description | Default | Example |
2121
| ----- | ----- | ----- | ----- |
2222
| HTTP_HOST | The protocol, hostname, and port (if necessary) for clients to connect to your server | | `http://192.168.0.2:8000` or `https://pinpoint.example.com` |
23-
| MQTT_HOST | The hostname for clients to connect to the MQTT server. If set, client configuration links will use MQTT instead of HTTP settings. | | `pinpointmqtt.example.com` |
23+
| MQTT_HOST | The hostname for clients to connect to the MQTT server. If not set, MQTT mode will be disabled. | | `pinpointmqtt.example.com` |
2424
| ADMIN_PASSWORD | The password for the admin account | `pinpointadmin` | `mysupersecretpassword` |
2525
| APPRISE_HOST | Either `cli` to use the [Apprise CLI](https://github.com/caronc/apprise) to send notifications or the protocol, hostname, and port of an [Apprise API](https://github.com/caronc/apprise-api) server | `cli` | `http://127.0.0.1:8000` |
2626
| APPRISE_EMAIL_URL | [Apprise URI](https://github.com/caronc/apprise/wiki) for sending emails to users. The user's email address will be appended to this to build the final URI. If not set, notifications will be disabled. | | `mailgun://admin@example.com/my-mailgun-token/` |

bin/www

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,27 @@
33
require('dotenv').config();
44

55
const aedes = require('aedes')();
6+
const async = require('async');
67
const { CronJob } = require('cron');
78
const http = require('http');
89
const ws = require('websocket-stream');
910
const app = require('../src/app');
1011
const { CardSeen } = require('../src/models/CardSeen');
12+
const { User } = require('../src/models/User');
13+
const { Device } = require('../src/models/Device');
1114
const mqtt = require('../src/mqtt');
1215

16+
// Ensure device links are up to date with current settings (in case they changed)
17+
(async () => {
18+
const allUsers = await User.getAll();
19+
await async.each(allUsers, async (user) => {
20+
const allDevices = await user.getDevices();
21+
await async.each(allDevices, async (device) => {
22+
await device.updateConfigLinks(user);
23+
});
24+
});
25+
})();
26+
1327
// Get port from environment and store in Express.
1428

1529
const port = normalizePort(process.env.PORT || '8000');

src/models/Device.js

Lines changed: 40 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ const { Base } = require('./Base');
77
userId: "string",
88
name: "string",
99
initials: ["string"],
10-
configLink: "string",
10+
configLink: "string", <- old way
11+
httpConfigLink: "string",
12+
mqttConfigLink: "string",
1113
createdAt: Date,
1214
updatedAt: Date,
1315
}
@@ -24,27 +26,39 @@ class Device extends Base {
2426
static async create(name, initials, card, user) {
2527
const existing = await Device.findOne({ userId: user._id, name });
2628
if (existing) return null;
27-
const configLink = getDeviceConfig(user, name, initials);
29+
const mqttConfigLink = getMqttDeviceConfig(user, name, initials);
30+
const httpConfigLink = getHttpDeviceConfig(user, name, initials);
2831
const device = new Device({
2932
userId: user._id,
3033
name,
3134
initials,
3235
card,
33-
configLink,
36+
httpConfigLink,
37+
mqttConfigLink
3438
});
3539
await device.save();
3640
return device;
3741
}
3842

3943
async update(name, initials, card, user) {
40-
this.configLink = getDeviceConfig(user, name, initials);
44+
this.mqttConfigLink = getMqttDeviceConfig(user, name, initials);
45+
this.httpConfigLink = getHttpDeviceConfig(user, name, initials);
46+
if (this.configLink) this.configLink = null;
4147
this.name = name;
4248
this.initials = initials;
4349
this.card = card;
4450
const device = await this.save();
4551
return device;
4652
}
4753

54+
async updateConfigLinks(user) {
55+
this.mqttConfigLink = getMqttDeviceConfig(user, this.name, this.initials);
56+
this.httpConfigLink = getHttpDeviceConfig(user, this.name, this.initials);
57+
if (this.configLink) this.configLink = null;
58+
const device = await this.save();
59+
return device;
60+
}
61+
4862
static async getByUserId(userId) {
4963
const devices = await Device.find({ userId });
5064
return devices;
@@ -55,7 +69,27 @@ function cleanString(str) {
5569
return str.replace(/[^A-Za-z0-9]/g, '');
5670
}
5771

58-
function getDeviceConfig(userData, deviceName, initials) {
72+
function getHttpDeviceConfig(userData, deviceName, initials) {
73+
const httpConfig = {
74+
_type: 'configuration',
75+
autostartOnBoot: true,
76+
deviceId: cleanString(deviceName),
77+
locatorInterval: 300,
78+
mode: 3,
79+
monitoring: 1,
80+
password: userData.passwordHash,
81+
ping: 30,
82+
pubExtendedData: true,
83+
tid: initials,
84+
tls: true,
85+
url: `${process.env.HTTP_HOST}/pub`,
86+
username: userData.username,
87+
};
88+
const configBuffer = Buffer.from(JSON.stringify(httpConfig), 'utf8');
89+
return `owntracks:///config?inline=${configBuffer.toString('base64')}`;
90+
}
91+
92+
function getMqttDeviceConfig(userData, deviceName, initials) {
5993
const mqttConfig = {
6094
_type: 'configuration',
6195
autostartOnBoot: true,
@@ -81,24 +115,7 @@ function getDeviceConfig(userData, deviceName, initials) {
81115
username: userData.username,
82116
ws: true,
83117
};
84-
85-
const httpConfig = {
86-
_type: 'configuration',
87-
autostartOnBoot: true,
88-
deviceId: cleanString(deviceName),
89-
locatorInterval: 300,
90-
mode: 3,
91-
monitoring: 1,
92-
password: userData.passwordHash,
93-
ping: 30,
94-
pubExtendedData: true,
95-
tid: initials,
96-
tls: true,
97-
url: `${process.env.HTTP_HOST}/pub`,
98-
username: userData.username,
99-
}
100-
101-
const configBuffer = Buffer.from(JSON.stringify(process.env.MQTT_HOST ? mqttConfig : httpConfig), 'utf8');
118+
const configBuffer = Buffer.from(JSON.stringify(mqttConfig), 'utf8');
102119
return `owntracks:///config?inline=${configBuffer.toString('base64')}`;
103120
}
104121

src/models/User.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ class User extends Base {
104104
let sharers = [];
105105

106106
groups.forEach((group) => {
107-
const members = group.members.map((member) => member.userId);
107+
const members = group.members.filter((member) => member.accepted).map((member) => member.userId);
108108
sharers = sharers.concat(members);
109109
});
110110

@@ -114,6 +114,15 @@ class User extends Base {
114114
return [...new Set(sharers)];
115115
}
116116

117+
async getFriendsAndGroupies() {
118+
const groups = await this.getGroups();
119+
let friends = [...this.friends, this.username];
120+
121+
friends.concat(groups.flatMap((group) => group.members.filter((member) => member.accepted).map((member) => member.username)));
122+
123+
return [...new Set(friends)];
124+
}
125+
117126
static async getByUsername(uname) {
118127
const user = await this.findOne({ username: uname });
119128
return user;

src/mqtt.js

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const bcrypt = require('bcrypt');
33
const { User } = require('./models/User');
44
const { Device } = require('./models/Device');
55
const { CardSeen } = require('./models/CardSeen');
6+
const { Location } = require('./models/Location');
67

78
let aedes;
89
const clientMap = {};
@@ -49,16 +50,24 @@ module.exports.authorizePublish = async (client, packet, callback) => {
4950
console.log(`${client.id} (${username}) published to ${packet.topic}: ${packet.payload}`);
5051
if (packet.topic.startsWith(`owntracks/${username}/`)) {
5152
callback(null);
52-
await publishToFriends(client, packet);
53+
await publishToPinpoint(client, packet);
5354
} else {
5455
callback(new Error('Invalid topic'));
5556
}
5657
};
5758

58-
async function publishToFriends(client, packet) {
59+
async function publishToPinpoint(client, packet) {
5960
const username = clientMap[client.id];
6061
const user = await User.getByUsername(username);
6162
if (!user) return;
63+
64+
const deviceRegex = new RegExp(`^owntracks/${username}/`);
65+
const deviceName = packet.topic.replace(deviceRegex, '');
66+
const device = await Device.findOne({ userId: user._id, name: deviceName });
67+
if (!device) return;
68+
69+
await Location.create(JSON.parse(packet.payload.toString()), user, device._id);
70+
6271
const { friends } = user;
6372
const groups = await user.getGroups();
6473
await async.eachSeries(groups, async (group) => {
@@ -70,14 +79,11 @@ async function publishToFriends(client, packet) {
7079
});
7180
if (!friends.includes(username)) friends.push(username);
7281
await async.eachSeries(friends, async (friend) => {
73-
console.log(`Forwarding packet to ${friend}`);
7482
const newPacket = { ...packet };
7583
newPacket.topic = newPacket.topic.replace(/^owntracks/g, friend);
84+
console.log(`Forwarding packet to ${newPacket.topic}`);
7685
aedes.publish(newPacket);
7786
// publish card if unseen
78-
const deviceRegex = new RegExp(`^owntracks/${username}/`);
79-
const deviceName = packet.topic.replace(deviceRegex, '');
80-
const device = await Device.findOne({ userId: user._id, name: deviceName });
8187
if (device && device.card) {
8288
const friendData = await User.getByUsername(friend);
8389
const seen = await CardSeen.findOne({ deviceId: device._id, seerId: friendData._id });

src/routes/pub.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const async = require('async');
22
const express = require('express');
33
const userMw = require('../middleware/user');
4+
const mqtt = require('../mqtt');
45
const { Device } = require('../models/Device');
56
const { CardSeen } = require('../models/CardSeen');
67
const { Location } = require('../models/Location');
@@ -11,14 +12,31 @@ router.post('/', userMw.one, async (req, res) => {
1112
if (typeof req.body === 'object' && req.body?._type === 'location') {
1213
const deviceName = deviceNameFromTopic(req.user.username, req.body.topic);
1314
const userDevice = (await Device.getByUserId(req.User._id)).find((device) => device.name === deviceName);
15+
if (!userDevice) {
16+
console.log(`${req.user.username} posted a location update for an invalid device: ${deviceName}`);
17+
return;
18+
}
1419
console.log(`${req.user.username} posted a location update for device: ${deviceName}`);
1520
await Location.create(req.body, req.User, userDevice._id);
1621
const returnData = [];
1722
const returnUsernames = [];
18-
const sharers = await req.User.getUsersSharingWith();
19-
if (!sharers.includes(req.User._id)) sharers.push(req.User._id);
23+
24+
// Publish to friends MQTT topics
25+
if (process.env.MQTT_HOST) {
26+
const friends = await req.User.getFriendsAndGroupies();
27+
friends.forEach((friend) => {
28+
console.log(`Publishing location to ${friend}/${req.User.username}/${deviceName}: ${JSON.stringify(req.body)}`);
29+
try {
30+
mqtt.publish({ cmd: 'publish', topic: `${friend}/${req.User.username}/${deviceName}`, payload: JSON.stringify(req.body) }, (err) => { if (err) console.error(err); });
31+
} catch (e) {
32+
console.log(e);
33+
}
34+
});
35+
}
2036

2137
// Loop through all people that share with the user who just posted their location
38+
const sharers = await req.User.getUsersSharingWith();
39+
if (!sharers.includes(req.User._id)) sharers.push(req.User._id);
2240
await async.eachSeries(sharers, async (sharer) => {
2341
const lastLocs = await Location.getLastByUserId(sharer); // Get last location for all devices for the user
2442
console.log(`Sharer ${sharer} has ${lastLocs.length} location(s) to share`)
@@ -42,8 +60,9 @@ router.post('/', userMw.one, async (req, res) => {
4260
console.log(`Returning ${returnUsernames.length} user location(s): ${returnUsernames.join(', ')}`);
4361
console.log(`Response size ${JSON.stringify(returnData).length} bytes`);
4462
return res.send(returnData);
63+
} else {
64+
res.send('Got it');
4565
}
46-
res.send('Got it');
4766
});
4867

4968
function deviceNameFromTopic(username, topic) {

src/routes/user.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ router.get('/dismiss-help', userMw.one, async (req, res) => {
3131
res.send('');
3232
});
3333

34+
router.get('/show-help', userMw.one, async (req, res) => {
35+
res.render('user-help.html');
36+
});
37+
3438
// !!!! devices !!!!
3539

3640
router.get('/add-device', async (req, res) => {

src/views/navbar.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@
2828
</li>
2929
{% endif %}
3030
</ul>
31+
{% if userData and userData.helpDismissed %}
32+
<ul class="navbar-nav ms-auto">
33+
<li class="nav-item">
34+
<button class="btn btn-outline-secondary" hx-get="/user/show-help" hx-target="#help">Help</button>
35+
</li>
36+
</ul>
37+
{% endif %}
3138
</div>
3239
</div>
3340
{% endblock %}

src/views/user-devices.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,16 @@
22
{% for device in userDevices %}
33
<tr>
44
<th scope="row">{{ device.name }}</th>
5+
{% if device.httpConfigLink %}
6+
<th scope="row">
7+
<a class="btn btn-info" href="{{ device.httpConfigLink }}" role="button">OwnTracks HTTP Config</a>
8+
{% if settings.mqttEnabled and device.mqttConfigLink %}
9+
<a class="btn btn-info" href="{{ device.mqttConfigLink }}" role="button">OwnTracks MQTT Config</a>
10+
{% endif %}
11+
</th>
12+
{% else %}
513
<th scope="row"><a class="btn btn-info" href="{{ device.configLink | replace(urlRegExp, 'owntracks:///c') }}" role="button">OwnTracks Link</a></th>
14+
{% endif %}
615
<td scope="row">
716
<button class="btn btn-secondary" role="button" data-bs-toggle="modal" data-bs-target="#add-edit-device-modal"
817
hx-get="/user/edit-device/{{ device._id }}" hx-target="#add-edit-device-dialog">Edit</button>

src/views/user-help.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ <h5 class="card-title">Welcome to Pinpoint!</h5>
2727
<li class="list-group-item">
2828
<div class="container">
2929
<div class="row justify-content-start align-items-center">
30-
<div class="col">📲 Select the "OwnTracks Link" on a device with OwnTracks installed and you will be prompted to import the configuration.</div>
30+
<div class="col">📲 Select one of the "OwnTracks Config" links on a device with OwnTracks installed and you will be prompted to import the configuration. MQTT will give you instant updates when a friend updates their location, whereas HTTP polls periodically for updates and uses slightly less battery.</div>
3131
</div>
3232
</div>
3333
</li>
@@ -53,6 +53,6 @@ <h5 class="card-title">Welcome to Pinpoint!</h5>
5353
</div>
5454
</li>
5555
</ul>
56-
<button class="btn btn-secondary" hx-get="/user/dismiss-help" hx-target="#user-help" hx-swap="outerHTML">Dismiss</button>
56+
<button class="btn btn-secondary" hx-get="/user/dismiss-help" hx-target="#help">Dismiss</button>
5757
</div>
5858
</div>

0 commit comments

Comments
 (0)