diff --git a/README.md b/README.md index f377a9c..2707d59 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ 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/acronyms.txt operator-editable ACRONYM|expansion catalog +data/acronyms.txt operator-editable ACRONYM|expansion[|context] catalog data/scramble.txt local word-scramble catalog data/weapons.txt local high-level firearm and weapons-name catalog data/sports.txt local sports suggestion list diff --git a/config.yaml b/config.yaml index 7e540fc..ade52e7 100644 --- a/config.yaml +++ b/config.yaml @@ -79,7 +79,7 @@ plugins: cve: {enabled: true, timeout_seconds: 8, max_length: 360} # Keyless IP-API lookups. The free endpoint is rate limited and uses HTTP. ipinfo: {enabled: true, timeout_seconds: 8, max_length: 320} - # Local operator-maintained ACRONYM|expansion entries. + # Local operator-maintained ACRONYM|expansion[|context] entries. acronym: {enabled: true, data_file: "data/acronyms.txt", max_length: 320} weather: {enabled: true, default_units: imperial} steam: {enabled: true, timeout_seconds: 10, max_length: 360} diff --git a/data/acronyms.txt b/data/acronyms.txt index 3a71885..6dba905 100644 --- a/data/acronyms.txt +++ b/data/acronyms.txt @@ -1,9 +1,12 @@ -# One entry per line: ACRONYM|expansion -# Curated for GoBot from common computing, networking, operations, security, -# and software-delivery terminology. Definitions are kept short so the catalog -# remains useful in IRC; operators can add local terms without changing code. -# Reference glossaries: https://csrc.nist.gov/glossary/ -# https://www.rfc-editor.org/ and https://glossary.cncf.io/ +# One entry per line: ACRONYM|expansion[|context] +# The first meaning is returned by default; add a context to select another +# meaning, for example: API|Application Programming Interface|technology. +# Curated offline from public reference glossaries, including the official +# Section508 acronym list, NIST CSRC, IETF RFCs, and CNCF terminology. +# Operators can add or replace entries here without changing GoBot code. +# https://www.section508.gov/tools/acronyms-abbreviations/ +# https://csrc.nist.gov/glossary/ https://www.rfc-editor.org/ +# https://glossary.cncf.io/ 2FA|Two-Factor Authentication AI|Artificial Intelligence ACL|Access Control List @@ -12,7 +15,7 @@ ADR|Architecture Decision Record AES|Advanced Encryption Standard AGI|Artificial General Intelligence ALU|Arithmetic Logic Unit -API|Application Programming Interface +API|Application Programming Interface|technology AV|Antivirus APM|Application Performance Monitoring ARP|Address Resolution Protocol @@ -229,3 +232,223 @@ XSS|Cross-Site Scripting YAML|YAML Ain't Markup Language YARA|Yet Another Recursive Acronym rDNS|Reverse DNS + +# Business, workplace, and management +ASAP|As Soon As Possible|business +BAU|Business As Usual|business +B2B|Business To Business|business +B2C|Business To Consumer|business +COB|Close Of Business|business +CEO|Chief Executive Officer|business +CFO|Chief Financial Officer|business +CIO|Chief Information Officer|business +CMO|Chief Marketing Officer|business +COO|Chief Operating Officer|business +CPO|Chief Product Officer|business +CRO|Chief Revenue Officer|business +CTO|Chief Technology Officer|business +CSO|Chief Security Officer|business +EOD|End Of Day|business +EOM|End Of Month|business +ETA|Estimated Time of Arrival|business +ETC|Et Cetera|general +EVP|Executive Vice President|business +FY|Fiscal Year|business +FYI|For Your Information|business +HR|Human Resources|business +KPI|Key Performance Indicator|business +OKR|Objectives and Key Results|business +OOO|Out Of Office|business +PTO|Paid Time Off|business +RFP|Request For Proposal|business +RFQ|Request For Quotation|business +SME|Subject Matter Expert|business +TBA|To Be Announced|business +TBC|To Be Confirmed|business +TBD|To Be Determined|business +VP|Vice President|business + +# Finance and accounting +ACH|Automated Clearing House|finance +APR|Annual Percentage Rate|finance +APY|Annual Percentage Yield|finance +ARR|Annual Recurring Revenue|finance +AUM|Assets Under Management|finance +CAC|Customer Acquisition Cost|finance +CAGR|Compound Annual Growth Rate|finance +CAPEX|Capital Expenditure|finance +COGS|Cost Of Goods Sold|finance +DCF|Discounted Cash Flow|finance +EBITDA|Earnings Before Interest, Taxes, Depreciation, and Amortization|finance +EPS|Earnings Per Share|finance +ETF|Exchange-Traded Fund|finance +FDIC|Federal Deposit Insurance Corporation|finance +FICO|Fair Isaac Corporation credit score|finance +GAAP|Generally Accepted Accounting Principles|finance +GDP|Gross Domestic Product|finance +GNP|Gross National Product|finance +IPO|Initial Public Offering|finance +IRR|Internal Rate of Return|finance +LLC|Limited Liability Company|finance +NAV|Net Asset Value|finance +NPV|Net Present Value|finance +P and L|Profit and Loss|finance +PE|Price to Earnings ratio|finance +ROE|Return On Equity|finance +ROIC|Return On Invested Capital|finance +ROI|Return On Investment|finance +SEC|Securities and Exchange Commission|finance +SOX|Sarbanes-Oxley Act|finance + +# Medical and health +API|Active Pharmaceutical Ingredient|medical +ADHD|Attention-Deficit/Hyperactivity Disorder|medical +AIDS|Acquired Immunodeficiency Syndrome|medical +ALS|Amyotrophic Lateral Sclerosis|medical +BMI|Body Mass Index|medical +BP|Blood Pressure|medical +CDC|Centers for Disease Control and Prevention|medical +CPR|Cardiopulmonary Resuscitation|medical +CT|Computed Tomography|medical +DM|Diabetes Mellitus|medical +DNR|Do Not Resuscitate|medical +DNI|Do Not Intubate|medical +ECG|Electrocardiogram|medical +ED|Emergency Department|medical +EKG|Electrocardiogram|medical +EMT|Emergency Medical Technician|medical +ER|Emergency Room|medical +FDA|Food and Drug Administration|medical +HIV|Human Immunodeficiency Virus|medical +ICU|Intensive Care Unit|medical +IV|Intravenous|medical +MCI|Mild Cognitive Impairment|medical +MRI|Magnetic Resonance Imaging|medical +OTC|Over The Counter|medical +PCP|Primary Care Physician|medical +PHI|Protected Health Information|medical +PTSD|Post-Traumatic Stress Disorder|medical +Rx|Prescription|medical +SARS|Severe Acute Respiratory Syndrome|medical +SOAP|Subjective, Objective, Assessment, Plan|medical +STD|Sexually Transmitted Disease|medical +STI|Sexually Transmitted Infection|medical +TB|Tuberculosis|medical +TBI|Traumatic Brain Injury|medical +UTI|Urinary Tract Infection|medical +WHO|World Health Organization|medical + +# Education +ACT|American College Testing|education +AP|Advanced Placement|education +BA|Bachelor of Arts|education +BS|Bachelor of Science|education +ELL|English Language Learner|education +ESL|English as a Second Language|education +GED|General Educational Development|education +GPA|Grade Point Average|education +GRE|Graduate Record Examinations|education +IB|International Baccalaureate|education +K12|Kindergarten Through Grade 12|education +MA|Master of Arts|education +MS|Master of Science|education +PhD|Doctor of Philosophy|education +PSAT|Preliminary SAT|education +PTA|Parent-Teacher Association|education +SAT|Scholastic Assessment Test|education +STEM|Science, Technology, Engineering, and Mathematics|education +STEAM|Science, Technology, Engineering, Arts, and Mathematics|education +TA|Teaching Assistant|education + +# Government, civic, and public services +CIA|Central Intelligence Agency|government +DMV|Department of Motor Vehicles|government +DHS|Department of Homeland Security|government +DOJ|Department of Justice|government +EPA|Environmental Protection Agency|government +EU|European Union|government +FBI|Federal Bureau of Investigation|government +FCC|Federal Communications Commission|government +FEMA|Federal Emergency Management Agency|government +FOIA|Freedom of Information Act|government +FTC|Federal Trade Commission|government +G7|Group of Seven|government +G20|Group of Twenty|government +IRS|Internal Revenue Service|government +NATO|North Atlantic Treaty Organization|government +NGO|Non-Governmental Organization|government +NSA|National Security Agency|government +POTUS|President of the United States|government +SCOTUS|Supreme Court of the United States|government +TSA|Transportation Security Administration|government +UN|United Nations|government +USPS|United States Postal Service|government + +# Internet, chat, and social shorthand +AFK|Away From Keyboard|chat +AFAIK|As Far As I Know|chat +AMA|Ask Me Anything|internet +ASL|Age, Sex, Location|chat +BBL|Be Back Later|chat +BFF|Best Friends Forever|chat +BRB|Be Right Back|chat +BTW|By The Way|chat +DM|Direct Message|internet +FOMO|Fear Of Missing Out|internet +FTW|For The Win|chat +ICYMI|In Case You Missed It|internet +IDK|I Don't Know|chat +IIRC|If I Recall Correctly|chat +IMO|In My Opinion|chat +IMHO|In My Humble Opinion|chat +IRL|In Real Life|internet +JK|Just Kidding|chat +LMAO|Laughing My Ass Off|chat +LOL|Laughing Out Loud|chat +NSFW|Not Safe For Work|internet +NVM|Never Mind|chat +OMW|On My Way|chat +OP|Original Poster|internet +ROFL|Rolling On the Floor Laughing|chat +SMH|Shaking My Head|chat +TBH|To Be Honest|chat +TIL|Today I Learned|internet +TLDR|Too Long; Didn't Read|internet +TMI|Too Much Information|chat +TY|Thank You|chat +WIP|Work In Progress|chat +WTF|What The F***|chat +WTH|What The Heck|chat +YOLO|You Only Live Once|internet + +# Science, engineering, and measurements +ATP|Adenosine Triphosphate|science +CO2|Carbon Dioxide|science +DNA|Deoxyribonucleic Acid|science +GMO|Genetically Modified Organism|science +GPS|Global Positioning System|science +IR|Infrared|science +ISS|International Space Station|science +LASER|Light Amplification by Stimulated Emission of Radiation|science +LCD|Liquid-Crystal Display|technology +LED|Light-Emitting Diode|science +LHC|Large Hadron Collider|science +OLED|Organic Light-Emitting Diode|technology +pH|Potential of Hydrogen|science +RADAR|Radio Detection and Ranging|science +RNA|Ribonucleic Acid|science +SI|International System of Units|science +SONAR|Sound Navigation and Ranging|science +UV|Ultraviolet|science + +# General everyday abbreviations +AKA|Also Known As|general +ASAP|As Soon As Possible|general +DOB|Date Of Birth|general +DIY|Do It Yourself|general +ID|Identification|general +PIN|Personal Identification Number|general +RSVP|Répondez S'il Vous Plaît|general +RIP|Rest In Peace|general +TBD|To Be Determined|general +VIP|Very Important Person|general diff --git a/docs/configuration.md b/docs/configuration.md index 4fece10..a814c4e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -113,6 +113,7 @@ plugins: youtube: {enabled: true, max_length: 320, timeout_seconds: 10, api_key: ""} cve: {enabled: true, timeout_seconds: 8, max_length: 360} ipinfo: {enabled: true, timeout_seconds: 8, max_length: 320} + # Local operator-maintained ACRONYM|expansion[|context] entries. acronym: {enabled: true, data_file: "data/acronyms.txt", max_length: 320} steam: {enabled: true, timeout_seconds: 10, max_length: 360} # Keyless source-grounded answers by default; AI rewriting is opt-in. diff --git a/docs/plugins.md b/docs/plugins.md index 3855c89..3bde1a6 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -368,20 +368,32 @@ plugins: ## Acronyms -`acronym` is a local text-file expander for operations jargon: +`acronym` is a local, offline text-file expander for common business, medical, +education, government, science, internet/chat, finance, and technology terms: ~~~text !acronym MTTR !acro rDNS +!acronym API +!acronym API technology +!acronym htpp ~~~ -Entries use one `ACRONYM|expansion` per line. Blank lines and `#` comments are -ignored, matching is case-insensitive, and malformed entries are skipped. The -bundled catalog covers common computing, networking, operations, security, and -software-delivery terms. It is intentionally local and versioned so responses -remain fast and deterministic; edit or replace it to add organization-specific -jargon without an external service. The baseline terminology was cross-checked -against NIST CSRC, IETF RFCs, and the CNCF Cloud Native Glossary. +Entries use `ACRONYM|expansion[|context]` per line. Blank lines and `#` comments +are ignored, matching is case-insensitive, and malformed entries are skipped. +The first entry for an acronym is the common/default meaning; additional +meanings can be selected with a context, such as `!acronym API medical`. +Multi-word keys such as `PCI DSS` are supported. If an exact lookup fails, +GoBot performs a bounded local fuzzy check and returns at most one suggestion, +for example: `[acronym] no exact match for htpp; did you mean HTTP?` It does +not silently substitute the suggestion or make a network request. + +The bundled catalog is intentionally local, versioned, fast, and deterministic. +Operators can edit or replace `data/acronyms.txt` to add organization-specific +jargon without an API key or runtime dependency. Its broad common-term catalog +was researched offline using public references including the [Section508 +acronym list](https://www.section508.gov/tools/acronyms-abbreviations/), NIST +CSRC, IETF RFCs, and the CNCF Cloud Native Glossary. ~~~yaml plugins: diff --git a/go.mod b/go.mod index 869589f..c80a16a 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,12 @@ module github.com/variablenix/GoBot go 1.25.12 require ( + github.com/agnivade/levenshtein v1.2.1 github.com/spf13/viper v1.21.0 go.etcd.io/bbolt v1.5.0 go.uber.org/zap v1.28.0 golang.org/x/net v0.57.0 + golang.org/x/text v0.40.0 gopkg.in/irc.v3 v3.1.4 ) @@ -23,6 +25,5 @@ require ( go.uber.org/multierr v1.10.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.40.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index d3d56cb..68d83ad 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= diff --git a/plugins/acronym.go b/plugins/acronym.go index 0b233ab..60f528c 100644 --- a/plugins/acronym.go +++ b/plugins/acronym.go @@ -5,26 +5,31 @@ import ( "fmt" "os" "path/filepath" + "sort" "strings" + "unicode" + "github.com/agnivade/levenshtein" "github.com/variablenix/GoBot/bot" "github.com/variablenix/GoBot/storage" ) type Acronym struct { entries map[string]acronymEntry + meanings map[string][]acronymEntry maxLength int } type acronymEntry struct { Name string Expansion string + Context string } func (p *Acronym) Name() string { return "acronym" } func (p *Acronym) Commands() []string { return []string{"acronym", "acro"} } func (p *Acronym) Help() string { - return "!acronym ACRONYM — expand an operator-maintained ACRONYM|expansion entry" + return "!acronym ACRONYM [context] — expand a local ACRONYM|expansion[|context] entry; misspellings get one suggestion" } func (p *Acronym) Init(c bot.PluginConfig, _ *storage.DB) error { @@ -33,6 +38,7 @@ func (p *Acronym) Init(c bot.PluginConfig, _ *storage.DB) error { p.maxLength = 320 } p.entries = make(map[string]acronymEntry) + p.meanings = make(map[string][]acronymEntry) path := c.String("data_file", filepath.Join("data", "acronyms.txt")) file, err := os.Open(path) if err != nil { @@ -48,16 +54,28 @@ func (p *Acronym) Init(c bot.PluginConfig, _ *storage.DB) error { if line == "" || strings.HasPrefix(line, "#") { continue } - parts := strings.SplitN(line, "|", 2) - if len(parts) != 2 { + parts := strings.SplitN(line, "|", 3) + if len(parts) < 2 { continue } name := cleanExternalText(strings.TrimSpace(parts[0])) expansion := cleanExternalText(strings.TrimSpace(parts[1])) + context := "" + if len(parts) == 3 { + context = cleanExternalText(strings.TrimSpace(parts[2])) + } if !validAcronym(name) || expansion == "" { continue } - p.entries[strings.ToLower(name)] = acronymEntry{Name: name, Expansion: expansion} + if context != "" && !validAcronymContext(context) { + continue + } + key := strings.ToLower(name) + entry := acronymEntry{Name: name, Expansion: expansion, Context: context} + p.meanings[key] = append(p.meanings[key], entry) + if _, exists := p.entries[key]; !exists { + p.entries[key] = entry + } } if err := scanner.Err(); err != nil { return fmt.Errorf("read acronym file: %w", err) @@ -70,25 +88,125 @@ func (p *Acronym) Handle(b *bot.Bot, m bot.Message) bool { if !ok || (strings.ToLower(cmd) != "acronym" && strings.ToLower(cmd) != "acro") { return false } - key := strings.ToLower(strings.TrimSpace(arg)) + raw := strings.TrimSpace(arg) + fields := strings.Fields(raw) + key := "" + context := "" + if raw != "" { + key = strings.ToLower(raw) + if _, exists := p.entries[key]; !exists && len(fields) > 0 { + key = "" + for split := len(fields) - 1; split >= 1; split-- { + candidate := strings.ToLower(strings.Join(fields[:split], " ")) + if _, exists := p.entries[candidate]; exists { + key = candidate + context = strings.ToLower(strings.Join(fields[split:], " ")) + break + } + } + if key == "" { + key = strings.ToLower(fields[0]) + context = strings.ToLower(strings.Join(fields[1:], " ")) + } + } + } entry, exists := p.entries[key] - if key == "" || !exists { + if key == "" { + b.Send(m.ReplyTarget(), ircColor(ircYellow, "usage: !acronym ACRONYM [context]")) + return true + } + if !exists { + if suggestion, ok := p.fuzzySuggestion(key); ok { + message := fmt.Sprintf("%s no exact match for %s; did you mean %s?", ircColor(ircYellow, "[acronym]"), cleanExternalText(fields[0]), ircColor(ircCyan, suggestion.Name)) + b.Send(m.ReplyTarget(), truncateIRCMessage(message, p.maxLength)) + return true + } if len(p.entries) == 0 { b.Send(m.ReplyTarget(), ircColor(ircYellow, "the acronym catalog is unavailable")) } else { - b.Send(m.ReplyTarget(), ircColor(ircYellow, "usage: !acronym ACRONYM (no matching entry found)")) + b.Send(m.ReplyTarget(), ircColor(ircYellow, "no matching acronym found; try !acronym ACRONYM [context]")) } return true } + if context != "" { + entry, exists = p.contextEntry(key, context) + if !exists { + b.Send(m.ReplyTarget(), ircColor(ircYellow, "no matching acronym context; omit the context for the common meaning")) + return true + } + } message := formatAcronymEntry(entry) b.Send(m.ReplyTarget(), truncateIRCMessage(message, p.maxLength)) return true } func formatAcronymEntry(entry acronymEntry) string { + if entry.Context != "" { + return fmt.Sprintf("%s [%s] — %s", ircColor(ircCyan, entry.Name), cleanExternalText(entry.Context), entry.Expansion) + } return fmt.Sprintf("%s — %s", ircColor(ircCyan, entry.Name), entry.Expansion) } +func (p *Acronym) contextEntry(key, requested string) (acronymEntry, bool) { + for _, entry := range p.meanings[key] { + if strings.Contains(strings.ToLower(entry.Context), requested) { + return entry, true + } + } + return acronymEntry{}, false +} + +func (p *Acronym) fuzzySuggestion(query string) (acronymEntry, bool) { + query = normalizeAcronymKey(query) + if query == "" { + return acronymEntry{}, false + } + keys := make([]string, 0, len(p.entries)) + for key := range p.entries { + keys = append(keys, key) + } + sort.Strings(keys) + bestDistance := -1 + bestKey := "" + for _, key := range keys { + distance := levenshtein.ComputeDistance(query, normalizeAcronymKey(key)) + if distance > acronymFuzzyDistance(query, key) { + continue + } + if bestDistance == -1 || distance < bestDistance { + bestDistance = distance + bestKey = key + } + } + if bestKey == "" { + return acronymEntry{}, false + } + return p.entries[bestKey], true +} + +func acronymFuzzyDistance(query, candidate string) int { + length := len([]rune(query)) + if candidateLength := len([]rune(candidate)); candidateLength > length { + length = candidateLength + } + switch { + case length <= 3: + return 1 + case length <= 8: + return 2 + default: + return 3 + } +} + +func normalizeAcronymKey(value string) string { + value = strings.ToLower(strings.TrimSpace(value)) + value = strings.ReplaceAll(value, "-", "") + value = strings.ReplaceAll(value, "_", "") + value = strings.ReplaceAll(value, " ", "") + return value +} + func validAcronym(value string) bool { if value == "" || len([]rune(value)) > 32 || strings.ContainsAny(value, "|\r\n") { return false @@ -100,3 +218,7 @@ func validAcronym(value string) bool { } return true } + +func validAcronymContext(value string) bool { + return value != "" && len([]rune(value)) <= 48 && !strings.ContainsAny(value, "|\r\n") && !strings.ContainsFunc(value, unicode.IsControl) +} diff --git a/plugins/acronym_test.go b/plugins/acronym_test.go index d7a18fc..8bab8aa 100644 --- a/plugins/acronym_test.go +++ b/plugins/acronym_test.go @@ -11,7 +11,7 @@ import ( func TestAcronymLoadsCaseInsensitiveEntries(t *testing.T) { path := t.TempDir() + "/acronyms.txt" - if err := os.WriteFile(path, []byte("# comment\nAPI|Application Programming Interface\nRCA|Root Cause Analysis\ninvalid line\n"), 0600); err != nil { + if err := os.WriteFile(path, []byte("# comment\nAPI|Application Programming Interface|technology\nAPI|Acute Pain Index|medical\nPCI DSS|Payment Card Industry Data Security Standard|security\nRCA|Root Cause Analysis\ninvalid line\n"), 0600); err != nil { t.Fatalf("write acronym file: %v", err) } plugin := &Acronym{} @@ -22,25 +22,54 @@ func TestAcronymLoadsCaseInsensitiveEntries(t *testing.T) { if !ok || entry.Expansion != "Application Programming Interface" { t.Fatalf("loaded entries = %+v", plugin.entries) } + if len(plugin.meanings["api"]) != 2 { + t.Fatalf("API meanings = %+v", plugin.meanings["api"]) + } + if contextEntry, ok := plugin.contextEntry("api", "medical"); !ok || contextEntry.Expansion != "Acute Pain Index" { + t.Fatalf("medical API meaning = %+v, found=%v", contextEntry, ok) + } + if multiWord, ok := plugin.entries["pci dss"]; !ok || multiWord.Expansion == "" { + t.Fatalf("multi-word acronym was not loaded: %+v", plugin.entries) + } if _, ok := plugin.entries["invalid line"]; ok { t.Fatal("malformed acronym entry was loaded") } - if plain := stripPluginIRC(formatAcronymEntry(entry)); !strings.Contains(plain, "API — Application Programming Interface") { + if plain := stripPluginIRC(formatAcronymEntry(entry)); !strings.Contains(plain, "API [technology] — Application Programming Interface") { t.Fatalf("formatted acronym = %q", plain) } } +func TestAcronymFuzzySuggestion(t *testing.T) { + plugin := &Acronym{entries: map[string]acronymEntry{ + "http": {Name: "HTTP", Expansion: "Hypertext Transfer Protocol"}, + "https": {Name: "HTTPS", Expansion: "Hypertext Transfer Protocol Secure"}, + }} + entry, ok := plugin.fuzzySuggestion("htpp") + if !ok || entry.Name != "HTTP" { + t.Fatalf("fuzzy suggestion = %+v, found=%v", entry, ok) + } + if _, ok := plugin.fuzzySuggestion("zzzzzz"); ok { + t.Fatal("unexpected fuzzy suggestion for unrelated query") + } +} + func TestBundledAcronymCatalogIsBroadAndValid(t *testing.T) { plugin := &Acronym{} if err := plugin.Init(bot.PluginConfig{"data_file": filepath.Join("..", "data", "acronyms.txt")}, nil); err != nil { t.Fatalf("Init returned error: %v", err) } - if len(plugin.entries) < 150 { + if len(plugin.entries) < 350 { t.Fatalf("bundled acronym catalog has only %d entries", len(plugin.entries)) } - for _, key := range []string{"api", "cvss", "dnssec", "ircv3", "mttr", "rbac", "sre", "tls", "xss"} { + for _, key := range []string{"api", "ceo", "dna", "gpa", "http", "ircv3", "lol", "mri", "nato", "roi", "sre", "tls", "xss"} { if entry, ok := plugin.entries[key]; !ok || entry.Expansion == "" { t.Fatalf("bundled catalog missing usable %q entry", key) } } + if len(plugin.meanings["api"]) < 2 { + t.Fatal("bundled catalog should preserve multiple API meanings") + } + if entry, ok := plugin.contextEntry("api", "technology"); !ok || entry.Expansion != "Application Programming Interface" { + t.Fatalf("bundled technology API meaning = %+v, found=%v", entry, ok) + } }