summaryrefslogtreecommitdiff
path: root/localmarkdown2sms
blob: 17677887dd056344e898b2466cc1103aab4a0874 (plain)
  1. #!/usr/bin/perl
  2. #
  3. # /usr/local/sbin/localmarkdown2sms
  4. # Copyright 2009-2010, Jonas Smedegaard <dr@jones.dk>
  5. #
  6. # Send series of messages through Kannel from simplified Markdown files
  7. # * Lines starting with "#" are "keywords" activating a message series
  8. # * write only a single word
  9. # * use each keyword only once across the whole system
  10. # * use only minuscles (not majuscles, i.e. CAPITAL LETTERS)
  11. # * Lines starting with "##" express pauses
  12. # * a pause is a number + a single letter, without spaces between
  13. # * a pause line can contain multiple pauses, separated by space
  14. # Suggestion for writing style:
  15. #
  16. # * Write explicitly how to activate next series
  17. # * pick keywords tied to nex series rather than the previous
  18. # * use same instruction jargon across all series in the system
  19. use strict;
  20. use warnings;
  21. use Env qw[$debug $info $warn $dummy $nosleep $urldecode];
  22. use Log::Log4perl qw(:easy);
  23. use File::Spec;
  24. use File::Slurp;
  25. use Time::Duration::Parse;
  26. use Text::Unidecode;
  27. use Encode 2.12 qw(encode decode_utf8 _utf8_off); # need v2.12 to support coderef
  28. use LWP::UserAgent;
  29. use URI::Escape;
  30. use Proc::Daemon;
  31. Proc::Daemon::Init unless ($debug);
  32. my $sms_url = $ENV{SMS_URL} || "http://localhost:13013/cgi-bin/sendsms";
  33. my $sms_usertag = $ENV{SMS_USERTAG} || "username";
  34. my $sms_user = $ENV{SMS_USER} || "tester";
  35. my $sms_pw = $ENV{SMS_PW} || "foobar";
  36. my $sms_api = $ENV{SMS_API};
  37. my $sms_phone = $ENV{SMS_PHONE};
  38. my $sms_smsc = $ENV{SMS_SMSC};
  39. my $sms_msgtag = $ENV{SMS_MSGTAG} || "text";
  40. my $sms_cp = $ENV{SMS_CP} || "utf8";
  41. my $sms_concat = $ENV{SMS_CONCAT};
  42. my $sms_dlr_mask = $ENV{SMS_DLR_MASK};
  43. my $sms_dlr_url = $ENV{SMS_DLR_URL};
  44. my $sms_validity = $ENV{SMS_VALIDITY};
  45. my $stripprefix = $ENV{stripprefix};
  46. my $path = $ENV{mdpath};
  47. my (%file, %delay, %reply);
  48. # decode data if passed from kannel
  49. if ($urldecode) {
  50. @ARGV = uri_unescape(@ARGV);
  51. }
  52. my ($phone) = shift @ARGV;
  53. my ($key) = lc (shift @ARGV);
  54. # strip international prefix
  55. # (prefix is optional some places and illegal at other places - forgot where)
  56. $phone =~ s/^\+//g if ($stripprefix);
  57. $sms_phone =~ s/^\+//g if ($stripprefix);
  58. # strip non-word chars from keyword (and use only first chunk of word chars)
  59. $key =~ s/.*?(\w+).*?/$1/;
  60. if ($debug) {
  61. Log::Log4perl->easy_init($DEBUG);
  62. } elsif ($INFO) {
  63. Log::Log4perl->easy_init($INFO);
  64. } elsif ($WARN) {
  65. Log::Log4perl->easy_init($WARN);
  66. } elsif ($ERROR) {
  67. Log::Log4perl->easy_init($ERROR);
  68. }
  69. unless ($path and -d $path) {
  70. ERROR "environment variable \"mdpath\" missing or wrong.";
  71. exit 1;
  72. }
  73. foreach my $file (read_dir( $path )) {
  74. my ($key, $i, $skipkeysection, $skipcontent);
  75. # suppress repeated warnings for same issue
  76. my ($warn_nonkey_delay, $warn_nonkey_content);
  77. next unless ($file =~ /\.mdwn$/);
  78. foreach my $line (read_file( File::Spec->catfile($path, $file))) {
  79. chomp $line;
  80. my $content;
  81. # headline
  82. if ($line =~ /^(#+)\s*(.*?)\s*$/) {
  83. # tidy latest reply
  84. if (defined($key) and defined($reply{$key}[$i])) {
  85. $reply{$key}[$i] = &tidymsg($reply{$key}[$i]);
  86. ($reply{$key}[$i]) || delete $reply{$key}[$i];
  87. }
  88. my $level = length($1);
  89. $content = $2;
  90. # key
  91. if ($level == 1 and $content =~ /(\w+)/) {
  92. $key = lc($1);
  93. $i = 0;
  94. $skipkeysection = undef;
  95. $skipcontent = undef;
  96. if (lc($content) ne $key) {
  97. WARN "key \"$key\" extracted from fuzzy string \"$content\" in file \"$file\"";
  98. }
  99. if (!defined( $delay{$key})) {
  100. $delay{$key}[0] = 0;
  101. $warn_nonkey_delay = undef;
  102. $warn_nonkey_content = undef;
  103. } else {
  104. WARN "skipping non-unique key \"$key\" in file \"$file\"";
  105. $key = undef;
  106. $skipkeysection = 1;
  107. $skipcontent = 1;
  108. }
  109. # delay
  110. } elsif ($level == 2 and $content =~ /((\d+[sm]\s?)+)/) {
  111. $skipcontent = undef;
  112. if (defined( $key)) {
  113. my $delay = parse_duration($1);
  114. if (defined($reply{$key}[$i])) {
  115. $i++;
  116. $delay{$key}[$i] = $delay{$key}[$i - 1];
  117. }
  118. # $delay{$key}[$i] += $delay; # accumulate: forked replies
  119. $delay{$key}[$i] = $delay; # simple: queued replies
  120. if ($content ne $1) {
  121. WARN "delay (${delay}s) resolved from fuzzy string \"$content\" in file \"$file\"";
  122. }
  123. } elsif ($skipkeysection or $warn_nonkey_delay) {
  124. # skipping - already warned about it...
  125. } else {
  126. WARN "ignoring non-key'ed delay line \"$1\" in file \"$file\"";
  127. $warn_nonkey_delay = 1;
  128. $skipcontent = 1;
  129. }
  130. } else {
  131. WARN "ignoring non-parsable headline \"$line\" in file \"$file\"";
  132. $skipcontent = 1;
  133. }
  134. # reply
  135. } else {
  136. $content = $line . "\n";
  137. # ikiwiki directives - strip from content and parse for tags
  138. $content =~ s/(?<!\\)\[\[([^\[\]]*)(?<!\\)\]\]//gs and do {
  139. my $directive_string = $1;
  140. my ($directive, $directive_content);
  141. $directive_string =~ /^\s*\!(tag|taglink)\s*((\s*?\b\w+)+)/ and $file{$file}{'directive'}{'tag'} = [ split /\s+/, $2 ];
  142. };
  143. if ( defined( $key ) and not ($skipcontent)) {
  144. $reply{$key}[$i] .= $content;
  145. } elsif ($skipkeysection or $skipcontent or $warn_nonkey_content) {
  146. # skipping - already warned about it...
  147. } else {
  148. $content =~ /\S/s && WARN "skipping non-key'ed content \"$line\" in file \"$file\"";
  149. $warn_nonkey_content = 1;
  150. }
  151. }
  152. }
  153. # tidy latest reply
  154. if (defined($key) and defined($reply{$key}[$i])) {
  155. $reply{$key}[$i] = &tidymsg($reply{$key}[$i]);
  156. ($reply{$key}[$i]) || delete $reply{$key}[$i];
  157. }
  158. }
  159. sub tidymsg {
  160. my $msg = shift @_;
  161. $msg =~ s/^\h*$//g; # clean virtually empty lines
  162. $msg =~ s/(\S)\h$/$1/g; # strip single trailing space
  163. $msg =~ s/\n\n+/\n\n/g; # strip excess newlines
  164. $msg =~ s/(\S)\n([^\n])/$1 $2/g; # convert newline to space
  165. $msg =~ s/\h*$//g; # strip all trailing spaces
  166. $msg =~ s/^\s*(\w*?.*?)\s*$/$1/s; # strip surrounding space
  167. return $msg;
  168. }
  169. # based on Text::Unidecode bug#8017: http://rt.cpan.org/Ticket/Display.html?id=8017#txn-322351
  170. sub transliterate {
  171. my ($tocharset, $string) = @_;
  172. my $res = encode($tocharset, decode_utf8($string), sub {
  173. my $ascii = unidecode(chr $_[0]);
  174. _utf8_off($ascii);
  175. $ascii;
  176. });
  177. return $res;
  178. }
  179. sub sendmsg {
  180. my ($phone, $desc, $msg) = @_;
  181. unless ($dummy) {
  182. my $ua = LWP::UserAgent->new(agent => "localmarkdown2sms");
  183. $ua->timeout(10);
  184. my $url = $sms_url
  185. . '?' . $sms_usertag . '=' . uri_escape($sms_user)
  186. . '&password=' . uri_escape($sms_pw)
  187. . '&to=' . uri_escape($phone);
  188. $url .= '&api_id=' . uri_escape($sms_api) if ($sms_api);
  189. $url .= '&from=' . uri_escape($sms_phone) if ($sms_phone);
  190. $url .= '&smsc=' . uri_escape($sms_smsc) if ($sms_smsc);
  191. $url .= '&concat=' . uri_escape($sms_concat) if ($sms_concat);
  192. $url .= '&dlr-mask=' . uri_escape($sms_dlr_mask) if ($sms_dlr_mask);
  193. $url .= '&dlr-url=' . uri_escape($sms_dlr_url) if ($sms_dlr_url);
  194. $url .= '&validity=' . uri_escape($sms_validity) if ($sms_validity);
  195. $url .= '&' . $sms_msgtag . '=' . uri_escape(transliterate($sms_cp, $msg));
  196. DEBUG "Sending request: $url";
  197. my $response = $ua->request(HTTP::Request->new('GET', $url));
  198. unless ($response->is_success) {
  199. ERROR $response->status_line;
  200. }
  201. DEBUG "Done $desc";
  202. } else {
  203. print STDERR "\n --> $phone: $desc\n";
  204. print STDERR $msg . "\n";
  205. }
  206. }
  207. my $num_children = $#{ $reply{$key} } + 1; # How many children we'll create
  208. if (0 == $num_children) {
  209. &sendmsg($phone, "fallback message", "Sorry, the sms code \"$key\" is unknown.\nPlease send only sms codes to this number.");
  210. exit;
  211. }
  212. if ($debug) {
  213. DEBUG "queueing $num_children replies:";
  214. for my $num ( 0 .. $num_children - 1 ) {
  215. DEBUG " [" . $delay{$key}[$num] . "s]";
  216. }
  217. # DEBUG "\n";
  218. }
  219. for my $num ( 0 .. $num_children - 1 ) {
  220. sleep($delay{$key}[$num]) unless ($nosleep);
  221. &sendmsg($phone, "reply #$num [" . $delay{$key}[$num] . "s]", $reply{$key}[$num]);
  222. }
  223. 1;