1 // Smuxi - Smart MUltipleXed Irc
2 //
3 // Copyright (c) 2011, 2014-2017 Mirco Bauer <meebey@meebey.net>
4 //
5 // Full GPL License: <http://www.gnu.org/licenses/gpl.txt>
6 //
7 // This program is free software; you can redistribute it and/or modify
8 // it under the terms of the GNU General Public License as published by
9 // the Free Software Foundation; either version 2 of the License, or
10 // (at your option) any later version.
11 //
12 // This program is distributed in the hope that it will be useful,
13 // but WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 // GNU General Public License for more details.
16 //
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307 USA
20 using System;
21 using System.Text;
22 using System.Text.RegularExpressions;
23 using System.Collections.Generic;
24 using Smuxi.Common;
25 
26 namespace Smuxi.Engine
27 {
28     public class MessageBuilderSettings
29     {
30         static List<MessagePatternModel> BuiltinPatterns { get; set; }
31         static MessagePatternModel EmojiMessagePattern { get; set; }
32         public List<MessagePatternModel> UserPatterns { get; set; }
33         public List<MessagePatternModel> Patterns { get; set; }
34         public bool NickColors { get; set; }
35         public bool StripFormattings { get; set; }
36         public bool StripColors { get; set; }
37         public TextColor HighlightColor { get; set; }
38         public List<string> HighlightWords { get; set; }
39 
40         public bool Emojis {
41             get {
42                 return Patterns.Contains(EmojiMessagePattern);
43             }
44             set {
45                 if (value && !Emojis) {
46                     Patterns.Add(EmojiMessagePattern);
47                 }
48                 if (!value) {
49                     Patterns.Remove(EmojiMessagePattern);
50                 }
51             }
52         }
53 
MessageBuilderSettings()54         static MessageBuilderSettings()
55         {
56             // OPT: this emoji regex is really long, around 27k characters
57             var emojiRegexBuilder = new StringBuilder(32 * 1024);
58             emojiRegexBuilder.Append(":(");
59             foreach (var emojiShortname in Emojione.ShortnameToUnicodeMap.Keys) {
60                 emojiRegexBuilder.AppendFormat("{0}|", Regex.Escape(emojiShortname));
61             }
62             // remove trailing |
63             emojiRegexBuilder.Length--;
64             emojiRegexBuilder.Append("):");
65 
66             var emojiRegex = new Regex(emojiRegexBuilder.ToString(), RegexOptions.Compiled);
67             EmojiMessagePattern = new MessagePatternModel(emojiRegex) {
68                 MessagePartType = typeof(ImageMessagePartModel),
69                 LinkFormat = "smuxi-emoji://{1}"
70             };
71 
72             BuiltinPatterns = new List<MessagePatternModel>();
73             InitBuiltinSmartLinks();
74         }
75 
MessageBuilderSettings()76         public MessageBuilderSettings()
77         {
78             NickColors = true;
79 
80             // No need to lock BuiltinPatterns as List<T> is thread-safe for
81             // multiple readers as long as there is no writer at the same time.
82             // BuiltinPatterns is only written once before the first instance
83             // of MessageBuilderSettings is created via the static initializer.
84             Patterns = new List<MessagePatternModel>(BuiltinPatterns);
85         }
86 
MessageBuilderSettings(MessageBuilderSettings settings)87         public MessageBuilderSettings(MessageBuilderSettings settings)
88         {
89             if (settings == null) {
90                 throw new ArgumentNullException("settings");
91             }
92 
93             UserPatterns = new List<MessagePatternModel>(settings.UserPatterns);
94             Patterns = new List<MessagePatternModel>(settings.Patterns);
95             NickColors = settings.NickColors;
96             StripFormattings = settings.StripFormattings;
97             StripColors = settings.StripColors;
98             HighlightColor = settings.HighlightColor;
99             HighlightWords = settings.HighlightWords;
100         }
101 
102         internal const string StartDelimiterGroupName = "DelimiterForStartOfPattern";
103         internal const string EndDelimiterGroupName = "DelimiterForEndOfPattern";
104 
InitBuiltinSmartLinks()105         static void InitBuiltinSmartLinks()
106         {
107             string path_last_chars = @"a-zA-Z0-9#/%&@=\-_+;:~'";
108             string path_chars = path_last_chars + @"\(\)\[\]\{\}?!.,";
109             string domain_chars = @"[a-z0-9\-\p{L}]+";
110             string subdomain = domain_chars + @"\.";
111             string common_tld = @"de|es|im|us|com|net|org|info|biz|gov|name|edu|onion|museum";
112             string any_tld = @"[a-z\p{L}]+";
113             string ip6 = @"(?:[0-9a-f]{0,4}:){1,7}[0-9a-f]{1,4}";
114             string quoted_ip6 = @"\[" + ip6 + @"\]";
115             string ip4 = @"(?:[0-9]{1,3}\.){3}[0-9]{1,3}";
116             string ip = "(?:" + ip4 + "|" + ip6 + "|" + quoted_ip6 + ")";
117             string domain = @"(?:(?:" + subdomain + ")+(?:" + any_tld + ")|localhost)";
118             string bare_host = @"[a-z]+";
119             string host = "(?:" + domain + "|" + bare_host + "|" + ip + ")";
120             string short_number = "[1-9][0-9]{0,4}";
121             string port = ":" + short_number;
122             string user = "[a-z0-9._%+-]+@";
123             string host_port = host + "(?:" + port + ")?";
124             string user_host_port = "(?:" + user + ")?" + host_port;
125             string user_domain = user + domain;
126             string path = @"/(?:["+ path_chars +"]*["+ path_last_chars +"]+)?";
127             string protocol = @"[a-z][a-z0-9\-+]*://";
128             string protocol_user_host_port_path = protocol + user_host_port + "(?:" + path + ")?";
129             string start_delimiter = String.Format(@"(?<{0}>^|\s|\W)", StartDelimiterGroupName);
130             string end_delimiter = String.Format(@"(?<{0}>$|\s|\W)", EndDelimiterGroupName);
131 
132             // facebook attachment
133             var regex = new Regex(
134                 @"(<[1-9][0-9]* attachments?>) (http://www\.facebook\.com/messages/\?action=read&tid=[0-9a-f]+)",
135                 RegexOptions.Compiled
136             );
137             BuiltinPatterns.Add(new MessagePatternModel(regex) {
138                 LinkFormat = "{2}",
139                 TextFormat = "{1}",
140             });
141 
142             // protocol://user@domain:port/path
143             regex = new Regex(
144                 protocol_user_host_port_path,
145                 RegexOptions.IgnoreCase | RegexOptions.Compiled
146             );
147             BuiltinPatterns.Add(new MessagePatternModel(regex));
148 
149             // email
150             regex = new Regex(
151                 @"(?:mailto:)?(" + user_domain + ")",
152                 RegexOptions.IgnoreCase | RegexOptions.Compiled
153             );
154             BuiltinPatterns.Add(new MessagePatternModel(regex) {
155                 LinkFormat = "mailto:{1}"
156             });
157 
158             // bitcoin address
159             var bitcoin_address = @"[13][a-km-zA-HJ-NP-Z1-9]{25,34}";
160             var query = @"(\?[" + path_chars + @"]+)?";
161             var bitcoin_address_query = bitcoin_address + query;
162             regex = new Regex(
163                 @"bitcoin:(" + bitcoin_address_query + @")",
164                 RegexOptions.IgnoreCase | RegexOptions.Compiled
165             );
166             BuiltinPatterns.Add(new MessagePatternModel(regex) {
167                 LinkFormat = "bitcoin:{1}"
168             });
169 
170             // bitcoin address (heuristical)
171             regex = new Regex(
172                 start_delimiter + @"(" + bitcoin_address_query + @")" + end_delimiter,
173                 RegexOptions.IgnoreCase | RegexOptions.Compiled
174             );
175             BuiltinPatterns.Add(new MessagePatternModel(regex) {
176                 LinkFormat = "bitcoin:{1}"
177             });
178 
179             // bitcoin tx hash
180             var bitcoin_tx_hash = @"[a-fA-F0-9]{64}";
181             regex = new Regex(
182                 start_delimiter + @"(" + bitcoin_tx_hash + @")" + end_delimiter,
183                 RegexOptions.IgnoreCase | RegexOptions.Compiled
184             );
185             BuiltinPatterns.Add(new MessagePatternModel(regex) {
186                 LinkFormat = "https://blockchain.info/tx/{1}"
187             });
188 
189             // addresses without protocol (heuristical)
190             // include well known TLDs to prevent autogen.sh, configure.ac or
191             // Gst.Buffer.Unref() from matching
192             string heuristic_domain = @"(?:(?:" + subdomain + ")+(?:" + common_tld + ")|localhost)";
193             string heuristic_address = @"(" +heuristic_domain + ")(?:" + path + ")?" + end_delimiter;
194             regex = new Regex(
195                 heuristic_address,
196                 RegexOptions.IgnoreCase | RegexOptions.Compiled
197             );
198             BuiltinPatterns.Add(new MessagePatternModel(regex) {
199                 LinkFormat = "http://{1}"
200             });
201 
202             // Smuxi bugtracker
203             regex = new Regex(@"smuxi#([0-9]+)",
204                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
205             BuiltinPatterns.Add(new MessagePatternModel(regex) {
206                 LinkFormat = "https://smuxi.im/issues/show/{1}"
207             });
208 
209             // RFCs
210             regex = new Regex(@"RFC[ -]?([0-9]+) (?:s\.|ss\.|sec\.|sect\.|section) ?([1-9][0-9.]*)",
211                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
212             BuiltinPatterns.Add(new MessagePatternModel(regex) {
213                 LinkFormat = "http://tools.ietf.org/html/rfc{1}#section-{2}"
214             });
215             regex = new Regex(@"RFC[ -]?([0-9]+) (?:p\.|pp\.|page) ?(" + short_number + ")",
216                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
217             BuiltinPatterns.Add(new MessagePatternModel(regex) {
218                 LinkFormat = "http://tools.ietf.org/html/rfc{1}#page-{2}"
219             });
220             regex = new Regex(@"RFC[ -]?([0-9]+)",
221                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
222             BuiltinPatterns.Add(new MessagePatternModel(regex) {
223                 LinkFormat = "http://www.ietf.org/rfc/rfc{1}.txt"
224             });
225 
226             // XEPs
227             regex = new Regex(@"XEP[ -]?([0-9]{4})",
228                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
229             BuiltinPatterns.Add(new MessagePatternModel(regex) {
230                 LinkFormat = "http://xmpp.org/extensions/xep-{1}.html"
231             });
232 
233             // ISO
234             regex = new Regex(@"ISO[ -]?([0-9]{4,5}(?:-[0-9]+)?(?::[0-9]{4})?)",
235                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
236             BuiltinPatterns.Add(new MessagePatternModel(regex) {
237                 LinkFormat = "http://www.iso.org/iso/home/search.htm?qt={1}&published=on"
238             });
239 
240             // ECMA
241             regex = new Regex(@"ECMA[ -]?([0-9]{1,4})",
242                                   RegexOptions.IgnoreCase | RegexOptions.Compiled);
243             BuiltinPatterns.Add(new MessagePatternModel(regex) {
244                 LinkFormat = "http://www.ecma-international.org/publications/standards/ECMA-{1}.htm"
245             });
246 
247             // IEEE: IEEE-1394b, IEEE 802.11, IEEE 802.1ap-2008, IEEE 802.1AEbn-2011
248             regex = new Regex(@"IEEE[ -]?([0-9]{1,4}(?:\.[0-9]{1,4})?(?:[a-z]{1,4})?(?:-[0-9]{4})?)",
249                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
250             BuiltinPatterns.Add(new MessagePatternModel(regex) {
251                 LinkFormat = "http://odysseus.ieee.org/query.html?qt={1}&style=standard"
252             });
253 
254             // bugtracker prefixes are taken from:
255             // http://en.opensuse.org/openSUSE:Packaging_Patches_guidelines#Current_set_of_abbreviations
256 
257             // boost bugtracker
258             regex = new Regex(@"boost#([0-9]+)",
259                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
260             BuiltinPatterns.Add(new MessagePatternModel(regex) {
261                 LinkFormat = "https://svn.boost.org/trac/boost/ticket/{1}"
262             });
263 
264             // Claws bugtracker
265             regex = new Regex(@"claws#([0-9]+)",
266                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
267             BuiltinPatterns.Add(new MessagePatternModel(regex) {
268                 LinkFormat = "http://www.thewildbeast.co.uk/claws-mail/bugzilla/show_bug.cgi?id={1}"
269             });
270 
271             // CVE list
272             regex = new Regex(@"CVE-[0-9]{4}-[0-9]{4,}",
273                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
274             BuiltinPatterns.Add(new MessagePatternModel(regex) {
275                 LinkFormat = "http://cve.mitre.org/cgi-bin/cvename.cgi?name={0}"
276             });
277 
278             // CPAN bugtracker
279             regex = new Regex(@"RT#([0-9]+)",
280                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
281             BuiltinPatterns.Add(new MessagePatternModel(regex) {
282                 LinkFormat = "http://rt.cpan.org/Public/Bug/Display.html?id={1}"
283             });
284 
285             // Debian bugtracker
286             regex = new Regex(@"deb#([0-9]+)",
287                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
288             BuiltinPatterns.Add(new MessagePatternModel(regex) {
289                 LinkFormat = "http://bugs.debian.org/{1}"
290             });
291 
292             // Debian Security Advisories (DSA)
293             regex = new Regex(@"DSA[ -]?([0-9]{4})(-[0-9]{1,2})?",
294                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
295             BuiltinPatterns.Add(new MessagePatternModel(regex) {
296                 LinkFormat = "http://www.debian.org/security/dsa-{1}"
297             });
298 
299             // openSUSE feature tracker
300             regex = new Regex(@"fate#([0-9]+)",
301                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
302             BuiltinPatterns.Add(new MessagePatternModel(regex) {
303                 LinkFormat = "http://features.opensuse.org/{1}"
304             });
305 
306             // freedesktop bugtracker
307             regex = new Regex(@"fdo#([0-9]+)",
308                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
309             BuiltinPatterns.Add(new MessagePatternModel(regex) {
310                 LinkFormat = "http://bugs.freedesktop.org/{1}"
311             });
312 
313             // GNU bugtracker
314             regex = new Regex(@"gnu#([0-9]+)",
315                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
316             BuiltinPatterns.Add(new MessagePatternModel(regex) {
317                 LinkFormat = "http://debbugs.gnu.org/{1}"
318             });
319 
320             // GCC bugtracker
321             regex = new Regex(@"gcc#([0-9]+)",
322                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
323             BuiltinPatterns.Add(new MessagePatternModel(regex) {
324                 LinkFormat = "http://gcc.gnu.org/bugzilla/show_bug.cgi?id={1}"
325             });
326 
327             // GNOME bugtracker
328             regex = new Regex(@"bgo#([0-9]+)",
329                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
330             BuiltinPatterns.Add(new MessagePatternModel(regex) {
331                 LinkFormat = "http://bugzilla.gnome.org/{1}"
332             });
333 
334             // KDE bugtracker
335             regex = new Regex(@"kde#([0-9]+)",
336                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
337             BuiltinPatterns.Add(new MessagePatternModel(regex) {
338                 LinkFormat = "http://bugs.kde.org/{1}"
339             });
340 
341             // kernel bugtracker
342             regex = new Regex(@"bko#([0-9]+)",
343                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
344             BuiltinPatterns.Add(new MessagePatternModel(regex) {
345                 LinkFormat = "http://bugzilla.kernel.org/show_bug.cgi?id={1}"
346             });
347 
348             // launchpad bugtracker
349             regex = new Regex(@"LP#([0-9]+)",
350                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
351             BuiltinPatterns.Add(new MessagePatternModel(regex) {
352                 LinkFormat = "http://launchpad.net/bugs/{1}"
353             });
354 
355             // Mozilla bugtracker
356             regex = new Regex(@"bmo#([0-9]+)",
357                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
358             BuiltinPatterns.Add(new MessagePatternModel(regex) {
359                 LinkFormat = "http://bugzilla.mozilla.org/{1}"
360             });
361 
362             // Novell bugtracker
363             regex = new Regex(@"bnc#([0-9]+)",
364                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
365             BuiltinPatterns.Add(new MessagePatternModel(regex) {
366                 LinkFormat = "http://bugzilla.novell.com/{1}"
367             });
368 
369             // Redhat bugtracker
370             regex = new Regex(@"rh#([0-9]+)",
371                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
372             BuiltinPatterns.Add(new MessagePatternModel(regex) {
373                 LinkFormat = "http://bugzilla.redhat.com/{1}"
374             });
375 
376             // Samba bugtracker
377             regex = new Regex(@"bso#([0-9]+)",
378                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
379             BuiltinPatterns.Add(new MessagePatternModel(regex) {
380                 LinkFormat = "http://bugzilla.samba.org/show_bug.cgi?id={1}"
381             });
382 
383             // sourceforge bugtracker
384             regex = new Regex(@"sf#([0-9]+)",
385                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
386             BuiltinPatterns.Add(new MessagePatternModel(regex) {
387                 LinkFormat = "http://sf.net/support/tracker.php?aid={1}"
388             });
389 
390             // Xfce bugtracker
391             regex = new Regex(@"bxo#([0-9]+)",
392                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
393             BuiltinPatterns.Add(new MessagePatternModel(regex) {
394                 LinkFormat = "http://bugzilla.xfce.org/show_bug.cgi?id={1}"
395             });
396 
397             // Xamarin bugtracker
398             regex = new Regex(@"bxc#([0-9]+)",
399                               RegexOptions.IgnoreCase | RegexOptions.Compiled);
400             BuiltinPatterns.Add(new MessagePatternModel(regex) {
401                 LinkFormat = "http://bugzilla.xamarin.com/show_bug.cgi?id={1}"
402             });
403 
404             // TODO: msgid -> http://mid.gmane.org/{1}
405             // TODO: ISSN/ISBN
406             // TODO: Path: / or X:\
407             // TODO: GPS -> Google Maps
408             // TODO: IP -> Browser / Whois
409             // TODO: Domain -> Browser / Whois
410             // TODO: ANSI
411             // TODO: maybe more on http://ikiwiki.info/shortcuts/
412             // TODO: JID
413         }
414 
ApplyConfig(UserConfig userConfig)415         public void ApplyConfig(UserConfig userConfig)
416         {
417             if (userConfig == null) {
418                 throw new ArgumentNullException("userConfig");
419             }
420 
421             NickColors = (bool) userConfig["Interface/Notebook/Channel/NickColors"];
422             StripColors = (bool) userConfig["Interface/Notebook/StripColors"];
423             StripFormattings = (bool) userConfig["Interface/Notebook/StripFormattings"];
424             HighlightColor = TextColor.Parse(
425                 (string) userConfig["Interface/Notebook/Tab/HighlightColor"]
426             );
427             HighlightWords = new List<string>(
428                 (string[]) userConfig["Interface/Chat/HighlightWords"]
429             );
430 
431             var patternController = new MessagePatternListController(userConfig);
432             var userPatterns = patternController.GetList();
433             var builtinPatterns = BuiltinPatterns;
434             var patterns = new List<MessagePatternModel>(builtinPatterns.Count +
435                                                          userPatterns.Count);
436             // No need to lock BuiltinPatterns as List<T> is thread-safe for
437             // multiple readers as long as there is no writer at the same time.
438             // BuiltinPatterns is only written once before the first instance
439             // of MessageBuilderSettings is created via the static initializer.
440             patterns.AddRange(builtinPatterns);
441             patterns.AddRange(userPatterns);
442             Patterns = patterns;
443             UserPatterns = userPatterns;
444 
445             // The Patterns property has to be initialized before we set the
446             // Emojis property as it will modify the existing Patterns collection
447             Emojis = (bool) userConfig["Interface/Chat/Emojis"];
448         }
449     }
450 }
451