Users
├── .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.
9 | NearTalk is chat platform to talk to people nearby.
10 |
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 |
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 |
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 |
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 | %d chat rooms %d chatters Last message: %s
14 | NearTalk is chat platform to talk to people nearby.
15 |
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 |
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 |
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 |
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 |
41 | Send this special message:
46 | Of course! NearTalk is licensed under the AGPLv3,
47 | and source code is available on GitHub.
48 |
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 |
55 | You can email me about NearTalk at: makeworld (AT) protonmail (DOT) com
56 |
58 | I'd be happy to hear about any fun stories.
59 |
46 | About |
47 | Privacy Policy
48 | Users
13 | NearTalk is intended to be a privacy-preserving service.
14 | Your data is yours, and I don't want it.
15 |
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 |
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 |
26 | As server admin, I can ONLY see:
27 | What is it?
8 | Why is it?
23 |
`, len(cs.rooms))
53 | for ip, room := range cs.rooms {
54 | fmt.Fprintf(
55 | w, `%s
%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 | About NearTalk
12 | What is it?
13 | Why is it?
28 | How do I change my nickname?
40 | /nick my-new-nickname
42 | It will go away when you reload the page.
43 | Source code? Self hosting?
45 | Contact
54 | 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
44 |
45 |
54 |
Privacy Policy
12 |
28 |
32 | Nothing else is available to me, by design, and none of this is saved permanently.
33 |
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
`, nicks[i])) 79 | } 80 | b.WriteString(`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 |