├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── admin.go ├── chat.go ├── data ├── adjectives.go └── animals.go ├── go.mod ├── go.sum ├── html ├── about.html ├── admin.html ├── index.css ├── index.html ├── privacy_policy.html └── simple.css ├── main.go ├── message.go ├── neartalk.example.service ├── nicknames.go └── version.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: makew0rld 2 | ko_fi: makeworld 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binary 2 | neartalk 3 | # My personal SystemD service file, that contains the admin key 4 | neartalk.service 5 | # My personal notes 6 | NOTES.md 7 | 8 | # Created by https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,linux 9 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,visualstudiocode,linux 10 | 11 | ### Go ### 12 | # Binaries for programs and plugins 13 | *.exe 14 | *.exe~ 15 | *.dll 16 | *.so 17 | *.dylib 18 | 19 | # Test binary, built with `go test -c` 20 | *.test 21 | 22 | # Output of the go coverage tool, specifically when used with LiteIDE 23 | *.out 24 | 25 | # Dependency directories (remove the comment below to include it) 26 | # vendor/ 27 | 28 | ### Go Patch ### 29 | /vendor/ 30 | /Godeps/ 31 | 32 | ### Linux ### 33 | *~ 34 | 35 | # temporary files which can be created if a process still has a handle open of a deleted file 36 | .fuse_hidden* 37 | 38 | # KDE directory preferences 39 | .directory 40 | 41 | # Linux trash folder which might appear on any partition or disk 42 | .Trash-* 43 | 44 | # .nfs files are created when an open file is removed but is still being accessed 45 | .nfs* 46 | 47 | ### VisualStudioCode ### 48 | .vscode/* 49 | !.vscode/settings.json 50 | !.vscode/tasks.json 51 | !.vscode/launch.json 52 | !.vscode/extensions.json 53 | *.code-workspace 54 | 55 | # Local History for Visual Studio Code 56 | .history/ 57 | 58 | ### VisualStudioCode Patch ### 59 | # Ignore all local history of files 60 | .history 61 | .ionide 62 | 63 | # End of https://www.toptal.com/developers/gitignore/api/go,visualstudiocode,linux 64 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GITV != git describe --tags 2 | GITC != git rev-parse --verify HEAD 3 | SRC != find . -type f -name '*.go' ! -name '*_test.go' 4 | TEST != find . -type f -name '*_test.go' 5 | 6 | PREFIX ?= /usr/local 7 | VERSION ?= $(GITV) 8 | COMMIT ?= $(GITC) 9 | BUILDER ?= Makefile 10 | 11 | GO := go 12 | INSTALL := install 13 | RM := rm 14 | 15 | neartalk: go.mod go.sum $(SRC) 16 | GO111MODULE=on CGO_ENABLED=0 $(GO) build -o $@ -ldflags="-s -w -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.builtBy=$(BUILDER)" 17 | 18 | .PHONY: clean 19 | clean: 20 | $(RM) -f neartalk 21 | 22 | .PHONY: install 23 | install: neartalk 24 | install -d $(PREFIX)/bin/ 25 | install -m 755 neartalk $(PREFIX)/bin/neartalk 26 | 27 | .PHONY: uninstall 28 | uninstall: 29 | $(RM) -f $(PREFIX)/bin/neartalk 30 | 31 | # Development helpers 32 | .PHONY: fmt 33 | fmt: 34 | go fmt ./... 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NearTalk 2 | 3 | Visit [neartalk.makeworld.space](https://neartalk.makeworld.space) to check it out! That server always runs the latest code on the `main` branch. 4 | 5 | 6 | 7 |

What is it?

8 |

9 | NearTalk is chat platform to talk to people nearby. 10 |

11 |

12 | Anyone with the same IP address is in the same chat room. For example, everyone 13 | in your house will get the same chat room if they visit NearTalk. If you go to 14 | your local coffee shop, everyone who visit NearTalk will be in the same chat room. 15 | This extends to larger organizations like college/university campuses. 16 |

17 |

18 | Depending on how the network is set up, all mobile devices using data with the same 19 | network provider as you may be chatting together. Or similarly, all the other homes 20 | using the same ISP. This is the minority of cases however. 21 |

22 |

Why is it?

23 |

24 | For fun, mostly. I wanted to make a chat application and I wanted to use 25 | htmx, and this seemed like a fun idea. 26 |

27 |

28 | There are many reasons why NearTalk isn't useful, and talking to your fellow humans 29 | face to face 30 | is much better. However there are some times when having a local chatroom is useful, 31 | like for discussing (or dragging) a presentation going on. At the end of the day, 32 | I'm happy to have made something. 33 |

34 | 35 | 36 | 37 | ## Building 38 | 39 | GNU Make is required to use the Makefile. Compiling with `make` automatically embeds version information into the binary from Git, and it's the only supported way to build the project. 40 | 41 | Only the latest Go (1.17) is tested, but Go 1.16+ should compile. 42 | 43 | ## Deploying 44 | 45 | You can look at the [neartalk.example.service](./neartalk.example.service) file in the repo as an example for running NearTalk under systemd. 46 | 47 | Currently the code does not handle TLS certificates, and so a reverse-proxy is required to use TLS and ensure user security. Make sure you set up your reverse-proxy so that websockets work as well. Just look up ` reverse proxy websocket` to find a configuration. 48 | 49 | Currently the code is also designed to work under a domain or subdomain, not a subpath. 50 | 51 | Please let me know why you deploy your own instance if you do! 52 | 53 | ## License 54 | 55 | NearTalk is licensed under the [AGPLv3](https://www.gnu.org/licenses/agpl-3.0.en.html). If host your own version, you must release your source code. 56 | -------------------------------------------------------------------------------- /admin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This file has functions that handle the admin interface over HTTP. 4 | 5 | import ( 6 | "bufio" 7 | "fmt" 8 | "io" 9 | "log" 10 | "net/http" 11 | "net/url" 12 | "os" 13 | "strings" 14 | "time" 15 | 16 | "github.com/dustin/go-humanize" 17 | ) 18 | 19 | // adminHandler serves the admin.html file, but checks if the provided key is correct first. 20 | func (cs *chatServer) adminHandler(w http.ResponseWriter, r *http.Request) { 21 | query, err := url.QueryUnescape(r.URL.RawQuery) 22 | if err != nil { 23 | // Shouldn't happnen unless there's a browser bug I guess 24 | query = r.URL.RawQuery 25 | } 26 | if query != adminKey { 27 | w.WriteHeader(http.StatusForbidden) 28 | return 29 | } 30 | adminHtml, err := os.Open("html/admin.html") 31 | if err != nil { 32 | log.Printf("chatServer.adminHandler: err opening admin.html: %v", err) 33 | w.WriteHeader(http.StatusInternalServerError) 34 | return 35 | } 36 | defer adminHtml.Close() 37 | io.Copy(w, adminHtml) 38 | } 39 | 40 | func (cs *chatServer) adminDataHandler(rw http.ResponseWriter, r *http.Request) { 41 | if !strings.HasSuffix(r.Header.Get("HX-Current-URL"), "?"+url.QueryEscape(adminKey)) { 42 | // Admin data wasn't requested from inside the admin page 43 | rw.WriteHeader(http.StatusForbidden) 44 | return 45 | } 46 | 47 | cs.roomsMu.Lock() 48 | defer cs.roomsMu.Unlock() 49 | 50 | // Write HTML data as it's processed, but with a buffer 51 | w := bufio.NewWriter(rw) 52 | fmt.Fprintf(w, `

%d chat rooms


`, len(cs.rooms)) 53 | for ip, room := range cs.rooms { 54 | fmt.Fprintf( 55 | w, `

%s

%d chatters

Last message: %s

`, 56 | ip, room.numClients(), humanize.RelTime(room.whenLastMsg, time.Now(), "ago", "from now"), 57 | ) 58 | } 59 | w.Flush() 60 | } 61 | -------------------------------------------------------------------------------- /chat.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This file handles the majority of the application. 4 | // Chat messages, HTTP server, websocket handling, etc. 5 | // 6 | // It was adapted from some chat example code by nhooyr: 7 | // https://github.com/nhooyr/websocket/tree/b6adc4bc5c001d513d1604ec9efd97e73e1d082a/examples/chat 8 | // That code is under the MIT license, so there are no licensing issues with it 9 | // being adapted for here. Note it was adapted, and not directly copied. The 10 | // example was used as a starting point. 11 | 12 | import ( 13 | "context" 14 | "errors" 15 | "fmt" 16 | "log" 17 | "net" 18 | "net/http" 19 | "sort" 20 | "strings" 21 | "sync" 22 | "time" 23 | 24 | "golang.org/x/time/rate" 25 | "nhooyr.io/websocket" 26 | "nhooyr.io/websocket/wsjson" 27 | ) 28 | 29 | // clientMsgBuffer controls the max number 30 | // of messages that can be queued for a client 31 | // before it is kicked. 32 | const clientMsgBuffer = 16 33 | 34 | // The max number of unprocessed messages from clients the server can have 35 | // before clients are prevented from sending more messages. 36 | const serverMsgBuffer = 20 37 | 38 | // msg is used to pass messages from users around in the server code. 39 | type msg struct { 40 | // nick is the nickname of the user at the time the message was sent. 41 | // An empty nickname indicates this is a message from the server. 42 | nick string 43 | // text is the message text. It is stored unsanitized. 44 | text string 45 | // author points to the client that sent the message. A nil author 46 | // indicates this message is from the server 47 | author *client 48 | // when is when the message was sent. 49 | when time.Time 50 | // raw is a string that indicates that is message was pre-rendered to HTML, 51 | // and doesn't need any processing. 52 | // TODO: this is a hack to allow the server to queue messages custom 53 | // messages, like join/leave 54 | raw string 55 | } 56 | 57 | type chatRoom struct { 58 | // incoming is where messages sent by clients are temporarily stored. 59 | incoming chan msg 60 | // quit is used to stop the chatRoom goroutine 61 | quit chan struct{} 62 | // limiter rate limits the messages sent to the server for this room. 63 | // This prevents the server from being spammed by messages. 64 | limiter *rate.Limiter 65 | // whenLastMsg is when the most recent message was sent 66 | whenLastMsg time.Time 67 | 68 | clientsMu sync.Mutex 69 | clients map[*client]struct{} // map is used for easy removal 70 | } 71 | 72 | func newChatRoom() *chatRoom { 73 | cr := &chatRoom{ 74 | incoming: make(chan msg, serverMsgBuffer), 75 | quit: make(chan struct{}), 76 | clients: make(map[*client]struct{}), 77 | // TODO: is this a good limiter? 78 | limiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8), 79 | } 80 | go cr.start() 81 | return cr 82 | } 83 | 84 | func (cr *chatRoom) start() { 85 | for { 86 | select { 87 | case <-cr.quit: 88 | return 89 | case m := <-cr.incoming: 90 | cr.limiter.Wait(context.Background()) 91 | 92 | authorMsg, chatMsg := cr.handleMsg(m) 93 | if chatMsg == "" { 94 | // No message needs to be sent to all clients 95 | continue 96 | } 97 | cr.clientsMu.Lock() 98 | for c := range cr.clients { 99 | if m.author == c { 100 | // This client sent the message, so clear their input field 101 | c.sendText(authorMsg + clearInputFieldMsg) 102 | } else { 103 | c.sendText(chatMsg) 104 | } 105 | } 106 | cr.clientsMu.Unlock() 107 | } 108 | } 109 | } 110 | 111 | // addClient adds a client to the chat room. 112 | // It also generates a nickname for them. 113 | // The chatServer addClient method should be used by clients instead. 114 | func (cr *chatRoom) addClient(c *client) { 115 | cr.clientsMu.Lock() 116 | defer cr.clientsMu.Unlock() 117 | 118 | c.nick = cr.getNewNick() 119 | cr.clients[c] = struct{}{} 120 | cr.incoming <- createJoinMsg(c, cr.nicks()) 121 | } 122 | 123 | // removeClient removes a client from the chat room. 124 | // The chatServer removeClient method should be used by clients instead. 125 | func (cr *chatRoom) removeClient(c *client) { 126 | cr.clientsMu.Lock() 127 | defer cr.clientsMu.Unlock() 128 | 129 | delete(cr.clients, c) 130 | if len(cr.clients) > 0 { 131 | // Send leave message to clients left in the room 132 | cr.incoming <- createLeaveMsg(c, cr.nicks()) 133 | } 134 | } 135 | 136 | // numClients returns the number of clients in the room. 137 | // It holds the client mutex. 138 | func (cr *chatRoom) numClients() int { 139 | cr.clientsMu.Lock() 140 | defer cr.clientsMu.Unlock() 141 | return len(cr.clients) 142 | } 143 | 144 | // nickInUse returns a bool that indicates whether the provided nickname is 145 | // already used by another client. It does not lock the clientsMu, callers 146 | // should do that. 147 | func (cr *chatRoom) nickInUse(nick string) bool { 148 | for c := range cr.clients { 149 | if nick == c.nick { 150 | return true 151 | } 152 | } 153 | return false 154 | } 155 | 156 | // getNewNick returns a random nickname that no other client in the room has used. 157 | // It does not lock the clientsMu, callers should do that. 158 | func (cr *chatRoom) getNewNick() string { 159 | // If nick is already used, just increment number until it's not 160 | 161 | ogNick := genNick() 162 | nick := ogNick 163 | i := 2 164 | for cr.nickInUse(nick) { 165 | nick = fmt.Sprintf("%s%d", ogNick, i) 166 | i++ 167 | } 168 | return nick 169 | } 170 | 171 | // nicks returns all the nicknames currently in use in this chat room. 172 | // The nicknames are sorted alphabetically. 173 | // It does not lock the clientsMu, callers should do that. 174 | func (cr *chatRoom) nicks() []string { 175 | nks := make([]string, len(cr.clients)) 176 | 177 | i := 0 178 | for c := range cr.clients { 179 | nks[i] = c.nick 180 | i++ 181 | } 182 | sort.Strings(nks) 183 | return nks 184 | } 185 | 186 | type client struct { 187 | // nick (nickname) is the name that appears beside every chat message they send. 188 | // It is stored sanitized. 189 | nick string 190 | // outgoing is where messages to be sent to the client are temporarily stored. 191 | // It recieves pre-rendered messages, no processing is needed. 192 | outgoing chan string 193 | // closeSlow is called if the client can't keep up with messages 194 | closeSlow func() 195 | } 196 | 197 | // sendText tries to send the provided string to the client. If the client's 198 | // outgoing channel is full, the client's closeSlow func is called in a goroutine. 199 | func (c *client) sendText(s string) { 200 | select { 201 | case c.outgoing <- s: 202 | default: 203 | go c.closeSlow() 204 | 205 | } 206 | } 207 | 208 | // chatServer manages all the chat rooms. 209 | // There should only be one instance of it for the site. 210 | type chatServer struct { 211 | // rooms maps IP address strings to chat rooms 212 | rooms map[string]*chatRoom 213 | roomsMu sync.Mutex 214 | 215 | serveMux http.ServeMux 216 | } 217 | 218 | func newChatServer() *chatServer { 219 | cs := &chatServer{ 220 | rooms: make(map[string]*chatRoom), 221 | } 222 | cs.serveMux.Handle("/", noCacheHandler(http.StripPrefix("/", http.FileServer(http.Dir("html"))))) 223 | cs.serveMux.HandleFunc("/connect", cs.connectHandler) 224 | cs.serveMux.HandleFunc("/admin", noCache(cs.adminHandler)) 225 | cs.serveMux.HandleFunc("/admin.html", noCache(cs.adminHandler)) 226 | cs.serveMux.HandleFunc("/admin-data", cs.adminDataHandler) 227 | cs.serveMux.HandleFunc("/version", func(w http.ResponseWriter, r *http.Request) { 228 | fmt.Fprint(w, versionInfo) 229 | }) 230 | return cs 231 | } 232 | 233 | // noCache disables caching of responses. 234 | func noCache(next http.HandlerFunc) http.HandlerFunc { 235 | return func(rw http.ResponseWriter, r *http.Request) { 236 | rw.Header().Add("Cache-Control", "no-store, max-age=0") 237 | rw.Header().Add("Pragma", "no-cache") 238 | next(rw, r) 239 | } 240 | } 241 | 242 | // noCacheHandler disables caching of responses for http.Handler. 243 | func noCacheHandler(h http.Handler) http.HandlerFunc { 244 | return func(rw http.ResponseWriter, r *http.Request) { 245 | rw.Header().Add("Cache-Control", "no-store, max-age=0") 246 | rw.Header().Add("Pragma", "no-cache") 247 | h.ServeHTTP(rw, r) 248 | } 249 | } 250 | 251 | // addClient adds a client to the approriate chat room, creating it if needed. 252 | // The room the client is in is returned. It also generates and sets a nickname 253 | // for the client. 254 | func (cs *chatServer) addClient(ip string, c *client) *chatRoom { 255 | cs.roomsMu.Lock() 256 | defer cs.roomsMu.Unlock() 257 | 258 | room, ok := cs.rooms[ip] 259 | if !ok { 260 | // Room didn't previously exist, create it 261 | room = newChatRoom() 262 | cs.rooms[ip] = room 263 | } 264 | 265 | // Nickname generation happens inside the room func 266 | room.addClient(c) 267 | 268 | // Insert room name 269 | c.outgoing <- fmt.Sprintf(`

%s

`, ip) 270 | 271 | return room 272 | } 273 | 274 | // removeClient removes a client from the approriate chat room, removing the 275 | // entire chat room if it's empty. 276 | func (cs *chatServer) removeClient(ip string, c *client) { 277 | cs.roomsMu.Lock() 278 | defer cs.roomsMu.Unlock() 279 | 280 | room, ok := cs.rooms[ip] 281 | if !ok { 282 | // Room doesn't exist, so ignore 283 | log.Printf("chatServer.removeClient: Tried to remove client from non-existent room %s", ip) 284 | return 285 | } 286 | room.removeClient(c) 287 | 288 | if room.numClients() == 0 { 289 | delete(cs.rooms, ip) 290 | room.quit <- struct{}{} 291 | } 292 | } 293 | 294 | func (cs *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 295 | cs.serveMux.ServeHTTP(w, r) 296 | } 297 | 298 | // connectHandler accepts the WebSocket connection and sets up the duplex messaging. 299 | func (cs *chatServer) connectHandler(w http.ResponseWriter, r *http.Request) { 300 | conn, err := websocket.Accept(w, r, nil) 301 | if err != nil { 302 | log.Printf("subscribeHandler: Websocket accept error: %v", err) 303 | return 304 | } 305 | defer conn.Close(websocket.StatusInternalError, "") 306 | 307 | err = cs.connect(r.Context(), getIPString(r), conn) 308 | if errors.Is(err, context.Canceled) { 309 | return 310 | } 311 | if websocket.CloseStatus(err) == websocket.StatusNormalClosure || 312 | websocket.CloseStatus(err) == websocket.StatusGoingAway { 313 | return 314 | } 315 | if err != nil { 316 | log.Printf("chatServer.connectHandler: %v", err) 317 | return 318 | } 319 | } 320 | 321 | // htmxJson decodes a JSON websocket message from the web UI, which uses htmx (htmx.org) 322 | // This is the message sent when the user sends a message. 323 | type htmxJson struct { 324 | Msg string `json:"message"` 325 | Headers map[string]interface{} `json:"HEADERS"` 326 | } 327 | 328 | // connect creates a client and passes messages to and from it. 329 | // If the context is cancelled or an error occurs, it returns and removes the client. 330 | func (cs *chatServer) connect(ctx context.Context, ip string, conn *websocket.Conn) error { 331 | cl := &client{ 332 | outgoing: make(chan string, clientMsgBuffer), 333 | closeSlow: func() { 334 | conn.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") 335 | }, 336 | } 337 | room := cs.addClient(ip, cl) 338 | defer cs.removeClient(ip, cl) 339 | 340 | // Read websocket messages from user into channel 341 | // Cancel context when connection is closed 342 | readCh := make(chan string, serverMsgBuffer) 343 | ctx, cancel := context.WithCancel(ctx) 344 | go func() { 345 | for { 346 | var webMsg htmxJson 347 | err := wsjson.Read(ctx, conn, &webMsg) 348 | if err != nil { 349 | // Treat any error the same as it being closed 350 | cancel() 351 | conn.Close(websocket.StatusPolicyViolation, "unexpected error") 352 | return 353 | } 354 | readCh <- webMsg.Msg 355 | } 356 | }() 357 | 358 | for { 359 | select { 360 | case text := <-cl.outgoing: 361 | // Send message to user 362 | err := writeTimeout(ctx, time.Second*5, conn, text) 363 | if err != nil { 364 | return err 365 | } 366 | case text := <-readCh: 367 | // Send message to chat room 368 | room.incoming <- msg{ 369 | nick: cl.nick, 370 | text: text, 371 | author: cl, 372 | when: time.Now(), 373 | } 374 | case <-ctx.Done(): 375 | return ctx.Err() 376 | } 377 | } 378 | } 379 | 380 | func getIPString(r *http.Request) string { 381 | xForwardedFor := r.Header.Get("X-Forwarded-For") 382 | forwardedIPs := strings.Split(xForwardedFor, ", ") 383 | if len(forwardedIPs) > 0 { 384 | // The server is reverse-proxied. 385 | 386 | // Return final value in the list, to guard against spoofing 387 | // https://stackoverflow.com/a/65270044 388 | return forwardedIPs[len(forwardedIPs)-1] 389 | } 390 | 391 | // Otherwise, the server is not being reverse-proxied. 392 | // This is most likely during debugging. 393 | 394 | ip, _, err := net.SplitHostPort(r.RemoteAddr) 395 | if err != nil { 396 | log.Printf("getIPString: net.SplitHostPort(%s): %v", r.RemoteAddr, err) 397 | return r.RemoteAddr 398 | } 399 | parsed := net.ParseIP(ip) 400 | 401 | if parsed.IsPrivate() || parsed.IsLoopback() { 402 | // IP is from a local address, from the same machine as the server, or from the LAN 403 | // This would happen during testing, like if the server is being run on a dev machine 404 | // Return a fake IP address key, as there would be multiple IP addresses within the LAN 405 | return "lan" 406 | } 407 | return ip 408 | } 409 | 410 | func writeTimeout(ctx context.Context, timeout time.Duration, conn *websocket.Conn, text string) error { 411 | ctx, cancel := context.WithTimeout(ctx, timeout) 412 | defer cancel() 413 | 414 | return conn.Write(ctx, websocket.MessageText, []byte(text)) 415 | } 416 | -------------------------------------------------------------------------------- /data/adjectives.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | var Adjectives = []string{ 4 | "able", 5 | "above", 6 | "absent", 7 | "absolute", 8 | "abstract", 9 | "abundant", 10 | "academic", 11 | "acceptable", 12 | "accepted", 13 | "accessible", 14 | "accurate", 15 | "accused", 16 | "active", 17 | "actual", 18 | "acute", 19 | "added", 20 | "additional", 21 | "adequate", 22 | "adjacent", 23 | "administrative", 24 | "adorable", 25 | "advanced", 26 | "adverse", 27 | "advisory", 28 | "aesthetic", 29 | "afraid", 30 | "aggregate", 31 | "aggressive", 32 | "agreeable", 33 | "agreed", 34 | "agricultural", 35 | "alert", 36 | "alive", 37 | "alleged", 38 | "allied", 39 | "alone", 40 | "alright", 41 | "alternative", 42 | "amateur", 43 | "amazing", 44 | "ambitious", 45 | "amused", 46 | "ancient", 47 | "angry", 48 | "annoyed", 49 | "annual", 50 | "anonymous", 51 | "anxious", 52 | "appalling", 53 | "apparent", 54 | "applicable", 55 | "appropriate", 56 | "arbitrary", 57 | "architectural", 58 | "armed", 59 | "arrogant", 60 | "artificial", 61 | "artistic", 62 | "ashamed", 63 | "asleep", 64 | "assistant", 65 | "associated", 66 | "atomic", 67 | "attractive", 68 | "automatic", 69 | "autonomous", 70 | "available", 71 | "average", 72 | "awake", 73 | "aware", 74 | "awful", 75 | "awkward", 76 | "back", 77 | "bad", 78 | "balanced", 79 | "bare", 80 | "basic", 81 | "beautiful", 82 | "beneficial", 83 | "better", 84 | "bewildered", 85 | "big", 86 | "binding", 87 | "biological", 88 | "bitter", 89 | "bizarre", 90 | "blank", 91 | "blind", 92 | "blonde", 93 | "bloody", 94 | "blushing", 95 | "boiling", 96 | "bold", 97 | "bored", 98 | "boring", 99 | "bottom", 100 | "brainy", 101 | "brave", 102 | "breakable", 103 | "breezy", 104 | "brief", 105 | "bright", 106 | "brilliant", 107 | "broad", 108 | "broken", 109 | "bumpy", 110 | "burning", 111 | "busy", 112 | "calm", 113 | "capable", 114 | "capitalist", 115 | "careful", 116 | "casual", 117 | "causal", 118 | "cautious", 119 | "central", 120 | "certain", 121 | "changing", 122 | "characteristic", 123 | "charming", 124 | "cheap", 125 | "cheerful", 126 | "chemical", 127 | "chief", 128 | "chilly", 129 | "chosen", 130 | "christian", 131 | "chronic", 132 | "chubby", 133 | "circular", 134 | "civic", 135 | "civil", 136 | "civilian", 137 | "classic", 138 | "classical", 139 | "clean", 140 | "clear", 141 | "clever", 142 | "clinical", 143 | "close", 144 | "closed", 145 | "cloudy", 146 | "clumsy", 147 | "coastal", 148 | "cognitive", 149 | "coherent", 150 | "cold", 151 | "collective", 152 | "colonial", 153 | "colorful", 154 | "colossal", 155 | "coloured", 156 | "colourful", 157 | "combative", 158 | "combined", 159 | "comfortable", 160 | "coming", 161 | "commercial", 162 | "common", 163 | "communist", 164 | "compact", 165 | "comparable", 166 | "comparative", 167 | "compatible", 168 | "competent", 169 | "competitive", 170 | "complete", 171 | "complex", 172 | "complicated", 173 | "comprehensive", 174 | "compulsory", 175 | "conceptual", 176 | "concerned", 177 | "concrete", 178 | "condemned", 179 | "confident", 180 | "confidential", 181 | "confused", 182 | "conscious", 183 | "conservation", 184 | "conservative", 185 | "considerable", 186 | "consistent", 187 | "constant", 188 | "constitutional", 189 | "contemporary", 190 | "content", 191 | "continental", 192 | "continued", 193 | "continuing", 194 | "continuous", 195 | "controlled", 196 | "controversial", 197 | "convenient", 198 | "conventional", 199 | "convinced", 200 | "convincing", 201 | "cooing", 202 | "cool", 203 | "cooperative", 204 | "corporate", 205 | "correct", 206 | "corresponding", 207 | "costly", 208 | "courageous", 209 | "crazy", 210 | "creative", 211 | "creepy", 212 | "criminal", 213 | "critical", 214 | "crooked", 215 | "crowded", 216 | "crucial", 217 | "crude", 218 | "cruel", 219 | "cuddly", 220 | "cultural", 221 | "curious", 222 | "curly", 223 | "current", 224 | "curved", 225 | "cute", 226 | "daily", 227 | "damaged", 228 | "damp", 229 | "dangerous", 230 | "dark", 231 | "dead", 232 | "deaf", 233 | "deafening", 234 | "dear", 235 | "decent", 236 | "decisive", 237 | "deep", 238 | "defeated", 239 | "defensive", 240 | "defiant", 241 | "definite", 242 | "deliberate", 243 | "delicate", 244 | "delicious", 245 | "delighted", 246 | "delightful", 247 | "democratic", 248 | "dependent", 249 | "depressed", 250 | "desirable", 251 | "desperate", 252 | "detailed", 253 | "determined", 254 | "developed", 255 | "developing", 256 | "devoted", 257 | "different", 258 | "difficult", 259 | "digital", 260 | "diplomatic", 261 | "direct", 262 | "dirty", 263 | "disabled", 264 | "disappointed", 265 | "disastrous", 266 | "disciplinary", 267 | "disgusted", 268 | "distant", 269 | "distinct", 270 | "distinctive", 271 | "distinguished", 272 | "disturbed", 273 | "disturbing", 274 | "diverse", 275 | "divine", 276 | "dizzy", 277 | "domestic", 278 | "dominant", 279 | "double", 280 | "doubtful", 281 | "drab", 282 | "dramatic", 283 | "dreadful", 284 | "driving", 285 | "drunk", 286 | "dry", 287 | "dual", 288 | "due", 289 | "dull", 290 | "dusty", 291 | "dutch", 292 | "dying", 293 | "dynamic", 294 | "eager", 295 | "early", 296 | "eastern", 297 | "easy", 298 | "economic", 299 | "educational", 300 | "eerie", 301 | "effective", 302 | "efficient", 303 | "elaborate", 304 | "elated", 305 | "elderly", 306 | "eldest", 307 | "electoral", 308 | "electric", 309 | "electrical", 310 | "electronic", 311 | "elegant", 312 | "eligible", 313 | "embarrassed", 314 | "embarrassing", 315 | "emotional", 316 | "empirical", 317 | "empty", 318 | "enchanting", 319 | "encouraging", 320 | "endless", 321 | "energetic", 322 | "enormous", 323 | "enthusiastic", 324 | "entire", 325 | "entitled", 326 | "envious", 327 | "environmental", 328 | "equal", 329 | "equivalent", 330 | "essential", 331 | "established", 332 | "estimated", 333 | "ethical", 334 | "ethnic", 335 | "eventual", 336 | "everyday", 337 | "evident", 338 | "evil", 339 | "evolutionary", 340 | "exact", 341 | "excellent", 342 | "exceptional", 343 | "excess", 344 | "excessive", 345 | "excited", 346 | "exciting", 347 | "exclusive", 348 | "existing", 349 | "exotic", 350 | "expected", 351 | "expensive", 352 | "experienced", 353 | "experimental", 354 | "explicit", 355 | "extended", 356 | "extensive", 357 | "external", 358 | "extra", 359 | "extraordinary", 360 | "extreme", 361 | "exuberant", 362 | "faint", 363 | "fair", 364 | "faithful", 365 | "familiar", 366 | "famous", 367 | "fancy", 368 | "fantastic", 369 | "far", 370 | "fascinating", 371 | "fashionable", 372 | "fast", 373 | "fat", 374 | "fatal", 375 | "favourable", 376 | "favourite", 377 | "federal", 378 | "fellow", 379 | "female", 380 | "feminist", 381 | "few", 382 | "fierce", 383 | "filthy", 384 | "final", 385 | "financial", 386 | "fine", 387 | "firm", 388 | "fiscal", 389 | "fit", 390 | "fixed", 391 | "flaky", 392 | "flat", 393 | "flexible", 394 | "fluffy", 395 | "fluttering", 396 | "flying", 397 | "following", 398 | "fond", 399 | "foolish", 400 | "foreign", 401 | "formal", 402 | "formidable", 403 | "forthcoming", 404 | "fortunate", 405 | "forward", 406 | "fragile", 407 | "frail", 408 | "frantic", 409 | "free", 410 | "frequent", 411 | "fresh", 412 | "friendly", 413 | "frightened", 414 | "front", 415 | "frozen", 416 | "full", 417 | "fun", 418 | "functional", 419 | "fundamental", 420 | "funny", 421 | "furious", 422 | "future", 423 | "fuzzy", 424 | "gastric", 425 | "gay", 426 | "general", 427 | "generous", 428 | "genetic", 429 | "gentle", 430 | "genuine", 431 | "geographical", 432 | "giant", 433 | "gigantic", 434 | "given", 435 | "glad", 436 | "glamorous", 437 | "gleaming", 438 | "global", 439 | "glorious", 440 | "golden", 441 | "good", 442 | "gorgeous", 443 | "gothic", 444 | "governing", 445 | "graceful", 446 | "gradual", 447 | "grand", 448 | "grateful", 449 | "greasy", 450 | "great", 451 | "grieving", 452 | "grim", 453 | "gross", 454 | "grotesque", 455 | "growing", 456 | "grubby", 457 | "grumpy", 458 | "guilty", 459 | "handicapped", 460 | "handsome", 461 | "happy", 462 | "hard", 463 | "harsh", 464 | "head", 465 | "healthy", 466 | "heavy", 467 | "helpful", 468 | "helpless", 469 | "hidden", 470 | "high", 471 | "hilarious", 472 | "hissing", 473 | "historic", 474 | "historical", 475 | "hollow", 476 | "holy", 477 | "homeless", 478 | "homely", 479 | "hon", 480 | "honest", 481 | "horizontal", 482 | "horrible", 483 | "hostile", 484 | "hot", 485 | "huge", 486 | "human", 487 | "hungry", 488 | "hurt", 489 | "hushed", 490 | "husky", 491 | "icy", 492 | "ideal", 493 | "identical", 494 | "ideological", 495 | "ill", 496 | "illegal", 497 | "imaginative", 498 | "immediate", 499 | "immense", 500 | "imperial", 501 | "implicit", 502 | "important", 503 | "impossible", 504 | "impressed", 505 | "impressive", 506 | "improved", 507 | "inadequate", 508 | "inappropriate", 509 | "inc", 510 | "inclined", 511 | "increased", 512 | "increasing", 513 | "incredible", 514 | "independent", 515 | "indirect", 516 | "individual", 517 | "industrial", 518 | "inevitable", 519 | "influential", 520 | "informal", 521 | "inherent", 522 | "initial", 523 | "injured", 524 | "inland", 525 | "inner", 526 | "innocent", 527 | "innovative", 528 | "inquisitive", 529 | "instant", 530 | "institutional", 531 | "insufficient", 532 | "intact", 533 | "integral", 534 | "integrated", 535 | "intellectual", 536 | "intelligent", 537 | "intense", 538 | "intensive", 539 | "interested", 540 | "interesting", 541 | "interim", 542 | "interior", 543 | "intermediate", 544 | "internal", 545 | "international", 546 | "intimate", 547 | "invisible", 548 | "involved", 549 | "irrelevant", 550 | "isolated", 551 | "itchy", 552 | "jealous", 553 | "jittery", 554 | "joint", 555 | "jolly", 556 | "joyous", 557 | "judicial", 558 | "juicy", 559 | "junior", 560 | "just", 561 | "keen", 562 | "key", 563 | "kind", 564 | "known", 565 | "labour", 566 | "large", 567 | "late", 568 | "latin", 569 | "lazy", 570 | "leading", 571 | "left", 572 | "legal", 573 | "legislative", 574 | "legitimate", 575 | "lengthy", 576 | "lesser", 577 | "level", 578 | "lexical", 579 | "liable", 580 | "liberal", 581 | "light", 582 | "like", 583 | "likely", 584 | "limited", 585 | "linear", 586 | "linguistic", 587 | "liquid", 588 | "literary", 589 | "little", 590 | "live", 591 | "lively", 592 | "living", 593 | "local", 594 | "logical", 595 | "lonely", 596 | "long", 597 | "loose", 598 | "lost", 599 | "loud", 600 | "lovely", 601 | "low", 602 | "loyal", 603 | "ltd", 604 | "lucky", 605 | "mad", 606 | "magic", 607 | "magnetic", 608 | "magnificent", 609 | "main", 610 | "major", 611 | "male", 612 | "mammoth", 613 | "managerial", 614 | "managing", 615 | "manual", 616 | "many", 617 | "marginal", 618 | "marine", 619 | "marked", 620 | "married", 621 | "marvellous", 622 | "marxist", 623 | "mass", 624 | "massive", 625 | "mathematical", 626 | "mature", 627 | "maximum", 628 | "mean", 629 | "meaningful", 630 | "mechanical", 631 | "medical", 632 | "medieval", 633 | "melodic", 634 | "melted", 635 | "mental", 636 | "mere", 637 | "metropolitan", 638 | "mid", 639 | "middle", 640 | "mighty", 641 | "mild", 642 | "military", 643 | "miniature", 644 | "minimal", 645 | "minimum", 646 | "ministerial", 647 | "minor", 648 | "miserable", 649 | "misleading", 650 | "missing", 651 | "misty", 652 | "mixed", 653 | "moaning", 654 | "mobile", 655 | "moderate", 656 | "modern", 657 | "modest", 658 | "molecular", 659 | "monetary", 660 | "monthly", 661 | "moral", 662 | "motionless", 663 | "muddy", 664 | "multiple", 665 | "mushy", 666 | "musical", 667 | "mute", 668 | "mutual", 669 | "mysterious", 670 | "naked", 671 | "narrow", 672 | "nasty", 673 | "national", 674 | "native", 675 | "natural", 676 | "naughty", 677 | "naval", 678 | "near", 679 | "nearby", 680 | "neat", 681 | "necessary", 682 | "negative", 683 | "neighbouring", 684 | "nervous", 685 | "net", 686 | "neutral", 687 | "new", 688 | "nice", 689 | "noble", 690 | "noisy", 691 | "normal", 692 | "northern", 693 | "nosy", 694 | "notable", 695 | "novel", 696 | "nuclear", 697 | "numerous", 698 | "nursing", 699 | "nutritious", 700 | "nutty", 701 | "obedient", 702 | "objective", 703 | "obliged", 704 | "obnoxious", 705 | "obvious", 706 | "occasional", 707 | "occupational", 708 | "odd", 709 | "official", 710 | "ok", 711 | "okay", 712 | "old", 713 | "olympic", 714 | "only", 715 | "open", 716 | "operational", 717 | "opposite", 718 | "optimistic", 719 | "oral", 720 | "ordinary", 721 | "organic", 722 | "organisational", 723 | "original", 724 | "orthodox", 725 | "other", 726 | "outdoor", 727 | "outer", 728 | "outrageous", 729 | "outside", 730 | "outstanding", 731 | "overall", 732 | "overseas", 733 | "overwhelming", 734 | "painful", 735 | "pale", 736 | "panicky", 737 | "parallel", 738 | "parental", 739 | "parliamentary", 740 | "partial", 741 | "particular", 742 | "passing", 743 | "passive", 744 | "past", 745 | "patient", 746 | "payable", 747 | "peaceful", 748 | "peculiar", 749 | "perfect", 750 | "permanent", 751 | "persistent", 752 | "personal", 753 | "petite", 754 | "philosophical", 755 | "physical", 756 | "plain", 757 | "planned", 758 | "plastic", 759 | "pleasant", 760 | "pleased", 761 | "poised", 762 | "polite", 763 | "political", 764 | "poor", 765 | "popular", 766 | "positive", 767 | "possible", 768 | "potential", 769 | "powerful", 770 | "practical", 771 | "precious", 772 | "precise", 773 | "preferred", 774 | "pregnant", 775 | "preliminary", 776 | "premier", 777 | "prepared", 778 | "present", 779 | "presidential", 780 | "pretty", 781 | "previous", 782 | "prickly", 783 | "primary", 784 | "prime", 785 | "primitive", 786 | "principal", 787 | "printed", 788 | "prior", 789 | "private", 790 | "probable", 791 | "productive", 792 | "professional", 793 | "profitable", 794 | "profound", 795 | "progressive", 796 | "prominent", 797 | "promising", 798 | "proper", 799 | "proposed", 800 | "prospective", 801 | "protective", 802 | "protestant", 803 | "proud", 804 | "provincial", 805 | "psychiatric", 806 | "psychological", 807 | "public", 808 | "puny", 809 | "pure", 810 | "purring", 811 | "puzzled", 812 | "quaint", 813 | "qualified", 814 | "quarrelsome", 815 | "querulous", 816 | "quick", 817 | "quickest", 818 | "quiet", 819 | "quintessential", 820 | "quixotic", 821 | "racial", 822 | "radical", 823 | "rainy", 824 | "random", 825 | "rapid", 826 | "rare", 827 | "raspy", 828 | "rational", 829 | "ratty", 830 | "raw", 831 | "ready", 832 | "real", 833 | "realistic", 834 | "rear", 835 | "reasonable", 836 | "recent", 837 | "reduced", 838 | "redundant", 839 | "regional", 840 | "registered", 841 | "regular", 842 | "regulatory", 843 | "related", 844 | "relative", 845 | "relaxed", 846 | "relevant", 847 | "reliable", 848 | "relieved", 849 | "religious", 850 | "reluctant", 851 | "remaining", 852 | "remarkable", 853 | "remote", 854 | "renewed", 855 | "representative", 856 | "repulsive", 857 | "required", 858 | "resident", 859 | "residential", 860 | "resonant", 861 | "respectable", 862 | "respective", 863 | "responsible", 864 | "resulting", 865 | "retail", 866 | "retired", 867 | "revolutionary", 868 | "rich", 869 | "ridiculous", 870 | "right", 871 | "rigid", 872 | "ripe", 873 | "rising", 874 | "rival", 875 | "roasted", 876 | "robust", 877 | "rolling", 878 | "romantic", 879 | "rotten", 880 | "rough", 881 | "round", 882 | "royal", 883 | "rubber", 884 | "rude", 885 | "ruling", 886 | "running", 887 | "rural", 888 | "sacred", 889 | "sad", 890 | "safe", 891 | "salty", 892 | "satisfactory", 893 | "satisfied", 894 | "scared", 895 | "scary", 896 | "scattered", 897 | "scientific", 898 | "scornful", 899 | "scrawny", 900 | "screeching", 901 | "secondary", 902 | "secret", 903 | "secure", 904 | "select", 905 | "selected", 906 | "selective", 907 | "selfish", 908 | "semantic", 909 | "senior", 910 | "sensible", 911 | "sensitive", 912 | "separate", 913 | "serious", 914 | "severe", 915 | "sexual", 916 | "shaggy", 917 | "shaky", 918 | "shallow", 919 | "shared", 920 | "sharp", 921 | "sheer", 922 | "shiny", 923 | "shivering", 924 | "shocked", 925 | "short", 926 | "shrill", 927 | "shy", 928 | "sick", 929 | "significant", 930 | "silent", 931 | "silky", 932 | "silly", 933 | "similar", 934 | "simple", 935 | "single", 936 | "skilled", 937 | "skinny", 938 | "sleepy", 939 | "slight", 940 | "slim", 941 | "slimy", 942 | "slippery", 943 | "slow", 944 | "small", 945 | "smart", 946 | "smiling", 947 | "smoggy", 948 | "smooth", 949 | "social", 950 | "socialist", 951 | "soft", 952 | "solar", 953 | "sole", 954 | "solid", 955 | "sophisticated", 956 | "sore", 957 | "sorry", 958 | "sound", 959 | "sour", 960 | "southern", 961 | "soviet", 962 | "spare", 963 | "sparkling", 964 | "spatial", 965 | "special", 966 | "specific", 967 | "specified", 968 | "spectacular", 969 | "spicy", 970 | "spiritual", 971 | "splendid", 972 | "spontaneous", 973 | "sporting", 974 | "spotless", 975 | "spotty", 976 | "square", 977 | "squealing", 978 | "stable", 979 | "stale", 980 | "standard", 981 | "static", 982 | "statistical", 983 | "statutory", 984 | "steady", 985 | "steep", 986 | "sticky", 987 | "stiff", 988 | "still", 989 | "stingy", 990 | "stormy", 991 | "straight", 992 | "straightforward", 993 | "strange", 994 | "strategic", 995 | "strict", 996 | "striking", 997 | "striped", 998 | "strong", 999 | "structural", 1000 | "stuck", 1001 | "stupid", 1002 | "subjective", 1003 | "subsequent", 1004 | "substantial", 1005 | "subtle", 1006 | "successful", 1007 | "successive", 1008 | "sudden", 1009 | "sufficient", 1010 | "suitable", 1011 | "sunny", 1012 | "super", 1013 | "superb", 1014 | "superior", 1015 | "supporting", 1016 | "supposed", 1017 | "supreme", 1018 | "sure", 1019 | "surprised", 1020 | "surprising", 1021 | "surrounding", 1022 | "surviving", 1023 | "suspicious", 1024 | "sweet", 1025 | "swift", 1026 | "symbolic", 1027 | "sympathetic", 1028 | "systematic", 1029 | "tall", 1030 | "tame", 1031 | "tart", 1032 | "tasteless", 1033 | "tasty", 1034 | "technical", 1035 | "technological", 1036 | "teenage", 1037 | "temporary", 1038 | "tender", 1039 | "tense", 1040 | "terrible", 1041 | "territorial", 1042 | "testy", 1043 | "then", 1044 | "theoretical", 1045 | "thick", 1046 | "thin", 1047 | "thirsty", 1048 | "thorough", 1049 | "thoughtful", 1050 | "thoughtless", 1051 | "thundering", 1052 | "tight", 1053 | "tiny", 1054 | "tired", 1055 | "top", 1056 | "tory", 1057 | "total", 1058 | "tough", 1059 | "toxic", 1060 | "traditional", 1061 | "tragic", 1062 | "tremendous", 1063 | "tricky", 1064 | "tropical", 1065 | "troubled", 1066 | "typical", 1067 | "ugliest", 1068 | "ugly", 1069 | "ultimate", 1070 | "unable", 1071 | "unacceptable", 1072 | "unaware", 1073 | "uncertain", 1074 | "unchanged", 1075 | "uncomfortable", 1076 | "unconscious", 1077 | "underground", 1078 | "underlying", 1079 | "unemployed", 1080 | "uneven", 1081 | "unexpected", 1082 | "unfair", 1083 | "unfortunate", 1084 | "unhappy", 1085 | "uniform", 1086 | "uninterested", 1087 | "unique", 1088 | "united", 1089 | "universal", 1090 | "unknown", 1091 | "unlikely", 1092 | "unnecessary", 1093 | "unpleasant", 1094 | "unsightly", 1095 | "unusual", 1096 | "unwilling", 1097 | "upper", 1098 | "upset", 1099 | "uptight", 1100 | "urban", 1101 | "urgent", 1102 | "used", 1103 | "useful", 1104 | "useless", 1105 | "usual", 1106 | "vague", 1107 | "valid", 1108 | "valuable", 1109 | "variable", 1110 | "varied", 1111 | "various", 1112 | "varying", 1113 | "vast", 1114 | "verbal", 1115 | "vertical", 1116 | "very", 1117 | "vicarious", 1118 | "vicious", 1119 | "victorious", 1120 | "violent", 1121 | "visible", 1122 | "visiting", 1123 | "visual", 1124 | "vital", 1125 | "vitreous", 1126 | "vivacious", 1127 | "vivid", 1128 | "vocal", 1129 | "vocational", 1130 | "voiceless", 1131 | "voluminous", 1132 | "voluntary", 1133 | "vulnerable", 1134 | "wandering", 1135 | "warm", 1136 | "wasteful", 1137 | "watery", 1138 | "weak", 1139 | "wealthy", 1140 | "weary", 1141 | "wee", 1142 | "weekly", 1143 | "weird", 1144 | "welcome", 1145 | "well", 1146 | "western", 1147 | "wet", 1148 | "whispering", 1149 | "whole", 1150 | "wicked", 1151 | "wide", 1152 | "widespread", 1153 | "wild", 1154 | "wilful", 1155 | "willing", 1156 | "willowy", 1157 | "wily", 1158 | "wise", 1159 | "wispy", 1160 | "wittering", 1161 | "witty", 1162 | "wonderful", 1163 | "wooden", 1164 | "working", 1165 | "worldwide", 1166 | "worried", 1167 | "worrying", 1168 | "worthwhile", 1169 | "worthy", 1170 | "written", 1171 | "wrong", 1172 | "xenacious", 1173 | "xenial", 1174 | "xenogeneic", 1175 | "xenophobic", 1176 | "xeric", 1177 | "xerothermic", 1178 | "yabbering", 1179 | "yammering", 1180 | "yappiest", 1181 | "yappy", 1182 | "yawning", 1183 | "yearling", 1184 | "yearning", 1185 | "yeasty", 1186 | "yelling", 1187 | "yelping", 1188 | "yielding", 1189 | "yodelling", 1190 | "young", 1191 | "youngest", 1192 | "youthful", 1193 | "ytterbic", 1194 | "yucky", 1195 | "yummy", 1196 | "zany", 1197 | "zealous", 1198 | "zeroth", 1199 | "zestful", 1200 | "zesty", 1201 | "zippy", 1202 | "zonal", 1203 | "zoophagous", 1204 | "zygomorphic", 1205 | "zygotic", 1206 | } 1207 | -------------------------------------------------------------------------------- /data/animals.go: -------------------------------------------------------------------------------- 1 | package data 2 | 3 | // Generated by: 4 | // curl -sSL https://github.com/andreasonny83/unique-names-generator/raw/main/src/dictionaries/animals.ts | sed "1d" | sed "s/'/\"/g" | sed '$ d' 5 | var Animals = []string{ 6 | "aardvark", 7 | "aardwolf", 8 | "albatross", 9 | "alligator", 10 | "alpaca", 11 | "amphibian", 12 | "anaconda", 13 | "angelfish", 14 | "anglerfish", 15 | "ant", 16 | "anteater", 17 | "antelope", 18 | "antlion", 19 | "ape", 20 | "aphid", 21 | "armadillo", 22 | "asp", 23 | "baboon", 24 | "badger", 25 | "bandicoot", 26 | "barnacle", 27 | "barracuda", 28 | "basilisk", 29 | "bass", 30 | "bat", 31 | "bear", 32 | "beaver", 33 | "bedbug", 34 | "bee", 35 | "beetle", 36 | "bird", 37 | "bison", 38 | "blackbird", 39 | "boa", 40 | "boar", 41 | "bobcat", 42 | "bobolink", 43 | "bonobo", 44 | "booby", 45 | "bovid", 46 | "bug", 47 | "butterfly", 48 | "buzzard", 49 | "camel", 50 | "canid", 51 | "canidae", 52 | "capybara", 53 | "cardinal", 54 | "caribou", 55 | "carp", 56 | "cat", 57 | "caterpillar", 58 | "catfish", 59 | "catshark", 60 | "cattle", 61 | "centipede", 62 | "cephalopod", 63 | "chameleon", 64 | "cheetah", 65 | "chickadee", 66 | "chicken", 67 | "chimpanzee", 68 | "chinchilla", 69 | "chipmunk", 70 | "cicada", 71 | "clam", 72 | "clownfish", 73 | "cobra", 74 | "cockroach", 75 | "cod", 76 | "condor", 77 | "constrictor", 78 | "coral", 79 | "cougar", 80 | "cow", 81 | "coyote", 82 | "crab", 83 | "crane", 84 | "crawdad", 85 | "crayfish", 86 | "cricket", 87 | "crocodile", 88 | "crow", 89 | "cuckoo", 90 | "damselfly", 91 | "deer", 92 | "dingo", 93 | "dinosaur", 94 | "dog", 95 | "dolphin", 96 | "donkey", 97 | "dormouse", 98 | "dove", 99 | "dragon", 100 | "dragonfly", 101 | "duck", 102 | "eagle", 103 | "earthworm", 104 | "earwig", 105 | "echidna", 106 | "eel", 107 | "egret", 108 | "elephant", 109 | "elk", 110 | "emu", 111 | "ermine", 112 | "falcon", 113 | "felidae", 114 | "ferret", 115 | "finch", 116 | "firefly", 117 | "fish", 118 | "flamingo", 119 | "flea", 120 | "fly", 121 | "flyingfish", 122 | "fowl", 123 | "fox", 124 | "frog", 125 | "galliform", 126 | "gamefowl", 127 | "gayal", 128 | "gazelle", 129 | "gecko", 130 | "gerbil", 131 | "gibbon", 132 | "giraffe", 133 | "goat", 134 | "goldfish", 135 | "goose", 136 | "gopher", 137 | "gorilla", 138 | "grasshopper", 139 | "grouse", 140 | "guan", 141 | "guanaco", 142 | "guineafowl", 143 | "gull", 144 | "guppy", 145 | "haddock", 146 | "halibut", 147 | "hamster", 148 | "hare", 149 | "harrier", 150 | "hawk", 151 | "hedgehog", 152 | "heron", 153 | "herring", 154 | "hippopotamus", 155 | "hookworm", 156 | "hornet", 157 | "horse", 158 | "hoverfly", 159 | "hummingbird", 160 | "hyena", 161 | "iguana", 162 | "impala", 163 | "jackal", 164 | "jaguar", 165 | "jay", 166 | "jellyfish", 167 | "junglefowl", 168 | "kangaroo", 169 | "kingfisher", 170 | "kite", 171 | "kiwi", 172 | "koala", 173 | "koi", 174 | "krill", 175 | "ladybug", 176 | "lamprey", 177 | "landfowl", 178 | "lark", 179 | "leech", 180 | "lemming", 181 | "lemur", 182 | "leopard", 183 | "leopon", 184 | "limpet", 185 | "lion", 186 | "lizard", 187 | "llama", 188 | "lobster", 189 | "locust", 190 | "loon", 191 | "louse", 192 | "lungfish", 193 | "lynx", 194 | "macaw", 195 | "mackerel", 196 | "magpie", 197 | "mammal", 198 | "manatee", 199 | "mandrill", 200 | "marlin", 201 | "marmoset", 202 | "marmot", 203 | "marsupial", 204 | "marten", 205 | "mastodon", 206 | "meadowlark", 207 | "meerkat", 208 | "mink", 209 | "minnow", 210 | "mite", 211 | "mockingbird", 212 | "mole", 213 | "mollusk", 214 | "mongoose", 215 | "monkey", 216 | "moose", 217 | "mosquito", 218 | "moth", 219 | "mouse", 220 | "mule", 221 | "muskox", 222 | "narwhal", 223 | "newt", 224 | "nightingale", 225 | "ocelot", 226 | "octopus", 227 | "opossum", 228 | "orangutan", 229 | "orca", 230 | "ostrich", 231 | "otter", 232 | "owl", 233 | "ox", 234 | "panda", 235 | "panther", 236 | "parakeet", 237 | "parrot", 238 | "parrotfish", 239 | "partridge", 240 | "peacock", 241 | "peafowl", 242 | "pelican", 243 | "penguin", 244 | "perch", 245 | "pheasant", 246 | "pig", 247 | "pigeon", 248 | "pike", 249 | "pinniped", 250 | "piranha", 251 | "planarian", 252 | "platypus", 253 | "pony", 254 | "porcupine", 255 | "porpoise", 256 | "possum", 257 | "prawn", 258 | "primate", 259 | "ptarmigan", 260 | "puffin", 261 | "puma", 262 | "python", 263 | "quail", 264 | "quelea", 265 | "quokka", 266 | "rabbit", 267 | "raccoon", 268 | "rat", 269 | "rattlesnake", 270 | "raven", 271 | "reindeer", 272 | "reptile", 273 | "rhinoceros", 274 | "roadrunner", 275 | "rodent", 276 | "rook", 277 | "rooster", 278 | "roundworm", 279 | "sailfish", 280 | "salamander", 281 | "salmon", 282 | "sawfish", 283 | "scallop", 284 | "scorpion", 285 | "seahorse", 286 | "shark", 287 | "sheep", 288 | "shrew", 289 | "shrimp", 290 | "silkworm", 291 | "silverfish", 292 | "skink", 293 | "skunk", 294 | "sloth", 295 | "slug", 296 | "smelt", 297 | "snail", 298 | "snake", 299 | "snipe", 300 | "sole", 301 | "sparrow", 302 | "spider", 303 | "spoonbill", 304 | "squid", 305 | "squirrel", 306 | "starfish", 307 | "stingray", 308 | "stoat", 309 | "stork", 310 | "sturgeon", 311 | "swallow", 312 | "swan", 313 | "swift", 314 | "swordfish", 315 | "swordtail", 316 | "tahr", 317 | "takin", 318 | "tapir", 319 | "tarantula", 320 | "tarsier", 321 | "termite", 322 | "tern", 323 | "thrush", 324 | "tick", 325 | "tiger", 326 | "tiglon", 327 | "toad", 328 | "tortoise", 329 | "toucan", 330 | "trout", 331 | "tuna", 332 | "turkey", 333 | "turtle", 334 | "tyrannosaurus", 335 | "unicorn", 336 | "urial", 337 | "vicuna", 338 | "viper", 339 | "vole", 340 | "vulture", 341 | "wallaby", 342 | "walrus", 343 | "warbler", 344 | "wasp", 345 | "weasel", 346 | "whale", 347 | "whippet", 348 | "whitefish", 349 | "wildcat", 350 | "wildebeest", 351 | "wildfowl", 352 | "wolf", 353 | "wolverine", 354 | "wombat", 355 | "woodpecker", 356 | "worm", 357 | "wren", 358 | "xerinae", 359 | "yak", 360 | "zebra", 361 | } 362 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/makeworld-the-better-one/neartalk 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/dustin/go-humanize v1.0.1-0.20210705192016-249ff6c91207 7 | github.com/rivo/uniseg v0.2.0 8 | golang.org/x/text v0.3.7 9 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 10 | nhooyr.io/websocket v1.8.7 11 | ) 12 | 13 | require github.com/klauspost/compress v1.10.3 // indirect 14 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/dustin/go-humanize v1.0.1-0.20210705192016-249ff6c91207 h1:06VJ6lVl9r9kvzqs3r1gSfUDm6aiMKmyaZyLVc2ShmA= 4 | github.com/dustin/go-humanize v1.0.1-0.20210705192016-249ff6c91207/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 5 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 6 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 7 | github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= 8 | github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= 9 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 10 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 11 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 12 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 13 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 14 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 15 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 16 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= 17 | github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= 18 | github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= 19 | github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= 20 | github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= 21 | github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= 22 | github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= 23 | github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= 24 | github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= 25 | github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= 26 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 27 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 28 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= 29 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 30 | github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= 31 | github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 32 | github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= 33 | github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= 34 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 35 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 36 | github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= 37 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 38 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= 39 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 40 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= 41 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 42 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 43 | github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= 44 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 45 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 46 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 47 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 48 | github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= 49 | github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= 50 | github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= 51 | github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= 52 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= 53 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 54 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 55 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 56 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 57 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= 58 | golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 59 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 60 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 61 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 62 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 63 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 64 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 65 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 66 | nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= 67 | nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= 68 | -------------------------------------------------------------------------------- /html/about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NearTalk | About 6 | 7 | 8 | 9 | 10 | 11 |

About NearTalk

12 |

What is it?

13 |

14 | NearTalk is chat platform to talk to people nearby. 15 |

16 |

17 | Anyone with the same IP address is in the same chat room. For example, everyone 18 | in your house will get the same chat room if they visit NearTalk. If you go to 19 | your local coffee shop, everyone who visits NearTalk will be in the same chat room. 20 | This extends to larger organizations like college/university campuses. 21 |

22 |

23 | Depending on how the network is set up, all mobile devices using data with the same 24 | network provider as you may be chatting together. Or similarly, all the other homes 25 | using the same ISP. This is the minority of cases however. 26 |

27 |

Why is it?

28 |

29 | For fun, mostly. I wanted to make a chat application and I wanted to use 30 | htmx, and this seemed like a fun idea. 31 |

32 |

33 | There are many reasons why NearTalk isn't useful, and talking to your fellow humans 34 | face to face 35 | is much better. However there are a few times when having a local chatroom is useful, 36 | like for discussing (or dragging) a presentation going on. At the end of the day, 37 | I'm happy to have made something. 38 |

39 |

How do I change my nickname?

40 |

41 | Send this special message: /nick my-new-nickname
42 | It will go away when you reload the page. 43 |

44 |

Source code? Self hosting?

45 |

46 | Of course! NearTalk is licensed under the AGPLv3, 47 | and source code is available on GitHub. 48 |

49 |

50 | You're welcome to host your own version, as long as you comply with the license by publishing your source 51 | code. Feel free to report bugs and submit PRs as well! 52 |

53 |

Contact

54 |

55 | You can email me about NearTalk at: makeworld (AT) protonmail (DOT) com 56 |

57 |

58 | I'd be happy to hear about any fun stories. 59 |

60 | 61 | 62 | -------------------------------------------------------------------------------- /html/admin.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NearTalk Admin 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |

Admin Interface

17 |
18 | 19 | -------------------------------------------------------------------------------- /html/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | word-wrap: break-word; 3 | background-color: white; 4 | font-size: 1rem 5 | } 6 | 7 | .center { 8 | text-align: center; 9 | } 10 | 11 | #root { 12 | display: flex; 13 | flex-direction: column; 14 | height: 100vh; 15 | padding-left: 20px; 16 | padding-right: 20px; 17 | padding-bottom: 20px; 18 | margin: auto; 19 | } 20 | 21 | #content { 22 | flex: 1; 23 | display: flex; 24 | flex-direction: row; 25 | height: 0px; /* Needed for dynamic flex height sizing, idk */ 26 | } 27 | 28 | #chat { 29 | flex: 1; 30 | display: flex; 31 | flex-direction: column; 32 | } 33 | 34 | #users { 35 | flex: none; 36 | min-width: 10vw; 37 | width: min-content; 38 | margin-left: 20px; 39 | height: 100%; 40 | display: flex; 41 | flex-direction: column; 42 | } 43 | 44 | #users-header { 45 | flex: none; 46 | } 47 | 48 | #users-list { 49 | flex: 1; 50 | overflow-y: auto; 51 | line-height: .5; 52 | } 53 | 54 | /* 55 | Disable users list for mobile, as usually it's too much. 56 | This includes larger screens like iPads in landscape but whatever 57 | */ 58 | @media (pointer:none), (pointer:coarse) { 59 | #chat { 60 | width: 100%; 61 | } 62 | #users { 63 | display: none; 64 | } 65 | } 66 | 67 | /* 100vh on safari does not include the bottom bar. */ 68 | @supports (-webkit-overflow-scrolling: touch) { 69 | #root { 70 | height: 85vh; 71 | } 72 | } 73 | /* Same with Firefox Mobile */ 74 | @media (pointer:none), (pointer:coarse) { 75 | @supports ( -moz-appearance:none ) { 76 | #root { 77 | height: 90vh; 78 | } 79 | } 80 | } 81 | 82 | #header { 83 | flex: none; 84 | line-height: 1; 85 | } 86 | 87 | #messages { 88 | flex: 1; 89 | overflow-y: auto; 90 | } 91 | 92 | #send-form-div { 93 | flex: none; 94 | margin-top: 10px; 95 | width: 100%; 96 | } 97 | 98 | #send-form { 99 | width: 100%; 100 | display: flex; 101 | flex-direction: row; 102 | } 103 | 104 | #message-input { 105 | flex: 1; 106 | margin-right: 10px; 107 | min-width: 0px; 108 | } 109 | /* Try to prevent form zoom on iOS */ 110 | @media (pointer:none), (pointer:coarse) { 111 | #message-input { 112 | font-size: 16px !important; 113 | } 114 | } 115 | 116 | #send-btn { 117 | flex: none; 118 | } 119 | 120 | #message-table { 121 | display: block; 122 | } 123 | 124 | td { 125 | padding: 0 .5em; 126 | vertical-align: top; 127 | } 128 | @media (pointer:none), (pointer:coarse) { 129 | /* Allow table cell wrapping on mobile and reduce unecessary padding */ 130 | td { 131 | padding: 0 .2em; 132 | display: inline-block; 133 | } 134 | } 135 | 136 | /* Don't wrap timestamps in table */ 137 | #message-table-tbody > tr > td:nth-of-type(1) { 138 | white-space: nowrap; 139 | } 140 | @media (pointer:none), (pointer:coarse) { 141 | /* Hide timestamps on mobile */ 142 | #message-table-tbody > tr > td:nth-of-type(1) { 143 | display: none; 144 | } 145 | } 146 | /* Nicknames */ 147 | #message-table-tbody > tr > td:nth-of-type(2) { 148 | font-weight: bold; 149 | } 150 | /* Third table column, where msgs are */ 151 | #message-table-tbody > tr > td:nth-of-type(3) { 152 | word-break: break-word; 153 | line-height: 1.1; 154 | padding-top: 0.2em; 155 | } 156 | 157 | #send-form input[type="text"] { 158 | -moz-appearance: none; 159 | -webkit-appearance: none; 160 | word-break: normal; 161 | border-radius: 5px; 162 | border: 1px solid #ccc; 163 | } 164 | 165 | #send-form input[type="submit"] { 166 | color: white; 167 | background-color: black; 168 | border-radius: 5px; 169 | padding: 5px 10px; 170 | border: none; 171 | } 172 | 173 | #send-form input[type="submit"]:hover { 174 | background-color: green; 175 | cursor: pointer; 176 | } 177 | 178 | #send-form input[type="submit"]:active { 179 | background-color: green; 180 | } 181 | 182 | @media (pointer:none), (pointer:coarse) { 183 | /* Button stays green after pressing otherwise */ 184 | #send-form input[type="submit"]:hover { 185 | background-color: black; 186 | } 187 | } 188 | 189 | /* Message classes */ 190 | 191 | @media (pointer:none), (pointer:coarse) { 192 | /* Keep messages without nicknames inline with others */ 193 | .special-msg > td:nth-of-type(2) { 194 | padding: 0; 195 | } 196 | } 197 | 198 | .error { 199 | color: red; 200 | } 201 | 202 | .notif { 203 | color: gray; 204 | font-style: italic; 205 | } 206 | 207 | .my-msg { 208 | } 209 | 210 | 211 | 212 | .my-nick { 213 | color: gray; 214 | font-weight: normal !important; 215 | } 216 | 217 | 218 | 219 | /* Simple classes */ 220 | 221 | .bold { 222 | font-weight: bold; 223 | } -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NearTalk 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 38 | 39 | 40 | 41 |
42 | 50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | 58 | 59 |
60 |
61 |
62 |
63 |

Users

64 |
65 |
66 |
67 |
68 | 69 | 70 | -------------------------------------------------------------------------------- /html/privacy_policy.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NearTalk | Privacy Policy 6 | 7 | 8 | 9 | 10 | 11 |

Privacy Policy

12 |

13 | NearTalk is intended to be a privacy-preserving service. 14 | Your data is yours, and I don't want it. 15 |

16 |

17 | No data is retained long term. If the service experiences abuse I may turn on HTTP logging, 18 | meaning IP addresses, user agents, and times when the site was visited would be available to me. 19 | Currently, even that is not turned on, so no data is retained. 20 |

21 |

22 | The content of messages is never stored anywhere. It is removed from the server RAM as soon as 23 | the message has be sent to all users in the chat room. 24 |

25 |

26 | As server admin, I can ONLY see: 27 |

    28 |
  • IP addresses
  • 29 |
  • Number of chat rooms
  • 30 |
  • When the last message was sent in each room
  • 31 |
32 | Nothing else is available to me, by design, and none of this is saved permanently. 33 |

34 |

35 | If you'd like to verify this yourself, you can read the 36 | source code. 37 |

38 | 39 | -------------------------------------------------------------------------------- /html/simple.css: -------------------------------------------------------------------------------- 1 | /* 2 | Hi there! Please take this CSS and use it for your own projects. 3 | It's a great little thing. 4 | */ 5 | 6 | body { 7 | margin: 40px auto; 8 | max-width: 720px; 9 | line-height: 1.6; 10 | font-size: 18px; 11 | color: rgb(0, 0, 0); 12 | padding: 0 20px; 13 | word-wrap: break-word; 14 | font-family: Sans-Serif; 15 | } 16 | 17 | h1, h2, h3 { 18 | line-height: 1.2 19 | } 20 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "math/rand" 9 | "net" 10 | "net/http" 11 | "os" 12 | "os/signal" 13 | "strconv" 14 | "syscall" 15 | "time" 16 | ) 17 | 18 | // Flag vars 19 | var ( 20 | host string 21 | port uint 22 | adminKey string 23 | versionFlag bool 24 | ) 25 | 26 | func main() { 27 | flag.StringVar(&host, "host", "127.0.0.1", "Host for HTTP server") 28 | flag.UintVar(&port, "port", 8000, "Port number for HTTP server") 29 | flag.StringVar(&adminKey, "key", "", "Key/password to access admin interface") 30 | flag.BoolVar(&versionFlag, "version", false, "See version info") 31 | flag.Parse() 32 | 33 | if versionFlag { 34 | fmt.Print(versionInfo) 35 | return 36 | } 37 | if adminKey == "" { 38 | fmt.Println("No admin key set! Use -help for details.") 39 | return 40 | } 41 | 42 | err := run() 43 | if err != nil { 44 | log.Fatal(err) 45 | } 46 | } 47 | 48 | func run() error { 49 | rand.Seed(time.Now().UnixNano()) 50 | 51 | l, err := net.Listen("tcp", net.JoinHostPort(host, strconv.FormatUint(uint64(port), 10))) 52 | if err != nil { 53 | return err 54 | } 55 | log.Printf("listening on http://%v", l.Addr()) 56 | 57 | // Create and run HTTP server 58 | cs := newChatServer() 59 | s := &http.Server{ 60 | Handler: cs, 61 | ReadTimeout: time.Second * 10, 62 | WriteTimeout: time.Second * 10, 63 | } 64 | errc := make(chan error, 1) 65 | go func() { 66 | errc <- s.Serve(l) 67 | }() 68 | 69 | // Wait for server error or process signals (like Ctrl-C) 70 | sigs := make(chan os.Signal, 1) 71 | signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) 72 | select { 73 | case err := <-errc: 74 | log.Printf("failed to serve: %v", err) 75 | case sig := <-sigs: 76 | log.Printf("terminating: %v", sig) 77 | } 78 | 79 | // Gracefully shut down HTTP server with 5 second timeout 80 | ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) 81 | defer cancel() 82 | return s.Shutdown(ctx) 83 | } 84 | -------------------------------------------------------------------------------- /message.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // This file deals with messages coming from or going to the web UI. 4 | // The web UI uses htmx (htmx.org) and so HTML is passed over the websocket for 5 | // updates. 6 | // Like with any chat service, messages coming in have to sanitized. 7 | // This file also deals with rendering any kinds of special messages, like red 8 | // for errors. 9 | 10 | import ( 11 | "fmt" 12 | "html" 13 | "regexp" 14 | "strings" 15 | "time" 16 | 17 | "github.com/rivo/uniseg" 18 | "golang.org/x/text/unicode/norm" 19 | ) 20 | 21 | const maxNickLen = 30 22 | const maxMsgTextLen = 512 23 | 24 | // URL Regex 25 | // Source: 26 | // John Gruber has a blog post: https://daringfireball.net/2010/07/improved_regex_for_matching_urls 27 | // That links to this gist: https://gist.github.com/gruber/249502 28 | // I modified the regex slightly for Go (\x60 instead of `) 29 | // I also changed it so it wouldn't recognize non-URLs like "bit.com/test" 30 | // I also made the protocol required 31 | // I applied the change mention in this comment: 32 | // https://gist.github.com/gruber/249502#gistcomment-1381560 33 | // That way magnet links and similar are picked up 34 | var urlRe = regexp.MustCompile(`(?i)\b(?:[a-z][\w.+-]+:(?:/{1,3}|[?+]?[a-z0-9%]))(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s\x60!()\[\]{};:'".,<>?«»“”‘’])`) 35 | 36 | // Sending this through the websocket to htmx clears whatever message was 37 | // written in the input field. This is used to clear the field after the user 38 | // sends a message. 39 | const clearInputFieldMsg = `` 40 | 41 | // createChatMsg takes the message from a user and returns HTML 42 | // that can be sent over websocket to the htmx web UI. 43 | // It returns two messages, one for the author, and one for everyone else. 44 | // It will return empty strings if the provided msg is considered invalid. 45 | func createChatMsg(m msg) (string, string) { 46 | sanitizedMsgText := renderMsgText(m.text) 47 | if !isMsgTextValid(sanitizedMsgText) { 48 | return "", "" 49 | } 50 | ts := m.when.UTC().Format(time.RFC3339) 51 | author := fmt.Sprintf( 52 | // Add message to log 53 | ` 54 | %s%s%s 55 | `, 56 | ts, m.nick, sanitizedMsgText, // nick is already sanitized 57 | ) 58 | nonAuthor := fmt.Sprintf( 59 | // Add message to log 60 | ` 61 | %s%s%s 62 | `, 63 | ts, m.nick, sanitizedMsgText, 64 | ) 65 | return author, nonAuthor 66 | } 67 | 68 | func isMsgTextValid(s string) bool { 69 | return s != "" 70 | } 71 | 72 | // createUserListMsg creates HTML that can replace the current user list. 73 | // It assume the nicknames provided are already HTML escaped. 74 | func createUserListMsg(nicks []string) string { 75 | var b strings.Builder 76 | b.WriteString(`
`) 77 | for i := range nicks { 78 | b.WriteString(fmt.Sprintf(`

%s

`, nicks[i])) 79 | } 80 | b.WriteString(`
`) 81 | b.WriteString(fmt.Sprintf(`

Users (%d)

`, len(nicks))) 82 | return b.String() 83 | } 84 | 85 | // createSpecialMsg creates a message not from any specific user, that has a 86 | // CSS class. This can be used for error messages, or notifications. 87 | func createSpecialMsg(text string, class string) string { 88 | var ts string 89 | if class == "notif" { 90 | // Notification messages are timestamped 91 | ts = time.Now().UTC().Format(time.RFC3339) 92 | } 93 | return fmt.Sprintf( 94 | // Add message to log 95 | ` 96 | %s%s 97 | `, 98 | ts, class, html.EscapeString(text), 99 | ) 100 | } 101 | 102 | // createJoinMsg creates a msg struct that can be sent to a chat room when a client joins. 103 | func createJoinMsg(c *client, nicks []string) msg { 104 | return msg{ 105 | raw: createSpecialMsg(fmt.Sprintf("%s has joined", c.nick), "notif") + 106 | createUserListMsg(nicks), 107 | when: time.Now(), 108 | } 109 | } 110 | 111 | // createLeaveMsg creates a msg struct that can be sent to a chat room when a client leaves. 112 | func createLeaveMsg(c *client, nicks []string) msg { 113 | return msg{ 114 | raw: createSpecialMsg(fmt.Sprintf("%s has left", c.nick), "notif") + 115 | createUserListMsg(nicks), 116 | when: time.Now(), 117 | } 118 | } 119 | 120 | func sanitizeNick(nick string) string { 121 | nick = strings.ToValidUTF8(nick, "\uFFFD") 122 | nick = strings.TrimSpace(nick) 123 | // Unicode normalization, to prevent look-alike nicknames 124 | nick = norm.NFC.String(nick) 125 | 126 | // Truncate by graphemes instead of runes, so multi-rune things like flags work 127 | g := uniseg.NewGraphemes(nick) 128 | i := 0 129 | nick = "" 130 | for g.Next() && i < maxNickLen { 131 | nick += g.Str() 132 | i++ 133 | } 134 | 135 | nick = html.EscapeString(nick) 136 | return nick 137 | } 138 | 139 | func renderMsgText(text string) string { 140 | text = strings.ToValidUTF8(text, "\uFFFD") 141 | text = strings.TrimSpace(text) 142 | 143 | // TODO: is this too slow? 144 | g := uniseg.NewGraphemes(text) 145 | i := 0 146 | var b strings.Builder 147 | for g.Next() && i < maxMsgTextLen { 148 | b.Write(g.Bytes()) 149 | i++ 150 | } 151 | text = b.String() 152 | text = html.EscapeString(text) 153 | 154 | // Linkify URLs 155 | text = urlRe.ReplaceAllStringFunc(text, func(urlText string) string { 156 | return fmt.Sprintf(`%s`, urlText, urlText) 157 | }) 158 | 159 | return text 160 | } 161 | 162 | // Message handlers 163 | 164 | // handleMsg takes a msg and performs the appropriate action. 165 | // This may involve sending a message back to the author. If a message should 166 | // sent to all chat room clients, handleMsg returns a two strings, one to send 167 | // to the author, and another to send to everyone else. 168 | // Otherwise empty strings are returned. 169 | func (cr *chatRoom) handleMsg(m msg) (string, string) { 170 | cr.clientsMu.Lock() 171 | defer cr.clientsMu.Unlock() 172 | 173 | if m.raw != "" { 174 | // Message is already rendered 175 | return m.raw, m.raw 176 | } 177 | 178 | if strings.HasPrefix(m.text, "/nick ") && len(m.text) > len("/nick ") { 179 | newNick := sanitizeNick(m.text[len("/nick "):]) 180 | if newNick == "" { 181 | // Empty nickname, invalid 182 | m.author.sendText(createSpecialMsg("Nickname cannot be empty", "error")) 183 | return "", "" 184 | } 185 | if cr.nickInUse(newNick) { 186 | m.author.sendText(createSpecialMsg("That nickname is already in use", "error")) 187 | return "", "" 188 | } 189 | oldNick := m.author.nick 190 | m.author.nick = newNick 191 | // Tell everyone about name change, and update user list 192 | s := createSpecialMsg( 193 | fmt.Sprintf("%s is now known as %s", oldNick, newNick), "notif", 194 | ) + 195 | createUserListMsg(cr.nicks()) 196 | return s, s 197 | } 198 | 199 | // Regular message 200 | cr.whenLastMsg = m.when 201 | return createChatMsg(m) 202 | } 203 | -------------------------------------------------------------------------------- /neartalk.example.service: -------------------------------------------------------------------------------- 1 | # This is an example systemd service file for deploying NearTalk 2 | # "webserver" is the web server you are using for reverse proxying. 3 | 4 | [Unit] 5 | Description=NearTalk server 6 | After=webserver.service 7 | 8 | [Install] 9 | WantedBy=webserver.service 10 | 11 | [Service] 12 | WorkingDirectory=/path/to/neartalk-git 13 | ExecStart=/path/to/neartalk-git/neartalk -port 1234 -key PUT-ADMIN-KEY-HERE 14 | Restart=always 15 | -------------------------------------------------------------------------------- /nicknames.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "strings" 7 | 8 | "github.com/makeworld-the-better-one/neartalk/data" 9 | ) 10 | 11 | // genNick returns a new random nickname. 12 | func genNick() string { 13 | adjective := data.Adjectives[rand.Intn(len(data.Adjectives))] 14 | animal := data.Animals[rand.Intn(len(data.Animals))] 15 | 16 | // Convert to CamelCase 17 | return strings.ReplaceAll( 18 | strings.Title(fmt.Sprintf("%s %s", adjective, animal)), 19 | " ", "", 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "fmt" 4 | 5 | // Version info. This is set by the Makefile 6 | var ( 7 | version string 8 | commit string 9 | builtBy string 10 | ) 11 | 12 | var versionInfo = fmt.Sprintf("Version: %s\nCommit: %s\nBuilt by: %s\n", version, commit, builtBy) 13 | --------------------------------------------------------------------------------