WIP message-delivery-method-web-push
Main daploy / deploy (push) Successful in 1m22s

This commit is contained in:
2024-11-27 15:57:11 +02:00
parent 056247a414
commit 1c8019d41a
48 changed files with 464 additions and 128 deletions
+1
View File
@@ -17,6 +17,7 @@
},
"dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@hereconnect/types": "^1.0.0",
"@primevue/forms": "^4.2.1",
"@primevue/themes": "^4.2.1",
"@tanstack/vue-query": "^5.60.6",
+2 -2
View File
@@ -4,8 +4,8 @@ import type {
QRCodeDocument,
MessageDocument,
WebPushSubscriptionDocument,
} from '@/types/DBDocumentTypes'
import type { PublicSettingsDocument } from '@/types/DBDocumentTypes/PublicSettingsDocument'
PublicSettingsDocument,
} from '@hereconnect/types'
export const qrCodeDbRemote = new PouchDB<QRCodeDocument>(`${import.meta.env.VITE_API_URL}/qr`, {
skip_setup: true,
+1 -1
View File
@@ -1,4 +1,4 @@
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
import { qrCodeDbLocal, qrCodeDbRemote } from '@/api/dbs'
import { getUserUuid } from '@/utils/getUserUuid'
import { getBrowserUuid } from '@/utils/getBrowserUuid'
+1 -1
View File
@@ -1,4 +1,4 @@
import type { WebPushSubscriptionDocument } from '@/types/DBDocumentTypes'
import type { WebPushSubscriptionDocument } from '@hereconnect/types'
import { publicSettingsDbRemote, webPushSubscriptionsDbRemote } from '@/api/dbs'
import { getUserUuid } from '@/utils/getUserUuid'
import { getBrowserUuid } from '@/utils/getBrowserUuid'
@@ -40,7 +40,9 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, useTemplateRef, watch, nextTick } from 'vue'
import { messagesDbLocal as db, messagesDbRemote as remoteDb } from '@/api/dbs'
import { type MessageDocument } from '@/types/DBDocumentTypes'
import { type MessageDocument } from '@hereconnect/types'
import { getBrowserUuid } from '@/utils/getBrowserUuid'
import { getUserUuid } from '@/utils/getUserUuid'
const formRef = useTemplateRef('form')
const messagesContainerRef = useTemplateRef('messagesContainer')
@@ -84,9 +86,11 @@ async function sendMessage() {
type: 'message',
qr_code_uri,
chat,
created_at,
from: userRole,
body: newMessage.value.trim(),
browser_uuid: getBrowserUuid(),
user_uuid: getUserUuid(),
created_at,
}
try {
@@ -27,7 +27,7 @@
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import CreateMessageDeliveryOptionTelegram from '@/components/create/CreateMessageDeliveryOptionTelegram.vue'
import CreateMessageDeliveryOptionWebPush from '@/components/create/CreateMessageDeliveryOptionWebPush.vue'
import { ref } from 'vue'
@@ -17,7 +17,7 @@
</template>
<script setup lang="ts">
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
import type { DeliveryOption } from '@/constants/deliveryOptions'
defineProps<{
@@ -9,7 +9,7 @@
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import { type DeliveryOption, DELIVERY_METHOD_TELEGRAM } from '@/constants/deliveryOptions'
import { computed } from 'vue'
@@ -42,7 +42,7 @@
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import { type DeliveryOption, DELIVERY_METHOD_WEB_PUSH } from '@/constants/deliveryOptions'
import { computed } from 'vue'
import { useQuery } from '@tanstack/vue-query'
@@ -19,7 +19,7 @@
<script setup lang="ts">
import { qrCodePlacements } from '@/constants/qrCodePlacements'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
const selectedPlacement = defineModel<null | QRCodeDocument['placement']>()
defineEmits(['nextStep'])
@@ -31,7 +31,7 @@
<script setup lang="ts">
import { nextTick, useTemplateRef, watch } from 'vue'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import { defaultMessages } from '@/constants/defaultMessages'
// Пропсы
@@ -14,7 +14,7 @@
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
// Пропсы, которые будут передаваться из родительского компонента
const props = defineProps<{
@@ -14,7 +14,7 @@
</template>
<script setup lang="ts">
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
defineProps<{ buttonLabel: string }>()
const name = defineModel<QRCodeDocument['name']>('name')
const emit = defineEmits(['nextStep'])
@@ -13,7 +13,7 @@
import { computed } from 'vue'
import { VueMarkdownIt } from '@f3ve/vue-markdown-it'
import { useQuery } from '@tanstack/vue-query'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
// Импортируем все инструкции из директории
const importInstructions = import.meta.glob('/src/content/placementInstructions/*.md', {
@@ -48,7 +48,7 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import md5 from 'md5'
import type { ContactInfo } from '@/types/DBDocumentTypes'
import type { ContactInfo } from '@hereconnect/types'
import { sanitizeFileName } from '@/utils/sanitizeFileName'
const { contactInfo } = defineProps<{ contactInfo: ContactInfo }>()
@@ -45,8 +45,10 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { messagesDbLocal as db } from '@/api/dbs'
import type { MessageDocument, QRCodeDocument } from '@/types/DBDocumentTypes'
import type { MessageDocument, QRCodeDocument } from '@hereconnect/types'
import { ROUTE_NAMES } from '@/constants/routes'
import { getUserUuid } from '@/utils/getUserUuid'
import { getBrowserUuid } from '@/utils/getBrowserUuid'
const { doc } = defineProps<{
doc: QRCodeDocument
@@ -76,6 +78,8 @@ async function sendMessage(content: string) {
chat,
created_at: new Date().toISOString(),
from: 'guest',
user_uuid: getUserUuid(),
browser_uuid: getBrowserUuid(),
body: content.trim(),
}
@@ -1,16 +0,0 @@
import type { BaseDocument } from '@/types/DBDocumentTypes/BaseDocument'
export interface PublicSettingsDocument extends BaseDocument {
_id: string // Формат: "public_settings:<name>"
type: 'public_settings'
/**
* Название настройки
*/
name: string
/**
* Значение настройки
*/
value: string
}
@@ -1,11 +0,0 @@
import type { BaseDocument } from '@/types/DBDocumentTypes/BaseDocument'
export interface WebPushSubscriptionDocument extends BaseDocument {
_id: string // Формат: "web_push_subscription:<user_uuid>:<browser_uuid>"
type: 'web_push_subscription'
user_uuid: string // UUID пользователя, связанного с подпиской
browser_uuid: string // UUID браузера, связанного с подпиской
subscription: PushSubscriptionJSON
created_at: string // Дата создания подписки в ISO формате
updated_at?: string // Дата последнего обновления подписки
}
@@ -1,5 +0,0 @@
export * from './BaseDocument'
export * from './QRCodeDocument'
export * from './MessageDocument'
export * from './UserDocument'
export * from './WebPushSubscriptionDocument'
+1 -1
View File
@@ -44,7 +44,7 @@
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { createQrCodeDocument } from '@/api/qrCode'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
import { ROUTE_NAMES } from '@/constants/routes'
const placement = ref<null | QRCodeDocument['placement']>(null)
@@ -1,23 +1,32 @@
<template>
<div class="flex flex-col min-h-full">
<main class="flex-grow flex items-center justify-center py-6 px-4 md:px-6">
<main
class="flex-grow flex justify-center py-6 px-4 md:px-6"
:class="{
'items-center':
svgQueryIsLoading || qrCodeQueryIsLoading || svgQueryIsError || qrCodeQueryIsError,
}"
>
<section class="max-w-prose w-full flex flex-col gap-1">
<h2 class="text-xl font-semibold mb-4">Ваш QRкод готов</h2>
<p class="text-gray-600 mb-4">Сохраните его и ознакомьтесь с инструкциями по размещению.</p>
<!-- Рендер QRкода -->
<div v-if="!svgQueryIsLoading && svgQueryData" class="border p-4 bg-white rounded-lg mb-6">
<img :src="svgQueryData" alt="QR‑код" />
</div>
<p v-if="svgQueryIsLoading || qrCodeQueryIsLoading" class="text-gray-500 mt-4">
Загрузка...
</p>
<p v-else-if="svgQueryIsError" class="text-red-500 mt-4">
Ошибка генерации QRкода: {{ svgQueryError?.message }}
</p>
<!-- Сообщение об ошибке -->
<p v-if="qrCodeQueryIsError" class="text-red-500 mt-4">
<p v-else-if="qrCodeQueryIsError" class="text-red-500 mt-4">
Ошибка загрузки данных QRкода: {{ qrCodeQueryError!.message }}
</p>
<p v-if="svgQueryIsLoading" class="text-gray-500 mt-4">Загрузка...</p>
<template v-else>
<h2 class="text-xl font-semibold mb-4">Ваш QRкод готов</h2>
<p class="text-gray-600 mb-4">
Сохраните его и ознакомьтесь с инструкциями по размещению.
</p>
<!-- Рендер QRкода -->
<div class="border p-4 bg-white rounded-lg mb-6">
<img v-if="!svgQueryIsLoading && svgQueryData" :src="svgQueryData" alt="QR‑код" />
</div>
<!-- Кнопки для скачивания -->
<div
@@ -47,6 +56,7 @@
:placement="qrCodeDoc?.placement"
v-if="qrCodeDoc?.placement"
/>
</template>
</section>
</main>
@@ -71,7 +81,7 @@ import QRCodeStyling from 'qr-code-styling'
import { optimizeSvg } from '@/utils/images/svg/optimizeSvg'
import { replaceFillInSvg } from '@/utils/images/svg/replaceFillInSvg'
import { getQrCodeDocument } from '@/api/qrCode'
import { type QRCodeDocument } from '@/types/DBDocumentTypes'
import { type QRCodeDocument } from '@hereconnect/types'
import { addLabelToSvg } from '@/utils/images/svg/addLabelToSvg'
import logo from '@/assets/logo.svg?raw'
import { sanitizeFileName } from '@/utils/sanitizeFileName'
@@ -86,11 +96,15 @@ const height = ref(width.value)
// Хук для загрузки данных QR‑кода
const {
data: qrCodeDoc,
isLoading: qrCodeQueryIsLoading,
isError: qrCodeQueryIsError,
error: qrCodeQueryError,
} = useQuery({
queryKey: ['qrCode', computed(() => qr_code_uri)],
queryFn: () => getQrCodeDocument(qr_code_uri),
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
return getQrCodeDocument(qr_code_uri)
},
})
const sanitizedFileName = computed(() => {
@@ -110,6 +124,8 @@ const {
enabled: computed(() => !!qrCodeDoc.value?.url),
retry: false,
queryFn: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000))
// Заменяем цвет `fill` в логотипе
const updatedLogo = replaceFillInSvg(logo, logoColor.value)
+1 -1
View File
@@ -45,7 +45,7 @@
</template>
<script setup lang="ts">
import type { MessageDocument } from '@/types/DBDocumentTypes'
import type { MessageDocument } from '@hereconnect/types'
import ChatComponent from '@/components/ChatComponent.vue'
defineProps<{ qr_code_uri: MessageDocument['qr_code_uri']; chat: MessageDocument['chat'] }>()
</script>
+1 -1
View File
@@ -31,7 +31,7 @@
<script setup lang="ts">
import { useQuery } from '@tanstack/vue-query'
import { getQrCodeDocument } from '@/api/qrCode'
import type { QRCodeDocument } from '@/types/DBDocumentTypes'
import type { QRCodeDocument } from '@hereconnect/types'
import QueryRender from '@/components/QueryRender.vue'
const { qr_code_uri } = defineProps<{ qr_code_uri: string }>()
+6
View File
@@ -12,3 +12,9 @@ services:
- ./services/couchdb/config:/opt/couchdb/etc/local.d
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "10"
+39 -26
View File
@@ -26,6 +26,7 @@
"license": "@hereconnect/license",
"dependencies": {
"@f3ve/vue-markdown-it": "^0.2.3",
"@hereconnect/types": "^1.0.0",
"@primevue/forms": "^4.2.1",
"@primevue/themes": "^4.2.1",
"@tanstack/vue-query": "^5.60.6",
@@ -1194,6 +1195,10 @@
"resolved": "services/message-delivery-method-web-push",
"link": true
},
"node_modules/@hereconnect/types": {
"resolved": "packages/types",
"link": true
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@@ -3298,6 +3303,12 @@
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/couchdb-changes-stream": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/couchdb-changes-stream/-/couchdb-changes-stream-1.0.1.tgz",
"integrity": "sha512-msaurXYnqqAa4s9RmKV14NH+6kK8h7pppoSwTYfR55fdmaYqYLaIuPVoNuWixZaXPEgZ+cHRKIf4PpF1/tu8BQ==",
"license": "MIT"
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -4911,6 +4922,7 @@
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"dev": true,
"license": "MIT"
},
"node_modules/js-yaml": {
@@ -5179,19 +5191,6 @@
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/loupe": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz",
@@ -6543,19 +6542,6 @@
],
"license": "MIT"
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-query": {
"version": "3.39.3",
"resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz",
@@ -8449,6 +8435,30 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"packages/couchdb-changes-stream": {
"name": "@hereconnect/couchdb-changes-stream",
"version": "1.0.0",
"extraneous": true,
"license": "MIT",
"devDependencies": {
"@jest/types": "^29.6.3",
"@types/jest": "^29.5.14",
"@types/node": "^20.8.0",
"eslint": "^8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-prettier": "^5.0.0",
"fast-check": "^3.10.0",
"jest": "^29.7.0",
"prettier": "^3.1.0",
"ts-jest": "^29.2.5",
"typescript": "^5.2.2",
"vitest": "^0.34.5"
}
},
"packages/types": {
"name": "@hereconnect/types",
"version": "1.0.0"
},
"services/db-migrations": {
"name": "@hereconnect/db-migrations",
"version": "1.0.0",
@@ -8470,8 +8480,11 @@
"version": "1.0.0",
"license": "@hereconnect/license",
"dependencies": {
"@hereconnect/types": "^1.0.0",
"couchdb-changes-stream": "^1.0.1",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-core": "^9.0.0",
"pouchdb-find": "^9.0.0",
"web-push": "^3.6.7"
},
"engines": {
-1
View File
@@ -21,4 +21,3 @@
"lint": "npm run lint --workspaces"
}
}
+1
View File
@@ -0,0 +1 @@
dist
@@ -23,6 +23,12 @@ export interface MessageDocument extends BaseDocument {
/** Опциональная геопозиция (широта и долгота) */
location?: { lat: number; lng: number }
/** Идентификатор пользователя, отправившего сообщение */
user_uuid: string
/** Идентификатор браузера, отправившего сообщение */
browser_uuid: string
/** Вложения, такие как фото */
// _attachments?: {
// [key: string]: Attachment
@@ -0,0 +1,16 @@
import type { BaseDocument } from "./BaseDocument";
export interface PublicSettingsDocument extends BaseDocument {
_id: string; // Формат: "public_settings:<name>"
type: "public_settings";
/**
* Название настройки
*/
name: string;
/**
* Значение настройки
*/
value: string;
}
@@ -0,0 +1,24 @@
import type { BaseDocument } from "./BaseDocument";
type MessageDeliveryMethodWebPushVapid = {
name: "message_delivery_method_web_push_vapid";
value: {
subject: string;
keys: {
publicKey: string;
privateKey: string;
};
};
};
type MessageDeliveryMethodWebPushGcmApiKey = {
name: "message_delivery_method_web_push_gcm_api_key";
value: string;
};
export type ServerSettingsDocument = (
| MessageDeliveryMethodWebPushVapid
| MessageDeliveryMethodWebPushGcmApiKey
) & {
_id: string; // Формат: "server_settings:<name>"
type: "server_settings";
} & BaseDocument;
@@ -0,0 +1,11 @@
import type { BaseDocument } from "./BaseDocument";
export interface WebPushSubscriptionDocument extends BaseDocument {
_id: string; // Формат: "web_push_subscription:<user_uuid>:<browser_uuid>"
type: "web_push_subscription";
user_uuid: string; // UUID пользователя, связанного с подпиской
browser_uuid: string; // UUID браузера, связанного с подпиской
subscription: PushSubscriptionJSON;
created_at: string; // Дата создания подписки в ISO формате
updated_at?: string; // Дата последнего обновления подписки
}
+7
View File
@@ -0,0 +1,7 @@
export * from "./BaseDocument";
export * from "./MessageDocument";
export * from "./PublicSettingsDocument";
export * from "./QRCodeDocument";
export * from "./ServerSettingsDocument";
export * from "./UserDocument";
export * from "./WebPushSubscriptionDocument";
+1
View File
@@ -0,0 +1 @@
export * from "./DBDocuments";
+10
View File
@@ -0,0 +1,10 @@
{
"name": "@hereconnect/types",
"version": "1.0.0",
"main": "index.ts",
"types": "index.ts",
"private": true,
"scripts": {
"build": "tsc"
}
}
+9
View File
@@ -0,0 +1,9 @@
{
"compilerOptions": {
"declaration": true,
"emitDeclarationOnly": true,
"outDir": "./dist",
"strict": true
},
"include": ["index.ts"]
}
@@ -1,4 +0,0 @@
import PouchDB from "pouchdb-core";
import PouchDBAdapterHttp from "pouchdb-adapter-http";
export default PouchDB.plugin(PouchDBAdapterHttp);
@@ -2,7 +2,7 @@
"name": "@hereconnect/message-delivery-method-web-push",
"version": "1.0.0",
"description": "",
"main": "index.js",
"main": "src/server.ts",
"type": "module",
"license": "@hereconnect/license",
"private": true,
@@ -24,13 +24,16 @@
},
"contributors": [],
"scripts": {
"generate-keys": "node ./generate-keys.js",
"test-send-notification": "node ./test-send-notification.js",
"test-send-notification-cancel": "node ./test-send-notification-cancel.js"
"generate-keys": "node ./src/generate-keys.js",
"test-send-notification": "node ./src/test-send-notification.js",
"test-send-notification-cancel": "node ./src/test-send-notification-cancel.js"
},
"dependencies": {
"@hereconnect/types": "^1.0.0",
"couchdb-changes-stream": "^1.0.1",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-core": "^9.0.0",
"pouchdb-find": "^9.0.0",
"web-push": "^3.6.7"
}
}
@@ -0,0 +1,7 @@
import PouchDB from "pouchdb-core";
import PouchDBAdapterHttp from "pouchdb-adapter-http";
import PouchDBFindPlugin from 'pouchdb-find'
export default PouchDB
.plugin(PouchDBAdapterHttp)
.plugin(PouchDBFindPlugin);
@@ -0,0 +1,38 @@
import PouchDB from "./PouchDB";
import type {
MessageDocument,
QRCodeDocument,
ServerSettingsDocument,
} from "@hereconnect/types";
export interface ServiceDoc {
_id: string;
lastSeq: string | number;
}
if (!process.env.API_URL) {
throw new Error("Environment variable API_URL is not set");
}
export const messagesDbUrl = `${process.env.API_URL}/messages`;
export const serverSettingsDb = new PouchDB<ServerSettingsDocument>(
`${process.env.API_URL}/server_settings`,
{
skip_setup: true,
},
);
export const qrDb = new PouchDB<QRCodeDocument>(`${process.env.API_URL}/qr`, {
skip_setup: true,
});
export const { messagesDb, serviceDb } = (() => {
const messagesDB = new PouchDB(messagesDbUrl, {
skip_setup: true,
});
return {
messagesDb: messagesDB as PouchDB.Database<MessageDocument>,
serviceDb: messagesDB as PouchDB.Database<ServiceDoc>,
} as const;
})();
@@ -0,0 +1,81 @@
import webpush from "web-push";
import { CouchDBChangesStream } from "couchdb-changes-stream";
import { qrDb, messagesDbUrl } from "./dbs";
import { setupVapidDetails } from "./setupVapidDetails";
import type { MessageDocument } from "@hereconnect/types";
import { ServiceSeqTracker } from "./serviceSeqTracker";
const serviceSeqTracker = new ServiceSeqTracker();
const start = async () => {
await setupVapidDetails();
const since = await serviceSeqTracker.getSince();
const changesStream = new CouchDBChangesStream<MessageDocument>(
messagesDbUrl,
{
since,
feed: "continuous",
include_docs: true,
heartbeat: 1000,
filter: "_selector",
selector: {
type: "message",
},
},
);
// Gracefully handle SIGINT
process.on("SIGINT", async () => {
changesStream.stop();
process.stdout.write("\n");
process.exit(2);
});
console.info("Service started from sequence", since);
for await (const change of changesStream) {
const { seq, doc } = change;
if (!doc) {
console.error("Change missing document:", change);
continue;
}
serviceSeqTracker.checkNextSeq(seq);
try {
if (doc.from === "guest") {
const qrDoc = await qrDb.get(`qr:${doc.qr_code_uri}`);
if (qrDoc.messageDeliveryMethod !== "webPush") {
console.log(`Skip !webPush`);
continue;
}
console.log("FROM GUEST");
console.log(qrDoc.user_uuid);
console.log(qrDoc.browser_uuid);
} else if (doc.from === "owner") {
console.log("FROM OWNER");
} else {
continue;
}
console.log(
`Processed change: ${JSON.stringify({ _id: change.id, ref: doc._rev, seq: change.seq })}`,
);
} catch (error) {
console.error(
`Failed processing: ${JSON.stringify(error)} change: ${JSON.stringify({ id: doc._id, rev: doc._rev, seq: seq })}`,
);
} finally {
await serviceSeqTracker.saveSeq(seq);
}
}
};
start().catch((error) => {
console.error("error", error);
process.exit(1);
});
@@ -0,0 +1,38 @@
import { serviceDb, type ServiceDoc } from "./dbs";
const SERVICE_DOC_ID = "_local/service:message-delivery-method-web-push";
export class ServiceSeqTracker {
private serviceDoc: ServiceDoc;
async getSince() {
this.serviceDoc = await serviceDb.get(SERVICE_DOC_ID).catch((error) => {
if (error.name === "not_found") {
return {
_id: SERVICE_DOC_ID,
lastSeq: "0",
};
}
throw error; // Re-throw other errors
});
this.serviceDoc.lastSeq = 0;
return this.serviceDoc.lastSeq;
}
checkNextSeq(seq: number | string) {
const next = parseInt(seq as string, 10);
const current = parseInt(this.serviceDoc.lastSeq as string, 10);
if (next < current) {
throw new Error(
`Sequence number is too old: ${seq} (${next} < ${current})`,
);
}
}
saveSeq(seq: number | string) {
this.serviceDoc.lastSeq = seq;
return serviceDb.put(this.serviceDoc);
}
}
@@ -0,0 +1,77 @@
import webpush from "web-push";
import { serverSettingsDb } from "./dbs";
import { ServerSettingsDocument } from "@hereconnect/types";
function isType<N extends ServerSettingsDocument["name"]>(
doc: ServerSettingsDocument,
name: N,
): doc is ServerSettingsDocument & { name: N } {
return doc.name === name;
}
export const setupVapidDetails = async () => {
const changes = serverSettingsDb.changes({
since: "now",
live: true,
include_docs: true,
doc_ids: [
"server_settings:message_delivery_method_web_push_vapid",
"public_settings:message_delivery_method_web_push_gcm_api_key",
],
});
const {
rows: [vapidDetailsRow, gcmApiRow],
} = await serverSettingsDb.allDocs({
keys: [
"server_settings:message_delivery_method_web_push_vapid",
"public_settings:message_delivery_method_web_push_gcm_api_key",
],
include_docs: true,
});
if (
!("doc" in vapidDetailsRow) ||
!isType(vapidDetailsRow.doc, "message_delivery_method_web_push_vapid")
) {
throw new Error(
`VAPID details ${"error" in vapidDetailsRow ? vapidDetailsRow.error : "not exists"}`,
);
}
if (
"doc" in gcmApiRow &&
isType(gcmApiRow.doc, "message_delivery_method_web_push_gcm_api_key")
) {
webpush.setGCMAPIKey(gcmApiRow.doc.value);
}
webpush.setVapidDetails(
vapidDetailsRow.doc.value.subject,
vapidDetailsRow.doc.value.keys.publicKey,
vapidDetailsRow.doc.value.keys.privateKey,
);
changes.on("change", (change) => {
if (!("doc" in change)) {
return;
}
if (isType(change.doc, "message_delivery_method_web_push_vapid")) {
console.log("update vapid details");
webpush.setVapidDetails(
change.doc.value.subject,
change.doc.value.keys.publicKey,
change.doc.value.keys.privateKey,
);
return;
}
if (
"doc" in change &&
isType(change.doc, "message_delivery_method_web_push_gcm_api_key")
) {
console.log("update gcm api key");
webpush.setGCMAPIKey(change.doc.value);
}
});
};