From 71ed3832f2263d1578ca20f394527770e1942e39 Mon Sep 17 00:00:00 2001 From: AK Date: Sat, 1 Aug 2026 13:09:13 -0700 Subject: [PATCH 1/2] feat: add probabilistic join greetings --- .gitignore | 1 + README.md | 1 + bot/bot.go | 27 ++++++++ bot/event_test.go | 40 +++++++++++ bot/plugin.go | 6 ++ config.yaml | 3 + data/welcome.txt | 146 ++++++++++++++++++++++++++++++++++++++++ docs/configuration.md | 10 +++ docs/plugins.md | 29 ++++++++ plugins/plugins.go | 1 + plugins/welcome.go | 95 ++++++++++++++++++++++++++ plugins/welcome_test.go | 75 +++++++++++++++++++++ 12 files changed, 434 insertions(+) create mode 100644 bot/event_test.go create mode 100644 data/welcome.txt create mode 100644 plugins/welcome.go create mode 100644 plugins/welcome_test.go diff --git a/.gitignore b/.gitignore index feae736..7c75f27 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ data/* !data/cars.txt !data/fun/ !data/fun/*.txt +!data/welcome.txt !data/weapons.txt .env .env.bak diff --git a/README.md b/README.md index 22d3985..e318498 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ bot/ IRC connection, config, queue, stats, and interfaces plugins/ built-in plugins and tests data/foods/ local food, cuisine, and beer suggestion lists data/fun/ local joke, pun, one-liner, and wisdom catalogs +data/welcome.txt short original join-greeting catalog data/weapons.txt local high-level firearm and weapons-name catalog data/sports.txt local sports suggestion list data/cars.txt local car make/model suggestion list diff --git a/bot/bot.go b/bot/bot.go index 6ad0511..e2034d2 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -83,6 +83,9 @@ func (b *Bot) connect(ctx context.Context) error { if m.Command == "INVITE" { b.handleInvite(m) } + if m.Command == "JOIN" { + b.dispatchEvent(ParseMessage(m)) + } if m.Command == "001" { actualNick := "" if len(m.Params) > 0 { @@ -197,6 +200,13 @@ func (b *Bot) channelWarming(channel string) bool { return !until.IsZero() && time.Now().Before(until) } +// ChannelWarming reports whether a channel is still inside the post-join +// backlog protection window. Event-driven plugins can use this to avoid +// reacting to replayed join/activity events during startup. +func (b *Bot) ChannelWarming(channel string) bool { + return b.channelWarming(channel) +} + func validChannelName(channel string) bool { if len(channel) < 2 || len(channel) > 200 || (channel[0] != '#' && channel[0] != '&') { return false @@ -424,6 +434,23 @@ func (b *Bot) dispatch(msg Message) { } } } + +func (b *Bot) dispatchEvent(msg Message) { + for _, p := range b.Plugins { + handler, ok := p.(EventHandler) + if !ok { + continue + } + func() { + defer func() { + if r := recover(); r != nil { + b.Log.Error("plugin event panic", zap.String("plugin", p.Name()), zap.Any("panic", r)) + } + }() + handler.HandleEvent(b, msg) + }() + } +} func (b *Bot) Run(ctx context.Context) error { backoff := 5 * time.Second for { diff --git a/bot/event_test.go b/bot/event_test.go new file mode 100644 index 0000000..5d76438 --- /dev/null +++ b/bot/event_test.go @@ -0,0 +1,40 @@ +package bot + +import ( + "context" + "testing" + + "github.com/variablenix/GoBot/storage" + "go.uber.org/zap" +) + +type eventProbe struct { + events int + handles int +} + +func (p *eventProbe) Name() string { return "event-probe" } +func (p *eventProbe) Commands() []string { return nil } +func (p *eventProbe) Help() string { return "test event probe" } +func (p *eventProbe) Init(PluginConfig, *storage.DB) error { return nil } +func (p *eventProbe) Handle(*Bot, Message) bool { + p.handles++ + return false +} +func (p *eventProbe) HandleEvent(*Bot, Message) bool { + p.events++ + return true +} + +func TestDispatchEventOnlyCallsEventHandlers(t *testing.T) { + probe := &eventProbe{} + b := New(Config{}, nil, []Plugin{probe}, zap.NewNop()) + b.dispatchEvent(Message{Command: "JOIN", Target: "#test", IsChannel: true, Nick: "Alice"}) + if probe.events != 1 { + t.Fatalf("event handler calls = %d, want 1", probe.events) + } + if probe.handles != 0 { + t.Fatalf("message handler calls = %d, want 0", probe.handles) + } + b.Queue.Drain(context.Background()) +} diff --git a/bot/plugin.go b/bot/plugin.go index 87868a5..9de3ac2 100644 --- a/bot/plugin.go +++ b/bot/plugin.go @@ -13,3 +13,9 @@ type Plugin interface { type Starter interface { Start(*Bot) } + +// EventHandler lets a plugin observe non-message IRC events without making +// every command plugin inspect the raw event stream. +type EventHandler interface { + HandleEvent(*Bot, Message) bool +} diff --git a/config.yaml b/config.yaml index 99874a4..3815500 100644 --- a/config.yaml +++ b/config.yaml @@ -51,6 +51,9 @@ plugins: # Set fortune_dir to an explicit, curated fortune directory if desired. # Leave empty to avoid loading the entire system fortune collection. banter: {enabled: true, probability: 0.54, quotes_file: "quotes/banter.txt", fortune_dir: ""} + # Optional playful join greetings. The cooldown is per channel; placeholders + # such as {nick} are replaced with the joining nickname. + welcome: {enabled: false, probability: 0.15, cooldown_seconds: 120, messages_file: "data/welcome.txt"} # Optional YouTube Data API v3 key for reliable title/channel/duration data. # Without it, GoBot uses YouTube's public oEmbed/player fallbacks. urltitle: {enabled: true, max_title_length: 120, timeout_seconds: 5, youtube_api_key: ""} diff --git a/data/welcome.txt b/data/welcome.txt new file mode 100644 index 0000000..111bb77 --- /dev/null +++ b/data/welcome.txt @@ -0,0 +1,146 @@ +The Evil Has Landed. +A wild plot twist has entered the channel. +The channel has acquired another witness. +Incoming personality detected. Please remain calm. +Alert: the guest list just got more interesting. +The chat has unlocked a new character. +A mysterious figure approaches the snack table. +The room was peaceful for several minutes. It was nice while it lasted. +Fresh human detected. Deploy the tiny welcome parade. +The channel's plot thickens. +We were just talking about you. Probably. +Excellent timing. The snacks have not gone stale yet. +The doors were unlocked, and look what happened. +Another brave soul enters the scroll zone. +The conversation has received a surprise expansion pack. +Welcome aboard. The emergency exit is mostly decorative. +The channel has a new co-conspirator. +A new legend has entered, or at least a new nickname. +Please welcome our latest agent of delightful chaos. +The guest has arrived; everyone act natural. +The chat's charisma level just changed. +New arrival! Somebody hide the good opinions. +The channel is now 3% more interesting. +The welcome committee is underqualified but enthusiastic. +The room has gained another pair of eyes and one more set of opinions. +The conversation was missing exactly one wildcard. +A wild {nick} appeared! +{nick} has entered the arena. The arena is mostly text. +{nick} joined, so the plot can continue. +{nick} has arrived with suspiciously good timing. +{nick} is here. Please secure the dessert. +{nick} entered the channel; the paperwork is already confusing. +{nick} has joined the party. No party favors were prepared. +{nick} arrived and immediately improved the attendance numbers. +{nick} has crossed into the chatlands. +{nick} joined. Somebody tell the historians. +{nick} has entered the channel with main-character timing. +{nick} is here; let the completely reasonable nonsense begin. +{nick} just spawned in with full confidence and no tutorial. +{nick} has joined the channel. The prophecy was oddly specific. +{nick} entered the room; the room is pretending it planned this. +{nick} has arrived. Please keep hands, feet, and hot takes inside the channel. +{nick} joined the channel and passed the vibe check by existing. +{nick} is here. The channel may now begin its second act. +{nick} has appeared from the digital mist. +{nick} joined, and the audience has resumed its seats. +{nick} has entered the chat. Applause may be silent. +{nick} arrived just in time for absolutely no announcements. +{nick} has joined. The channel's tiny imaginary budget is now spent. +{nick} is here, and somebody just heard a dramatic chord. +{nick} has entered the channel. Please update the lore. +{nick} arrived; the group chat has gained a plot device. +{nick} joined. Statistically, this is a good sign. +{nick} is here. Release the ceremonial confetti. +{nick} entered the channel; the pixels are thrilled. +{nick} has joined. The welcome mat is slightly haunted. +{nick} arrived with no warning and excellent dramatic timing. +{nick} joined the channel. Nobody panic until the second message. +{nick} is here; the channel may now claim it has plans. +{nick} has entered. The bureaucracy of friendship begins now. +{nick} joined, proving the invite button works. +{nick} has arrived. The silence has been formally notified. +{nick} entered the room and found us exactly like this. +{nick} joined the channel. The lore department is taking notes. +{nick} has appeared. Quick, look busy. +{nick} arrived; the channel's population counter is feeling proud. +{nick} is here. The conversation has been promoted to a larger cast. +{nick} joined, and the channel gained one more possible alibi. +{nick} has arrived. The welcome cannon is still being assembled. +{nick} entered the chat; the chat entered a brief state of awe. +{nick} joined the channel. Please offer them the least suspicious chair. +{nick} is here, and the channel's imaginary house band is warming up. +{nick} has arrived with the energy of a surprise season finale. +{nick} joined. We can now blame at least one more person for the plot. +{nick} entered the channel; the welcome committee has misplaced its clipboard. +{nick} is here. This is either excellent news or very entertaining news. +{nick} has joined, and the channel has achieved a new level of populated. +{nick} arrived. The chat's tiny parade is stuck in traffic. +{nick} joined the channel; please enjoy the complimentary awkward wave. +{nick} is here. The channel's loading screen has finally finished. +{nick} has entered the room; the room has no objection. +{nick} joined. The welcome speech has been replaced by this sentence. +{nick} has arrived, and the channel is legally a little busier. +{nick} entered the chat. The invisible velvet rope has been removed. +{nick} joined the channel; the snack budget is under review. +{nick} is here. Everyone may resume their regularly scheduled typing. +{nick} has arrived with zero fanfare and maximum potential. +{nick} joined. The channel's council of experts is now one person larger. +{nick} entered the chat; somebody ring the tiny bell. +{nick} has appeared, and the narrative refuses to explain why. +{nick} joined the channel. A perfectly normal amount of excitement follows. +{nick} is here; the channel's dramatic lighting has improved. +{nick} has arrived. The welcome wagon contains mostly puns. +{nick} entered the room, and the room gave a polite digital nod. +{nick} joined. The channel has accepted the terms and conditions. +{nick} is here. The plot twist has excellent attendance today. +{nick} has arrived; somebody add a chair to the seating chart. +{nick} joined the channel. The census taker is delighted. +{nick} entered the chat with the confidence of someone who knows the shortcut. +{nick} is here. The channel is now accepting questions and snacks. +{nick} joined, and the silence has officially lost its majority. +{nick} has arrived. A small parade is forming somewhere off-screen. +{nick} entered the channel; the welcome mat is doing its best. +{nick} joined. Please add one point to the room's people-shaped scoreboard. +{nick} is here, so the channel can stop pretending it is exclusive. +{nick} has appeared. The digital door closes softly behind them. +{nick} arrived with impeccable timing and questionable suspense. +{nick} joined the channel; the imaginary bouncer gives a respectful nod. +{nick} is here. The channel's forecast calls for scattered conversation. +{nick} has entered the room. The room remains under observation. +{nick} joined. The welcome committee has unanimously approved this development. +{nick} arrived; please update the channel's character roster. +{nick} is here, and the chat just gained another possible punchline. +{nick} has joined the channel. Let the low-stakes adventure begin. +{nick} entered the chat; the imaginary applause is thunderous. +{nick} joined. No passwords were harmed in this entrance. +{nick} has arrived, and the channel is pretending this was scheduled. +{nick} is here. The conversation has a fresh pair of headphones. +{nick} entered the room; the room would like everyone to remain calm. +{nick} joined the channel. The fine print says fun may occur. +{nick} has arrived. The channel's backup personality is now online. +{nick} joined, and the welcome mat has filed a status report. +{nick} is here; the chat's imaginary elevator has reached the lobby. +{nick} has entered the channel. Please enjoy this entirely unrequested fanfare. +{nick} arrived, and the channel is one anecdote richer. +{nick} joined. The universe has added one more tab. +{nick} is here. Someone cue the tasteful entrance music. +{nick} has appeared; the channel is now a little less mysterious. +{nick} entered the room and found the good chairs already taken. +{nick} joined the channel. We have achieved another successful internet arrival. +{nick} has arrived; the welcome algorithm is smiling politely. +{nick} is here. The chat may now proceed with enhanced personnel. +{nick} joined, and the channel's imaginary scoreboard says: nice. +{nick} has entered the chat. The first impression committee is on break. +{nick} arrived. The channel's tiny trumpet has been sounded. +{nick} joined the channel; please hand them a map of the ongoing nonsense. +{nick} is here, and the channel has successfully resisted a dramatic backstory. +{nick} has appeared. Somebody mark the calendar with a tiny star. +{nick} entered the channel; the chat is now slightly more fully loaded. +{nick} joined. The welcome message has been approved by absolutely nobody. +{nick} has arrived, and the room's imaginary plants look happier. +{nick} is here. The channel has gained one more reason to keep scrolling. +{nick} entered the chat with the stealth of a notification. +{nick} joined. We are now prepared for a tasteful amount of mischief. +{nick} has arrived; the channel's social battery just increased by one bar. +{nick} is here. The official unofficial welcome is hereby delivered. diff --git a/docs/configuration.md b/docs/configuration.md index 73938f2..6bbae08 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -72,6 +72,8 @@ plugins: foods: {enabled: true, data_dir: "data/foods", max_length: 240} sports: {enabled: true, data_file: "data/sports.txt", max_length: 200} car: {enabled: true, data_file: "data/cars.txt", max_length: 240} + # Optional playful join greetings; disabled by default. + welcome: {enabled: false, probability: 0.15, cooldown_seconds: 120, messages_file: "data/welcome.txt"} # 65% is a balanced casual default; use 100% for deterministic testing. pool: {enabled: true, game_timeout_minutes: 30, turn_timeout_seconds: 120, shot_success_percent: 65} horoscope: {enabled: true, max_summary_length: 360} @@ -88,6 +90,14 @@ lookups. Their output limits prevent a slow or unusually large response from holding up the bot or flooding IRC. Disable any of them with `enabled: false` if they are not wanted. +The optional `welcome` plugin listens for users joining a channel. It applies +the configured probability to each join and then enforces a per-channel +cooldown, so it can add personality without greeting every person in a busy +room. Lines are read from `data/welcome.txt`; `{nick}` is replaced with the +joining nickname. Keep each line short enough for one IRC message. The plugin +also respects `join_warmup_seconds`, so replayed join events during startup do +not cause a burst of greetings. + For deployment secrets, use `.env` or the service manager's environment rather than committing them to the example configuration. `BOT_GITHUB_TOKEN` is optional and is only needed when the anonymous GitHub API limit is not enough. diff --git a/docs/plugins.md b/docs/plugins.md index 933ff45..0a44b1c 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -8,6 +8,7 @@ Plugins are enabled or disabled under plugins..enabled in config.yaml. - correction: IRC-style spelling corrections - banter: optional replies when GoBot is directly addressed +- welcome: optional, probability-based join greetings with a per-channel cooldown - urltitle: page titles and YouTube metadata - weather: Open-Meteo weather, no key required - news: NewsAPI headlines and search @@ -72,6 +73,34 @@ a short humorous message instead of queueing a message for itself. A queued tell is delivered when that nickname next sends either a channel message or a private message to GoBot. +## Join greetings + +The optional `welcome` plugin posts a short, playful line when another user +joins a channel. It is disabled by default. Enable it in `config.yaml`: + +~~~yaml +plugins: + welcome: + enabled: true + probability: 0.15 + cooldown_seconds: 120 + messages_file: "data/welcome.txt" +~~~ + +`probability` is the chance that an individual join produces a greeting. The +cooldown is tracked separately for each channel, so a busy channel cannot be +flooded by a greeting for every arrival. The catalog contains short original +lines and supports the `{nick}` placeholder: + +~~~text +[Welcome] The Evil Has Landed. +[Welcome] A wild Pzycho appeared! +~~~ + +There is no command to enable it at runtime. Once enabled, it applies to +channels where GoBot is present. The bot's own join is ignored, and the normal +post-join warmup also suppresses replayed events during startup. + ## Correction Use familiar IRC correction syntax immediately after the message to correct: diff --git a/plugins/plugins.go b/plugins/plugins.go index d6a6f0e..1a48ae5 100644 --- a/plugins/plugins.go +++ b/plugins/plugins.go @@ -6,6 +6,7 @@ func All() []bot.Plugin { return []bot.Plugin{ &Correction{}, &Banter{}, + &Welcome{}, &URLTitle{}, &Weather{}, &News{}, diff --git a/plugins/welcome.go b/plugins/welcome.go new file mode 100644 index 0000000..41cab11 --- /dev/null +++ b/plugins/welcome.go @@ -0,0 +1,95 @@ +package plugins + +import ( + "math/rand" + "strings" + "sync" + "time" + + "github.com/variablenix/GoBot/bot" + "github.com/variablenix/GoBot/storage" +) + +const defaultWelcomeFile = "data/welcome.txt" + +var fallbackWelcomeLines = []string{ + "The Evil Has Landed.", + "A wild plot twist has entered the channel.", + "Alert: the guest list just got more interesting.", + "The channel has acquired another witness.", + "Incoming personality detected. Please remain calm.", +} + +// Welcome adds occasional, short join greetings. It is deliberately event +// driven and cooldown-limited so a busy channel does not become a wall of bot +// messages. +type Welcome struct { + mu sync.Mutex + probability float64 + cooldown time.Duration + lines []string + last map[string]time.Time +} + +func (p *Welcome) Name() string { return "welcome" } +func (p *Welcome) Commands() []string { return nil } +func (p *Welcome) Help() string { + return "occasionally posts a short, playful line when someone joins a channel" +} + +func (p *Welcome) Init(c bot.PluginConfig, _ *storage.DB) error { + p.probability = c.Float("probability", 0.15) + if p.probability < 0 { + p.probability = 0 + } + if p.probability > 1 { + p.probability = 1 + } + cooldownSeconds := c.Int("cooldown_seconds", 120) + if cooldownSeconds < 1 { + cooldownSeconds = 120 + } + p.cooldown = time.Duration(cooldownSeconds) * time.Second + p.lines = readQuotes(c.String("messages_file", defaultWelcomeFile)) + if len(p.lines) == 0 { + p.lines = append([]string(nil), fallbackWelcomeLines...) + } + p.last = make(map[string]time.Time) + return nil +} + +// Handle is intentionally empty: welcome reacts to JOIN events through the +// EventHandler hook and never treats ordinary chat as a reason to speak. +func (p *Welcome) Handle(_ *bot.Bot, _ bot.Message) bool { return false } + +func (p *Welcome) HandleEvent(b *bot.Bot, m bot.Message) bool { + if m.Command != "JOIN" || !m.IsChannel || m.Nick == "" || b.ChannelWarming(m.Target) { + return false + } + if strings.EqualFold(m.Nick, b.Config.Identity.Nick) { + return false + } + + key := strings.ToLower(b.Config.NetworkName + "\x00" + m.Target) + now := time.Now() + p.mu.Lock() + if last, ok := p.last[key]; ok && now.Sub(last) < p.cooldown { + p.mu.Unlock() + return false + } + if rand.Float64() >= p.probability { + p.mu.Unlock() + return false + } + p.last[key] = now + line := p.lines[rand.Intn(len(p.lines))] + p.mu.Unlock() + + line = strings.ReplaceAll(line, "{nick}", cleanExternalText(m.Nick)) + line = cleanExternalText(line) + if line == "" { + return false + } + b.Send(m.Target, ircColor(ircCyan, "[Welcome] "+line)) + return true +} diff --git a/plugins/welcome_test.go b/plugins/welcome_test.go new file mode 100644 index 0000000..00e1530 --- /dev/null +++ b/plugins/welcome_test.go @@ -0,0 +1,75 @@ +package plugins + +import ( + "context" + "os" + "strings" + "testing" + + "github.com/variablenix/GoBot/bot" + "go.uber.org/zap" +) + +func TestWelcomeDefaults(t *testing.T) { + plugin := &Welcome{} + if err := plugin.Init(nil, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + if plugin.probability != 0.15 || plugin.cooldown.Seconds() != 120 { + t.Fatalf("unexpected defaults: probability=%v cooldown=%v", plugin.probability, plugin.cooldown) + } + if len(plugin.lines) == 0 { + t.Fatal("expected fallback welcome lines") + } +} + +func TestWelcomeLoadsConfiguredLines(t *testing.T) { + path := t.TempDir() + "/welcome.txt" + if err := os.WriteFile(path, []byte("hello {nick}\n\nkeep it short\n"), 0600); err != nil { + t.Fatalf("write test catalog: %v", err) + } + plugin := &Welcome{} + if err := plugin.Init(bot.PluginConfig{ + "probability": 1.0, + "cooldown_seconds": 1, + "messages_file": path, + }, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + if len(plugin.lines) != 2 || plugin.lines[0] != "hello {nick}" { + t.Fatalf("unexpected loaded lines: %#v", plugin.lines) + } + if got := strings.ReplaceAll(plugin.lines[0], "{nick}", "Alice"); got != "hello Alice" { + t.Fatalf("placeholder replacement = %q", got) + } +} + +func TestWelcomeSkipsSelfAndAppliesChannelCooldown(t *testing.T) { + plugin := &Welcome{} + if err := plugin.Init(bot.PluginConfig{ + "probability": 1.0, + "cooldown_seconds": 120, + }, nil); err != nil { + t.Fatalf("Init returned error: %v", err) + } + b := bot.New(bot.Config{}, nil, nil, zap.NewNop()) + b.Config.NetworkName = "test" + b.Config.Identity.Nick = "GoBot" + join := bot.Message{Command: "JOIN", Target: "#chat", IsChannel: true, Nick: "Alice"} + if !plugin.HandleEvent(b, join) { + t.Fatal("expected first join to produce a greeting") + } + if plugin.HandleEvent(b, bot.Message{Command: "JOIN", Target: "#chat", IsChannel: true, Nick: "Bob"}) { + t.Fatal("expected channel cooldown to suppress the next greeting") + } + if plugin.HandleEvent(b, bot.Message{Command: "JOIN", Target: "#chat", IsChannel: true, Nick: "GoBot"}) { + t.Fatal("expected the bot's own join to be ignored") + } + b.Queue.Drain(context.Background()) +} + +func TestWelcomeUsesMIRCColor(t *testing.T) { + if !strings.Contains(ircColor(ircCyan, "[Welcome] hello"), "\x03") { + t.Fatal("expected standard mIRC color control code") + } +} From d32b807e1eee9d464f9086ce2af0a3cc6ea77f88 Mon Sep 17 00:00:00 2001 From: AK Date: Sat, 1 Aug 2026 13:11:16 -0700 Subject: [PATCH 2/2] feat: expand join greeting catalog --- data/welcome.txt | 61 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/data/welcome.txt b/data/welcome.txt index 111bb77..465e46b 100644 --- a/data/welcome.txt +++ b/data/welcome.txt @@ -144,3 +144,64 @@ A wild {nick} appeared! {nick} joined. We are now prepared for a tasteful amount of mischief. {nick} has arrived; the channel's social battery just increased by one bar. {nick} is here. The official unofficial welcome is hereby delivered. +Please follow this chart: https://i.imgur.com/0xTZQHh.png +Please state the nature of the technical emergency. +Your arrival has triggered the mandatory visual briefing: https://i.ytimg.com/vi/jYr_rs67RBQ/sddefault.jpg +The welcome committee requests that you review this totally normal orientation material. +New arrival detected. Please locate the nearest emergency snack. +The channel has entered a new era of lightly supervised nonsense. +Welcome! Your complimentary map is upside down, but the confidence is real. +We have prepared a tasteful fanfare and misplaced it somewhere nearby. +The channel welcomes you and accepts payment in interesting anecdotes. +A new participant has joined. The spreadsheet has become optimistic. +Please remain calm while the welcome machinery makes a tiny whirring sound. +Your entrance has been logged under: probably important. +The channel's official response is a respectful double thumbs-up. +Welcome to the room where the rules are mostly made of suggestions. +New arrival! The imaginary confetti has been successfully deployed. +The channel is pleased to announce that someone new knows where the door is. +Please enjoy this complimentary tour of absolutely nowhere. +Your arrival has increased our population and decreased our ability to blame one person. +The welcome committee has reviewed your application and forgotten the conclusion. +Welcome! Please place all hot takes in the overhead compartment. +The channel's tiny announcer says: excellent, another human. +New user entering the chat. Initiating polite excitement. +Your arrival has been approved by the Department of Unnecessary Announcements. +Welcome aboard the conversation train; delays are frequent and the snacks are imaginary. +The channel just received a fresh shipment of personality. +New arrival! The atmosphere is now legally more social. +Please accept this ceremonial nod from the entire typing population. +The channel has upgraded from quiet to quietly suspicious. +Welcome! The tutorial is unavailable, so improvise confidently. +Your arrival has caused one imaginary server fan to spin faster. +The channel's welcome mat has requested hazard pay. +New participant detected. Somebody pretend we have a plan. +Welcome to the channel; the dress code is comfortable and the lore is complicated. +The room has gained another storyteller. Please keep the plot within reasonable limits. +Your entrance was subtle, elegant, and visible to everyone. +New arrival! The channel's morale has received a small but noticeable buff. +Welcome; please choose a chair that has not already been claimed by a pun. +The chat is now accepting new ideas, old jokes, and suspiciously specific trivia. +Your arrival has been marked with a tasteful invisible ribbon. +Welcome to the place where every simple question has a sequel. +The channel has one more person to ask when something mysteriously breaks. +New arrival! Please sign the guestbook with your best harmless theory. +Welcome; the channel has been expecting you since approximately five minutes ago. +The imaginary orchestra is playing a welcome theme in a key nobody recognizes. +Your arrival has been certified as delightfully unscheduled. +New user joined. The room is now 11% less likely to be an echo chamber. +Welcome! The nearest exit is wherever your browser tab went. +The channel is honored by your presence and mildly concerned by its own excitement. +Your entrance has been added to the unofficial history of this room. +New arrival! The chat's emergency backup personality is standing by. +Welcome to the channel, where the punchline occasionally arrives before the setup. +The channel has gained another possible expert. No pressure whatsoever. +Please enjoy the warm hospitality and the cool refresh rate. +Your arrival has caused the welcome committee to nod in unison. +New user detected. Deploy the friendly blinking light. +Welcome! We promise to keep the nonsense at a professionally manageable level. +The room is now populated enough to form a very small committee. +Your arrival has been received with maximum available enthusiasm. +New participant joined; the channel's imaginary weather is partly conversational. +Welcome! If you hear a beep, that is probably just the lore updating. +The channel has accepted your arrival and is pretending it was prepared.