Add files via upload

This commit is contained in:
lft3t8bx
2024-10-01 12:35:24 +00:00
committed by GitHub
parent 932fe9247b
commit e93e9e8d0c
4 changed files with 7204 additions and 0 deletions

View File

@ -0,0 +1,85 @@
# Stage 1: Build Haskell Simplex Chat Backend
ARG TAG=22.04
FROM ubuntu:${TAG} AS haskell-build
# Install dependencies for building simplex-chat
RUN apt-get update && apt-get install -y curl git build-essential libgmp3-dev zlib1g-dev llvm-12 llvm-12-dev libnuma-dev libssl-dev
# Set up environment variables for GHC and Cabal versions
ENV BOOTSTRAP_HASKELL_GHC_VERSION=9.6.3
ENV BOOTSTRAP_HASKELL_CABAL_VERSION=3.10.1.0
# Install ghcup for managing GHC and Cabal versions
RUN curl --proto '=https' --tlsv1.2 -sSf https://get-ghcup.haskell.org | BOOTSTRAP_HASKELL_NONINTERACTIVE=1 sh
# Add GHCup and Cabal to PATH
ENV PATH="/root/.cabal/bin:/root/.ghcup/bin:$PATH"
# Set the default GHC and Cabal versions
RUN ghcup set ghc "${BOOTSTRAP_HASKELL_GHC_VERSION}" && \
ghcup set cabal "${BOOTSTRAP_HASKELL_CABAL_VERSION}"
# Copy the project source code into the container
RUN git clone https://github.com/simplex-chat/simplex-chat /project
WORKDIR /project
# Copy Linux-specific cabal configuration file
RUN cp ./scripts/cabal.project.local.linux ./cabal.project.local
# Update Cabal package list and build the executable
RUN cabal update
RUN cabal build exe:simplex-chat
# Strip debug symbols to reduce the binary size
RUN bin=$(find /project/dist-newstyle -name "simplex-chat" -type f -executable) && \
mv "$bin" ./ && \
strip ./simplex-chat
# Stage 2: Build Node.js Simplex Chat Client and Bot
FROM node:18 AS node-build
# Install git for cloning repositories
RUN apt-get update && apt-get install -y git
# Clone the SimpleX Chat repository
RUN git clone https://github.com/simplex-chat/simplex-chat /app/simplex-chat
# Set working directory to the TypeScript client folder
WORKDIR /app/simplex-chat/packages/simplex-chat-client/typescript
# Copy your bot script into the examples directory
COPY ./bot/simplex_bot.js /app/simplex-chat/packages/simplex-chat-client/typescript/bot/simplex_bot.js
# Copy the updated package.json and package-lock.json into the container
COPY ./package.json ./package-lock.json ./
# Install the required npm packages, including sqlite3
RUN npm install
# Build the client
RUN npm run build
# Final Stage: Combine Haskell Backend and Node.js Bot
FROM ubuntu:${TAG}
# Install runtime dependencies for the Haskell backend and Node.js
RUN apt-get update && apt-get install -y libgmp10 zlib1g curl
# Install Node.js and npm in the final stage
RUN curl -sL https://deb.nodesource.com/setup_18.x | bash - && \
apt-get install -y nodejs
# Set working directory for the final container
WORKDIR /app/simplex-chat/packages/simplex-chat-client/typescript
# Copy the built Haskell executable from the build stage
COPY --from=haskell-build /project/simplex-chat /usr/local/bin/simplex-chat
# Copy the built Node.js client and bot script from the node-build stage
COPY --from=node-build /app/simplex-chat /app/simplex-chat
# Install the required npm packages, including sqlite3
RUN npm install sqlite3 pg
# Expose necessary ports
EXPOSE 5225 3000

View File

@ -0,0 +1,347 @@
const {ChatClient} = require("..")
const {ChatType} = require("../dist/command")
const {ciContentText, ChatInfoType} = require("../dist/response")
const { Pool } = require('pg');
const sqlite3 = require('sqlite3').verbose()
run()
async function run() {
// Connect to SimpleX server
const chat = await ChatClient.create("ws://localhost:5225")
const user = await chat.apiGetActiveUser()
if (!user) {
console.log("No user profile")
return
}
console.log(`Bot profile: ${user.profile.displayName} (${user.profile.fullName})`)
// Create or use existing long-term address for the bot
const address = (await chat.apiGetUserAddress()) || (await chat.apiCreateUserAddress())
console.log(`Bot address: ${address}`)
// Enable automatic acceptance of contact connections
await chat.enableAddressAutoAccept()
// Set up local database (SQLite) for the bot's data
const db = new sqlite3.Database('./botdata.sqlite', (err) => {
if (err) {
console.error('Error opening database:', err)
} else {
console.log('Connected to the bot data database.')
}
})
// Create tables if they don't exist
db.serialize(() => {
db.run(`CREATE TABLE IF NOT EXISTS user_mappings (
simplex_user_id INTEGER,
robot_id INTEGER,
mapping_created_at DATETIME,
notifications_enabled BOOLEAN,
PRIMARY KEY(simplex_user_id, robot_id)
)`)
db.run(`CREATE TABLE IF NOT EXISTS sent_notifications (
id INTEGER PRIMARY KEY AUTOINCREMENT,
notification_id INTEGER,
simplex_user_id INTEGER,
FOREIGN KEY(simplex_user_id) REFERENCES user_mappings(simplex_user_id)
)`)
})
// Start processing messages
processMessages(chat, db)
// Start polling for notifications
pollNotifications(chat, db)
async function processMessages(chat, db) {
for await (const r of chat.msgQ) {
const resp = r instanceof Promise ? await r : r
switch (resp.type) {
case "contactConnected": {
// Send welcome message when a new contact is connected
const { contact } = resp
console.log(`${contact.profile.displayName} connected`)
await chat.apiSendTextMessage(
ChatType.Direct,
contact.contactId,
"Welcome to the bot. Please send /start <token> to register."
)
continue
}
case "newChatItem": {
const { chatInfo } = resp.chatItem
if (chatInfo.type !== ChatInfoType.Direct) continue
const msg = ciContentText(resp.chatItem.chatItem.content)
if (msg) {
// Check if message starts with /start
if (msg.startsWith('/start ')) {
const parts = msg.split(' ')
if (parts.length >= 2) {
const token = parts[1]
// Verify token with backend
const valid = await verifyToken(token)
if (valid) {
// Save mapping
await saveUserMapping(chatInfo.contact.contactId, valid.robot_id, db)
await chat.apiSendTextMessage(
ChatType.Direct,
chatInfo.contact.contactId,
`You have successfully registered. You will receive notifications here.`
)
} else {
await chat.apiSendTextMessage(
ChatType.Direct,
chatInfo.contact.contactId,
`Invalid token. Please make sure you copied it correctly.`
)
}
} else {
await chat.apiSendTextMessage(
ChatType.Direct,
chatInfo.contact.contactId,
`Please provide a token. Usage: /start <token>`
)
}
} else {
// Handle other messages
await chat.apiSendTextMessage(
ChatType.Direct,
chatInfo.contact.contactId,
`Unknown command. Please use /start <token> to register.`
)
}
}
}
}
}
}
async function verifyToken(token) {
// Connect to the backend Postgres database
const pgClient = new Client({
// Connection details
user: 'postgres',
host: 'localhost',
user: 'postgres',
password: 'postgres',
port: 5432,
})
try {
await pgClient.connect()
// Query the Robot table for the token
const res = await pgClient.query(
'SELECT id FROM api_robot WHERE telegram_token = $1',
[token]
)
await pgClient.end()
if (res.rows.length > 0) {
// Token is valid
return { robot_id: res.rows[0].id }
} else {
// Token is invalid
return null
}
} catch (err) {
console.error('Error verifying token:', err)
return null
}
}
function saveUserMapping(simplex_user_id, robot_id, db) {
return new Promise((resolve, reject) => {
const now = new Date().toISOString()
db.run(
'INSERT INTO user_mappings (simplex_user_id, robot_id, mapping_created_at, notifications_enabled) VALUES (?, ?, ?, ?)' +
' ON CONFLICT(simplex_user_id, robot_id) DO UPDATE SET mapping_created_at=excluded.mapping_created_at, notifications_enabled=1',
[simplex_user_id, robot_id, now, 1],
function(err) {
if (err) {
console.error('Error saving user mapping:', err)
reject(err)
} else {
console.log(`Saved mapping: simplex_user_id=${simplex_user_id}, robot_id=${robot_id}`)
resolve()
}
}
)
})
}
async function pollNotifications(chat, db) {
// Poll the backend database every 3 minutes
setInterval(async () => {
try {
// Get all user mappings
const userMappings = await getUserMappings(db)
const now = new Date()
const twoDaysInMs = 2 * 24 * 60 * 60 * 1000
for (const mapping of userMappings) {
const { simplex_user_id, robot_id, mapping_created_at, notifications_enabled } = mapping
const mappingDate = new Date(mapping_created_at)
if (notifications_enabled) {
if (now - mappingDate >= twoDaysInMs) {
// Disable notifications
await disableNotifications(simplex_user_id, robot_id, db)
// Get robot name
const robotName = await getRobotName(robot_id)
// Send message to user
const message = `Notifications for ${robotName} are disabled. The lifetime of the notification is 2 days. You can reenable the notification for this robot sending /start <token in cleartext for that specific robot>, but you should consider creating a new robot for each trade`
await chat.apiSendTextMessage(
ChatType.Direct,
simplex_user_id,
message
)
} else {
// Continue processing notifications
const notifications = await getNotificationsForRobot(robot_id, mapping_created_at)
for (const notification of notifications) {
// Check if notification has already been sent
const sent = await checkNotificationSent(notification.id, simplex_user_id, db)
if (!sent) {
// Send notification
await chat.apiSendTextMessage(
ChatType.Direct,
simplex_user_id,
`${notification.title} ${notification.description}`
)
// Mark notification as sent
await markNotificationSent(notification.id, simplex_user_id, db)
}
}
}
}
}
} catch (err) {
console.error('Error polling notifications:', err)
}
}, 180000) // Every 180 seconds
}
function getUserMappings(db) {
return new Promise((resolve, reject) => {
db.all('SELECT simplex_user_id, robot_id, mapping_created_at, notifications_enabled FROM user_mappings', [], function(err, rows) {
if (err) {
console.error('Error getting user mappings:', err)
reject(err)
} else {
resolve(rows)
}
})
})
}
async function getNotificationsForRobot(robot_id, sinceTime) {
// Connect to the backend Postgres database
const pgClient = new Client({
// Connection details
user: 'postgres',
host: 'localhost',
user: 'postgres',
password: 'postgres',
port: 5432,
})
try {
await pgClient.connect()
// Query the Notification table for notifications for this robot created after 'sinceTime'
const res = await pgClient.query(
'SELECT id, title, description FROM api_notification WHERE robot_id = $1 AND created_at > $2',
[robot_id, sinceTime]
)
await pgClient.end()
return res.rows
} catch (err) {
console.error('Error getting notifications:', err)
return []
}
}
function checkNotificationSent(notification_id, simplex_user_id, db) {
return new Promise((resolve, reject) => {
db.get(
'SELECT id FROM sent_notifications WHERE notification_id = ? AND simplex_user_id = ?',
[notification_id, simplex_user_id],
function(err, row) {
if (err) {
console.error('Error checking notification sent:', err)
reject(err)
} else {
resolve(row ? true : false)
}
}
)
})
}
function markNotificationSent(notification_id, simplex_user_id, db) {
return new Promise((resolve, reject) => {
db.run(
'INSERT INTO sent_notifications (notification_id, simplex_user_id) VALUES (?, ?)',
[notification_id, simplex_user_id],
function(err) {
if (err) {
console.error('Error marking notification sent:', err)
reject(err)
} else {
resolve()
}
}
)
})
}
function disableNotifications(simplex_user_id, robot_id, db) {
return new Promise((resolve, reject) => {
db.run(
'UPDATE user_mappings SET notifications_enabled = 0 WHERE simplex_user_id = ? AND robot_id = ?',
[simplex_user_id, robot_id],
function(err) {
if (err) {
console.error('Error disabling notifications:', err)
reject(err)
} else {
resolve()
}
}
)
})
}
async function getRobotName(robot_id) {
const pgClient = new Client({
user: 'postgres',
host: 'localhost',
user: 'postgres',
password: 'postgres',
port: 5432,
})
try {
await pgClient.connect()
const res = await pgClient.query(
'SELECT user_id FROM api_robot WHERE id = $1',
[robot_id]
)
if (res.rows.length > 0) {
const user_id = res.rows[0].user_id
// Now get username from auth_user table
const userRes = await pgClient.query(
'SELECT username FROM auth_user WHERE id = $1',
[user_id]
)
await pgClient.end()
if (userRes.rows.length > 0) {
return userRes.rows[0].username
} else {
return 'Unknown Robot'
}
} else {
await pgClient.end()
return 'Unknown Robot'
}
} catch (err) {
console.error('Error getting robot name:', err)
return 'Unknown Robot'
}
}
}

6709
compose/simplex/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,63 @@
{
"name": "simplex-chat",
"version": "0.2.0",
"description": "SimpleX Chat client",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"test": "npm run prettier:check && npm run eslint && jest --coverage",
"build": "npm run prettier:write && npm run eslint && tsc && ./copy && npm run bundle",
"bundle": "rollup dist/index-web.js --file dist/index.bundle.js --format umd --name simplex",
"eslint": "eslint --ext .ts ./src/**/*.ts",
"prettier:write": "prettier --write './**/*.{json,yaml,js,ts}'",
"prettier:check": "prettier --list-different './**/*.{json,yaml,js,ts}'"
},
"repository": {
"type": "git",
"url": "git+https://github.com/simplex-chat/simplex-chat.git"
},
"keywords": [
"messenger",
"chat",
"privacy",
"security"
],
"author": "SimpleX Chat",
"license": "AGPL-3.0",
"bugs": {
"url": "https://github.com/simplex-chat/simplex-chat/issues"
},
"homepage": "https://github.com/simplex-chat/simplex-chat/packages/simplex-chat-client/typescript#readme",
"dependencies": {
"isomorphic-ws": "^4.0.1",
"pg": "^8.13.0",
"sqlite3": "^5.1.6"
},
"devDependencies": {
"@types/jest": "^27.5.1",
"@types/node": "^18.11.18",
"@typescript-eslint/eslint-plugin": "^5.23.0",
"@typescript-eslint/parser": "^5.23.0",
"eslint": "^8.15.0",
"eslint-config-prettier": "^8.5.0",
"husky": "^7.0.4",
"jest": "^28.1.0",
"lint-staged": "^12.3.8",
"prettier": "^2.6.2",
"rollup": "^2.72.1",
"ts-jest": "^28.0.2",
"ts-node": "^10.7.0",
"typescript": "^4.9.3"
},
"husky": {
"hooks": {
"pre-commit": "lint-staged"
}
},
"lint-staged": {
"**/*": "prettier --write --ignore-unknown"
}
}