-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsignal-api-to-inbox
executable file
·195 lines (160 loc) · 6.6 KB
/
signal-api-to-inbox
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#!/usr/bin/env node
/* Repo: https://github.com/jneidel/signal-cli-to-file */
const fs = require('node:fs/promises');
const path = require("path");
// In signal-cli registered number you want to receive messages for
const signalNumber = "";
// for example:
// const signalNumber = "+4917222222222";
// The directory where messages are to be created as files
const inboxDir = "";
// for example:
// const inboxDir = path.join(process.env.HOME, "org/inbox");
// The host + port where the signal-cli-rest-api service is running
const apiHost = "";
// for example:
// const apiHost = "http://192.168.178.23:8080";
// Default message file extension
const messageFileExtension = "md";
// Turn on to enable debugging output
const debugging = false;
// Turn off to disable messages being backed-up in $XDG_CACHE_HOME
const backupMessages = true;
// Turn on to not delete the attachments on the server after downloading them
const keepAllAttachments = false;
// Add the numbers of senders and groups you want to receive messages from
// If array is non-empty, all messages not from senders or groups in the list will be ignored
// Format: [ "+49172171717", "nDfe1bpw9GDAm68w/i3VENs6JWqoTtBYm42DY5o3ShY=" ]
const senderAndGroupWhitelist = [];
const filenameFromBodyCutoff = 60;
const filenameFromTitleCutoff = 80;
if (!signalNumber || !inboxDir || !apiHost) {
console.error("The required configuration variables are not set.")
process.exit(1);
}
fs.stat(inboxDir).catch(() => {
console.error( `Configured inboxDir '${inboxDir}' does not exist` )
process.exit(2);
})
fetch(`${apiHost}/v1/health`).catch(() => {
console.error( `Configured apiHost '${apiHost}' is not reachable` )
process.exit(3);
})
const unixTime = Math.floor(Date.now()/1000);
const errorFile = path.join(inboxDir, `signal-api-errors-${unixTime}`)
const backupDataDir = path.join(process.env.XDG_CACHE_HOME ? process.env.XDG_CACHE_HOME : path.join( process.env.HOME, ".cache"), "signal-api-backups" );
if (backupMessages) fs.mkdir( backupDataDir, { recursive: true } );
function logError(msg) {
return fs.appendFile(errorFile, String(msg)+"\n\n");
}
function writeToFile(filename, data) {
if (debugging) console.log("Writing to: " + filename);
return fs.appendFile(path.join(inboxDir, filename), data)
.catch(err => logError( `Writing file: ${filename}
With message: ${data}
` + err ));
}
function fetchApi(endpoint, options = {}) {
return fetch( `${apiHost}${endpoint}`, options )
.catch(err => logError( `fetching ${endpoint}
` + err))
}
function writeNote(msg) {
let filename = msg.replaceAll("/", "");
const firstLineOfFilename = filename.split("\n")[0];
if (firstLineOfFilename.match(/:/) && ! filename.match(/^http/)) {
filename = firstLineOfFilename.match(/(.*):/)[1].slice(0, filenameFromTitleCutoff);
} else {
filename = filename.replaceAll("\n", " ").trim();
if (filename.length > filenameFromBodyCutoff)
filename = filename.slice(0, filenameFromBodyCutoff) + "…";
}
if (msg.match(/^http/))
msg = `{${msg}}`;
return writeToFile( `${filename}.${messageFileExtension}`, msg );
}
function returnFileExtForContentType(ct) {
switch(ct) {
case "audio/mpeg":
return ".aac";
case "application/octet-stream":
return ".bin";
default:
logError( `FileExt is empty and contentType of ${contentType} is not being matched for attachement ${id}` );
return ".bin";
}
}
function isAttachmentALongMessage(name) {
// If a message is really long signal will truncate the message and attach the whole thing as an attachment
return String(name).match(/^long-message-.*\.txt/);
}
function deleteAttachment(id) {
if (!keepAllAttachments) {
if (debugging) console.log( "Deleting remote attachement: ", id );
return fetchApi( `/v1/attachments/${id}`, { method: "DELETE" } );
}
}
async function writeAttachment({id, contentType, name}, title, n) {
// id is a random string given by signal
// name is the filename of the file on the uploaders device
// title are the contents of an accompanying message, which functions as a title for the file
let filename = name ? name : id;
let fileExt = filename.match(/\..*$/);
if (!fileExt) {
fileExt = returnFileExtForContentType(contentType);
filename = `${filename.slice(0, filenameFromBodyCutoff)}${fileExt}`;
} else {
fileExt = fileExt[0];
}
if (title) {
if (n) fileExt = `-${n}${fileExt}`;
filename = `${title.replaceAll("\n", " ").slice(0, filenameFromBodyCutoff).trim()}${fileExt}`;
}
const attachmentData = await fetchApi( `/v1/attachments/${id}` )
.then( r => r.arrayBuffer() )
.then( r => Buffer.from(r) )
.catch( () => null );
if (!attachmentData) return;
let writeFunc = () => writeToFile(filename, attachmentData);
if (isAttachmentALongMessage(name))
writeFunc = () => writeNote(String(attachmentData));
return writeFunc()
.then( () => deleteAttachment(id) );
}
function getGroupIdOrSenderNumber(envelope) {
return envelope?.dataMessage?.groupInfo?.groupId || envelope?.sourceNumber || null;
}
fetchApi( `/v1/receive/${signalNumber}?ignore_stories=true&send_read_receipts=true` )
.then( r => r.json() )
.then( messages => {
if (debugging) console.log( "Results from /receive:\n" + JSON.stringify(messages, null, 2) )
if ( messages?.error )
logError(`Error on /receive:
${messages.error}`).then(() => process.exit(1))
if (Array.isArray(messages)) {
if (backupMessages && messages) fs.writeFile( path.join(backupDataDir, `messages-${unixTime}`), JSON.stringify(messages, null, 2) )
messages.map( m => {
const message = m.envelope.dataMessage?.message;
const attachArr = m.envelope.dataMessage?.attachments;
let attachments = (attachArr ? attachArr : []).map( ({id, contentType, filename}) => ({id, contentType, name: filename}) );
const sender = getGroupIdOrSenderNumber(m.envelope);
if (senderAndGroupWhitelist && !senderAndGroupWhitelist.includes(sender)) {
if (debugging) console.log( "Ignoring message from sender or group that is not whitelisted: ", sender );
if (attachments) attachments.forEach(a => deleteAttachment(a.id))
return
}
if (!message && !attachments.length && debugging ) {
logError( `Message and attachements are empty for:
${JSON.stringify(m, null, 2)}` )
return
}
return { message, attachments };
} ).forEach( m => {
if (m?.attachments.length) {
m.attachments.forEach((data, index) => writeAttachment(data, m.message, index))
} else if (m?.message) {
writeNote(m.message);
}
} )
}
})