Browse Source

sphinx-relay

feature/dockerfile-arm v0.7.2
Evan Feenstra 5 years ago
commit
f6abf808dc
  1. 6
      .babelrc
  2. 20
      .gitignore
  3. 16
      Dockerfile
  4. 21
      LICENSE
  5. 42
      README.md
  6. 318
      api/controllers/chats.ts
  7. 240
      api/controllers/contacts.ts
  8. 123
      api/controllers/details.ts
  9. 139
      api/controllers/index.ts
  10. 118
      api/controllers/invites.ts
  11. 273
      api/controllers/invoices.ts
  12. 514
      api/controllers/media.ts
  13. 277
      api/controllers/messages.ts
  14. 199
      api/controllers/payment.ts
  15. 29
      api/controllers/schemas.ts
  16. 387
      api/controllers/subscriptions.ts
  17. 60
      api/controllers/uploads.ts
  18. 63
      api/crypto/rsa.ts
  19. 144
      api/grpc/index.ts
  20. 247
      api/helpers.ts
  21. 219
      api/hub.ts
  22. 16
      api/models/index.ts
  23. 48
      api/models/ts/chat.ts
  24. 57
      api/models/ts/contact.ts
  25. 35
      api/models/ts/invite.ts
  26. 39
      api/models/ts/mediaKey.ts
  27. 89
      api/models/ts/message.ts
  28. 49
      api/models/ts/subscription.ts
  29. 27
      api/utils/case.ts
  30. 49
      api/utils/cron.ts
  31. 312
      api/utils/decode/index.js
  32. 49
      api/utils/gitinfo.ts
  33. 53
      api/utils/json.ts
  34. 153
      api/utils/ldat.ts
  35. 321
      api/utils/lightning.ts
  36. 5
      api/utils/lock.ts
  37. 28
      api/utils/logger.ts
  38. 79
      api/utils/msg.ts
  39. 84
      api/utils/nodeinfo.ts
  40. 19
      api/utils/res.ts
  41. 86
      api/utils/setup.ts
  42. 33
      api/utils/socket.ts
  43. 12
      api/utils/zbase32/index.ts
  44. 23035
      api/utils/zbase32/tv42_zbase32_gopherjs.js
  45. 114
      app.ts
  46. 32
      config/app.json
  47. 18
      config/config.json
  48. 52
      config/constants.json
  49. 308
      dist/api/controllers/chats.js
  50. 1
      dist/api/controllers/chats.js.map
  51. 205
      dist/api/controllers/contacts.js
  52. 1
      dist/api/controllers/contacts.js.map
  53. 133
      dist/api/controllers/details.js
  54. 1
      dist/api/controllers/details.js.map
  55. 139
      dist/api/controllers/index.js
  56. 1
      dist/api/controllers/index.js.map
  57. 101
      dist/api/controllers/invites.js
  58. 1
      dist/api/controllers/invites.js.map
  59. 241
      dist/api/controllers/invoices.js
  60. 1
      dist/api/controllers/invoices.js.map
  61. 481
      dist/api/controllers/media.js
  62. 1
      dist/api/controllers/media.js.map
  63. 239
      dist/api/controllers/messages.js
  64. 1
      dist/api/controllers/messages.js.map
  65. 178
      dist/api/controllers/payment.js
  66. 1
      dist/api/controllers/payment.js.map
  67. 25
      dist/api/controllers/schemas.js
  68. 1
      dist/api/controllers/schemas.js.map
  69. 402
      dist/api/controllers/subscriptions.js
  70. 1
      dist/api/controllers/subscriptions.js.map
  71. 64
      dist/api/controllers/uploads.js
  72. 1
      dist/api/controllers/uploads.js.map
  73. 65
      dist/api/crypto/rsa.js
  74. 1
      dist/api/crypto/rsa.js.map
  75. 150
      dist/api/grpc/index.js
  76. 1
      dist/api/grpc/index.js.map
  77. 240
      dist/api/helpers.js
  78. 1
      dist/api/helpers.js.map
  79. 201
      dist/api/hub.js
  80. 1
      dist/api/hub.js.map
  81. 10
      dist/api/models/index.js
  82. 1
      dist/api/models/index.js.map
  83. 72
      dist/api/models/ts/chat.js
  84. 1
      dist/api/models/ts/chat.js.map
  85. 84
      dist/api/models/ts/contact.js
  86. 1
      dist/api/models/ts/contact.js.map
  87. 56
      dist/api/models/ts/invite.js
  88. 1
      dist/api/models/ts/invite.js.map
  89. 59
      dist/api/models/ts/mediaKey.js
  90. 1
      dist/api/models/ts/mediaKey.js.map
  91. 128
      dist/api/models/ts/message.js
  92. 1
      dist/api/models/ts/message.js.map
  93. 76
      dist/api/models/ts/subscription.js
  94. 1
      dist/api/models/ts/subscription.js.map
  95. 28
      dist/api/utils/case.js
  96. 1
      dist/api/utils/case.js.map
  97. 45
      dist/api/utils/cron.js
  98. 1
      dist/api/utils/cron.js.map
  99. 294
      dist/api/utils/decode/index.js
  100. 1
      dist/api/utils/decode/index.js.map

6
.babelrc

@ -0,0 +1,6 @@
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
]
}

20
.gitignore

@ -0,0 +1,20 @@
dist/config/app.json
dist/config/config.json
node_modules/*
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
.pgpass
.vscode
.DS_Store
public/uploads/*
!public/uploads/.gitkeep
sqlite/sphinx.db
sqlite/sphinxpg.sql
sqlite/sphinxlite.sql

16
Dockerfile

@ -0,0 +1,16 @@
FROM node:8
RUN apt-get update
RUN apt-get install -f sqlite3
USER node
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
ENV PATH=$PATH:/home/node/.npm-global/bin
WORKDIR /home/node
COPY package.json .
RUN npm install
RUN npm install -g nodemon --save-dev
RUN npm install -g express --save-dev
RUN npm install -g webpack webpack-cli --save-dev
RUN npm install -g sqlite3 --build-from-source --save-dev
RUN npm install -g --save-dev sequelize
RUN npm rebuild
COPY . .

21
LICENSE

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 stakwork
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

42
README.md

@ -0,0 +1,42 @@
# Relay
**Relay** is a Node.js wrapper around [LND](https://github.com/lightningnetwork/lnd), handling connectivity and storage for [**Sphinx**](https://github.com/stakwork/sphinx). Communication between Relay nodes takes place entirely on the Lightning Network, so is decentralized, untraceable, and encrypted. Message content is also end-to-end encrypted using client public keys, on the **Sphinx** app itself.
![Relay](https://github.com/stakwork/sphinx-node/raw/master/public/relay.jpg)
Relay stores:
- Aliases
- Messages
- Recurring payment configurations
- Invites (so you can add your friends)
- Media Keys: keys for decrypting media files, asymetrically encrypted for each contact in a chat
# run your own sphinx node
You can run your own Sphinx node in order to have full ownership over your communication!
### download
`git clone https://github.com/stakwork/sphinx-node`
`cd sphinx-node`
`npm install`
### dependencies
sqlite3: `apt-get install sqlite3`
### configure
Edit the "production" section of config/app.json:
- Change `macaroon_location` to the location of your LND admin macaroon
- Change `tls_location` to the location of your LND cert
### run
`npm run prod`
# Roadmap
- linking recurring payments to files, to enable use cases such as subscribing to podcasts with BTC!

318
api/controllers/chats.ts

@ -0,0 +1,318 @@
import { models } from '../models'
import * as jsonUtils from '../utils/json'
import { success, failure } from '../utils/res'
import * as helpers from '../helpers'
import * as socket from '../utils/socket'
import { sendNotification } from '../hub'
import * as md5 from 'md5'
const constants = require(__dirname + '/../../config/constants.json')
async function getChats(req, res) {
const chats = await models.Chat.findAll({ where:{deleted:false}, raw: true })
const c = chats.map(chat => jsonUtils.chatToJson(chat));
success(res, c)
}
async function mute(req, res) {
const chatId = req.params['chat_id']
const mute = req.params['mute_unmute']
if (!["mute", "unmute"].includes(mute)) {
return failure(res, "invalid option for mute")
}
const chat = await models.Chat.findOne({ where: { id: chatId } })
if (!chat) {
return failure(res, 'chat not found')
}
chat.update({ isMuted: (mute == "mute") })
success(res, jsonUtils.chatToJson(chat))
}
async function createGroupChat(req, res) {
const {
name,
contact_ids,
} = req.body
const members: { [k: string]: {[k:string]:string} } = {} //{pubkey:{key,alias}, ...}
const owner = await models.Contact.findOne({ where: { isOwner: true } })
members[owner.publicKey] = {
key:owner.contactKey, alias:owner.alias
}
await asyncForEach(contact_ids, async cid => {
const contact = await models.Contact.findOne({ where: { id: cid } })
members[contact.publicKey] = {
key: contact.contactKey,
alias: contact.alias||''
}
})
const chatParams = createGroupChatParams(owner, contact_ids, members, name)
helpers.sendMessage({
chat: { ...chatParams, members },
sender: owner,
type: constants.message_types.group_create,
message: {},
failure: function (e) {
failure(res, e)
},
success: async function () {
const chat = await models.Chat.create(chatParams)
success(res, jsonUtils.chatToJson(chat))
}
})
}
async function addGroupMembers(req, res) {
const {
contact_ids,
} = req.body
const { id } = req.params
const members: { [k: string]: {[k:string]:string} } = {} //{pubkey:{key,alias}, ...}
const owner = await models.Contact.findOne({ where: { isOwner: true } })
let chat = await models.Chat.findOne({ where: { id } })
const contactIds = JSON.parse(chat.contactIds || '[]')
// for all members (existing and new)
members[owner.publicKey] = {key:owner.contactKey, alias:owner.alias}
const allContactIds = contactIds.concat(contact_ids)
await asyncForEach(allContactIds, async cid => {
const contact = await models.Contact.findOne({ where: { id: cid } })
if(contact) {
members[contact.publicKey] = {
key: contact.contactKey,
alias: contact.alias
}
}
})
success(res, jsonUtils.chatToJson(chat))
helpers.sendMessage({ // send ONLY to new members
chat: { ...chat.dataValues, contactIds:contact_ids, members },
sender: owner,
type: constants.message_types.group_invite,
message: {}
})
}
const deleteChat = async (req, res) => {
const { id } = req.params
const owner = await models.Contact.findOne({ where: { isOwner: true } })
const chat = await models.Chat.findOne({ where: { id } })
helpers.sendMessage({
chat,
sender: owner,
message: {},
type: constants.message_types.group_leave,
})
await chat.update({
deleted: true,
uuid:'',
contactIds:'[]',
name:''
})
await models.Message.destroy({ where: { chatId: id } })
success(res, { chat_id: id })
}
async function receiveGroupLeave(payload) {
console.log('=> receiveGroupLeave')
const { sender_pub_key, chat_uuid } = await helpers.parseReceiveParams(payload)
const chat = await models.Chat.findOne({ where: { uuid: chat_uuid } })
if (!chat) return
const sender = await models.Contact.findOne({ where: { publicKey: sender_pub_key } })
if (!sender) return
const oldContactIds = JSON.parse(chat.contactIds || '[]')
const contactIds = oldContactIds.filter(cid => cid !== sender.id)
await chat.update({ contactIds: JSON.stringify(contactIds) })
var date = new Date();
date.setMilliseconds(0)
const msg = {
chatId: chat.id,
type: constants.message_types.group_leave,
sender: sender.id,
date: date,
messageContent: '',
remoteMessageContent: '',
status: constants.statuses.confirmed,
createdAt: date,
updatedAt: date
}
const message = await models.Message.create(msg)
socket.sendJson({
type: 'group_leave',
response: {
contact: jsonUtils.contactToJson(sender),
chat: jsonUtils.chatToJson(chat),
message: jsonUtils.messageToJson(message, null)
}
})
}
async function receiveGroupJoin(payload) {
console.log('=> receiveGroupJoin')
const { sender_pub_key, chat_uuid, chat_members } = await helpers.parseReceiveParams(payload)
const chat = await models.Chat.findOne({ where: { uuid: chat_uuid } })
if (!chat) return
let theSender: any = null
const sender = await models.Contact.findOne({ where: { publicKey: sender_pub_key } })
const contactIds = JSON.parse(chat.contactIds || '[]')
if (sender) {
theSender = sender // might already include??
if(!contactIds.includes(sender.id)) contactIds.push(sender.id)
} else {
const member = chat_members[sender_pub_key]
if(member && member.key) {
const createdContact = await models.Contact.create({
publicKey: sender_pub_key,
contactKey: member.key,
alias: member.alias||'Unknown',
status: 1
})
theSender = createdContact
contactIds.push(createdContact.id)
}
}
await chat.update({ contactIds: JSON.stringify(contactIds) })
var date = new Date();
date.setMilliseconds(0)
const msg = {
chatId: chat.id,
type: constants.message_types.group_join,
sender: sender.id,
date: date,
messageContent: '',
remoteMessageContent: '',
status: constants.statuses.confirmed,
createdAt: date,
updatedAt: date
}
const message = await models.Message.create(msg)
socket.sendJson({
type: 'group_join',
response: {
contact: jsonUtils.contactToJson(theSender),
chat: jsonUtils.chatToJson(chat),
message: jsonUtils.messageToJson(message, null)
}
})
}
async function receiveGroupCreateOrInvite(payload) {
const { chat_members, chat_name, chat_uuid } = await helpers.parseReceiveParams(payload)
const contactIds: number[] = []
const newContacts: any[] = []
for (let [pubkey, member] of Object.entries(chat_members)) {
const contact = await models.Contact.findOne({ where: { publicKey: pubkey } })
if (!contact && member && member.key) {
const createdContact = await models.Contact.create({
publicKey: pubkey,
contactKey: member.key,
alias: member.alias||'Unknown',
status: 1
})
contactIds.push(createdContact.id)
newContacts.push(createdContact.dataValues)
} else {
contactIds.push(contact.id)
}
}
const owner = await models.Contact.findOne({ where: { isOwner: true } })
if(!contactIds.includes(owner.id)) contactIds.push(owner.id)
// make chat
let date = new Date();
date.setMilliseconds(0)
const chat = await models.Chat.create({
uuid: chat_uuid,
contactIds: JSON.stringify(contactIds),
createdAt: date,
updatedAt: date,
name: chat_name,
type: constants.chat_types.group
})
socket.sendJson({
type: 'group_create',
response: jsonUtils.messageToJson({ newContacts }, chat)
})
sendNotification(chat, chat_name, 'group')
if (payload.type === constants.message_types.group_invite) {
const owner = await models.Contact.findOne({ where: { isOwner: true } })
helpers.sendMessage({
chat: {
...chat.dataValues, members: {
[owner.publicKey]: {
key: owner.contactKey,
alias: owner.alias||''
}
}
},
sender: owner,
message: {},
type: constants.message_types.group_join,
})
}
}
function createGroupChatParams(owner, contactIds, members, name) {
let date = new Date();
date.setMilliseconds(0)
if (!(owner && members && contactIds && Array.isArray(contactIds))) {
return
}
const pubkeys: string[] = []
for (let pubkey of Object.keys(members)) { // just the key
pubkeys.push(String(pubkey))
}
if (!(pubkeys && pubkeys.length)) return
const allkeys = pubkeys.includes(owner.publicKey) ? pubkeys : [owner.publicKey].concat(pubkeys)
const hash = md5(allkeys.sort().join("-"))
const theContactIds = contactIds.includes(owner.id) ? contactIds : [owner.id].concat(contactIds)
return {
uuid: `${new Date().valueOf()}-${hash}`,
contactIds: JSON.stringify(theContactIds),
createdAt: date,
updatedAt: date,
name: name,
type: constants.chat_types.group
}
}
export {
getChats, mute, addGroupMembers,
receiveGroupCreateOrInvite, createGroupChat,
deleteChat, receiveGroupLeave, receiveGroupJoin
}
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}

240
api/controllers/contacts.ts

@ -0,0 +1,240 @@
import {models} from '../models'
import * as crypto from 'crypto'
import * as socket from '../utils/socket'
import * as helpers from '../helpers'
import * as jsonUtils from '../utils/json'
import {success, failure} from '../utils/res'
const constants = require(__dirname + '/../../config/constants.json')
const getContacts = async (req, res) => {
const contacts = await models.Contact.findAll({ where:{deleted:false}, raw: true })
const invites = await models.Invite.findAll({ raw: true })
const chats = await models.Chat.findAll({ where:{deleted:false}, raw: true })
const subscriptions = await models.Subscription.findAll({ raw: true })
const contactsResponse = contacts.map(contact => {
let contactJson = jsonUtils.contactToJson(contact)
let invite = invites.find(invite => invite.contactId == contact.id)
if (invite) {
contactJson.invite = jsonUtils.inviteToJson(invite)
}
return contactJson
});
const subsResponse = subscriptions.map(s=> jsonUtils.subscriptionToJson(s,null))
const chatsResponse = chats.map(chat => jsonUtils.chatToJson(chat))
success(res, {
contacts: contactsResponse,
chats: chatsResponse,
subscriptions: subsResponse
})
}
const generateToken = async (req, res) => {
console.log('=> generateToken called', { body: req.body, params: req.params, query: req.query })
const owner = await models.Contact.findOne({ where: { isOwner: true, authToken: null }})
if (owner) {
const hash = crypto.createHash('sha256').update(req.body['token']).digest('base64');
console.log("req.params['token']", req.params['token']);
console.log("hash", hash);
owner.update({ authToken: hash })
success(res,{})
} else {
failure(res,{})
}
}
const updateContact = async (req, res) => {
console.log('=> updateContact called', { body: req.body, params: req.params, query: req.query })
let attrs = extractAttrs(req.body)
const contact = await models.Contact.findOne({ where: { id: req.params.id }})
let shouldUpdateContactKey = (contact.isOwner && contact.contactKey == null && attrs["contact_key"] != null)
const owner = await contact.update(jsonUtils.jsonToContact(attrs))
success(res, jsonUtils.contactToJson(owner))
if (!shouldUpdateContactKey) {
return
}
// definitely "owner" now
const contactIds = await models.Contact.findAll({where:{deleted:false}}).map(c => c.id)
if (contactIds.length == 0) {
return
}
helpers.sendContactKeys({
contactIds: contactIds,
sender: owner,
type: constants.message_types.contact_key,
})
}
const exchangeKeys = async (req, res) => {
console.log('=> exchangeKeys called', { body: req.body, params: req.params, query: req.query })
const contact = await models.Contact.findOne({ where: { id: req.params.id }})
const owner = await models.Contact.findOne({ where: { isOwner: true }})
success(res, jsonUtils.contactToJson(contact))
helpers.sendContactKeys({
contactIds: [contact.id],
sender: owner,
type: constants.message_types.contact_key,
})
}
const createContact = async (req, res) => {
console.log('=> createContact called', { body: req.body, params: req.params, query: req.query })
let attrs = extractAttrs(req.body)
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const createdContact = await models.Contact.create(attrs)
const contact = await createdContact.update(jsonUtils.jsonToContact(attrs))
success(res, jsonUtils.contactToJson(contact))
helpers.sendContactKeys({
contactIds: [contact.id],
sender: owner,
type: constants.message_types.contact_key,
})
}
const deleteContact = async (req, res) => {
const id = parseInt(req.params.id||'0')
if(!id || id===1) {
failure(res, 'Cannot delete self')
return
}
const contact = await models.Contact.findOne({ where: { id } })
await contact.update({
deleted:true,
publicKey:'',
photoUrl:'',
alias:'Unknown',
contactKey:'',
})
// find and destroy chat & messages
const chats = await models.Chat.findAll({where:{deleted:false}})
chats.map(async chat => {
if (chat.type === constants.chat_types.conversation) {
const contactIds = JSON.parse(chat.contactIds)
if (contactIds.includes(id)) {
await chat.update({
deleted: true,
uuid:'',
contactIds:'[]',
name:''
})
await models.Message.destroy({ where: { chatId: chat.id } })
}
}
})
await models.Invite.destroy({ where: { contactId: id } })
await models.Subscription.destroy({ where: { contactId: id } })
success(res, {})
}
const receiveConfirmContactKey = async (payload) => {
console.log('=> confirm contact key', { payload })
const dat = payload.content || payload
const sender_pub_key = dat.sender.pub_key
const sender_contact_key = dat.sender.contact_key
const sender_alias = dat.sender.alias || 'Unknown'
const sender_photo_url = dat.sender.photoUrl
if(sender_photo_url){
// download and store photo locally
}
const sender = await models.Contact.findOne({ where: { publicKey: sender_pub_key, status: constants.contact_statuses.confirmed }})
if (sender_contact_key && sender) {
if(!sender.alias || sender.alias==='Unknown') {
sender.update({ contactKey: sender_contact_key, alias: sender_alias })
} else {
sender.update({ contactKey: sender_contact_key })
}
socket.sendJson({
type: 'contact',
response: jsonUtils.contactToJson(sender)
})
}
}
const receiveContactKey = async (payload) => {
console.log('=> received contact key', JSON.stringify(payload))
const dat = payload.content || payload
const sender_pub_key = dat.sender.pub_key
const sender_contact_key = dat.sender.contact_key
const sender_alias = dat.sender.alias || 'Unknown'
const sender_photo_url = dat.sender.photoUrl
if(sender_photo_url){
// download and store photo locally
}
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const sender = await models.Contact.findOne({ where: { publicKey: sender_pub_key, status: constants.contact_statuses.confirmed }})
if (sender_contact_key && sender) {
if(!sender.alias || sender.alias==='Unknown') {
sender.update({ contactKey: sender_contact_key, alias: sender_alias })
} else {
sender.update({ contactKey: sender_contact_key })
}
socket.sendJson({
type: 'contact',
response: jsonUtils.contactToJson(sender)
})
}
helpers.sendContactKeys({
contactPubKey: sender_pub_key,
sender: owner,
type: constants.message_types.contact_key_confirmation,
})
}
const extractAttrs = body => {
let fields_to_update = ["public_key", "node_alias", "alias", "photo_url", "device_id", "status", "contact_key"]
let attrs = {}
Object.keys(body).forEach(key => {
if (fields_to_update.includes(key)) {
attrs[key] = body[key]
}
})
return attrs
}
export {
generateToken,
exchangeKeys,
getContacts,
updateContact,
createContact,
deleteContact,
receiveContactKey,
receiveConfirmContactKey
}

123
api/controllers/details.ts

@ -0,0 +1,123 @@
import {loadLightning} from '../utils/lightning'
import { success, failure } from '../utils/res'
import * as readLastLines from 'read-last-lines'
import { nodeinfo } from '../utils/nodeinfo';
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/app.json')[env];
const defaultLogFiles = [
'/home/lnd/.pm2/logs/app-error.log',
'/var/log/syslog',
]
async function getLogsSince(req, res) {
const logFiles = config.log_file ? [config.log_file] : defaultLogFiles
let txt
let err
await asyncForEach(logFiles, async filepath=>{
if(!txt){
try {
const lines = await readLastLines.read(filepath, 500)
if(lines) {
var linesArray = lines.split('\n')
linesArray.reverse()
txt = linesArray.join('\n')
}
} catch(e) {
err = e
}
}
})
if(txt) success(res, txt)
else failure(res, err)
}
const getInfo = async (req, res) => {
const lightning = loadLightning()
var request = {}
lightning.getInfo(request, function(err, response) {
res.status(200);
if (err == null) {
res.json({ success: true, response });
} else {
res.json({ success: false });
}
res.end();
});
};
const getChannels = async (req, res) => {
const lightning = loadLightning()
var request = {}
lightning.listChannels(request, function(err, response) {
res.status(200);
if (err == null) {
res.json({ success: true, response });
} else {
res.json({ success: false });
}
res.end();
});
};
const getBalance = (req, res) => {
const lightning = loadLightning()
var request = {}
lightning.channelBalance(request, function(err, response) {
res.status(200);
if (err == null) {
res.json({ success: true, response });
} else {
res.json({ success: false });
}
res.end();
});
};
const getLocalRemoteBalance = async (req, res) => {
const lightning = loadLightning()
lightning.listChannels({}, (err, channelList) => {
const { channels } = channelList
const localBalances = channels.map(c => c.local_balance)
const remoteBalances = channels.map(c => c.remote_balance)
const totalLocalBalance = localBalances.reduce((a, b) => parseInt(a) + parseInt(b), 0)
const totalRemoteBalance = remoteBalances.reduce((a, b) => parseInt(a) + parseInt(b), 0)
res.status(200);
if (err == null) {
res.json({ success: true, response: { local_balance: totalLocalBalance, remote_balance: totalRemoteBalance } });
} else {
res.json({ success: false });
}
res.end();
})
};
const getNodeInfo = async (req, res) => {
var ipOfSource = req.connection.remoteAddress;
if(!(ipOfSource.includes('127.0.0.1') || ipOfSource.includes('localhost'))){
res.status(401)
res.end()
return
}
const node = await nodeinfo()
res.status(200)
res.json(node)
res.end()
}
export {
getInfo,
getBalance,
getChannels,
getLocalRemoteBalance,
getLogsSince,
getNodeInfo,
}
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}

139
api/controllers/index.ts

@ -0,0 +1,139 @@
import {models} from '../models'
import * as lndService from '../grpc'
import {checkTag} from '../utils/gitinfo'
import {checkConnection} from '../utils/lightning'
const constants = require(__dirname + '/../../config/constants.json');
const env = process.env.NODE_ENV || 'development';
console.log("=> env:",env)
let controllers = {
messages: require('./messages'),
invoices: require('./invoices'),
uploads: require('./uploads'),
contacts: require('./contacts'),
invites: require('./invites'),
payments: require('./payment'),
details: require('./details'),
chats: require('./chats'),
subcriptions: require('./subscriptions'),
media: require('./media'),
}
async function iniGrpcSubscriptions() {
try{
await checkConnection()
const types = constants.message_types
await lndService.subscribeInvoices({
[types.contact_key]: controllers.contacts.receiveContactKey,
[types.contact_key_confirmation]: controllers.contacts.receiveConfirmContactKey,
[types.message]: controllers.messages.receiveMessage,
[types.invoice]: controllers.invoices.receiveInvoice,
[types.direct_payment]: controllers.payments.receivePayment,
[types.confirmation]: controllers.messages.receiveConfirmation,
[types.attachment]: controllers.media.receiveAttachment,
[types.purchase]: controllers.media.receivePurchase,
[types.purchase_accept]: controllers.media.receivePurchaseAccept,
[types.purchase_deny]: controllers.media.receivePurchaseDeny,
[types.group_create]: controllers.chats.receiveGroupCreateOrInvite,
[types.group_invite]: controllers.chats.receiveGroupCreateOrInvite,
[types.group_join]: controllers.chats.receiveGroupJoin,
[types.group_leave]: controllers.chats.receiveGroupLeave,
})
} catch(e) {
throw e
}
}
async function set(app) {
if(models && models.Subscription){
controllers.subcriptions.initializeCronJobs()
}
try{
await controllers.media.cycleMediaToken()
} catch(e) {
console.log('=> could not auth with media server', e.message)
}
app.get('/chats', controllers.chats.getChats)
app.post('/group', controllers.chats.createGroupChat)
app.post('/chats/:chat_id/:mute_unmute', controllers.chats.mute)
app.delete('/chat/:id', controllers.chats.deleteChat)
app.put('/chat/:id', controllers.chats.addGroupMembers)
app.post('/contacts/tokens', controllers.contacts.generateToken)
app.post('/upload', controllers.uploads.avatarUpload.single('file'), controllers.uploads.uploadFile)
app.post('/invites', controllers.invites.createInvite)
app.post('/invites/:invite_string/pay', controllers.invites.payInvite)
app.post('/invites/finish', controllers.invites.finishInvite)
app.get('/contacts', controllers.contacts.getContacts)
app.put('/contacts/:id', controllers.contacts.updateContact)
app.post('/contacts/:id/keys', controllers.contacts.exchangeKeys)
app.post('/contacts', controllers.contacts.createContact)
app.delete('/contacts/:id', controllers.contacts.deleteContact)
app.get('/messages', controllers.messages.getMessages)
app.post('/messages', controllers.messages.sendMessage)
app.post('/messages/:chat_id/read', controllers.messages.readMessages)
app.post('/messages/clear', controllers.messages.clearMessages)
app.get('/subscriptions', controllers.subcriptions.getAllSubscriptions)
app.get('/subscription/:id', controllers.subcriptions.getSubscription)
app.delete('/subscription/:id', controllers.subcriptions.deleteSubscription)
app.post('/subscriptions', controllers.subcriptions.createSubscription)
app.put('/subscription/:id', controllers.subcriptions.editSubscription)
app.get('/subscriptions/contact/:contactId', controllers.subcriptions.getSubscriptionsForContact)
app.put('/subscription/:id/pause', controllers.subcriptions.pauseSubscription)
app.put('/subscription/:id/restart', controllers.subcriptions.restartSubscription)
app.post('/attachment', controllers.media.sendAttachmentMessage)
app.post('/purchase', controllers.media.purchase)
app.get('/signer/:challenge', controllers.media.signer)
app.post('/invoices', controllers.invoices.createInvoice)
app.get('/invoices', controllers.invoices.listInvoices)
app.put('/invoices', controllers.invoices.payInvoice)
app.post('/invoices/cancel', controllers.invoices.cancelInvoice)
app.post('/payment', controllers.payments.sendPayment)
app.get('/payments', controllers.payments.listPayments)
app.get('/channels', controllers.details.getChannels)
app.get('/balance', controllers.details.getBalance)
app.get('/balance/all', controllers.details.getLocalRemoteBalance)
app.get('/getinfo', controllers.details.getInfo)
app.get('/logs', controllers.details.getLogsSince)
app.get('/info', controllers.details.getNodeInfo)
app.get('/version', async function(req,res) {
const version = await checkTag()
res.send({version})
})
if (env != "production") { // web dashboard login
app.post('/login', login)
}
}
const login = (req, res) => {
const { code } = req.body;
if (code == "sphinx") {
models.Contact.findOne({ where: { isOwner: true } }).then(owner => {
res.status(200);
res.json({ success: true, token: owner.authToken });
res.end();
})
} else {
res.status(200);
res.json({ success: false });
res.end();
}
}
export {set, iniGrpcSubscriptions}

118
api/controllers/invites.ts

@ -0,0 +1,118 @@
import {models} from '../models'
import * as crypto from 'crypto'
import * as jsonUtils from '../utils/json'
import {finishInviteInHub, createInviteInHub, payInviteInHub} from '../hub'
const finishInvite = async (req, res) => {
const {
invite_string
} = req.body
const params = {
invite: {
pin: invite_string
}
}
function onSuccess() {
res.status(200)
res.json({ success: true })
res.end()
}
function onFailure() {
res.status(200)
res.json({ success: false })
res.end()
}
finishInviteInHub(params, onSuccess, onFailure)
}
const payInvite = async (req, res) => {
const params = {
node_ip: process.env.NODE_IP
}
const invite_string = req.params['invite_string']
const onSuccess = async (response) => {
const invite = response.object
console.log("response", invite)
const dbInvite = await models.Invite.findOne({ where: { inviteString: invite.pin }})
if (dbInvite.status != invite.invite_status) {
dbInvite.update({ status: invite.invite_status })
}
res.status(200)
res.json({ success: true, response: { invite: jsonUtils.inviteToJson(dbInvite) } })
res.end()
}
const onFailure = (response) => {
res.status(200)
res.json({ success: false })
res.end()
}
payInviteInHub(invite_string, params, onSuccess, onFailure)
}
const createInvite = async (req, res) => {
const {
nickname,
welcome_message
} = req.body
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const params = {
invite: {
nickname: owner.alias,
pubkey: owner.publicKey,
contact_nickname: nickname,
message: welcome_message,
pin: crypto.randomBytes(20).toString('hex')
}
}
const onSuccess = async (response) => {
console.log("response", response)
const inviteCreated = response.object
const contact = await models.Contact.create({
alias: nickname,
status: 0
})
const invite = await models.Invite.create({
welcomeMessage: inviteCreated.message,
contactId: contact.id,
status: inviteCreated.invite_status,
inviteString: inviteCreated.pin
})
let contactJson = jsonUtils.contactToJson(contact)
if (invite) {
contactJson.invite = jsonUtils.inviteToJson(invite)
}
res.status(200)
res.json({ success: true, contact: contactJson })
res.end()
}
const onFailure = (response) => {
res.status(200)
res.json(response)
res.end()
}
createInviteInHub(params, onSuccess, onFailure)
}
export {
createInvite,
finishInvite,
payInvite
}

273
api/controllers/invoices.ts

@ -0,0 +1,273 @@
import { models } from '../models'
import { loadLightning } from '../utils/lightning'
import * as socket from '../utils/socket'
import * as jsonUtils from '../utils/json'
import * as decodeUtils from '../utils/decode'
import * as helpers from '../helpers'
import { sendNotification } from '../hub'
import { success } from '../utils/res'
const constants = require(__dirname + '/../../config/constants.json');
const payInvoice = async (req, res) => {
const lightning = await loadLightning()
const { payment_request } = req.body;
var call = lightning.sendPayment({})
call.on('data', async response => {
console.log('[pay invoice data]', response)
const message = await models.Message.findOne({ where: { payment_request } })
if (!message) { // invoice still paid
return success(res, {
success: true,
response: { payment_request }
})
}
message.status = constants.statuses.confirmed;
message.save();
var date = new Date();
date.setMilliseconds(0)
const chat = await models.Chat.findOne({ where: { id: message.chatId } })
const contactIds = JSON.parse(chat.contactIds)
const senderId = contactIds.find(id => id != message.sender)
const paidMessage = await models.Message.create({
chatId: message.chatId,
sender: senderId,
type: constants.message_types.payment,
amount: message.amount,
amountMsat: message.amountMsat,
paymentHash: message.paymentHash,
date: date,
expirationDate: null,
messageContent: null,
status: constants.statuses.confirmed,
createdAt: date,
updatedAt: date
})
console.log('[pay invoice] stored message', paidMessage)
success(res, jsonUtils.messageToJson(paidMessage, chat))
})
call.write({ payment_request })
};
const cancelInvoice = (req, res) => {
res.status(200);
res.json({ success: false });
res.end();
};
const createInvoice = async (req, res) => {
const lightning = await loadLightning()
const {
amount,
memo,
remote_memo,
chat_id,
contact_id
} = req.body;
var request = {
value: amount,
memo: remote_memo || memo
}
if (amount == null) {
res.status(200);
res.json({ err: "no amount specified", });
res.end();
} else {
lightning.addInvoice(request, function (err, response) {
console.log({ err, response })
if (err == null) {
const { payment_request } = response
if (!contact_id && !chat_id) { // if no contact
success(res, {
invoice: payment_request
})
return // end here
}
lightning.decodePayReq({ pay_req: payment_request }, async (error, invoice) => {
if (res) {
console.log('decoded pay req', { invoice })
const owner = await models.Contact.findOne({ where: { isOwner: true } })
const chat = await helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id
})
let timestamp = parseInt(invoice.timestamp + '000')
let expiry = parseInt(invoice.expiry + '000')
if (error) {
res.status(200)
res.json({ success: false, error })
res.end()
} else {
const message = await models.Message.create({
chatId: chat.id,
sender: owner.id,
type: constants.message_types.invoice,
amount: parseInt(invoice.num_satoshis),
amountMsat: parseInt(invoice.num_satoshis) * 1000,
paymentHash: invoice.payment_hash,
paymentRequest: payment_request,
date: new Date(timestamp),
expirationDate: new Date(timestamp + expiry),
messageContent: memo,
remoteMessageContent: remote_memo,
status: constants.statuses.pending,
createdAt: new Date(timestamp),
updatedAt: new Date(timestamp)
})
success(res, jsonUtils.messageToJson(message, chat))
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.invoice,
message: {
id: message.id,
invoice: message.paymentRequest
}
})
}
} else {
console.log('error decoding pay req', { err, res })
res.status(500);
res.json({ err, res })
res.end();
}
})
} else {
console.log({ err, response })
}
});
}
};
const listInvoices = async (req, res) => {
const lightning = await loadLightning()
lightning.listInvoices({}, (err, response) => {
console.log({ err, response })
if (err == null) {
res.status(200);
res.json(response);
res.end();
} else {
console.log({ err, response })
}
});
};
const receiveInvoice = async (payload) => {
console.log('received invoice', payload)
const total_spent = 1
const dat = payload.content || payload
const payment_request = dat.message.invoice
var date = new Date();
date.setMilliseconds(0)
const { owner, sender, chat, msg_id } = await helpers.parseReceiveParams(payload)
if (!owner || !sender || !chat) {
return console.log('=> no group chat!')
}
const { memo, sat, msat, paymentHash, invoiceDate, expirationSeconds } = decodePaymentRequest(payment_request)
const message = await models.Message.create({
chatId: chat.id,
type: constants.message_types.invoice,
sender: sender.id,
amount: sat,
amountMsat: msat,
paymentRequest: payment_request,
asciiEncodedTotal: total_spent,
paymentHash: paymentHash,
messageContent: memo,
expirationDate: new Date(invoiceDate + expirationSeconds),
date: new Date(invoiceDate),
status: constants.statuses.pending,
createdAt: date,
updatedAt: date
})
console.log('received keysend invoice message', message.id)
socket.sendJson({
type: 'invoice',
response: jsonUtils.messageToJson(message, chat)
})
sendNotification(chat, sender.alias, 'message')
sendConfirmation({ chat, sender: owner, msg_id })
}
const sendConfirmation = ({ chat, sender, msg_id }) => {
helpers.sendMessage({
chat,
sender,
message: { id: msg_id },
type: constants.message_types.confirmation,
})
}
export {
listInvoices,
payInvoice,
cancelInvoice,
createInvoice,
receiveInvoice
}
// lnd invoice stuff
function decodePaymentRequest(paymentRequest) {
var decodedPaymentRequest: any = decodeUtils.decode(paymentRequest)
var expirationSeconds = 3600
var paymentHash = ""
var memo = ""
for (var i = 0; i < decodedPaymentRequest.data.tags.length; i++) {
let tag = decodedPaymentRequest.data.tags[i];
if(tag) {
if (tag.description == 'payment_hash') {
paymentHash = tag.value;
} else if (tag.description == 'description') {
memo = tag.value;
} else if (tag.description == 'expiry') {
expirationSeconds = tag.value;
}
}
}
expirationSeconds = parseInt(expirationSeconds.toString() + '000');
let invoiceDate = parseInt(decodedPaymentRequest.data.time_stamp.toString() + '000');
let amount = decodedPaymentRequest['human_readable_part']['amount'];
var msat = 0;
var sat = 0;
if (Number.isInteger(amount)) {
msat = amount;
sat = amount / 1000;
}
return { sat, msat, paymentHash, invoiceDate, expirationSeconds, memo }
}

514
api/controllers/media.ts

@ -0,0 +1,514 @@
import {models} from '../models'
import * as socket from '../utils/socket'
import * as jsonUtils from '../utils/json'
import * as resUtils from '../utils/res'
import * as helpers from '../helpers'
import { sendNotification } from '../hub'
import { signBuffer, verifyMessage } from '../utils/lightning'
import * as rp from 'request-promise'
import { loadLightning } from '../utils/lightning'
import {parseLDAT, tokenFromTerms, urlBase64FromBytes, testLDAT} from '../utils/ldat'
import {CronJob} from 'cron'
import * as zbase32 from '../utils/zbase32'
import * as schemas from './schemas'
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/app.json')[env];
const constants = require(__dirname + '/../../config/constants.json');
/*
TODO line 233: parse that from token itself, dont use getMediaInfo at all
"attachment": sends a message to a chat with a signed receipt for a file, which can be accessed from sphinx-meme server
If the attachment has a price, then the media must be purchased to get the receipt
"purchase" sends sats.
if the amount matches the price, the media owner
will respond ("purchase_accept" or "purchase_deny" type)
with the signed token, which can only be used by the buyer
purchase_accept should update the original attachment message with the terms and receipt
(both Relay and client need to do this) or make new???
purchase_deny returns the sats
*/
const sendAttachmentMessage = async (req, res) => {
// try {
// schemas.attachment.validateSync(req.body)
// } catch(e) {
// return resUtils.failure(res, e.message)
// }
const {
chat_id,
contact_id,
muid,
text,
remote_text,
remote_text_map,
media_key_map,
media_type,
file_name,
ttl,
price, // IF AMOUNT>0 THEN do NOT sign or send receipt
} = req.body
console.log('[send attachment]', req.body)
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const chat = await helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id
})
let TTL = ttl
if(ttl) {
TTL = parseInt(ttl)
}
if(!TTL) TTL = 31536000 // default year
const amt = price||0
// generate media token for self!
const myMediaToken = await tokenFromTerms({
muid, ttl:TTL, host:'',
pubkey: owner.publicKey,
meta:{...amt && {amt}, ttl}
})
const date = new Date();
date.setMilliseconds(0)
const myMediaKey = (media_key_map && media_key_map[owner.id]) || ''
const mediaType = media_type || ''
const remoteMessageContent = remote_text_map?JSON.stringify(remote_text_map) : remote_text
const message = await models.Message.create({
chatId: chat.id,
sender: owner.id,
type: constants.message_types.attachment,
status: constants.statuses.pending,
messageContent: text||file_name||'',
remoteMessageContent,
mediaToken: myMediaToken,
mediaKey: myMediaKey,
mediaType: mediaType,
date,
createdAt: date,
updatedAt: date
})
saveMediaKeys(muid, media_key_map, chat.id, message.id)
const mediaTerms: {[k:string]:any} = {
muid, ttl:TTL,
meta:{...amt && {amt}},
skipSigning: amt ? true : false // only sign if its free
}
const msg: {[k:string]:any} = {
mediaTerms, // this gets converted to mediaToken
id: message.id,
content: remote_text_map||remote_text||text||file_name||'',
mediaKey: media_key_map,
mediaType: mediaType,
}
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.attachment,
message: msg,
success: async (data) => {
console.log('attachment sent', { data })
resUtils.success(res, jsonUtils.messageToJson(message, chat))
},
failure: error=> resUtils.failure(res, error.message),
})
}
function saveMediaKeys(muid, mediaKeyMap, chatId, messageId){
if (typeof mediaKeyMap!=='object'){
console.log('wrong type for mediaKeyMap')
return
}
var date = new Date();
date.setMilliseconds(0)
for (let [contactId, key] of Object.entries(mediaKeyMap)) {
models.MediaKey.create({
muid, chatId, contactId, key, messageId,
createdAt: date,
})
}
}
const purchase = async (req, res) => {
const {
chat_id,
contact_id,
amount,
mediaToken,
} = req.body
var date = new Date();
date.setMilliseconds(0)
try {
schemas.purchase.validateSync(req.body)
} catch(e) {
return resUtils.failure(res, e.message)
}
console.log('purchase!')
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const chat = await helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id
})
const message = await models.Message.create({
sender: owner.id,
type: constants.message_types.purchase,
mediaToken: mediaToken,
date: date,
createdAt: date,
updatedAt: date
})
const msg={
amount, mediaToken, id:message.id,
}
helpers.sendMessage({
chat: {...chat, contactIds:[contact_id]},
sender: owner,
type: constants.message_types.purchase,
message: msg,
success: async (data) => {
console.log('purchase sent', { data })
resUtils.success(res, jsonUtils.messageToJson(message))
},
failure: error=> resUtils.failure(res, error.message),
})
}
/* RECEIVERS */
const receivePurchase = async (payload) => {
console.log('received purchase', { payload })
var date = new Date();
date.setMilliseconds(0)
const {owner, sender, chat, amount, mediaToken} = await helpers.parseReceiveParams(payload)
if(!owner || !sender || !chat) {
return console.log('=> group chat not found!')
}
await models.Message.create({
chatId: chat.id,
sender: sender.id,
type: constants.message_types.purchase,
mediaToken: mediaToken,
date: date,
createdAt: date,
updatedAt: date
})
const muid = mediaToken && mediaToken.split('.').length && mediaToken.split('.')[1]
if(!muid){
return console.log('no muid')
}
const ogMessage = models.Message.findOne({
where:{mediaToken}
})
if (!ogMessage){
return console.log('no original message')
}
// find mediaKey for who sent
const mediaKey = models.MediaKey.findOne({where:{
muid, receiver: sender.id,
}})
const terms = parseLDAT(mediaToken)
// get info
let TTL = terms.meta && terms.meta.ttl
let price = terms.meta && terms.meta.amt
if(!TTL || !price){
const media = await getMediaInfo(muid)
console.log("GOT MEDIA", media)
if(media) {
TTL = media.ttl && parseInt(media.ttl)
price = media.price
}
if(!TTL) TTL = 31536000
if(!price) price = 0
}
if (amount < price) { // didnt pay enough
return helpers.sendMessage({ // "purchase_deny"
chat: {...chat, contactIds:[sender.id]}, // only send back to sender
sender: owner,
amount: amount,
type: constants.message_types.purchase_deny,
message: {amount,content:'Payment Denied'},
success: async (data) => {
console.log('purchase_deny sent', { data })
},
failure: error=> console.log('=> couldnt send purcahse deny', error),
})
}
const acceptTerms = {
muid, ttl: TTL,
meta: {amt:amount},
}
helpers.sendMessage({
chat: {...chat, contactIds:[sender.id]}, // only to sender
sender: owner,
type: constants.message_types.purchase_accept,
message: {
mediaTerms: acceptTerms, // converted to token in utils/msg.ts
mediaKey: mediaKey.key,
mediaType: ogMessage.mediaType,
},
success: async (data) => {
console.log('purchase_accept sent', { data })
},
failure: error=> console.log('=> couldnt send purchase accept', error),
})
}
const receivePurchaseAccept = async (payload) => {
var date = new Date();
date.setMilliseconds(0)
const {owner, sender, chat, mediaToken, mediaKey, mediaType} = await helpers.parseReceiveParams(payload)
if(!owner || !sender || !chat) {
return console.log('=> no group chat!')
}
const termsArray = mediaToken.split('.')
// const host = termsArray[0]
const muid = termsArray[1]
if(!muid){
return console.log('wtf no muid')
}
// const attachmentMessage = await models.Message.findOne({where:{
// mediaToken: {$like: `${host}.${muid}%`}
// }})
// if(attachmentMessage){
// console.log('=> updated msg!')
// attachmentMessage.update({
// mediaToken, mediaKey
// })
// }
const msg = await models.Message.create({
chatId: chat.id,
sender: sender.id,
type: constants.message_types.purchase_accept,
status: constants.statuses.received,
mediaToken,
mediaKey,
mediaType,
date: date,
createdAt: date,
updatedAt: date
})
socket.sendJson({
type: 'purchase_accept',
response: jsonUtils.messageToJson(msg, chat)
})
}
const receivePurchaseDeny = async (payload) => {
var date = new Date();
date.setMilliseconds(0)
const {owner, sender, chat, amount, mediaToken} = await helpers.parseReceiveParams(payload)
if(!owner || !sender || !chat) {
return console.log('=> no group chat!')
}
const msg = await models.Message.create({
chatId: chat.id,
sender: sender.id,
type: constants.message_types.purchase_deny,
status: constants.statuses.received,
messageContent:'Purchase has been denied and sats returned to you',
amount: amount,
amountMsat: parseFloat(amount) * 1000,
mediaToken,
date: date,
createdAt: date,
updatedAt: date
})
socket.sendJson({
type: 'purchase_deny',
response: jsonUtils.messageToJson(msg, chat)
})
}
const receiveAttachment = async (payload) => {
console.log('received attachment', { payload })
var date = new Date();
date.setMilliseconds(0)
const {owner, sender, chat, mediaToken, mediaKey, mediaType, content, msg_id} = await helpers.parseReceiveParams(payload)
if(!owner || !sender || !chat) {
return console.log('=> no group chat!')
}
const msg: {[k:string]:any} = {
chatId: chat.id,
type: constants.message_types.attachment,
sender: sender.id,
date: date,
createdAt: date,
updatedAt: date
}
if(content) msg.messageContent = content
if(mediaToken) msg.mediaToken = mediaToken
if(mediaKey) msg.mediaKey = mediaKey
if(mediaType) msg.mediaType = mediaType
const message = await models.Message.create(msg)
console.log('saved attachment', message.dataValues)
socket.sendJson({
type: 'attachment',
response: jsonUtils.messageToJson(message, chat)
})
sendNotification(chat, sender.alias, 'message')
sendConfirmation({ chat, sender: owner, msg_id })
}
const sendConfirmation = ({ chat, sender, msg_id }) => {
helpers.sendMessage({
chat,
sender,
message: {id:msg_id},
type: constants.message_types.confirmation,
})
}
async function signer(req, res) {
if(!req.params.challenge) return resUtils.failure(res, "no challenge")
try {
const sig = await signBuffer(
Buffer.from(req.params.challenge, 'base64')
)
const sigBytes = zbase32.decode(sig)
const sigBase64 = urlBase64FromBytes(sigBytes)
resUtils.success(res, {
sig: sigBase64
})
} catch(e) {
resUtils.failure(res, e)
}
}
async function verifier(msg, sig) {
try {
const res = await verifyMessage(msg, sig)
return res
} catch(e) {
console.log(e)
}
}
async function getMyPubKey(){
return new Promise((resolve,reject)=>{
const lightning = loadLightning()
var request = {}
lightning.getInfo(request, function(err, response) {
if(err) reject(err)
if(!response.identity_pubkey) reject('no pub key')
else resolve(response.identity_pubkey)
});
})
}
async function cycleMediaToken() {
try{
if (process.env.TEST_LDAT) testLDAT()
const mt = await getMediaToken(null)
if(mt) console.log('=> [meme] authed!')
new CronJob('1 * * * *', function() { // every hour
getMediaToken(true)
})
} catch(e) {
console.log(e.message)
}
}
const mediaURL = 'http://' + config.media_host + '/'
let mediaToken;
async function getMediaToken(force) {
if(!force && mediaToken) return mediaToken
await helpers.sleep(3000)
try {
const res = await rp.get(mediaURL+'ask')
const r = JSON.parse(res)
if (!(r && r.challenge && r.id)) {
throw new Error('no challenge')
}
const sig = await signBuffer(
Buffer.from(r.challenge, 'base64')
)
if(!sig) throw new Error('no signature')
const pubkey = await getMyPubKey()
if(!pubkey){
throw new Error('no pub key!')
}
const sigBytes = zbase32.decode(sig)
const sigBase64 = urlBase64FromBytes(sigBytes)
const bod = await rp.post(mediaURL+'verify', {
form:{id: r.id, sig:sigBase64, pubkey}
})
const body = JSON.parse(bod)
if(!(body && body.token)){
throw new Error('no token')
}
mediaToken = body.token
return body.token
} catch(e) {
throw e
}
}
async function getMediaInfo(muid) {
try {
const token = await getMediaToken(null)
const res = await rp.get(mediaURL+'mymedia/'+muid,{
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
json:true
})
return res
} catch(e) {
return null
}
}
export {
sendAttachmentMessage,
receiveAttachment,
receivePurchase,
receivePurchaseAccept,
receivePurchaseDeny,
purchase,
signer,
verifier,
getMediaToken,
cycleMediaToken
}

277
api/controllers/messages.ts

@ -0,0 +1,277 @@
import {models} from '../models'
import { Op } from 'sequelize'
import { indexBy } from 'underscore'
import { sendNotification } from '../hub'
import * as socket from '../utils/socket'
import * as jsonUtils from '../utils/json'
import * as helpers from '../helpers'
import { success } from '../utils/res'
import lock from '../utils/lock'
const constants = require(__dirname + '/../../config/constants.json')
const getMessages = async (req, res) => {
const dateToReturn = req.query.date;
if (!dateToReturn) {
return getAllMessages(req, res)
}
console.log(dateToReturn)
const owner = await models.Contact.findOne({ where: { isOwner: true } })
// const chatId = req.query.chat_id
let newMessagesWhere = {
date: { [Op.gte]: dateToReturn },
[Op.or]: [
{receiver: owner.id},
{receiver: null}
]
}
let confirmedMessagesWhere = {
updated_at: { [Op.gte]: dateToReturn },
status: constants.statuses.received,
sender: owner.id
}
// if (chatId) {
// newMessagesWhere.chat_id = chatId
// confirmedMessagesWhere.chat_id = chatId
// }
const newMessages = await models.Message.findAll({ where: newMessagesWhere })
const confirmedMessages = await models.Message.findAll({ where: confirmedMessagesWhere })
const chatIds: number[] = []
newMessages.forEach(m => {
if(!chatIds.includes(m.chatId)) chatIds.push(m.chatId)
})
confirmedMessages.forEach(m => {
if(!chatIds.includes(m.chatId)) chatIds.push(m.chatId)
})
let chats = chatIds.length > 0 ? await models.Chat.findAll({ where: {deleted:false, id: chatIds} }) : []
const chatsById = indexBy(chats, 'id')
res.json({
success: true,
response: {
new_messages: newMessages.map(message =>
jsonUtils.messageToJson(message, chatsById[parseInt(message.chatId)])
),
confirmed_messages: confirmedMessages.map(message =>
jsonUtils.messageToJson(message, chatsById[parseInt(message.chatId)])
)
}
});
res.status(200)
res.end()
}
const getAllMessages = async (req, res) => {
const messages = await models.Message.findAll({ order: [['id', 'asc']] })
const chatIds = messages.map(m => m.chatId)
console.log('=> getAllMessages, chatIds',chatIds)
let chats = chatIds.length > 0 ? await models.Chat.findAll({ where: {deleted:false, id: chatIds} }) : []
const chatsById = indexBy(chats, 'id')
success(res, {
new_messages: messages.map(
message => jsonUtils.messageToJson(message, chatsById[parseInt(message.chatId)])
),
confirmed_messages: []
})
};
const sendMessage = async (req, res) => {
// try {
// schemas.message.validateSync(req.body)
// } catch(e) {
// return failure(res, e.message)
// }
const {
contact_id,
text,
remote_text,
chat_id,
remote_text_map,
} = req.body
console.log('[sendMessage]',)
var date = new Date();
date.setMilliseconds(0)
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const chat = await helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id,
})
const remoteMessageContent = remote_text_map?JSON.stringify(remote_text_map) : remote_text
const msg={
chatId: chat.id,
type: constants.message_types.message,
sender: owner.id,
date: date,
messageContent: text,
remoteMessageContent,
status: constants.statuses.pending,
createdAt: date,
updatedAt: date
}
// console.log(msg)
const message = await models.Message.create(msg)
success(res, jsonUtils.messageToJson(message, chat))
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.message,
message: {
id: message.id,
content: remote_text_map || remote_text || text
}
})
}
const receiveMessage = async (payload) => {
console.log('received message', { payload })
var date = new Date();
date.setMilliseconds(0)
const total_spent = 1
const {owner, sender, chat, content, msg_id} = await helpers.parseReceiveParams(payload)
if(!owner || !sender || !chat) {
return console.log('=> no group chat!')
}
const text = content
const message = await models.Message.create({
chatId: chat.id,
type: constants.message_types.message,
asciiEncodedTotal: total_spent,
sender: sender.id,
date: date,
messageContent: text,
createdAt: date,
updatedAt: date,
status: constants.statuses.received
})
console.log('saved message', message.dataValues)
socket.sendJson({
type: 'message',
response: jsonUtils.messageToJson(message, chat)
})
sendNotification(chat, sender.alias, 'message')
sendConfirmation({ chat, sender: owner, msg_id })
}
const sendConfirmation = ({ chat, sender, msg_id }) => {
helpers.sendMessage({
chat,
sender,
message: {id:msg_id},
type: constants.message_types.confirmation,
})
}
const receiveConfirmation = async (payload) => {
console.log('received confirmation', { payload })
const dat = payload.content || payload
const chat_uuid = dat.chat.uuid
const msg_id = dat.message.id
const sender_pub_key = dat.sender.pub_key
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const sender = await models.Contact.findOne({ where: { publicKey: sender_pub_key } })
const chat = await models.Chat.findOne({ where: { uuid: chat_uuid } })
// new confirmation logic
if(msg_id){
lock.acquire('confirmation', async function(done){
console.log("update status map")
const message = await models.Message.findOne({ where:{id:msg_id} })
if(message){
let statusMap = {}
try{
statusMap = JSON.parse(message.statusMap||'{}')
} catch(e){}
statusMap[sender.id] = constants.statuses.received
await message.update({
status: constants.statuses.received,
statusMap: JSON.stringify(statusMap)
})
socket.sendJson({
type: 'confirmation',
response: jsonUtils.messageToJson(message, chat)
})
}
done()
})
} else { // old logic
const messages = await models.Message.findAll({
limit: 1,
where: {
chatId: chat.id,
sender: owner.id,
type: [
constants.message_types.message,
constants.message_types.invoice,
constants.message_types.attachment,
],
status: constants.statuses.pending,
},
order: [['createdAt', 'desc']]
})
const message = messages[0]
message.update({ status: constants.statuses.received })
socket.sendJson({
type: 'confirmation',
response: jsonUtils.messageToJson(message, chat)
})
}
}
const readMessages = async (req, res) => {
const chat_id = req.params.chat_id;
const owner = await models.Contact.findOne({ where: { isOwner: true }})
models.Message.update({ seen: true }, {
where: {
sender: {
[Op.ne]: owner.id
},
chatId: chat_id
}
});
success(res, {})
}
const clearMessages = (req, res) => {
models.Message.destroy({ where: {}, truncate: true })
success(res, {})
}
export {
getMessages,
sendMessage,
receiveMessage,
receiveConfirmation,
clearMessages,
readMessages
}

199
api/controllers/payment.ts

@ -0,0 +1,199 @@
import {models} from '../models'
import { sendNotification } from '../hub'
import * as socket from '../utils/socket'
import * as jsonUtils from '../utils/json'
import * as helpers from '../helpers'
import { success } from '../utils/res'
import * as lightning from '../utils/lightning'
import {tokenFromTerms} from '../utils/ldat'
const constants = require(__dirname + '/../../config/constants.json');
const sendPayment = async (req, res) => {
const {
amount,
chat_id,
contact_id,
destination_key,
media_type,
muid,
text,
remote_text,
dimensions,
} = req.body
console.log('[send payment]', req.body)
if (destination_key && !contact_id && !chat_id) {
return helpers.performKeysendMessage({
destination_key,
amount,
msg:'{}',
success: () => {
console.log('payment sent!')
success(res, {destination_key, amount})
},
failure: (error) => {
res.status(200);
res.json({ success: false, error });
res.end();
}
})
}
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const chat = await helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id
})
var date = new Date();
date.setMilliseconds(0)
const msg: {[k:string]:any} = {
chatId: chat.id,
sender: owner.id,
type: constants.message_types.direct_payment,
amount: amount,
amountMsat: parseFloat(amount) * 1000,
date: date,
createdAt: date,
updatedAt: date
}
if(text) msg.messageContent = text
if(remote_text) msg.remoteMessageContent = remote_text
if(muid){
const myMediaToken = await tokenFromTerms({
meta:{dim:dimensions}, host:'',
muid, ttl:null, // default one year
pubkey: owner.publicKey
})
msg.mediaToken = myMediaToken
msg.mediaType = media_type || ''
}
const message = await models.Message.create(msg)
const msgToSend: {[k:string]:any} = {
id:message.id,
amount,
}
if(muid) {
msgToSend.mediaType = media_type||'image/jpeg'
msgToSend.mediaTerms = {muid,meta:{dim:dimensions}}
}
if(remote_text) msgToSend.content = remote_text
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.direct_payment,
message: msgToSend,
amount: amount,
success: async (data) => {
// console.log('payment sent', { data })
success(res, jsonUtils.messageToJson(message, chat))
},
failure: (error) => {
res.status(200);
res.json({ success: false, error });
res.end();
}
})
};
const receivePayment = async (payload) => {
console.log('received payment', { payload })
var date = new Date();
date.setMilliseconds(0)
const {owner, sender, chat, amount, content, mediaType, mediaToken} = await helpers.parseReceiveParams(payload)
if(!owner || !sender || !chat) {
return console.log('=> no group chat!')
}
const msg: {[k:string]:any} = {
chatId: chat.id,
type: constants.message_types.direct_payment,
sender: sender.id,
amount: amount,
amountMsat: parseFloat(amount) * 1000,
date: date,
createdAt: date,
updatedAt: date
}
if(content) msg.messageContent = content
if(mediaType) msg.mediaType = mediaType
if(mediaToken) msg.mediaToken = mediaToken
const message = await models.Message.create(msg)
console.log('saved message', message.dataValues)
socket.sendJson({
type: 'direct_payment',
response: jsonUtils.messageToJson(message, chat)
})
sendNotification(chat, sender.alias, 'message')
}
const listPayments = async (req, res) => {
const limit = (req.query.limit && parseInt(req.query.limit)) || 100
const offset = (req.query.offset && parseInt(req.query.offset)) || 0
const payments: any[] = []
const response:any = await lightning.listInvoices()
const invs = response && response.invoices
if(invs && invs.length){
invs.forEach(inv=>{
const val = inv.value && parseInt(inv.value)
if(val && val>1) {
let payment_hash=''
if(inv.r_hash){
payment_hash = Buffer.from(inv.r_hash).toString('hex')
}
payments.push({
type:'invoice',
amount:parseInt(inv.value),
date:parseInt(inv.creation_date),
payment_request:inv.payment_request,
payment_hash
})
}
})
}
const res2:any = await lightning.listPayments()
const pays = res2 && res2.payments
if(pays && pays.length){
pays.forEach(pay=>{
const val = pay.value && parseInt(pay.value)
if(val && val>1) {
payments.push({
type:'payment',
amount:parseInt(pay.value),
date:parseInt(pay.creation_date),
pubkey:pay.path[pay.path.length-1],
payment_hash: pay.payment_hash,
})
}
})
}
// latest one first
payments.sort((a,b)=> b.date - a.date)
success(res, payments.splice(offset, limit))
};
export {
sendPayment,
receivePayment,
listPayments,
}

29
api/controllers/schemas.ts

@ -0,0 +1,29 @@
import * as yup from 'yup'
/*
These schemas validate payloads coming from app,
do not necessarily match up with Models
*/
const attachment = yup.object().shape({
muid: yup.string().required(),
media_type: yup.string().required(),
media_key_map: yup.object().required(),
})
const message = yup.object().shape({
contact_id: yup.number().required(),
})
const purchase = yup.object().shape({
chat_id: yup.number().required(),
contact_id: yup.number().required(),
mediaToken: yup.string().required(),
amount: yup.number().required()
})
export {
attachment,
purchase,
message,
}

387
api/controllers/subscriptions.ts

@ -0,0 +1,387 @@
import {models} from '../models'
import {success, failure} from '../utils/res'
import {CronJob} from 'cron'
import {toCamel} from '../utils/case'
import * as cronUtils from '../utils/cron'
import * as socket from '../utils/socket'
import * as jsonUtils from '../utils/json'
import * as helpers from '../helpers'
import * as rsa from '../crypto/rsa'
import * as moment from 'moment'
const constants = require(__dirname + '/../../config/constants.json');
// store all current running jobs in memory
let jobs = {}
// init jobs from DB
const initializeCronJobs = async () => {
await helpers.sleep(1000)
const subs = await getRawSubs({ where: { ended: false } })
subs.length && subs.forEach(sub => {
console.log("=> starting subscription cron job",sub.id+":",sub.cron)
startCronJob(sub)
})
}
async function startCronJob(sub) {
jobs[sub.id] = new CronJob(sub.cron, async function () {
const subscription = await models.Subscription.findOne({ where: { id: sub.id } })
if (!subscription) {
delete jobs[sub.id]
return this.stop()
}
console.log('EXEC CRON =>', subscription.id)
if (subscription.paused) { // skip, still in jobs{} tho
return this.stop()
}
let STOP = checkSubscriptionShouldAlreadyHaveEnded(subscription)
if (STOP) { // end the job and return
console.log("stop")
subscription.update({ ended: true })
delete jobs[subscription.id]
return this.stop()
}
// SEND PAYMENT!!!
sendSubscriptionPayment(subscription, false)
}, null, true);
}
function checkSubscriptionShouldAlreadyHaveEnded(sub) {
if (sub.endDate) {
const now = new Date()
if (now.getTime() > sub.endDate.getTime()) {
return true
}
}
if (sub.endNumber) {
if (sub.count >= sub.endNumber) {
return true
}
}
return false
}
function checkSubscriptionShouldEndAfterThisPayment(sub) {
if (sub.endDate) {
const { ms } = cronUtils.parse(sub.cron)
const now = new Date()
if ((now.getTime() + ms) > sub.endDate.getTime()) {
return true
}
}
if (sub.endNumber) {
if (sub.count + 1 >= sub.endNumber) {
return true
}
}
return false
}
function msgForSubPayment(owner, sub, isFirstMessage, forMe){
let text = ''
if (isFirstMessage) {
const alias = forMe ? 'You' : owner.alias
text = `${alias} subscribed\n`
} else {
text = 'Subscription\n'
}
text += `Amount: ${sub.amount} sats\n`
text += `Interval: ${cronUtils.parse(sub.cron).interval}\n`
if(sub.endDate) {
text += `End: ${moment(sub.endDate).format('MM/DD/YY')}\n`
text += `Status: ${sub.count+1} sent`
} else if(sub.endNumber) {
text += `Status: ${sub.count+1} of ${sub.endNumber} sent`
}
return text
}
async function sendSubscriptionPayment(sub, isFirstMessage) {
const owner = await models.Contact.findOne({ where: { isOwner: true } })
var date = new Date();
date.setMilliseconds(0)
const subscription = await models.Subscription.findOne({ where: { id: sub.id } })
if (!subscription) {
return
}
const chat = await models.Chat.findOne({ where: {id:subscription.chatId} })
if (!subscription) {
console.log("=> no sub for this payment!!!")
return
}
const forMe = false
const text = msgForSubPayment(owner, sub, isFirstMessage, forMe)
const contact = await models.Contact.findByPk(sub.contactId)
const enc = rsa.encrypt(contact.contactKey, text)
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.direct_payment,
message: { amount: sub.amount, content: enc },
amount: sub.amount,
success: async (data) => {
const shouldEnd = checkSubscriptionShouldEndAfterThisPayment(subscription)
const obj = {
totalPaid: parseFloat(subscription.totalPaid||0) + parseFloat(subscription.amount),
count: parseInt(subscription.count||0) + 1,
ended: false,
}
if(shouldEnd) {
obj.ended = true
if(jobs[sub.id]) jobs[subscription.id].stop()
delete jobs[subscription.id]
}
await subscription.update(obj)
const forMe = true
const text2 = msgForSubPayment(owner, sub, isFirstMessage, forMe)
const encText = rsa.encrypt(owner.contactKey, text2)
const message = await models.Message.create({
chatId: chat.id,
sender: owner.id,
type: constants.message_types.direct_payment,
status: constants.statuses.confirmed,
messageContent: encText,
amount: subscription.amount,
amountMsat: parseFloat(subscription.amount) * 1000,
date: date,
createdAt: date,
updatedAt: date,
subscriptionId: subscription.id,
})
socket.sendJson({
type: 'direct_payment',
response: jsonUtils.messageToJson(message, chat)
})
},
failure: async (err) => {
console.log("SEND PAY ERROR")
let errMessage = constants.payment_errors[err] || 'Unknown'
errMessage = 'Payment Failed: ' + errMessage
const message = await models.Message.create({
chatId: chat.id,
sender: owner.id,
type: constants.message_types.direct_payment,
status: constants.statuses.failed,
messageContent: errMessage,
amount: sub.amount,
amountMsat: parseFloat(sub.amount) * 1000,
date: date,
createdAt: date,
updatedAt: date,
subscriptionId: sub.id,
})
socket.sendJson({
type: 'direct_payment',
response: jsonUtils.messageToJson(message, chat)
})
}
})
}
// pause sub
async function pauseSubscription(req, res) {
const id = parseInt(req.params.id)
try {
const sub = await models.Subscription.findOne({ where: { id } })
if (sub) {
sub.update({ paused: true })
if (jobs[id]) jobs[id].stop()
success(res, jsonUtils.subscriptionToJson(sub,null))
} else {
failure(res, 'not found')
}
} catch (e) {
console.log('ERROR pauseSubscription', e)
failure(res, e)
}
};
// restart sub
async function restartSubscription(req, res) {
const id = parseInt(req.params.id)
try {
const sub = await models.Subscription.findOne({ where: { id } })
if (sub) {
sub.update({ paused: false })
if (jobs[id]) jobs[id].start()
success(res, jsonUtils.subscriptionToJson(sub,null))
} else {
failure(res, 'not found')
}
} catch (e) {
console.log('ERROR restartSubscription', e)
failure(res, e)
}
};
async function getRawSubs(opts = {}) {
const options: {[k: string]: any} = { order: [['id', 'asc']], ...opts }
try {
const subs = await models.Subscription.findAll(options)
return subs
} catch (e) {
throw e
}
}
// all subs
const getAllSubscriptions = async (req, res) => {
try {
const subs = await getRawSubs()
success(res, subs.map(sub => jsonUtils.subscriptionToJson(sub,null)))
} catch (e) {
console.log('ERROR getAllSubscriptions', e)
failure(res, e)
}
};
// one sub by id
async function getSubscription(req, res) {
try {
const sub = await models.Subscription.findOne({ where: { id: req.params.id } })
success(res, jsonUtils.subscriptionToJson(sub,null))
} catch (e) {
console.log('ERROR getSubscription', e)
failure(res, e)
}
};
// delete sub by id
async function deleteSubscription(req, res) {
const id = req.params.id
if (!id) return
try {
if (jobs[id]) {
jobs[id].stop()
delete jobs[id]
}
models.Subscription.destroy({ where: { id } })
success(res, true)
} catch (e) {
console.log('ERROR deleteSubscription', e)
failure(res, e)
}
};
// all subs for contact id
const getSubscriptionsForContact = async (req, res) => {
try {
const subs = await getRawSubs({ where: { contactId: req.params.contactId } })
success(res, subs.map(sub => jsonUtils.subscriptionToJson(sub,null)))
} catch (e) {
console.log('ERROR getSubscriptionsForContact', e)
failure(res, e)
}
};
// create new sub
async function createSubscription(req, res) {
const date = new Date()
date.setMilliseconds(0)
const s = jsonToSubscription({
...req.body,
count: 0,
total_paid: 0,
createdAt: date,
ended: false,
paused: false
})
if(!s.cron){
return failure(res, 'Invalid interval')
}
try {
const owner = await models.Contact.findOne({ where: { isOwner: true } })
const chat = await helpers.findOrCreateChat({
chat_id: req.body.chat_id,
owner_id: owner.id,
recipient_id: req.body.contact_id,
})
s.chatId = chat.id // add chat id if newly created
if(!owner || !chat){
return failure(res, 'Invalid chat or contact')
}
const sub = await models.Subscription.create(s)
startCronJob(sub)
const isFirstMessage = true
sendSubscriptionPayment(sub, isFirstMessage)
success(res, jsonUtils.subscriptionToJson(sub, chat))
} catch (e) {
console.log('ERROR createSubscription', e)
failure(res, e)
}
};
async function editSubscription(req, res) {
console.log('======> editSubscription')
const date = new Date()
date.setMilliseconds(0)
const id = parseInt(req.params.id)
const s = jsonToSubscription({
...req.body,
count: 0,
createdAt: date,
ended: false,
paused: false
})
try {
if(!id || !s.chatId || !s.cron){
return failure(res, 'Invalid data')
}
const subRecord = await models.Subscription.findOne({ where: { id }})
if(!subRecord) {
return failure(res, 'No subscription found')
}
// stop so it can be restarted
if (jobs[id]) jobs[id].stop()
const obj: {[k: string]: any} = {
cron: s.cron,
updatedAt: date,
}
if(s.amount) obj.amount = s.amount
if(s.endDate) obj.endDate = s.endDate
if(s.endNumber) obj.endNumber = s.endNumber
const sub = await subRecord.update(obj)
const end = checkSubscriptionShouldAlreadyHaveEnded(sub)
if(end) {
await subRecord.update({ended:true})
delete jobs[id]
} else {
startCronJob(sub) // restart
}
const chat = await models.Chat.findOne({ where: { id: s.chatId }})
success(res, jsonUtils.subscriptionToJson(sub, chat))
} catch (e) {
console.log('ERROR createSubscription', e)
failure(res, e)
}
};
function jsonToSubscription(j) {
console.log("=>",j)
const cron = cronUtils.make(j.interval)
return toCamel({
...j,
cron,
})
}
export {
initializeCronJobs,
getAllSubscriptions,
getSubscription,
createSubscription,
getSubscriptionsForContact,
pauseSubscription,
restartSubscription,
deleteSubscription,
editSubscription,
}

60
api/controllers/uploads.ts

@ -0,0 +1,60 @@
import {models} from '../models'
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/app.json')[env];
// setup disk storage
var multer = require('multer')
var avatarStorage = multer.diskStorage({
destination: (req, file, cb) => {
let dir = __dirname.includes('/dist/') ? __dirname+'/..' : __dirname
cb(null, dir + '/../../public/uploads')
},
filename: (req, file, cb) => {
const mime = file.mimetype
const extA = mime.split("/")
const ext = extA[extA.length-1]
if(req.body.chat_id){
cb(null, `chat_${req.body.chat_id}_picture.${ext}`)
} else {
cb(null, `${req.body.contact_id}_profile_picture.${ext}`)
}
}
})
var avatarUpload = multer({ storage: avatarStorage })
const uploadFile = async (req, res) => {
const { contact_id, chat_id } = req.body
const { file } = req
const photo_url =
config.node_http_protocol +
'://' +
process.env.NODE_IP +
'/static/uploads/' +
file.filename
if(contact_id){
const contact = await models.Contact.findOne({ where: { id: contact_id } })
if(contact) contact.update({ photoUrl: photo_url })
}
if(chat_id){
const chat = await models.Chat.findOne({ where: { id: chat_id } })
if(chat) chat.update({ photoUrl: photo_url })
}
res.status(200)
res.json({
success: true,
contact_id: parseInt(contact_id||0),
chat_id: parseInt(chat_id||0),
photo_url
});
res.end();
}
export {
avatarUpload,
uploadFile
}

63
api/crypto/rsa.ts

@ -0,0 +1,63 @@
import * as crypto from "crypto";
export function encrypt(key, txt){
try{
const pubc = cert.pub(key)
const buf = crypto.publicEncrypt({
key:pubc,
padding:crypto.constants.RSA_PKCS1_PADDING,
}, Buffer.from(txt,'utf-8'))
return buf.toString('base64')
} catch(e) {
return ''
}
}
export function decrypt(privateKey, enc){
try{
const privc = cert.priv(privateKey)
const buf = crypto.privateDecrypt({
key:privc,
padding:crypto.constants.RSA_PKCS1_PADDING,
}, Buffer.from(enc,'base64'))
return buf.toString('utf-8')
} catch(e) {
return ''
}
}
export function testRSA(){
crypto.generateKeyPair('rsa', {
modulusLength: 2048
}, (err, publicKey, priv)=>{
const pubPEM = publicKey.export({
type:'pkcs1',format:'pem'
})
const pub = cert.unpub(pubPEM)
const msg = 'hi'
const enc = encrypt(pub, msg)
const dec = decrypt(priv, enc)
console.log("FINAL:",dec)
})
}
const cert = {
unpub: function(key){
let s = key
s = s.replace('-----BEGIN RSA PUBLIC KEY-----','')
s = s.replace('-----END RSA PUBLIC KEY-----','')
return s.replace(/[\r\n]+/gm, '')
},
pub:function(key){
return '-----BEGIN RSA PUBLIC KEY-----\n' +
key + '\n' +
'-----END RSA PUBLIC KEY-----'
},
priv:function(key){
return '-----BEGIN RSA PRIVATE KEY-----\n' +
key + '\n' +
'-----END RSA PRIVATE KEY-----'
}
}

144
api/grpc/index.ts

@ -0,0 +1,144 @@
import {models} from '../models'
import * as socket from '../utils/socket'
import { sendNotification } from '../hub'
import * as jsonUtils from '../utils/json'
import * as decodeUtils from '../utils/decode'
import {loadLightning, SPHINX_CUSTOM_RECORD_KEY} from '../utils/lightning'
const constants = require(__dirname + '/../../config/constants.json');
function parseKeysendInvoice(i, actions){
const recs = i.htlcs && i.htlcs[0] && i.htlcs[0].custom_records
const buf = recs && recs[SPHINX_CUSTOM_RECORD_KEY]
const data = buf && buf.toString()
const value = i && i.value && parseInt(i.value)
if(!data) return
let payload
if(data[0]==='{'){
try {
payload = JSON.parse(data)
} catch(e){}
} else {
const threads = weave(data)
if(threads) payload = JSON.parse(threads)
}
if(payload){
const dat = payload.content || payload
if(value && dat && dat.message){
dat.message.amount = value
}
if(actions[payload.type]) {
actions[payload.type](payload)
} else {
console.log('Incorrect payload type:', payload.type)
}
}
}
const chunks = {}
function weave(p){
const pa = p.split('_')
if(pa.length<4) return
const ts = pa[0]
const i = pa[1]
const n = pa[2]
const m = pa.filter((u,i)=>i>2).join('_')
chunks[ts] = chunks[ts] ? [...chunks[ts], {i,n,m}] : [{i,n,m}]
if(chunks[ts].length===parseInt(n)){
// got em all!
const all = chunks[ts]
let payload = ''
all.slice().sort((a,b)=>a.i-b.i).forEach(obj=>{
payload += obj.m
})
delete chunks[ts]
return payload
}
}
function subscribeInvoices(actions) {
return new Promise(async(resolve,reject)=>{
const lightning = await loadLightning()
var call = lightning.subscribeInvoices();
call.on('data', async function(response) {
// console.log('subscribed invoices', { response })
if (response['state'] !== 'SETTLED') {
return
}
// console.log("IS KEYSEND", response.is_keysend)
if(response.is_keysend) {
parseKeysendInvoice(response, actions)
} else {
const invoice = await models.Message.findOne({ where: { type: constants.message_types.invoice, payment_request: response['payment_request'] } })
if (invoice == null) {
// console.log("ERROR: Invoice " + response['payment_request'] + " not found");
socket.sendJson({
type: 'invoice_payment',
response: {invoice: response['payment_request']}
})
return
}
models.Message.update({ status: constants.statuses.confirmed }, { where: { id: invoice.id } })
let decodedPaymentRequest = decodeUtils.decode(response['payment_request']);
var paymentHash = "";
for (var i=0; i<decodedPaymentRequest["data"]["tags"].length; i++) {
let tag = decodedPaymentRequest["data"]["tags"][i];
if (tag['description'] == 'payment_hash') {
paymentHash = tag['value'];
break;
}
}
let settleDate = parseInt(response['settle_date'] + '000');
const chat = await models.Chat.findOne({ where: { id: invoice.chatId } })
const contactIds = JSON.parse(chat.contactIds)
const senderId = contactIds.find(id => id != invoice.sender)
const message = await models.Message.create({
chatId: invoice.chatId,
type: constants.message_types.payment,
sender: senderId,
amount: response['amt_paid_sat'],
amountMsat: response['amt_paid_msat'],
paymentHash: paymentHash,
date: new Date(settleDate),
messageContent: response['memo'],
status: constants.statuses.confirmed,
createdAt: new Date(settleDate),
updatedAt: new Date(settleDate)
})
socket.sendJson({
type: 'payment',
response: jsonUtils.messageToJson(message, chat)
})
const sender = await models.Contact.findOne({ where: { id: senderId } })
sendNotification(chat, sender.alias, 'message')
}
});
call.on('status', function(status) {
console.log("Status", status);
resolve(status)
});
call.on('error', function(err){
// console.log(err)
reject(err)
})
call.on('end', function() {
console.log("Closed stream");
// The server has closed the stream.
});
setTimeout(()=>{
resolve(null)
},100)
})
}
export {
subscribeInvoices,
}

247
api/helpers.ts

@ -0,0 +1,247 @@
import { models } from './models'
import * as md5 from 'md5'
import { keysendMessage } from './utils/lightning'
import {personalizeMessage} from './utils/msg'
const constants = require('../config/constants.json');
const findOrCreateChat = async (params) => {
const { chat_id, owner_id, recipient_id } = params
let chat
let date = new Date();
date.setMilliseconds(0)
if (chat_id) {
chat = await models.Chat.findOne({ where: { id: chat_id } })
// console.log('findOrCreateChat: chat_id exists')
} else {
console.log("chat does not exists, create new")
const owner = await models.Contact.findOne({ where: { id: owner_id } })
const recipient = await models.Contact.findOne({ where: { id: recipient_id } })
const uuid = md5([owner.publicKey, recipient.publicKey].sort().join("-"))
// find by uuid
chat = await models.Chat.findOne({ where:{uuid} })
if(!chat){ // no chat! create new
chat = await models.Chat.create({
uuid: uuid,
contactIds: JSON.stringify([parseInt(owner_id), parseInt(recipient_id)]),
createdAt: date,
updatedAt: date,
type: constants.chat_types.conversation
})
}
}
return chat
}
const sendContactKeys = async (args) => {
const { type, contactIds, contactPubKey, sender, success, failure } = args
const msg = newkeyexchangemsg(type, sender)
let yes:any = null
let no:any = null
let cids = contactIds
if(!contactIds) cids = [null] // nully
await asyncForEach(cids, async contactId => {
let destination_key:string
if(!contactId){ // nully
destination_key = contactPubKey
} else {
if (contactId == sender.id) {
return
}
const contact = await models.Contact.findOne({ where: { id: contactId } })
destination_key = contact.publicKey
}
performKeysendMessage({
destination_key,
amount: 1,
msg: JSON.stringify(msg),
success: (data) => {
yes = data
},
failure: (error) => {
no = error
}
})
})
if(no && failure){
failure(no)
}
if(!no && yes && success){
success(yes)
}
}
const sendMessage = async (params) => {
const { type, chat, message, sender, amount, success, failure } = params
const m = newmsg(type, chat, sender, message)
const contactIds = typeof chat.contactIds==='string' ? JSON.parse(chat.contactIds) : chat.contactIds
let yes:any = null
let no:any = null
console.log('all contactIds',contactIds)
await asyncForEach(contactIds, async contactId => {
if (contactId == sender.id) {
return
}
const contact = await models.Contact.findOne({ where: { id: contactId } })
const destkey = contact.publicKey
const finalMsg = await personalizeMessage(m, contactId, destkey)
const opts = {
dest: destkey,
data: JSON.stringify(finalMsg),
amt: amount || 1,
}
try {
const r = await keysendMessage(opts)
yes = r
} catch (e) {
console.log("KEYSEND ERROR", e)
no = e
}
})
if(yes){
if(success) success(yes)
} else {
if(failure) failure(no)
}
}
const performKeysendMessage = async ({ destination_key, amount, msg, success, failure }) => {
const opts = {
dest: destination_key,
data: msg || JSON.stringify({}),
amt: amount || 1
}
try {
const r = await keysendMessage(opts)
console.log("MESSAGE SENT outside SW!", r)
if (success) success(r)
} catch (e) {
console.log("MESSAGE ERROR", e)
if (failure) failure(e)
}
}
async function findOrCreateContactByPubkey(senderPubKey) {
let sender = await models.Contact.findOne({ where: { publicKey: senderPubKey } })
if (!sender) {
sender = await models.Contact.create({
publicKey: senderPubKey,
alias: "Unknown",
status: 1
})
const owner = await models.Contact.findOne({ where: { isOwner: true } })
sendContactKeys({
contactIds: [sender.id],
sender: owner,
type: constants.message_types.contact_key,
})
}
return sender
}
async function findOrCreateChatByUUID(chat_uuid, contactIds) {
let chat = await models.Chat.findOne({ where: { uuid: chat_uuid } })
if (!chat) {
var date = new Date();
date.setMilliseconds(0)
chat = await models.Chat.create({
uuid: chat_uuid,
contactIds: JSON.stringify(contactIds || []),
createdAt: date,
updatedAt: date,
type: 0 // conversation
})
}
return chat
}
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms))
}
async function parseReceiveParams(payload) {
const dat = payload.content || payload
const sender_pub_key = dat.sender.pub_key
const chat_uuid = dat.chat.uuid
const chat_type = dat.chat.type
const chat_members: { [k: string]: any } = dat.chat.members || {}
const chat_name = dat.chat.name
const amount = dat.message.amount
const content = dat.message.content
const mediaToken = dat.message.mediaToken
const msg_id = dat.message.id||0
const mediaKey = dat.message.mediaKey
const mediaType = dat.message.mediaType
const isGroup = chat_type && chat_type == constants.chat_types.group
let sender
let chat
const owner = await models.Contact.findOne({ where: { isOwner: true } })
if (isGroup) {
sender = await models.Contact.findOne({ where: { publicKey: sender_pub_key } })
chat = await models.Chat.findOne({ where: { uuid: chat_uuid } })
} else {
sender = await findOrCreateContactByPubkey(sender_pub_key)
chat = await findOrCreateChatByUUID(
chat_uuid, [parseInt(owner.id), parseInt(sender.id)]
)
}
return { owner, sender, chat, sender_pub_key, chat_uuid, amount, content, mediaToken, mediaKey, mediaType, chat_type, msg_id, chat_members, chat_name }
}
export {
findOrCreateChat,
sendMessage,
sendContactKeys,
findOrCreateContactByPubkey,
findOrCreateChatByUUID,
sleep,
parseReceiveParams,
performKeysendMessage
}
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
function newmsg(type, chat, sender, message){
return {
type: type,
chat: {
uuid: chat.uuid,
...chat.name && { name: chat.name },
...chat.type && { type: chat.type },
...chat.members && { members: chat.members },
},
message: message,
sender: {
pub_key: sender.publicKey,
// ...sender.contactKey && {contact_key: sender.contactKey}
}
}
}
function newkeyexchangemsg(type, sender){
return {
type: type,
sender: {
pub_key: sender.publicKey,
contact_key: sender.contactKey,
...sender.alias && {alias: sender.alias},
// ...sender.photoUrl && {photoUrl: sender.photoUrl}
}
}
}

219
api/hub.ts

@ -0,0 +1,219 @@
import {models} from './models'
import * as fetch from 'node-fetch'
import { Op } from 'sequelize'
import * as socket from './utils/socket'
import * as jsonUtils from './utils/json'
import * as helpers from './helpers'
import {nodeinfo} from './utils/nodeinfo'
const constants = require(__dirname + '/../config/constants.json');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/app.json')[env];
const checkInviteHub = async (params = {}) => {
if (env != "production") {
return
}
const owner = await models.Contact.findOne({ where: { isOwner: true }})
//console.log('[hub] checking invites ping')
const inviteStrings = await models.Invite.findAll({ where: { status: { [Op.notIn]: [constants.invite_statuses.complete, constants.invite_statuses.expired] } } }).map(invite => invite.inviteString)
fetch(config.hub_api_url + '/invites/check', {
method: 'POST' ,
body: JSON.stringify({ invite_strings: inviteStrings }),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
if (json.object) {
json.object.invites.map(async object => {
const invite = object.invite
const pubkey = object.pubkey
const price = object.price
const dbInvite = await models.Invite.findOne({ where: { inviteString: invite.pin }})
const contact = await models.Contact.findOne({ where: { id: dbInvite.contactId } })
if (dbInvite.status != invite.invite_status) {
dbInvite.update({ status: invite.invite_status, price: price })
socket.sendJson({
type: 'invite',
response: jsonUtils.inviteToJson(dbInvite)
})
if (dbInvite.status == constants.invite_statuses.ready && contact) {
sendNotification(-1, contact.alias, 'invite')
}
}
if (pubkey && dbInvite.status == constants.invite_statuses.complete && contact) {
contact.update({ publicKey: pubkey, status: constants.contact_statuses.confirmed })
var contactJson = jsonUtils.contactToJson(contact)
contactJson.invite = jsonUtils.inviteToJson(dbInvite)
socket.sendJson({
type: 'contact',
response: contactJson
})
helpers.sendContactKeys({
contactIds: [contact.id],
sender: owner,
type: constants.message_types.contact_key,
})
}
})
}
})
.catch(error => {
console.log('[hub error]', error)
})
}
const pingHub = async (params = {}) => {
if (env != "production") {
return
}
const node = await nodeinfo()
sendHubCall({ ...params, node })
}
const sendHubCall = (params) => {
// console.log('[hub] sending ping')
fetch(config.hub_api_url + '/ping', {
method: 'POST',
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
// ?
})
.catch(error => {
console.log('[hub error]', error)
})
}
const pingHubInterval = (ms) => {
setInterval(pingHub, ms)
}
const checkInvitesHubInterval = (ms) => {
setInterval(checkInviteHub, ms)
}
const finishInviteInHub = (params, onSuccess, onFailure) => {
fetch(config.hub_api_url + '/invites/finish', {
method: 'POST' ,
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
if (json.object) {
console.log('[hub] finished invite to hub')
onSuccess(json)
} else {
console.log('[hub] fail to finish invite in hub')
onFailure(json)
}
})
}
const payInviteInHub = (invite_string, params, onSuccess, onFailure) => {
fetch(config.hub_api_url + '/invites/' + invite_string + '/pay', {
method: 'POST' ,
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
if (json.object) {
console.log('[hub] finished pay to hub')
onSuccess(json)
} else {
console.log('[hub] fail to pay invite in hub')
onFailure(json)
}
})
}
const createInviteInHub = (params, onSuccess, onFailure) => {
fetch(config.hub_api_url + '/invites', {
method: 'POST' ,
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
if (json.object) {
console.log('[hub] sent invite to be created to hub')
onSuccess(json)
} else {
console.log('[hub] fail to create invite in hub')
onFailure(json)
}
})
}
const sendNotification = async (chat, name, type) => {
let message = `You have a new message from ${name}`
if(type==='invite'){
message = `Your invite to ${name} is ready`
}
if(type==='group'){
message = `You have been added to group ${name}`
}
if(type==='message' && chat.type==constants.chat_types.group && chat.name && chat.name.length){
message += ` on ${chat.name}`
}
console.log('[send notification]', { chat_id:chat.id, message })
if (chat.isMuted) {
console.log('[send notification] skipping. chat is muted.')
return
}
const owner = await models.Contact.findOne({ where: { isOwner: true }})
if (!owner.deviceId) {
console.log('[send notification] skipping. owner.deviceId not set.')
return
}
const unseenMessages = await models.Message.findAll({ where: { sender: { [Op.ne]: owner.id }, seen: false } })
const params = {
device_id: owner.deviceId,
notification: {
chat_id: chat.id,
message,
badge: unseenMessages.length
}
}
fetch("http://hub.sphinx.chat/api/v1/nodes/notify", {
method: 'POST' ,
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => console.log('[hub notification]', json))
}
export {
pingHubInterval,
checkInvitesHubInterval,
sendHubCall,
sendNotification,
createInviteInHub,
finishInviteInHub,
payInviteInHub
}

16
api/models/index.ts

@ -0,0 +1,16 @@
import {Sequelize} from 'sequelize-typescript';
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/config.json')[env];
const sequelize = new Sequelize({
...config,
logging: process.env.SQL_LOG==='true' ? console.log : false,
models: [__dirname + '/ts']
})
const models = sequelize.models
export {
sequelize,
models,
}

48
api/models/ts/chat.ts

@ -0,0 +1,48 @@
import { Table, Column, Model, DataType } from 'sequelize-typescript';
@Table({tableName: 'sphinx_chats', underscored: true})
export default class Chat extends Model<Chat> {
@Column({
type: DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
})
id: number
@Column
uuid: string
@Column
name: string
@Column
photoUrl: string
@Column(DataType.BIGINT)
type: number
@Column(DataType.BIGINT)
status: number
@Column
contactIds: string
@Column
isMuted: boolean
@Column
createdAt: Date
@Column
updatedAt: Date
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
allowNull: false
})
deleted: boolean
}

57
api/models/ts/contact.ts

@ -0,0 +1,57 @@
import { Table, Column, Model, DataType } from 'sequelize-typescript';
@Table({tableName: 'sphinx_contacts', underscored: true})
export default class Contact extends Model<Contact> {
@Column({
type: DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
})
id: number
@Column
publicKey: string
@Column
nodeAlias: string
@Column
alias: string
@Column
photoUrl: string
@Column
isOwner: boolean
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
allowNull: false
})
deleted: boolean
@Column
authToken: string
@Column(DataType.BIGINT)
remoteId: number
@Column(DataType.BIGINT)
status: number
@Column(DataType.TEXT)
contactKey: string
@Column
deviceId: string
@Column
createdAt: Date
@Column
updatedAt: Date
}

35
api/models/ts/invite.ts

@ -0,0 +1,35 @@
import { Table, Column, Model, DataType } from 'sequelize-typescript';
@Table({tableName: 'sphinx_invites', underscored: true})
export default class Invite extends Model<Invite> {
@Column({
type: DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
})
id: number
@Column
inviteString: string
@Column
welcomeMessage: string
@Column(DataType.BIGINT)
contactId: number
@Column(DataType.BIGINT)
status: number
@Column(DataType.DECIMAL(10, 2))
price: number
@Column
createdAt: Date
@Column
updatedAt: Date
}

39
api/models/ts/mediaKey.ts

@ -0,0 +1,39 @@
import { Table, Column, Model, DataType } from 'sequelize-typescript';
/*
Used for media uploads. When you upload a file,
also upload the symetric key encrypted for each chat member.
When they buy the file, they can retrieve the key from here.
"received" media keys are not stored here, only in Message
*/
@Table({tableName: 'sphinx_media_keys', underscored: true})
export default class MediaKey extends Model<MediaKey> {
@Column({
type: DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
})
id: number
@Column
muid: string
@Column(DataType.BIGINT)
chatId: number
@Column(DataType.BIGINT)
receiver: number
@Column
key: string
@Column(DataType.BIGINT)
messageId: number
@Column
createdAt: Date
}

89
api/models/ts/message.ts

@ -0,0 +1,89 @@
import { Table, Column, Model, DataType } from 'sequelize-typescript';
@Table({tableName: 'sphinx_messages', underscored: true})
export default class Message extends Model<Message> {
@Column({
type: DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
})
id: number
@Column(DataType.BIGINT)
chatId: number
@Column(DataType.BIGINT)
type: number
@Column(DataType.BIGINT)
sender: number
@Column(DataType.BIGINT)
receiver: number
@Column(DataType.DECIMAL)
amount: number
@Column(DataType.DECIMAL)
amountMsat: number
@Column
paymentHash: string
@Column(DataType.TEXT)
paymentRequest: string
@Column
date: Date
@Column
expirationDate: Date
@Column(DataType.TEXT)
messageContent: string
@Column(DataType.TEXT)
remoteMessageContent: string
@Column(DataType.BIGINT)
status: number
@Column(DataType.TEXT)
statusMap: string
@Column(DataType.BIGINT)
parentId: number
@Column(DataType.BIGINT)
subscriptionId: number
@Column
mediaTerms: string
@Column
receipt: string
@Column
mediaKey: string
@Column
mediaType: string
@Column
mediaToken: string
@Column({
type: DataType.BOOLEAN,
defaultValue: false,
allowNull: false
})
seen: boolean
@Column
createdAt: Date
@Column
updatedAt: Date
}

49
api/models/ts/subscription.ts

@ -0,0 +1,49 @@
import { Table, Column, Model, DataType } from 'sequelize-typescript';
@Table({tableName: 'sphinx_subscriptions', underscored: true})
export default class Subscription extends Model<Subscription> {
@Column({
type: DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
})
id: number
@Column(DataType.BIGINT)
chatId: number
@Column(DataType.BIGINT)
contactId: number
@Column(DataType.TEXT)
cron: string
@Column(DataType.DECIMAL)
amount: number
@Column(DataType.DECIMAL)
totalPaid: number
@Column(DataType.BIGINT)
endNumber: number
@Column
endDate: Date
@Column(DataType.BIGINT)
count: number
@Column
ended: boolean
@Column
paused: boolean
@Column
createdAt: Date
@Column
updatedAt: Date
}

27
api/utils/case.ts

@ -0,0 +1,27 @@
import * as changeCase from "change-case";
const dateKeys = ['date','createdAt','updatedAt','created_at','updated_at']
function toSnake(obj) {
const ret: {[k: string]: any} = {}
for (let [key, value] of Object.entries(obj)) {
if(dateKeys.includes(key) && value){
const v: any = value
const d = new Date(v)
ret[changeCase.snakeCase(key)] = d.toISOString()
} else {
ret[changeCase.snakeCase(key)] = value
}
}
return ret
}
function toCamel(obj) {
const ret: {[k: string]: any} = {}
for (let [key, value] of Object.entries(obj)) {
ret[changeCase.camelCase(key)] = value
}
return ret
}
export {toSnake, toCamel}

49
api/utils/cron.ts

@ -0,0 +1,49 @@
import * as parser from 'cron-parser'
function daily() {
const now = new Date()
const minute = now.getMinutes()
const hour = now.getHours()
return `${minute} ${hour} * * *`
}
function weekly() {
const now = new Date()
const minute = now.getMinutes()
const hour = now.getHours()
const dayOfWeek = now.getDay()
return `${minute} ${hour} * * ${dayOfWeek}`
}
function monthly() {
const now = new Date()
const minute = now.getMinutes()
const hour = now.getHours()
const dayOfMonth = now.getDate()
return `${minute} ${hour} ${dayOfMonth} * *`
}
function parse(s) {
var interval = parser.parseExpression(s);
const next = interval.next().toString()
if(s.endsWith(' * * *')) {
return {interval: 'daily', next, ms:86400000}
}
if(s.endsWith(' * *')) {
return {interval: 'monthly', next, ms:86400000*30}
}
return {interval: 'weekly', next, ms:86400000*7}
}
function make(interval) {
if(interval==='daily') return daily()
if(interval==='weekly') return weekly()
if(interval==='monthly') return monthly()
}
export {
parse,
make,
}

312
api/utils/decode/index.js

@ -0,0 +1,312 @@
const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
module.exports = {
decode: function(paymentRequest) {
let input = paymentRequest.toLowerCase();
let splitPosition = input.lastIndexOf('1');
let humanReadablePart = input.substring(0, splitPosition);
let data = input.substring(splitPosition + 1, input.length - 6);
let checksum = input.substring(input.length - 6, input.length);
if (!this.verify_checksum(humanReadablePart, this.bech32ToFiveBitArray(data + checksum))) {
return 'error';
}
return {
'human_readable_part': this.decodeHumanReadablePart(humanReadablePart),
'data': this.decodeData(data, humanReadablePart),
'checksum': checksum
}
},
decodeHumanReadablePart: function(humanReadablePart) {
let prefixes = ['lnbc', 'lntb', 'lnbcrt'];
let prefix;
prefixes.forEach(value => {
if (humanReadablePart.substring(0, value.length) === value) {
prefix = value;
}
});
if (prefix == null) return 'error'; // A reader MUST fail if it does not understand the prefix.
let amount = this.decodeAmount(humanReadablePart.substring(prefix.length, humanReadablePart.length));
return {
'prefix': prefix,
'amount': amount
}
},
decodeData: function(data, humanReadablePart) {
let date32 = data.substring(0, 7);
let dateEpoch = this.bech32ToInt(date32);
let signature = data.substring(data.length - 104, data.length);
let tagData = data.substring(7, data.length - 104);
let decodedTags = this.decodeTags(tagData);
let value = this.bech32ToFiveBitArray(date32 + tagData);
value = this.fiveBitArrayTo8BitArray(value, true);
value = this.textToHexString(humanReadablePart).concat(this.byteArrayToHexString(value));
return {
'time_stamp': dateEpoch,
'tags': decodedTags,
'signature': this.decodeSignature(signature),
'signing_data': value
}
},
decodeSignature: function(signature) {
let data = this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(signature));
let recoveryFlag = data[data.length - 1];
let r = this.byteArrayToHexString(data.slice(0, 32));
let s = this.byteArrayToHexString(data.slice(32, data.length - 1));
return {
'r': r,
's': s,
'recovery_flag': recoveryFlag
}
},
decodeAmount: function(str) {
let multiplier = str.charAt(str.length - 1);
let amount = str.substring(0, str.length - 1);
if (amount.substring(0, 1) === '0') {
return 'error';
}
amount = Number(amount);
if (amount < 0 || !Number.isInteger(amount)) {
return 'error';
}
switch (multiplier) {
case '':
return 'Any amount'; // A reader SHOULD indicate if amount is unspecified
case 'p':
return amount / 10;
case 'n':
return amount * 100;
case 'u':
return amount * 100000;
case 'm':
return amount * 100000000;
default:
// A reader SHOULD fail if amount is followed by anything except a defined multiplier.
return 'error';
}
},
decodeTags: function(tagData) {
let tags = this.extractTags(tagData);
let decodedTags = [];
tags.forEach(value => decodedTags.push(this.decodeTag(value.type, value.length, value.data)));
return decodedTags;
},
extractTags: function(str) {
let tags = [];
while (str.length > 0) {
let type = str.charAt(0);
let dataLength = this.bech32ToInt(str.substring(1, 3));
let data = str.substring(3, dataLength + 3);
tags.push({
'type': type,
'length': dataLength,
'data': data
});
str = str.substring(3 + dataLength, str.length);
}
return tags;
},
decodeTag: function(type, length, data) {
switch (type) {
case 'p':
if (length !== 52) break; // A reader MUST skip over a 'p' field that does not have data_length 52
return {
'type': type,
'length': length,
'description': 'payment_hash',
'value': this.byteArrayToHexString(this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data)))
};
case 'd':
return {
'type': type,
'length': length,
'description': 'description',
'value': this.bech32ToUTF8String(data)
};
case 'n':
if (length !== 53) break; // A reader MUST skip over a 'n' field that does not have data_length 53
return {
'type': type,
'length': length,
'description': 'payee_public_key',
'value': this.byteArrayToHexString(this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data)))
};
case 'h':
if (length !== 52) break; // A reader MUST skip over a 'h' field that does not have data_length 52
return {
'type': type,
'length': length,
'description': 'description_hash',
'value': data
};
case 'x':
return {
'type': type,
'length': length,
'description': 'expiry',
'value': this.bech32ToInt(data)
};
case 'c':
return {
'type': type,
'length': length,
'description': 'min_final_cltv_expiry',
'value': this.bech32ToInt(data)
};
case 'f':
let version = this.bech32ToFiveBitArray(data.charAt(0))[0];
if (version < 0 || version > 18) break; // a reader MUST skip over an f field with unknown version.
data = data.substring(1, data.length);
return {
'type': type,
'length': length,
'description': 'fallback_address',
'value': {
'version': version,
'fallback_address': data
}
};
case 'r':
data = this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data));
let pubkey = data.slice(0, 33);
let shortChannelId = data.slice(33, 41);
let feeBaseMsat = data.slice(41, 45);
let feeProportionalMillionths = data.slice(45, 49);
let cltvExpiryDelta = data.slice(49, 51);
return {
'type': type,
'length': length,
'description': 'routing_information',
'value': {
'public_key': this.byteArrayToHexString(pubkey),
'short_channel_id': this.byteArrayToHexString(shortChannelId),
'fee_base_msat': this.byteArrayToInt(feeBaseMsat),
'fee_proportional_millionths': this.byteArrayToInt(feeProportionalMillionths),
'cltv_expiry_delta': this.byteArrayToInt(cltvExpiryDelta)
}
};
default:
// reader MUST skip over unknown fields
}
},
polymod: function(values) {
let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
let chk = 1;
values.forEach((value) => {
let b = (chk >> 25);
chk = (chk & 0x1ffffff) << 5 ^ value;
for (let i = 0; i < 5; i++) {
if (((b >> i) & 1) === 1) {
chk ^= GEN[i];
} else {
chk ^= 0;
}
}
});
return chk;
},
expand: function(str) {
let array = [];
for (let i = 0; i < str.length; i++) {
array.push(str.charCodeAt(i) >> 5);
}
array.push(0);
for (let i = 0; i < str.length; i++) {
array.push(str.charCodeAt(i) & 31);
}
return array;
},
verify_checksum: function(hrp, data) {
hrp = this.expand(hrp);
let all = hrp.concat(data);
let bool = this.polymod(all);
return bool === 1;
},
byteArrayToInt: function(byteArray) {
let value = 0;
for (let i = 0; i < byteArray.length; ++i) {
value = (value << 8) + byteArray[i];
}
return value;
},
bech32ToInt: function(str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum = sum * 32;
sum = sum + bech32CharValues.indexOf(str.charAt(i));
}
return sum;
},
bech32ToFiveBitArray: function(str) {
let array = [];
for (let i = 0; i < str.length; i++) {
array.push(bech32CharValues.indexOf(str.charAt(i)));
}
return array;
},
fiveBitArrayTo8BitArray: function(int5Array, includeOverflow) {
let count = 0;
let buffer = 0;
let byteArray = [];
int5Array.forEach((value) => {
buffer = (buffer << 5) + value;
count += 5;
if (count >= 8) {
byteArray.push(buffer >> (count - 8) & 255);
count -= 8;
}
});
if (includeOverflow && count > 0) {
byteArray.push(buffer << (8 - count) & 255);
}
return byteArray;
},
bech32ToUTF8String: function(str) {
let int5Array = this.bech32ToFiveBitArray(str);
let byteArray = this.fiveBitArrayTo8BitArray(int5Array);
let utf8String = '';
for (let i = 0; i < byteArray.length; i++) {
utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2);
}
return decodeURIComponent(utf8String);
},
byteArrayToHexString: function(byteArray) {
return Array.prototype.map.call(byteArray, function (byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
},
textToHexString: function(text) {
let hexString = '';
for (let i = 0; i < text.length; i++) {
hexString += text.charCodeAt(i).toString(16);
}
return hexString;
},
epochToDate: function(int) {
let date = new Date(int * 1000);
return date.toUTCString();
}
}

49
api/utils/gitinfo.ts

@ -0,0 +1,49 @@
import { exec } from 'child_process'
let commitHash
function checkCommitHash(){
return new Promise((resolve, reject)=>{
if(commitHash) {
return resolve(commitHash)
}
try{
exec(`git log -1 --pretty=format:%h`, {timeout:999}, (error, stdout, stderr) => {
if(stdout){
commitHash = stdout.trim()
return resolve(commitHash)
} else {
resolve('')
}
})
} catch(e) {
console.log(e)
resolve('')
}
})
}
let tag
function checkTag(){
return new Promise((resolve, reject)=>{
if(tag) {
return resolve(tag)
}
try{
exec(`git describe --abbrev=0 --tags`, {timeout:999}, (error, stdout, stderr) => {
if(stdout){
tag = stdout.trim()
return resolve(tag)
} else {
resolve('')
}
})
} catch(e) {
console.log(e)
resolve('')
}
})
}
export {
checkCommitHash, checkTag
}

53
api/utils/json.ts

@ -0,0 +1,53 @@
import {toSnake,toCamel} from '../utils/case'
import * as cronUtils from './cron'
function chatToJson(c) {
const chat = c.dataValues||c
let contactIds = chat.contactIds || null
if(chat.contactIds && typeof chat.contactIds==='string'){
contactIds = JSON.parse(chat.contactIds)
}
return toSnake({
...chat,
contactIds
})
}
function messageToJson(msg, chat = null) {
const message = msg.dataValues||msg
let statusMap = message.statusMap || null
if(message.statusMap && typeof message.statusMap==='string'){
statusMap = JSON.parse(message.statusMap)
}
return toSnake({
...message,
statusMap,
chat: chat ? chatToJson(chat) : null,
})
}
const contactToJson = (contact) => toSnake(contact.dataValues||contact)
const inviteToJson = (invite) => toSnake(invite.dataValues||invite)
const jsonToContact = (json) => toCamel(json)
function subscriptionToJson(subscription, chat) {
const sub = subscription.dataValues || subscription
const { interval, next } = cronUtils.parse(sub.cron)
return toSnake({
...sub,
interval,
next,
chat: chat ? chatToJson(chat) : null,
})
}
export {
messageToJson,
contactToJson,
inviteToJson,
jsonToContact,
chatToJson,
subscriptionToJson,
}

153
api/utils/ldat.ts

@ -0,0 +1,153 @@
import * as zbase32 from './zbase32'
import {signBuffer} from './lightning'
const env = process.env.NODE_ENV || 'development'
const config = require(__dirname + '/../../config/app.json')[env]
/*
Lightning Data Access Token
Base64 strings separated by dots:
{host}.{muid}.{buyerPubKey}.{exp}.{metadata}.{signature}
- host: web host for data (ascii->base64)
- muid: ID of media
- buyerPubKey
- exp: unix timestamp expiration (encoded into 4 bytes)
- meta: key/value pairs, url query encoded (alphabetically ordered, ascii->base64)
- signature of all that (concatenated bytes of each)
*/
async function tokenFromTerms({host,muid,ttl,pubkey,meta}){
const theHost = host || config.media_host || ''
const pubkeyBytes = Buffer.from(pubkey, 'hex')
const pubkey64 = urlBase64FromBytes(pubkeyBytes)
const now = Math.floor(Date.now()/1000)
const exp = ttl ? now + (60*60*24*365) : 0
const ldat = startLDAT(theHost,muid,pubkey64,exp,meta)
if(pubkey!=''){
const sig = await signBuffer(ldat.bytes)
const sigBytes = zbase32.decode(sig)
return ldat.terms + "." + urlBase64FromBytes(sigBytes)
} else {
return ldat.terms
}
}
// host.muid.pk.exp.meta
function startLDAT(host:string,muid:string,pk:string,exp:number,meta:{[k:string]:any}={}){
const empty = Buffer.from([])
var hostBuf = Buffer.from(host, 'ascii')
var muidBuf = Buffer.from(muid, 'base64')
var pkBuf = pk ? Buffer.from(pk, 'base64') : empty
var expBuf = exp ? Buffer.from(exp.toString(16), 'hex') : empty
var metaBuf = meta ? Buffer.from(serializeMeta(meta), 'ascii') : empty
const totalLength = hostBuf.length + muidBuf.length + pkBuf.length + expBuf.length + metaBuf.length
const buf = Buffer.concat([hostBuf, muidBuf, pkBuf, expBuf, metaBuf], totalLength)
let terms = `${urlBase64(hostBuf)}.${urlBase64(muidBuf)}.${urlBase64(pkBuf)}.${urlBase64(expBuf)}.${urlBase64(metaBuf)}`
return {terms, bytes: buf}
}
const termKeys = [{
key:'host',
func: buf=> buf.toString('ascii')
},{
key:'muid',
func: buf=> urlBase64(buf)
},{
key:'pubkey',
func: buf=> buf.toString('hex')
},{
key:'ts',
func: buf=> parseInt('0x' + buf.toString('hex'))
},{
key:'meta',
func: buf=> {
const ascii = buf.toString('ascii')
return ascii?deserializeMeta(ascii):{} // parse this
}
},{
key:'sig',
func: buf=> urlBase64(buf)
}]
function parseLDAT(ldat){
const a = ldat.split('.')
const o: {[k:string]:any} = {}
termKeys.forEach((t,i)=>{
if(a[i]) o[t.key] = t.func(Buffer.from(a[i], 'base64'))
})
return o
}
export {
startLDAT, parseLDAT, tokenFromTerms,
urlBase64, urlBase64FromAscii,
urlBase64FromBytes, testLDAT
}
async function testLDAT(){
console.log('testLDAT')
const terms = {
host:'',
ttl:31536000, //one year
muid:'qFSOa50yWeGSG8oelsMvctLYdejPRD090dsypBSx_xg=',
pubkey:'0373ca36a331d8fd847f190908715a34997b15dc3c5d560ca032cf3412fcf494e4',
meta:{
amt:100,
ttl:31536000,
dim:'1500x1300'
}
}
const token = await tokenFromTerms(terms)
console.log(token)
const terms2 = {
host:'',
ttl:0, //one year
muid:'qFSOa50yWeGSG8oelsMvctLYdejPRD090dsypBSx_xg=',
pubkey:'',
meta:{
amt:100,
ttl:31536000,
}
}
const token2 = await tokenFromTerms(terms2)
console.log(token2)
console.log(parseLDAT(token2))
}
function serializeMeta(obj) {
var str: string[] = []
for (var p in obj) {
if (obj.hasOwnProperty(p)) {
str.push(encodeURIComponent(p) + "=" + encodeURIComponent(obj[p]));
}
}
str.sort((a,b)=>(a > b ? 1 : -1))
return str.join("&");
}
function deserializeMeta(str){
const json = str && str.length>2 ? JSON.parse('{"' + str.replace(/&/g, '","').replace(/=/g,'":"') + '"}', function(key, value) { return key===""?value:decodeURIComponent(value) }) : {}
const ret = {}
for (let [k, v] of Object.entries(json)) {
const value = (typeof v==='string' && parseInt(v)) || v
ret[k] = value
}
return ret
}
function urlBase64(buf){
return buf.toString('base64').replace(/\//g, '_').replace(/\+/g, '-')
}
function urlBase64FromBytes(buf){
return Buffer.from(buf).toString('base64').replace(/\//g, '_').replace(/\+/g, '-')
}
function urlBase64FromAscii(ascii){
return Buffer.from(ascii,'ascii').toString('base64').replace(/\//g, '_').replace(/\+/g, '-')
}

321
api/utils/lightning.ts

@ -0,0 +1,321 @@
import * as ByteBuffer from 'bytebuffer'
import * as fs from 'fs'
import * as grpc from 'grpc'
import { sleep } from '../helpers';
import * as sha from 'js-sha256'
import * as crypto from 'crypto'
// var protoLoader = require('@grpc/proto-loader')
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/app.json')[env];
const LND_KEYSEND_KEY = 5482373484
const SPHINX_CUSTOM_RECORD_KEY = 133773310
var lightningClient = <any> null;
var walletUnlocker = <any> null;
const loadCredentials = () => {
var lndCert = fs.readFileSync(config.tls_location);
var sslCreds = grpc.credentials.createSsl(lndCert);
var m = fs.readFileSync(config.macaroon_location);
var macaroon = m.toString('hex');
var metadata = new grpc.Metadata()
metadata.add('macaroon', macaroon)
var macaroonCreds = grpc.credentials.createFromMetadataGenerator((_args, callback) => {
callback(null, metadata);
});
return grpc.credentials.combineChannelCredentials(sslCreds, macaroonCreds);
}
// async function loadLightningNew() {
// if (lightningClient) {
// return lightningClient
// } else {
// var credentials = loadCredentials()
// const packageDefinition = await protoLoader.load("rpc.proto", {})
// const lnrpcDescriptor = grpc.loadPackageDefinition(packageDefinition);
// var { lnrpc } = lnrpcDescriptor;
// lightningClient = new lnrpc.Lightning(config.node_ip + ':' + config.lnd_port, credentials);
// return lightningClient
// }
// }
const loadLightning = () => {
if (lightningClient) {
return lightningClient
} else {
try{
var credentials = loadCredentials()
var lnrpcDescriptor = grpc.load("rpc.proto");
var lnrpc: any = lnrpcDescriptor.lnrpc
lightningClient = new lnrpc.Lightning(config.node_ip + ':' + config.lnd_port, credentials);
return lightningClient
} catch(e) {
throw e
}
}
}
const loadWalletUnlocker = () => {
if (walletUnlocker) {
return walletUnlocker
} else {
var credentials = loadCredentials()
try{
var lnrpcDescriptor = grpc.load("rpc.proto");
var lnrpc: any = lnrpcDescriptor.lnrpc
walletUnlocker = new lnrpc.WalletUnlocker(config.node_ip + ':' + config.lnd_port, credentials);
return walletUnlocker
} catch(e) {
console.log(e)
}
}
}
const getHeaders = (req) => {
return {
"X-User-Token": req.headers['x-user-token'],
"X-User-Email": req.headers['x-user-email']
}
}
var isLocked = false
let lockTimeout: ReturnType<typeof setTimeout>;
const getLock = () => isLocked
const setLock = (value) => {
isLocked = value
console.log({ isLocked })
if (lockTimeout) clearTimeout(lockTimeout)
lockTimeout = setTimeout(() => {
isLocked = false
console.log({ isLocked })
}, 1000 * 60 * 2)
}
const getRoute = async (pub_key, amt, callback) => {
let lightning = await loadLightning()
lightning.queryRoutes(
{ pub_key, amt },
(err, response) => callback(err, response)
)
}
const keysend = (opts) => {
return new Promise(async function(resolve, reject) {
let lightning = await loadLightning()
const randoStr = crypto.randomBytes(32).toString('hex');
const preimage = ByteBuffer.fromHex(randoStr)
const options = {
amt: opts.amt,
final_cltv_delta: 10,
dest: ByteBuffer.fromHex(opts.dest),
dest_custom_records: {
[`${LND_KEYSEND_KEY}`]: preimage,
[`${SPHINX_CUSTOM_RECORD_KEY}`]: ByteBuffer.fromUTF8(opts.data),
},
payment_hash: sha.sha256.arrayBuffer(preimage.toBuffer()),
dest_features:[9],
}
const call = lightning.sendPayment()
call.on('data', function(payment) {
if(payment.payment_error){
reject(payment.payment_error)
} else {
resolve(payment)
}
})
call.on('error', function(err) {
reject(err)
})
call.write(options)
})
}
const MAX_MSG_LENGTH = 972 // 1146 - 20
async function keysendMessage(opts) {
return new Promise(async function(resolve, reject) {
if(!opts.data || typeof opts.data!=='string') {
return reject('string plz')
}
if(opts.data.length<MAX_MSG_LENGTH){
try {
const res = await keysend(opts)
resolve(res)
} catch(e) {
reject(e)
}
return
}
// too long! need to send serial
const n = Math.ceil(opts.data.length / MAX_MSG_LENGTH)
let success = false
let fail = false
let res:any = null
const ts = new Date().valueOf()
await asyncForEach(Array.from(Array(n)), async(u,i)=> {
const spliti = Math.ceil(opts.data.length/n)
const m = opts.data.substr(i*spliti, spliti)
try {
res = await keysend({...opts,
data: `${ts}_${i}_${n}_${m}`
})
success = true
await sleep(432)
} catch(e) {
console.log(e)
fail = true
}
})
if(success && !fail) {
resolve(res)
} else {
reject(new Error('fail'))
}
})
}
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
await callback(array[index], index, array);
}
}
async function signAscii(ascii) {
try {
const sig = await signMessage(ascii_to_hexa(ascii))
return sig
} catch(e) {
throw e
}
}
function listInvoices() {
return new Promise(async(resolve, reject)=> {
const lightning = await loadLightning()
lightning.listInvoices({
num_max_invoices:100000,
reversed:true,
}, (err, response) => {
if(!err) {
resolve(response)
} else {
reject(err)
}
});
})
}
function listPayments() {
return new Promise(async(resolve, reject)=> {
const lightning = await loadLightning()
lightning.listPayments({}, (err, response) => {
if(!err) {
resolve(response)
} else {
reject(err)
}
});
})
}
const signMessage = (msg) => {
return new Promise(async(resolve, reject)=> {
let lightning = await loadLightning()
try {
const options = {msg:ByteBuffer.fromHex(msg)}
lightning.signMessage(options, function(err,sig){
if(err || !sig.signature) {
reject(err)
} else {
resolve(sig.signature)
}
})
} catch(e) {
reject(e)
}
})
}
const signBuffer = (msg) => {
return new Promise(async (resolve, reject)=> {
let lightning = await loadLightning()
try {
const options = {msg}
lightning.signMessage(options, function(err,sig){
if(err || !sig.signature) {
reject(err)
} else {
resolve(sig.signature)
}
})
} catch(e) {
reject(e)
}
})
}
const verifyMessage = (msg,sig) => {
return new Promise(async(resolve, reject)=> {
let lightning = await loadLightning()
try {
const options = {
msg:ByteBuffer.fromHex(msg),
signature:sig,
}
console.log(options)
lightning.verifyMessage(options, function(err,res){
if(err || !res.pubkey) {
reject(err)
} else {
resolve(res)
}
})
} catch(e) {
reject(e)
}
})
}
async function checkConnection(){
return new Promise((resolve,reject)=>{
const lightning = loadLightning()
lightning.getInfo({}, function(err, response) {
if (err == null) {
resolve(response)
} else {
reject(err)
}
});
})
}
function ascii_to_hexa(str){
var arr1 = <string[]> [];
for (var n = 0, l = str.length; n < l; n ++) {
var hex = Number(str.charCodeAt(n)).toString(16);
arr1.push(hex);
}
return arr1.join('');
}
export {
loadCredentials,
loadLightning,
loadWalletUnlocker,
getHeaders,
getLock,
setLock,
getRoute,
keysendMessage,
signMessage,
verifyMessage,
signAscii,
signBuffer,
LND_KEYSEND_KEY,
SPHINX_CUSTOM_RECORD_KEY,
listInvoices,
listPayments,
checkConnection,
}

5
api/utils/lock.ts

@ -0,0 +1,5 @@
import * as AsyncLock from 'async-lock'
const lock = new AsyncLock()
export default lock

28
api/utils/logger.ts

@ -0,0 +1,28 @@
import * as expressWinston from 'express-winston'
import * as winston from 'winston'
import * as moment from 'moment'
const tsFormat = (ts) => moment(ts).format('YYYY-MM-DD hh:mm:ss').trim();
const logger = expressWinston.logger({
transports: [
new winston.transports.Console()
],
format: winston.format.combine(
winston.format.timestamp(),
winston.format.colorize(),
winston.format.printf(info=>{
return `-> ${tsFormat(info.timestamp)}: ${info.message}`
})
),
meta: false, // optional: control whether you want to log the meta data about the request (default to true)
// msg: "HTTP {{req.method}} {{req.url}}", // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}"
expressFormat: true, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true
colorize: true, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red).
ignoreRoute: function (req, res) {
if(req.path.startsWith('/json')) return true // debugger
return false;
} // optional: allows to skip some log messages based on request and/or response
})
export default logger

79
api/utils/msg.ts

@ -0,0 +1,79 @@
import { tokenFromTerms } from './ldat'
function addInRemoteText(full:{[k:string]:any}, contactId){
const m = full && full.message
if (!(m && m.content)) return full
if (!(typeof m.content==='object')) return full
return fillmsg(full, {content: m.content[contactId+'']})
}
function removeRecipientFromChatMembers(full:{[k:string]:any}, destkey){
const c = full && full.chat
if (!(c && c.members)) return full
if (!(typeof c.members==='object')) return full
const members = {...c.members}
if(members[destkey]) delete members[destkey]
return fillchatmsg(full, {members})
}
function addInMediaKey(full:{[k:string]:any}, contactId){
const m = full && full.message
if (!(m && m.mediaKey)) return full
if (!(m && m.mediaTerms)) return full
const mediaKey = m.mediaTerms.skipSigning ? '' : m.mediaKey[contactId+'']
return fillmsg(full, {mediaKey})
}
// add the token if its free, but if a price just the base64(host).muid
async function finishTermsAndReceipt(full:{[k:string]:any}, destkey) {
const m = full && full.message
if (!(m && m.mediaTerms)) return full
const t = m.mediaTerms
const meta = t.meta || {}
t.ttl = t.ttl || 31536000
meta.ttl = t.ttl
const mediaToken = await tokenFromTerms({
host: t.host || '',
muid: t.muid,
ttl: t.skipSigning ? 0 : t.ttl,
pubkey: t.skipSigning ? '' : destkey,
meta
})
const fullmsg = fillmsg(full, {mediaToken})
delete fullmsg.message.mediaTerms
return fullmsg
}
async function personalizeMessage(m,contactId,destkey){
const cloned = JSON.parse(JSON.stringify(m))
const msg = addInRemoteText(cloned, contactId)
const cleanMsg = removeRecipientFromChatMembers(msg, destkey)
const msgWithMediaKey = addInMediaKey(cleanMsg, contactId)
const finalMsg = await finishTermsAndReceipt(msgWithMediaKey, destkey)
return finalMsg
}
function fillmsg(full, props){
return {
...full, message: {
...full.message,
...props,
}
}
}
function fillchatmsg(full, props){
return {
...full, chat: {
...full.chat,
...props,
}
}
}
export {
personalizeMessage
}

84
api/utils/nodeinfo.ts

@ -0,0 +1,84 @@
import {loadLightning} from '../utils/lightning'
import * as publicIp from 'public-ip'
import {checkTag, checkCommitHash} from '../utils/gitinfo'
import {models} from '../models'
function nodeinfo(){
return new Promise(async (resolve, reject)=>{
let public_ip = ""
try {
public_ip = await publicIp.v4()
} catch(e){
console.log(e)
}
const commitHash = await checkCommitHash()
const tag = await checkTag()
const lightning = loadLightning()
const owner = await models.Contact.findOne({ where: { isOwner: true }})
const clean = await isClean()
lightning.channelBalance({}, (err, channelBalance) => {
if(err) console.log(err)
// const { balance, pending_open_balance } = channelBalance
lightning.listChannels({}, (err, channelList) => {
if(err) console.log(err)
const { channels } = channelList
const localBalances = channels.map(c => c.local_balance)
const remoteBalances = channels.map(c => c.remote_balance)
const largestLocalBalance = Math.max(...localBalances)
const largestRemoteBalance = Math.max(...remoteBalances)
const totalLocalBalance = localBalances.reduce((a, b) => parseInt(a) + parseInt(b), 0)
lightning.pendingChannels({}, (err, pendingChannels) => {
if(err) console.log(err)
lightning.getInfo({}, (err, info) => {
if(err) console.log(err)
if(!err && info){
const node = {
node_alias: process.env.NODE_ALIAS,
ip: process.env.NODE_IP,
relay_commit: commitHash,
public_ip: public_ip,
pubkey: owner.publicKey,
number_channels: channels.length,
number_active_channels: info.num_active_channels,
number_pending_channels: info.num_pending_channels,
number_peers: info.num_peers,
largest_local_balance: largestLocalBalance,
largest_remote_balance: largestRemoteBalance,
total_local_balance: totalLocalBalance,
lnd_version: info.version,
relay_version: tag,
payment_channel: '', // ?
hosting_provider: '', // ?
open_channel_data: channels,
pending_channel_data: pendingChannels,
synced_to_chain: info.synced_to_chain,
synced_to_graph: info.synced_to_graph,
best_header_timestamp: info.best_header_timestamp,
testnet: info.testnet,
clean,
}
resolve(node)
}
})
})
})
});
})
}
export {nodeinfo}
async function isClean(){
// has owner but with no auth token
const cleanOwner = await models.Contact.findOne({ where: { isOwner: true, authToken: null }})
if(cleanOwner) return true
return false
}

19
api/utils/res.ts

@ -0,0 +1,19 @@
function success(res, json) {
res.status(200);
res.json({
success: true,
response: json,
});
res.end();
}
function failure(res, e) {
res.status(400);
res.json({
success: false,
error: (e&&e.message) || e,
});
res.end();
}
export {success, failure}

86
api/utils/setup.ts

@ -0,0 +1,86 @@
import { loadLightning } from './lightning'
import {sequelize, models} from '../models'
import { exec } from 'child_process'
const USER_VERSION = 1
const setupDatabase = async () => {
console.log('=> [db] starting setup...')
await setVersion()
try {
await sequelize.sync()
console.log("=> [db] done syncing")
} catch(e) {
console.log("db sync failed",e)
}
await migrate()
setupOwnerContact()
console.log('=> [db] setup done')
}
async function setVersion(){
try {
await sequelize.query(`PRAGMA user_version = ${USER_VERSION}`)
} catch(e) {
console.log('=> setVersion failed',e)
}
}
async function migrate(){
try {
await sequelize.query(`update sphinx_chats SET deleted=false where deleted is null`)
} catch(e) {
console.log('=> migrate failed',e)
}
}
const setupOwnerContact = async () => {
const owner = await models.Contact.findOne({ where: { isOwner: true }})
if (!owner) {
const lightning = await loadLightning()
lightning.getInfo({}, async (err, info) => {
if (err) {
console.log('[db] error creating node owner due to lnd failure', err)
} else {
try {
const one = await models.Contact.findOne({ where: { id: 1 }})
if(!one){
const contact = await models.Contact.create({
id: 1,
publicKey: info.identity_pubkey,
isOwner: true,
authToken: null
})
console.log('[db] created node owner contact, id:', contact.id)
}
} catch(error) {
console.log('[db] error creating owner contact', error)
}
}
})
}
}
const runMigrations = async () => {
await new Promise((resolve, reject) => {
const migrate: any = exec('node_modules/.bin/sequelize db:migrate',
{env: process.env},
(err, stdout, stderr) => {
if (err) {
reject(err);
} else {
resolve();
}
}
);
// Forward stdout+stderr to this process
migrate.stdout.pipe(process.stdout);
migrate.stderr.pipe(process.stderr);
});
}
export { setupDatabase, setupOwnerContact, runMigrations }

33
api/utils/socket.ts

@ -0,0 +1,33 @@
import * as WebSocket from 'ws'
let connections = new Map()
let connectionCounter = 0
const connect = (server) => {
server = new WebSocket.Server({ server })
console.log('[socket] connected to server')
server.on('connection', socket => {
console.log('[socket] connection received')
var id = connectionCounter++;
connections.set(id, socket)
})
}
const send = (body) => {
connections.forEach((socket, index) => {
socket.send(body)
})
}
const sendJson = (object) => {
send(JSON.stringify(object))
}
export {
connect,
send,
sendJson
}

12
api/utils/zbase32/index.ts

@ -0,0 +1,12 @@
import './tv42_zbase32_gopherjs'
function encode(b){
return global['zbase32'].Encode(b)
}
function decode(txt){
return global['zbase32'].Decode(txt)
}
export {
encode,
decode,
}

23035
api/utils/zbase32/tv42_zbase32_gopherjs.js

File diff suppressed because one or more lines are too long

114
app.ts

@ -0,0 +1,114 @@
import * as express from 'express'
import * as bodyParser from 'body-parser'
import * as helmet from 'helmet'
import * as cookieParser from 'cookie-parser'
import * as crypto from 'crypto'
import {models} from './api/models'
import logger from './api/utils/logger'
import {pingHubInterval, checkInvitesHubInterval} from './api/hub'
import {setupDatabase} from './api/utils/setup'
import * as controllers from './api/controllers'
import * as socket from './api/utils/socket'
let server: any = null
const port = process.env.PORT || 3001;
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/config/app.json')[env];
process.env.GRPC_SSL_CIPHER_SUITES = 'HIGH+ECDSA'
var i = 0
// START SETUP!
connectToLND()
async function connectToLND(){
i++
console.log(`=> [lnd] connecting... attempt #${i}`)
try {
await controllers.iniGrpcSubscriptions()
mainSetup()
} catch(e) {
setTimeout(async()=>{ // retry each 2 secs
await connectToLND()
},2000)
}
}
async function mainSetup(){
await setupDatabase();
if (config.hub_api_url) {
pingHubInterval(5000)
checkInvitesHubInterval(5000)
}
await setupApp()
}
async function setupApp(){
const app = express();
const server = require("http").Server(app);
app.use(helmet());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(logger)
app.options('*', (req, res) => res.send(200));
app.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type, Accept');
res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate');
res.setHeader('Expires', '-1');
res.setHeader('Pragma', 'no-cache');
next();
});
app.use(cookieParser())
if (env != 'development') {
app.use(authModule);
}
app.use('/static', express.static('public'));
app.get('/app', (req, res) => res.sendFile(__dirname + '/public/index.html'))
server.listen(port, (err) => {
if (err) throw err;
/* eslint-disable no-console */
console.log(`Node listening on ${port}.`);
});
controllers.set(app);
socket.connect(server)
}
async function authModule(req, res, next) {
if (
req.path == '/app' ||
req.path == '/' ||
req.path == '/info' ||
req.path == '/contacts/tokens' ||
req.path == '/login' ||
req.path.startsWith('/static') ||
req.path == '/contacts/set_dev'
) {
next()
return
}
const token = req.headers['x-user-token'] || req.cookies['x-user-token']
if (token == null) {
res.writeHead(401, 'Access invalid for user', {'Content-Type' : 'text/plain'});
res.end('Invalid credentials');
} else {
const user = await models.Contact.findOne({ where: { isOwner: true }})
const hashedToken = crypto.createHash('sha256').update(token).digest('base64');
if (user.authToken == null || user.authToken != hashedToken) {
res.writeHead(401, 'Access invalid for user', {'Content-Type' : 'text/plain'});
res.end('Invalid credentials');
} else {
next();
}
}
}
export default server

32
config/app.json

@ -0,0 +1,32 @@
{
"development": {
"senza_url": "http://localhost:3000/api/v2",
"macaroon_location": "/Users/evanfeenstra/code/lnd-dev/alice/data/chain/bitcoin/simnet/admin.macaroon",
"tls_location": "/Users/evanfeenstra/Library/Application Support/Lnd/tls.cert",
"node_ip": "127.0.0.1",
"lnd_port": "10001",
"node_http_protocol": "http",
"node_http_port": "3001",
"hub_api_url": "http://lvh.me/api/v1",
"hub_url": "http://lvh.me/ping",
"hub_invite_url": "http://lvh.me/invites",
"hub_check_invite_url": "http://lvh.me/check_invite",
"media_host": "localhost:5000"
},
"production": {
"senza_url": "https://staging.senza.us/api/v2/",
"macaroon_location": "/home/ubuntu/.lnd/data/chain/bitcoin/mainnet/admin.macaroon",
"tls_location": "/home/ubuntu/.lnd/tls.cert",
"lnd_log_location": "/home/ubuntu/.lnd/logs/bitcoin/mainnet/lnd.log",
"lncli_location": "/home/ubuntu/go/bin",
"node_ip": "localhost",
"node_http_protocol": "http",
"node_http_port": "80",
"lnd_port": "10009",
"hub_api_url": "http://hub.sphinx.chat/api/v1",
"hub_url": "http://hub.sphinx.chat/ping",
"hub_invite_url": "http://hub.sphinx.chat/invites",
"hub_check_invite_url": "http://hub.sphinx.chat/check_invite",
"media_host": "memes.sphinx.chat"
}
}

18
config/config.json

@ -0,0 +1,18 @@
{
"development": {
"dialect": "sqlite",
"storage": "/Users/Shared/sphinx.db"
},
"docker_development": {
"dialect": "sqlite",
"storage": "./sphinx.db"
},
"test": {
"dialect": "sqlite",
"storage": "/home/ubuntu/sphinx.db"
},
"production": {
"dialect": "sqlite",
"storage": "/home/ubuntu/sphinx.db"
}
}

52
config/constants.json

@ -0,0 +1,52 @@
{
"invite_statuses": {
"pending": 0,
"ready": 1,
"delivered": 2,
"in_progress": 3,
"complete": 4,
"expired": 5,
"payment_pending": 6
},
"contact_statuses": {
"pending": 0,
"confirmed": 1
},
"statuses": {
"pending": 0,
"confirmed": 1,
"cancelled": 2,
"received": 3,
"failed": 4
},
"message_types": {
"message": 0,
"confirmation": 1,
"invoice": 2,
"payment": 3,
"cancellation": 4,
"direct_payment": 5,
"attachment": 6,
"purchase": 7,
"purchase_accept": 8,
"purchase_deny": 9,
"contact_key": 10,
"contact_key_confirmation": 11,
"group_create": 12,
"group_invite": 13,
"group_join": 14,
"group_leave": 15,
"group_query": 16
},
"payment_errors": {
"timeout": "Timed Out",
"no_route": "No Route To Receiver",
"error": "Error",
"incorrect_payment_details": "Incorrect Payment Details",
"unknown": "Unknown"
},
"chat_types": {
"conversation": 0,
"group": 1
}
}

308
dist/api/controllers/chats.js

@ -0,0 +1,308 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const jsonUtils = require("../utils/json");
const res_1 = require("../utils/res");
const helpers = require("../helpers");
const socket = require("../utils/socket");
const hub_1 = require("../hub");
const md5 = require("md5");
const constants = require(__dirname + '/../../config/constants.json');
function getChats(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const chats = yield models_1.models.Chat.findAll({ where: { deleted: false }, raw: true });
const c = chats.map(chat => jsonUtils.chatToJson(chat));
res_1.success(res, c);
});
}
exports.getChats = getChats;
function mute(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const chatId = req.params['chat_id'];
const mute = req.params['mute_unmute'];
if (!["mute", "unmute"].includes(mute)) {
return res_1.failure(res, "invalid option for mute");
}
const chat = yield models_1.models.Chat.findOne({ where: { id: chatId } });
if (!chat) {
return res_1.failure(res, 'chat not found');
}
chat.update({ isMuted: (mute == "mute") });
res_1.success(res, jsonUtils.chatToJson(chat));
});
}
exports.mute = mute;
function createGroupChat(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const { name, contact_ids, } = req.body;
const members = {}; //{pubkey:{key,alias}, ...}
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
members[owner.publicKey] = {
key: owner.contactKey, alias: owner.alias
};
yield asyncForEach(contact_ids, (cid) => __awaiter(this, void 0, void 0, function* () {
const contact = yield models_1.models.Contact.findOne({ where: { id: cid } });
members[contact.publicKey] = {
key: contact.contactKey,
alias: contact.alias || ''
};
}));
const chatParams = createGroupChatParams(owner, contact_ids, members, name);
helpers.sendMessage({
chat: Object.assign(Object.assign({}, chatParams), { members }),
sender: owner,
type: constants.message_types.group_create,
message: {},
failure: function (e) {
res_1.failure(res, e);
},
success: function () {
return __awaiter(this, void 0, void 0, function* () {
const chat = yield models_1.models.Chat.create(chatParams);
res_1.success(res, jsonUtils.chatToJson(chat));
});
}
});
});
}
exports.createGroupChat = createGroupChat;
function addGroupMembers(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const { contact_ids, } = req.body;
const { id } = req.params;
const members = {}; //{pubkey:{key,alias}, ...}
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
let chat = yield models_1.models.Chat.findOne({ where: { id } });
const contactIds = JSON.parse(chat.contactIds || '[]');
// for all members (existing and new)
members[owner.publicKey] = { key: owner.contactKey, alias: owner.alias };
const allContactIds = contactIds.concat(contact_ids);
yield asyncForEach(allContactIds, (cid) => __awaiter(this, void 0, void 0, function* () {
const contact = yield models_1.models.Contact.findOne({ where: { id: cid } });
if (contact) {
members[contact.publicKey] = {
key: contact.contactKey,
alias: contact.alias
};
}
}));
res_1.success(res, jsonUtils.chatToJson(chat));
helpers.sendMessage({
chat: Object.assign(Object.assign({}, chat.dataValues), { contactIds: contact_ids, members }),
sender: owner,
type: constants.message_types.group_invite,
message: {}
});
});
}
exports.addGroupMembers = addGroupMembers;
const deleteChat = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { id } = req.params;
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const chat = yield models_1.models.Chat.findOne({ where: { id } });
helpers.sendMessage({
chat,
sender: owner,
message: {},
type: constants.message_types.group_leave,
});
yield chat.update({
deleted: true,
uuid: '',
contactIds: '[]',
name: ''
});
yield models_1.models.Message.destroy({ where: { chatId: id } });
res_1.success(res, { chat_id: id });
});
exports.deleteChat = deleteChat;
function receiveGroupLeave(payload) {
return __awaiter(this, void 0, void 0, function* () {
console.log('=> receiveGroupLeave');
const { sender_pub_key, chat_uuid } = yield helpers.parseReceiveParams(payload);
const chat = yield models_1.models.Chat.findOne({ where: { uuid: chat_uuid } });
if (!chat)
return;
const sender = yield models_1.models.Contact.findOne({ where: { publicKey: sender_pub_key } });
if (!sender)
return;
const oldContactIds = JSON.parse(chat.contactIds || '[]');
const contactIds = oldContactIds.filter(cid => cid !== sender.id);
yield chat.update({ contactIds: JSON.stringify(contactIds) });
var date = new Date();
date.setMilliseconds(0);
const msg = {
chatId: chat.id,
type: constants.message_types.group_leave,
sender: sender.id,
date: date,
messageContent: '',
remoteMessageContent: '',
status: constants.statuses.confirmed,
createdAt: date,
updatedAt: date
};
const message = yield models_1.models.Message.create(msg);
socket.sendJson({
type: 'group_leave',
response: {
contact: jsonUtils.contactToJson(sender),
chat: jsonUtils.chatToJson(chat),
message: jsonUtils.messageToJson(message, null)
}
});
});
}
exports.receiveGroupLeave = receiveGroupLeave;
function receiveGroupJoin(payload) {
return __awaiter(this, void 0, void 0, function* () {
console.log('=> receiveGroupJoin');
const { sender_pub_key, chat_uuid, chat_members } = yield helpers.parseReceiveParams(payload);
const chat = yield models_1.models.Chat.findOne({ where: { uuid: chat_uuid } });
if (!chat)
return;
let theSender = null;
const sender = yield models_1.models.Contact.findOne({ where: { publicKey: sender_pub_key } });
const contactIds = JSON.parse(chat.contactIds || '[]');
if (sender) {
theSender = sender; // might already include??
if (!contactIds.includes(sender.id))
contactIds.push(sender.id);
}
else {
const member = chat_members[sender_pub_key];
if (member && member.key) {
const createdContact = yield models_1.models.Contact.create({
publicKey: sender_pub_key,
contactKey: member.key,
alias: member.alias || 'Unknown',
status: 1
});
theSender = createdContact;
contactIds.push(createdContact.id);
}
}
yield chat.update({ contactIds: JSON.stringify(contactIds) });
var date = new Date();
date.setMilliseconds(0);
const msg = {
chatId: chat.id,
type: constants.message_types.group_join,
sender: sender.id,
date: date,
messageContent: '',
remoteMessageContent: '',
status: constants.statuses.confirmed,
createdAt: date,
updatedAt: date
};
const message = yield models_1.models.Message.create(msg);
socket.sendJson({
type: 'group_join',
response: {
contact: jsonUtils.contactToJson(theSender),
chat: jsonUtils.chatToJson(chat),
message: jsonUtils.messageToJson(message, null)
}
});
});
}
exports.receiveGroupJoin = receiveGroupJoin;
function receiveGroupCreateOrInvite(payload) {
return __awaiter(this, void 0, void 0, function* () {
const { chat_members, chat_name, chat_uuid } = yield helpers.parseReceiveParams(payload);
const contactIds = [];
const newContacts = [];
for (let [pubkey, member] of Object.entries(chat_members)) {
const contact = yield models_1.models.Contact.findOne({ where: { publicKey: pubkey } });
if (!contact && member && member.key) {
const createdContact = yield models_1.models.Contact.create({
publicKey: pubkey,
contactKey: member.key,
alias: member.alias || 'Unknown',
status: 1
});
contactIds.push(createdContact.id);
newContacts.push(createdContact.dataValues);
}
else {
contactIds.push(contact.id);
}
}
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
if (!contactIds.includes(owner.id))
contactIds.push(owner.id);
// make chat
let date = new Date();
date.setMilliseconds(0);
const chat = yield models_1.models.Chat.create({
uuid: chat_uuid,
contactIds: JSON.stringify(contactIds),
createdAt: date,
updatedAt: date,
name: chat_name,
type: constants.chat_types.group
});
socket.sendJson({
type: 'group_create',
response: jsonUtils.messageToJson({ newContacts }, chat)
});
hub_1.sendNotification(chat, chat_name, 'group');
if (payload.type === constants.message_types.group_invite) {
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
helpers.sendMessage({
chat: Object.assign(Object.assign({}, chat.dataValues), { members: {
[owner.publicKey]: {
key: owner.contactKey,
alias: owner.alias || ''
}
} }),
sender: owner,
message: {},
type: constants.message_types.group_join,
});
}
});
}
exports.receiveGroupCreateOrInvite = receiveGroupCreateOrInvite;
function createGroupChatParams(owner, contactIds, members, name) {
let date = new Date();
date.setMilliseconds(0);
if (!(owner && members && contactIds && Array.isArray(contactIds))) {
return;
}
const pubkeys = [];
for (let pubkey of Object.keys(members)) { // just the key
pubkeys.push(String(pubkey));
}
if (!(pubkeys && pubkeys.length))
return;
const allkeys = pubkeys.includes(owner.publicKey) ? pubkeys : [owner.publicKey].concat(pubkeys);
const hash = md5(allkeys.sort().join("-"));
const theContactIds = contactIds.includes(owner.id) ? contactIds : [owner.id].concat(contactIds);
return {
uuid: `${new Date().valueOf()}-${hash}`,
contactIds: JSON.stringify(theContactIds),
createdAt: date,
updatedAt: date,
name: name,
type: constants.chat_types.group
};
}
function asyncForEach(array, callback) {
return __awaiter(this, void 0, void 0, function* () {
for (let index = 0; index < array.length; index++) {
yield callback(array[index], index, array);
}
});
}
//# sourceMappingURL=chats.js.map

1
dist/api/controllers/chats.js.map

File diff suppressed because one or more lines are too long

205
dist/api/controllers/contacts.js

@ -0,0 +1,205 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const crypto = require("crypto");
const socket = require("../utils/socket");
const helpers = require("../helpers");
const jsonUtils = require("../utils/json");
const res_1 = require("../utils/res");
const constants = require(__dirname + '/../../config/constants.json');
const getContacts = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const contacts = yield models_1.models.Contact.findAll({ where: { deleted: false }, raw: true });
const invites = yield models_1.models.Invite.findAll({ raw: true });
const chats = yield models_1.models.Chat.findAll({ where: { deleted: false }, raw: true });
const subscriptions = yield models_1.models.Subscription.findAll({ raw: true });
const contactsResponse = contacts.map(contact => {
let contactJson = jsonUtils.contactToJson(contact);
let invite = invites.find(invite => invite.contactId == contact.id);
if (invite) {
contactJson.invite = jsonUtils.inviteToJson(invite);
}
return contactJson;
});
const subsResponse = subscriptions.map(s => jsonUtils.subscriptionToJson(s, null));
const chatsResponse = chats.map(chat => jsonUtils.chatToJson(chat));
res_1.success(res, {
contacts: contactsResponse,
chats: chatsResponse,
subscriptions: subsResponse
});
});
exports.getContacts = getContacts;
const generateToken = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
console.log('=> generateToken called', { body: req.body, params: req.params, query: req.query });
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true, authToken: null } });
if (owner) {
const hash = crypto.createHash('sha256').update(req.body['token']).digest('base64');
console.log("req.params['token']", req.params['token']);
console.log("hash", hash);
owner.update({ authToken: hash });
res_1.success(res, {});
}
else {
res_1.failure(res, {});
}
});
exports.generateToken = generateToken;
const updateContact = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
console.log('=> updateContact called', { body: req.body, params: req.params, query: req.query });
let attrs = extractAttrs(req.body);
const contact = yield models_1.models.Contact.findOne({ where: { id: req.params.id } });
let shouldUpdateContactKey = (contact.isOwner && contact.contactKey == null && attrs["contact_key"] != null);
const owner = yield contact.update(jsonUtils.jsonToContact(attrs));
res_1.success(res, jsonUtils.contactToJson(owner));
if (!shouldUpdateContactKey) {
return;
}
// definitely "owner" now
const contactIds = yield models_1.models.Contact.findAll({ where: { deleted: false } }).map(c => c.id);
if (contactIds.length == 0) {
return;
}
helpers.sendContactKeys({
contactIds: contactIds,
sender: owner,
type: constants.message_types.contact_key,
});
});
exports.updateContact = updateContact;
const exchangeKeys = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
console.log('=> exchangeKeys called', { body: req.body, params: req.params, query: req.query });
const contact = yield models_1.models.Contact.findOne({ where: { id: req.params.id } });
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
res_1.success(res, jsonUtils.contactToJson(contact));
helpers.sendContactKeys({
contactIds: [contact.id],
sender: owner,
type: constants.message_types.contact_key,
});
});
exports.exchangeKeys = exchangeKeys;
const createContact = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
console.log('=> createContact called', { body: req.body, params: req.params, query: req.query });
let attrs = extractAttrs(req.body);
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const createdContact = yield models_1.models.Contact.create(attrs);
const contact = yield createdContact.update(jsonUtils.jsonToContact(attrs));
res_1.success(res, jsonUtils.contactToJson(contact));
helpers.sendContactKeys({
contactIds: [contact.id],
sender: owner,
type: constants.message_types.contact_key,
});
});
exports.createContact = createContact;
const deleteContact = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const id = parseInt(req.params.id || '0');
if (!id || id === 1) {
res_1.failure(res, 'Cannot delete self');
return;
}
const contact = yield models_1.models.Contact.findOne({ where: { id } });
yield contact.update({
deleted: true,
publicKey: '',
photoUrl: '',
alias: 'Unknown',
contactKey: '',
});
// find and destroy chat & messages
const chats = yield models_1.models.Chat.findAll({ where: { deleted: false } });
chats.map((chat) => __awaiter(void 0, void 0, void 0, function* () {
if (chat.type === constants.chat_types.conversation) {
const contactIds = JSON.parse(chat.contactIds);
if (contactIds.includes(id)) {
yield chat.update({
deleted: true,
uuid: '',
contactIds: '[]',
name: ''
});
yield models_1.models.Message.destroy({ where: { chatId: chat.id } });
}
}
}));
yield models_1.models.Invite.destroy({ where: { contactId: id } });
yield models_1.models.Subscription.destroy({ where: { contactId: id } });
res_1.success(res, {});
});
exports.deleteContact = deleteContact;
const receiveConfirmContactKey = (payload) => __awaiter(void 0, void 0, void 0, function* () {
console.log('=> confirm contact key', { payload });
const dat = payload.content || payload;
const sender_pub_key = dat.sender.pub_key;
const sender_contact_key = dat.sender.contact_key;
const sender_alias = dat.sender.alias || 'Unknown';
const sender_photo_url = dat.sender.photoUrl;
if (sender_photo_url) {
// download and store photo locally
}
const sender = yield models_1.models.Contact.findOne({ where: { publicKey: sender_pub_key, status: constants.contact_statuses.confirmed } });
if (sender_contact_key && sender) {
if (!sender.alias || sender.alias === 'Unknown') {
sender.update({ contactKey: sender_contact_key, alias: sender_alias });
}
else {
sender.update({ contactKey: sender_contact_key });
}
socket.sendJson({
type: 'contact',
response: jsonUtils.contactToJson(sender)
});
}
});
exports.receiveConfirmContactKey = receiveConfirmContactKey;
const receiveContactKey = (payload) => __awaiter(void 0, void 0, void 0, function* () {
console.log('=> received contact key', JSON.stringify(payload));
const dat = payload.content || payload;
const sender_pub_key = dat.sender.pub_key;
const sender_contact_key = dat.sender.contact_key;
const sender_alias = dat.sender.alias || 'Unknown';
const sender_photo_url = dat.sender.photoUrl;
if (sender_photo_url) {
// download and store photo locally
}
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const sender = yield models_1.models.Contact.findOne({ where: { publicKey: sender_pub_key, status: constants.contact_statuses.confirmed } });
if (sender_contact_key && sender) {
if (!sender.alias || sender.alias === 'Unknown') {
sender.update({ contactKey: sender_contact_key, alias: sender_alias });
}
else {
sender.update({ contactKey: sender_contact_key });
}
socket.sendJson({
type: 'contact',
response: jsonUtils.contactToJson(sender)
});
}
helpers.sendContactKeys({
contactPubKey: sender_pub_key,
sender: owner,
type: constants.message_types.contact_key_confirmation,
});
});
exports.receiveContactKey = receiveContactKey;
const extractAttrs = body => {
let fields_to_update = ["public_key", "node_alias", "alias", "photo_url", "device_id", "status", "contact_key"];
let attrs = {};
Object.keys(body).forEach(key => {
if (fields_to_update.includes(key)) {
attrs[key] = body[key];
}
});
return attrs;
};
//# sourceMappingURL=contacts.js.map

1
dist/api/controllers/contacts.js.map

File diff suppressed because one or more lines are too long

133
dist/api/controllers/details.js

@ -0,0 +1,133 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const lightning_1 = require("../utils/lightning");
const res_1 = require("../utils/res");
const readLastLines = require("read-last-lines");
const nodeinfo_1 = require("../utils/nodeinfo");
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/app.json')[env];
const defaultLogFiles = [
'/home/lnd/.pm2/logs/app-error.log',
'/var/log/syslog',
];
function getLogsSince(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const logFiles = config.log_file ? [config.log_file] : defaultLogFiles;
let txt;
let err;
yield asyncForEach(logFiles, (filepath) => __awaiter(this, void 0, void 0, function* () {
if (!txt) {
try {
const lines = yield readLastLines.read(filepath, 500);
if (lines) {
var linesArray = lines.split('\n');
linesArray.reverse();
txt = linesArray.join('\n');
}
}
catch (e) {
err = e;
}
}
}));
if (txt)
res_1.success(res, txt);
else
res_1.failure(res, err);
});
}
exports.getLogsSince = getLogsSince;
const getInfo = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const lightning = lightning_1.loadLightning();
var request = {};
lightning.getInfo(request, function (err, response) {
res.status(200);
if (err == null) {
res.json({ success: true, response });
}
else {
res.json({ success: false });
}
res.end();
});
});
exports.getInfo = getInfo;
const getChannels = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const lightning = lightning_1.loadLightning();
var request = {};
lightning.listChannels(request, function (err, response) {
res.status(200);
if (err == null) {
res.json({ success: true, response });
}
else {
res.json({ success: false });
}
res.end();
});
});
exports.getChannels = getChannels;
const getBalance = (req, res) => {
const lightning = lightning_1.loadLightning();
var request = {};
lightning.channelBalance(request, function (err, response) {
res.status(200);
if (err == null) {
res.json({ success: true, response });
}
else {
res.json({ success: false });
}
res.end();
});
};
exports.getBalance = getBalance;
const getLocalRemoteBalance = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const lightning = lightning_1.loadLightning();
lightning.listChannels({}, (err, channelList) => {
const { channels } = channelList;
const localBalances = channels.map(c => c.local_balance);
const remoteBalances = channels.map(c => c.remote_balance);
const totalLocalBalance = localBalances.reduce((a, b) => parseInt(a) + parseInt(b), 0);
const totalRemoteBalance = remoteBalances.reduce((a, b) => parseInt(a) + parseInt(b), 0);
res.status(200);
if (err == null) {
res.json({ success: true, response: { local_balance: totalLocalBalance, remote_balance: totalRemoteBalance } });
}
else {
res.json({ success: false });
}
res.end();
});
});
exports.getLocalRemoteBalance = getLocalRemoteBalance;
const getNodeInfo = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
var ipOfSource = req.connection.remoteAddress;
if (!(ipOfSource.includes('127.0.0.1') || ipOfSource.includes('localhost'))) {
res.status(401);
res.end();
return;
}
const node = yield nodeinfo_1.nodeinfo();
res.status(200);
res.json(node);
res.end();
});
exports.getNodeInfo = getNodeInfo;
function asyncForEach(array, callback) {
return __awaiter(this, void 0, void 0, function* () {
for (let index = 0; index < array.length; index++) {
yield callback(array[index], index, array);
}
});
}
//# sourceMappingURL=details.js.map

1
dist/api/controllers/details.js.map

@ -0,0 +1 @@
{"version":3,"file":"details.js","sourceRoot":"","sources":["../../../api/controllers/details.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,kDAAgD;AAChD,sCAA+C;AAC/C,iDAAgD;AAChD,gDAA6C;AAE7C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC;AAClD,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,wBAAwB,CAAC,CAAC,GAAG,CAAC,CAAC;AAElE,MAAM,eAAe,GAAG;IACvB,mCAAmC;IACnC,iBAAiB;CACjB,CAAA;AACD,SAAe,YAAY,CAAC,GAAG,EAAE,GAAG;;QACnC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,eAAe,CAAA;QACtE,IAAI,GAAG,CAAA;QACP,IAAI,GAAG,CAAA;QACP,MAAM,YAAY,CAAC,QAAQ,EAAE,CAAM,QAAQ,EAAA,EAAE;YAC5C,IAAG,CAAC,GAAG,EAAC;gBACP,IAAI;oBACH,MAAM,KAAK,GAAG,MAAM,aAAa,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAA;oBACrD,IAAG,KAAK,EAAE;wBACT,IAAI,UAAU,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;wBAClC,UAAU,CAAC,OAAO,EAAE,CAAA;wBACpB,GAAG,GAAG,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;qBAC3B;iBACD;gBAAC,OAAM,CAAC,EAAE;oBACV,GAAG,GAAG,CAAC,CAAA;iBACP;aACD;QACF,CAAC,CAAA,CAAC,CAAA;QACF,IAAG,GAAG;YAAE,aAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;;YACpB,aAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;IACvB,CAAC;CAAA;AAkFA,oCAAY;AAhFb,MAAM,OAAO,GAAG,CAAO,GAAG,EAAE,GAAG,EAAE,EAAE;IAClC,MAAM,SAAS,GAAG,yBAAa,EAAE,CAAA;IACjC,IAAI,OAAO,GAAG,EAAE,CAAA;IAChB,SAAS,CAAC,OAAO,CAAC,OAAO,EAAE,UAAS,GAAG,EAAE,QAAQ;QAChD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,GAAG,IAAI,IAAI,EAAE;YAChB,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;SACtC;aAAM;YACN,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;SAC7B;QACD,GAAG,CAAC,GAAG,EAAE,CAAC;IACX,CAAC,CAAC,CAAC;AACJ,CAAC,CAAA,CAAC;AAgED,0BAAO;AA9DR,MAAM,WAAW,GAAG,CAAO,GAAG,EAAE,GAAG,EAAE,EAAE;IACrC,MAAM,SAAS,GAAG,yBAAa,EAAE,CAAA;IAClC,IAAI,OAAO,GAAG,EAAE,CAAA;IAChB,SAAS,CAAC,YAAY,CAAC,OAAO,EAAE,UAAS,GAAG,EAAE,QAAQ;QACrD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,GAAG,IAAI,IAAI,EAAE;YAChB,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;SACtC;aAAM;YACN,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;SAC7B;QACD,GAAG,CAAC,GAAG,EAAE,CAAC;IACX,CAAC,CAAC,CAAC;AACJ,CAAC,CAAA,CAAC;AAoDD,kCAAW;AAlDZ,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,EAAE;IAC9B,MAAM,SAAS,GAAG,yBAAa,EAAE,CAAA;IAClC,IAAI,OAAO,GAAG,EAAE,CAAA;IAChB,SAAS,CAAC,cAAc,CAAC,OAAO,EAAE,UAAS,GAAG,EAAE,QAAQ;QACvD,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,GAAG,IAAI,IAAI,EAAE;YAChB,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;SACtC;aAAM;YACN,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;SAC7B;QACD,GAAG,CAAC,GAAG,EAAE,CAAC;IACX,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC;AAqCD,gCAAU;AAnCX,MAAM,qBAAqB,GAAG,CAAO,GAAG,EAAE,GAAG,EAAE,EAAE;IAChD,MAAM,SAAS,GAAG,yBAAa,EAAE,CAAA;IACjC,SAAS,CAAC,YAAY,CAAC,EAAE,EAAE,CAAC,GAAG,EAAE,WAAW,EAAE,EAAE;QAC/C,MAAM,EAAE,QAAQ,EAAE,GAAG,WAAW,CAAA;QAEhC,MAAM,aAAa,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,CAAA;QACxD,MAAM,cAAc,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,cAAc,CAAC,CAAA;QAC1D,MAAM,iBAAiB,GAAG,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QACtF,MAAM,kBAAkB,GAAG,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAExF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAChB,IAAI,GAAG,IAAI,IAAI,EAAE;YAChB,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,aAAa,EAAE,iBAAiB,EAAE,cAAc,EAAE,kBAAkB,EAAE,EAAE,CAAC,CAAC;SAChH;aAAM;YACN,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAC;SAC7B;QACD,GAAG,CAAC,GAAG,EAAE,CAAC;IACT,CAAC,CAAC,CAAA;AACL,CAAC,CAAA,CAAC;AAmBD,sDAAqB;AAjBtB,MAAM,WAAW,GAAG,CAAO,GAAG,EAAE,GAAG,EAAE,EAAE;IACtC,IAAI,UAAU,GAAG,GAAG,CAAC,UAAU,CAAC,aAAa,CAAC;IAC9C,IAAG,CAAC,CAAC,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,UAAU,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,EAAC;QAC1E,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACf,GAAG,CAAC,GAAG,EAAE,CAAA;QACT,OAAM;KACN;IACD,MAAM,IAAI,GAAG,MAAM,mBAAQ,EAAE,CAAA;IAC7B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACf,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;IACd,GAAG,CAAC,GAAG,EAAE,CAAA;AACV,CAAC,CAAA,CAAA;AAQA,kCAAW;AAGZ,SAAe,YAAY,CAAC,KAAK,EAAE,QAAQ;;QAC1C,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE;YAChD,MAAM,QAAQ,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;SAC7C;IACF,CAAC;CAAA"}

139
dist/api/controllers/index.js

@ -0,0 +1,139 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const lndService = require("../grpc");
const gitinfo_1 = require("../utils/gitinfo");
const lightning_1 = require("../utils/lightning");
const constants = require(__dirname + '/../../config/constants.json');
const env = process.env.NODE_ENV || 'development';
console.log("=> env:", env);
let controllers = {
messages: require('./messages'),
invoices: require('./invoices'),
uploads: require('./uploads'),
contacts: require('./contacts'),
invites: require('./invites'),
payments: require('./payment'),
details: require('./details'),
chats: require('./chats'),
subcriptions: require('./subscriptions'),
media: require('./media'),
};
function iniGrpcSubscriptions() {
return __awaiter(this, void 0, void 0, function* () {
try {
yield lightning_1.checkConnection();
const types = constants.message_types;
yield lndService.subscribeInvoices({
[types.contact_key]: controllers.contacts.receiveContactKey,
[types.contact_key_confirmation]: controllers.contacts.receiveConfirmContactKey,
[types.message]: controllers.messages.receiveMessage,
[types.invoice]: controllers.invoices.receiveInvoice,
[types.direct_payment]: controllers.payments.receivePayment,
[types.confirmation]: controllers.messages.receiveConfirmation,
[types.attachment]: controllers.media.receiveAttachment,
[types.purchase]: controllers.media.receivePurchase,
[types.purchase_accept]: controllers.media.receivePurchaseAccept,
[types.purchase_deny]: controllers.media.receivePurchaseDeny,
[types.group_create]: controllers.chats.receiveGroupCreateOrInvite,
[types.group_invite]: controllers.chats.receiveGroupCreateOrInvite,
[types.group_join]: controllers.chats.receiveGroupJoin,
[types.group_leave]: controllers.chats.receiveGroupLeave,
});
}
catch (e) {
throw e;
}
});
}
exports.iniGrpcSubscriptions = iniGrpcSubscriptions;
function set(app) {
return __awaiter(this, void 0, void 0, function* () {
if (models_1.models && models_1.models.Subscription) {
controllers.subcriptions.initializeCronJobs();
}
try {
yield controllers.media.cycleMediaToken();
}
catch (e) {
console.log('=> could not auth with media server', e.message);
}
app.get('/chats', controllers.chats.getChats);
app.post('/group', controllers.chats.createGroupChat);
app.post('/chats/:chat_id/:mute_unmute', controllers.chats.mute);
app.delete('/chat/:id', controllers.chats.deleteChat);
app.put('/chat/:id', controllers.chats.addGroupMembers);
app.post('/contacts/tokens', controllers.contacts.generateToken);
app.post('/upload', controllers.uploads.avatarUpload.single('file'), controllers.uploads.uploadFile);
app.post('/invites', controllers.invites.createInvite);
app.post('/invites/:invite_string/pay', controllers.invites.payInvite);
app.post('/invites/finish', controllers.invites.finishInvite);
app.get('/contacts', controllers.contacts.getContacts);
app.put('/contacts/:id', controllers.contacts.updateContact);
app.post('/contacts/:id/keys', controllers.contacts.exchangeKeys);
app.post('/contacts', controllers.contacts.createContact);
app.delete('/contacts/:id', controllers.contacts.deleteContact);
app.get('/messages', controllers.messages.getMessages);
app.post('/messages', controllers.messages.sendMessage);
app.post('/messages/:chat_id/read', controllers.messages.readMessages);
app.post('/messages/clear', controllers.messages.clearMessages);
app.get('/subscriptions', controllers.subcriptions.getAllSubscriptions);
app.get('/subscription/:id', controllers.subcriptions.getSubscription);
app.delete('/subscription/:id', controllers.subcriptions.deleteSubscription);
app.post('/subscriptions', controllers.subcriptions.createSubscription);
app.put('/subscription/:id', controllers.subcriptions.editSubscription);
app.get('/subscriptions/contact/:contactId', controllers.subcriptions.getSubscriptionsForContact);
app.put('/subscription/:id/pause', controllers.subcriptions.pauseSubscription);
app.put('/subscription/:id/restart', controllers.subcriptions.restartSubscription);
app.post('/attachment', controllers.media.sendAttachmentMessage);
app.post('/purchase', controllers.media.purchase);
app.get('/signer/:challenge', controllers.media.signer);
app.post('/invoices', controllers.invoices.createInvoice);
app.get('/invoices', controllers.invoices.listInvoices);
app.put('/invoices', controllers.invoices.payInvoice);
app.post('/invoices/cancel', controllers.invoices.cancelInvoice);
app.post('/payment', controllers.payments.sendPayment);
app.get('/payments', controllers.payments.listPayments);
app.get('/channels', controllers.details.getChannels);
app.get('/balance', controllers.details.getBalance);
app.get('/balance/all', controllers.details.getLocalRemoteBalance);
app.get('/getinfo', controllers.details.getInfo);
app.get('/logs', controllers.details.getLogsSince);
app.get('/info', controllers.details.getNodeInfo);
app.get('/version', function (req, res) {
return __awaiter(this, void 0, void 0, function* () {
const version = yield gitinfo_1.checkTag();
res.send({ version });
});
});
if (env != "production") { // web dashboard login
app.post('/login', login);
}
});
}
exports.set = set;
const login = (req, res) => {
const { code } = req.body;
if (code == "sphinx") {
models_1.models.Contact.findOne({ where: { isOwner: true } }).then(owner => {
res.status(200);
res.json({ success: true, token: owner.authToken });
res.end();
});
}
else {
res.status(200);
res.json({ success: false });
res.end();
}
};
//# sourceMappingURL=index.js.map

1
dist/api/controllers/index.js.map

File diff suppressed because one or more lines are too long

101
dist/api/controllers/invites.js

@ -0,0 +1,101 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const crypto = require("crypto");
const jsonUtils = require("../utils/json");
const hub_1 = require("../hub");
const finishInvite = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { invite_string } = req.body;
const params = {
invite: {
pin: invite_string
}
};
function onSuccess() {
res.status(200);
res.json({ success: true });
res.end();
}
function onFailure() {
res.status(200);
res.json({ success: false });
res.end();
}
hub_1.finishInviteInHub(params, onSuccess, onFailure);
});
exports.finishInvite = finishInvite;
const payInvite = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const params = {
node_ip: process.env.NODE_IP
};
const invite_string = req.params['invite_string'];
const onSuccess = (response) => __awaiter(void 0, void 0, void 0, function* () {
const invite = response.object;
console.log("response", invite);
const dbInvite = yield models_1.models.Invite.findOne({ where: { inviteString: invite.pin } });
if (dbInvite.status != invite.invite_status) {
dbInvite.update({ status: invite.invite_status });
}
res.status(200);
res.json({ success: true, response: { invite: jsonUtils.inviteToJson(dbInvite) } });
res.end();
});
const onFailure = (response) => {
res.status(200);
res.json({ success: false });
res.end();
};
hub_1.payInviteInHub(invite_string, params, onSuccess, onFailure);
});
exports.payInvite = payInvite;
const createInvite = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { nickname, welcome_message } = req.body;
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const params = {
invite: {
nickname: owner.alias,
pubkey: owner.publicKey,
contact_nickname: nickname,
message: welcome_message,
pin: crypto.randomBytes(20).toString('hex')
}
};
const onSuccess = (response) => __awaiter(void 0, void 0, void 0, function* () {
console.log("response", response);
const inviteCreated = response.object;
const contact = yield models_1.models.Contact.create({
alias: nickname,
status: 0
});
const invite = yield models_1.models.Invite.create({
welcomeMessage: inviteCreated.message,
contactId: contact.id,
status: inviteCreated.invite_status,
inviteString: inviteCreated.pin
});
let contactJson = jsonUtils.contactToJson(contact);
if (invite) {
contactJson.invite = jsonUtils.inviteToJson(invite);
}
res.status(200);
res.json({ success: true, contact: contactJson });
res.end();
});
const onFailure = (response) => {
res.status(200);
res.json(response);
res.end();
};
hub_1.createInviteInHub(params, onSuccess, onFailure);
});
exports.createInvite = createInvite;
//# sourceMappingURL=invites.js.map

1
dist/api/controllers/invites.js.map

@ -0,0 +1 @@
{"version":3,"file":"invites.js","sourceRoot":"","sources":["../../../api/controllers/invites.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,sCAAgC;AAChC,iCAAgC;AAChC,2CAA0C;AAC1C,gCAA2E;AAE3E,MAAM,YAAY,GAAG,CAAO,GAAG,EAAE,GAAG,EAAE,EAAE;IACvC,MAAM,EACL,aAAa,EACX,GAAG,GAAG,CAAC,IAAI,CAAA;IACd,MAAM,MAAM,GAAG;QACd,MAAM,EAAE;YACP,GAAG,EAAE,aAAa;SAClB;KACD,CAAA;IAED,SAAS,SAAS;QACjB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAA;QAC3B,GAAG,CAAC,GAAG,EAAE,CAAA;IACV,CAAC;IACD,SAAS,SAAS;QACjB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;QAC5B,GAAG,CAAC,GAAG,EAAE,CAAA;IACV,CAAC;IAEE,uBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,CAAA;AACnD,CAAC,CAAA,CAAA;AAwFA,oCAAY;AAtFb,MAAM,SAAS,GAAG,CAAO,GAAG,EAAE,GAAG,EAAE,EAAE;IACpC,MAAM,MAAM,GAAG;QACd,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO;KAC5B,CAAA;IAED,MAAM,aAAa,GAAG,GAAG,CAAC,MAAM,CAAC,eAAe,CAAC,CAAA;IAEjD,MAAM,SAAS,GAAG,CAAO,QAAQ,EAAE,EAAE;QACpC,MAAM,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAA;QAE9B,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,MAAM,CAAC,CAAA;QAE/B,MAAM,QAAQ,GAAG,MAAM,eAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,YAAY,EAAE,MAAM,CAAC,GAAG,EAAE,EAAC,CAAC,CAAA;QAEpF,IAAI,QAAQ,CAAC,MAAM,IAAI,MAAM,CAAC,aAAa,EAAE;YAC5C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,aAAa,EAAE,CAAC,CAAA;SACjD;QAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,EAAE,MAAM,EAAE,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAA;QACnF,GAAG,CAAC,GAAG,EAAE,CAAA;IACV,CAAC,CAAA,CAAA;IAED,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,EAAE;QAC9B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC,CAAA;QAC5B,GAAG,CAAC,GAAG,EAAE,CAAA;IACV,CAAC,CAAA;IAEE,oBAAc,CAAC,aAAa,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,CAAA;AAC/D,CAAC,CAAA,CAAA;AAyDA,8BAAS;AAvDV,MAAM,YAAY,GAAG,CAAO,GAAG,EAAE,GAAG,EAAE,EAAE;IACvC,MAAM,EACL,QAAQ,EACR,eAAe,EACd,GAAG,GAAG,CAAC,IAAI,CAAA;IAEZ,MAAM,KAAK,GAAG,MAAM,eAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAC,CAAC,CAAA;IAExE,MAAM,MAAM,GAAG;QACd,MAAM,EAAE;YACP,QAAQ,EAAE,KAAK,CAAC,KAAK;YACrB,MAAM,EAAE,KAAK,CAAC,SAAS;YACvB,gBAAgB,EAAE,QAAQ;YAC1B,OAAO,EAAE,eAAe;YACxB,GAAG,EAAE,MAAM,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC;SAC3C;KACD,CAAA;IAED,MAAM,SAAS,GAAG,CAAO,QAAQ,EAAE,EAAE;QACpC,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAA;QAEjC,MAAM,aAAa,GAAG,QAAQ,CAAC,MAAM,CAAA;QAErC,MAAM,OAAO,GAAG,MAAM,eAAM,CAAC,OAAO,CAAC,MAAM,CAAC;YAC3C,KAAK,EAAE,QAAQ;YACf,MAAM,EAAE,CAAC;SACT,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,MAAM,eAAM,CAAC,MAAM,CAAC,MAAM,CAAC;YACzC,cAAc,EAAE,aAAa,CAAC,OAAO;YACrC,SAAS,EAAE,OAAO,CAAC,EAAE;YACrB,MAAM,EAAE,aAAa,CAAC,aAAa;YACnC,YAAY,EAAE,aAAa,CAAC,GAAG;SAC/B,CAAC,CAAA;QACF,IAAI,WAAW,GAAG,SAAS,CAAC,aAAa,CAAC,OAAO,CAAC,CAAA;QAClD,IAAI,MAAM,EAAE;YACX,WAAW,CAAC,MAAM,GAAG,SAAS,CAAC,YAAY,CAAC,MAAM,CAAC,CAAA;SACnD;QAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC,CAAA;QACjD,GAAG,CAAC,GAAG,EAAE,CAAA;IACV,CAAC,CAAA,CAAA;IAED,MAAM,SAAS,GAAG,CAAC,QAAQ,EAAE,EAAE;QAC9B,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QACf,GAAG,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QAClB,GAAG,CAAC,GAAG,EAAE,CAAA;IACV,CAAC,CAAA;IAEE,uBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,CAAA;AACnD,CAAC,CAAA,CAAA;AAGA,oCAAY"}

241
dist/api/controllers/invoices.js

@ -0,0 +1,241 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const lightning_1 = require("../utils/lightning");
const socket = require("../utils/socket");
const jsonUtils = require("../utils/json");
const decodeUtils = require("../utils/decode");
const helpers = require("../helpers");
const hub_1 = require("../hub");
const res_1 = require("../utils/res");
const constants = require(__dirname + '/../../config/constants.json');
const payInvoice = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const lightning = yield lightning_1.loadLightning();
const { payment_request } = req.body;
var call = lightning.sendPayment({});
call.on('data', (response) => __awaiter(void 0, void 0, void 0, function* () {
console.log('[pay invoice data]', response);
const message = yield models_1.models.Message.findOne({ where: { payment_request } });
if (!message) { // invoice still paid
return res_1.success(res, {
success: true,
response: { payment_request }
});
}
message.status = constants.statuses.confirmed;
message.save();
var date = new Date();
date.setMilliseconds(0);
const chat = yield models_1.models.Chat.findOne({ where: { id: message.chatId } });
const contactIds = JSON.parse(chat.contactIds);
const senderId = contactIds.find(id => id != message.sender);
const paidMessage = yield models_1.models.Message.create({
chatId: message.chatId,
sender: senderId,
type: constants.message_types.payment,
amount: message.amount,
amountMsat: message.amountMsat,
paymentHash: message.paymentHash,
date: date,
expirationDate: null,
messageContent: null,
status: constants.statuses.confirmed,
createdAt: date,
updatedAt: date
});
console.log('[pay invoice] stored message', paidMessage);
res_1.success(res, jsonUtils.messageToJson(paidMessage, chat));
}));
call.write({ payment_request });
});
exports.payInvoice = payInvoice;
const cancelInvoice = (req, res) => {
res.status(200);
res.json({ success: false });
res.end();
};
exports.cancelInvoice = cancelInvoice;
const createInvoice = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const lightning = yield lightning_1.loadLightning();
const { amount, memo, remote_memo, chat_id, contact_id } = req.body;
var request = {
value: amount,
memo: remote_memo || memo
};
if (amount == null) {
res.status(200);
res.json({ err: "no amount specified", });
res.end();
}
else {
lightning.addInvoice(request, function (err, response) {
console.log({ err, response });
if (err == null) {
const { payment_request } = response;
if (!contact_id && !chat_id) { // if no contact
res_1.success(res, {
invoice: payment_request
});
return; // end here
}
lightning.decodePayReq({ pay_req: payment_request }, (error, invoice) => __awaiter(this, void 0, void 0, function* () {
if (res) {
console.log('decoded pay req', { invoice });
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const chat = yield helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id
});
let timestamp = parseInt(invoice.timestamp + '000');
let expiry = parseInt(invoice.expiry + '000');
if (error) {
res.status(200);
res.json({ success: false, error });
res.end();
}
else {
const message = yield models_1.models.Message.create({
chatId: chat.id,
sender: owner.id,
type: constants.message_types.invoice,
amount: parseInt(invoice.num_satoshis),
amountMsat: parseInt(invoice.num_satoshis) * 1000,
paymentHash: invoice.payment_hash,
paymentRequest: payment_request,
date: new Date(timestamp),
expirationDate: new Date(timestamp + expiry),
messageContent: memo,
remoteMessageContent: remote_memo,
status: constants.statuses.pending,
createdAt: new Date(timestamp),
updatedAt: new Date(timestamp)
});
res_1.success(res, jsonUtils.messageToJson(message, chat));
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.invoice,
message: {
id: message.id,
invoice: message.paymentRequest
}
});
}
}
else {
console.log('error decoding pay req', { err, res });
res.status(500);
res.json({ err, res });
res.end();
}
}));
}
else {
console.log({ err, response });
}
});
}
});
exports.createInvoice = createInvoice;
const listInvoices = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const lightning = yield lightning_1.loadLightning();
lightning.listInvoices({}, (err, response) => {
console.log({ err, response });
if (err == null) {
res.status(200);
res.json(response);
res.end();
}
else {
console.log({ err, response });
}
});
});
exports.listInvoices = listInvoices;
const receiveInvoice = (payload) => __awaiter(void 0, void 0, void 0, function* () {
console.log('received invoice', payload);
const total_spent = 1;
const dat = payload.content || payload;
const payment_request = dat.message.invoice;
var date = new Date();
date.setMilliseconds(0);
const { owner, sender, chat, msg_id } = yield helpers.parseReceiveParams(payload);
if (!owner || !sender || !chat) {
return console.log('=> no group chat!');
}
const { memo, sat, msat, paymentHash, invoiceDate, expirationSeconds } = decodePaymentRequest(payment_request);
const message = yield models_1.models.Message.create({
chatId: chat.id,
type: constants.message_types.invoice,
sender: sender.id,
amount: sat,
amountMsat: msat,
paymentRequest: payment_request,
asciiEncodedTotal: total_spent,
paymentHash: paymentHash,
messageContent: memo,
expirationDate: new Date(invoiceDate + expirationSeconds),
date: new Date(invoiceDate),
status: constants.statuses.pending,
createdAt: date,
updatedAt: date
});
console.log('received keysend invoice message', message.id);
socket.sendJson({
type: 'invoice',
response: jsonUtils.messageToJson(message, chat)
});
hub_1.sendNotification(chat, sender.alias, 'message');
sendConfirmation({ chat, sender: owner, msg_id });
});
exports.receiveInvoice = receiveInvoice;
const sendConfirmation = ({ chat, sender, msg_id }) => {
helpers.sendMessage({
chat,
sender,
message: { id: msg_id },
type: constants.message_types.confirmation,
});
};
// lnd invoice stuff
function decodePaymentRequest(paymentRequest) {
var decodedPaymentRequest = decodeUtils.decode(paymentRequest);
var expirationSeconds = 3600;
var paymentHash = "";
var memo = "";
for (var i = 0; i < decodedPaymentRequest.data.tags.length; i++) {
let tag = decodedPaymentRequest.data.tags[i];
if (tag) {
if (tag.description == 'payment_hash') {
paymentHash = tag.value;
}
else if (tag.description == 'description') {
memo = tag.value;
}
else if (tag.description == 'expiry') {
expirationSeconds = tag.value;
}
}
}
expirationSeconds = parseInt(expirationSeconds.toString() + '000');
let invoiceDate = parseInt(decodedPaymentRequest.data.time_stamp.toString() + '000');
let amount = decodedPaymentRequest['human_readable_part']['amount'];
var msat = 0;
var sat = 0;
if (Number.isInteger(amount)) {
msat = amount;
sat = amount / 1000;
}
return { sat, msat, paymentHash, invoiceDate, expirationSeconds, memo };
}
//# sourceMappingURL=invoices.js.map

1
dist/api/controllers/invoices.js.map

File diff suppressed because one or more lines are too long

481
dist/api/controllers/media.js

@ -0,0 +1,481 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const socket = require("../utils/socket");
const jsonUtils = require("../utils/json");
const resUtils = require("../utils/res");
const helpers = require("../helpers");
const hub_1 = require("../hub");
const lightning_1 = require("../utils/lightning");
const rp = require("request-promise");
const lightning_2 = require("../utils/lightning");
const ldat_1 = require("../utils/ldat");
const cron_1 = require("cron");
const zbase32 = require("../utils/zbase32");
const schemas = require("./schemas");
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/app.json')[env];
const constants = require(__dirname + '/../../config/constants.json');
/*
TODO line 233: parse that from token itself, dont use getMediaInfo at all
"attachment": sends a message to a chat with a signed receipt for a file, which can be accessed from sphinx-meme server
If the attachment has a price, then the media must be purchased to get the receipt
"purchase" sends sats.
if the amount matches the price, the media owner
will respond ("purchase_accept" or "purchase_deny" type)
with the signed token, which can only be used by the buyer
purchase_accept should update the original attachment message with the terms and receipt
(both Relay and client need to do this) or make new???
purchase_deny returns the sats
*/
const sendAttachmentMessage = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
// try {
// schemas.attachment.validateSync(req.body)
// } catch(e) {
// return resUtils.failure(res, e.message)
// }
const { chat_id, contact_id, muid, text, remote_text, remote_text_map, media_key_map, media_type, file_name, ttl, price, } = req.body;
console.log('[send attachment]', req.body);
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const chat = yield helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id
});
let TTL = ttl;
if (ttl) {
TTL = parseInt(ttl);
}
if (!TTL)
TTL = 31536000; // default year
const amt = price || 0;
// generate media token for self!
const myMediaToken = yield ldat_1.tokenFromTerms({
muid, ttl: TTL, host: '',
pubkey: owner.publicKey,
meta: Object.assign(Object.assign({}, amt && { amt }), { ttl })
});
const date = new Date();
date.setMilliseconds(0);
const myMediaKey = (media_key_map && media_key_map[owner.id]) || '';
const mediaType = media_type || '';
const remoteMessageContent = remote_text_map ? JSON.stringify(remote_text_map) : remote_text;
const message = yield models_1.models.Message.create({
chatId: chat.id,
sender: owner.id,
type: constants.message_types.attachment,
status: constants.statuses.pending,
messageContent: text || file_name || '',
remoteMessageContent,
mediaToken: myMediaToken,
mediaKey: myMediaKey,
mediaType: mediaType,
date,
createdAt: date,
updatedAt: date
});
saveMediaKeys(muid, media_key_map, chat.id, message.id);
const mediaTerms = {
muid, ttl: TTL,
meta: Object.assign({}, amt && { amt }),
skipSigning: amt ? true : false // only sign if its free
};
const msg = {
mediaTerms,
id: message.id,
content: remote_text_map || remote_text || text || file_name || '',
mediaKey: media_key_map,
mediaType: mediaType,
};
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.attachment,
message: msg,
success: (data) => __awaiter(void 0, void 0, void 0, function* () {
console.log('attachment sent', { data });
resUtils.success(res, jsonUtils.messageToJson(message, chat));
}),
failure: error => resUtils.failure(res, error.message),
});
});
exports.sendAttachmentMessage = sendAttachmentMessage;
function saveMediaKeys(muid, mediaKeyMap, chatId, messageId) {
if (typeof mediaKeyMap !== 'object') {
console.log('wrong type for mediaKeyMap');
return;
}
var date = new Date();
date.setMilliseconds(0);
for (let [contactId, key] of Object.entries(mediaKeyMap)) {
models_1.models.MediaKey.create({
muid, chatId, contactId, key, messageId,
createdAt: date,
});
}
}
const purchase = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { chat_id, contact_id, amount, mediaToken, } = req.body;
var date = new Date();
date.setMilliseconds(0);
try {
schemas.purchase.validateSync(req.body);
}
catch (e) {
return resUtils.failure(res, e.message);
}
console.log('purchase!');
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const chat = yield helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id
});
const message = yield models_1.models.Message.create({
sender: owner.id,
type: constants.message_types.purchase,
mediaToken: mediaToken,
date: date,
createdAt: date,
updatedAt: date
});
const msg = {
amount, mediaToken, id: message.id,
};
helpers.sendMessage({
chat: Object.assign(Object.assign({}, chat), { contactIds: [contact_id] }),
sender: owner,
type: constants.message_types.purchase,
message: msg,
success: (data) => __awaiter(void 0, void 0, void 0, function* () {
console.log('purchase sent', { data });
resUtils.success(res, jsonUtils.messageToJson(message));
}),
failure: error => resUtils.failure(res, error.message),
});
});
exports.purchase = purchase;
/* RECEIVERS */
const receivePurchase = (payload) => __awaiter(void 0, void 0, void 0, function* () {
console.log('received purchase', { payload });
var date = new Date();
date.setMilliseconds(0);
const { owner, sender, chat, amount, mediaToken } = yield helpers.parseReceiveParams(payload);
if (!owner || !sender || !chat) {
return console.log('=> group chat not found!');
}
yield models_1.models.Message.create({
chatId: chat.id,
sender: sender.id,
type: constants.message_types.purchase,
mediaToken: mediaToken,
date: date,
createdAt: date,
updatedAt: date
});
const muid = mediaToken && mediaToken.split('.').length && mediaToken.split('.')[1];
if (!muid) {
return console.log('no muid');
}
const ogMessage = models_1.models.Message.findOne({
where: { mediaToken }
});
if (!ogMessage) {
return console.log('no original message');
}
// find mediaKey for who sent
const mediaKey = models_1.models.MediaKey.findOne({ where: {
muid, receiver: sender.id,
} });
const terms = ldat_1.parseLDAT(mediaToken);
// get info
let TTL = terms.meta && terms.meta.ttl;
let price = terms.meta && terms.meta.amt;
if (!TTL || !price) {
const media = yield getMediaInfo(muid);
console.log("GOT MEDIA", media);
if (media) {
TTL = media.ttl && parseInt(media.ttl);
price = media.price;
}
if (!TTL)
TTL = 31536000;
if (!price)
price = 0;
}
if (amount < price) { // didnt pay enough
return helpers.sendMessage({
chat: Object.assign(Object.assign({}, chat), { contactIds: [sender.id] }),
sender: owner,
amount: amount,
type: constants.message_types.purchase_deny,
message: { amount, content: 'Payment Denied' },
success: (data) => __awaiter(void 0, void 0, void 0, function* () {
console.log('purchase_deny sent', { data });
}),
failure: error => console.log('=> couldnt send purcahse deny', error),
});
}
const acceptTerms = {
muid, ttl: TTL,
meta: { amt: amount },
};
helpers.sendMessage({
chat: Object.assign(Object.assign({}, chat), { contactIds: [sender.id] }),
sender: owner,
type: constants.message_types.purchase_accept,
message: {
mediaTerms: acceptTerms,
mediaKey: mediaKey.key,
mediaType: ogMessage.mediaType,
},
success: (data) => __awaiter(void 0, void 0, void 0, function* () {
console.log('purchase_accept sent', { data });
}),
failure: error => console.log('=> couldnt send purchase accept', error),
});
});
exports.receivePurchase = receivePurchase;
const receivePurchaseAccept = (payload) => __awaiter(void 0, void 0, void 0, function* () {
var date = new Date();
date.setMilliseconds(0);
const { owner, sender, chat, mediaToken, mediaKey, mediaType } = yield helpers.parseReceiveParams(payload);
if (!owner || !sender || !chat) {
return console.log('=> no group chat!');
}
const termsArray = mediaToken.split('.');
// const host = termsArray[0]
const muid = termsArray[1];
if (!muid) {
return console.log('wtf no muid');
}
// const attachmentMessage = await models.Message.findOne({where:{
// mediaToken: {$like: `${host}.${muid}%`}
// }})
// if(attachmentMessage){
// console.log('=> updated msg!')
// attachmentMessage.update({
// mediaToken, mediaKey
// })
// }
const msg = yield models_1.models.Message.create({
chatId: chat.id,
sender: sender.id,
type: constants.message_types.purchase_accept,
status: constants.statuses.received,
mediaToken,
mediaKey,
mediaType,
date: date,
createdAt: date,
updatedAt: date
});
socket.sendJson({
type: 'purchase_accept',
response: jsonUtils.messageToJson(msg, chat)
});
});
exports.receivePurchaseAccept = receivePurchaseAccept;
const receivePurchaseDeny = (payload) => __awaiter(void 0, void 0, void 0, function* () {
var date = new Date();
date.setMilliseconds(0);
const { owner, sender, chat, amount, mediaToken } = yield helpers.parseReceiveParams(payload);
if (!owner || !sender || !chat) {
return console.log('=> no group chat!');
}
const msg = yield models_1.models.Message.create({
chatId: chat.id,
sender: sender.id,
type: constants.message_types.purchase_deny,
status: constants.statuses.received,
messageContent: 'Purchase has been denied and sats returned to you',
amount: amount,
amountMsat: parseFloat(amount) * 1000,
mediaToken,
date: date,
createdAt: date,
updatedAt: date
});
socket.sendJson({
type: 'purchase_deny',
response: jsonUtils.messageToJson(msg, chat)
});
});
exports.receivePurchaseDeny = receivePurchaseDeny;
const receiveAttachment = (payload) => __awaiter(void 0, void 0, void 0, function* () {
console.log('received attachment', { payload });
var date = new Date();
date.setMilliseconds(0);
const { owner, sender, chat, mediaToken, mediaKey, mediaType, content, msg_id } = yield helpers.parseReceiveParams(payload);
if (!owner || !sender || !chat) {
return console.log('=> no group chat!');
}
const msg = {
chatId: chat.id,
type: constants.message_types.attachment,
sender: sender.id,
date: date,
createdAt: date,
updatedAt: date
};
if (content)
msg.messageContent = content;
if (mediaToken)
msg.mediaToken = mediaToken;
if (mediaKey)
msg.mediaKey = mediaKey;
if (mediaType)
msg.mediaType = mediaType;
const message = yield models_1.models.Message.create(msg);
console.log('saved attachment', message.dataValues);
socket.sendJson({
type: 'attachment',
response: jsonUtils.messageToJson(message, chat)
});
hub_1.sendNotification(chat, sender.alias, 'message');
sendConfirmation({ chat, sender: owner, msg_id });
});
exports.receiveAttachment = receiveAttachment;
const sendConfirmation = ({ chat, sender, msg_id }) => {
helpers.sendMessage({
chat,
sender,
message: { id: msg_id },
type: constants.message_types.confirmation,
});
};
function signer(req, res) {
return __awaiter(this, void 0, void 0, function* () {
if (!req.params.challenge)
return resUtils.failure(res, "no challenge");
try {
const sig = yield lightning_1.signBuffer(Buffer.from(req.params.challenge, 'base64'));
const sigBytes = zbase32.decode(sig);
const sigBase64 = ldat_1.urlBase64FromBytes(sigBytes);
resUtils.success(res, {
sig: sigBase64
});
}
catch (e) {
resUtils.failure(res, e);
}
});
}
exports.signer = signer;
function verifier(msg, sig) {
return __awaiter(this, void 0, void 0, function* () {
try {
const res = yield lightning_1.verifyMessage(msg, sig);
return res;
}
catch (e) {
console.log(e);
}
});
}
exports.verifier = verifier;
function getMyPubKey() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise((resolve, reject) => {
const lightning = lightning_2.loadLightning();
var request = {};
lightning.getInfo(request, function (err, response) {
if (err)
reject(err);
if (!response.identity_pubkey)
reject('no pub key');
else
resolve(response.identity_pubkey);
});
});
});
}
function cycleMediaToken() {
return __awaiter(this, void 0, void 0, function* () {
try {
if (process.env.TEST_LDAT)
ldat_1.testLDAT();
const mt = yield getMediaToken(null);
if (mt)
console.log('=> [meme] authed!');
new cron_1.CronJob('1 * * * *', function () {
getMediaToken(true);
});
}
catch (e) {
console.log(e.message);
}
});
}
exports.cycleMediaToken = cycleMediaToken;
const mediaURL = 'http://' + config.media_host + '/';
let mediaToken;
function getMediaToken(force) {
return __awaiter(this, void 0, void 0, function* () {
if (!force && mediaToken)
return mediaToken;
yield helpers.sleep(3000);
try {
const res = yield rp.get(mediaURL + 'ask');
const r = JSON.parse(res);
if (!(r && r.challenge && r.id)) {
throw new Error('no challenge');
}
const sig = yield lightning_1.signBuffer(Buffer.from(r.challenge, 'base64'));
if (!sig)
throw new Error('no signature');
const pubkey = yield getMyPubKey();
if (!pubkey) {
throw new Error('no pub key!');
}
const sigBytes = zbase32.decode(sig);
const sigBase64 = ldat_1.urlBase64FromBytes(sigBytes);
const bod = yield rp.post(mediaURL + 'verify', {
form: { id: r.id, sig: sigBase64, pubkey }
});
const body = JSON.parse(bod);
if (!(body && body.token)) {
throw new Error('no token');
}
mediaToken = body.token;
return body.token;
}
catch (e) {
throw e;
}
});
}
exports.getMediaToken = getMediaToken;
function getMediaInfo(muid) {
return __awaiter(this, void 0, void 0, function* () {
try {
const token = yield getMediaToken(null);
const res = yield rp.get(mediaURL + 'mymedia/' + muid, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
json: true
});
return res;
}
catch (e) {
return null;
}
});
}
//# sourceMappingURL=media.js.map

1
dist/api/controllers/media.js.map

File diff suppressed because one or more lines are too long

239
dist/api/controllers/messages.js

@ -0,0 +1,239 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const sequelize_1 = require("sequelize");
const underscore_1 = require("underscore");
const hub_1 = require("../hub");
const socket = require("../utils/socket");
const jsonUtils = require("../utils/json");
const helpers = require("../helpers");
const res_1 = require("../utils/res");
const lock_1 = require("../utils/lock");
const constants = require(__dirname + '/../../config/constants.json');
const getMessages = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const dateToReturn = req.query.date;
if (!dateToReturn) {
return getAllMessages(req, res);
}
console.log(dateToReturn);
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
// const chatId = req.query.chat_id
let newMessagesWhere = {
date: { [sequelize_1.Op.gte]: dateToReturn },
[sequelize_1.Op.or]: [
{ receiver: owner.id },
{ receiver: null }
]
};
let confirmedMessagesWhere = {
updated_at: { [sequelize_1.Op.gte]: dateToReturn },
status: constants.statuses.received,
sender: owner.id
};
// if (chatId) {
// newMessagesWhere.chat_id = chatId
// confirmedMessagesWhere.chat_id = chatId
// }
const newMessages = yield models_1.models.Message.findAll({ where: newMessagesWhere });
const confirmedMessages = yield models_1.models.Message.findAll({ where: confirmedMessagesWhere });
const chatIds = [];
newMessages.forEach(m => {
if (!chatIds.includes(m.chatId))
chatIds.push(m.chatId);
});
confirmedMessages.forEach(m => {
if (!chatIds.includes(m.chatId))
chatIds.push(m.chatId);
});
let chats = chatIds.length > 0 ? yield models_1.models.Chat.findAll({ where: { deleted: false, id: chatIds } }) : [];
const chatsById = underscore_1.indexBy(chats, 'id');
res.json({
success: true,
response: {
new_messages: newMessages.map(message => jsonUtils.messageToJson(message, chatsById[parseInt(message.chatId)])),
confirmed_messages: confirmedMessages.map(message => jsonUtils.messageToJson(message, chatsById[parseInt(message.chatId)]))
}
});
res.status(200);
res.end();
});
exports.getMessages = getMessages;
const getAllMessages = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const messages = yield models_1.models.Message.findAll({ order: [['id', 'asc']] });
const chatIds = messages.map(m => m.chatId);
console.log('=> getAllMessages, chatIds', chatIds);
let chats = chatIds.length > 0 ? yield models_1.models.Chat.findAll({ where: { deleted: false, id: chatIds } }) : [];
const chatsById = underscore_1.indexBy(chats, 'id');
res_1.success(res, {
new_messages: messages.map(message => jsonUtils.messageToJson(message, chatsById[parseInt(message.chatId)])),
confirmed_messages: []
});
});
const sendMessage = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
// try {
// schemas.message.validateSync(req.body)
// } catch(e) {
// return failure(res, e.message)
// }
const { contact_id, text, remote_text, chat_id, remote_text_map, } = req.body;
console.log('[sendMessage]');
var date = new Date();
date.setMilliseconds(0);
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const chat = yield helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id,
});
const remoteMessageContent = remote_text_map ? JSON.stringify(remote_text_map) : remote_text;
const msg = {
chatId: chat.id,
type: constants.message_types.message,
sender: owner.id,
date: date,
messageContent: text,
remoteMessageContent,
status: constants.statuses.pending,
createdAt: date,
updatedAt: date
};
// console.log(msg)
const message = yield models_1.models.Message.create(msg);
res_1.success(res, jsonUtils.messageToJson(message, chat));
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.message,
message: {
id: message.id,
content: remote_text_map || remote_text || text
}
});
});
exports.sendMessage = sendMessage;
const receiveMessage = (payload) => __awaiter(void 0, void 0, void 0, function* () {
console.log('received message', { payload });
var date = new Date();
date.setMilliseconds(0);
const total_spent = 1;
const { owner, sender, chat, content, msg_id } = yield helpers.parseReceiveParams(payload);
if (!owner || !sender || !chat) {
return console.log('=> no group chat!');
}
const text = content;
const message = yield models_1.models.Message.create({
chatId: chat.id,
type: constants.message_types.message,
asciiEncodedTotal: total_spent,
sender: sender.id,
date: date,
messageContent: text,
createdAt: date,
updatedAt: date,
status: constants.statuses.received
});
console.log('saved message', message.dataValues);
socket.sendJson({
type: 'message',
response: jsonUtils.messageToJson(message, chat)
});
hub_1.sendNotification(chat, sender.alias, 'message');
sendConfirmation({ chat, sender: owner, msg_id });
});
exports.receiveMessage = receiveMessage;
const sendConfirmation = ({ chat, sender, msg_id }) => {
helpers.sendMessage({
chat,
sender,
message: { id: msg_id },
type: constants.message_types.confirmation,
});
};
const receiveConfirmation = (payload) => __awaiter(void 0, void 0, void 0, function* () {
console.log('received confirmation', { payload });
const dat = payload.content || payload;
const chat_uuid = dat.chat.uuid;
const msg_id = dat.message.id;
const sender_pub_key = dat.sender.pub_key;
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const sender = yield models_1.models.Contact.findOne({ where: { publicKey: sender_pub_key } });
const chat = yield models_1.models.Chat.findOne({ where: { uuid: chat_uuid } });
// new confirmation logic
if (msg_id) {
lock_1.default.acquire('confirmation', function (done) {
return __awaiter(this, void 0, void 0, function* () {
console.log("update status map");
const message = yield models_1.models.Message.findOne({ where: { id: msg_id } });
if (message) {
let statusMap = {};
try {
statusMap = JSON.parse(message.statusMap || '{}');
}
catch (e) { }
statusMap[sender.id] = constants.statuses.received;
yield message.update({
status: constants.statuses.received,
statusMap: JSON.stringify(statusMap)
});
socket.sendJson({
type: 'confirmation',
response: jsonUtils.messageToJson(message, chat)
});
}
done();
});
});
}
else { // old logic
const messages = yield models_1.models.Message.findAll({
limit: 1,
where: {
chatId: chat.id,
sender: owner.id,
type: [
constants.message_types.message,
constants.message_types.invoice,
constants.message_types.attachment,
],
status: constants.statuses.pending,
},
order: [['createdAt', 'desc']]
});
const message = messages[0];
message.update({ status: constants.statuses.received });
socket.sendJson({
type: 'confirmation',
response: jsonUtils.messageToJson(message, chat)
});
}
});
exports.receiveConfirmation = receiveConfirmation;
const readMessages = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const chat_id = req.params.chat_id;
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
models_1.models.Message.update({ seen: true }, {
where: {
sender: {
[sequelize_1.Op.ne]: owner.id
},
chatId: chat_id
}
});
res_1.success(res, {});
});
exports.readMessages = readMessages;
const clearMessages = (req, res) => {
models_1.models.Message.destroy({ where: {}, truncate: true });
res_1.success(res, {});
};
exports.clearMessages = clearMessages;
//# sourceMappingURL=messages.js.map

1
dist/api/controllers/messages.js.map

File diff suppressed because one or more lines are too long

178
dist/api/controllers/payment.js

@ -0,0 +1,178 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const hub_1 = require("../hub");
const socket = require("../utils/socket");
const jsonUtils = require("../utils/json");
const helpers = require("../helpers");
const res_1 = require("../utils/res");
const lightning = require("../utils/lightning");
const ldat_1 = require("../utils/ldat");
const constants = require(__dirname + '/../../config/constants.json');
const sendPayment = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { amount, chat_id, contact_id, destination_key, media_type, muid, text, remote_text, dimensions, } = req.body;
console.log('[send payment]', req.body);
if (destination_key && !contact_id && !chat_id) {
return helpers.performKeysendMessage({
destination_key,
amount,
msg: '{}',
success: () => {
console.log('payment sent!');
res_1.success(res, { destination_key, amount });
},
failure: (error) => {
res.status(200);
res.json({ success: false, error });
res.end();
}
});
}
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const chat = yield helpers.findOrCreateChat({
chat_id,
owner_id: owner.id,
recipient_id: contact_id
});
var date = new Date();
date.setMilliseconds(0);
const msg = {
chatId: chat.id,
sender: owner.id,
type: constants.message_types.direct_payment,
amount: amount,
amountMsat: parseFloat(amount) * 1000,
date: date,
createdAt: date,
updatedAt: date
};
if (text)
msg.messageContent = text;
if (remote_text)
msg.remoteMessageContent = remote_text;
if (muid) {
const myMediaToken = yield ldat_1.tokenFromTerms({
meta: { dim: dimensions }, host: '',
muid, ttl: null,
pubkey: owner.publicKey
});
msg.mediaToken = myMediaToken;
msg.mediaType = media_type || '';
}
const message = yield models_1.models.Message.create(msg);
const msgToSend = {
id: message.id,
amount,
};
if (muid) {
msgToSend.mediaType = media_type || 'image/jpeg';
msgToSend.mediaTerms = { muid, meta: { dim: dimensions } };
}
if (remote_text)
msgToSend.content = remote_text;
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.direct_payment,
message: msgToSend,
amount: amount,
success: (data) => __awaiter(void 0, void 0, void 0, function* () {
// console.log('payment sent', { data })
res_1.success(res, jsonUtils.messageToJson(message, chat));
}),
failure: (error) => {
res.status(200);
res.json({ success: false, error });
res.end();
}
});
});
exports.sendPayment = sendPayment;
const receivePayment = (payload) => __awaiter(void 0, void 0, void 0, function* () {
console.log('received payment', { payload });
var date = new Date();
date.setMilliseconds(0);
const { owner, sender, chat, amount, content, mediaType, mediaToken } = yield helpers.parseReceiveParams(payload);
if (!owner || !sender || !chat) {
return console.log('=> no group chat!');
}
const msg = {
chatId: chat.id,
type: constants.message_types.direct_payment,
sender: sender.id,
amount: amount,
amountMsat: parseFloat(amount) * 1000,
date: date,
createdAt: date,
updatedAt: date
};
if (content)
msg.messageContent = content;
if (mediaType)
msg.mediaType = mediaType;
if (mediaToken)
msg.mediaToken = mediaToken;
const message = yield models_1.models.Message.create(msg);
console.log('saved message', message.dataValues);
socket.sendJson({
type: 'direct_payment',
response: jsonUtils.messageToJson(message, chat)
});
hub_1.sendNotification(chat, sender.alias, 'message');
});
exports.receivePayment = receivePayment;
const listPayments = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const limit = (req.query.limit && parseInt(req.query.limit)) || 100;
const offset = (req.query.offset && parseInt(req.query.offset)) || 0;
const payments = [];
const response = yield lightning.listInvoices();
const invs = response && response.invoices;
if (invs && invs.length) {
invs.forEach(inv => {
const val = inv.value && parseInt(inv.value);
if (val && val > 1) {
let payment_hash = '';
if (inv.r_hash) {
payment_hash = Buffer.from(inv.r_hash).toString('hex');
}
payments.push({
type: 'invoice',
amount: parseInt(inv.value),
date: parseInt(inv.creation_date),
payment_request: inv.payment_request,
payment_hash
});
}
});
}
const res2 = yield lightning.listPayments();
const pays = res2 && res2.payments;
if (pays && pays.length) {
pays.forEach(pay => {
const val = pay.value && parseInt(pay.value);
if (val && val > 1) {
payments.push({
type: 'payment',
amount: parseInt(pay.value),
date: parseInt(pay.creation_date),
pubkey: pay.path[pay.path.length - 1],
payment_hash: pay.payment_hash,
});
}
});
}
// latest one first
payments.sort((a, b) => b.date - a.date);
res_1.success(res, payments.splice(offset, limit));
});
exports.listPayments = listPayments;
//# sourceMappingURL=payment.js.map

1
dist/api/controllers/payment.js.map

File diff suppressed because one or more lines are too long

25
dist/api/controllers/schemas.js

@ -0,0 +1,25 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const yup = require("yup");
/*
These schemas validate payloads coming from app,
do not necessarily match up with Models
*/
const attachment = yup.object().shape({
muid: yup.string().required(),
media_type: yup.string().required(),
media_key_map: yup.object().required(),
});
exports.attachment = attachment;
const message = yup.object().shape({
contact_id: yup.number().required(),
});
exports.message = message;
const purchase = yup.object().shape({
chat_id: yup.number().required(),
contact_id: yup.number().required(),
mediaToken: yup.string().required(),
amount: yup.number().required()
});
exports.purchase = purchase;
//# sourceMappingURL=schemas.js.map

1
dist/api/controllers/schemas.js.map

@ -0,0 +1 @@
{"version":3,"file":"schemas.js","sourceRoot":"","sources":["../../../api/controllers/schemas.ts"],"names":[],"mappings":";;AAAA,2BAA0B;AAE1B;;;EAGE;AAEF,MAAM,UAAU,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC;IAClC,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAC7B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACzC,CAAC,CAAA;AAcE,gCAAU;AAZd,MAAM,OAAO,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC;IAC/B,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CACtC,CAAC,CAAA;AAYE,0BAAO;AAVX,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,KAAK,CAAC;IAChC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IAChC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,UAAU,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;IACnC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,CAAC,QAAQ,EAAE;CAClC,CAAC,CAAA;AAIE,4BAAQ"}

402
dist/api/controllers/subscriptions.js

@ -0,0 +1,402 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const res_1 = require("../utils/res");
const cron_1 = require("cron");
const case_1 = require("../utils/case");
const cronUtils = require("../utils/cron");
const socket = require("../utils/socket");
const jsonUtils = require("../utils/json");
const helpers = require("../helpers");
const rsa = require("../crypto/rsa");
const moment = require("moment");
const constants = require(__dirname + '/../../config/constants.json');
// store all current running jobs in memory
let jobs = {};
// init jobs from DB
const initializeCronJobs = () => __awaiter(void 0, void 0, void 0, function* () {
yield helpers.sleep(1000);
const subs = yield getRawSubs({ where: { ended: false } });
subs.length && subs.forEach(sub => {
console.log("=> starting subscription cron job", sub.id + ":", sub.cron);
startCronJob(sub);
});
});
exports.initializeCronJobs = initializeCronJobs;
function startCronJob(sub) {
return __awaiter(this, void 0, void 0, function* () {
jobs[sub.id] = new cron_1.CronJob(sub.cron, function () {
return __awaiter(this, void 0, void 0, function* () {
const subscription = yield models_1.models.Subscription.findOne({ where: { id: sub.id } });
if (!subscription) {
delete jobs[sub.id];
return this.stop();
}
console.log('EXEC CRON =>', subscription.id);
if (subscription.paused) { // skip, still in jobs{} tho
return this.stop();
}
let STOP = checkSubscriptionShouldAlreadyHaveEnded(subscription);
if (STOP) { // end the job and return
console.log("stop");
subscription.update({ ended: true });
delete jobs[subscription.id];
return this.stop();
}
// SEND PAYMENT!!!
sendSubscriptionPayment(subscription, false);
});
}, null, true);
});
}
function checkSubscriptionShouldAlreadyHaveEnded(sub) {
if (sub.endDate) {
const now = new Date();
if (now.getTime() > sub.endDate.getTime()) {
return true;
}
}
if (sub.endNumber) {
if (sub.count >= sub.endNumber) {
return true;
}
}
return false;
}
function checkSubscriptionShouldEndAfterThisPayment(sub) {
if (sub.endDate) {
const { ms } = cronUtils.parse(sub.cron);
const now = new Date();
if ((now.getTime() + ms) > sub.endDate.getTime()) {
return true;
}
}
if (sub.endNumber) {
if (sub.count + 1 >= sub.endNumber) {
return true;
}
}
return false;
}
function msgForSubPayment(owner, sub, isFirstMessage, forMe) {
let text = '';
if (isFirstMessage) {
const alias = forMe ? 'You' : owner.alias;
text = `${alias} subscribed\n`;
}
else {
text = 'Subscription\n';
}
text += `Amount: ${sub.amount} sats\n`;
text += `Interval: ${cronUtils.parse(sub.cron).interval}\n`;
if (sub.endDate) {
text += `End: ${moment(sub.endDate).format('MM/DD/YY')}\n`;
text += `Status: ${sub.count + 1} sent`;
}
else if (sub.endNumber) {
text += `Status: ${sub.count + 1} of ${sub.endNumber} sent`;
}
return text;
}
function sendSubscriptionPayment(sub, isFirstMessage) {
return __awaiter(this, void 0, void 0, function* () {
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
var date = new Date();
date.setMilliseconds(0);
const subscription = yield models_1.models.Subscription.findOne({ where: { id: sub.id } });
if (!subscription) {
return;
}
const chat = yield models_1.models.Chat.findOne({ where: { id: subscription.chatId } });
if (!subscription) {
console.log("=> no sub for this payment!!!");
return;
}
const forMe = false;
const text = msgForSubPayment(owner, sub, isFirstMessage, forMe);
const contact = yield models_1.models.Contact.findByPk(sub.contactId);
const enc = rsa.encrypt(contact.contactKey, text);
helpers.sendMessage({
chat: chat,
sender: owner,
type: constants.message_types.direct_payment,
message: { amount: sub.amount, content: enc },
amount: sub.amount,
success: (data) => __awaiter(this, void 0, void 0, function* () {
const shouldEnd = checkSubscriptionShouldEndAfterThisPayment(subscription);
const obj = {
totalPaid: parseFloat(subscription.totalPaid || 0) + parseFloat(subscription.amount),
count: parseInt(subscription.count || 0) + 1,
ended: false,
};
if (shouldEnd) {
obj.ended = true;
if (jobs[sub.id])
jobs[subscription.id].stop();
delete jobs[subscription.id];
}
yield subscription.update(obj);
const forMe = true;
const text2 = msgForSubPayment(owner, sub, isFirstMessage, forMe);
const encText = rsa.encrypt(owner.contactKey, text2);
const message = yield models_1.models.Message.create({
chatId: chat.id,
sender: owner.id,
type: constants.message_types.direct_payment,
status: constants.statuses.confirmed,
messageContent: encText,
amount: subscription.amount,
amountMsat: parseFloat(subscription.amount) * 1000,
date: date,
createdAt: date,
updatedAt: date,
subscriptionId: subscription.id,
});
socket.sendJson({
type: 'direct_payment',
response: jsonUtils.messageToJson(message, chat)
});
}),
failure: (err) => __awaiter(this, void 0, void 0, function* () {
console.log("SEND PAY ERROR");
let errMessage = constants.payment_errors[err] || 'Unknown';
errMessage = 'Payment Failed: ' + errMessage;
const message = yield models_1.models.Message.create({
chatId: chat.id,
sender: owner.id,
type: constants.message_types.direct_payment,
status: constants.statuses.failed,
messageContent: errMessage,
amount: sub.amount,
amountMsat: parseFloat(sub.amount) * 1000,
date: date,
createdAt: date,
updatedAt: date,
subscriptionId: sub.id,
});
socket.sendJson({
type: 'direct_payment',
response: jsonUtils.messageToJson(message, chat)
});
})
});
});
}
// pause sub
function pauseSubscription(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const id = parseInt(req.params.id);
try {
const sub = yield models_1.models.Subscription.findOne({ where: { id } });
if (sub) {
sub.update({ paused: true });
if (jobs[id])
jobs[id].stop();
res_1.success(res, jsonUtils.subscriptionToJson(sub, null));
}
else {
res_1.failure(res, 'not found');
}
}
catch (e) {
console.log('ERROR pauseSubscription', e);
res_1.failure(res, e);
}
});
}
exports.pauseSubscription = pauseSubscription;
;
// restart sub
function restartSubscription(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const id = parseInt(req.params.id);
try {
const sub = yield models_1.models.Subscription.findOne({ where: { id } });
if (sub) {
sub.update({ paused: false });
if (jobs[id])
jobs[id].start();
res_1.success(res, jsonUtils.subscriptionToJson(sub, null));
}
else {
res_1.failure(res, 'not found');
}
}
catch (e) {
console.log('ERROR restartSubscription', e);
res_1.failure(res, e);
}
});
}
exports.restartSubscription = restartSubscription;
;
function getRawSubs(opts = {}) {
return __awaiter(this, void 0, void 0, function* () {
const options = Object.assign({ order: [['id', 'asc']] }, opts);
try {
const subs = yield models_1.models.Subscription.findAll(options);
return subs;
}
catch (e) {
throw e;
}
});
}
// all subs
const getAllSubscriptions = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
try {
const subs = yield getRawSubs();
res_1.success(res, subs.map(sub => jsonUtils.subscriptionToJson(sub, null)));
}
catch (e) {
console.log('ERROR getAllSubscriptions', e);
res_1.failure(res, e);
}
});
exports.getAllSubscriptions = getAllSubscriptions;
// one sub by id
function getSubscription(req, res) {
return __awaiter(this, void 0, void 0, function* () {
try {
const sub = yield models_1.models.Subscription.findOne({ where: { id: req.params.id } });
res_1.success(res, jsonUtils.subscriptionToJson(sub, null));
}
catch (e) {
console.log('ERROR getSubscription', e);
res_1.failure(res, e);
}
});
}
exports.getSubscription = getSubscription;
;
// delete sub by id
function deleteSubscription(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const id = req.params.id;
if (!id)
return;
try {
if (jobs[id]) {
jobs[id].stop();
delete jobs[id];
}
models_1.models.Subscription.destroy({ where: { id } });
res_1.success(res, true);
}
catch (e) {
console.log('ERROR deleteSubscription', e);
res_1.failure(res, e);
}
});
}
exports.deleteSubscription = deleteSubscription;
;
// all subs for contact id
const getSubscriptionsForContact = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
try {
const subs = yield getRawSubs({ where: { contactId: req.params.contactId } });
res_1.success(res, subs.map(sub => jsonUtils.subscriptionToJson(sub, null)));
}
catch (e) {
console.log('ERROR getSubscriptionsForContact', e);
res_1.failure(res, e);
}
});
exports.getSubscriptionsForContact = getSubscriptionsForContact;
// create new sub
function createSubscription(req, res) {
return __awaiter(this, void 0, void 0, function* () {
const date = new Date();
date.setMilliseconds(0);
const s = jsonToSubscription(Object.assign(Object.assign({}, req.body), { count: 0, total_paid: 0, createdAt: date, ended: false, paused: false }));
if (!s.cron) {
return res_1.failure(res, 'Invalid interval');
}
try {
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
const chat = yield helpers.findOrCreateChat({
chat_id: req.body.chat_id,
owner_id: owner.id,
recipient_id: req.body.contact_id,
});
s.chatId = chat.id; // add chat id if newly created
if (!owner || !chat) {
return res_1.failure(res, 'Invalid chat or contact');
}
const sub = yield models_1.models.Subscription.create(s);
startCronJob(sub);
const isFirstMessage = true;
sendSubscriptionPayment(sub, isFirstMessage);
res_1.success(res, jsonUtils.subscriptionToJson(sub, chat));
}
catch (e) {
console.log('ERROR createSubscription', e);
res_1.failure(res, e);
}
});
}
exports.createSubscription = createSubscription;
;
function editSubscription(req, res) {
return __awaiter(this, void 0, void 0, function* () {
console.log('======> editSubscription');
const date = new Date();
date.setMilliseconds(0);
const id = parseInt(req.params.id);
const s = jsonToSubscription(Object.assign(Object.assign({}, req.body), { count: 0, createdAt: date, ended: false, paused: false }));
try {
if (!id || !s.chatId || !s.cron) {
return res_1.failure(res, 'Invalid data');
}
const subRecord = yield models_1.models.Subscription.findOne({ where: { id } });
if (!subRecord) {
return res_1.failure(res, 'No subscription found');
}
// stop so it can be restarted
if (jobs[id])
jobs[id].stop();
const obj = {
cron: s.cron,
updatedAt: date,
};
if (s.amount)
obj.amount = s.amount;
if (s.endDate)
obj.endDate = s.endDate;
if (s.endNumber)
obj.endNumber = s.endNumber;
const sub = yield subRecord.update(obj);
const end = checkSubscriptionShouldAlreadyHaveEnded(sub);
if (end) {
yield subRecord.update({ ended: true });
delete jobs[id];
}
else {
startCronJob(sub); // restart
}
const chat = yield models_1.models.Chat.findOne({ where: { id: s.chatId } });
res_1.success(res, jsonUtils.subscriptionToJson(sub, chat));
}
catch (e) {
console.log('ERROR createSubscription', e);
res_1.failure(res, e);
}
});
}
exports.editSubscription = editSubscription;
;
function jsonToSubscription(j) {
console.log("=>", j);
const cron = cronUtils.make(j.interval);
return case_1.toCamel(Object.assign(Object.assign({}, j), { cron }));
}
//# sourceMappingURL=subscriptions.js.map

1
dist/api/controllers/subscriptions.js.map

File diff suppressed because one or more lines are too long

64
dist/api/controllers/uploads.js

@ -0,0 +1,64 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/app.json')[env];
// setup disk storage
var multer = require('multer');
var avatarStorage = multer.diskStorage({
destination: (req, file, cb) => {
let dir = __dirname.includes('/dist/') ? __dirname + '/..' : __dirname;
cb(null, dir + '/../../public/uploads');
},
filename: (req, file, cb) => {
const mime = file.mimetype;
const extA = mime.split("/");
const ext = extA[extA.length - 1];
if (req.body.chat_id) {
cb(null, `chat_${req.body.chat_id}_picture.${ext}`);
}
else {
cb(null, `${req.body.contact_id}_profile_picture.${ext}`);
}
}
});
var avatarUpload = multer({ storage: avatarStorage });
exports.avatarUpload = avatarUpload;
const uploadFile = (req, res) => __awaiter(void 0, void 0, void 0, function* () {
const { contact_id, chat_id } = req.body;
const { file } = req;
const photo_url = config.node_http_protocol +
'://' +
process.env.NODE_IP +
'/static/uploads/' +
file.filename;
if (contact_id) {
const contact = yield models_1.models.Contact.findOne({ where: { id: contact_id } });
if (contact)
contact.update({ photoUrl: photo_url });
}
if (chat_id) {
const chat = yield models_1.models.Chat.findOne({ where: { id: chat_id } });
if (chat)
chat.update({ photoUrl: photo_url });
}
res.status(200);
res.json({
success: true,
contact_id: parseInt(contact_id || 0),
chat_id: parseInt(chat_id || 0),
photo_url
});
res.end();
});
exports.uploadFile = uploadFile;
//# sourceMappingURL=uploads.js.map

1
dist/api/controllers/uploads.js.map

@ -0,0 +1 @@
{"version":3,"file":"uploads.js","sourceRoot":"","sources":["../../../api/controllers/uploads.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,sCAAgC;AAEhC,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC;AAClD,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,wBAAwB,CAAC,CAAC,GAAG,CAAC,CAAC;AAElE,qBAAqB;AACrB,IAAI,MAAM,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;AAC9B,IAAI,aAAa,GAAG,MAAM,CAAC,WAAW,CAAC;IACrC,WAAW,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QAC7B,IAAI,GAAG,GAAG,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,SAAS,GAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;QACpE,EAAE,CAAC,IAAI,EAAE,GAAG,GAAG,uBAAuB,CAAC,CAAA;IACzC,CAAC;IACD,QAAQ,EAAE,CAAC,GAAG,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAA;QAC1B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;QAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,GAAC,CAAC,CAAC,CAAA;QAC/B,IAAG,GAAG,CAAC,IAAI,CAAC,OAAO,EAAC;YAClB,EAAE,CAAC,IAAI,EAAE,QAAQ,GAAG,CAAC,IAAI,CAAC,OAAO,YAAY,GAAG,EAAE,CAAC,CAAA;SACpD;aAAM;YACL,EAAE,CAAC,IAAI,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC,UAAU,oBAAoB,GAAG,EAAE,CAAC,CAAA;SAC1D;IACH,CAAC;CACF,CAAC,CAAA;AACF,IAAI,YAAY,GAAG,MAAM,CAAC,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAA;AAkCnD,oCAAY;AAhCd,MAAM,UAAU,GAAG,CAAO,GAAG,EAAE,GAAG,EAAE,EAAE;IACpC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,IAAI,CAAA;IACxC,MAAM,EAAE,IAAI,EAAE,GAAG,GAAG,CAAA;IAEpB,MAAM,SAAS,GACb,MAAM,CAAC,kBAAkB;QACzB,KAAK;QACL,OAAO,CAAC,GAAG,CAAC,OAAO;QACnB,kBAAkB;QAClB,IAAI,CAAC,QAAQ,CAAA;IAEf,IAAG,UAAU,EAAC;QACZ,MAAM,OAAO,GAAG,MAAM,eAAM,CAAC,OAAO,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,CAAC,CAAA;QAC3E,IAAG,OAAO;YAAE,OAAO,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAA;KACpD;IAED,IAAG,OAAO,EAAC;QACT,MAAM,IAAI,GAAG,MAAM,eAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,CAAC,CAAA;QAClE,IAAG,IAAI;YAAE,IAAI,CAAC,MAAM,CAAC,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAA;KAC9C;IAED,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;IACf,GAAG,CAAC,IAAI,CAAC;QACP,OAAO,EAAE,IAAI;QACb,UAAU,EAAE,QAAQ,CAAC,UAAU,IAAE,CAAC,CAAC;QACnC,OAAO,EAAE,QAAQ,CAAC,OAAO,IAAE,CAAC,CAAC;QAC7B,SAAS;KACV,CAAC,CAAC;IACH,GAAG,CAAC,GAAG,EAAE,CAAC;AACZ,CAAC,CAAA,CAAA;AAIA,gCAAU"}

65
dist/api/crypto/rsa.js

@ -0,0 +1,65 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const crypto = require("crypto");
function encrypt(key, txt) {
try {
const pubc = cert.pub(key);
const buf = crypto.publicEncrypt({
key: pubc,
padding: crypto.constants.RSA_PKCS1_PADDING,
}, Buffer.from(txt, 'utf-8'));
return buf.toString('base64');
}
catch (e) {
return '';
}
}
exports.encrypt = encrypt;
function decrypt(privateKey, enc) {
try {
const privc = cert.priv(privateKey);
const buf = crypto.privateDecrypt({
key: privc,
padding: crypto.constants.RSA_PKCS1_PADDING,
}, Buffer.from(enc, 'base64'));
return buf.toString('utf-8');
}
catch (e) {
return '';
}
}
exports.decrypt = decrypt;
function testRSA() {
crypto.generateKeyPair('rsa', {
modulusLength: 2048
}, (err, publicKey, priv) => {
const pubPEM = publicKey.export({
type: 'pkcs1', format: 'pem'
});
const pub = cert.unpub(pubPEM);
const msg = 'hi';
const enc = encrypt(pub, msg);
const dec = decrypt(priv, enc);
console.log("FINAL:", dec);
});
}
exports.testRSA = testRSA;
const cert = {
unpub: function (key) {
let s = key;
s = s.replace('-----BEGIN RSA PUBLIC KEY-----', '');
s = s.replace('-----END RSA PUBLIC KEY-----', '');
return s.replace(/[\r\n]+/gm, '');
},
pub: function (key) {
return '-----BEGIN RSA PUBLIC KEY-----\n' +
key + '\n' +
'-----END RSA PUBLIC KEY-----';
},
priv: function (key) {
return '-----BEGIN RSA PRIVATE KEY-----\n' +
key + '\n' +
'-----END RSA PRIVATE KEY-----';
}
};
//# sourceMappingURL=rsa.js.map

1
dist/api/crypto/rsa.js.map

@ -0,0 +1 @@
{"version":3,"file":"rsa.js","sourceRoot":"","sources":["../../../api/crypto/rsa.ts"],"names":[],"mappings":";;AAAA,iCAAiC;AAEjC,SAAgB,OAAO,CAAC,GAAG,EAAE,GAAG;IAC9B,IAAG;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QAC1B,MAAM,GAAG,GAAG,MAAM,CAAC,aAAa,CAAC;YAC/B,GAAG,EAAC,IAAI;YACR,OAAO,EAAC,MAAM,CAAC,SAAS,CAAC,iBAAiB;SAC3C,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAC,OAAO,CAAC,CAAC,CAAA;QAC5B,OAAO,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAA;KAC9B;IAAC,OAAM,CAAC,EAAE;QACT,OAAO,EAAE,CAAA;KACV;AACH,CAAC;AAXD,0BAWC;AAED,SAAgB,OAAO,CAAC,UAAU,EAAE,GAAG;IACrC,IAAG;QACD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;QACnC,MAAM,GAAG,GAAG,MAAM,CAAC,cAAc,CAAC;YAChC,GAAG,EAAC,KAAK;YACT,OAAO,EAAC,MAAM,CAAC,SAAS,CAAC,iBAAiB;SAC3C,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,EAAC,QAAQ,CAAC,CAAC,CAAA;QAC7B,OAAO,GAAG,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAA;KAC7B;IAAC,OAAM,CAAC,EAAE;QACT,OAAO,EAAE,CAAA;KACV;AACH,CAAC;AAXD,0BAWC;AAED,SAAgB,OAAO;IACrB,MAAM,CAAC,eAAe,CAAC,KAAK,EAAE;QAC5B,aAAa,EAAE,IAAI;KACpB,EAAE,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,EAAC,EAAE;QACzB,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAC;YAC9B,IAAI,EAAC,OAAO,EAAC,MAAM,EAAC,KAAK;SAC1B,CAAC,CAAA;QACF,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAA;QAE9B,MAAM,GAAG,GAAG,IAAI,CAAA;QAChB,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAA;QAE7B,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,EAAE,GAAG,CAAC,CAAA;QAC9B,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAC,GAAG,CAAC,CAAA;IAC3B,CAAC,CAAC,CAAA;AACJ,CAAC;AAfD,0BAeC;AAED,MAAM,IAAI,GAAG;IACX,KAAK,EAAE,UAAS,GAAG;QACjB,IAAI,CAAC,GAAG,GAAG,CAAA;QACX,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,gCAAgC,EAAC,EAAE,CAAC,CAAA;QAClD,CAAC,GAAG,CAAC,CAAC,OAAO,CAAC,8BAA8B,EAAC,EAAE,CAAC,CAAA;QAChD,OAAO,CAAC,CAAC,OAAO,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;IACnC,CAAC;IACD,GAAG,EAAC,UAAS,GAAG;QACd,OAAO,kCAAkC;YACvC,GAAG,GAAG,IAAI;YACV,8BAA8B,CAAA;IAClC,CAAC;IACD,IAAI,EAAC,UAAS,GAAG;QACf,OAAO,mCAAmC;YACxC,GAAG,GAAG,IAAI;YACV,+BAA+B,CAAA;IACnC,CAAC;CACF,CAAA"}

150
dist/api/grpc/index.js

@ -0,0 +1,150 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("../models");
const socket = require("../utils/socket");
const hub_1 = require("../hub");
const jsonUtils = require("../utils/json");
const decodeUtils = require("../utils/decode");
const lightning_1 = require("../utils/lightning");
const constants = require(__dirname + '/../../config/constants.json');
function parseKeysendInvoice(i, actions) {
const recs = i.htlcs && i.htlcs[0] && i.htlcs[0].custom_records;
const buf = recs && recs[lightning_1.SPHINX_CUSTOM_RECORD_KEY];
const data = buf && buf.toString();
const value = i && i.value && parseInt(i.value);
if (!data)
return;
let payload;
if (data[0] === '{') {
try {
payload = JSON.parse(data);
}
catch (e) { }
}
else {
const threads = weave(data);
if (threads)
payload = JSON.parse(threads);
}
if (payload) {
const dat = payload.content || payload;
if (value && dat && dat.message) {
dat.message.amount = value;
}
if (actions[payload.type]) {
actions[payload.type](payload);
}
else {
console.log('Incorrect payload type:', payload.type);
}
}
}
const chunks = {};
function weave(p) {
const pa = p.split('_');
if (pa.length < 4)
return;
const ts = pa[0];
const i = pa[1];
const n = pa[2];
const m = pa.filter((u, i) => i > 2).join('_');
chunks[ts] = chunks[ts] ? [...chunks[ts], { i, n, m }] : [{ i, n, m }];
if (chunks[ts].length === parseInt(n)) {
// got em all!
const all = chunks[ts];
let payload = '';
all.slice().sort((a, b) => a.i - b.i).forEach(obj => {
payload += obj.m;
});
delete chunks[ts];
return payload;
}
}
function subscribeInvoices(actions) {
return new Promise((resolve, reject) => __awaiter(this, void 0, void 0, function* () {
const lightning = yield lightning_1.loadLightning();
var call = lightning.subscribeInvoices();
call.on('data', function (response) {
return __awaiter(this, void 0, void 0, function* () {
// console.log('subscribed invoices', { response })
if (response['state'] !== 'SETTLED') {
return;
}
// console.log("IS KEYSEND", response.is_keysend)
if (response.is_keysend) {
parseKeysendInvoice(response, actions);
}
else {
const invoice = yield models_1.models.Message.findOne({ where: { type: constants.message_types.invoice, payment_request: response['payment_request'] } });
if (invoice == null) {
// console.log("ERROR: Invoice " + response['payment_request'] + " not found");
socket.sendJson({
type: 'invoice_payment',
response: { invoice: response['payment_request'] }
});
return;
}
models_1.models.Message.update({ status: constants.statuses.confirmed }, { where: { id: invoice.id } });
let decodedPaymentRequest = decodeUtils.decode(response['payment_request']);
var paymentHash = "";
for (var i = 0; i < decodedPaymentRequest["data"]["tags"].length; i++) {
let tag = decodedPaymentRequest["data"]["tags"][i];
if (tag['description'] == 'payment_hash') {
paymentHash = tag['value'];
break;
}
}
let settleDate = parseInt(response['settle_date'] + '000');
const chat = yield models_1.models.Chat.findOne({ where: { id: invoice.chatId } });
const contactIds = JSON.parse(chat.contactIds);
const senderId = contactIds.find(id => id != invoice.sender);
const message = yield models_1.models.Message.create({
chatId: invoice.chatId,
type: constants.message_types.payment,
sender: senderId,
amount: response['amt_paid_sat'],
amountMsat: response['amt_paid_msat'],
paymentHash: paymentHash,
date: new Date(settleDate),
messageContent: response['memo'],
status: constants.statuses.confirmed,
createdAt: new Date(settleDate),
updatedAt: new Date(settleDate)
});
socket.sendJson({
type: 'payment',
response: jsonUtils.messageToJson(message, chat)
});
const sender = yield models_1.models.Contact.findOne({ where: { id: senderId } });
hub_1.sendNotification(chat, sender.alias, 'message');
}
});
});
call.on('status', function (status) {
console.log("Status", status);
resolve(status);
});
call.on('error', function (err) {
// console.log(err)
reject(err);
});
call.on('end', function () {
console.log("Closed stream");
// The server has closed the stream.
});
setTimeout(() => {
resolve(null);
}, 100);
}));
}
exports.subscribeInvoices = subscribeInvoices;
//# sourceMappingURL=index.js.map

1
dist/api/grpc/index.js.map

File diff suppressed because one or more lines are too long

240
dist/api/helpers.js

@ -0,0 +1,240 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("./models");
const md5 = require("md5");
const lightning_1 = require("./utils/lightning");
const msg_1 = require("./utils/msg");
const constants = require('../config/constants.json');
const findOrCreateChat = (params) => __awaiter(void 0, void 0, void 0, function* () {
const { chat_id, owner_id, recipient_id } = params;
let chat;
let date = new Date();
date.setMilliseconds(0);
if (chat_id) {
chat = yield models_1.models.Chat.findOne({ where: { id: chat_id } });
// console.log('findOrCreateChat: chat_id exists')
}
else {
console.log("chat does not exists, create new");
const owner = yield models_1.models.Contact.findOne({ where: { id: owner_id } });
const recipient = yield models_1.models.Contact.findOne({ where: { id: recipient_id } });
const uuid = md5([owner.publicKey, recipient.publicKey].sort().join("-"));
// find by uuid
chat = yield models_1.models.Chat.findOne({ where: { uuid } });
if (!chat) { // no chat! create new
chat = yield models_1.models.Chat.create({
uuid: uuid,
contactIds: JSON.stringify([parseInt(owner_id), parseInt(recipient_id)]),
createdAt: date,
updatedAt: date,
type: constants.chat_types.conversation
});
}
}
return chat;
});
exports.findOrCreateChat = findOrCreateChat;
const sendContactKeys = (args) => __awaiter(void 0, void 0, void 0, function* () {
const { type, contactIds, contactPubKey, sender, success, failure } = args;
const msg = newkeyexchangemsg(type, sender);
let yes = null;
let no = null;
let cids = contactIds;
if (!contactIds)
cids = [null]; // nully
yield asyncForEach(cids, (contactId) => __awaiter(void 0, void 0, void 0, function* () {
let destination_key;
if (!contactId) { // nully
destination_key = contactPubKey;
}
else {
if (contactId == sender.id) {
return;
}
const contact = yield models_1.models.Contact.findOne({ where: { id: contactId } });
destination_key = contact.publicKey;
}
performKeysendMessage({
destination_key,
amount: 1,
msg: JSON.stringify(msg),
success: (data) => {
yes = data;
},
failure: (error) => {
no = error;
}
});
}));
if (no && failure) {
failure(no);
}
if (!no && yes && success) {
success(yes);
}
});
exports.sendContactKeys = sendContactKeys;
const sendMessage = (params) => __awaiter(void 0, void 0, void 0, function* () {
const { type, chat, message, sender, amount, success, failure } = params;
const m = newmsg(type, chat, sender, message);
const contactIds = typeof chat.contactIds === 'string' ? JSON.parse(chat.contactIds) : chat.contactIds;
let yes = null;
let no = null;
console.log('all contactIds', contactIds);
yield asyncForEach(contactIds, (contactId) => __awaiter(void 0, void 0, void 0, function* () {
if (contactId == sender.id) {
return;
}
const contact = yield models_1.models.Contact.findOne({ where: { id: contactId } });
const destkey = contact.publicKey;
const finalMsg = yield msg_1.personalizeMessage(m, contactId, destkey);
const opts = {
dest: destkey,
data: JSON.stringify(finalMsg),
amt: amount || 1,
};
try {
const r = yield lightning_1.keysendMessage(opts);
yes = r;
}
catch (e) {
console.log("KEYSEND ERROR", e);
no = e;
}
}));
if (yes) {
if (success)
success(yes);
}
else {
if (failure)
failure(no);
}
});
exports.sendMessage = sendMessage;
const performKeysendMessage = ({ destination_key, amount, msg, success, failure }) => __awaiter(void 0, void 0, void 0, function* () {
const opts = {
dest: destination_key,
data: msg || JSON.stringify({}),
amt: amount || 1
};
try {
const r = yield lightning_1.keysendMessage(opts);
console.log("MESSAGE SENT outside SW!", r);
if (success)
success(r);
}
catch (e) {
console.log("MESSAGE ERROR", e);
if (failure)
failure(e);
}
});
exports.performKeysendMessage = performKeysendMessage;
function findOrCreateContactByPubkey(senderPubKey) {
return __awaiter(this, void 0, void 0, function* () {
let sender = yield models_1.models.Contact.findOne({ where: { publicKey: senderPubKey } });
if (!sender) {
sender = yield models_1.models.Contact.create({
publicKey: senderPubKey,
alias: "Unknown",
status: 1
});
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
sendContactKeys({
contactIds: [sender.id],
sender: owner,
type: constants.message_types.contact_key,
});
}
return sender;
});
}
exports.findOrCreateContactByPubkey = findOrCreateContactByPubkey;
function findOrCreateChatByUUID(chat_uuid, contactIds) {
return __awaiter(this, void 0, void 0, function* () {
let chat = yield models_1.models.Chat.findOne({ where: { uuid: chat_uuid } });
if (!chat) {
var date = new Date();
date.setMilliseconds(0);
chat = yield models_1.models.Chat.create({
uuid: chat_uuid,
contactIds: JSON.stringify(contactIds || []),
createdAt: date,
updatedAt: date,
type: 0 // conversation
});
}
return chat;
});
}
exports.findOrCreateChatByUUID = findOrCreateChatByUUID;
function sleep(ms) {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => setTimeout(resolve, ms));
});
}
exports.sleep = sleep;
function parseReceiveParams(payload) {
return __awaiter(this, void 0, void 0, function* () {
const dat = payload.content || payload;
const sender_pub_key = dat.sender.pub_key;
const chat_uuid = dat.chat.uuid;
const chat_type = dat.chat.type;
const chat_members = dat.chat.members || {};
const chat_name = dat.chat.name;
const amount = dat.message.amount;
const content = dat.message.content;
const mediaToken = dat.message.mediaToken;
const msg_id = dat.message.id || 0;
const mediaKey = dat.message.mediaKey;
const mediaType = dat.message.mediaType;
const isGroup = chat_type && chat_type == constants.chat_types.group;
let sender;
let chat;
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
if (isGroup) {
sender = yield models_1.models.Contact.findOne({ where: { publicKey: sender_pub_key } });
chat = yield models_1.models.Chat.findOne({ where: { uuid: chat_uuid } });
}
else {
sender = yield findOrCreateContactByPubkey(sender_pub_key);
chat = yield findOrCreateChatByUUID(chat_uuid, [parseInt(owner.id), parseInt(sender.id)]);
}
return { owner, sender, chat, sender_pub_key, chat_uuid, amount, content, mediaToken, mediaKey, mediaType, chat_type, msg_id, chat_members, chat_name };
});
}
exports.parseReceiveParams = parseReceiveParams;
function asyncForEach(array, callback) {
return __awaiter(this, void 0, void 0, function* () {
for (let index = 0; index < array.length; index++) {
yield callback(array[index], index, array);
}
});
}
function newmsg(type, chat, sender, message) {
return {
type: type,
chat: Object.assign(Object.assign(Object.assign({ uuid: chat.uuid }, chat.name && { name: chat.name }), chat.type && { type: chat.type }), chat.members && { members: chat.members }),
message: message,
sender: {
pub_key: sender.publicKey,
}
};
}
function newkeyexchangemsg(type, sender) {
return {
type: type,
sender: Object.assign({ pub_key: sender.publicKey, contact_key: sender.contactKey }, sender.alias && { alias: sender.alias })
};
}
//# sourceMappingURL=helpers.js.map

1
dist/api/helpers.js.map

File diff suppressed because one or more lines are too long

201
dist/api/hub.js

@ -0,0 +1,201 @@
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const models_1 = require("./models");
const fetch = require("node-fetch");
const sequelize_1 = require("sequelize");
const socket = require("./utils/socket");
const jsonUtils = require("./utils/json");
const helpers = require("./helpers");
const nodeinfo_1 = require("./utils/nodeinfo");
const constants = require(__dirname + '/../config/constants.json');
const env = process.env.NODE_ENV || 'development';
const config = require('../config/app.json')[env];
const checkInviteHub = (params = {}) => __awaiter(void 0, void 0, void 0, function* () {
if (env != "production") {
return;
}
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
//console.log('[hub] checking invites ping')
const inviteStrings = yield models_1.models.Invite.findAll({ where: { status: { [sequelize_1.Op.notIn]: [constants.invite_statuses.complete, constants.invite_statuses.expired] } } }).map(invite => invite.inviteString);
fetch(config.hub_api_url + '/invites/check', {
method: 'POST',
body: JSON.stringify({ invite_strings: inviteStrings }),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
if (json.object) {
json.object.invites.map((object) => __awaiter(void 0, void 0, void 0, function* () {
const invite = object.invite;
const pubkey = object.pubkey;
const price = object.price;
const dbInvite = yield models_1.models.Invite.findOne({ where: { inviteString: invite.pin } });
const contact = yield models_1.models.Contact.findOne({ where: { id: dbInvite.contactId } });
if (dbInvite.status != invite.invite_status) {
dbInvite.update({ status: invite.invite_status, price: price });
socket.sendJson({
type: 'invite',
response: jsonUtils.inviteToJson(dbInvite)
});
if (dbInvite.status == constants.invite_statuses.ready && contact) {
sendNotification(-1, contact.alias, 'invite');
}
}
if (pubkey && dbInvite.status == constants.invite_statuses.complete && contact) {
contact.update({ publicKey: pubkey, status: constants.contact_statuses.confirmed });
var contactJson = jsonUtils.contactToJson(contact);
contactJson.invite = jsonUtils.inviteToJson(dbInvite);
socket.sendJson({
type: 'contact',
response: contactJson
});
helpers.sendContactKeys({
contactIds: [contact.id],
sender: owner,
type: constants.message_types.contact_key,
});
}
}));
}
})
.catch(error => {
console.log('[hub error]', error);
});
});
const pingHub = (params = {}) => __awaiter(void 0, void 0, void 0, function* () {
if (env != "production") {
return;
}
const node = yield nodeinfo_1.nodeinfo();
sendHubCall(Object.assign(Object.assign({}, params), { node }));
});
const sendHubCall = (params) => {
// console.log('[hub] sending ping')
fetch(config.hub_api_url + '/ping', {
method: 'POST',
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
// ?
})
.catch(error => {
console.log('[hub error]', error);
});
};
exports.sendHubCall = sendHubCall;
const pingHubInterval = (ms) => {
setInterval(pingHub, ms);
};
exports.pingHubInterval = pingHubInterval;
const checkInvitesHubInterval = (ms) => {
setInterval(checkInviteHub, ms);
};
exports.checkInvitesHubInterval = checkInvitesHubInterval;
const finishInviteInHub = (params, onSuccess, onFailure) => {
fetch(config.hub_api_url + '/invites/finish', {
method: 'POST',
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
if (json.object) {
console.log('[hub] finished invite to hub');
onSuccess(json);
}
else {
console.log('[hub] fail to finish invite in hub');
onFailure(json);
}
});
};
exports.finishInviteInHub = finishInviteInHub;
const payInviteInHub = (invite_string, params, onSuccess, onFailure) => {
fetch(config.hub_api_url + '/invites/' + invite_string + '/pay', {
method: 'POST',
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
if (json.object) {
console.log('[hub] finished pay to hub');
onSuccess(json);
}
else {
console.log('[hub] fail to pay invite in hub');
onFailure(json);
}
});
};
exports.payInviteInHub = payInviteInHub;
const createInviteInHub = (params, onSuccess, onFailure) => {
fetch(config.hub_api_url + '/invites', {
method: 'POST',
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => {
if (json.object) {
console.log('[hub] sent invite to be created to hub');
onSuccess(json);
}
else {
console.log('[hub] fail to create invite in hub');
onFailure(json);
}
});
};
exports.createInviteInHub = createInviteInHub;
const sendNotification = (chat, name, type) => __awaiter(void 0, void 0, void 0, function* () {
let message = `You have a new message from ${name}`;
if (type === 'invite') {
message = `Your invite to ${name} is ready`;
}
if (type === 'group') {
message = `You have been added to group ${name}`;
}
if (type === 'message' && chat.type == constants.chat_types.group && chat.name && chat.name.length) {
message += ` on ${chat.name}`;
}
console.log('[send notification]', { chat_id: chat.id, message });
if (chat.isMuted) {
console.log('[send notification] skipping. chat is muted.');
return;
}
const owner = yield models_1.models.Contact.findOne({ where: { isOwner: true } });
if (!owner.deviceId) {
console.log('[send notification] skipping. owner.deviceId not set.');
return;
}
const unseenMessages = yield models_1.models.Message.findAll({ where: { sender: { [sequelize_1.Op.ne]: owner.id }, seen: false } });
const params = {
device_id: owner.deviceId,
notification: {
chat_id: chat.id,
message,
badge: unseenMessages.length
}
};
fetch("http://hub.sphinx.chat/api/v1/nodes/notify", {
method: 'POST',
body: JSON.stringify(params),
headers: { 'Content-Type': 'application/json' }
})
.then(res => res.json())
.then(json => console.log('[hub notification]', json));
});
exports.sendNotification = sendNotification;
//# sourceMappingURL=hub.js.map

1
dist/api/hub.js.map

File diff suppressed because one or more lines are too long

10
dist/api/models/index.js

@ -0,0 +1,10 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const sequelize_typescript_1 = require("sequelize-typescript");
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../../config/config.json')[env];
const sequelize = new sequelize_typescript_1.Sequelize(Object.assign(Object.assign({}, config), { logging: process.env.SQL_LOG === 'true' ? console.log : false, models: [__dirname + '/ts'] }));
exports.sequelize = sequelize;
const models = sequelize.models;
exports.models = models;
//# sourceMappingURL=index.js.map

1
dist/api/models/index.js.map

@ -0,0 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../api/models/index.ts"],"names":[],"mappings":";;AAAA,+DAA+C;AAE/C,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,aAAa,CAAC;AAClD,MAAM,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,2BAA2B,CAAC,CAAC,GAAG,CAAC,CAAC;AAErE,MAAM,SAAS,GAAG,IAAI,gCAAS,iCAC1B,MAAM,KACT,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,OAAO,KAAG,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAC3D,MAAM,EAAE,CAAC,SAAS,GAAG,KAAK,CAAC,IAC3B,CAAA;AAIA,8BAAS;AAHX,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,CAAA;AAI7B,wBAAM"}

72
dist/api/models/ts/chat.js

@ -0,0 +1,72 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
const sequelize_typescript_1 = require("sequelize-typescript");
let Chat = class Chat extends sequelize_typescript_1.Model {
};
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
}),
__metadata("design:type", Number)
], Chat.prototype, "id", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Chat.prototype, "uuid", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Chat.prototype, "name", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Chat.prototype, "photoUrl", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Chat.prototype, "type", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Chat.prototype, "status", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Chat.prototype, "contactIds", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Boolean)
], Chat.prototype, "isMuted", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Chat.prototype, "createdAt", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Chat.prototype, "updatedAt", void 0);
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BOOLEAN,
defaultValue: false,
allowNull: false
}),
__metadata("design:type", Boolean)
], Chat.prototype, "deleted", void 0);
Chat = __decorate([
sequelize_typescript_1.Table({ tableName: 'sphinx_chats', underscored: true })
], Chat);
exports.default = Chat;
//# sourceMappingURL=chat.js.map

1
dist/api/models/ts/chat.js.map

@ -0,0 +1 @@
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../../../../api/models/ts/chat.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,+DAAsE;AAGtE,IAAqB,IAAI,GAAzB,MAAqB,IAAK,SAAQ,4BAAW;CA4C5C,CAAA;AApCC;IANC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,MAAM;QACrB,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,IAAI;QACZ,aAAa,EAAE,IAAI;KACpB,CAAC;;gCACQ;AAGV;IADC,6BAAM;;kCACK;AAGZ;IADC,6BAAM;;kCACK;AAGZ;IADC,6BAAM;;sCACS;AAGhB;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;kCACZ;AAGZ;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;oCACV;AAGd;IADC,6BAAM;;wCACW;AAGlB;IADC,6BAAM;;qCACS;AAGhB;IADC,6BAAM;8BACI,IAAI;uCAAA;AAGf;IADC,6BAAM;8BACI,IAAI;uCAAA;AAOf;IALC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,OAAO;QACtB,YAAY,EAAE,KAAK;QACnB,SAAS,EAAE,KAAK;KACjB,CAAC;;qCACc;AA1CG,IAAI;IADxB,4BAAK,CAAC,EAAC,SAAS,EAAE,cAAc,EAAE,WAAW,EAAE,IAAI,EAAC,CAAC;GACjC,IAAI,CA4CxB;kBA5CoB,IAAI"}

84
dist/api/models/ts/contact.js

@ -0,0 +1,84 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
const sequelize_typescript_1 = require("sequelize-typescript");
let Contact = class Contact extends sequelize_typescript_1.Model {
};
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
}),
__metadata("design:type", Number)
], Contact.prototype, "id", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Contact.prototype, "publicKey", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Contact.prototype, "nodeAlias", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Contact.prototype, "alias", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Contact.prototype, "photoUrl", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Boolean)
], Contact.prototype, "isOwner", void 0);
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BOOLEAN,
defaultValue: false,
allowNull: false
}),
__metadata("design:type", Boolean)
], Contact.prototype, "deleted", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Contact.prototype, "authToken", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Contact.prototype, "remoteId", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Contact.prototype, "status", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.TEXT),
__metadata("design:type", String)
], Contact.prototype, "contactKey", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Contact.prototype, "deviceId", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Contact.prototype, "createdAt", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Contact.prototype, "updatedAt", void 0);
Contact = __decorate([
sequelize_typescript_1.Table({ tableName: 'sphinx_contacts', underscored: true })
], Contact);
exports.default = Contact;
//# sourceMappingURL=contact.js.map

1
dist/api/models/ts/contact.js.map

@ -0,0 +1 @@
{"version":3,"file":"contact.js","sourceRoot":"","sources":["../../../../api/models/ts/contact.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,+DAAsE;AAGtE,IAAqB,OAAO,GAA5B,MAAqB,OAAQ,SAAQ,4BAAc;CAqDlD,CAAA;AA7CC;IANC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,MAAM;QACrB,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,IAAI;QACZ,aAAa,EAAE,IAAI;KACpB,CAAC;;mCACQ;AAGV;IADC,6BAAM;;0CACU;AAGjB;IADC,6BAAM;;0CACU;AAGjB;IADC,6BAAM;;sCACM;AAGb;IADC,6BAAM;;yCACS;AAGhB;IADC,6BAAM;;wCACS;AAOhB;IALC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,OAAO;QACtB,YAAY,EAAE,KAAK;QACnB,SAAS,EAAE,KAAK;KACjB,CAAC;;wCACc;AAGhB;IADC,6BAAM;;0CACU;AAGjB;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;yCACR;AAGhB;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;uCACV;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,IAAI,CAAC;;2CACJ;AAGlB;IADC,6BAAM;;yCACS;AAGhB;IADC,6BAAM;8BACI,IAAI;0CAAA;AAGf;IADC,6BAAM;8BACI,IAAI;0CAAA;AAnDI,OAAO;IAD3B,4BAAK,CAAC,EAAC,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE,IAAI,EAAC,CAAC;GACpC,OAAO,CAqD3B;kBArDoB,OAAO"}

56
dist/api/models/ts/invite.js

@ -0,0 +1,56 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
const sequelize_typescript_1 = require("sequelize-typescript");
let Invite = class Invite extends sequelize_typescript_1.Model {
};
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
}),
__metadata("design:type", Number)
], Invite.prototype, "id", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Invite.prototype, "inviteString", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Invite.prototype, "welcomeMessage", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Invite.prototype, "contactId", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Invite.prototype, "status", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.DECIMAL(10, 2)),
__metadata("design:type", Number)
], Invite.prototype, "price", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Invite.prototype, "createdAt", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Invite.prototype, "updatedAt", void 0);
Invite = __decorate([
sequelize_typescript_1.Table({ tableName: 'sphinx_invites', underscored: true })
], Invite);
exports.default = Invite;
//# sourceMappingURL=invite.js.map

1
dist/api/models/ts/invite.js.map

@ -0,0 +1 @@
{"version":3,"file":"invite.js","sourceRoot":"","sources":["../../../../api/models/ts/invite.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,+DAAsE;AAGtE,IAAqB,MAAM,GAA3B,MAAqB,MAAO,SAAQ,4BAAa;CA+BhD,CAAA;AAvBC;IANC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,MAAM;QACrB,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,IAAI;QACZ,aAAa,EAAE,IAAI;KACpB,CAAC;;kCACQ;AAGV;IADC,6BAAM;;4CACa;AAGpB;IADC,6BAAM;;8CACe;AAGtB;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;yCACP;AAGjB;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;sCACV;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;;qCACnB;AAGb;IADC,6BAAM;8BACI,IAAI;yCAAA;AAGf;IADC,6BAAM;8BACI,IAAI;yCAAA;AA7BI,MAAM;IAD1B,4BAAK,CAAC,EAAC,SAAS,EAAE,gBAAgB,EAAE,WAAW,EAAE,IAAI,EAAC,CAAC;GACnC,MAAM,CA+B1B;kBA/BoB,MAAM"}

59
dist/api/models/ts/mediaKey.js

@ -0,0 +1,59 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
const sequelize_typescript_1 = require("sequelize-typescript");
/*
Used for media uploads. When you upload a file,
also upload the symetric key encrypted for each chat member.
When they buy the file, they can retrieve the key from here.
"received" media keys are not stored here, only in Message
*/
let MediaKey = class MediaKey extends sequelize_typescript_1.Model {
};
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
}),
__metadata("design:type", Number)
], MediaKey.prototype, "id", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], MediaKey.prototype, "muid", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], MediaKey.prototype, "chatId", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], MediaKey.prototype, "receiver", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], MediaKey.prototype, "key", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], MediaKey.prototype, "messageId", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], MediaKey.prototype, "createdAt", void 0);
MediaKey = __decorate([
sequelize_typescript_1.Table({ tableName: 'sphinx_media_keys', underscored: true })
], MediaKey);
exports.default = MediaKey;
//# sourceMappingURL=mediaKey.js.map

1
dist/api/models/ts/mediaKey.js.map

@ -0,0 +1 @@
{"version":3,"file":"mediaKey.js","sourceRoot":"","sources":["../../../../api/models/ts/mediaKey.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,+DAAsE;AAEtE;;;;;;EAME;AAGF,IAAqB,QAAQ,GAA7B,MAAqB,QAAS,SAAQ,4BAAe;CA2BpD,CAAA;AAnBC;IANC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,MAAM;QACrB,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,IAAI;QACZ,aAAa,EAAE,IAAI;KACpB,CAAC;;oCACQ;AAGV;IADC,6BAAM;;sCACK;AAGZ;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;wCACV;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;0CACR;AAGhB;IADC,6BAAM;;qCACI;AAGX;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;2CACP;AAGjB;IADC,6BAAM;8BACI,IAAI;2CAAA;AA1BI,QAAQ;IAD5B,4BAAK,CAAC,EAAC,SAAS,EAAE,mBAAmB,EAAE,WAAW,EAAE,IAAI,EAAC,CAAC;GACtC,QAAQ,CA2B5B;kBA3BoB,QAAQ"}

128
dist/api/models/ts/message.js

@ -0,0 +1,128 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
const sequelize_typescript_1 = require("sequelize-typescript");
let Message = class Message extends sequelize_typescript_1.Model {
};
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
}),
__metadata("design:type", Number)
], Message.prototype, "id", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Message.prototype, "chatId", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Message.prototype, "type", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Message.prototype, "sender", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Message.prototype, "receiver", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.DECIMAL),
__metadata("design:type", Number)
], Message.prototype, "amount", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.DECIMAL),
__metadata("design:type", Number)
], Message.prototype, "amountMsat", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Message.prototype, "paymentHash", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.TEXT),
__metadata("design:type", String)
], Message.prototype, "paymentRequest", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Message.prototype, "date", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Message.prototype, "expirationDate", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.TEXT),
__metadata("design:type", String)
], Message.prototype, "messageContent", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.TEXT),
__metadata("design:type", String)
], Message.prototype, "remoteMessageContent", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Message.prototype, "status", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.TEXT),
__metadata("design:type", String)
], Message.prototype, "statusMap", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Message.prototype, "parentId", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Message.prototype, "subscriptionId", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Message.prototype, "mediaTerms", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Message.prototype, "receipt", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Message.prototype, "mediaKey", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Message.prototype, "mediaType", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", String)
], Message.prototype, "mediaToken", void 0);
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BOOLEAN,
defaultValue: false,
allowNull: false
}),
__metadata("design:type", Boolean)
], Message.prototype, "seen", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Message.prototype, "createdAt", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Message.prototype, "updatedAt", void 0);
Message = __decorate([
sequelize_typescript_1.Table({ tableName: 'sphinx_messages', underscored: true })
], Message);
exports.default = Message;
//# sourceMappingURL=message.js.map

1
dist/api/models/ts/message.js.map

@ -0,0 +1 @@
{"version":3,"file":"message.js","sourceRoot":"","sources":["../../../../api/models/ts/message.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,+DAAsE;AAGtE,IAAqB,OAAO,GAA5B,MAAqB,OAAQ,SAAQ,4BAAc;CAqFlD,CAAA;AA7EC;IANC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,MAAM;QACrB,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,IAAI;QACZ,aAAa,EAAE,IAAI;KACpB,CAAC;;mCACQ;AAGV;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;uCACV;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;qCACZ;AAGZ;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;uCACV;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;yCACR;AAGhB;IADC,6BAAM,CAAC,+BAAQ,CAAC,OAAO,CAAC;;uCACX;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,OAAO,CAAC;;2CACP;AAGlB;IADC,6BAAM;;4CACY;AAGnB;IADC,6BAAM,CAAC,+BAAQ,CAAC,IAAI,CAAC;;+CACA;AAGtB;IADC,6BAAM;8BACD,IAAI;qCAAA;AAGV;IADC,6BAAM;8BACS,IAAI;+CAAA;AAGpB;IADC,6BAAM,CAAC,+BAAQ,CAAC,IAAI,CAAC;;+CACA;AAGtB;IADC,6BAAM,CAAC,+BAAQ,CAAC,IAAI,CAAC;;qDACM;AAG5B;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;uCACV;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,IAAI,CAAC;;0CACL;AAGjB;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;yCACR;AAGhB;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;+CACF;AAGtB;IADC,6BAAM;;2CACW;AAGlB;IADC,6BAAM;;wCACQ;AAGf;IADC,6BAAM;;yCACS;AAGhB;IADC,6BAAM;;0CACU;AAGjB;IADC,6BAAM;;2CACW;AAOlB;IALC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,OAAO;QACtB,YAAY,EAAE,KAAK;QACnB,SAAS,EAAE,KAAK;KACjB,CAAC;;qCACW;AAGb;IADC,6BAAM;8BACI,IAAI;0CAAA;AAGf;IADC,6BAAM;8BACI,IAAI;0CAAA;AApFI,OAAO;IAD3B,4BAAK,CAAC,EAAC,SAAS,EAAE,iBAAiB,EAAE,WAAW,EAAE,IAAI,EAAC,CAAC;GACpC,OAAO,CAqF3B;kBArFoB,OAAO"}

76
dist/api/models/ts/subscription.js

@ -0,0 +1,76 @@
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
const sequelize_typescript_1 = require("sequelize-typescript");
let Subscription = class Subscription extends sequelize_typescript_1.Model {
};
__decorate([
sequelize_typescript_1.Column({
type: sequelize_typescript_1.DataType.BIGINT,
primaryKey: true,
unique: true,
autoIncrement: true
}),
__metadata("design:type", Number)
], Subscription.prototype, "id", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Subscription.prototype, "chatId", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Subscription.prototype, "contactId", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.TEXT),
__metadata("design:type", String)
], Subscription.prototype, "cron", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.DECIMAL),
__metadata("design:type", Number)
], Subscription.prototype, "amount", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.DECIMAL),
__metadata("design:type", Number)
], Subscription.prototype, "totalPaid", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Subscription.prototype, "endNumber", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Subscription.prototype, "endDate", void 0);
__decorate([
sequelize_typescript_1.Column(sequelize_typescript_1.DataType.BIGINT),
__metadata("design:type", Number)
], Subscription.prototype, "count", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Boolean)
], Subscription.prototype, "ended", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Boolean)
], Subscription.prototype, "paused", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Subscription.prototype, "createdAt", void 0);
__decorate([
sequelize_typescript_1.Column,
__metadata("design:type", Date)
], Subscription.prototype, "updatedAt", void 0);
Subscription = __decorate([
sequelize_typescript_1.Table({ tableName: 'sphinx_subscriptions', underscored: true })
], Subscription);
exports.default = Subscription;
//# sourceMappingURL=subscription.js.map

1
dist/api/models/ts/subscription.js.map

@ -0,0 +1 @@
{"version":3,"file":"subscription.js","sourceRoot":"","sources":["../../../../api/models/ts/subscription.ts"],"names":[],"mappings":";;;;;;;;;;;AAAA,+DAAsE;AAGtE,IAAqB,YAAY,GAAjC,MAAqB,YAAa,SAAQ,4BAAmB;CA6C5D,CAAA;AArCC;IANC,6BAAM,CAAC;QACN,IAAI,EAAE,+BAAQ,CAAC,MAAM;QACrB,UAAU,EAAE,IAAI;QAChB,MAAM,EAAE,IAAI;QACZ,aAAa,EAAE,IAAI;KACpB,CAAC;;wCACQ;AAGV;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;4CACV;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;+CACP;AAGjB;IADC,6BAAM,CAAC,+BAAQ,CAAC,IAAI,CAAC;;0CACV;AAGZ;IADC,6BAAM,CAAC,+BAAQ,CAAC,OAAO,CAAC;;4CACX;AAGd;IADC,6BAAM,CAAC,+BAAQ,CAAC,OAAO,CAAC;;+CACR;AAGjB;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;+CACP;AAGjB;IADC,6BAAM;8BACE,IAAI;6CAAA;AAGb;IADC,6BAAM,CAAC,+BAAQ,CAAC,MAAM,CAAC;;2CACX;AAGb;IADC,6BAAM;;2CACO;AAGd;IADC,6BAAM;;4CACQ;AAGf;IADC,6BAAM;8BACI,IAAI;+CAAA;AAGf;IADC,6BAAM;8BACI,IAAI;+CAAA;AA5CI,YAAY;IADhC,4BAAK,CAAC,EAAC,SAAS,EAAE,sBAAsB,EAAE,WAAW,EAAE,IAAI,EAAC,CAAC;GACzC,YAAY,CA6ChC;kBA7CoB,YAAY"}

28
dist/api/utils/case.js

@ -0,0 +1,28 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const changeCase = require("change-case");
const dateKeys = ['date', 'createdAt', 'updatedAt', 'created_at', 'updated_at'];
function toSnake(obj) {
const ret = {};
for (let [key, value] of Object.entries(obj)) {
if (dateKeys.includes(key) && value) {
const v = value;
const d = new Date(v);
ret[changeCase.snakeCase(key)] = d.toISOString();
}
else {
ret[changeCase.snakeCase(key)] = value;
}
}
return ret;
}
exports.toSnake = toSnake;
function toCamel(obj) {
const ret = {};
for (let [key, value] of Object.entries(obj)) {
ret[changeCase.camelCase(key)] = value;
}
return ret;
}
exports.toCamel = toCamel;
//# sourceMappingURL=case.js.map

1
dist/api/utils/case.js.map

@ -0,0 +1 @@
{"version":3,"file":"case.js","sourceRoot":"","sources":["../../../api/utils/case.ts"],"names":[],"mappings":";;AAAA,0CAA0C;AAE1C,MAAM,QAAQ,GAAG,CAAC,MAAM,EAAC,WAAW,EAAC,WAAW,EAAC,YAAY,EAAC,YAAY,CAAC,CAAA;AAE3E,SAAS,OAAO,CAAC,GAAG;IAChB,MAAM,GAAG,GAAuB,EAAE,CAAA;IAClC,KAAK,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;QAC1C,IAAG,QAAQ,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,KAAK,EAAC;YAC/B,MAAM,CAAC,GAAQ,KAAK,CAAA;YACpB,MAAM,CAAC,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAA;YACrB,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAA;SACnD;aAAM;YACH,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAA;SACzC;KACJ;IACD,OAAO,GAAG,CAAA;AACd,CAAC;AAUO,0BAAO;AARf,SAAS,OAAO,CAAC,GAAG;IAChB,MAAM,GAAG,GAAuB,EAAE,CAAA;IAClC,KAAK,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE;QAC1C,GAAG,CAAC,UAAU,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,KAAK,CAAA;KACzC;IACD,OAAO,GAAG,CAAA;AACd,CAAC;AAEgB,0BAAO"}

45
dist/api/utils/cron.js

@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const parser = require("cron-parser");
function daily() {
const now = new Date();
const minute = now.getMinutes();
const hour = now.getHours();
return `${minute} ${hour} * * *`;
}
function weekly() {
const now = new Date();
const minute = now.getMinutes();
const hour = now.getHours();
const dayOfWeek = now.getDay();
return `${minute} ${hour} * * ${dayOfWeek}`;
}
function monthly() {
const now = new Date();
const minute = now.getMinutes();
const hour = now.getHours();
const dayOfMonth = now.getDate();
return `${minute} ${hour} ${dayOfMonth} * *`;
}
function parse(s) {
var interval = parser.parseExpression(s);
const next = interval.next().toString();
if (s.endsWith(' * * *')) {
return { interval: 'daily', next, ms: 86400000 };
}
if (s.endsWith(' * *')) {
return { interval: 'monthly', next, ms: 86400000 * 30 };
}
return { interval: 'weekly', next, ms: 86400000 * 7 };
}
exports.parse = parse;
function make(interval) {
if (interval === 'daily')
return daily();
if (interval === 'weekly')
return weekly();
if (interval === 'monthly')
return monthly();
}
exports.make = make;
//# sourceMappingURL=cron.js.map

1
dist/api/utils/cron.js.map

@ -0,0 +1 @@
{"version":3,"file":"cron.js","sourceRoot":"","sources":["../../../api/utils/cron.ts"],"names":[],"mappings":";;AAAA,sCAAqC;AAErC,SAAS,KAAK;IACV,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;IACtB,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,EAAE,CAAA;IAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAA;IAC3B,OAAO,GAAG,MAAM,IAAI,IAAI,QAAQ,CAAA;AACpC,CAAC;AAED,SAAS,MAAM;IACX,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;IACtB,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,EAAE,CAAA;IAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAA;IAC3B,MAAM,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,CAAA;IAC9B,OAAO,GAAG,MAAM,IAAI,IAAI,QAAQ,SAAS,EAAE,CAAA;AAC/C,CAAC;AAED,SAAS,OAAO;IACZ,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAA;IACtB,MAAM,MAAM,GAAG,GAAG,CAAC,UAAU,EAAE,CAAA;IAC/B,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAA;IAC3B,MAAM,UAAU,GAAG,GAAG,CAAC,OAAO,EAAE,CAAA;IAChC,OAAO,GAAG,MAAM,IAAI,IAAI,IAAI,UAAU,MAAM,CAAA;AAChD,CAAC;AAED,SAAS,KAAK,CAAC,CAAC;IACZ,IAAI,QAAQ,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,CAAC;IACzC,MAAM,IAAI,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,QAAQ,EAAE,CAAA;IAEvC,IAAG,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE;QACrB,OAAO,EAAC,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAC,QAAQ,EAAC,CAAA;KAChD;IACD,IAAG,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE;QACnB,OAAO,EAAC,QAAQ,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,EAAC,QAAQ,GAAC,EAAE,EAAC,CAAA;KACrD;IACD,OAAO,EAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE,EAAC,QAAQ,GAAC,CAAC,EAAC,CAAA;AACpD,CAAC;AASG,sBAAK;AAPT,SAAS,IAAI,CAAC,QAAQ;IAClB,IAAG,QAAQ,KAAG,OAAO;QAAE,OAAO,KAAK,EAAE,CAAA;IACrC,IAAG,QAAQ,KAAG,QAAQ;QAAE,OAAO,MAAM,EAAE,CAAA;IACvC,IAAG,QAAQ,KAAG,SAAS;QAAE,OAAO,OAAO,EAAE,CAAA;AAC7C,CAAC;AAIG,oBAAI"}

294
dist/api/utils/decode/index.js

@ -0,0 +1,294 @@
const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';
module.exports = {
decode: function (paymentRequest) {
let input = paymentRequest.toLowerCase();
let splitPosition = input.lastIndexOf('1');
let humanReadablePart = input.substring(0, splitPosition);
let data = input.substring(splitPosition + 1, input.length - 6);
let checksum = input.substring(input.length - 6, input.length);
if (!this.verify_checksum(humanReadablePart, this.bech32ToFiveBitArray(data + checksum))) {
return 'error';
}
return {
'human_readable_part': this.decodeHumanReadablePart(humanReadablePart),
'data': this.decodeData(data, humanReadablePart),
'checksum': checksum
};
},
decodeHumanReadablePart: function (humanReadablePart) {
let prefixes = ['lnbc', 'lntb', 'lnbcrt'];
let prefix;
prefixes.forEach(value => {
if (humanReadablePart.substring(0, value.length) === value) {
prefix = value;
}
});
if (prefix == null)
return 'error'; // A reader MUST fail if it does not understand the prefix.
let amount = this.decodeAmount(humanReadablePart.substring(prefix.length, humanReadablePart.length));
return {
'prefix': prefix,
'amount': amount
};
},
decodeData: function (data, humanReadablePart) {
let date32 = data.substring(0, 7);
let dateEpoch = this.bech32ToInt(date32);
let signature = data.substring(data.length - 104, data.length);
let tagData = data.substring(7, data.length - 104);
let decodedTags = this.decodeTags(tagData);
let value = this.bech32ToFiveBitArray(date32 + tagData);
value = this.fiveBitArrayTo8BitArray(value, true);
value = this.textToHexString(humanReadablePart).concat(this.byteArrayToHexString(value));
return {
'time_stamp': dateEpoch,
'tags': decodedTags,
'signature': this.decodeSignature(signature),
'signing_data': value
};
},
decodeSignature: function (signature) {
let data = this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(signature));
let recoveryFlag = data[data.length - 1];
let r = this.byteArrayToHexString(data.slice(0, 32));
let s = this.byteArrayToHexString(data.slice(32, data.length - 1));
return {
'r': r,
's': s,
'recovery_flag': recoveryFlag
};
},
decodeAmount: function (str) {
let multiplier = str.charAt(str.length - 1);
let amount = str.substring(0, str.length - 1);
if (amount.substring(0, 1) === '0') {
return 'error';
}
amount = Number(amount);
if (amount < 0 || !Number.isInteger(amount)) {
return 'error';
}
switch (multiplier) {
case '':
return 'Any amount'; // A reader SHOULD indicate if amount is unspecified
case 'p':
return amount / 10;
case 'n':
return amount * 100;
case 'u':
return amount * 100000;
case 'm':
return amount * 100000000;
default:
// A reader SHOULD fail if amount is followed by anything except a defined multiplier.
return 'error';
}
},
decodeTags: function (tagData) {
let tags = this.extractTags(tagData);
let decodedTags = [];
tags.forEach(value => decodedTags.push(this.decodeTag(value.type, value.length, value.data)));
return decodedTags;
},
extractTags: function (str) {
let tags = [];
while (str.length > 0) {
let type = str.charAt(0);
let dataLength = this.bech32ToInt(str.substring(1, 3));
let data = str.substring(3, dataLength + 3);
tags.push({
'type': type,
'length': dataLength,
'data': data
});
str = str.substring(3 + dataLength, str.length);
}
return tags;
},
decodeTag: function (type, length, data) {
switch (type) {
case 'p':
if (length !== 52)
break; // A reader MUST skip over a 'p' field that does not have data_length 52
return {
'type': type,
'length': length,
'description': 'payment_hash',
'value': this.byteArrayToHexString(this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data)))
};
case 'd':
return {
'type': type,
'length': length,
'description': 'description',
'value': this.bech32ToUTF8String(data)
};
case 'n':
if (length !== 53)
break; // A reader MUST skip over a 'n' field that does not have data_length 53
return {
'type': type,
'length': length,
'description': 'payee_public_key',
'value': this.byteArrayToHexString(this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data)))
};
case 'h':
if (length !== 52)
break; // A reader MUST skip over a 'h' field that does not have data_length 52
return {
'type': type,
'length': length,
'description': 'description_hash',
'value': data
};
case 'x':
return {
'type': type,
'length': length,
'description': 'expiry',
'value': this.bech32ToInt(data)
};
case 'c':
return {
'type': type,
'length': length,
'description': 'min_final_cltv_expiry',
'value': this.bech32ToInt(data)
};
case 'f':
let version = this.bech32ToFiveBitArray(data.charAt(0))[0];
if (version < 0 || version > 18)
break; // a reader MUST skip over an f field with unknown version.
data = data.substring(1, data.length);
return {
'type': type,
'length': length,
'description': 'fallback_address',
'value': {
'version': version,
'fallback_address': data
}
};
case 'r':
data = this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data));
let pubkey = data.slice(0, 33);
let shortChannelId = data.slice(33, 41);
let feeBaseMsat = data.slice(41, 45);
let feeProportionalMillionths = data.slice(45, 49);
let cltvExpiryDelta = data.slice(49, 51);
return {
'type': type,
'length': length,
'description': 'routing_information',
'value': {
'public_key': this.byteArrayToHexString(pubkey),
'short_channel_id': this.byteArrayToHexString(shortChannelId),
'fee_base_msat': this.byteArrayToInt(feeBaseMsat),
'fee_proportional_millionths': this.byteArrayToInt(feeProportionalMillionths),
'cltv_expiry_delta': this.byteArrayToInt(cltvExpiryDelta)
}
};
default:
// reader MUST skip over unknown fields
}
},
polymod: function (values) {
let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
let chk = 1;
values.forEach((value) => {
let b = (chk >> 25);
chk = (chk & 0x1ffffff) << 5 ^ value;
for (let i = 0; i < 5; i++) {
if (((b >> i) & 1) === 1) {
chk ^= GEN[i];
}
else {
chk ^= 0;
}
}
});
return chk;
},
expand: function (str) {
let array = [];
for (let i = 0; i < str.length; i++) {
array.push(str.charCodeAt(i) >> 5);
}
array.push(0);
for (let i = 0; i < str.length; i++) {
array.push(str.charCodeAt(i) & 31);
}
return array;
},
verify_checksum: function (hrp, data) {
hrp = this.expand(hrp);
let all = hrp.concat(data);
let bool = this.polymod(all);
return bool === 1;
},
byteArrayToInt: function (byteArray) {
let value = 0;
for (let i = 0; i < byteArray.length; ++i) {
value = (value << 8) + byteArray[i];
}
return value;
},
bech32ToInt: function (str) {
let sum = 0;
for (let i = 0; i < str.length; i++) {
sum = sum * 32;
sum = sum + bech32CharValues.indexOf(str.charAt(i));
}
return sum;
},
bech32ToFiveBitArray: function (str) {
let array = [];
for (let i = 0; i < str.length; i++) {
array.push(bech32CharValues.indexOf(str.charAt(i)));
}
return array;
},
fiveBitArrayTo8BitArray: function (int5Array, includeOverflow) {
let count = 0;
let buffer = 0;
let byteArray = [];
int5Array.forEach((value) => {
buffer = (buffer << 5) + value;
count += 5;
if (count >= 8) {
byteArray.push(buffer >> (count - 8) & 255);
count -= 8;
}
});
if (includeOverflow && count > 0) {
byteArray.push(buffer << (8 - count) & 255);
}
return byteArray;
},
bech32ToUTF8String: function (str) {
let int5Array = this.bech32ToFiveBitArray(str);
let byteArray = this.fiveBitArrayTo8BitArray(int5Array);
let utf8String = '';
for (let i = 0; i < byteArray.length; i++) {
utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2);
}
return decodeURIComponent(utf8String);
},
byteArrayToHexString: function (byteArray) {
return Array.prototype.map.call(byteArray, function (byte) {
return ('0' + (byte & 0xFF).toString(16)).slice(-2);
}).join('');
},
textToHexString: function (text) {
let hexString = '';
for (let i = 0; i < text.length; i++) {
hexString += text.charCodeAt(i).toString(16);
}
return hexString;
},
epochToDate: function (int) {
let date = new Date(int * 1000);
return date.toUTCString();
}
};
//# sourceMappingURL=index.js.map

1
dist/api/utils/decode/index.js.map

File diff suppressed because one or more lines are too long

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save