mirror of
https://github.com/RoboSats/robosats-deploy.git
synced 2025-08-01 23:52:00 +00:00
Merge a12faf371dc9ce62bc60ad9aa41a48bf20bf07e6 into d60ed65f849ef6387dde6178054672563cb3d6d6
This commit is contained in:
@ -153,6 +153,21 @@ services:
|
||||
- ${STRFRY_CONF}:/etc/strfry.conf:ro
|
||||
- ${STRFRY_DATA}/db:/app/strfry-db:rw
|
||||
network_mode: service:tor
|
||||
|
||||
simplex:
|
||||
build: ./simplex
|
||||
command: >
|
||||
bash -c "
|
||||
simplex-chat -p 5225 -d /simplex_database/test_db &
|
||||
sleep 5;
|
||||
cd /app/simplex-chat/packages/simplex-chat-client/typescript/bot/;
|
||||
node simplex_bot.js"
|
||||
container_name: simplex${SUFFIX}
|
||||
volumes:
|
||||
- ${SIMPLEX_DB}:/simplex_database
|
||||
- ${SIMPLEX_BOT}:/app/simplex-chat/packages/simplex-chat-client/typescript/bot/
|
||||
network_mode: service:tor
|
||||
|
||||
|
||||
# Example simple backup service (copy/paste to attached storage locations)
|
||||
# backup:
|
||||
|
@ -33,6 +33,9 @@ STRFRY_URLS_EXTERNAL='./strfry/tn.external_urls.txt'
|
||||
STRFRY_URLS_FEDERATION='./strfry/tn.federation_urls.txt'
|
||||
STRFRY_DATA='/custom_path/testnet/strfry'
|
||||
|
||||
SIMPLEX_DB='./simplex/simplex_database'
|
||||
SIMPLEX_BOT='./simplex/bot'
|
||||
|
||||
# Port and number of HTTP server workers for the robosats backend
|
||||
WEB_LOCAL_PORT=8001
|
||||
GUNICORN_WORKERS=2
|
||||
|
85
compose/simplex/Dockerfile
Normal file
85
compose/simplex/Dockerfile
Normal 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
|
315
compose/simplex/bot/simplex_bot.js
Normal file
315
compose/simplex/bot/simplex_bot.js
Normal file
@ -0,0 +1,315 @@
|
||||
const { ChatClient } = require("..");
|
||||
const { ChatType } = require("../dist/command");
|
||||
const { ciContentText, ChatInfoType } = require("../dist/response");
|
||||
const { Client } = require("pg");
|
||||
const sqlite3 = require("sqlite3").verbose();
|
||||
|
||||
// Define PostgreSQL connection details at the top of the script
|
||||
const dbConfig = {
|
||||
user: "postgres", // Change to your actual user
|
||||
host: "localhost",
|
||||
database: "postgres", // Change to your actual database name
|
||||
password: "postgres", // Change to your actual pass
|
||||
port: 5432,
|
||||
};
|
||||
|
||||
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;
|
||||
if (resp.type === "newChatItem") {
|
||||
const { chatInfo } = resp.chatItem;
|
||||
if (chatInfo.type !== ChatInfoType.Direct) continue;
|
||||
const msg = ciContentText(resp.chatItem.chatItem.content);
|
||||
|
||||
if (msg.startsWith("/start ")) {
|
||||
const parts = msg.split(" ");
|
||||
if (parts.length >= 2) {
|
||||
const token = parts[1];
|
||||
|
||||
// Verify token and get robot info
|
||||
const valid = await verifyToken(token);
|
||||
|
||||
if (valid) {
|
||||
// Save the user mapping for this robot
|
||||
await saveUserMapping(chatInfo.contact.contactId, valid.robot_id, db);
|
||||
|
||||
// Send the message with the robot name
|
||||
const message = `You have successfully registered ✅. You will receive notifications here for Robot 🤖: ${valid.robot_name}.`;
|
||||
await chat.apiSendTextMessage(ChatType.Direct, chatInfo.contact.contactId, message);
|
||||
} 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>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function verifyToken(token) {
|
||||
// Use dbConfig for PostgreSQL connection
|
||||
const pgClient = new Client(dbConfig);
|
||||
|
||||
try {
|
||||
await pgClient.connect();
|
||||
|
||||
// Query the robot ID using the token
|
||||
const res = await pgClient.query("SELECT id, user_id FROM api_robot WHERE telegram_token = $1", [token]);
|
||||
|
||||
if (res.rows.length > 0) {
|
||||
const robotId = res.rows[0].id;
|
||||
const userId = res.rows[0].user_id;
|
||||
|
||||
// Query the robot's name using the user_id
|
||||
const robotNameRes = await pgClient.query("SELECT username FROM auth_user WHERE id = $1", [userId]);
|
||||
|
||||
await pgClient.end(); // Close the connection
|
||||
|
||||
if (robotNameRes.rows.length > 0) {
|
||||
const robotName = robotNameRes.rows[0].username;
|
||||
return { robot_id: robotId, robot_name: robotName };
|
||||
} else {
|
||||
return { robot_id: robotId, robot_name: "Unknown Robot" };
|
||||
}
|
||||
} else {
|
||||
await pgClient.end(); // Close the connection
|
||||
return null; // Invalid token
|
||||
}
|
||||
} 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) {
|
||||
// Use dbConfig for PostgreSQL connection
|
||||
const pgClient = new Client(dbConfig);
|
||||
|
||||
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) {
|
||||
// Use dbConfig for PostgreSQL connection
|
||||
const pgClient = new Client(dbConfig);
|
||||
|
||||
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
6709
compose/simplex/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
63
compose/simplex/package.json
Normal file
63
compose/simplex/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
compose/simplex/simplex_database/.gitkeep
Normal file
1
compose/simplex/simplex_database/.gitkeep
Normal file
@ -0,0 +1 @@
|
||||
|
Reference in New Issue
Block a user