ircd.rkt 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. #lang typed/racket
  2. ;; single room IRC server
  3. (require "private/irc-functions.rkt"
  4. (only-in "private/ws-typed.rkt" WS [ws-close! ws:close-conn] [ws-conn-closed? ws:conn-closed?])
  5. (prefix-in ws: "api.rkt")
  6. (prefix-in movie-night: "chat.rkt"))
  7. (provide (all-defined-out))
  8. ;; global crap
  9. ;; needs to be cleaned out
  10. (define movie-night-ws-url-base (make-parameter "wss://stream.ihatebeinga.live"))
  11. (define (make-ws-url [un : String])
  12. (string-append (movie-night-ws-url-base) "/channels/" un "/ws"))
  13. ;; Main entry point
  14. ;; Returns the main server loop thread (for synchronizing) and
  15. ;; a function for killing the server.
  16. (: serve (->* ()
  17. (#:port Integer #:hostname (U False String))
  18. (Values Thread (-> Void))))
  19. (define (serve #:port [port-no 6667] #:hostname [host #f])
  20. (define serve-cust (make-custodian))
  21. (parameterize ([current-custodian serve-cust])
  22. (define listener (tcp-listen port-no 5 #t host))
  23. (: loop (-> Nothing))
  24. (define (loop)
  25. (accept-and-handle listener)
  26. (loop))
  27. (define t (thread loop))
  28. (values t
  29. (lambda ()
  30. (custodian-shutdown-all serve-cust)))))
  31. ;; Accepting new clients
  32. (: accept-and-handle (-> TCP-Listener Thread))
  33. (define (accept-and-handle listener)
  34. (define cust (make-custodian))
  35. (parameterize ([current-custodian cust])
  36. (define-values (in out) (tcp-accept listener))
  37. ;; once the ports are bound we spawn a new thread
  38. ;; in oreder to allow the main server loop to handler
  39. ;; other connections
  40. (thread
  41. (lambda ()
  42. (define user-conn (accept-irc-connection in out cust))
  43. (thread (lambda () (handle-user-messages user-conn)))))))
  44. ;; When we accept a new IRC connection we need to do several things:
  45. ;; 1. Receive the user nick/user information
  46. ;; 2. Prepare the IO ports
  47. ;; 3. Create a new WebSocket connection through chat.rkt
  48. ;; 4. Set up a ping thread
  49. ;; 5. Send the MOTD to the user
  50. (: accept-irc-connection (-> Input-Port Output-Port Custodian irc-connection))
  51. (define (accept-irc-connection in out cust)
  52. (define nick
  53. (let #{loop : (-> String)} ()
  54. (match (read-from-input-port in)
  55. [(irc-message _ "NICK" params)
  56. (car params)]
  57. [_ (loop)])))
  58. (log-info (format "~a connected" nick))
  59. (define user
  60. (let #{loop : (-> String)} ()
  61. (match (read-from-input-port in)
  62. [(irc-message _ "USER" params)
  63. (car params)]
  64. [_ (loop)])))
  65. (file-stream-buffer-mode out 'line)
  66. (: conn irc-connection)
  67. (define conn (irc-connection in out nick user cust '()))
  68. (welcome-user conn)
  69. (void (thread (lambda () (ping-pong-thread conn))))
  70. conn)
  71. (: ping-pong-thread (-> irc-connection Void))
  72. (define (ping-pong-thread conn)
  73. (sleep 120)
  74. (send-to-client conn (irc-message
  75. ":lolcathost"
  76. "PING"
  77. '("fffffffffffffffffffffff")))
  78. (ping-pong-thread conn))
  79. ;; Callbacks for the MoveNight chat api
  80. (: notify-users (-> irc-connection String (Listof String) Void))
  81. (define (notify-users conn channel users)
  82. (send-to-client conn (irc-message
  83. ":lolcathost"
  84. RPL_NAMEREPLY
  85. (list (irc-connection-nick conn) "@" channel
  86. (format ":~a" (string-join users)))))
  87. (send-to-client conn (irc-message
  88. ":lolcathost"
  89. RPL_ENDOFNAMES
  90. (list (irc-connection-nick conn) channel ":End of /NAMES list."))))
  91. (: on-chat (-> irc-connection String String String Void))
  92. (define (on-chat conn channel from message)
  93. (unless (equal? from (irc-connection-nick conn))
  94. (send-to-client
  95. conn
  96. (irc-message (format "~a!~a@lolcathost" from from)
  97. "PRIVMSG"
  98. (list channel (format ":~a" message))))))
  99. (: on-response (-> irc-connection String String Void))
  100. (define (on-response conn channel message)
  101. (send-to-client
  102. conn
  103. (irc-message "OwO!SERVER@lolcathost"
  104. "NOTICE"
  105. (list channel
  106. (format ":!!! [ ~a ]" message)))))
  107. (: on-nick-collision (-> irc-connection String Void))
  108. (define (on-nick-collision conn channel)
  109. (part-and-remove-channel! conn channel)
  110. (send-to-client
  111. conn
  112. (irc-message "OwO!SERVER@lolcathost"
  113. "NOTICE"
  114. (list (irc-connection-nick conn)
  115. (format ":Sowwy, cannot join ~a because youw nickname is awweady in use :(" channel)))))
  116. (: on-topic (-> irc-connection String String Void))
  117. (define (on-topic conn channel topic)
  118. (send-to-client conn (irc-message
  119. ":lolcathost"
  120. RPL_TOPIC
  121. (list (irc-connection-nick conn)
  122. channel
  123. (string-append ":" topic)))))
  124. (: on-join (-> irc-connection String String Void))
  125. (define (on-join conn channel nick)
  126. (unless (equal? nick (irc-connection-nick conn))
  127. (send-to-client
  128. conn
  129. (irc-message (format "~a!~a@lolcathost" nick nick)
  130. "JOIN"
  131. (list channel)))))
  132. (: on-leave (-> irc-connection String String Void))
  133. (define (on-leave conn channel nick)
  134. (unless (equal? nick (irc-connection-nick conn))
  135. (send-to-client
  136. conn
  137. (irc-message (format "~a!~a@lolcathost" nick nick)
  138. "PART"
  139. (list channel)))))
  140. (: on-name-change (-> irc-connection String String Void))
  141. (define (on-name-change conn old-nick new-nick)
  142. (unless (equal? old-nick (irc-connection-nick conn))
  143. (send-to-client
  144. conn
  145. (irc-message (format "~a!~a@lolcathost" old-nick old-nick)
  146. "NICK"
  147. (list new-nick)))))
  148. ;; The loop for handling commands from the client
  149. ;; return type is Nothing ==> the function does not terminate
  150. ;; NB: we read from the client with the timeout
  151. ;; of 333 > the frequency of PINGs
  152. ;; so the client should respond to the PING within
  153. ;; some number of seconds in order to keep the connection alive
  154. ;; XXX this is a galaxy brain version of making sure that the connection stays alive
  155. ;; ideally this should be rewritten
  156. (: handle-user-messages (-> irc-connection Nothing))
  157. (define (handle-user-messages conn)
  158. (define msg (read-from-client conn #:timeout 333))
  159. (define nick (irc-connection-nick conn))
  160. (define custodian (irc-connection-custodian conn))
  161. (match msg
  162. [(irc-message _ "PING" (list ping))
  163. (send-to-client conn
  164. (irc-message "lolcathost"
  165. "PONG"
  166. (list "lolcathost"
  167. (string-append ":" ping))))]
  168. [(irc-message _ "PONG" (list pong))
  169. (void)]
  170. [(irc-message _ "NICK" params)
  171. ;; TODO: propagate this info along the WS
  172. (set-irc-connection-nick! conn (car params))]
  173. [(irc-message _ "JOIN" (list chan))
  174. #:when (not (channel-joined? chan (irc-connection-channels conn)))
  175. (join-new-channel! conn chan)]
  176. [(irc-message _ "PART" (list chan))
  177. (part-and-remove-channel! conn chan)]
  178. [(irc-message _ "MODE" (cons chan _))
  179. #:when (channel-joined? chan (irc-connection-channels conn))
  180. (send-to-client conn (irc-message
  181. ":lolcathost"
  182. RPL_CHANNELMODEIS
  183. (list nick chan "+OwO")))]
  184. [(irc-message _ "LIST" _)
  185. (send-to-client conn (irc-message
  186. ":lolcathost"
  187. "002"
  188. (list nick " /list not implemented ")))
  189. ]
  190. [(irc-message _ "WHO" (list chan))
  191. #:when (channel-joined? chan (irc-connection-channels conn))
  192. (send-to-client conn (irc-message
  193. ":lolcathost"
  194. RPL_WHOREPLY
  195. (list nick chan
  196. (irc-connection-user conn)
  197. "lolcathost"
  198. "lolcathost"
  199. nick
  200. "H"
  201. ":0")))
  202. (send-to-client conn (irc-message
  203. ":lolcathost"
  204. RPL_ENDOFWHO
  205. (list nick chan ":End of /WHO list.")))]
  206. [(irc-message _ "WHOIS" (list target))
  207. (send-to-client conn (irc-message
  208. ":lolcathost"
  209. RPL_WHOISUSER
  210. (list nick target "neko" "lolcathost" "*" ":This user is a cat")))
  211. (send-to-client conn (irc-message
  212. ":lolcathost"
  213. RPL_WHOISSERVER
  214. (list nick target "lolcathost" ":🐈")))
  215. (send-to-client conn (irc-message
  216. ":lolcathost"
  217. RPL_ENDOFWHOIS
  218. (list nick target ":End of /WHOIS list")))]
  219. [(irc-message _ "PRIVMSG" (list chan msg))
  220. (define ws (lookup-ws-conn (irc-connection-channels conn) chan))
  221. (when ws
  222. (ws:send-message ws msg))]
  223. ;; TODO: imlement STATS again
  224. ;; [(irc-message _ "STATS" '())
  225. ;; (send-ws-message conn "/STATS")]
  226. [(irc-message _ "MOTD" _)
  227. (send-to-client conn (irc-message
  228. ":lolcathost"
  229. RPL_MOTDSTART
  230. (list nick ":- lolcathost Message of the day - ")))
  231. (for ([x cofe])
  232. (send-to-client conn (irc-message
  233. ":lolcathost"
  234. RPL_MOTD
  235. (list nick (string-append ":" x)))))
  236. (send-to-client conn (irc-message
  237. ":lolcathost"
  238. RPL_ENDOFMOTD
  239. (list nick ":End of /MOTD command.")))]
  240. [(or (? eof-object?)
  241. (irc-message _ "QUIT" _))
  242. ;; somehow attach this to a custodian?
  243. (log-warning (format "ircd.rkt: Closing socket for ~a" (irc-connection-nick conn)))
  244. (custodian-shutdown-all custodian)]
  245. [#f ;;; were unable to parse the string correctly, just ignore it
  246. (void)]
  247. [(var msg)
  248. (log-warning (format "ircd.rkt/handle-user-message: unknown message: ~a" msg))])
  249. (log-warning (format "XXX: channels: ~a" (irc-connection-channels conn)))
  250. (handle-user-messages conn))
  251. (: join-new-channel! (-> irc-connection String Void))
  252. (define (join-new-channel! conn channel-name)
  253. (define wsurl (make-ws-url (string-trim channel-name "#" #:right? #f)))
  254. (: ws-c WS)
  255. (define ws-c
  256. (movie-night:make-connection
  257. wsurl
  258. (irc-connection-nick conn)
  259. ;; XXX: it's a good idea NOT to do anything with the websockets in any of those callbacks
  260. ;; because they are executed in a different thread than the main message loop, which already
  261. ;; communicates with websockets
  262. #:on-join (lambda ([n : String]) (on-join conn channel-name n))
  263. #:on-leave (lambda ([n : String]) (on-leave conn channel-name n))
  264. #:on-name-change (lambda ([n1 : String] [n2 : String])
  265. (on-name-change conn n1 n2))
  266. #:on-users (lambda ([l : (Listof String)])
  267. (notify-users conn channel-name l))
  268. #:on-chat (lambda ([from : String] [msg : String])
  269. (on-chat conn channel-name from msg))
  270. #:on-response (lambda ([msg : String]) (on-response conn channel-name msg))
  271. #:on-notify (lambda ([msg : String]) (on-response conn channel-name msg))
  272. #:on-topic (lambda ([topic : String]) (on-topic conn channel-name topic))
  273. #:on-nick-collision (lambda () (on-nick-collision conn channel-name))
  274. #:on-close-conn (lambda ()
  275. ;; ok here we do fuck with the ws
  276. (part-and-remove-channel! conn channel-name))))
  277. ;; XXX: it is very important that we update the irc-connection-channels
  278. ;; before we send the join message via a websocket.
  279. ;; otherwise the websocket response might trigger on-nick-collision /before/ we updated
  280. ;; the list of joined channels
  281. (set-irc-connection-channels!
  282. conn
  283. (add-channel channel-name ws-c (irc-connection-channels conn)))
  284. (ws:send-join ws-c (irc-connection-nick conn) "#00FFAA")
  285. (send-to-client conn (irc-message
  286. (format "~a!~a@lolcathost"
  287. (irc-connection-nick conn)
  288. (irc-connection-user conn))
  289. "JOIN"
  290. (list channel-name)))
  291. (send-to-client conn (irc-message
  292. ":lolcathost"
  293. RPL_TOPIC
  294. (list (irc-connection-nick conn) channel-name ":chatting hard")))
  295. (sleep 1.5) ;; is there a way around going to sleep? :-<
  296. ;; have to do the check here in case part-and-remove-channel! was called
  297. ;; during the sleep or at any other point
  298. ;; The WS connections are monotone in a sense that if it becomes closed it's not going
  299. ;; to become open again
  300. (unless (ws:conn-closed? ws-c) (ws:send-users ws-c)))
  301. (: part-and-remove-channel! (-> irc-connection String Void))
  302. (define (part-and-remove-channel! conn chan)
  303. (define channels (irc-connection-channels conn))
  304. (define nick (irc-connection-nick conn))
  305. (define ws (lookup-ws-conn channels chan))
  306. (send-to-client conn
  307. (irc-message (format "~a!~a@lolcathost" nick nick)
  308. "PART"
  309. (list chan)))
  310. (set-irc-connection-channels! conn (remove-channel chan channels))
  311. (when ws (ws:close-conn ws)))
  312. ;; Utils
  313. (: welcome-user (-> irc-connection Void))
  314. (define (welcome-user conn)
  315. (define nick (irc-connection-nick conn))
  316. (: notify-nick (-> String Void))
  317. (define (notify-nick msg)
  318. (send-to-client conn
  319. (irc-message "lolcathost" "002" (list nick msg))))
  320. ;; "001" has to be a string, otherwise it's converted to 1
  321. (send-to-client conn (irc-message "lolcathost" "001" (list nick "[OwO]")))
  322. (notify-nick ":-----------------------------------------------------------------------------")
  323. (notify-nick ":If you encounter an error, try reconnecting!")
  324. (notify-nick ":This IRCd does not support many features, like user to user messages or channel lists.")
  325. (notify-nick ":A lot of things are broken, please submit an issues to ")
  326. (notify-nick ": --> <https://notabug.org/epi/movie-night-chat> <--")
  327. (notify-nick ":This pwogwam comes with ABSOWUTEWY NyO WAWWANTY!11oneone")
  328. (notify-nick ":This is fwee softwawe, and you awe wewcome to wedistwibute it")
  329. (notify-nick ":undew cewtain conditions; see the LICENSE file for details UwU :3")
  330. (notify-nick ":-----------------------------------------------------------------------------")
  331. (for ([x cofe])
  332. (notify-nick (string-append ":" x)))
  333. (notify-nick ":Please join #<CHANNEL>, so for epi join #epi")
  334. (notify-nick ":Hope it works")
  335. (notify-nick ":[Apr 2020] /STATS is broken, but there are now multiple chatrooms!"))
  336. (define cofe
  337. '(" ,. ,."
  338. " || ||"
  339. " ,''--''. ON THIS SERVER"
  340. " : (.)(.) : WE DRINK COFE"
  341. " ,' `. "
  342. " : : "
  343. " : : hash tag IHBA gang"
  344. " -ctr- `._m____m_,' "))