@lordbex/thelounge 4.4.3-blowfish
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.thelounge_home +1 -0
- package/LICENSE +22 -0
- package/README.md +95 -0
- package/client/index.html.tpl +69 -0
- package/dist/defaults/config.js +465 -0
- package/dist/package.json +174 -0
- package/dist/server/client.js +678 -0
- package/dist/server/clientManager.js +220 -0
- package/dist/server/command-line/index.js +85 -0
- package/dist/server/command-line/install.js +123 -0
- package/dist/server/command-line/outdated.js +30 -0
- package/dist/server/command-line/start.js +34 -0
- package/dist/server/command-line/storage.js +103 -0
- package/dist/server/command-line/uninstall.js +40 -0
- package/dist/server/command-line/upgrade.js +64 -0
- package/dist/server/command-line/users/add.js +67 -0
- package/dist/server/command-line/users/edit.js +39 -0
- package/dist/server/command-line/users/index.js +17 -0
- package/dist/server/command-line/users/list.js +53 -0
- package/dist/server/command-line/users/remove.js +37 -0
- package/dist/server/command-line/users/reset.js +64 -0
- package/dist/server/command-line/utils.js +177 -0
- package/dist/server/config.js +138 -0
- package/dist/server/helper.js +161 -0
- package/dist/server/identification.js +139 -0
- package/dist/server/index.js +3 -0
- package/dist/server/log.js +35 -0
- package/dist/server/models/chan.js +275 -0
- package/dist/server/models/msg.js +92 -0
- package/dist/server/models/network.js +546 -0
- package/dist/server/models/prefix.js +31 -0
- package/dist/server/models/user.js +42 -0
- package/dist/server/plugins/auth/ldap.js +188 -0
- package/dist/server/plugins/auth/local.js +41 -0
- package/dist/server/plugins/auth.js +70 -0
- package/dist/server/plugins/changelog.js +103 -0
- package/dist/server/plugins/clientCertificate.js +115 -0
- package/dist/server/plugins/dev-server.js +33 -0
- package/dist/server/plugins/inputs/action.js +54 -0
- package/dist/server/plugins/inputs/away.js +20 -0
- package/dist/server/plugins/inputs/ban.js +45 -0
- package/dist/server/plugins/inputs/blow.js +44 -0
- package/dist/server/plugins/inputs/connect.js +41 -0
- package/dist/server/plugins/inputs/ctcp.js +29 -0
- package/dist/server/plugins/inputs/disconnect.js +15 -0
- package/dist/server/plugins/inputs/ignore.js +74 -0
- package/dist/server/plugins/inputs/ignorelist.js +50 -0
- package/dist/server/plugins/inputs/index.js +105 -0
- package/dist/server/plugins/inputs/invite.js +31 -0
- package/dist/server/plugins/inputs/kick.js +26 -0
- package/dist/server/plugins/inputs/kill.js +13 -0
- package/dist/server/plugins/inputs/list.js +12 -0
- package/dist/server/plugins/inputs/mode.js +55 -0
- package/dist/server/plugins/inputs/msg.js +106 -0
- package/dist/server/plugins/inputs/mute.js +56 -0
- package/dist/server/plugins/inputs/nick.js +55 -0
- package/dist/server/plugins/inputs/notice.js +42 -0
- package/dist/server/plugins/inputs/part.js +46 -0
- package/dist/server/plugins/inputs/quit.js +27 -0
- package/dist/server/plugins/inputs/raw.js +13 -0
- package/dist/server/plugins/inputs/rejoin.js +25 -0
- package/dist/server/plugins/inputs/topic.js +24 -0
- package/dist/server/plugins/inputs/whois.js +19 -0
- package/dist/server/plugins/irc-events/away.js +59 -0
- package/dist/server/plugins/irc-events/cap.js +62 -0
- package/dist/server/plugins/irc-events/chghost.js +29 -0
- package/dist/server/plugins/irc-events/connection.js +152 -0
- package/dist/server/plugins/irc-events/ctcp.js +72 -0
- package/dist/server/plugins/irc-events/error.js +80 -0
- package/dist/server/plugins/irc-events/help.js +21 -0
- package/dist/server/plugins/irc-events/info.js +21 -0
- package/dist/server/plugins/irc-events/invite.js +27 -0
- package/dist/server/plugins/irc-events/join.js +53 -0
- package/dist/server/plugins/irc-events/kick.js +39 -0
- package/dist/server/plugins/irc-events/link.js +442 -0
- package/dist/server/plugins/irc-events/list.js +47 -0
- package/dist/server/plugins/irc-events/message.js +187 -0
- package/dist/server/plugins/irc-events/mode.js +124 -0
- package/dist/server/plugins/irc-events/modelist.js +67 -0
- package/dist/server/plugins/irc-events/motd.js +29 -0
- package/dist/server/plugins/irc-events/names.js +21 -0
- package/dist/server/plugins/irc-events/nick.js +45 -0
- package/dist/server/plugins/irc-events/part.js +35 -0
- package/dist/server/plugins/irc-events/quit.js +32 -0
- package/dist/server/plugins/irc-events/sasl.js +26 -0
- package/dist/server/plugins/irc-events/topic.js +42 -0
- package/dist/server/plugins/irc-events/unhandled.js +31 -0
- package/dist/server/plugins/irc-events/welcome.js +22 -0
- package/dist/server/plugins/irc-events/whois.js +57 -0
- package/dist/server/plugins/messageStorage/sqlite.js +454 -0
- package/dist/server/plugins/messageStorage/text.js +124 -0
- package/dist/server/plugins/packages/index.js +200 -0
- package/dist/server/plugins/packages/publicClient.js +66 -0
- package/dist/server/plugins/packages/themes.js +61 -0
- package/dist/server/plugins/storage.js +88 -0
- package/dist/server/plugins/sts.js +85 -0
- package/dist/server/plugins/uploader.js +267 -0
- package/dist/server/plugins/webpush.js +99 -0
- package/dist/server/server.js +857 -0
- package/dist/server/storageCleaner.js +131 -0
- package/dist/server/utils/fish.js +432 -0
- package/dist/shared/irc.js +19 -0
- package/dist/shared/linkify.js +81 -0
- package/dist/shared/types/chan.js +22 -0
- package/dist/shared/types/changelog.js +2 -0
- package/dist/shared/types/config.js +2 -0
- package/dist/shared/types/mention.js +2 -0
- package/dist/shared/types/msg.js +34 -0
- package/dist/shared/types/network.js +2 -0
- package/dist/shared/types/storage.js +2 -0
- package/dist/shared/types/user.js +2 -0
- package/dist/webpack.config.js +224 -0
- package/index.js +38 -0
- package/package.json +174 -0
- package/public/audio/pop.wav +0 -0
- package/public/css/style.css +12 -0
- package/public/css/style.css.map +1 -0
- package/public/favicon.ico +0 -0
- package/public/fonts/fa-solid-900.woff +0 -0
- package/public/fonts/fa-solid-900.woff2 +0 -0
- package/public/img/favicon-alerted.ico +0 -0
- package/public/img/icon-alerted-black-transparent-bg-72x72px.png +0 -0
- package/public/img/icon-alerted-grey-bg-192x192px.png +0 -0
- package/public/img/icon-black-transparent-bg.svg +1 -0
- package/public/img/logo-grey-bg-120x120px.png +0 -0
- package/public/img/logo-grey-bg-152x152px.png +0 -0
- package/public/img/logo-grey-bg-167x167px.png +0 -0
- package/public/img/logo-grey-bg-180x180px.png +0 -0
- package/public/img/logo-grey-bg-192x192px.png +0 -0
- package/public/img/logo-grey-bg-512x512px.png +0 -0
- package/public/img/logo-grey-bg.svg +1 -0
- package/public/img/logo-horizontal-transparent-bg-inverted.svg +1 -0
- package/public/img/logo-horizontal-transparent-bg.svg +1 -0
- package/public/img/logo-transparent-bg-inverted.svg +1 -0
- package/public/img/logo-transparent-bg.svg +1 -0
- package/public/img/logo-vertical-transparent-bg-inverted.svg +1 -0
- package/public/img/logo-vertical-transparent-bg.svg +1 -0
- package/public/js/bundle.js +2 -0
- package/public/js/bundle.js.map +1 -0
- package/public/js/bundle.vendor.js +3 -0
- package/public/js/bundle.vendor.js.LICENSE.txt +18 -0
- package/public/js/bundle.vendor.js.map +1 -0
- package/public/js/loading-error-handlers.js +1 -0
- package/public/robots.txt +2 -0
- package/public/service-worker.js +1 -0
- package/public/thelounge.webmanifest +53 -0
- package/public/themes/default.css +35 -0
- package/public/themes/morning.css +183 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || function (mod) {
|
|
19
|
+
if (mod && mod.__esModule) return mod;
|
|
20
|
+
var result = {};
|
|
21
|
+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
|
|
22
|
+
__setModuleDefault(result, mod);
|
|
23
|
+
return result;
|
|
24
|
+
};
|
|
25
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
26
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
27
|
+
};
|
|
28
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
|
+
const cheerio = __importStar(require("cheerio"));
|
|
30
|
+
const got_1 = __importDefault(require("got"));
|
|
31
|
+
const url_1 = require("url");
|
|
32
|
+
const mime_types_1 = __importDefault(require("mime-types"));
|
|
33
|
+
const log_1 = __importDefault(require("../../log"));
|
|
34
|
+
const config_1 = __importDefault(require("../../config"));
|
|
35
|
+
const linkify_1 = require("../../../shared/linkify");
|
|
36
|
+
const storage_1 = __importDefault(require("../storage"));
|
|
37
|
+
const currentFetchPromises = new Map();
|
|
38
|
+
const imageTypeRegex = /^image\/.+/;
|
|
39
|
+
const mediaTypeRegex = /^(audio|video)\/.+/;
|
|
40
|
+
function default_1(client, chan, msg, cleanText) {
|
|
41
|
+
if (!config_1.default.values.prefetch) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
msg.previews = (0, linkify_1.findLinksWithSchema)(cleanText).reduce((cleanLinks, link) => {
|
|
45
|
+
const url = normalizeURL(link.link);
|
|
46
|
+
// If the URL is invalid and cannot be normalized, don't fetch it
|
|
47
|
+
if (!url) {
|
|
48
|
+
return cleanLinks;
|
|
49
|
+
}
|
|
50
|
+
// If there are too many urls in this message, only fetch first X valid links
|
|
51
|
+
if (cleanLinks.length > 4) {
|
|
52
|
+
return cleanLinks;
|
|
53
|
+
}
|
|
54
|
+
// Do not fetch duplicate links twice
|
|
55
|
+
if (cleanLinks.some((l) => l.link === link.link)) {
|
|
56
|
+
return cleanLinks;
|
|
57
|
+
}
|
|
58
|
+
const preview = {
|
|
59
|
+
type: "loading",
|
|
60
|
+
head: "",
|
|
61
|
+
body: "",
|
|
62
|
+
thumb: "",
|
|
63
|
+
size: -1,
|
|
64
|
+
link: link.link, // Send original matched link to the client
|
|
65
|
+
shown: null,
|
|
66
|
+
};
|
|
67
|
+
cleanLinks.push(preview);
|
|
68
|
+
fetch(url, {
|
|
69
|
+
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
|
70
|
+
language: client.config.browser?.language || "",
|
|
71
|
+
})
|
|
72
|
+
.then((res) => {
|
|
73
|
+
parse(msg, chan, preview, res, client);
|
|
74
|
+
})
|
|
75
|
+
.catch((err) => {
|
|
76
|
+
preview.type = "error";
|
|
77
|
+
preview.error = "message";
|
|
78
|
+
preview.message = err.message;
|
|
79
|
+
emitPreview(client, chan, msg, preview);
|
|
80
|
+
});
|
|
81
|
+
return cleanLinks;
|
|
82
|
+
}, []);
|
|
83
|
+
}
|
|
84
|
+
exports.default = default_1;
|
|
85
|
+
function parseHtml(preview, res, client) {
|
|
86
|
+
// TODO:
|
|
87
|
+
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
|
88
|
+
return new Promise((resolve) => {
|
|
89
|
+
const $ = cheerio.load(res.data);
|
|
90
|
+
return parseHtmlMedia($, preview, client)
|
|
91
|
+
.then((newRes) => resolve(newRes))
|
|
92
|
+
.catch(() => {
|
|
93
|
+
preview.type = "link";
|
|
94
|
+
preview.head =
|
|
95
|
+
$('meta[property="og:title"]').attr("content") ||
|
|
96
|
+
$("head > title, title").first().text() ||
|
|
97
|
+
"";
|
|
98
|
+
preview.body =
|
|
99
|
+
$('meta[property="og:description"]').attr("content") ||
|
|
100
|
+
$('meta[name="description"]').attr("content") ||
|
|
101
|
+
"";
|
|
102
|
+
if (preview.head.length) {
|
|
103
|
+
preview.head = preview.head.substr(0, 100);
|
|
104
|
+
}
|
|
105
|
+
if (preview.body.length) {
|
|
106
|
+
preview.body = preview.body.substr(0, 300);
|
|
107
|
+
}
|
|
108
|
+
if (!config_1.default.values.prefetchStorage && config_1.default.values.disableMediaPreview) {
|
|
109
|
+
resolve(res);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
let thumb = $('meta[property="og:image"]').attr("content") ||
|
|
113
|
+
$('meta[name="twitter:image:src"]').attr("content") ||
|
|
114
|
+
$('link[rel="image_src"]').attr("href") ||
|
|
115
|
+
"";
|
|
116
|
+
// Make sure thumbnail is a valid and absolute url
|
|
117
|
+
if (thumb.length) {
|
|
118
|
+
thumb = normalizeURL(thumb, preview.link) || "";
|
|
119
|
+
}
|
|
120
|
+
// Verify that thumbnail pic exists and is under allowed size
|
|
121
|
+
if (thumb.length) {
|
|
122
|
+
fetch(thumb, { language: client.config.browser?.language || "" })
|
|
123
|
+
.then((resThumb) => {
|
|
124
|
+
if (resThumb !== null &&
|
|
125
|
+
imageTypeRegex.test(resThumb.type) &&
|
|
126
|
+
resThumb.size <= config_1.default.values.prefetchMaxImageSize * 1024) {
|
|
127
|
+
preview.thumbActualUrl = thumb;
|
|
128
|
+
}
|
|
129
|
+
resolve(resThumb);
|
|
130
|
+
})
|
|
131
|
+
.catch(() => resolve(null));
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
resolve(res);
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
// TODO: type $
|
|
140
|
+
function parseHtmlMedia($, preview, client) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
if (config_1.default.values.disableMediaPreview) {
|
|
143
|
+
reject();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
let foundMedia = false;
|
|
147
|
+
const openGraphType = $('meta[property="og:type"]').attr("content");
|
|
148
|
+
// Certain news websites may include video and audio tags,
|
|
149
|
+
// despite actually being an article (as indicated by og:type).
|
|
150
|
+
// If there is og:type tag, we will only select video or audio if it matches
|
|
151
|
+
if (openGraphType &&
|
|
152
|
+
!openGraphType.startsWith("video") &&
|
|
153
|
+
!openGraphType.startsWith("music")) {
|
|
154
|
+
reject();
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
["video", "audio"].forEach((type) => {
|
|
158
|
+
if (foundMedia) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
$(`meta[property="og:${type}:type"]`).each(function (i) {
|
|
162
|
+
const mimeType = $(this).attr("content");
|
|
163
|
+
if (!mimeType) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (mediaTypeRegex.test(mimeType)) {
|
|
167
|
+
// If we match a clean video or audio tag, parse that as a preview instead
|
|
168
|
+
let mediaUrl = $($(`meta[property="og:${type}"]`).get(i)).attr("content");
|
|
169
|
+
if (!mediaUrl) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
// Make sure media is a valid url
|
|
173
|
+
mediaUrl = normalizeURL(mediaUrl, preview.link, true);
|
|
174
|
+
// Make sure media is a valid url
|
|
175
|
+
if (!mediaUrl) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
foundMedia = true;
|
|
179
|
+
fetch(mediaUrl, {
|
|
180
|
+
accept: type === "video"
|
|
181
|
+
? "video/webm,video/ogg,video/*;q=0.9,application/ogg;q=0.7,audio/*;q=0.6,*/*;q=0.5"
|
|
182
|
+
: "audio/webm, audio/ogg, audio/wav, audio/*;q=0.9, application/ogg;q=0.7, video/*;q=0.6; */*;q=0.5",
|
|
183
|
+
language: client.config.browser?.language || "",
|
|
184
|
+
})
|
|
185
|
+
.then((resMedia) => {
|
|
186
|
+
if (resMedia === null || !mediaTypeRegex.test(resMedia.type)) {
|
|
187
|
+
return reject();
|
|
188
|
+
}
|
|
189
|
+
preview.type = type;
|
|
190
|
+
preview.media = mediaUrl;
|
|
191
|
+
preview.mediaType = resMedia.type;
|
|
192
|
+
resolve(resMedia);
|
|
193
|
+
})
|
|
194
|
+
.catch(reject);
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
if (!foundMedia) {
|
|
200
|
+
reject();
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function parse(msg, chan, preview, res, client) {
|
|
205
|
+
let promise = null;
|
|
206
|
+
preview.size = res.size;
|
|
207
|
+
switch (res.type) {
|
|
208
|
+
case "text/html":
|
|
209
|
+
preview.size = -1;
|
|
210
|
+
promise = parseHtml(preview, res, client);
|
|
211
|
+
break;
|
|
212
|
+
case "text/plain":
|
|
213
|
+
preview.type = "link";
|
|
214
|
+
preview.body = res.data.toString().substr(0, 300);
|
|
215
|
+
break;
|
|
216
|
+
case "image/png":
|
|
217
|
+
case "image/gif":
|
|
218
|
+
case "image/jpg":
|
|
219
|
+
case "image/jpeg":
|
|
220
|
+
case "image/jxl":
|
|
221
|
+
case "image/webp":
|
|
222
|
+
case "image/avif":
|
|
223
|
+
if (!config_1.default.values.prefetchStorage && config_1.default.values.disableMediaPreview) {
|
|
224
|
+
return removePreview(msg, preview);
|
|
225
|
+
}
|
|
226
|
+
if (res.size > config_1.default.values.prefetchMaxImageSize * 1024) {
|
|
227
|
+
preview.type = "error";
|
|
228
|
+
preview.error = "image-too-big";
|
|
229
|
+
preview.maxSize = config_1.default.values.prefetchMaxImageSize * 1024;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
preview.type = "image";
|
|
233
|
+
preview.thumbActualUrl = preview.link;
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
case "audio/midi":
|
|
237
|
+
case "audio/mpeg":
|
|
238
|
+
case "audio/mpeg3":
|
|
239
|
+
case "audio/ogg":
|
|
240
|
+
case "audio/wav":
|
|
241
|
+
case "audio/x-wav":
|
|
242
|
+
case "audio/x-mid":
|
|
243
|
+
case "audio/x-midi":
|
|
244
|
+
case "audio/x-mpeg":
|
|
245
|
+
case "audio/x-mpeg-3":
|
|
246
|
+
case "audio/flac":
|
|
247
|
+
case "audio/x-flac":
|
|
248
|
+
case "audio/mp4":
|
|
249
|
+
case "audio/x-m4a":
|
|
250
|
+
if (!preview.link.startsWith("https://")) {
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
if (config_1.default.values.disableMediaPreview) {
|
|
254
|
+
return removePreview(msg, preview);
|
|
255
|
+
}
|
|
256
|
+
preview.type = "audio";
|
|
257
|
+
preview.media = preview.link;
|
|
258
|
+
preview.mediaType = res.type;
|
|
259
|
+
break;
|
|
260
|
+
case "video/webm":
|
|
261
|
+
case "video/ogg":
|
|
262
|
+
case "video/mp4":
|
|
263
|
+
if (!preview.link.startsWith("https://")) {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
if (config_1.default.values.disableMediaPreview) {
|
|
267
|
+
return removePreview(msg, preview);
|
|
268
|
+
}
|
|
269
|
+
preview.type = "video";
|
|
270
|
+
preview.media = preview.link;
|
|
271
|
+
preview.mediaType = res.type;
|
|
272
|
+
break;
|
|
273
|
+
default:
|
|
274
|
+
return removePreview(msg, preview);
|
|
275
|
+
}
|
|
276
|
+
if (!promise) {
|
|
277
|
+
return handlePreview(client, chan, msg, preview, res);
|
|
278
|
+
}
|
|
279
|
+
void promise.then((newRes) => handlePreview(client, chan, msg, preview, newRes));
|
|
280
|
+
}
|
|
281
|
+
function handlePreview(client, chan, msg, preview, res) {
|
|
282
|
+
const thumb = preview.thumbActualUrl || "";
|
|
283
|
+
delete preview.thumbActualUrl;
|
|
284
|
+
if (!thumb.length || !config_1.default.values.prefetchStorage) {
|
|
285
|
+
preview.thumb = thumb;
|
|
286
|
+
return emitPreview(client, chan, msg, preview);
|
|
287
|
+
}
|
|
288
|
+
// Get the correct file extension for the provided content-type
|
|
289
|
+
// This is done to prevent user-input being stored in the file name (extension)
|
|
290
|
+
const extension = mime_types_1.default.extension(res.type);
|
|
291
|
+
if (!extension) {
|
|
292
|
+
// For link previews, drop the thumbnail
|
|
293
|
+
// For other types, do not display preview at all
|
|
294
|
+
if (preview.type !== "link") {
|
|
295
|
+
return removePreview(msg, preview);
|
|
296
|
+
}
|
|
297
|
+
return emitPreview(client, chan, msg, preview);
|
|
298
|
+
}
|
|
299
|
+
storage_1.default.store(res.data, extension, (uri) => {
|
|
300
|
+
preview.thumb = uri;
|
|
301
|
+
emitPreview(client, chan, msg, preview);
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
function emitPreview(client, chan, msg, preview) {
|
|
305
|
+
// If there is no title but there is preview or description, set title
|
|
306
|
+
// otherwise bail out and show no preview
|
|
307
|
+
if (!preview.head.length && preview.type === "link") {
|
|
308
|
+
if (preview.thumb.length || preview.body.length) {
|
|
309
|
+
preview.head = "Untitled page";
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
return removePreview(msg, preview);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
client.emit("msg:preview", {
|
|
316
|
+
id: msg.id,
|
|
317
|
+
chan: chan.id,
|
|
318
|
+
preview: preview,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
function removePreview(msg, preview) {
|
|
322
|
+
// If a preview fails to load, remove the link from msg object
|
|
323
|
+
// So that client doesn't attempt to display an preview on page reload
|
|
324
|
+
const index = msg.previews.indexOf(preview);
|
|
325
|
+
if (index > -1) {
|
|
326
|
+
msg.previews.splice(index, 1);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function getRequestHeaders(headers) {
|
|
330
|
+
const formattedHeaders = {
|
|
331
|
+
// Certain websites like Amazon only add <meta> tags to known bots,
|
|
332
|
+
// lets pretend to be them to get the metadata
|
|
333
|
+
"User-Agent": "Mozilla/5.0 (compatible; The Lounge IRC Client; +https://github.com/thelounge/thelounge)" +
|
|
334
|
+
" facebookexternalhit/1.1 Twitterbot/1.0",
|
|
335
|
+
Accept: headers.accept || "*/*",
|
|
336
|
+
"X-Purpose": "preview",
|
|
337
|
+
};
|
|
338
|
+
if (headers.language) {
|
|
339
|
+
formattedHeaders["Accept-Language"] = headers.language;
|
|
340
|
+
}
|
|
341
|
+
return formattedHeaders;
|
|
342
|
+
}
|
|
343
|
+
function fetch(uri, headers) {
|
|
344
|
+
// Stringify the object otherwise the objects won't compute to the same value
|
|
345
|
+
const cacheKey = JSON.stringify([uri, headers]);
|
|
346
|
+
let promise = currentFetchPromises.get(cacheKey);
|
|
347
|
+
if (promise) {
|
|
348
|
+
return promise;
|
|
349
|
+
}
|
|
350
|
+
const prefetchTimeout = config_1.default.values.prefetchTimeout;
|
|
351
|
+
if (!prefetchTimeout) {
|
|
352
|
+
log_1.default.warn("prefetchTimeout is missing from your The Lounge configuration, defaulting to 5000 ms");
|
|
353
|
+
}
|
|
354
|
+
promise = new Promise((resolve, reject) => {
|
|
355
|
+
let buffer = Buffer.from("");
|
|
356
|
+
let contentLength = 0;
|
|
357
|
+
let contentType;
|
|
358
|
+
let limit = config_1.default.values.prefetchMaxImageSize * 1024;
|
|
359
|
+
try {
|
|
360
|
+
const gotStream = got_1.default.stream(uri, {
|
|
361
|
+
retry: 0,
|
|
362
|
+
timeout: prefetchTimeout || 5000, // milliseconds
|
|
363
|
+
headers: getRequestHeaders(headers),
|
|
364
|
+
localAddress: config_1.default.values.bind,
|
|
365
|
+
});
|
|
366
|
+
gotStream
|
|
367
|
+
.on("response", function (res) {
|
|
368
|
+
contentLength = parseInt(res.headers["content-length"], 10) || 0;
|
|
369
|
+
contentType = res.headers["content-type"];
|
|
370
|
+
if (contentType && imageTypeRegex.test(contentType)) {
|
|
371
|
+
// response is an image
|
|
372
|
+
// if Content-Length header reports a size exceeding the prefetch limit, abort fetch
|
|
373
|
+
// and if file is not to be stored we don't need to download further either
|
|
374
|
+
if (contentLength > limit || !config_1.default.values.prefetchStorage) {
|
|
375
|
+
gotStream.destroy();
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else if (contentType && mediaTypeRegex.test(contentType)) {
|
|
379
|
+
// We don't need to download the file any further after we received content-type header
|
|
380
|
+
gotStream.destroy();
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// if not image, limit download to the max search size, since we need only meta tags
|
|
384
|
+
// twitter.com sends opengraph meta tags within ~20kb of data for individual tweets, the default is set to 50.
|
|
385
|
+
// for sites like Youtube the og tags are in the first 300K and hence this is configurable by the admin
|
|
386
|
+
limit =
|
|
387
|
+
"prefetchMaxSearchSize" in config_1.default.values
|
|
388
|
+
? config_1.default.values.prefetchMaxSearchSize * 1024
|
|
389
|
+
: // set to the previous size if config option is unset
|
|
390
|
+
50 * 1024;
|
|
391
|
+
}
|
|
392
|
+
})
|
|
393
|
+
.on("error", (e) => reject(e))
|
|
394
|
+
.on("data", (data) => {
|
|
395
|
+
buffer = Buffer.concat([buffer, data], buffer.length + data.length);
|
|
396
|
+
if (buffer.length >= limit) {
|
|
397
|
+
gotStream.destroy();
|
|
398
|
+
}
|
|
399
|
+
})
|
|
400
|
+
.on("end", () => gotStream.destroy())
|
|
401
|
+
.on("close", () => {
|
|
402
|
+
let type = "";
|
|
403
|
+
// If we downloaded more data then specified in Content-Length, use real data size
|
|
404
|
+
const size = contentLength > buffer.length ? contentLength : buffer.length;
|
|
405
|
+
if (contentType) {
|
|
406
|
+
type = contentType.split(/ *; */).shift() || "";
|
|
407
|
+
}
|
|
408
|
+
resolve({ data: buffer, type, size });
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
return reject(e);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
const removeCache = () => currentFetchPromises.delete(cacheKey);
|
|
416
|
+
promise.then(removeCache).catch(removeCache);
|
|
417
|
+
currentFetchPromises.set(cacheKey, promise);
|
|
418
|
+
return promise;
|
|
419
|
+
}
|
|
420
|
+
function normalizeURL(link, baseLink, disallowHttp = false) {
|
|
421
|
+
try {
|
|
422
|
+
const url = new url_1.URL(link, baseLink);
|
|
423
|
+
// Only fetch http and https links
|
|
424
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
if (disallowHttp && url.protocol === "http:") {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
// Do not fetch links without hostname or ones that contain authorization
|
|
431
|
+
if (!url.hostname || url.username || url.password) {
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
// Drop hash from the url, if any
|
|
435
|
+
url.hash = "";
|
|
436
|
+
return url.toString();
|
|
437
|
+
}
|
|
438
|
+
catch (e) {
|
|
439
|
+
// if an exception was thrown, the url is not valid
|
|
440
|
+
}
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const chan_1 = require("../../../shared/types/chan");
|
|
4
|
+
exports.default = (function (irc, network) {
|
|
5
|
+
const client = this;
|
|
6
|
+
const MAX_CHANS = 500;
|
|
7
|
+
irc.on("channel list start", function () {
|
|
8
|
+
network.chanCache = [];
|
|
9
|
+
updateListStatus({
|
|
10
|
+
text: "Loading channel list, this can take a moment...",
|
|
11
|
+
});
|
|
12
|
+
});
|
|
13
|
+
irc.on("channel list", function (channels) {
|
|
14
|
+
Array.prototype.push.apply(network.chanCache, channels);
|
|
15
|
+
updateListStatus({
|
|
16
|
+
text: `Loaded ${network.chanCache.length} channels...`,
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
irc.on("channel list end", function () {
|
|
20
|
+
updateListStatus(network.chanCache.sort((a, b) => b.num_users - a.num_users).slice(0, MAX_CHANS));
|
|
21
|
+
network.chanCache = [];
|
|
22
|
+
});
|
|
23
|
+
function updateListStatus(msg) {
|
|
24
|
+
let chan = network.getChannel("Channel List");
|
|
25
|
+
if (typeof chan === "undefined") {
|
|
26
|
+
chan = client.createChannel({
|
|
27
|
+
type: chan_1.ChanType.SPECIAL,
|
|
28
|
+
special: chan_1.SpecialChanType.CHANNELLIST,
|
|
29
|
+
name: "Channel List",
|
|
30
|
+
data: msg,
|
|
31
|
+
});
|
|
32
|
+
client.emit("join", {
|
|
33
|
+
network: network.uuid,
|
|
34
|
+
chan: chan.getFilteredClone(true),
|
|
35
|
+
shouldOpen: false,
|
|
36
|
+
index: network.addChannel(chan),
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
chan.data = msg;
|
|
41
|
+
client.emit("msg:special", {
|
|
42
|
+
chan: chan.id,
|
|
43
|
+
data: msg,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const msg_1 = __importDefault(require("../../models/msg"));
|
|
7
|
+
const link_1 = __importDefault(require("./link"));
|
|
8
|
+
const irc_1 = require("../../../shared/irc");
|
|
9
|
+
const helper_1 = __importDefault(require("../../helper"));
|
|
10
|
+
const msg_2 = require("../../../shared/types/msg");
|
|
11
|
+
const chan_1 = require("../../../shared/types/chan");
|
|
12
|
+
const fish_1 = require("../../utils/fish");
|
|
13
|
+
const nickRegExp = /(?:\x03[0-9]{1,2}(?:,[0-9]{1,2})?)?([\w[\]\\`^{|}-]+)/g;
|
|
14
|
+
function convertForHandle(type, data) {
|
|
15
|
+
return { ...data, type: type };
|
|
16
|
+
}
|
|
17
|
+
exports.default = (function (irc, network) {
|
|
18
|
+
const client = this;
|
|
19
|
+
irc.on("notice", function (data) {
|
|
20
|
+
handleMessage(convertForHandle(msg_2.MessageType.NOTICE, data));
|
|
21
|
+
});
|
|
22
|
+
irc.on("action", function (data) {
|
|
23
|
+
handleMessage(convertForHandle(msg_2.MessageType.ACTION, data));
|
|
24
|
+
});
|
|
25
|
+
irc.on("privmsg", function (data) {
|
|
26
|
+
handleMessage(convertForHandle(msg_2.MessageType.MESSAGE, data));
|
|
27
|
+
});
|
|
28
|
+
irc.on("wallops", function (data) {
|
|
29
|
+
data.from_server = true;
|
|
30
|
+
handleMessage(convertForHandle(msg_2.MessageType.WALLOPS, data));
|
|
31
|
+
});
|
|
32
|
+
function handleMessage(data) {
|
|
33
|
+
let chan;
|
|
34
|
+
let from;
|
|
35
|
+
let highlight = false;
|
|
36
|
+
let showInActive = false;
|
|
37
|
+
const self = data.nick === irc.user.nick;
|
|
38
|
+
// Some servers send messages without any nickname
|
|
39
|
+
if (!data.nick) {
|
|
40
|
+
data.from_server = true;
|
|
41
|
+
data.nick = data.hostname || network.host;
|
|
42
|
+
}
|
|
43
|
+
// Check if the sender is in our ignore list
|
|
44
|
+
const shouldIgnore = !self &&
|
|
45
|
+
network.ignoreList.some(function (entry) {
|
|
46
|
+
return helper_1.default.compareHostmask(entry, data);
|
|
47
|
+
});
|
|
48
|
+
// Server messages that aren't targeted at a channel go to the server window
|
|
49
|
+
if (data.from_server &&
|
|
50
|
+
(!data.target ||
|
|
51
|
+
!network.getChannel(data.target) ||
|
|
52
|
+
network.getChannel(data.target)?.type !== chan_1.ChanType.CHANNEL)) {
|
|
53
|
+
chan = network.getLobby();
|
|
54
|
+
from = chan.getUser(data.nick);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
if (shouldIgnore) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
let target = data.target;
|
|
61
|
+
// If the message is targeted at us, use sender as target instead
|
|
62
|
+
if (target.toLowerCase() === irc.user.nick.toLowerCase()) {
|
|
63
|
+
target = data.nick;
|
|
64
|
+
}
|
|
65
|
+
chan = network.getChannel(target);
|
|
66
|
+
if (typeof chan === "undefined") {
|
|
67
|
+
// Send notices that are not targeted at us into the server window
|
|
68
|
+
if (data.type === msg_2.MessageType.NOTICE) {
|
|
69
|
+
showInActive = true;
|
|
70
|
+
chan = network.getLobby();
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
chan = client.createChannel({
|
|
74
|
+
type: chan_1.ChanType.QUERY,
|
|
75
|
+
name: target,
|
|
76
|
+
});
|
|
77
|
+
client.emit("join", {
|
|
78
|
+
network: network.uuid,
|
|
79
|
+
chan: chan.getFilteredClone(true),
|
|
80
|
+
shouldOpen: false,
|
|
81
|
+
index: network.addChannel(chan),
|
|
82
|
+
});
|
|
83
|
+
client.save();
|
|
84
|
+
chan.loadMessages(client, network);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
from = chan.getUser(data.nick);
|
|
88
|
+
// Attempt mIRC FiSH Blowfish decryption if applicable
|
|
89
|
+
if (chan.blowfishKey) {
|
|
90
|
+
const decrypted = (0, fish_1.tryDecryptFishLine)(data.message, chan.blowfishKey);
|
|
91
|
+
if (decrypted !== null) {
|
|
92
|
+
data.message = decrypted;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Query messages (unless self or muted) always highlight
|
|
96
|
+
if (chan.type === chan_1.ChanType.QUERY) {
|
|
97
|
+
highlight = !self;
|
|
98
|
+
}
|
|
99
|
+
else if (chan.type === chan_1.ChanType.CHANNEL) {
|
|
100
|
+
from.lastMessage = data.time || Date.now();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// msg is constructed down here because `from` is being copied in the constructor
|
|
104
|
+
const msg = new msg_1.default({
|
|
105
|
+
type: data.type,
|
|
106
|
+
time: data.time ? new Date(data.time) : undefined,
|
|
107
|
+
text: data.message,
|
|
108
|
+
self: self,
|
|
109
|
+
from: from,
|
|
110
|
+
highlight: highlight,
|
|
111
|
+
users: [],
|
|
112
|
+
});
|
|
113
|
+
if (showInActive) {
|
|
114
|
+
msg.showInActive = true;
|
|
115
|
+
}
|
|
116
|
+
// remove IRC formatting for custom highlight testing
|
|
117
|
+
const cleanMessage = (0, irc_1.cleanIrcMessage)(data.message);
|
|
118
|
+
// Self messages in channels are never highlighted
|
|
119
|
+
// Non-self messages are highlighted as soon as the nick is detected
|
|
120
|
+
if (!msg.highlight && !msg.self) {
|
|
121
|
+
msg.highlight = network.highlightRegex?.test(data.message);
|
|
122
|
+
// If we still don't have a highlight, test against custom highlights if there's any
|
|
123
|
+
if (!msg.highlight && client.highlightRegex) {
|
|
124
|
+
msg.highlight = client.highlightRegex.test(cleanMessage);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// if highlight exceptions match, do not highlight at all
|
|
128
|
+
if (msg.highlight && client.highlightExceptionRegex) {
|
|
129
|
+
msg.highlight = !client.highlightExceptionRegex.test(cleanMessage);
|
|
130
|
+
}
|
|
131
|
+
if (data.group) {
|
|
132
|
+
msg.statusmsgGroup = data.group;
|
|
133
|
+
}
|
|
134
|
+
let match;
|
|
135
|
+
while ((match = nickRegExp.exec(data.message))) {
|
|
136
|
+
if (chan.findUser(match[1])) {
|
|
137
|
+
msg.users.push(match[1]);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// No prefetch URLs unless are simple MESSAGE or ACTION types
|
|
141
|
+
if ([msg_2.MessageType.MESSAGE, msg_2.MessageType.ACTION].includes(data.type)) {
|
|
142
|
+
(0, link_1.default)(client, chan, msg, cleanMessage);
|
|
143
|
+
}
|
|
144
|
+
chan.pushMessage(client, msg, !msg.self);
|
|
145
|
+
// Do not send notifications if the channel is muted or for messages older than 15 minutes (znc buffer for example)
|
|
146
|
+
if (!chan.muted && msg.highlight && (!data.time || data.time > Date.now() - 900000)) {
|
|
147
|
+
let title = chan.name;
|
|
148
|
+
let body = cleanMessage;
|
|
149
|
+
if (msg.type === msg_2.MessageType.ACTION) {
|
|
150
|
+
// For actions, do not include colon in the message
|
|
151
|
+
body = `${data.nick} ${body}`;
|
|
152
|
+
}
|
|
153
|
+
else if (chan.type !== chan_1.ChanType.QUERY) {
|
|
154
|
+
// In channels, prepend sender nickname to the message
|
|
155
|
+
body = `${data.nick}: ${body}`;
|
|
156
|
+
}
|
|
157
|
+
// If a channel is active on any client, highlight won't increment and notification will say (0 mention)
|
|
158
|
+
if (chan.highlight > 0) {
|
|
159
|
+
title += ` (${chan.highlight} ${chan.type === chan_1.ChanType.QUERY ? "new message" : "mention"}${chan.highlight > 1 ? "s" : ""})`;
|
|
160
|
+
}
|
|
161
|
+
if (chan.highlight > 1) {
|
|
162
|
+
body += `\n\n… and ${chan.highlight - 1} other message${chan.highlight > 2 ? "s" : ""}`;
|
|
163
|
+
}
|
|
164
|
+
client.manager.webPush.push(client, {
|
|
165
|
+
type: "notification",
|
|
166
|
+
chanId: chan.id,
|
|
167
|
+
timestamp: data.time || Date.now(),
|
|
168
|
+
title: title,
|
|
169
|
+
body: body,
|
|
170
|
+
}, true);
|
|
171
|
+
}
|
|
172
|
+
// Keep track of all mentions in channels for this client
|
|
173
|
+
if (msg.highlight && chan.type === chan_1.ChanType.CHANNEL) {
|
|
174
|
+
client.mentions.push({
|
|
175
|
+
chanId: chan.id,
|
|
176
|
+
msgId: msg.id,
|
|
177
|
+
type: msg.type,
|
|
178
|
+
time: msg.time,
|
|
179
|
+
text: msg.text,
|
|
180
|
+
from: msg.from,
|
|
181
|
+
});
|
|
182
|
+
if (client.mentions.length > 100) {
|
|
183
|
+
client.mentions.splice(0, client.mentions.length - 100);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
});
|