Merge branch 'multiple-stations'

This commit is contained in:
Aleksander Wilczyński 2024-08-04 13:49:38 +02:00
commit 7e6095f541
Signed by untrusted user: alekswilc
GPG Key ID: D4464A248E5F27FE
15 changed files with 161 additions and 114 deletions

16
package-lock.json generated
View File

@ -9,6 +9,8 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@simrail/types": "^0.0.4",
"@types/mongoose": "^5.11.97",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.19.2", "express": "^4.19.2",
@ -85,6 +87,11 @@
"@redis/client": "^1.0.0" "@redis/client": "^1.0.0"
} }
}, },
"node_modules/@simrail/types": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@simrail/types/-/types-0.0.4.tgz",
"integrity": "sha512-AknM5FP+crERb3m/YtZqCgdFbrwUEwYB4BXfv1z+b7CVSrRsidAnaMIJZw/MhmsGxAmNIqQK7RPhDEx8qNzblA=="
},
"node_modules/@types/body-parser": { "node_modules/@types/body-parser": {
"version": "1.19.5", "version": "1.19.5",
"resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz",
@ -140,6 +147,15 @@
"integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==",
"dev": true "dev": true
}, },
"node_modules/@types/mongoose": {
"version": "5.11.97",
"resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz",
"integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==",
"deprecated": "Mongoose publishes its own types, so you do not need to install this package.",
"dependencies": {
"mongoose": "*"
}
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.0.0", "version": "22.0.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz",

View File

@ -4,7 +4,8 @@
"main": "index.js", "main": "index.js",
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"postbuild": "copyfiles --error --up 1 src/http/views/*.* dist" "postbuild": "copyfiles --error --up 1 src/http/views/*.* dist",
"dev": "npm run build && doppler run node dist"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
@ -19,6 +20,8 @@
}, },
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"@simrail/types": "^0.0.4",
"@types/mongoose": "^5.11.97",
"dayjs": "^1.11.12", "dayjs": "^1.11.12",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"express": "^4.19.2", "express": "^4.19.2",

View File

@ -5,7 +5,6 @@ Prosty projekt, logujący wyjścia z posterunków.
- Ułatwienie zgłaszania graczy, którzy robią "Hit and Run" (psuje i wychodze z posterunku) - Ułatwienie zgłaszania graczy, którzy robią "Hit and Run" (psuje i wychodze z posterunku)
## Dalszy rozwój ## Dalszy rozwój
- Wybieranie serwerów
- Obsługa pociagów, a nie tylko posterunków - Obsługa pociagów, a nie tylko posterunków
# Jak korzystać? # Jak korzystać?

View File

@ -26,9 +26,37 @@ export class ApiModule {
}); });
}) })
const generateSearch = (regex: RegExp) => [
{
stationName: { $regex: regex },
},
{
userUsername: { $regex: regex },
},
{
stationShort: { $regex: regex },
},
{
userSteamId: { $regex: regex },
},
{
server: { $regex: regex },
}
]
app.get('/search', async (req, res) => { app.get('/search', async (req, res) => {
if (!req.query.q) return res.redirect('/'); if (!req.query.q) return res.redirect('/');
const records = await MLog.find({ $text: { $search: req.query.q as string } }) const s = req.query.q.toString().split(',').map(x => new RegExp(x, "i"));
const records = await MLog.aggregate([
{
$match: {
$and: [
...s.map(x => ({ $or: generateSearch(x) }))
]
}
}
])
.sort({ leftDate: -1 }) .sort({ leftDate: -1 })
.limit(30) .limit(30)
res.render('search.ejs', { res.render('search.ejs', {

View File

@ -44,8 +44,8 @@
<body> <body>
<header> <header>
<h1><a style="color: white; text-decoration: none;" href="/">SimRail Logs [<span <h1><a style="color: white; text-decoration: none;" href="/">SimRail Logs</a></h1>
style="color:hotpink">PL2</span>]</a></h1> <p><a href="https://git.alekswilc.dev/alekswilc/simrail-logs">Dokumentacja</a></p>
</header> </header>
<div class="details"> <div class="details">
@ -55,8 +55,10 @@
</a></p> </a></p>
<p>Stacja: <%= record.stationName %> <p>Stacja: <%= record.stationName %>
</p> </p>
<p>Data wejścia: <%= dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') %> (<%= <p>Serwer: <%= record.server.toUpperCase() %>
dayjs(record.joinedDate).fromNow() %>)</p> </p>
<p>Data wejścia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') : '--:-- --/--/--' %> (<%=
record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>)</p>
<p>Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= dayjs(record.leftDate).fromNow() <p>Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= dayjs(record.leftDate).fromNow()
%>)</p> %>)</p>
@ -65,8 +67,9 @@
<br /> <br />
<code class="clickable" style="white-space: pre-line" onclick="copydata()" id="data">;station: <%= record.stationName %> <code class="clickable" style="white-space: pre-line" onclick="copydata()" id="data">;station: <%= record.stationName %>
;steam: <%= record.userSteamId %> ;steam: <%= record.userSteamId %>
;server: <%= record.server %>
;name: <%= record.userUsername %> ;name: <%= record.userUsername %>
;joined: <%=dayjs(record.joinedDate).format()%> ;joined: <%=record.joinedDate ? dayjs(record.joinedDate).format() : 'no-data'%>
;left: <%=dayjs(record.leftDate).format()%> ;left: <%=dayjs(record.leftDate).format()%>
;url: https://simrail.alekswilc.dev/details/<%= record.id %>/ ;url: https://simrail.alekswilc.dev/details/<%= record.id %>/
</code> </code>

View File

@ -22,33 +22,45 @@
<body> <body>
<header> <header>
<h1><a style="color: white; text-decoration: none;" href="/">SimRail Logs [<span style="color:hotpink">PL2</span>]</a></h1> <h1><a style="color: white; text-decoration: none;" href="/">SimRail Logs</a></h1>
<p><a href="https://git.alekswilc.dev/alekswilc/simrail-logs">Dokumentacja</a></p>
</header> </header>
<h2>Wyszukaj posterunek lub osobe</h2> <h2>Wyszukaj posterunek, osobe lub serwer</h2>
<div class="container"> <div class="container">
<input type="text" id="search"> <input type="text" id="search">
<button onclick="search()">Szukaj</button> <button onclick="search()">Szukaj</button>
<p>Użyj przecinka, aby wyszukać wiele wartości: pl2,Łazy</p>
</div> </div>
<h2>Ostatnie opuszczenia posterunków</h2> <h2>Ostatnie opuszczenia posterunków</h2>
<ul> <ul>
<% records.forEach(record => { %> <% records.forEach(record=> { %>
<li> <li>
<details> <details>
<summary>[<span style="color:lightskyblue">
<summary>[<span style="color:lightskyblue"><%= record.stationShort %></span>] <span style="color: lightskyblue"><%= record.stationName %></span> - <span style="color:hotpink"><%= record.userUsername %></span> <%= record.server.toUpperCase() %>
<p style="margin-bottom: 0; opacity: 0.5;"><%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %></p></summary> </span>] <span style="color: lightskyblue">
<p>Data dołączenia: <%= dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') %> (<%= dayjs(record.joinedDate).fromNow() %>)</p> <%= record.stationName %>
<p>Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= dayjs(record.leftDate).fromNow() %>)</p> </span> - <span style="color:hotpink">
<%= record.userUsername %>
</span>
<p style="margin-bottom: 0; opacity: 0.5;">
<%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %>
</p>
</summary>
<p>Data dołączenia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') : '--:-- --/--/--' %> (<%=
record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>)</p>
<p>Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%=
dayjs(record.leftDate).fromNow() %>)</p>
<a href="/details/<%= record.id %>"> <a href="/details/<%= record.id %>">
<button>Więcej</button> <button>Więcej</button>
</a> </a>
</details> </details>
</li> </li>
<% }) %> <% }) %>
</ul> </ul>
<script> <script>

View File

@ -23,16 +23,17 @@
<body> <body>
<header> <header>
<h1><a style="color: white; text-decoration: none;" href="/">SimRail Logs [<span <h1><a style="color: white; text-decoration: none;" href="/">SimRail Logs</a></h1>
style="color:hotpink">PL2</span>]</a></h1> <p><a href="https://git.alekswilc.dev/alekswilc/simrail-logs">Dokumentacja</a></p>
</header> </header>
<h2>Wyszukaj posterunek lub osobe</h2> <h2>Wyszukaj posterunek, osobe lub serwer</h2>
<div class="container"> <div class="container">
<input type="text" id="search" value="<%=q%>"> <input type="text" id="search" value="<%=q%>">
<button onclick="search()">Szukaj</button> <button onclick="search()">Szukaj</button>
<p>Użyj przecinka, aby wyszukać wiele wartości: pl2,Łazy</p>
</div> </div>
<h2>Wyniki wyszukiwania</h2> <h2>Wyniki wyszukiwania</h2>
@ -41,9 +42,8 @@
<% records.forEach(record=> { %> <% records.forEach(record=> { %>
<li> <li>
<details> <details>
<summary>[<span style="color:lightskyblue"> <summary>[<span style="color:lightskyblue">
<%= record.stationShort %> <%= record.server.toUpperCase() %>
</span>] <span style="color: lightskyblue"> </span>] <span style="color: lightskyblue">
<%= record.stationName %> <%= record.stationName %>
</span> - <span style="color:hotpink"> </span> - <span style="color:hotpink">
@ -53,8 +53,8 @@
<%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %>
</p> </p>
</summary> </summary>
<p>Data dołączenia: <%= dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') %> (<%= <p>Data dołączenia: <%= record.joinedDate ? dayjs(record.joinedDate).format('HH:mm DD/MM/YYYY') : '--:-- --/--/--' %> (<%=
dayjs(record.joinedDate).fromNow() %>)</p> record.joinedDate ? dayjs(record.joinedDate).fromNow() : '--' %>)</p>
<p>Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%= <p>Data wyjścia: <%= dayjs(record.leftDate).format('HH:mm DD/MM/YYYY') %> (<%=
dayjs(record.leftDate).fromNow() %>)</p> dayjs(record.leftDate).fromNow() %>)</p>
<a href="/details/<%= record.id %>"> <a href="/details/<%= record.id %>">

View File

@ -5,8 +5,8 @@ import dayjs from 'dayjs';
import { StationsModule } from './modules/stations.js'; import { StationsModule } from './modules/stations.js';
import { ApiModule } from './http/api.js'; import { ApiModule } from './http/api.js';
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { IStation } from './types/station.js';
import { IPlayer } from './types/player.js'; import { IPlayer } from './types/player.js';
import { Station, Server } from '@simrail/types';
; (async () => { ; (async () => {
@ -22,12 +22,12 @@ import { IPlayer } from './types/player.js';
console.log('MongoDB connected'); console.log('MongoDB connected');
global.client = new SimrailClient(); global.client = new SimrailClient();
client.on(SimrailClientEvents.StationJoined, (station: IStation, player: IPlayer) => { client.on(SimrailClientEvents.StationJoined, (server: Server, station: Station, player: IPlayer) => {
console.log(`${station.Name} | ${player.personaname} joined`); console.log(`${server.ServerCode} |${station.Name} | ${player.personaname} joined`);
}); });
client.on(SimrailClientEvents.StationLeft, (station: IStation, player: IPlayer, joinedAt: number) => { client.on(SimrailClientEvents.StationLeft, (server: Server, station: Station, player: IPlayer, joinedAt: number) => {
console.log(`${station.Name} | ${player.personaname} left. | ${joinedAt ? dayjs(joinedAt).fromNow() : 'no time data.'}`); console.log(`${server.ServerCode} | ${station.Name} | ${player.personaname} left. | ${joinedAt ? dayjs(joinedAt).fromNow() : 'no time data.'}`);
}); });
StationsModule.load(); StationsModule.load();

View File

@ -1,13 +1,13 @@
import { Server, Station } from '@simrail/types';
import { MLog } from '../mongo/logs.js'; import { MLog } from '../mongo/logs.js';
import { IPlayer } from '../types/player.js'; import { IPlayer } from '../types/player.js';
import { IStation } from '../types/station.js';
import { SimrailClientEvents } from '../util/SimrailClient.js'; import { SimrailClientEvents } from '../util/SimrailClient.js';
import { v4 } from 'uuid'; import { v4 } from 'uuid';
export class StationsModule { export class StationsModule {
public static load() { public static load() {
client.on(SimrailClientEvents.StationLeft, (station: IStation, player: IPlayer, joinedAt: number) => { client.on(SimrailClientEvents.StationLeft, (server: Server, station: Station, player: IPlayer, joinedAt: number) => {
const date = new Date(); const date = new Date();
MLog.create({ MLog.create({
@ -19,7 +19,7 @@ export class StationsModule {
leftDate: date.getTime(), leftDate: date.getTime(),
stationName: station.Name, stationName: station.Name,
stationShort: station.Prefix, stationShort: station.Prefix,
server: 'pl2' server: server.ServerCode
}); });
}) })
} }

View File

@ -42,7 +42,7 @@ const schema = new Schema<ILog>(
}, },
} }
); );
schema.index({ stationName: 'text', userUsername: 'text', stationShort: 'text', userSteamId: 'text' }) schema.index({ stationName: 'text', userUsername: 'text', stationShort: 'text', userSteamId: 'text', server: 'text' })
export type TMLog = Model<ILog> export type TMLog = Model<ILog>

View File

@ -1,4 +0,0 @@
export type ISimrailPayload = {
result: boolean;
description: string;
}

View File

@ -1,3 +1,5 @@
/* steam api */
export type IPlayer = { export type IPlayer = {
steamid: string, steamid: string,
communityvisibilitystate: number, communityvisibilitystate: number,

View File

@ -1,23 +0,0 @@
import { ISimrailPayload } from './payload.js';
export type IStation = {
Name: string;
Prefix: string;
DifficultyLevel: number;
Latititude: number;
Longitude: number;
MainImageURL: string;
AdditionalImage1URL: string;
AdditionalImage2URL: string;
DispatchedBy: IStationDispatch[];
}
export type IStationDispatch = {
ServerCode: string,
SteamId: number
}
export type IStationPayload = {
data: IStation[];
count: number;
} & ISimrailPayload;

View File

@ -3,7 +3,7 @@ import { IPlayerPayload } from '../types/player.js';
const STEAM_API_KEY = process.env.STEAM_APIKEY; const STEAM_API_KEY = process.env.STEAM_APIKEY;
export class PlayerUtil { export class PlayerUtil {
public static async getPlayer(steamId: string) { public static async getPlayer(steamId: number | string) {
const data = (await fetch(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=${STEAM_API_KEY}&format=json&steamids=${steamId}`).then(x => x.json())) as IPlayerPayload; const data = (await fetch(`https://api.steampowered.com/ISteamUser/GetPlayerSummaries/v2/?key=${STEAM_API_KEY}&format=json&steamids=${steamId}`).then(x => x.json())) as IPlayerPayload;
if (!data.response.players) return; if (!data.response.players) return;
return data.response.players[0]; return data.response.players[0];

View File

@ -1,25 +1,29 @@
import { EventEmitter } from 'node:events'; import { EventEmitter } from 'node:events';
import { IStation, IStationPayload } from '../types/station.js';
import { IPlayer } from '../types/player.js'; import { IPlayer } from '../types/player.js';
import { PlayerUtil } from './PlayerUtil.js'; import { PlayerUtil } from './PlayerUtil.js';
import { Station, ApiResponse, Server } from '@simrail/types';
export enum SimrailClientEvents { export enum SimrailClientEvents {
StationJoined = 'stationJoined', StationJoined = 'stationJoined',
StationLeft = 'stationLeft', StationLeft = 'stationLeft',
} }
export declare interface SimrailClient { export declare interface SimrailClient {
on(event: SimrailClientEvents.StationJoined, listener: (station: IStation, player: IPlayer) => void): this; on(event: SimrailClientEvents.StationJoined, listener: (server: Server, station: Station, player: IPlayer) => void): this;
on(event: SimrailClientEvents.StationLeft, listener: (station: IStation, player: IPlayer, joinedAt: number) => void): this; on(event: SimrailClientEvents.StationLeft, listener: (server: Server, station: Station, player: IPlayer, joinedAt: number) => void): this;
on(event: string, listener: Function): this; //on(event: string, listener: Function): this;
} }
export type OccupiedStation = {
SteamId: string;
JoinedAt: number;
}
export class SimrailClient extends EventEmitter { export class SimrailClient extends EventEmitter {
public stations: IStation[] = []; public stations: Record<Server['ServerCode'], Station[]> = {};
public stationsOccupied: Record<string, { steamId: string; joinedAt: number } | null> = {}; public stationsOccupied: Record<Server['ServerCode'], Record<string, OccupiedStation | null>> = {};
public constructor() { public constructor() {
super(); super();
@ -27,9 +31,9 @@ export class SimrailClient extends EventEmitter {
setTimeout(() => setInterval(() => this.update(), 500), 1000) setTimeout(() => setInterval(() => this.update(), 500), 1000)
} }
public getStation(name: string) { public getStation(server: Server['ServerCode'], name: string) {
if (!this.stationsOccupied[name]) return null; if (!this.stationsOccupied[server] || !this.stationsOccupied[server][name]) return null;
const player = PlayerUtil.getPlayer(this.stationsOccupied[name].steamId); const player = PlayerUtil.getPlayer(this.stationsOccupied[server][name].SteamId);
return { player, joinedAt: this.stationsOccupied[name].joinedAt }; return { player, joinedAt: this.stationsOccupied[name].joinedAt };
} }
@ -41,55 +45,62 @@ export class SimrailClient extends EventEmitter {
if (!await redis.json.get('stations_occupied')) if (!await redis.json.get('stations_occupied'))
redis.json.set('stations_occupied', '$', {}); redis.json.set('stations_occupied', '$', {});
this.stations = (await redis.json.get('stations') as unknown as IStation[]); this.stations = (await redis.json.get('stations') as unknown as SimrailClient['stations']);
this.stationsOccupied = (await redis.json.get('stations_occupied') as unknown as Record<string, { steamId: string; joinedAt: number } | null>); this.stationsOccupied = (await redis.json.get('stations_occupied') as unknown as SimrailClient['stationsOccupied']);
} }
private async update() { private async update() {
const servers = (await fetch('https://panel.simrail.eu:8084/stations-open?serverCode=pl2').then(x => x.json())) as IStationPayload; const servers = (await fetch('https://panel.simrail.eu:8084/servers-open').then(x => x.json()) as ApiResponse<Server>)
if (!servers.result) return .data?.filter(x => x.ServerName.includes('Polski')) ?? []; // no plans to support other servers
if (!this.stations.length) { // TODO: maybe node:worker_threads?
this.stations = servers.data; // TODO: check performance
redis.json.set('list', '$', this.stations); servers.forEach(async (server) => {
return const stations = (await fetch('https://panel.simrail.eu:8084/stations-open?serverCode=' + server.ServerCode).then(x => x.json())) as ApiResponse<Station>;
} if (!stations.result) return;
if (!this.stations[server.ServerCode]) this.stations[server.ServerCode] = [];
if (!this.stationsOccupied[server.ServerCode]) this.stationsOccupied[server.ServerCode] = {};
servers.data.forEach(async (x) => { if (!this.stations[server.ServerCode].length) {
const data = this.stations.find(y => y.Name === x.Name); this.stations[server.ServerCode] = stations.data;
if (!data) return; redis.json.set('stations', '$', this.stations);
if (data.DispatchedBy[0]?.SteamId !== x.DispatchedBy[0]?.SteamId) {
if (!data.DispatchedBy[0]?.SteamId) {
// join
const date = new Date();
const player = await PlayerUtil.getPlayer(x.DispatchedBy[0]?.SteamId.toString());
this.emit(SimrailClientEvents.StationJoined, x, player);
this.stationsOccupied[data.Prefix] = {
steamId: x.DispatchedBy[0]?.SteamId.toString(),
joinedAt: date.getTime()
}
redis.json.set('stations_occupied', '$', this.stationsOccupied);
return; return;
} }
const player = await PlayerUtil.getPlayer(data.DispatchedBy[0]?.SteamId.toString()) stations.data.forEach(async (x) => {
const data = this.stations[server.ServerCode].find(y => y.Name === x.Name);
if (!data) return;
this.emit(SimrailClientEvents.StationLeft, x, player, this.stationsOccupied[data.Prefix]?.joinedAt); if (data.DispatchedBy[0]?.SteamId !== x.DispatchedBy[0]?.SteamId) {
delete this.stationsOccupied[data.Prefix]; if (!data.DispatchedBy[0]?.SteamId) {
redis.json.set('stations_occupied', '$', this.stationsOccupied); // join
} const date = new Date();
}) const player = await PlayerUtil.getPlayer(x.DispatchedBy[0]?.SteamId);
this.emit(SimrailClientEvents.StationJoined, server, x, player);
this.stationsOccupied[server.ServerCode][data.Prefix] = {
SteamId: x.DispatchedBy[0]?.SteamId,
JoinedAt: date.getTime()
};
redis.json.set('stations_occupied', '$', this.stationsOccupied);
return;
}
// leave
const player = await PlayerUtil.getPlayer(data.DispatchedBy[0]?.SteamId)
this.emit(SimrailClientEvents.StationLeft, server, x, player, this.stationsOccupied[server.ServerCode][data.Prefix]?.JoinedAt);
delete this.stationsOccupied[server.ServerCode][data.Prefix];
redis.json.set('stations_occupied', '$', this.stationsOccupied);
}
})
this.stations = servers.data; this.stations[server.ServerCode] = stations.data;
redis.json.set('stations', '$', this.stations); redis.json.set('stations', '$', this.stations);
});
} }
} }