Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ data/*
!data/cars.txt
!data/fun/
!data/fun/*.txt
!data/welcome.txt
!data/weapons.txt
.env
.env.bak
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 27 additions & 0 deletions bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
40 changes: 40 additions & 0 deletions bot/event_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
6 changes: 6 additions & 0 deletions bot/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
3 changes: 3 additions & 0 deletions config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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: ""}
Expand Down
207 changes: 207 additions & 0 deletions data/welcome.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
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.
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.
10 changes: 10 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -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.
Expand Down
Loading