clone url: git://git.m455.casa/linkbud
linkbud.scm
1 | (import utf8 |
2 | openssl |
3 | srfi-95 |
4 | srfi-152 |
5 | (chicken pathname) |
6 | (chicken pretty-print) |
7 | (chicken io) |
8 | (chicken file) |
9 | (chicken string) ;; string-split will be used from here, because it's defined after srfi-152 |
10 | (chicken format) |
11 | (chicken process) |
12 | (chicken process-context) |
13 | (chicken tcp) |
14 | (chicken time posix)) |
15 |
|
16 | (define log-file "log.scm") |
17 | (define config-file "config.scm") |
18 |
|
19 | (define html-header-path "templates/html/header.html") |
20 | (define html-post-path "templates/html/post.html") |
21 | (define html-footer-path "templates/html/footer.html") |
22 | (define html-file "index.html") |
23 |
|
24 | (define rss-header-path "templates/rss/header.xml") |
25 | (define rss-item-path "templates/rss/item.xml") |
26 | (define rss-footer-path "templates/rss/footer.xml") |
27 | (define rss-file "feed.rss") |
28 |
|
29 | (define config-alist #f) |
30 | (define html-header-value #f) |
31 | (define html-post-value #f) |
32 | (define html-footer-value #f) |
33 | (define rss-header-value #f) |
34 | (define rss-item-value #f) |
35 | (define rss-footer-value #f) |
36 |
|
37 | (define-values (from-server to-server) (values #f #f)) |
38 |
|
39 | (define config-contents |
40 | #<<string-block |
41 | ( |
42 | ;; this file was generated by linkbud. |
43 | ;; source: https://git.m455.casa/linkbud |
44 | (irc-host . "irc.libera.chat") |
45 | (irc-port . 6667) |
46 | (irc-ssl . #f) ;; can be #t or #f |
47 |
|
48 | ;; can be #f, (irc-password "mycoolpassword"), or, if it's supported, you can |
49 | ;; use the following password as a sasl workaround (ergo.chat supports this): |
50 | ;; (irc-password "username:mycoolpassword") |
51 | (irc-password . #f) |
52 |
|
53 | ;; irc servers, such as https://ergo.chat/ provide a history playback feature, |
54 | ;; which can cause bots to pick up old, already-processed messages when |
55 | ;; connecting. set this to #t if you want to disable history playback. |
56 | ;; such as ergo.chat. |
57 | (irc-disable-history? . #f) |
58 |
|
59 | (irc-nickname . "linkbud") ;; this value shows up as the bot's name in chat, and can be registered |
60 | (irc-username . "linkbud") ;; this value shows up in join and leave messages |
61 | (irc-realname . "linkbud") ;; this value shows up in... i don't know haha |
62 |
|
63 | ;; add channels in the list below that you want linkbud to listen and respond to |
64 | ;; for example: |
65 | ;; (irc-channels ("#basement" |
66 | ;; "#another-channel" |
67 | ;; "#some-other-channel")) |
68 | (irc-channels . ("#linkbud-demo")) |
69 |
|
70 | ;; the following items use this value: |
71 | ;; - the templates/rss/rss-header.xml |
72 | ;; - the templates/rss/rss-item.xml |
73 | (base-url . "https://linkbudz.example.com") |
74 |
|
75 | ;; the directory where the index.html and feed.rss files will be generated for |
76 | ;; for example: |
77 | ;; (build-path "/var/www/my-website-directory") |
78 | (build-path . "build") |
79 |
|
80 | ;; this only shows up in the rss feed itself |
81 | (rss-feed-title . "linkbudz rss feed") |
82 | (rss-description-value . "links shared by friends") |
83 |
|
84 | ;; the html <title> and <h1> use this |
85 | (html-title . "LINKBUDZ") |
86 |
|
87 | ;; message shown after someone submits a post |
88 | (post-response . "post added to linkbudz!") |
89 | ) |
90 | string-block |
91 | ) |
92 |
|
93 | (define html-header-contents |
94 | #<<string-block |
95 | <!DOCTYPE html> |
96 | <html> |
97 | <head> |
98 | <meta charset="utf-8" /> |
99 | <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" /> |
100 | <link rel="icon" href="data:,"> |
101 | <link rel="alternate" type="application/rss+xml" title="{{rss-feed-title}}" href="{{base-url}}/feed.rss"/> |
102 | <title>{{html-title}}</title> |
103 | <style> |
104 | body { |
105 | font-family: sans-serif; |
106 | font-size: 18px; |
107 | margin: 0 auto; |
108 | max-width: 600px; |
109 | } |
110 |
|
111 | hr { |
112 | border: 0; |
113 | border-bottom: 2px solid black; |
114 | } |
115 |
|
116 | a { |
117 | color: blue; |
118 | text-decoration: none; |
119 | font-weight: bold; |
120 | } |
121 |
|
122 | dd { |
123 | margin: 4px 0; |
124 | } |
125 |
|
126 | .last-dd { |
127 | margin-bottom: 30px; |
128 | } |
129 |
|
130 | .protocol { |
131 | color: #3a4f3a; |
132 | background-color: #beffbe; |
133 | padding: 2px 5px; |
134 | } |
135 |
|
136 | @media only screen and (max-width: 700px) { |
137 | body { |
138 | margin: 10px; |
139 | } |
140 | } |
141 |
|
142 | @media (prefers-color-scheme: dark) { |
143 | body { |
144 | background-color: #111; |
145 | color: #eee; |
146 | } |
147 | hr { |
148 | border-bottom: 2px solid #eee; |
149 | } |
150 | a { |
151 | color: #0ff; |
152 | } |
153 | .protocol { |
154 | color: #ff7a7a; |
155 | background-color: #111; |
156 | padding: 0; |
157 | } |
158 | } |
159 | </style> |
160 | <script> |
161 | function copyId(button_tag) { |
162 | /* get the text */ |
163 | var elem = button_tag.parentElement.querySelector('span.id').innerHTML; |
164 |
|
165 | /* Copy the text inside the text field */ |
166 | navigator.clipboard.writeText(elem); |
167 |
|
168 | /* change the button text */ |
169 | button_tag.innerHTML = "Copied!"; |
170 | } |
171 | </script> |
172 | <noscript> |
173 | <style> |
174 | .dont-show-if-javascript-is-disabled { |
175 | display: none; |
176 | } |
177 | </style> |
178 | </noscript> |
179 | </head> |
180 | <body> |
181 | <h1>{{html-title}}</h1> |
182 | <p><a href="{{base-url}}/feed.rss">RSS</a></p> |
183 | <hr aria-hidden="true"> |
184 | <dl> |
185 | string-block |
186 | ) |
187 |
|
188 | (define html-post-contents |
189 | #<<string-block |
190 | <dt> |
191 | <a href="{{post-url}}">{{post-title}}</a> |
192 | </dt> |
193 | <dd> |
194 | <span class="protocol">{{post-protocol}}</span> posted by |
195 | <span class="sender">{{post-sender}}</span> on |
196 | <span class="time">{{post-date-human}}</span> |
197 | </dd> |
198 | <dd class="last-dd"> |
199 | <span aria-hidden="true">ID: <span class="id">{{post-guid}}</span></span> |
200 | <button class="dont-show-if-javascript-is-disabled" onclick="copyId(this)">Copy ID</button> |
201 | </dd> |
202 | string-block |
203 | ) |
204 |
|
205 | (define html-footer-contents |
206 | #<<string-block |
207 | </dl> |
208 | </body> |
209 | </html> |
210 | string-block |
211 | ) |
212 |
|
213 | (define rss-header-contents |
214 | #<<string-block |
215 | <?xml version="1.0" encoding="UTF-8" ?> |
216 | <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"> |
217 | <channel> |
218 | <title>{{rss-feed-title}}</title> |
219 | <link>{{base-url}}</link> |
220 | <description>{{rss-description-value}}</description> |
221 | <atom:link href="{{base-url}}/feed.rss" rel="self" type="application/rss+xml" /> |
222 | string-block |
223 | ) |
224 |
|
225 | (define rss-item-contents |
226 | #<<string-block |
227 | <item> |
228 | <title><![CDATA[{{item-title}}]]></title> |
229 | <link><![CDATA[{{item-url}}]]></link> |
230 | <guid isPermaLink="false">{{item-guid}}</guid> |
231 | <category domain="{{base-url}}">{{item-protocol}}</category> |
232 | <pubDate>{{item-date-rss}}</pubDate> |
233 | <description> |
234 | <![CDATA[ |
235 | <p><a href="{{item-url}}">{{item-title}}</a></p> |
236 | <p>[{{item-protocol}}] posted by {{item-sender}} on {{item-date-human}}</p> |
237 | ]]> |
238 | </description> |
239 | </item> |
240 | string-block |
241 | ) |
242 |
|
243 | (define rss-footer-contents |
244 | #<<string-block |
245 | </channel> |
246 | </rss> |
247 | string-block |
248 | ) |
249 |
|
250 | (define (ormap f l) |
251 | (if (null? l) |
252 | #f |
253 | (if (f (car l)) |
254 | #t |
255 | (ormap f (cdr l))))) |
256 |
|
257 | (define (file-write path contents) |
258 | (with-output-to-file path |
259 | (lambda () (print contents)))) |
260 |
|
261 | (define (file-append path contents) |
262 | (with-output-to-file path |
263 | (lambda () (print contents)) |
264 | #:append)) |
265 |
|
266 | (define (pretty-print-to-file path contents) |
267 | (with-output-to-file path |
268 | (lambda () (pretty-print contents)))) |
269 |
|
270 | (define (file-doesnt-exist-and-exit file) |
271 | (print (format "error: ~a doesn't exist. try running 'linkbud init'" file)) |
272 | (exit)) |
273 |
|
274 | (define (load-config!) |
275 | (if (file-exists? config-file) |
276 | (set! config-alist (with-input-from-file config-file read)) |
277 | (file-doesnt-exist-and-exit config-file))) |
278 |
|
279 | (define (config-get key) |
280 | (alist-ref key config-alist)) |
281 |
|
282 | ;; use syntax-rules here to delay evaluation of value, otherwise #f is passed to |
283 | ;; the parameter on startup, because I set the values all to #f by default on |
284 | ;; startup, which would cause this function to set everything to #f everytime |
285 | ;; it's called |
286 | (define-syntax load-template! |
287 | (syntax-rules () |
288 | ((_ value file) |
289 | (if (file-exists? file) |
290 | (set! value (with-input-from-file file read-string)) |
291 | (file-doesnt-exist-and-exit file))))) |
292 |
|
293 | (define (load-templates!) |
294 | (load-template! html-header-value html-header-path) |
295 | (load-template! html-post-value html-post-path) |
296 | (load-template! html-footer-value html-footer-path) |
297 | (load-template! rss-header-value rss-header-path) |
298 | (load-template! rss-item-value rss-item-path) |
299 | (load-template! rss-footer-value rss-footer-path)) |
300 |
|
301 | (define (pair->mustached-pair pair) |
302 | (let* ((first (car pair)) |
303 | (key (if (string? first) |
304 | first |
305 | (symbol->string first)))) |
306 | `(,(string-append "{{" key "}}") . ,(cdr pair)))) |
307 |
|
308 | (define (string-populate str smap) |
309 | (string-translate* |
310 | str |
311 | (map pair->mustached-pair smap))) |
312 |
|
313 | (define (string-populate-with-default-values str) |
314 | (string-translate* |
315 | str |
316 | (map pair->mustached-pair config-alist))) |
317 |
|
318 | (define (shell-command str) |
319 | (call-with-input-pipe str read-line)) |
320 |
|
321 | ;; returns "2022-02-14 21:17:26" because the `date -Rd'<some_date>'` doesn't |
322 | ;; like it without the hyphens or colons |
323 | (define (date-current) |
324 | (time->string (seconds->local-time) "%Y-%m-%d %H:%M:%S")) |
325 |
|
326 | ;; returns "Mon, 14 Feb 2022 21:17:26 -0500" |
327 | (define (date->rss-date str) |
328 | (time->string (string->time str "%Y-%m-%d %H:%M") |
329 | "%a, %d %b %Y %H:%M:%S %z")) |
330 |
|
331 | ;; returns "February 14, 2022" |
332 | (define (date->human-date str) |
333 | (time->string (string->time str "%Y-%m-%d %H:%M") |
334 | "%B %d, %Y")) |
335 |
|
336 | (define (clean-input-for-html str) |
337 | (if (or (string-contains str "javascript:") |
338 | (string-contains str "mailto:")) |
339 | (clean-input-for-html (string-translate* str '(("javascript:" . "") |
340 | ("mailto:" . "") |
341 | ("]]>" . "")))) |
342 | (string-translate* str '(("&" . "&") |
343 | ("<" . "<") |
344 | (">" . ">") |
345 | ("\"" . """) |
346 | ("'" . "'"))))) |
347 |
|
348 | (define (clean-input-html->rss str) |
349 | ;; we don't need the other stuff that clean-input-for-html does, such as the |
350 | ;; javascript and mailto stuff, because the str is passed through |
351 | ;; clean-input-for-html before getting to this procedure |
352 | (string-translate* str '(("&" . "&") |
353 | ("<" . "<") |
354 | (">" . ">") |
355 | (""" . "\"") |
356 | ("'" . "'")))) |
357 |
|
358 | (define (fix-url url-str) |
359 | (let ((split-str (string-split url-str "://"))) |
360 | (if (> (length split-str) 1) |
361 | url-str |
362 | (string-append "https://" url-str)))) |
363 |
|
364 | (define (get-protocol url-str) |
365 | (let ((split-str (string-split url-str "://"))) |
366 | (if (> (length split-str) 1) |
367 | (car split-str) |
368 | "https"))) |
369 |
|
370 | (define (sort-by-date mode log) |
371 | (let ((sort-list-of-alists (lambda () |
372 | (sort log (lambda (x-alist y-alist) |
373 | (< (alist-ref 'date x-alist) |
374 | (alist-ref 'date y-alist))))))) |
375 | (if (eq? mode 'reverse) |
376 | (reverse (sort-list-of-alists)) |
377 | (sort-list-of-alists)))) |
378 |
|
379 | (define (log-file->sexp) |
380 | (if (file-exists? log-file) |
381 | (with-input-from-file log-file read) |
382 | '())) |
383 |
|
384 | (define (id-exists? id-str list-of-post-alists) |
385 | (if (null? list-of-post-alists) |
386 | #f |
387 | (let ((current-alist (car list-of-post-alists))) |
388 | (if (equal? id-str (alist-ref 'guid current-alist)) |
389 | ;; we return this here, instead of returning #t, so we can use this |
390 | ;; procedure with an (remove-alist-from-list ... alist) procedure |
391 | current-alist |
392 | (id-exists? id-str (cdr list-of-post-alists)))))) |
393 |
|
394 | (define (remove-alist-from-list alist list-of-alists) |
395 | (if (null? list-of-alists) |
396 | '() |
397 | (if (equal? alist (car list-of-alists)) |
398 | (remove-alist-from-list alist (cdr list-of-alists)) |
399 | (cons (car list-of-alists) |
400 | (remove-alist-from-list alist (cdr list-of-alists)))))) |
401 |
|
402 | ;; irc util ==================================================================== |
403 | ;; (tcp-read-timeout #f) so (read-line ...) on a TCP connection doesn't timeout after 60 seconds |
404 | (tcp-read-timeout #f) |
405 |
|
406 | (define (send str) |
407 | (write-line (string-append str "\r\n") to-server)) |
408 |
|
409 | (define-syntax send-format |
410 | (syntax-rules () |
411 | ((_ str v ...) |
412 | (send (format str v ...))))) |
413 |
|
414 | (define (send-password) |
415 | (send-format "PASS ~a" (config-get 'irc-password))) |
416 |
|
417 | (define (send-nick) |
418 | (send-format "NICK ~a" (config-get 'irc-nickname))) |
419 |
|
420 | (define (send-user) |
421 | (send-format "USER ~a 0.0.0.0 ~a :~a" |
422 | (config-get 'irc-nickname) |
423 | (config-get 'irc-username) |
424 | (config-get 'irc-realname))) |
425 |
|
426 | (define (send-message channel message) |
427 | (send-format "PRIVMSG ~a :~a" channel message)) |
428 |
|
429 | (define (check-for-ping server-data) |
430 | (when (equal? "PING" (substring server-data 0 4)) |
431 | (send "PONG :lol"))) |
432 |
|
433 | (define (disable-history) |
434 | (send-format "PRIVMSG nickserv :SET autoreplay-lines 0")) |
435 |
|
436 | (define (join-channel channel) |
437 | (send-format "JOIN ~a" channel)) |
438 |
|
439 | (define (join-channels) |
440 | (for-each join-channel (config-get 'irc-channels))) |
441 |
|
442 | (define (get-sender mode split-server-data) |
443 | (let* ((sender-data (list-ref split-server-data 0)) ;; => ":nickname!~realname@user/realname" or ":nickname!realname@localhost" |
444 | (sender-data-split (string-split sender-data ":!~@"))) ;; => ("nickname" "realname" "user/realname") |
445 | (case mode |
446 | ('realname (cadr sender-data-split)) |
447 | ('nickname (car sender-data-split)) |
448 | (else "unknown")))) |
449 |
|
450 | (define (get-channel split-server-data) |
451 | (list-ref split-server-data 2)) |
452 |
|
453 | ;; returns a message in the form of '("this" "is" "a" "test") |
454 | (define (get-message split-server-data) |
455 | (let ((raw-message-split (list-tail split-server-data 3))) ;; => '(":this" "is" "a" "test") |
456 | (cons (substring (car raw-message-split) 1) ;; get the first item, and remove the colon |
457 | (cdr raw-message-split)))) |
458 |
|
459 | ;; generation utils ======================================================= |
460 | (define (generate-feed-rss list-of-item-alists) |
461 | (let ((rss-file-path (make-pathname (config-get 'build-path) rss-file))) |
462 | ;; create rss header and overwrite file |
463 | (file-write rss-file-path |
464 | (string-populate-with-default-values rss-header-value)) |
465 |
|
466 | ;; create rss items and append them to file |
467 | (for-each |
468 | (lambda (item-alist) |
469 | (file-append |
470 | rss-file-path |
471 | (string-populate (string-populate-with-default-values rss-item-value) |
472 | `((item-title . ,(clean-input-html->rss (alist-ref 'title item-alist))) |
473 | (item-url . ,(alist-ref 'url item-alist)) |
474 | (item-guid . ,(alist-ref 'guid item-alist)) |
475 | (item-protocol . ,(alist-ref 'protocol item-alist)) |
476 | (item-sender . ,(alist-ref 'sender item-alist)) |
477 | (item-date-rss . ,(alist-ref 'date-rss item-alist)) |
478 | (item-date-human . ,(alist-ref 'date-human item-alist)))))) |
479 | list-of-item-alists) |
480 | ;; append rss footer to file |
481 | (file-append rss-file-path rss-footer-value))) |
482 |
|
483 | ;; generate html utils ========================================== |
484 | (define (generate-feed-html list-of-post-alists) |
485 | (let ((html-file-path (make-pathname (config-get 'build-path) html-file))) |
486 | ;; create html header |
487 | (file-write html-file-path |
488 | (string-populate-with-default-values html-header-value)) |
489 |
|
490 | ;; append html posts |
491 | (for-each |
492 | (lambda (post-alist) |
493 | (file-append |
494 | html-file-path |
495 | (string-populate html-post-value |
496 | `((post-url . ,(alist-ref 'url post-alist)) |
497 | (post-title . ,(alist-ref 'title post-alist)) |
498 | (post-protocol . ,(alist-ref 'protocol post-alist)) |
499 | (post-sender . ,(alist-ref 'sender post-alist)) |
500 | (post-date-human . ,(alist-ref 'date-human post-alist)) |
501 | (post-guid . ,(alist-ref 'guid post-alist)))))) |
502 | list-of-post-alists) |
503 | ;; append html footer |
504 | (file-append html-file-path |
505 | html-footer-value))) |
506 |
|
507 | ;; commands =================================================================== |
508 | (define (post-delete channel realname id-str) |
509 | (let* ((log-file-sexp (log-file->sexp)) |
510 | (post-to-remove (id-exists? id-str log-file-sexp))) |
511 | (if post-to-remove |
512 | (let* ((posts-list (remove-alist-from-list post-to-remove log-file-sexp)) |
513 | (posts-list-reversed (sort-by-date 'reverse posts-list))) |
514 | (pretty-print-to-file log-file posts-list) |
515 | (generate-feed-html posts-list-reversed) |
516 | (generate-feed-rss posts-list-reversed) |
517 | (send-message channel "post deleted!")) |
518 | (send-message channel "oh no, that ID doesn't exist!")))) |
519 |
|
520 | ;; message split comes in the form of: "im-a-url im a title" |
521 | (define (post-add channel realname message-split) |
522 | (if (>= (length message-split) 2) ;; at least '(a b) or more |
523 | (let* ((log-file-contents (log-file->sexp)) |
524 | (date (date-current)) ;; => "2022-02-14 21:17:26" |
525 | (date-numbers-only (string-translate date "- :")) ;; => "20220214211726" (punctuation-less format for sorting by number) |
526 | (url (fix-url (clean-input-for-html (car message-split)))) |
527 | (contents-after-url (string-intersperse (cdr message-split))) |
528 | (post-alist `(((date . ,(string->number date-numbers-only)) |
529 | (date-rss . ,(date->rss-date date)) ;; => "Mon, 14 Feb 2022 21:17:26 -0500" (this can't convert the punctuation-less format to the RSS format unfortunately, so we use the punctuation format) |
530 | (date-human . ,(date->human-date date)) ;; => "February 14, 2022" |
531 | (sender . ,realname) |
532 | (url . ,url) |
533 | (title . ,(clean-input-for-html contents-after-url)) |
534 | (guid . ,(string-append realname date-numbers-only)) ;; => "nick20220214211726" |
535 | (protocol . ,(get-protocol url))))) ;; => whatever comes before the first "://". defaults to "http" if no protocol is provided |
536 | (posts-list (sort-by-date 'normal (append log-file-contents post-alist))) |
537 | (posts-list-reversed (sort-by-date 'reverse posts-list))) |
538 | ;; write to log file |
539 | (pretty-print-to-file log-file posts-list) |
540 | ;; generate html and rss |
541 | (generate-feed-html posts-list-reversed) |
542 | (generate-feed-rss posts-list-reversed) |
543 | ;; i don't use the URL here because it pings me on irc lol |
544 | (send-message channel (config-get 'post-response))) |
545 | (begin |
546 | (send-message channel "woops. you need to provide a link and a title, friend :O.") |
547 | (send-message channel "kind of like this: !post https://example.com your very cool title here")))) |
548 |
|
549 | (define (reload channel) |
550 | (load-config!) |
551 | (load-templates!) |
552 | (send-message channel "config.scm and template files reloaded!")) |
553 |
|
554 | (define (regenerate channel) |
555 | (let ((posts-list-reversed (sort-by-date 'reverse (log-file->sexp)))) |
556 | (if (null? posts-list-reversed) |
557 | (begin |
558 | (send-message channel "woops! it looks like there aren't any posts") |
559 | (send-message channel "try adding one with !post <link> <title>")) |
560 | (begin |
561 | (generate-feed-html posts-list-reversed) |
562 | (generate-feed-rss posts-list-reversed) |
563 | (send-message channel "feeds regenerated!"))))) |
564 |
|
565 | (define (help channel) |
566 | (send-message channel "i respond to the following commands:") |
567 | (send-message channel " !post <link> <title>: submit a link to the feed") |
568 | (send-message channel " !delete <id of post>: delete a link from the feed") |
569 | (send-message channel " !regenerate: regenerate the html and rss feeds") |
570 | (send-message channel " !reload: reload the config.scm and template files") |
571 | (send-message channel " !linkbud: display this help message")) |
572 |
|
573 | ;; irc loop =================================================================== |
574 | ;; how split-server-data looks: |
575 | ;; 0 1 2 3 |
576 | ;; '(":some-nickname!some-realname@localhost" "PRIVMSG" "#tildetown" ":this" "is" "a" "test") |
577 | ;; or sometimes with a tilde: |
578 | ;; '(":some-nickname!~some-realname@user/some-realname" "PRIVMSG" "#tildetown" ":this" "is" "a" "test") |
579 | ;; |
580 | ;; and note to self, this is how private messages look: |
581 | ;; :some-nickname!~some-realname@user/some-realname PRIVMSG linkbud :oh this is a test |
582 | ;; , because i'll need to reference this later |
583 | (define (check-for-command server-data) |
584 | (let ((split-server-data (string-split server-data))) |
585 | (when (equal? (list-ref split-server-data 1) "PRIVMSG") |
586 | (let* ((nickname (get-sender 'nickname split-server-data)) |
587 | ;; ergo.chat, if names are cloaked, uses "nickname!~u@ ...", so |
588 | ;; this doesn't really work here, which is why i commented it out |
589 | ;; for now |
590 | ; (realname (get-sender 'realname split-server-data)) |
591 | (realname (get-sender 'nickname split-server-data)) |
592 | (chan (get-channel split-server-data)) |
593 | (channel (if (equal? chan (config-get 'irc-nickname)) |
594 | nickname |
595 | chan)) |
596 | (message-split (get-message split-server-data))) |
597 | (when (not (null? message-split)) |
598 | (case (string->symbol (car message-split)) |
599 | ((!post !link) (post-add channel realname (cdr message-split))) |
600 | ('!delete (post-delete channel realname (cadr message-split))) |
601 | ('!regenerate (regenerate channel)) |
602 | ('!reload (reload channel)) |
603 | ('!linkbud (help channel)) |
604 | (else 'do-nothing))))))) |
605 |
|
606 | (define (start-irc-connection) |
607 | (when (config-get 'irc-password) |
608 | (send-password)) |
609 | (send-nick) |
610 | (send-user) |
611 | (when (config-get 'irc-disable-history?) (disable-history)) |
612 | (join-channels) |
613 | (sleep 1)) |
614 |
|
615 | (define (listen-to-irc-server) |
616 | (let ((server-data (read-line from-server))) |
617 | (print server-data) |
618 | (check-for-ping server-data) |
619 | (check-for-command server-data) |
620 | (sleep 1)) |
621 | (listen-to-irc-server)) |
622 |
|
623 | (define (create-init-file file contents) |
624 | (if (file-exists? file) |
625 | (print (format "~a already exists. skipping file creation" file)) |
626 | (let ((directory (pathname-directory file))) |
627 | ;; #t creates parent directories if they exist |
628 | (when directory (create-directory directory #t)) |
629 | (file-write file contents) |
630 | (print (format "created ~a" file))))) |
631 |
|
632 | (define (init) |
633 | (when (not (file-exists? config-file)) |
634 | (file-write config-file config-contents)) |
635 |
|
636 | ;; load config file contents into runtime because the build path needs |
637 | ;; to be retrieved from the generated config file |
638 | (load-config!) |
639 |
|
640 | (create-init-file html-header-path html-header-contents) |
641 | (create-init-file html-post-path html-post-contents) |
642 | (create-init-file html-footer-path html-footer-contents) |
643 | (create-init-file rss-header-path rss-header-contents) |
644 | (create-init-file rss-item-path rss-item-contents) |
645 | (create-init-file rss-footer-path rss-footer-contents) |
646 |
|
647 | (create-directory (config-get 'build-path))) |
648 |
|
649 | (define (run) |
650 | (if (file-exists? config-file) |
651 | (begin (load-config!) |
652 | (load-templates!) |
653 | (set!-values (from-server to-server) |
654 | (let ((irc-host (config-get 'irc-host)) |
655 | (irc-port (config-get 'irc-port))) |
656 | (if (config-get 'irc-ssl) |
657 | (ssl-connect* #:hostname irc-host |
658 | #:port irc-port) |
659 | (tcp-connect irc-host irc-port)))) |
660 | (start-irc-connection) |
661 | (listen-to-irc-server)) |
662 | (print "linkbud hasn't been initialized yet. Try running ./linkbud init."))) |
663 |
|
664 | (define help-message |
665 | #<<string-block |
666 | help - display this help message |
667 | init - generate default files |
668 | run - connect linkbud to an irc server and listen for commands |
669 | string-block |
670 | ) |
671 |
|
672 | (define (main args) |
673 | (if (null? args) |
674 | (print help-message) |
675 | (case (string->symbol (car args)) |
676 | ((--help -help help -h) |
677 | (print help-message)) |
678 | ('init (init)) |
679 | ('run (run)) |
680 | (else (print help-message))))) |
681 |
|
682 | (main (command-line-arguments)) |