This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,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,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'
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }>()
|
||||
|
||||
@@ -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"
|
||||
Generated
+39
-26
@@ -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": {
|
||||
|
||||
@@ -21,4 +21,3 @@
|
||||
"lint": "npm run lint --workspaces"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
dist
|
||||
+6
@@ -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; // Дата последнего обновления подписки
|
||||
}
|
||||
@@ -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";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./DBDocuments";
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "@hereconnect/types",
|
||||
"version": "1.0.0",
|
||||
"main": "index.ts",
|
||||
"types": "index.ts",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user