657 lines
16 KiB
Go
657 lines
16 KiB
Go
package libnmdc
|
|
|
|
import (
|
|
"encoding/base32"
|
|
"fmt"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type adcState int
|
|
|
|
const (
|
|
adcStateProtocol adcState = 0
|
|
adcStateIdentify adcState = 1
|
|
adcStateVerify adcState = 2
|
|
adcStateNormal adcState = 3
|
|
adcStateData adcState = 4
|
|
)
|
|
|
|
type AdcProtocol struct {
|
|
hc *HubConnection
|
|
state adcState
|
|
sid, pid, cid string // all in base32 encoding
|
|
supports map[string]struct{}
|
|
}
|
|
|
|
const (
|
|
// extra extensions that aren't flagged in SUPPORTS
|
|
adcSeparateApVe string = "SEPARATE_AP_VE" // we invented this string
|
|
)
|
|
|
|
func NewAdcProtocol(hc *HubConnection) Protocol {
|
|
proto := AdcProtocol{
|
|
hc: hc,
|
|
state: adcStateProtocol,
|
|
supports: make(map[string]struct{}),
|
|
}
|
|
|
|
rxPid := regexp.MustCompile("^[A-Z2-7]{39}$")
|
|
if !rxPid.MatchString(hc.Hco.AdcPID) {
|
|
hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Invalid custom PID, regenerating"})
|
|
hc.Hco.AdcPID = NewPID()
|
|
}
|
|
|
|
pid_base32 := hc.Hco.AdcPID
|
|
cid_base32, err := proto.pid2cid(pid_base32)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
proto.cid = cid_base32
|
|
proto.pid = pid_base32
|
|
|
|
// Start logging in
|
|
hc.SayRaw("HSUP ADBASE ADTIGR\n")
|
|
|
|
return &proto
|
|
}
|
|
|
|
func (this *AdcProtocol) pid2cid(pid_base32 string) (string, error) {
|
|
|
|
pid_raw, err := base32.StdEncoding.DecodeString(pid_base32 + "=")
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
cid_raw := Tiger(string(pid_raw))
|
|
cid_base32 := Base32(cid_raw)
|
|
|
|
return cid_base32, nil
|
|
}
|
|
|
|
func (this *AdcProtocol) SID2Nick(sid string) (string, bool) {
|
|
this.hc.usersMut.Lock()
|
|
defer this.hc.usersMut.Unlock()
|
|
|
|
nick, ok := this.hc.userSIDs[sid]
|
|
return nick, ok
|
|
}
|
|
|
|
func (this *AdcProtocol) Nick2SID(targetNick string) (string, bool) {
|
|
this.hc.usersMut.Lock()
|
|
defer this.hc.usersMut.Unlock()
|
|
|
|
for sid, nick := range this.hc.userSIDs {
|
|
if nick == targetNick {
|
|
return sid, true
|
|
}
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func (this *AdcProtocol) ProcessCommand(msg string) {
|
|
|
|
if len(msg) == 0 {
|
|
return
|
|
}
|
|
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: msg})
|
|
|
|
parts := strings.Split(msg, " ")
|
|
switch parts[0] {
|
|
|
|
case "ISUP":
|
|
if !(this.state == adcStateProtocol || this.state == adcStateNormal) {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
for _, supportflag := range parts[1:] {
|
|
if len(supportflag) < 2 {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
|
|
if supportflag[0:2] == "AD" {
|
|
this.supports[supportflag[2:]] = struct{}{}
|
|
} else if supportflag[0:2] == "RM" {
|
|
delete(this.supports, supportflag[2:])
|
|
} else {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
}
|
|
if this.state == adcStateProtocol {
|
|
this.state = adcStateIdentify
|
|
}
|
|
|
|
case "ISID":
|
|
if this.state != adcStateIdentify {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
this.sid = parts[1]
|
|
|
|
// State transition IDENTIFY --> VERIFY and send our own info
|
|
this.hc.SayRaw("BINF " + this.escape(this.sid) + " " + this.ourINFO(true) + "\n")
|
|
this.state = adcStateVerify
|
|
|
|
case "IINF":
|
|
// Hub telling information about itself
|
|
// ADCH++ sends this once we are successfully logged in
|
|
|
|
flags, err := this.parts2flags(parts[1:])
|
|
if err != nil {
|
|
this.logError(err)
|
|
return
|
|
}
|
|
|
|
if flags["CT"] != "32" {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
|
|
err = this.handleHubInfo(flags)
|
|
if err != nil {
|
|
this.logError(err)
|
|
return
|
|
}
|
|
|
|
if this.state != adcStateNormal {
|
|
this.enterNormalState() // successful login
|
|
}
|
|
|
|
case "BINF":
|
|
if this.state != adcStateNormal {
|
|
this.enterNormalState() // successful login
|
|
}
|
|
|
|
sid := parts[1]
|
|
flags, err := this.parts2flags(parts[2:])
|
|
if err != nil {
|
|
this.logError(err)
|
|
return
|
|
}
|
|
|
|
uinfo, err := this.handleUserInfo(flags)
|
|
if err != nil {
|
|
this.logError(err)
|
|
return
|
|
}
|
|
|
|
// Log this user in, and associate this SID with this user
|
|
this.hc.usersMut.Lock()
|
|
defer this.hc.usersMut.Unlock()
|
|
|
|
newNick := uinfo.Nick
|
|
|
|
oldNick, sidExists := this.hc.userSIDs[sid]
|
|
|
|
handleNewUser := func() {
|
|
// Install this SID as pointing to this nick
|
|
this.hc.userSIDs[sid] = uinfo.Nick
|
|
|
|
// Check if this nick was in use by any other SID already
|
|
for otherSid, otherSidNick := range this.hc.userSIDs {
|
|
if otherSidNick == newNick && otherSid != sid {
|
|
this.hc.processEvent(HubEvent{
|
|
EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN,
|
|
Message: fmt.Sprintf("Hub connection corrupted (duplicate SIDs '%s' and '%s' for nick '%s'), disconnecting", sid, otherSid, newNick),
|
|
})
|
|
this.hc.Disconnect()
|
|
return
|
|
}
|
|
}
|
|
|
|
// Notifications
|
|
this.hc.users[newNick] = *uinfo
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: newNick})
|
|
}
|
|
|
|
if sidExists && oldNick != newNick {
|
|
// Nick change = delete all trace of this user first, treat as new
|
|
|
|
delete(this.hc.users, oldNick)
|
|
delete(this.hc.userSIDs, sid)
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: oldNick})
|
|
handleNewUser()
|
|
|
|
} else if sidExists && oldNick == newNick {
|
|
// Updating existing user
|
|
this.hc.users[newNick] = *uinfo
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: newNick})
|
|
|
|
} else if !sidExists {
|
|
// User joined
|
|
handleNewUser()
|
|
}
|
|
|
|
case "IMSG":
|
|
// General message from the hub
|
|
if len(parts) < 2 {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: this.unescape(parts[1])})
|
|
|
|
case "ISTA":
|
|
// Error message from the hub
|
|
if len(parts) < 3 {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
|
|
code, _ := strconv.Atoi(parts[1])
|
|
msg := this.unescape(parts[2])
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: this.ErrorMessage(code, msg)})
|
|
|
|
case "IQUI":
|
|
// Error message from the hub
|
|
// IQUI V3M6 DI1 MSNick\staken,\splease\spick\sanother\sone TL-1
|
|
if len(parts) < 2 {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
|
|
sid := parts[1]
|
|
|
|
flags, err := this.parts2flags(parts[2:])
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
if sid == this.sid {
|
|
if msg, ok := flags["MS"]; ok {
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: "The hub is closing our connection because: " + this.unescape(msg)})
|
|
} else {
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Message: "The hub is closing our connection"})
|
|
}
|
|
} else {
|
|
this.hc.usersMut.Lock()
|
|
defer this.hc.usersMut.Unlock()
|
|
otherSidNick, ok := this.hc.userSIDs[sid]
|
|
if ok {
|
|
delete(this.hc.userSIDs, sid)
|
|
delete(this.hc.users, otherSidNick)
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: otherSidNick})
|
|
} else {
|
|
// ??
|
|
this.logError(fmt.Errorf("An unknown user quit the hub (SID=%s)", sid))
|
|
}
|
|
}
|
|
|
|
case "BMSG":
|
|
// Message from a user
|
|
// BMSG ZVF4 hi
|
|
if len(parts) < 3 {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
|
|
sid := this.unescape(parts[1])
|
|
msg := this.unescape(parts[2])
|
|
|
|
this.hc.usersMut.Lock()
|
|
defer this.hc.usersMut.Unlock()
|
|
nick, ok := this.hc.userSIDs[sid]
|
|
if !ok {
|
|
this.logError(fmt.Errorf("Recieved message from unknown SID '%s'", sid))
|
|
return
|
|
}
|
|
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: nick, Message: msg})
|
|
|
|
case "IGPA":
|
|
// Password is needed
|
|
// IGPA 7EIAAAECLMAAAPJQAAADQQYAAAWAYAAAKVFQAAF6EAAAAAYFAAAA
|
|
// HPAS LZDIJOTZDPWHINHGPT5RHT6WLU7DRME7DQO2O3Q
|
|
if len(parts) < 2 {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
|
|
/*
|
|
For GPA/PAS, assuming that '12345' is the random data supplied in GPA, then;
|
|
PAS = Base32( Hash( password + '12345' ) )
|
|
|
|
GPA: The data parameter is at least 24 random bytes (base32 encoded).
|
|
*/
|
|
|
|
data_base32 := parts[1]
|
|
if len(data_base32)%8 != 0 {
|
|
data_base32 += strings.Repeat("=", 8-(len(data_base32)%8))
|
|
}
|
|
|
|
data_raw, err := base32.StdEncoding.DecodeString(data_base32)
|
|
if err != nil {
|
|
this.logError(err)
|
|
return
|
|
}
|
|
|
|
resp := Base32(Tiger(this.hc.Hco.NickPassword + string(data_raw)))
|
|
this.hc.SayRaw("HPAS " + resp + "\n")
|
|
|
|
case "EMSG":
|
|
// Private message from other user
|
|
// EMSG I5RO FMWH test\spm PMI5RO
|
|
// EMSG sender recip==us message [flags...]
|
|
if len(parts) < 4 {
|
|
this.malformed(parts)
|
|
return
|
|
}
|
|
|
|
if parts[2] != this.sid {
|
|
this.logError(fmt.Errorf("Recieved a PM intended for someone else (got SID=%s expected SID=%s)", parts[2], this.sid))
|
|
return
|
|
}
|
|
|
|
senderSid := parts[1]
|
|
senderNick, ok := this.SID2Nick(parts[1])
|
|
if !ok {
|
|
this.logError(fmt.Errorf("Recieved a PM from an unknown user (SID=%s)", senderSid))
|
|
return
|
|
}
|
|
|
|
msg := this.unescape(parts[3])
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: senderNick, Message: msg})
|
|
|
|
// Ignored messages
|
|
// ````````````````
|
|
|
|
case "DCTM": // Client-client ConnectToMe
|
|
case "BSCH": // Search
|
|
|
|
default:
|
|
this.malformed(parts)
|
|
}
|
|
}
|
|
|
|
func (this *AdcProtocol) infoFlagsFor(u *UserInfo) map[string]string {
|
|
parts := map[string]string{
|
|
"NI": u.Nick,
|
|
"SS": fmt.Sprintf("%d", u.ShareSize),
|
|
"SF": fmt.Sprintf("%d", u.SharedFiles),
|
|
"US": fmt.Sprintf("%d", u.UploadSpeedBps),
|
|
"DS": fmt.Sprintf("%d", u.DownloadSpeedBps),
|
|
"SL": fmt.Sprintf("%d", u.Slots),
|
|
"HN": fmt.Sprintf("%d", u.HubsUnregistered),
|
|
"HR": fmt.Sprintf("%d", u.HubsRegistered),
|
|
"HO": fmt.Sprintf("%d", u.HubsOperator),
|
|
}
|
|
|
|
if _, ok := this.supports[adcSeparateApVe]; ok {
|
|
parts["AP"] = u.ClientTag
|
|
parts["VE"] = u.ClientVersion
|
|
} else {
|
|
parts["VE"] = fmt.Sprintf("%s %s", u.ClientVersion, u.ClientTag)
|
|
}
|
|
|
|
// Do not send the hub a CT (it decides what type we are)
|
|
|
|
return parts
|
|
}
|
|
|
|
func (this *AdcProtocol) ourINFO(includePid bool) string {
|
|
parts := this.infoFlagsFor(this.hc.Hco.Self)
|
|
parts["ID"] = this.cid
|
|
if includePid {
|
|
parts["PD"] = this.pid
|
|
}
|
|
|
|
ret := ""
|
|
for k, v := range parts {
|
|
ret += " " + k + this.escape(v)
|
|
}
|
|
return ret[1:]
|
|
}
|
|
|
|
func (this *AdcProtocol) parts2flags(parts []string) (map[string]string, error) {
|
|
flags := make(map[string]string, len(parts))
|
|
for _, flag := range parts {
|
|
if len(flag) < 2 {
|
|
return nil, fmt.Errorf("Malformed flag '%s'", flag)
|
|
}
|
|
flags[flag[0:2]] = this.unescape(flag[2:])
|
|
}
|
|
|
|
return flags, nil
|
|
}
|
|
|
|
func (this *AdcProtocol) handleHubInfo(flags map[string]string) error {
|
|
if flags["CT"] != "32" {
|
|
return fmt.Errorf("Expected CT==32")
|
|
}
|
|
|
|
// IINF DEADCH++\sTest\shub VE2.12.1\s(r"[unknown]")\sRelease HI1 NIADCH++ APADCH++ CT32
|
|
// AP: extension 3.24 "Application and version separation in INF"
|
|
// HI:
|
|
|
|
// Hub properties updated
|
|
|
|
// Special SUPPORT that is only indicated in IINF
|
|
if _, ok := flags["AP"]; ok {
|
|
this.supports[adcSeparateApVe] = struct{}{}
|
|
}
|
|
|
|
// Hub's name is in "NI", hub description in "DE"
|
|
hubName, ok := flags["NI"]
|
|
if ok {
|
|
if hubDesc, ok := flags["DE"]; ok && len(hubDesc) > 0 {
|
|
hubName += " - " + hubDesc
|
|
}
|
|
|
|
this.hc.HubName = hubName
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: this.hc.HubName})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (this *AdcProtocol) handleUserInfo(flags map[string]string) (*UserInfo, error) {
|
|
|
|
// User MyINFO
|
|
// BINF GUPR IDFEARIFD33NTGC4YBEZ3UFQS5R4ZXXTFL2QN2GRY PDZMIFLG5EKZG3BDRRMIJPG7ARNA6KW3JVIH3DF7Q NIivysaur5 SL3 FS3 SS0 SF0 HN1 HR0 HO0 VEEiskaltDC++\s2.2.9 US2621440 KPSHA256/3UPRORG4BLJ4CG6TO6R3G75A67LXOGD437NALQALRWJF6XBOECTA I40.0.0.0 U418301
|
|
// BINF GUPR I4172.17.0.1 U418301 IDFEARIFD33NTGC4YBEZ3UFQS5R4ZXXTFL2QN2GRY VEEiskaltDC++\s2.2.9 SF0 NIivysaur5 SL3 HN1 HO0 KPSHA256/3UPRORG4BLJ4CG6TO6R3G75A67LXOGD437NALQALRWJF6XBOECTA HR0 FS3 SS0 US2621440 SUSEGA,ADC0,TCP4,UDP4
|
|
// TODO
|
|
|
|
u := UserInfo{}
|
|
for prop, val := range flags {
|
|
switch prop {
|
|
case "ID":
|
|
u.CID = val
|
|
case "PD":
|
|
// ignore PID - it will only appear if we're talking about our own user
|
|
case "NI":
|
|
u.Nick = val
|
|
case "SL":
|
|
u.Slots, _ = strconv.ParseUint(val, 10, 64)
|
|
case "SS":
|
|
u.ShareSize, _ = strconv.ParseUint(val, 10, 64)
|
|
case "SF":
|
|
u.SharedFiles, _ = strconv.ParseUint(val, 10, 64)
|
|
case "HN":
|
|
u.HubsUnregistered, _ = strconv.ParseUint(val, 10, 64)
|
|
case "HR":
|
|
u.HubsRegistered, _ = strconv.ParseUint(val, 10, 64)
|
|
case "HO":
|
|
u.HubsOperator, _ = strconv.ParseUint(val, 10, 64)
|
|
case "US":
|
|
u.UploadSpeedBps, _ = strconv.ParseUint(val, 10, 64)
|
|
case "DS":
|
|
u.DownloadSpeedBps, _ = strconv.ParseUint(val, 10, 64)
|
|
case "KP":
|
|
u.Keyprint = val
|
|
case "I4":
|
|
u.IPv4Address = val
|
|
case "I6":
|
|
u.IPv6Address = val
|
|
case "U4":
|
|
u.IPv4UDPPort, _ = strconv.ParseUint(val, 10, 64)
|
|
case "U6":
|
|
u.IPv6UDPPort, _ = strconv.ParseUint(val, 10, 64)
|
|
case "SU":
|
|
u.SupportFlags = make(map[string]struct{})
|
|
for _, supportFlag := range strings.Split(val, ",") {
|
|
u.SupportFlags[supportFlag] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
// VE / AP
|
|
AP, hasAP := flags["AP"]
|
|
VE, hasVE := flags["VE"]
|
|
if hasAP && hasVE {
|
|
u.ClientTag = AP
|
|
u.ClientVersion = VE
|
|
} else if hasAP && !hasVE {
|
|
u.ClientTag, u.ClientVersion = this.getAPVEFromSingle(AP)
|
|
} else if !hasAP && hasVE {
|
|
u.ClientTag, u.ClientVersion = this.getAPVEFromSingle(VE)
|
|
}
|
|
|
|
if u.Nick == "" {
|
|
return nil, fmt.Errorf("Malformed user missing nick")
|
|
}
|
|
|
|
return &u, nil
|
|
}
|
|
|
|
func (this *AdcProtocol) getAPVEFromSingle(term string) (string, string) {
|
|
words := strings.Split(term, " ")
|
|
if len(words) > 1 {
|
|
return strings.Join(words[0:len(words)-1], " "), words[len(words)-1]
|
|
} else {
|
|
return term, "0"
|
|
}
|
|
}
|
|
|
|
func (this *AdcProtocol) enterNormalState() {
|
|
this.state = adcStateNormal
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED})
|
|
this.hc.State = CONNECTIONSTATE_CONNECTED
|
|
}
|
|
|
|
func (this *AdcProtocol) malformed(parts []string) {
|
|
this.logError(fmt.Errorf("Ignoring malformed, unhandled, or out-of-state protocol command %v", parts))
|
|
}
|
|
|
|
func (this *AdcProtocol) logError(e error) {
|
|
this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Protocol error: " + e.Error()})
|
|
}
|
|
|
|
func (this *AdcProtocol) escape(plaintext string) string {
|
|
// The string "\s" escapes space, "\n" newline and "\\" backslash. This version of the protocol reserves all other escapes for future use; any message containing unknown escapes must be discarded.
|
|
v1 := strings.Replace(plaintext, `\`, `\\`, -1)
|
|
v2 := strings.Replace(v1, "\n", `\n`, -1)
|
|
return strings.Replace(v2, " ", `\s`, -1)
|
|
}
|
|
|
|
func (this *AdcProtocol) unescape(encoded string) string {
|
|
v1 := strings.Replace(encoded, `\s`, " ", -1)
|
|
v2 := strings.Replace(v1, `\n`, "\n", -1)
|
|
return strings.Replace(v2, `\\`, `\`, -1)
|
|
}
|
|
|
|
func (this *AdcProtocol) SayPublic(msg string) {
|
|
this.hc.SayRaw("BMSG " + this.sid + " " + this.escape(msg) + "\n")
|
|
}
|
|
|
|
func (this *AdcProtocol) SayPrivate(user, message string) {
|
|
if sid, ok := this.Nick2SID(user); ok {
|
|
this.hc.SayRaw("DMSG " + this.sid + " " + sid + " " + this.escape(message) + "\n")
|
|
} else {
|
|
this.logError(fmt.Errorf("Unknown user '%s'", user))
|
|
}
|
|
}
|
|
|
|
func (this *AdcProtocol) ProtoMessageSeparator() string {
|
|
return "\n"
|
|
}
|
|
|
|
func (this *AdcProtocol) ErrorMessage(code int, msg string) string {
|
|
severity := code / 100
|
|
category := (code % 100) / 10
|
|
cat_sub := (code % 100)
|
|
|
|
formatSeverity := func(severity int) string {
|
|
switch severity {
|
|
case 0:
|
|
return "OK"
|
|
case 1:
|
|
return "Warning"
|
|
case 2:
|
|
return "Error"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
formatCategory := func(category int) string {
|
|
switch category {
|
|
case 0:
|
|
return ""
|
|
case 1:
|
|
return "Hub not accepting users"
|
|
case 2:
|
|
return "Login failed"
|
|
case 3:
|
|
return "Access denied"
|
|
case 4:
|
|
return "Protocol error"
|
|
case 5:
|
|
return "Transfer error"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
formatCatSub := func(cat_sub int) string {
|
|
switch cat_sub {
|
|
case 11:
|
|
return "Hub is full"
|
|
case 12:
|
|
return "Hub is disabled"
|
|
case 21:
|
|
return "Invalid nick"
|
|
case 22:
|
|
return "Nick is already in use"
|
|
case 23:
|
|
return "Invalid password"
|
|
case 24:
|
|
return "CID already connected"
|
|
case 25:
|
|
return "Access denied"
|
|
case 26:
|
|
return "Registered users only"
|
|
case 27:
|
|
return "Invalid PID"
|
|
case 31:
|
|
return "Permanently banned"
|
|
case 32:
|
|
return "Temporarily banned"
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
parts := make([]string, 0, 4)
|
|
if fs := formatSeverity(severity); len(fs) > 0 {
|
|
parts = append(parts, fs)
|
|
}
|
|
if fc := formatCategory(category); len(fc) > 0 {
|
|
parts = append(parts, fc)
|
|
}
|
|
if fcs := formatCatSub(cat_sub); len(fcs) > 0 {
|
|
parts = append(parts, fcs)
|
|
}
|
|
if len(msg) > 0 {
|
|
parts = append(parts, msg)
|
|
}
|
|
|
|
return strings.Join(parts, ": ") + fmt.Sprintf(" (code %d)", code)
|
|
|
|
}
|