diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 0000000..7f39efa --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,33 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + name = "code.ivysaur.me/libnmdc" + packages = ["."] + revision = "b58bed9c3c4612abb2b1d67c7bc25863c5ceef28" + version = "v0.17.0" + +[[projects]] + branch = "master" + name = "github.com/cxmcc/tiger" + packages = ["."] + revision = "bde35e2713d7f674987c2ecb21a6b0fc33749516" + +[[projects]] + name = "github.com/go-telegram-bot-api/telegram-bot-api" + packages = ["."] + revision = "0e0af0c480ea98e982d5f4d45fb39577c6ab1e3e" + version = "v4.6.2" + +[[projects]] + name = "github.com/technoweenie/multipartstreamer" + packages = ["."] + revision = "a90a01d73ae432e2611d178c18367fbaa13e0154" + version = "v1.0.1" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "75039ebfc2a88d68b24c32495279190af07226639cdd9acf711588a292e97f51" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 0000000..13c491c --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,30 @@ + +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +[[constraint]] + name = "github.com/go-telegram-bot-api/telegram-bot-api" + version = "4.6.2" + +[[constraint]] + name = "code.ivysaur.me/libnmdc" + version = "0.17.0" diff --git a/vendor/code.ivysaur.me/libnmdc/.hgignore b/vendor/code.ivysaur.me/libnmdc/.hgignore new file mode 100644 index 0000000..5c7f679 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/.hgignore @@ -0,0 +1,14 @@ +mode:regex + +# Compilation output +\.(?:exe|a)$ +^pkg/ + +# Dependencies +^src/(?:github.com|gopkg.in|golang.org)/ + +# Scratch space +^src/nmdc/ + +# Binary release artefacts +/?__dist/ diff --git a/vendor/code.ivysaur.me/libnmdc/.hgtags b/vendor/code.ivysaur.me/libnmdc/.hgtags new file mode 100644 index 0000000..48a284e --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/.hgtags @@ -0,0 +1,21 @@ +945ab4b16d05aa084f71bf5da9a3f687e0ec8bbd v0.1.0 +02a360e95480b97ddad83add5db48b2766339a99 nmdc-log-service-1.0.0 +137c1b65039e03c80379826a6efdfd808f6fbc8f v0.2.0 +d8b64d5527c2a5e4d76872e5bc3d69f7646135c6 v0.3.0 +fca41372e400853775b02e951f9db91d87f41adb nmdc-log-service-1.0.1 +050b424a7c5d5a27c9323c8810f3afbead1f5b96 v0.4.0 +da9f123633f9c28be6435ed7898139665d4c39d9 nmdc-log-service-1.0.2 +75a78f6a78f249a2cd8aa3d29f7e5e6319b4e03b v0.5.0 +4116422bb10229d887f9296970a166fa1ef8c5fd nmdc-log-service-1.0.3 +cb86f3a40115cc46f450c0c83fd9b9d3b740e820 nmdc-log-service-1.0.4 +cb86f3a40115cc46f450c0c83fd9b9d3b740e820 v0.6.0 +71343a2c641a438206d30ea7e75dc89a11dbef00 v0.7.0 +b0e57a5fcffdf4102d669db51a3648ddf66a0792 v0.8.0 +e7c2c71ef24b386add728fad35fff4a996fccbac v0.9.0 +3ecc037cf2d7080572fe87c2e39ecd153fb0e947 v0.10.0 +5149ffe70ea8475e480b682345b31aa45a3352db v0.11.0 +22b156a6fc2f6161765317f4ec9ab3731a26e0e2 v0.12.0 +3ee0f4ea5142d66079a9500bdcd48a53bdcf362f v0.13.0 +6422ed687cd308c339b6dc188bbe1034ed93f893 v0.14.0 +84fb191007017862ffc37af68dcdace5d8c06eee v0.15.0 +a48811ff2cfe5246c26801cf27da25bf56fec8bb v0.16.0 diff --git a/vendor/code.ivysaur.me/libnmdc/AdcProtocol.go b/vendor/code.ivysaur.me/libnmdc/AdcProtocol.go new file mode 100644 index 0000000..d423ee8 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/AdcProtocol.go @@ -0,0 +1,715 @@ +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 ADUCMD\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.SayInfo() + 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 + } + + // Log this user in, and associate this SID with this user + this.hc.usersMut.Lock() + defer this.hc.usersMut.Unlock() + + oldNick, sidExists := this.hc.userSIDs[sid] + + uinfo := UserInfo{} + if sidExists { + uinfo_lookup, ok := this.hc.users[oldNick] + if !ok { + // Shouldn't happen + this.hc.processEvent(HubEvent{ + EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, + Message: fmt.Sprintf("Hub connection corrupted (missing info for SID='%s' nick='%s'), disconnecting", sid, oldNick), + }) + this.hc.Disconnect() + return + } + + uinfo = uinfo_lookup + } + + this.updateUserInfo(&uinfo, flags) + newNick := uinfo.Nick + if len(newNick) == 0 { + this.logError(fmt.Errorf("Zero-length nick for user (SID='%s')", sid)) + } + + shouldHandleNewUser := false + 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}) + shouldHandleNewUser = true + + } 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 + shouldHandleNewUser = true + } + + // + + if shouldHandleNewUser { + // 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}) + } + + 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}) + + case "ICMD": + // Usercommand + // ICMD ADCH++/About\sthis\shub TTHMSG\s+about\n CT3 + + if len(parts) < 2 { + this.malformed(parts) + return + } + + uc := UserCommand{ + Message: this.unescape(parts[1]), + Type: USERCOMMAND_TYPE_RAW, // default + } + + flags, err := this.parts2flags(parts[2:]) + if err != nil { + this.malformed(parts) + return + } + + if ct, ok := flags["CT"]; ok { + ct64, _ := strconv.ParseUint(ct, 10, 64) + uc.Context = UserCommandContext(ct64) + } + + if tt, ok := flags["TT"]; ok { + uc.Command = tt + } + + if sp, ok := flags["SP"]; ok && sp == "1" { + uc.Type = USERCOMMAND_TYPE_SEPARATOR + } + + if co, ok := flags["CO"]; ok && co == "1" { + uc.Type = USERCOMMAND_TYPE_NICKLIMITED // "Constrained" in ADC parlance + } + + if rm, ok := flags["RM"]; ok && rm == "1" { + uc.RemoveThis = true + } + + this.hc.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uc}) + + // 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.ClientTag, u.ClientVersion) + } + + // 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) SayInfo() { + this.hc.SayRaw("BINF " + this.escape(this.sid) + " " + this.ourINFO(true) + "\n") +} + +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) updateUserInfo(u *UserInfo, flags map[string]string) { + + // 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 + // Or maybe only incremental: + // BINF Z3BA HO1 + // TODO + + 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) + } +} + +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) + +} diff --git a/vendor/code.ivysaur.me/libnmdc/AutodetectProtocol.go b/vendor/code.ivysaur.me/libnmdc/AutodetectProtocol.go new file mode 100644 index 0000000..3c188f6 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/AutodetectProtocol.go @@ -0,0 +1,90 @@ +package libnmdc + +import ( + "sync" + "time" +) + +type AutodetectProtocol struct { + hc *HubConnection + + realProtoMut sync.Mutex + realProto Protocol +} + +func NewAutodetectProtocol(hc *HubConnection) Protocol { + proto := AutodetectProtocol{ + hc: hc, + realProto: nil, + } + + go proto.timeout() + + return &proto +} + +func (this *AutodetectProtocol) timeout() { + time.Sleep(AUTODETECT_ADC_NMDC_TIMEOUT) + + this.realProtoMut.Lock() + defer this.realProtoMut.Unlock() + + if this.realProto == nil { + this.realProto = NewAdcProtocol(this.hc) + this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Detected ADC protocol"}) + } +} + +func (this *AutodetectProtocol) ProcessCommand(msg string) { + this.realProtoMut.Lock() + defer this.realProtoMut.Unlock() + + if this.realProto == nil { + // We actually got some data using $ as the separator? + // Upgrade to a full NMDC protocol + this.realProto = NewNmdcProtocol(this.hc) + this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Detected NMDC protocol"}) + } + + this.realProto.ProcessCommand(msg) +} + +func (this *AutodetectProtocol) SayPublic(msg string) { + this.realProtoMut.Lock() + defer this.realProtoMut.Unlock() + + if this.realProto == nil { + this.realProto = NewNmdcProtocol(this.hc) + } + this.realProto.SayPublic(msg) +} + +func (this *AutodetectProtocol) SayPrivate(user, message string) { + this.realProtoMut.Lock() + defer this.realProtoMut.Unlock() + + if this.realProto == nil { + this.realProto = NewNmdcProtocol(this.hc) + } + this.realProto.SayPrivate(user, message) +} + +func (this *AutodetectProtocol) SayInfo() { + this.realProtoMut.Lock() + defer this.realProtoMut.Unlock() + + if this.realProto == nil { + this.realProto = NewNmdcProtocol(this.hc) + } + this.realProto.SayInfo() +} + +func (this *AutodetectProtocol) ProtoMessageSeparator() string { + this.realProtoMut.Lock() + defer this.realProtoMut.Unlock() + + if this.realProto == nil { + return "|" + } + return this.realProto.ProtoMessageSeparator() +} diff --git a/vendor/code.ivysaur.me/libnmdc/ConnectionMode.go b/vendor/code.ivysaur.me/libnmdc/ConnectionMode.go new file mode 100644 index 0000000..00f0105 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/ConnectionMode.go @@ -0,0 +1,29 @@ +package libnmdc + +import ( + "fmt" +) + +type ConnectionMode rune + +const ( + CONNECTIONMODE_ACTIVE ConnectionMode = 'A' // 65 + CONNECTIONMODE_PASSIVE ConnectionMode = 'P' // 49 + CONNECTIONMODE_SOCKS5 ConnectionMode = '5' // 53 +) + +func (this ConnectionMode) String() string { + switch this { + case CONNECTIONMODE_ACTIVE: + return "Active" + + case CONNECTIONMODE_PASSIVE: + return "Passive" + + case CONNECTIONMODE_SOCKS5: + return "SOCKS5" + + default: + return fmt.Sprintf("ConnectionMode(\"%s\")", string(this)) + } +} diff --git a/vendor/code.ivysaur.me/libnmdc/ConnectionState.go b/vendor/code.ivysaur.me/libnmdc/ConnectionState.go new file mode 100644 index 0000000..fbb163e --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/ConnectionState.go @@ -0,0 +1,40 @@ +package libnmdc + +import ( + "net" +) + +type ConnectionState int + +const ( + CONNECTIONSTATE_DISCONNECTED = 1 + CONNECTIONSTATE_CONNECTING = 2 // Handshake in progress + CONNECTIONSTATE_CONNECTED = 3 +) + +func (cs ConnectionState) String() string { + switch cs { + case CONNECTIONSTATE_DISCONNECTED: + return "Disconnected" + case CONNECTIONSTATE_CONNECTING: + return "Connecting" + case CONNECTIONSTATE_CONNECTED: + return "Connected" + default: + return "?" + } +} + +func checkIsNetTimeout(err error) bool { + if err == nil { + return false + } + + switch err.(type) { + case net.Error: + return err.(net.Error).Timeout() + + default: + return false + } +} diff --git a/vendor/code.ivysaur.me/libnmdc/Example_test.go b/vendor/code.ivysaur.me/libnmdc/Example_test.go new file mode 100644 index 0000000..9f98b4c --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/Example_test.go @@ -0,0 +1,58 @@ +package libnmdc + +import ( + "fmt" +) + +func ExampleHubConnectionOptions_Connect() { + opts := HubConnectionOptions{ + Address: "127.0.0.1", + Self: NewUserInfo("slowpoke9"), + } + + events := make(chan HubEvent, 0) + hub := ConnectAsync(&opts, events) + + for event := range events { + switch event.EventType { + case EVENT_CONNECTION_STATE_CHANGED: + fmt.Printf("Connection -- %s (%s)\n", event.StateChange, event.Message) + + case EVENT_PUBLIC: + fmt.Printf("Message from '%s': '%s'\n", event.Nick, event.Message) + if event.Message == "how are you" { + hub.SayPublic("good thanks!") + } + + default: + fmt.Printf("%+v\n", event) + + } + } +} + +func ExampleHubConnectionOptions_ConnectSync() { + cb := func(hub *HubConnection, event HubEvent) { + switch event.EventType { + case EVENT_CONNECTION_STATE_CHANGED: + fmt.Printf("Connection -- %s (%s)\n", event.StateChange, event.Message) + + case EVENT_PUBLIC: + fmt.Printf("Message from '%s': '%s'\n", event.Nick, event.Message) + if event.Message == "how are you" { + hub.SayPublic("good thanks!") + } + + default: + fmt.Printf("%+v\n", event) + + } + } + + opts := HubConnectionOptions{ + Address: "127.0.0.1", + Self: NewUserInfo("slowpoke9"), + } + + ConnectSync(&opts, cb) // blocking +} diff --git a/vendor/code.ivysaur.me/libnmdc/Gopkg.lock b/vendor/code.ivysaur.me/libnmdc/Gopkg.lock new file mode 100644 index 0000000..32350dc --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/Gopkg.lock @@ -0,0 +1,15 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/cxmcc/tiger" + packages = ["."] + revision = "bde35e2713d7f674987c2ecb21a6b0fc33749516" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "c88ee670a5600b482019325b6d6633bb6b5fe789596dc29ef809aa7bb013927b" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/vendor/code.ivysaur.me/libnmdc/Gopkg.toml b/vendor/code.ivysaur.me/libnmdc/Gopkg.toml new file mode 100644 index 0000000..a16c0fb --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/Gopkg.toml @@ -0,0 +1,26 @@ + +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" + + +[[constraint]] + branch = "master" + name = "github.com/cxmcc/tiger" diff --git a/vendor/code.ivysaur.me/libnmdc/HubAddress.go b/vendor/code.ivysaur.me/libnmdc/HubAddress.go new file mode 100644 index 0000000..1847bbf --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/HubAddress.go @@ -0,0 +1,48 @@ +package libnmdc + +import ( + "net/url" + "strings" +) + +type HubAddress string + +func (this *HubAddress) parse() url.URL { + parsed, err := url.Parse(strings.ToLower(string(*this))) + if err != nil || len(parsed.Host) == 0 { + parsed = &url.URL{ + Scheme: "", + Host: string(*this), + } + } + + // Add default port if not specified + if !strings.ContainsRune(parsed.Host, ':') { + parsed.Host = parsed.Host + ":411" + } + + return *parsed +} + +func (this *HubAddress) IsSecure() bool { + parsed := this.parse() + + return parsed.Scheme == "nmdcs" || parsed.Scheme == "dchubs" || parsed.Scheme == "adcs" +} + +func (this *HubAddress) GetHostOnly() string { + return this.parse().Host +} + +func (this *HubAddress) GetProtocol() HubProtocol { + parsed := this.parse() + + switch parsed.Scheme { + case "nmdc", "dchub", "nmdcs", "dchubs": + return HubProtocolNmdc + case "adc", "adcs": + return HubProtocolAdc + default: + return HubProtocolAutodetect + } +} diff --git a/vendor/code.ivysaur.me/libnmdc/HubConnection.go b/vendor/code.ivysaur.me/libnmdc/HubConnection.go new file mode 100644 index 0000000..e1dd29a --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/HubConnection.go @@ -0,0 +1,233 @@ +package libnmdc + +import ( + "crypto/tls" + "fmt" + "net" + "regexp" + "sync" + "time" +) + +type HubConnection struct { + // Supplied parameters + Hco *HubConnectionOptions + + // Current remote status + HubName string + State ConnectionState + + usersMut sync.RWMutex + users map[string]UserInfo + userSIDs map[string]string + + proto Protocol + + // Event callback + processEvent func(HubEvent) + + // Private state + conn net.Conn // this is an interface + connValid bool + autoReconnect bool + lastDataRecieved time.Time +} + +// Thread-safe user accessor. +func (this *HubConnection) Users(cb func(*map[string]UserInfo) error) error { + this.usersMut.Lock() + defer this.usersMut.Unlock() + + return cb(&this.users) +} + +func (this *HubConnection) SayPublic(message string) { + this.proto.SayPublic(message) +} + +func (this *HubConnection) SayPrivate(recipient string, message string) { + this.proto.SayPrivate(recipient, message) +} + +func (this *HubConnection) SayInfo() { + this.proto.SayInfo() +} + +func (this *HubConnection) UserExists(nick string) bool { + this.usersMut.RLock() + defer this.usersMut.RUnlock() + + _, already_existed := this.users[nick] + return already_existed +} + +func (this *HubConnection) UserCount() int { + this.usersMut.RLock() + defer this.usersMut.RUnlock() + + return len(this.users) +} + +func (this *HubConnection) userJoined_NameOnly(nick string) { + if !this.UserExists(nick) { + + this.usersMut.Lock() + this.users[nick] = *NewUserInfo(nick) + this.usersMut.Unlock() // Don't lock over a processEvent boundary + + this.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: nick}) + } +} + +func (this *HubConnection) userJoined_Full(uinf *UserInfo) { + // n.b. also called when we get a replacement MyINFO for someone + this.usersMut.Lock() + _, userExisted := this.users[uinf.Nick] // don't use UserExists as it would deadlock the mutex + this.users[uinf.Nick] = *uinf + this.usersMut.Unlock() // Don't lock over a processEvent boundary + + if !userExisted { + this.processEvent(HubEvent{EventType: EVENT_USER_JOINED, Nick: uinf.Nick}) + } else { + this.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: uinf.Nick}) + } +} + +// SayRaw sends raw bytes over the TCP socket. Callers should add the protocol +// terminating character themselves (e.g. `|` for NMDC). +// Note that protocol messages are transmitted on the caller thread, not from +// any internal libnmdc thread. +func (this *HubConnection) SayRaw(protocolCommand string) error { + if !this.connValid { + return ErrNotConnected + } + + _, err := this.conn.Write([]byte(protocolCommand)) + return err +} + +func (this *HubConnection) SayKeepalive() error { + if !this.connValid { + return ErrNotConnected + } + + return this.SayRaw(this.proto.ProtoMessageSeparator()) +} + +func (this *HubConnection) Disconnect() { + this.autoReconnect = false + if this.conn != nil { + this.conn.Close() + } + // A CONNECTIONSTATE_DISCONNECTED message will be emitted by the worker. +} + +func (this *HubConnection) worker() { + var fullBuffer string + var err error = nil + var nbytes int = 0 + + for { + + // If we're not connected, attempt reconnect + if this.conn == nil { + + fullBuffer = "" // clear + + if this.Hco.Address.IsSecure() { + this.conn, err = tls.Dial("tcp", this.Hco.Address.GetHostOnly(), &tls.Config{ + InsecureSkipVerify: this.Hco.SkipVerifyTLS, + }) + } else { + this.conn, err = net.Dial("tcp", this.Hco.Address.GetHostOnly()) + } + + if err != nil { + this.State = CONNECTIONSTATE_DISCONNECTED + this.connValid = false + this.proto = nil + + } else { + this.State = CONNECTIONSTATE_CONNECTING + this.connValid = true + this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTING}) + this.proto = this.Hco.Address.GetProtocol().Create(this) + + } + } + + // Read from socket into our local buffer (blocking) + if this.connValid { + + readBuff := make([]byte, 1024) + this.conn.SetReadDeadline(time.Now().Add(SEND_KEEPALIVE_EVERY)) + + nbytes, err = this.conn.Read(readBuff) + + if checkIsNetTimeout(err) { + // No data before read deadline + err = nil + + if this.proto == nil { + // Autodetect: switch to ADC + this.proto = NewAdcProtocol(this) + } else { + // Normal + // Send KA packet + err = this.SayKeepalive() + } + } + + if nbytes > 0 { + this.lastDataRecieved = time.Now() + fullBuffer += string(readBuff[0:nbytes]) + } + } + + if this.proto != nil { + rxSeparator := regexp.QuoteMeta(this.proto.ProtoMessageSeparator()) + rxProtocolMessage := regexp.MustCompile(`(?ms)\A[^` + rxSeparator + `]*` + rxSeparator) + + // Attempt to parse a message block + for len(fullBuffer) > 0 { + + // FIXME nmdc + for len(fullBuffer) > 0 && fullBuffer[0] == '|' { + fullBuffer = fullBuffer[1:] + } + + protocolMessage := rxProtocolMessage.FindString(fullBuffer) + if len(protocolMessage) > 0 { + this.proto.ProcessCommand(protocolMessage[:len(protocolMessage)-1]) + fullBuffer = fullBuffer[len(protocolMessage):] + } else { + break + } + } + + if err == nil && time.Now().Sub(this.lastDataRecieved) > RECONNECT_IF_NO_DATA_RECIEVED_IN { + err = fmt.Errorf("No packets recieved since %s, connection presumed lost", this.lastDataRecieved.Format(time.RFC3339)) + } + } + + // Maybe we disconnected + // Perform this check *last*, to ensure we've had a final shot at + // clearing out any queued messages + if err != nil { + this.State = CONNECTIONSTATE_DISCONNECTED + this.conn = nil + this.connValid = false + this.proto = nil + this.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_DISCONNECTED, Message: err.Error()}) + + if this.autoReconnect { + time.Sleep(AUTO_RECONNECT_AFTER) // Wait before reconnect + continue + } else { + return // leave the worker for good + } + } + + } + +} diff --git a/vendor/code.ivysaur.me/libnmdc/HubConnectionOptions.go b/vendor/code.ivysaur.me/libnmdc/HubConnectionOptions.go new file mode 100644 index 0000000..063e775 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/HubConnectionOptions.go @@ -0,0 +1,79 @@ +package libnmdc + +import ( + "crypto/rand" +) + +type HubConnectionOptions struct { + Address HubAddress + SkipVerifyTLS bool // using a negative verb, because bools default to false + SkipAutoReconnect bool // as above + Self *UserInfo + NickPassword string + AdcPID string // blank: autogenerate +} + +func NewPID() string { + pidBytes := make([]byte, 24) + n, err := rand.Read(pidBytes) + if err != nil { + panic(err) // Insufficient cryptographic randomness + } + + if n != 24 { + panic("Insufficient cryptographic randomness") + } + + return Base32(pidBytes) +} + +func (this *HubConnectionOptions) prepareConnection() *HubConnection { + if this.Self.ClientTag == "" { + this.Self.ClientTag = DEFAULT_CLIENT_TAG + this.Self.ClientVersion = DEFAULT_CLIENT_VERSION + } + + // Shouldn't be blank either + if this.Self.ClientVersion == "" { + this.Self.ClientVersion = "0" + } + + if this.AdcPID == "" { + this.AdcPID = NewPID() + } + + hc := HubConnection{ + Hco: this, + HubName: DEFAULT_HUB_NAME, + State: CONNECTIONSTATE_DISCONNECTED, + users: make(map[string]UserInfo), + userSIDs: make(map[string]string), + + autoReconnect: !this.SkipAutoReconnect, + } + + return &hc +} + +// ConnectAsync connects to a hub server, and spawns a background goroutine to handle +// protocol messages. Events will be sent by channel to the supplied onEvent channel, +// the client is responsible for selecting off this. +func ConnectAsync(opts *HubConnectionOptions, onEvent chan HubEvent) *HubConnection { + hc := opts.prepareConnection() + hc.processEvent = func(ev HubEvent) { + onEvent <- ev + } + + go hc.worker() + return hc +} + +// ConnectSync connects to a hub server, and blocks forever to handle protocol messages. +// Client code should supply an event handling function as hco.OnEventSync. +func ConnectSync(opts *HubConnectionOptions, onEvent func(hub *HubConnection, ev HubEvent)) { + hc := opts.prepareConnection() + hc.processEvent = func(ev HubEvent) { + onEvent(hc, ev) + } + hc.worker() +} diff --git a/vendor/code.ivysaur.me/libnmdc/HubEvent.go b/vendor/code.ivysaur.me/libnmdc/HubEvent.go new file mode 100644 index 0000000..3334328 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/HubEvent.go @@ -0,0 +1,25 @@ +package libnmdc + +type HubEventType int + +const ( + EVENT_PUBLIC HubEventType = 1 + EVENT_PRIVATE HubEventType = 2 + EVENT_SYSTEM_MESSAGE_FROM_HUB HubEventType = 3 + EVENT_SYSTEM_MESSAGE_FROM_CONN HubEventType = 4 + EVENT_USER_JOINED HubEventType = 5 + EVENT_USER_PART HubEventType = 6 + EVENT_USER_UPDATED_INFO HubEventType = 7 + EVENT_CONNECTION_STATE_CHANGED HubEventType = 8 + EVENT_HUBNAME_CHANGED HubEventType = 9 + EVENT_DEBUG_MESSAGE HubEventType = 10 + EVENT_USERCOMMAND HubEventType = 11 +) + +type HubEvent struct { + EventType HubEventType + Nick string + Message string + StateChange ConnectionState + UserCommand *UserCommand +} diff --git a/vendor/code.ivysaur.me/libnmdc/NmdcProtocol.go b/vendor/code.ivysaur.me/libnmdc/NmdcProtocol.go new file mode 100644 index 0000000..1e708cb --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/NmdcProtocol.go @@ -0,0 +1,407 @@ +package libnmdc + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +type NmdcProtocol struct { + hc *HubConnection + sentOurHello bool + supports map[string]struct{} + + rxPublicChat *regexp.Regexp + rxIncomingTo *regexp.Regexp + rxUserCommand *regexp.Regexp + rxMyInfo *regexp.Regexp + rxMyInfoNoTag *regexp.Regexp +} + +func NewNmdcProtocol(hc *HubConnection) Protocol { + proto := NmdcProtocol{} + proto.hc = hc + + // With the `m` flag, use \A instead of ^ to anchor to start + // This fixes accidentally finding a better match in the middle of a multi-line message + + proto.rxPublicChat = regexp.MustCompile(`(?ms)\A<([^>]*)> (.*)$`) + proto.rxIncomingTo = regexp.MustCompile(`(?ms)\A([^ ]+) From: ([^ ]+) \$<([^>]*)> (.*)`) + proto.rxUserCommand = regexp.MustCompile(`(?ms)\A(\d+) (\d+)\s?([^\$]*)\$?(.*)`) + + // Format: $ALL $ $$$$ + + HEAD := `(?ms)^\$ALL ([^ ]+) ` + FOOT := `\$.\$([^$]+)\$([^$]*)\$([0-9]*)\$$` + + proto.rxMyInfo = regexp.MustCompile(HEAD + `([^<]*)<(.+?) V:([^,]+),M:(.),H:([0-9]+)/([0-9]+)/([0-9]+),S:([0-9]+)>` + FOOT) + proto.rxMyInfoNoTag = regexp.MustCompile(HEAD + `([^$]*)` + FOOT) // Fallback for no tag + + // Done + return &proto +} + +func (this *NmdcProtocol) ProcessCommand(message string) { + + // Zero-length protocol message + // ```````````````````````````` + if len(message) == 0 { + return + } + + // Public chat + // ``````````` + if this.rxPublicChat.MatchString(message) { + pubchat_parts := this.rxPublicChat.FindStringSubmatch(message) + this.hc.processEvent(HubEvent{EventType: EVENT_PUBLIC, Nick: pubchat_parts[1], Message: this.unescape(pubchat_parts[2])}) + return + } + + // System messages + // ``````````````` + if message[0] != '$' { + this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_HUB, Nick: this.hc.HubName, Message: this.unescape(message)}) + return + } + + // Protocol messages + // ````````````````` + + commandParts := strings.SplitN(message, " ", 2) + switch commandParts[0] { + + case "$Lock": + this.hc.SayRaw("$Supports NoHello NoGetINFO UserCommand UserIP2 QuickList ChatOnly|" + + "$Key " + this.unlock([]byte(commandParts[1])) + "|") + this.sentOurHello = false + + case "$Hello": + if commandParts[1] == this.hc.Hco.Self.Nick && !this.sentOurHello { + this.hc.SayRaw("$Version 1,0091|") + this.hc.SayRaw("$GetNickList|") + this.SayInfo() + this.sentOurHello = true + + } else { + this.hc.userJoined_NameOnly(commandParts[1]) + + } + + case "$HubName": + this.hc.HubName = commandParts[1] + this.hc.processEvent(HubEvent{EventType: EVENT_HUBNAME_CHANGED, Nick: commandParts[1]}) + + case "$ValidateDenide": // sic + if len(this.hc.Hco.NickPassword) > 0 { + this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."}) + } else { + this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Nick already in use."}) + } + + case "$HubIsFull": + this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Hub is full."}) + + case "$BadPass": + this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "Incorrect password."}) + + case "$GetPass": + if len(this.hc.Hco.NickPassword) == 0 { + // We've got a problem. MyPass with no arguments is a syntax error with no message = instant close + // Just drop the connection + this.hc.processEvent(HubEvent{EventType: EVENT_SYSTEM_MESSAGE_FROM_CONN, Message: "This account is passworded."}) + this.hc.Disconnect() + } else { + this.hc.SayRaw("$MyPass " + this.escape(this.hc.Hco.NickPassword) + "|") + } + + case "$Quit": + this.hc.usersMut.Lock() + delete(this.hc.users, commandParts[1]) + this.hc.usersMut.Unlock() // Don't lock over a processEvent boundary + + this.hc.processEvent(HubEvent{EventType: EVENT_USER_PART, Nick: commandParts[1]}) + + case "$MyINFO": + u, err := this.parseMyINFO(commandParts[1]) + if err != nil { + this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: err.Error()}) + return + } + + this.hc.userJoined_Full(u) + + case "$NickList": + nicklist := strings.Split(commandParts[1], "$$") + for _, nick := range nicklist { + if len(nick) > 0 { + this.hc.userJoined_NameOnly(nick) + } + } + + case "$OpList": + oplist := strings.Split(commandParts[1], "$$") + opmap := map[string]struct{}{} + + // Organise/sort the list, and ensure we're not meeting an operator for + // the first time + for _, nick := range oplist { + if len(nick) > 0 { + opmap[nick] = struct{}{} + this.hc.userJoined_NameOnly(nick) // assert existence; noop otherwise + } + } + + // Mark all mentioned nicks as being operators, and all unmentioned nicks + // as being /not/ an operator. (second pass minimises RW mutex use) + func() { + this.hc.usersMut.Lock() + defer this.hc.usersMut.Unlock() + + for nick, userinfo := range this.hc.users { + _, isop := opmap[nick] + + userinfo.IsOperator = isop + this.hc.users[nick] = userinfo + } + }() + + case "$To:": + valid := false + if this.rxIncomingTo.MatchString(commandParts[1]) { + txparts := this.rxIncomingTo.FindStringSubmatch(commandParts[1]) + if txparts[1] == this.hc.Hco.Self.Nick && txparts[2] == txparts[3] { + this.hc.processEvent(HubEvent{EventType: EVENT_PRIVATE, Nick: txparts[2], Message: this.unescape(txparts[4])}) + valid = true + } + } + + if !valid { + this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed private message '" + commandParts[1] + "'"}) + } + + case "$UserIP": + this.hc.usersMut.Lock() + + pairs := strings.Split(commandParts[1], "$$") + notifyOfUpdate := make([]string, 0, len(pairs)) + + nextIPPair: + for _, pair := range pairs { + parts := strings.SplitN(pair, " ", 2) + if len(parts) != 2 { + // ???? + continue nextIPPair + } + + ip2nick := parts[0] + ip2addr := parts[1] + + uinfo, ok := this.hc.users[ip2nick] + if !ok { + this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Recieved IP '" + ip2addr + "' for unknown user '" + ip2nick + "'"}) + continue nextIPPair + } + + if uinfo.IPAddress != ip2addr { + uinfo.IPAddress = ip2addr + notifyOfUpdate = append(notifyOfUpdate, ip2nick) + this.hc.users[ip2nick] = uinfo + } + } + + this.hc.usersMut.Unlock() + + for _, nick := range notifyOfUpdate { + this.hc.processEvent(HubEvent{EventType: EVENT_USER_UPDATED_INFO, Nick: nick}) + } + + case "$ForceMove": + this.hc.Hco.Address = HubAddress(commandParts[1]) + this.hc.conn.Close() // we'll reconnect onto the new address + + case "$UserCommand": + // $UserCommand 1 1 Group chat\New group chat$<%[mynick]> !groupchat_new|| + if this.rxUserCommand.MatchString(commandParts[1]) { + usc := this.rxUserCommand.FindStringSubmatch(commandParts[1]) + + typeInt, _ := strconv.Atoi(usc[1]) + contextInt, _ := strconv.Atoi(usc[2]) + + uscStruct := UserCommand{ + Type: UserCommandType(typeInt), + Context: UserCommandContext(contextInt), + Message: usc[3], + Command: this.unescape(usc[4]), + } + + this.hc.processEvent(HubEvent{EventType: EVENT_USERCOMMAND, UserCommand: &uscStruct}) + + } else { + this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Malformed usercommand '" + commandParts[1] + "'"}) + } + + case "$Supports": + this.supports = make(map[string]struct{}) + for _, s := range strings.Split(commandParts[1], " ") { + this.supports[s] = struct{}{} + } + + if !this.sentOurHello { + + // Need to log in. + // If the hub supports QuickList, we can skip one network roundtrip + if _, ok := this.supports["QuickList"]; ok { + this.SayInfo() + this.hc.SayRaw("$GetNickList|") + } else { + this.hc.SayRaw("$ValidateNick " + this.escape(this.hc.Hco.Self.Nick) + "|") + } + + // This also counts as the end of the handshake from our POV. Consider + // ourselves logged in + this.sentOurHello = true + if this.hc.State != CONNECTIONSTATE_CONNECTED { + this.hc.processEvent(HubEvent{EventType: EVENT_CONNECTION_STATE_CHANGED, StateChange: CONNECTIONSTATE_CONNECTED}) + this.hc.State = CONNECTIONSTATE_CONNECTED + } + + } + + // IGNORABLE COMMANDS + case "$HubTopic": + case "$Search": + case "$ConnectToMe": + + default: + this.hc.processEvent(HubEvent{EventType: EVENT_DEBUG_MESSAGE, Message: "Unhandled protocol command '" + commandParts[0] + "'"}) + + } +} + +func (this *NmdcProtocol) escape(plaintext string) string { + v1 := strings.Replace(plaintext, "&", "&", -1) + v2 := strings.Replace(v1, "|", "|", -1) + return strings.Replace(v2, "$", "$", -1) +} + +func (this *NmdcProtocol) unescape(encoded string) string { + v1 := strings.Replace(encoded, "$", "$", -1) + v2 := strings.Replace(v1, "|", "|", -1) + return strings.Replace(v2, "&", "&", -1) +} + +func (this *NmdcProtocol) SayPublic(message string) { + this.hc.SayRaw("<" + this.hc.Hco.Self.Nick + "> " + this.escape(message) + "|") +} + +func (this *NmdcProtocol) SayPrivate(recipient, message string) { + this.hc.SayRaw("$To: " + recipient + " From: " + this.hc.Hco.Self.Nick + " $<" + this.hc.Hco.Self.Nick + "> " + this.escape(message) + "|") +} + +func (this *NmdcProtocol) SayInfo() { + this.hc.SayRaw(this.getUserMyINFO(this.hc.Hco.Self) + "|") +} + +func (this *NmdcProtocol) parseMyINFO(protomsg string) (*UserInfo, error) { + ret := UserInfo{} + + // Normal format (with tag in exact V/M/H/S order) + matches := this.rxMyInfo.FindStringSubmatch(protomsg) + if matches != nil { + ret.Nick = matches[1] + ret.Description = this.unescape(matches[2]) + ret.ClientTag = this.unescape(matches[3]) + ret.ClientVersion = matches[4] + ret.ConnectionMode = ConnectionMode(matches[5][0]) + maybeParse(matches[6], &ret.HubsUnregistered, 0) + maybeParse(matches[7], &ret.HubsRegistered, 0) + maybeParse(matches[8], &ret.HubsOperator, 0) + maybeParse(matches[9], &ret.Slots, 0) + if len(matches[10]) > 1 { + ret.Speed = matches[10][:len(matches[10])-2] + } else { + ret.Speed = "" + } + ret.Flag = UserFlag(matches[10][len(matches[10])-1]) + ret.Email = this.unescape(matches[11]) + maybeParse(matches[12], &ret.ShareSize, 0) + + return &ret, nil + } + + // No-tag format, used in early connection + matches = this.rxMyInfoNoTag.FindStringSubmatch(protomsg) + if matches != nil { + ret.Nick = matches[1] + ret.Description = this.unescape(matches[2]) + ret.ClientTag = "" + ret.ClientVersion = "0" + ret.ConnectionMode = CONNECTIONMODE_PASSIVE + ret.HubsUnregistered = 0 + ret.HubsRegistered = 0 + ret.HubsOperator = 0 + ret.Slots = 0 + + if len(matches[3]) > 1 { + ret.Speed = matches[3][:len(matches[3])-2] + } else { + ret.Speed = "" + } + ret.Flag = UserFlag(matches[3][len(matches[3])-1]) + ret.Email = this.unescape(matches[4]) + maybeParse(matches[5], &ret.ShareSize, 0) + + return &ret, nil + } + + // Couldn't get anything out of it... + return nil, errors.New("Malformed MyINFO") +} + +// Returns the MyINFO command, WITH leading $MyINFO, and WITHOUT trailing pipe +func (this *NmdcProtocol) getUserMyINFO(u *UserInfo) string { + return fmt.Sprintf( + "$MyINFO $ALL %s %s<%s V:%s,M:%c,H:%d/%d/%d,S:%d>$ $%s%c$%s$%d$", + u.Nick, + u.Description, + u.ClientTag, + strings.Replace(u.ClientVersion, ",", "-", -1), // just in case + u.ConnectionMode, + u.HubsUnregistered, + u.HubsRegistered, + u.HubsOperator, + u.Slots, + u.Speed, + u.Flag, + u.Email, + u.ShareSize, + ) +} + +func (this *NmdcProtocol) ProtoMessageSeparator() string { + return "|" +} + +func (this *NmdcProtocol) unlock(lock []byte) string { + + nibble_swap := func(b byte) byte { + return ((b << 4) & 0xF0) | ((b >> 4) & 0x0F) + } + + chr := func(b byte) string { + if b == 0 || b == 5 || b == 36 || b == 96 || b == 124 || b == 126 { + return fmt.Sprintf("/%%DCN%04d%%/", b) + } else { + return string(b) + } + } + + key := chr(nibble_swap(lock[0] ^ lock[len(lock)-2] ^ lock[len(lock)-3] ^ 5)) + for i := 1; i < len(lock); i += 1 { + key += chr(nibble_swap(lock[i] ^ lock[i-1])) + } + + return key +} diff --git a/vendor/code.ivysaur.me/libnmdc/NmdcProtocol_test.go b/vendor/code.ivysaur.me/libnmdc/NmdcProtocol_test.go new file mode 100644 index 0000000..70c4f34 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/NmdcProtocol_test.go @@ -0,0 +1,97 @@ +package libnmdc + +import ( + "testing" +) + +func TestMyINFOParse(t *testing.T) { + + np := NewNmdcProtocol(nil).(*NmdcProtocol) + + type myInfoTestPair struct { + in string + expect UserInfo + } + + cases := []myInfoTestPair{ + + myInfoTestPair{ + in: "$ALL Bxxxy description$ $0.01\x01$xyz@example.com$53054999578$", + expect: UserInfo{ + Nick: "Bxxxy", + Description: "description", + ClientTag: "ApexDC++", + ClientVersion: "1.4.3", + Email: "xyz@example.com", + ShareSize: 53054999578, + Flag: FLAG_NORMAL, + Slots: 1, + HubsUnregistered: 9, + HubsRegistered: 0, + HubsOperator: 2, + UserInfo_NMDCOnly: UserInfo_NMDCOnly{ + ConnectionMode: CONNECTIONMODE_PASSIVE, + Speed: "0.0", + }, + }, + }, + myInfoTestPair{ + in: "$ALL ixxxxxxx0 $P$10A$$0$", + expect: UserInfo{ + Nick: "ixxxxxxx0", + ClientVersion: "0", // Auto-inserted by the parser for short-format MyINFO strings + Flag: UserFlag(rune('A')), + UserInfo_NMDCOnly: UserInfo_NMDCOnly{ + ConnectionMode: CONNECTIONMODE_PASSIVE, + Speed: "1", + }, + }, + }, + myInfoTestPair{ + in: "$ALL SXXXX_XXXXXXR $ $0.005Q$$0$", + expect: UserInfo{ + Nick: "SXXXX_XXXXXXR", + ClientTag: "ncdc", + ClientVersion: "1.19.1-12-g5561", + Flag: UserFlag(rune('Q')), + Slots: 10, + HubsUnregistered: 1, + UserInfo_NMDCOnly: UserInfo_NMDCOnly{ + ConnectionMode: CONNECTIONMODE_PASSIVE, + Speed: "0.00", + }, + }, + }, + myInfoTestPair{ + in: "$ALL mxxxu desccccc$ $p$$0$", + expect: UserInfo{ + Nick: "mxxxu", + Description: "desccccc", + ClientTag: "HexChat", + ClientVersion: "2.12.1", + Flag: UserFlag(rune('p')), + HubsUnregistered: 1, + Slots: 0, + UserInfo_NMDCOnly: UserInfo_NMDCOnly{ + ConnectionMode: CONNECTIONMODE_PASSIVE, + }, + }, + }, + } + + for _, v := range cases { + + got, err := np.parseMyINFO(v.in) + + if err != nil { + t.Errorf("MyINFO parse warning (%s)", err.Error()) + continue + } + + if *got != v.expect { + t.Errorf("MyINFO parse failure\nExpected:\n%+v\nGot:\n%+v\n", v.expect, got) + continue + } + } + +} diff --git a/vendor/code.ivysaur.me/libnmdc/Protocol.go b/vendor/code.ivysaur.me/libnmdc/Protocol.go new file mode 100644 index 0000000..416c398 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/Protocol.go @@ -0,0 +1,33 @@ +package libnmdc + +type Protocol interface { + ProcessCommand(msg string) + + SayPublic(string) + + SayPrivate(user, message string) + + SayInfo() + + ProtoMessageSeparator() string +} + +type HubProtocol int + +const ( + HubProtocolAutodetect HubProtocol = 0 + HubProtocolNmdc HubProtocol = 1 + HubProtocolAdc HubProtocol = 2 +) + +func (hp HubProtocol) Create(hc *HubConnection) Protocol { + if hp == HubProtocolNmdc { + return NewNmdcProtocol(hc) + + } else if hp == HubProtocolAdc { + return NewAdcProtocol(hc) + + } else { + return NewAutodetectProtocol(hc) + } +} diff --git a/vendor/code.ivysaur.me/libnmdc/TODO.txt b/vendor/code.ivysaur.me/libnmdc/TODO.txt new file mode 100644 index 0000000..bd95b9b --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/TODO.txt @@ -0,0 +1,6 @@ +NMDC: +- Implement ZPipe ($ZOn) + +ADC: +- Usercommands +- ??? diff --git a/vendor/code.ivysaur.me/libnmdc/UserCommand.go b/vendor/code.ivysaur.me/libnmdc/UserCommand.go new file mode 100644 index 0000000..6b7c0a5 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/UserCommand.go @@ -0,0 +1,27 @@ +package libnmdc + +type UserCommandType uint8 + +const ( + USERCOMMAND_TYPE_SEPARATOR UserCommandType = 0 + USERCOMMAND_TYPE_RAW UserCommandType = 1 + USERCOMMAND_TYPE_NICKLIMITED UserCommandType = 2 + USERCOMMAND_TYPE_CLEARALL UserCommandType = 255 +) + +type UserCommandContext uint8 + +const ( + USERCOMMAND_CONTEXT_HUB UserCommandContext = 1 + USERCOMMAND_CONTEXT_USER UserCommandContext = 2 + USERCOMMAND_CONTEXT_SEARCH UserCommandContext = 4 + USERCOMMAND_CONTEXT_FILELIST UserCommandContext = 8 +) + +type UserCommand struct { + Type UserCommandType + Context UserCommandContext + Message string + Command string + RemoveThis bool // Currently only set by ADC hubs +} diff --git a/vendor/code.ivysaur.me/libnmdc/UserFlag.go b/vendor/code.ivysaur.me/libnmdc/UserFlag.go new file mode 100644 index 0000000..7664aea --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/UserFlag.go @@ -0,0 +1,17 @@ +package libnmdc + +type UserFlag byte + +const ( + FLAG_NORMAL UserFlag = 1 + FLAG_AWAY_1 UserFlag = 2 + FLAG_AWAY_2 UserFlag = 3 + FLAG_SERVER_1 UserFlag = 4 + FLAG_SERVER_2 UserFlag = 5 + FLAG_SERVER_AWAY_1 UserFlag = 6 + FLAG_SERVER_AWAY_2 UserFlag = 7 + FLAG_FIREBALL_1 UserFlag = 8 + FLAG_FIREBALL_2 UserFlag = 9 + FLAG_FIREBALL_AWAY_1 UserFlag = 10 + FLAG_FIREBALL_AWAY_2 UserFlag = 11 +) diff --git a/vendor/code.ivysaur.me/libnmdc/UserInfo.go b/vendor/code.ivysaur.me/libnmdc/UserInfo.go new file mode 100644 index 0000000..9450e9e --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/UserInfo.go @@ -0,0 +1,53 @@ +package libnmdc + +// This structure represents a user connected to a hub. +type UserInfo struct { + Nick string + Description string + ClientTag string + ClientVersion string + Email string + ShareSize uint64 + Flag UserFlag + Slots uint64 + HubsUnregistered uint64 + HubsRegistered uint64 + HubsOperator uint64 + IsOperator bool + + UserInfo_NMDCOnly + UserInfo_ADCOnly +} + +type UserInfo_NMDCOnly struct { + Speed string + IPAddress string + ConnectionMode ConnectionMode +} + +type UserInfo_ADCOnly struct { + SharedFiles uint64 + UploadSpeedBps uint64 + DownloadSpeedBps uint64 + IsBot bool + IsRegistered bool + IsSuperUser bool + IsHubOwner bool + IPv4Address string // Passive <==> these fields are not set + IPv6Address string + IPv4UDPPort uint64 + IPv6UDPPort uint64 + Keyprint string + CID string + SupportFlags map[string]struct{} +} + +func NewUserInfo(username string) *UserInfo { + return &UserInfo{ + Nick: username, + HubsUnregistered: 1, + UserInfo_NMDCOnly: UserInfo_NMDCOnly{ + ConnectionMode: CONNECTIONMODE_PASSIVE, + }, + } +} diff --git a/vendor/code.ivysaur.me/libnmdc/__dist/README.txt b/vendor/code.ivysaur.me/libnmdc/__dist/README.txt new file mode 100644 index 0000000..41c6f3c --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/__dist/README.txt @@ -0,0 +1,101 @@ +An NMDC / ADC client protocol library for Golang. + +Written in golang +Tags: nmdc + +=FEATURES= + +- Connect to NMDC and ADC hubs +- SSL (NMDCS/ADCS) with option to ignore certificate validity +- Autodetect NMDC/ADC protocol by timeout +- Send public and private chat messages, UserCommand support +- Protocol keepalives +- Parse user details (including UserIP2 for NMDC) +- Fast NMDC login via NoHello and QuickList +- Both synchronous (callback) and asynchronous (channel) -based APIs, including example + +=GO GET= + +This package can be installed via go get: `go get code.ivysaur.me/libnmdc` +[go-get]code.ivysaur.me/libnmdc git https://git.ivysaur.me/code.ivysaur.me/libnmdc.git[/go-get] + +=CHANGELOG= + +2018-03-24 0.17 +- Feature: Re-add sayInfo() function for reannouncing MyINFO changes + +2017-11-26 0.16 +- Feature: Support connecting to ADC hubs +- BREAKING: Simplify connection API +- Vendor new dependency on github.com/cxmcc/tiger (MIT license) + +2017-11-14 0.15 +- Feature: Fallback reconnection if no data (not even keepalives) are recieved from the hub in 24 hours +- Fix an issue with detecting protocol messages inside multi-line chat messages +- Update examples and the default client version number + +2017-02-09 0.14 +- Fix an issue with crashing on malformed IP addresses supplied by the hub + +2017-02-09 0.13 +- Feature: Implement UserIP2 extension, to retrieve IP addresses of other users +- Enhancement: Implement QuickList extension (reduce one network roundtrip during initial connection) +- Enhancement: Implement NoHello extension (faster connection performance) +- Enhancement: Implement ChatOnly extension +- Fix an issue with not notifying client on all MyINFO updates + +2017-02-05 0.12 +- Fix an issue with mutex deadlock when accessing user information from a callback +- Fix an issue with silent disconnection if a password was required but not present + +2016-11-29 0.11 +- BREAKING: Remove some exported methods +- BREAKING: Fix an issue with missing sufficient parameters in the synchronous API +- Enhancement: Improve output under godoc +- Fix an issue with special characters appearing in recieved private messages +- Fix an issue with parsing active/passive connection modes +- Fix an issue with errors appearing on stdout + +2016-10-08 r10 +- Feature: Support `$UserCommand` + +2016-08-27 r9 +- Fix an issue with parsing MyINFO strings with zero-length speed descriptions +- Fix an issue with not storing updated profile information + +2016-05-10 r8 +- Enhancement: Separate `ClientTag` and `ClientVersion` in `UserInfo` structs + +2016-05-08 r7 +- BREAKING: Remove direct access to `HubConnection.Users` map +- Feature: Threadsafe user map accessor +- Feature: Option to disable auto-reconnection +- Feature: New `Disconnect()`, `UserCount()`, `UserExists()` functions +- Enhancement: Support `$OpList`, add `IsOperator` member to `UserInfo` structs +- Refactor into multiple files + +2016-04-16 r6 +- Fix an issue with calling `panic()` on certain types of abnormal network failure + +2016-04-04 r5 +- Enhancement: Support protocol keepalives +- Enhancement: Support hub redirects (`$ForceMove`) + +2016-04-03 r4 +- Feature: Add synchronous API +- Fix an issue with reading HubConnection's state parameter +- Fix an issue with buffered protocol commands + +2016-04-03 r3 +- Feature: Add `SkipVerifyTLS` option +- Fix an issue with calling `panic()` if connection failed + +2016-04-02 r2 +- Enhancement: Support NMDC-over-TLS (NMDCS) +- Fix an issue recieving private messages +- Fix an issue with calling `panic()` if connection failed +- Fix an issue parsing URIs without a specified port +- Move sample content into directory with excluded build + +2016-02-12 r1 +- Initial public release diff --git a/vendor/code.ivysaur.me/libnmdc/libnmdc.go b/vendor/code.ivysaur.me/libnmdc/libnmdc.go new file mode 100644 index 0000000..4566eb6 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/libnmdc.go @@ -0,0 +1,28 @@ +package libnmdc + +import ( + "errors" + "strconv" + "time" +) + +const ( + DEFAULT_CLIENT_TAG string = "libnmdc.go" + DEFAULT_CLIENT_VERSION string = "0.17" + DEFAULT_HUB_NAME string = "(unknown)" + SEND_KEEPALIVE_EVERY time.Duration = 29 * time.Second + AUTO_RECONNECT_AFTER time.Duration = 30 * time.Second + RECONNECT_IF_NO_DATA_RECIEVED_IN time.Duration = 24 * time.Hour // we expect keepalives wayyyy more frequently than this + AUTODETECT_ADC_NMDC_TIMEOUT time.Duration = 3 * time.Second +) + +var ErrNotConnected error = errors.New("Not connected") + +func maybeParse(str string, dest *uint64, default_val uint64) { + sz, err := strconv.ParseUint(str, 10, 64) + if err == nil { + *dest = sz + } else { + *dest = default_val + } +} diff --git a/vendor/code.ivysaur.me/libnmdc/tth.go b/vendor/code.ivysaur.me/libnmdc/tth.go new file mode 100644 index 0000000..612bef4 --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/tth.go @@ -0,0 +1,41 @@ +package libnmdc + +import ( + "encoding/base32" + "errors" + "strings" + + "github.com/cxmcc/tiger" +) + +// Base32 encodes the input slice in BASE32 string format without any trailing +// padding equals characters. +func Base32(input []byte) string { + return strings.TrimRight(base32.StdEncoding.EncodeToString(input), "=") +} + +// TTH returns the TTH hash of a string in raw byte format. Use the Base32() +// function to convert it to normal string format. +// This is a basic implementation that only supports content up to 1024 bytes in length. +func TTH(input string) ([]byte, error) { + + // Short segments do not need to be padded. + // Content above 1024 bytes needs tree handling (0x00 prefix for leaf nodes, + // 0x01 prefix for hash nodes) but for content less than 1024 bytes, just + // return the leaf hash + // @ref http://adc.sourceforge.net/draft-jchapweske-thex-02.html + if len(input) > 1024 { + return nil, errors.New("TTH content exceeded 1024 bytes") + } + + // Single leaf hash only + leafHash := tiger.New() + leafHash.Write([]byte("\x00" + input)) + return leafHash.Sum(nil), nil +} + +func Tiger(input string) []byte { + leafHash := tiger.New() + leafHash.Write([]byte(input)) + return leafHash.Sum(nil) +} diff --git a/vendor/code.ivysaur.me/libnmdc/tth_test.go b/vendor/code.ivysaur.me/libnmdc/tth_test.go new file mode 100644 index 0000000..817c61e --- /dev/null +++ b/vendor/code.ivysaur.me/libnmdc/tth_test.go @@ -0,0 +1,36 @@ +package libnmdc + +import ( + "strings" + "testing" +) + +func TestTTH(t *testing.T) { + + // echo -n 'hello world' | tthsum + testCases := [][2]string{ + [2]string{"hello world", "ZIIVRZDR2FD3W4KKNMNYUU3765LPPK7BWY64CHI"}, + [2]string{"", "LWPNACQDBZRYXW3VHJVCJ64QBZNGHOHHHZWCLNQ"}, + [2]string{"\x00", "VK54ZIEEVTWNAUI5D5RDFIL37LX2IQNSTAXFKSA"}, + [2]string{strings.Repeat("A", 1024), "L66Q4YVNAFWVS23X2HJIRA5ZJ7WXR3F26RSASFA"}, + } + + short := func(s string) string { + if len(s) > 15 { + return s[0:15] + "..." + } + return s + } + + for _, testCase := range testCases { + input, expected := testCase[0], testCase[1] + result, err := TTH(input) + if err != nil { + t.Fatalf("Error getting TTH for '%s': %s", short(input), err.Error()) + } + + if Base32(result) != expected { + t.Fatalf("Wrong TTH for '%s' (got '%s' expected '%s')", short(input), result, expected) + } + } +} diff --git a/vendor/github.com/cxmcc/tiger/.gitignore b/vendor/github.com/cxmcc/tiger/.gitignore new file mode 100644 index 0000000..0026861 --- /dev/null +++ b/vendor/github.com/cxmcc/tiger/.gitignore @@ -0,0 +1,22 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe diff --git a/vendor/github.com/cxmcc/tiger/.travis.yml b/vendor/github.com/cxmcc/tiger/.travis.yml new file mode 100644 index 0000000..69d2f6d --- /dev/null +++ b/vendor/github.com/cxmcc/tiger/.travis.yml @@ -0,0 +1,7 @@ +language: go + +go: + - 1.x + - 1.6 + - 1.7.x + - master diff --git a/vendor/github.com/cxmcc/tiger/LICENSE b/vendor/github.com/cxmcc/tiger/LICENSE new file mode 100644 index 0000000..c811438 --- /dev/null +++ b/vendor/github.com/cxmcc/tiger/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2013 Xiuming Chen + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/cxmcc/tiger/README.md b/vendor/github.com/cxmcc/tiger/README.md new file mode 100644 index 0000000..5306b7f --- /dev/null +++ b/vendor/github.com/cxmcc/tiger/README.md @@ -0,0 +1,48 @@ +Tiger cryptographic hash function for Go + +----- + +[![Build Status](https://travis-ci.org/cxmcc/tiger.svg?branch=master)](https://travis-ci.org/cxmcc/tiger) +[![GoDoc](http://godoc.org/github.com/cxmcc/tiger?status.png)](http://godoc.org/github.com/cxmcc/tiger) + + +### About Tiger + +* Tiger cryptographic hash function is designed by Ross Anderson and Eli Biham in 1995. +* The size of a Tiger hash value is 192 bits. Truncated versions (Tiger/128, Tiger/160) are simply prefixes of Tiger/192. +* Tiger2 is a variant where the message is padded by first appending a byte 0x80, rather than 0x01 as in the case of Tiger. +* Links: [paper](http://www.cs.technion.ac.il/~biham/Reports/Tiger/), [wikipedia](http://en.wikipedia.org/wiki/Tiger_\(cryptography\)) + +### API Documentation + +Implementing [hash.Hash](http://golang.org/pkg/hash/#Hash). Usage is pretty much the same as other stanard hashing libraries. +Documentation currently available at Godoc: [http://godoc.org/github.com/cxmcc/tiger](http://godoc.org/github.com/cxmcc/tiger) + + +### Installing +~~~ +go get github.com/cxmcc/tiger +~~~ + +### Example +~~~ go +package main + +import ( + "fmt" + "io" + "github.com/cxmcc/tiger" +) + +func main() { + h := tiger.New() + io.WriteString(h, "Example for tiger") + fmt.Printf("Output: %x\n", h.Sum(nil)) + // Output: 82bd060e19f945014f0063e8f0e6d7decfa9edfd97e76743 +} +~~~ + + +### License + +It's MIT License diff --git a/vendor/github.com/cxmcc/tiger/compress.go b/vendor/github.com/cxmcc/tiger/compress.go new file mode 100644 index 0000000..9da8b33 --- /dev/null +++ b/vendor/github.com/cxmcc/tiger/compress.go @@ -0,0 +1,96 @@ +package tiger + +import ( + "encoding/binary" + "unsafe" +) + +var littleEndian bool + +func init() { + x := uint32(0x04030201) + y := [4]byte{0x1, 0x2, 0x3, 0x4} + littleEndian = *(*[4]byte)(unsafe.Pointer(&x)) == y +} + +func pass(a, b, c uint64, x []uint64, mul uint64) (uint64, uint64, uint64) { + a, b, c = round(a, b, c, x[0], mul) + b, c, a = round(b, c, a, x[1], mul) + c, a, b = round(c, a, b, x[2], mul) + a, b, c = round(a, b, c, x[3], mul) + b, c, a = round(b, c, a, x[4], mul) + c, a, b = round(c, a, b, x[5], mul) + a, b, c = round(a, b, c, x[6], mul) + b, c, a = round(b, c, a, x[7], mul) + return a, b, c +} + +func round(a, b, c, x, mul uint64) (uint64, uint64, uint64) { + c ^= x + a -= t1[c&0xff] ^ t2[(c>>16)&0xff] ^ t3[(c>>32)&0xff] ^ t4[(c>>48)&0xff] + b += t4[(c>>8)&0xff] ^ t3[(c>>24)&0xff] ^ t2[(c>>40)&0xff] ^ t1[(c>>56)&0xff] + b *= mul + return a, b, c +} + +func keySchedule(x []uint64) { + x[0] -= x[7] ^ 0xa5a5a5a5a5a5a5a5 + x[1] ^= x[0] + x[2] += x[1] + x[3] -= x[2] ^ ((^x[1]) << 19) + x[4] ^= x[3] + x[5] += x[4] + x[6] -= x[5] ^ ((^x[4]) >> 23) + x[7] ^= x[6] + x[0] += x[7] + x[1] -= x[0] ^ ((^x[7]) << 19) + x[2] ^= x[1] + x[3] += x[2] + x[4] -= x[3] ^ ((^x[2]) >> 23) + x[5] ^= x[4] + x[6] += x[5] + x[7] -= x[6] ^ 0x0123456789abcdef +} + +func (d *digest) compress(data []byte) { + // save_abc + a := d.a + b := d.b + c := d.c + + var x []uint64 + if littleEndian { + x = []uint64{ + binary.LittleEndian.Uint64(data[0:8]), + binary.LittleEndian.Uint64(data[8:16]), + binary.LittleEndian.Uint64(data[16:24]), + binary.LittleEndian.Uint64(data[24:32]), + binary.LittleEndian.Uint64(data[32:40]), + binary.LittleEndian.Uint64(data[40:48]), + binary.LittleEndian.Uint64(data[48:56]), + binary.LittleEndian.Uint64(data[56:64]), + } + } else { + x = []uint64{ + binary.BigEndian.Uint64(data[0:8]), + binary.BigEndian.Uint64(data[8:16]), + binary.BigEndian.Uint64(data[16:24]), + binary.BigEndian.Uint64(data[24:32]), + binary.BigEndian.Uint64(data[32:40]), + binary.BigEndian.Uint64(data[40:48]), + binary.BigEndian.Uint64(data[48:56]), + binary.BigEndian.Uint64(data[56:64]), + } + } + + d.a, d.b, d.c = pass(d.a, d.b, d.c, x, 5) + keySchedule(x) + d.c, d.a, d.b = pass(d.c, d.a, d.b, x, 7) + keySchedule(x) + d.b, d.c, d.a = pass(d.b, d.c, d.a, x, 9) + + // feedforward + d.a ^= a + d.b -= b + d.c += c +} diff --git a/vendor/github.com/cxmcc/tiger/sboxes.go b/vendor/github.com/cxmcc/tiger/sboxes.go new file mode 100644 index 0000000..cd05dff --- /dev/null +++ b/vendor/github.com/cxmcc/tiger/sboxes.go @@ -0,0 +1,269 @@ +package tiger + +var t1 = [...]uint64{ + 0x02aab17cf7e90c5e, 0xac424b03e243a8ec, 0x72cd5be30dd5fcd3, 0x6d019b93f6f97f3a, + 0xcd9978ffd21f9193, 0x7573a1c9708029e2, 0xb164326b922a83c3, 0x46883eee04915870, + 0xeaace3057103ece6, 0xc54169b808a3535c, 0x4ce754918ddec47c, 0x0aa2f4dfdc0df40c, + 0x10b76f18a74dbefa, 0xc6ccb6235ad1ab6a, 0x13726121572fe2ff, 0x1a488c6f199d921e, + 0x4bc9f9f4da0007ca, 0x26f5e6f6e85241c7, 0x859079dbea5947b6, 0x4f1885c5c99e8c92, + 0xd78e761ea96f864b, 0x8e36428c52b5c17d, 0x69cf6827373063c1, 0xb607c93d9bb4c56e, + 0x7d820e760e76b5ea, 0x645c9cc6f07fdc42, 0xbf38a078243342e0, 0x5f6b343c9d2e7d04, + 0xf2c28aeb600b0ec6, 0x6c0ed85f7254bcac, 0x71592281a4db4fe5, 0x1967fa69ce0fed9f, + 0xfd5293f8b96545db, 0xc879e9d7f2a7600b, 0x860248920193194e, 0xa4f9533b2d9cc0b3, + 0x9053836c15957613, 0xdb6dcf8afc357bf1, 0x18beea7a7a370f57, 0x037117ca50b99066, + 0x6ab30a9774424a35, 0xf4e92f02e325249b, 0x7739db07061ccae1, 0xd8f3b49ceca42a05, + 0xbd56be3f51382f73, 0x45faed5843b0bb28, 0x1c813d5c11bf1f83, 0x8af0e4b6d75fa169, + 0x33ee18a487ad9999, 0x3c26e8eab1c94410, 0xb510102bc0a822f9, 0x141eef310ce6123b, + 0xfc65b90059ddb154, 0xe0158640c5e0e607, 0x884e079826c3a3cf, 0x930d0d9523c535fd, + 0x35638d754e9a2b00, 0x4085fccf40469dd5, 0xc4b17ad28be23a4c, 0xcab2f0fc6a3e6a2e, + 0x2860971a6b943fcd, 0x3dde6ee212e30446, 0x6222f32ae01765ae, 0x5d550bb5478308fe, + 0xa9efa98da0eda22a, 0xc351a71686c40da7, 0x1105586d9c867c84, 0xdcffee85fda22853, + 0xccfbd0262c5eef76, 0xbaf294cb8990d201, 0xe69464f52afad975, 0x94b013afdf133e14, + 0x06a7d1a32823c958, 0x6f95fe5130f61119, 0xd92ab34e462c06c0, 0xed7bde33887c71d2, + 0x79746d6e6518393e, 0x5ba419385d713329, 0x7c1ba6b948a97564, 0x31987c197bfdac67, + 0xde6c23c44b053d02, 0x581c49fed002d64d, 0xdd474d6338261571, 0xaa4546c3e473d062, + 0x928fce349455f860, 0x48161bbacaab94d9, 0x63912430770e6f68, 0x6ec8a5e602c6641c, + 0x87282515337ddd2b, 0x2cda6b42034b701b, 0xb03d37c181cb096d, 0xe108438266c71c6f, + 0x2b3180c7eb51b255, 0xdf92b82f96c08bbc, 0x5c68c8c0a632f3ba, 0x5504cc861c3d0556, + 0xabbfa4e55fb26b8f, 0x41848b0ab3baceb4, 0xb334a273aa445d32, 0xbca696f0a85ad881, + 0x24f6ec65b528d56c, 0x0ce1512e90f4524a, 0x4e9dd79d5506d35a, 0x258905fac6ce9779, + 0x2019295b3e109b33, 0xf8a9478b73a054cc, 0x2924f2f934417eb0, 0x3993357d536d1bc4, + 0x38a81ac21db6ff8b, 0x47c4fbf17d6016bf, 0x1e0faadd7667e3f5, 0x7abcff62938beb96, + 0xa78dad948fc179c9, 0x8f1f98b72911e50d, 0x61e48eae27121a91, 0x4d62f7ad31859808, + 0xeceba345ef5ceaeb, 0xf5ceb25ebc9684ce, 0xf633e20cb7f76221, 0xa32cdf06ab8293e4, + 0x985a202ca5ee2ca4, 0xcf0b8447cc8a8fb1, 0x9f765244979859a3, 0xa8d516b1a1240017, + 0x0bd7ba3ebb5dc726, 0xe54bca55b86adb39, 0x1d7a3afd6c478063, 0x519ec608e7669edd, + 0x0e5715a2d149aa23, 0x177d4571848ff194, 0xeeb55f3241014c22, 0x0f5e5ca13a6e2ec2, + 0x8029927b75f5c361, 0xad139fabc3d6e436, 0x0d5df1a94ccf402f, 0x3e8bd948bea5dfc8, + 0xa5a0d357bd3ff77e, 0xa2d12e251f74f645, 0x66fd9e525e81a082, 0x2e0c90ce7f687a49, + 0xc2e8bcbeba973bc5, 0x000001bce509745f, 0x423777bbe6dab3d6, 0xd1661c7eaef06eb5, + 0xa1781f354daacfd8, 0x2d11284a2b16affc, 0xf1fc4f67fa891d1f, 0x73ecc25dcb920ada, + 0xae610c22c2a12651, 0x96e0a810d356b78a, 0x5a9a381f2fe7870f, 0xd5ad62ede94e5530, + 0xd225e5e8368d1427, 0x65977b70c7af4631, 0x99f889b2de39d74f, 0x233f30bf54e1d143, + 0x9a9675d3d9a63c97, 0x5470554ff334f9a8, 0x166acb744a4f5688, 0x70c74caab2e4aead, + 0xf0d091646f294d12, 0x57b82a89684031d1, 0xefd95a5a61be0b6b, 0x2fbd12e969f2f29a, + 0x9bd37013feff9fe8, 0x3f9b0404d6085a06, 0x4940c1f3166cfe15, 0x09542c4dcdf3defb, + 0xb4c5218385cd5ce3, 0xc935b7dc4462a641, 0x3417f8a68ed3b63f, 0xb80959295b215b40, + 0xf99cdaef3b8c8572, 0x018c0614f8fcb95d, 0x1b14accd1a3acdf3, 0x84d471f200bb732d, + 0xc1a3110e95e8da16, 0x430a7220bf1a82b8, 0xb77e090d39df210e, 0x5ef4bd9f3cd05e9d, + 0x9d4ff6da7e57a444, 0xda1d60e183d4a5f8, 0xb287c38417998e47, 0xfe3edc121bb31886, + 0xc7fe3ccc980ccbef, 0xe46fb590189bfd03, 0x3732fd469a4c57dc, 0x7ef700a07cf1ad65, + 0x59c64468a31d8859, 0x762fb0b4d45b61f6, 0x155baed099047718, 0x68755e4c3d50baa6, + 0xe9214e7f22d8b4df, 0x2addbf532eac95f4, 0x32ae3909b4bd0109, 0x834df537b08e3450, + 0xfa209da84220728d, 0x9e691d9b9efe23f7, 0x0446d288c4ae8d7f, 0x7b4cc524e169785b, + 0x21d87f0135ca1385, 0xcebb400f137b8aa5, 0x272e2b66580796be, 0x3612264125c2b0de, + 0x057702bdad1efbb2, 0xd4babb8eacf84be9, 0x91583139641bc67b, 0x8bdc2de08036e024, + 0x603c8156f49f68ed, 0xf7d236f7dbef5111, 0x9727c4598ad21e80, 0xa08a0896670a5fd7, + 0xcb4a8f4309eba9cb, 0x81af564b0f7036a1, 0xc0b99aa778199abd, 0x959f1ec83fc8e952, + 0x8c505077794a81b9, 0x3acaaf8f056338f0, 0x07b43f50627a6778, 0x4a44ab49f5eccc77, + 0x3bc3d6e4b679ee98, 0x9cc0d4d1cf14108c, 0x4406c00b206bc8a0, 0x82a18854c8d72d89, + 0x67e366b35c3c432c, 0xb923dd61102b37f2, 0x56ab2779d884271d, 0xbe83e1b0ff1525af, + 0xfb7c65d4217e49a9, 0x6bdbe0e76d48e7d4, 0x08df828745d9179e, 0x22ea6a9add53bd34, + 0xe36e141c5622200a, 0x7f805d1b8cb750ee, 0xafe5c7a59f58e837, 0xe27f996a4fb1c23c, + 0xd3867dfb0775f0d0, 0xd0e673de6e88891a, 0x123aeb9eafb86c25, 0x30f1d5d5c145b895, + 0xbb434a2dee7269e7, 0x78cb67ecf931fa38, 0xf33b0372323bbf9c, 0x52d66336fb279c74, + 0x505f33ac0afb4eaa, 0xe8a5cd99a2cce187, 0x534974801e2d30bb, 0x8d2d5711d5876d90, + 0x1f1a412891bc038e, 0xd6e2e71d82e56648, 0x74036c3a497732b7, 0x89b67ed96361f5ab, + 0xffed95d8f1ea02a2, 0xe72b3bd61464d43d, 0xa6300f170bdc4820, 0xebc18760ed78a77a, +} + +var t2 = [...]uint64{ + 0xe6a6be5a05a12138, 0xb5a122a5b4f87c98, 0x563c6089140b6990, 0x4c46cb2e391f5dd5, + 0xd932addbc9b79434, 0x08ea70e42015aff5, 0xd765a6673e478cf1, 0xc4fb757eab278d99, + 0xdf11c6862d6e0692, 0xddeb84f10d7f3b16, 0x6f2ef604a665ea04, 0x4a8e0f0ff0e0dfb3, + 0xa5edeef83dbcba51, 0xfc4f0a2a0ea4371e, 0xe83e1da85cb38429, 0xdc8ff882ba1b1ce2, + 0xcd45505e8353e80d, 0x18d19a00d4db0717, 0x34a0cfeda5f38101, 0x0be77e518887caf2, + 0x1e341438b3c45136, 0xe05797f49089ccf9, 0xffd23f9df2591d14, 0x543dda228595c5cd, + 0x661f81fd99052a33, 0x8736e641db0f7b76, 0x15227725418e5307, 0xe25f7f46162eb2fa, + 0x48a8b2126c13d9fe, 0xafdc541792e76eea, 0x03d912bfc6d1898f, 0x31b1aafa1b83f51b, + 0xf1ac2796e42ab7d9, 0x40a3a7d7fcd2ebac, 0x1056136d0afbbcc5, 0x7889e1dd9a6d0c85, + 0xd33525782a7974aa, 0xa7e25d09078ac09b, 0xbd4138b3eac6edd0, 0x920abfbe71eb9e70, + 0xa2a5d0f54fc2625c, 0xc054e36b0b1290a3, 0xf6dd59ff62fe932b, 0x3537354511a8ac7d, + 0xca845e9172fadcd4, 0x84f82b60329d20dc, 0x79c62ce1cd672f18, 0x8b09a2add124642c, + 0xd0c1e96a19d9e726, 0x5a786a9b4ba9500c, 0x0e020336634c43f3, 0xc17b474aeb66d822, + 0x6a731ae3ec9baac2, 0x8226667ae0840258, 0x67d4567691caeca5, 0x1d94155c4875adb5, + 0x6d00fd985b813fdf, 0x51286efcb774cd06, 0x5e8834471fa744af, 0xf72ca0aee761ae2e, + 0xbe40e4cdaee8e09a, 0xe9970bbb5118f665, 0x726e4beb33df1964, 0x703b000729199762, + 0x4631d816f5ef30a7, 0xb880b5b51504a6be, 0x641793c37ed84b6c, 0x7b21ed77f6e97d96, + 0x776306312ef96b73, 0xae528948e86ff3f4, 0x53dbd7f286a3f8f8, 0x16cadce74cfc1063, + 0x005c19bdfa52c6dd, 0x68868f5d64d46ad3, 0x3a9d512ccf1e186a, 0x367e62c2385660ae, + 0xe359e7ea77dcb1d7, 0x526c0773749abe6e, 0x735ae5f9d09f734b, 0x493fc7cc8a558ba8, + 0xb0b9c1533041ab45, 0x321958ba470a59bd, 0x852db00b5f46c393, 0x91209b2bd336b0e5, + 0x6e604f7d659ef19f, 0xb99a8ae2782ccb24, 0xccf52ab6c814c4c7, 0x4727d9afbe11727b, + 0x7e950d0c0121b34d, 0x756f435670ad471f, 0xf5add442615a6849, 0x4e87e09980b9957a, + 0x2acfa1df50aee355, 0xd898263afd2fd556, 0xc8f4924dd80c8fd6, 0xcf99ca3d754a173a, + 0xfe477bacaf91bf3c, 0xed5371f6d690c12d, 0x831a5c285e687094, 0xc5d3c90a3708a0a4, + 0x0f7f903717d06580, 0x19f9bb13b8fdf27f, 0xb1bd6f1b4d502843, 0x1c761ba38fff4012, + 0x0d1530c4e2e21f3b, 0x8943ce69a7372c8a, 0xe5184e11feb5ce66, 0x618bdb80bd736621, + 0x7d29bad68b574d0b, 0x81bb613e25e6fe5b, 0x071c9c10bc07913f, 0xc7beeb7909ac2d97, + 0xc3e58d353bc5d757, 0xeb017892f38f61e8, 0xd4effb9c9b1cc21a, 0x99727d26f494f7ab, + 0xa3e063a2956b3e03, 0x9d4a8b9a4aa09c30, 0x3f6ab7d500090fb4, 0x9cc0f2a057268ac0, + 0x3dee9d2dedbf42d1, 0x330f49c87960a972, 0xc6b2720287421b41, 0x0ac59ec07c00369c, + 0xef4eac49cb353425, 0xf450244eef0129d8, 0x8acc46e5caf4deb6, 0x2ffeab63989263f7, + 0x8f7cb9fe5d7a4578, 0x5bd8f7644e634635, 0x427a7315bf2dc900, 0x17d0c4aa2125261c, + 0x3992486c93518e50, 0xb4cbfee0a2d7d4c3, 0x7c75d6202c5ddd8d, 0xdbc295d8e35b6c61, + 0x60b369d302032b19, 0xce42685fdce44132, 0x06f3ddb9ddf65610, 0x8ea4d21db5e148f0, + 0x20b0fce62fcd496f, 0x2c1b912358b0ee31, 0xb28317b818f5a308, 0xa89c1e189ca6d2cf, + 0x0c6b18576aaadbc8, 0xb65deaa91299fae3, 0xfb2b794b7f1027e7, 0x04e4317f443b5beb, + 0x4b852d325939d0a6, 0xd5ae6beefb207ffc, 0x309682b281c7d374, 0xbae309a194c3b475, + 0x8cc3f97b13b49f05, 0x98a9422ff8293967, 0x244b16b01076ff7c, 0xf8bf571c663d67ee, + 0x1f0d6758eee30da1, 0xc9b611d97adeb9b7, 0xb7afd5887b6c57a2, 0x6290ae846b984fe1, + 0x94df4cdeacc1a5fd, 0x058a5bd1c5483aff, 0x63166cc142ba3c37, 0x8db8526eb2f76f40, + 0xe10880036f0d6d4e, 0x9e0523c9971d311d, 0x45ec2824cc7cd691, 0x575b8359e62382c9, + 0xfa9e400dc4889995, 0xd1823ecb45721568, 0xdafd983b8206082f, 0xaa7d29082386a8cb, + 0x269fcd4403b87588, 0x1b91f5f728bdd1e0, 0xe4669f39040201f6, 0x7a1d7c218cf04ade, + 0x65623c29d79ce5ce, 0x2368449096c00bb1, 0xab9bf1879da503ba, 0xbc23ecb1a458058e, + 0x9a58df01bb401ecc, 0xa070e868a85f143d, 0x4ff188307df2239e, 0x14d565b41a641183, + 0xee13337452701602, 0x950e3dcf3f285e09, 0x59930254b9c80953, 0x3bf299408930da6d, + 0xa955943f53691387, 0xa15edecaa9cb8784, 0x29142127352be9a0, 0x76f0371fff4e7afb, + 0x0239f450274f2228, 0xbb073af01d5e868b, 0xbfc80571c10e96c1, 0xd267088568222e23, + 0x9671a3d48e80b5b0, 0x55b5d38ae193bb81, 0x693ae2d0a18b04b8, 0x5c48b4ecadd5335f, + 0xfd743b194916a1ca, 0x2577018134be98c4, 0xe77987e83c54a4ad, 0x28e11014da33e1b9, + 0x270cc59e226aa213, 0x71495f756d1a5f60, 0x9be853fb60afef77, 0xadc786a7f7443dbf, + 0x0904456173b29a82, 0x58bc7a66c232bd5e, 0xf306558c673ac8b2, 0x41f639c6b6c9772a, + 0x216defe99fda35da, 0x11640cc71c7be615, 0x93c43694565c5527, 0xea038e6246777839, + 0xf9abf3ce5a3e2469, 0x741e768d0fd312d2, 0x0144b883ced652c6, 0xc20b5a5ba33f8552, + 0x1ae69633c3435a9d, 0x97a28ca4088cfdec, 0x8824a43c1e96f420, 0x37612fa66eeea746, + 0x6b4cb165f9cf0e5a, 0x43aa1c06a0abfb4a, 0x7f4dc26ff162796b, 0x6cbacc8e54ed9b0f, + 0xa6b7ffefd2bb253e, 0x2e25bc95b0a29d4f, 0x86d6a58bdef1388c, 0xded74ac576b6f054, + 0x8030bdbc2b45805d, 0x3c81af70e94d9289, 0x3eff6dda9e3100db, 0xb38dc39fdfcc8847, + 0x123885528d17b87e, 0xf2da0ed240b1b642, 0x44cefadcd54bf9a9, 0x1312200e433c7ee6, + 0x9ffcc84f3a78c748, 0xf0cd1f72248576bb, 0xec6974053638cfe4, 0x2ba7b67c0cec4e4c, + 0xac2f4df3e5ce32ed, 0xcb33d14326ea4c11, 0xa4e9044cc77e58bc, 0x5f513293d934fcef, + 0x5dc9645506e55444, 0x50de418f317de40a, 0x388cb31a69dde259, 0x2db4a83455820a86, + 0x9010a91e84711ae9, 0x4df7f0b7b1498371, 0xd62a2eabc0977179, 0x22fac097aa8d5c0e, +} + +var t3 = [...]uint64{ + 0xf49fcc2ff1daf39b, 0x487fd5c66ff29281, 0xe8a30667fcdca83f, 0x2c9b4be3d2fcce63, + 0xda3ff74b93fbbbc2, 0x2fa165d2fe70ba66, 0xa103e279970e93d4, 0xbecdec77b0e45e71, + 0xcfb41e723985e497, 0xb70aaa025ef75017, 0xd42309f03840b8e0, 0x8efc1ad035898579, + 0x96c6920be2b2abc5, 0x66af4163375a9172, 0x2174abdcca7127fb, 0xb33ccea64a72ff41, + 0xf04a4933083066a5, 0x8d970acdd7289af5, 0x8f96e8e031c8c25e, 0xf3fec02276875d47, + 0xec7bf310056190dd, 0xf5adb0aebb0f1491, 0x9b50f8850fd58892, 0x4975488358b74de8, + 0xa3354ff691531c61, 0x0702bbe481d2c6ee, 0x89fb24057deded98, 0xac3075138596e902, + 0x1d2d3580172772ed, 0xeb738fc28e6bc30d, 0x5854ef8f63044326, 0x9e5c52325add3bbe, + 0x90aa53cf325c4623, 0xc1d24d51349dd067, 0x2051cfeea69ea624, 0x13220f0a862e7e4f, + 0xce39399404e04864, 0xd9c42ca47086fcb7, 0x685ad2238a03e7cc, 0x066484b2ab2ff1db, + 0xfe9d5d70efbf79ec, 0x5b13b9dd9c481854, 0x15f0d475ed1509ad, 0x0bebcd060ec79851, + 0xd58c6791183ab7f8, 0xd1187c5052f3eee4, 0xc95d1192e54e82ff, 0x86eea14cb9ac6ca2, + 0x3485beb153677d5d, 0xdd191d781f8c492a, 0xf60866baa784ebf9, 0x518f643ba2d08c74, + 0x8852e956e1087c22, 0xa768cb8dc410ae8d, 0x38047726bfec8e1a, 0xa67738b4cd3b45aa, + 0xad16691cec0dde19, 0xc6d4319380462e07, 0xc5a5876d0ba61938, 0x16b9fa1fa58fd840, + 0x188ab1173ca74f18, 0xabda2f98c99c021f, 0x3e0580ab134ae816, 0x5f3b05b773645abb, + 0x2501a2be5575f2f6, 0x1b2f74004e7e8ba9, 0x1cd7580371e8d953, 0x7f6ed89562764e30, + 0xb15926ff596f003d, 0x9f65293da8c5d6b9, 0x6ecef04dd690f84c, 0x4782275fff33af88, + 0xe41433083f820801, 0xfd0dfe409a1af9b5, 0x4325a3342cdb396b, 0x8ae77e62b301b252, + 0xc36f9e9f6655615a, 0x85455a2d92d32c09, 0xf2c7dea949477485, 0x63cfb4c133a39eba, + 0x83b040cc6ebc5462, 0x3b9454c8fdb326b0, 0x56f56a9e87ffd78c, 0x2dc2940d99f42bc6, + 0x98f7df096b096e2d, 0x19a6e01e3ad852bf, 0x42a99ccbdbd4b40b, 0xa59998af45e9c559, + 0x366295e807d93186, 0x6b48181bfaa1f773, 0x1fec57e2157a0a1d, 0x4667446af6201ad5, + 0xe615ebcacfb0f075, 0xb8f31f4f68290778, 0x22713ed6ce22d11e, 0x3057c1a72ec3c93b, + 0xcb46acc37c3f1f2f, 0xdbb893fd02aaf50e, 0x331fd92e600b9fcf, 0xa498f96148ea3ad6, + 0xa8d8426e8b6a83ea, 0xa089b274b7735cdc, 0x87f6b3731e524a11, 0x118808e5cbc96749, + 0x9906e4c7b19bd394, 0xafed7f7e9b24a20c, 0x6509eadeeb3644a7, 0x6c1ef1d3e8ef0ede, + 0xb9c97d43e9798fb4, 0xa2f2d784740c28a3, 0x7b8496476197566f, 0x7a5be3e6b65f069d, + 0xf96330ed78be6f10, 0xeee60de77a076a15, 0x2b4bee4aa08b9bd0, 0x6a56a63ec7b8894e, + 0x02121359ba34fef4, 0x4cbf99f8283703fc, 0x398071350caf30c8, 0xd0a77a89f017687a, + 0xf1c1a9eb9e423569, 0x8c7976282dee8199, 0x5d1737a5dd1f7abd, 0x4f53433c09a9fa80, + 0xfa8b0c53df7ca1d9, 0x3fd9dcbc886ccb77, 0xc040917ca91b4720, 0x7dd00142f9d1dcdf, + 0x8476fc1d4f387b58, 0x23f8e7c5f3316503, 0x032a2244e7e37339, 0x5c87a5d750f5a74b, + 0x082b4cc43698992e, 0xdf917becb858f63c, 0x3270b8fc5bf86dda, 0x10ae72bb29b5dd76, + 0x576ac94e7700362b, 0x1ad112dac61efb8f, 0x691bc30ec5faa427, 0xff246311cc327143, + 0x3142368e30e53206, 0x71380e31e02ca396, 0x958d5c960aad76f1, 0xf8d6f430c16da536, + 0xc8ffd13f1be7e1d2, 0x7578ae66004ddbe1, 0x05833f01067be646, 0xbb34b5ad3bfe586d, + 0x095f34c9a12b97f0, 0x247ab64525d60ca8, 0xdcdbc6f3017477d1, 0x4a2e14d4decad24d, + 0xbdb5e6d9be0a1eeb, 0x2a7e70f7794301ab, 0xdef42d8a270540fd, 0x01078ec0a34c22c1, + 0xe5de511af4c16387, 0x7ebb3a52bd9a330a, 0x77697857aa7d6435, 0x004e831603ae4c32, + 0xe7a21020ad78e312, 0x9d41a70c6ab420f2, 0x28e06c18ea1141e6, 0xd2b28cbd984f6b28, + 0x26b75f6c446e9d83, 0xba47568c4d418d7f, 0xd80badbfe6183d8e, 0x0e206d7f5f166044, + 0xe258a43911cbca3e, 0x723a1746b21dc0bc, 0xc7caa854f5d7cdd3, 0x7cac32883d261d9c, + 0x7690c26423ba942c, 0x17e55524478042b8, 0xe0be477656a2389f, 0x4d289b5e67ab2da0, + 0x44862b9c8fbbfd31, 0xb47cc8049d141365, 0x822c1b362b91c793, 0x4eb14655fb13dfd8, + 0x1ecbba0714e2a97b, 0x6143459d5cde5f14, 0x53a8fbf1d5f0ac89, 0x97ea04d81c5e5b00, + 0x622181a8d4fdb3f3, 0xe9bcd341572a1208, 0x1411258643cce58a, 0x9144c5fea4c6e0a4, + 0x0d33d06565cf620f, 0x54a48d489f219ca1, 0xc43e5eac6d63c821, 0xa9728b3a72770daf, + 0xd7934e7b20df87ef, 0xe35503b61a3e86e5, 0xcae321fbc819d504, 0x129a50b3ac60bfa6, + 0xcd5e68ea7e9fb6c3, 0xb01c90199483b1c7, 0x3de93cd5c295376c, 0xaed52edf2ab9ad13, + 0x2e60f512c0a07884, 0xbc3d86a3e36210c9, 0x35269d9b163951ce, 0x0c7d6e2ad0cdb5fa, + 0x59e86297d87f5733, 0x298ef221898db0e7, 0x55000029d1a5aa7e, 0x8bc08ae1b5061b45, + 0xc2c31c2b6c92703a, 0x94cc596baf25ef42, 0x0a1d73db22540456, 0x04b6a0f9d9c4179a, + 0xeffdafa2ae3d3c60, 0xf7c8075bb49496c4, 0x9cc5c7141d1cd4e3, 0x78bd1638218e5534, + 0xb2f11568f850246a, 0xedfabcfa9502bc29, 0x796ce5f2da23051b, 0xaae128b0dc93537c, + 0x3a493da0ee4b29ae, 0xb5df6b2c416895d7, 0xfcabbd25122d7f37, 0x70810b58105dc4b1, + 0xe10fdd37f7882a90, 0x524dcab5518a3f5c, 0x3c9e85878451255b, 0x4029828119bd34e2, + 0x74a05b6f5d3ceccb, 0xb610021542e13eca, 0x0ff979d12f59e2ac, 0x6037da27e4f9cc50, + 0x5e92975a0df1847d, 0xd66de190d3e623fe, 0x5032d6b87b568048, 0x9a36b7ce8235216e, + 0x80272a7a24f64b4a, 0x93efed8b8c6916f7, 0x37ddbff44cce1555, 0x4b95db5d4b99bd25, + 0x92d3fda169812fc0, 0xfb1a4a9a90660bb6, 0x730c196946a4b9b2, 0x81e289aa7f49da68, + 0x64669a0f83b1a05f, 0x27b3ff7d9644f48b, 0xcc6b615c8db675b3, 0x674f20b9bcebbe95, + 0x6f31238275655982, 0x5ae488713e45cf05, 0xbf619f9954c21157, 0xeabac46040a8eae9, + 0x454c6fe9f2c0c1cd, 0x419cf6496412691c, 0xd3dc3bef265b0f70, 0x6d0e60f5c3578a9e, +} + +var t4 = [...]uint64{ + 0x5b0e608526323c55, 0x1a46c1a9fa1b59f5, 0xa9e245a17c4c8ffa, 0x65ca5159db2955d7, + 0x05db0a76ce35afc2, 0x81eac77ea9113d45, 0x528ef88ab6ac0a0d, 0xa09ea253597be3ff, + 0x430ddfb3ac48cd56, 0xc4b3a67af45ce46f, 0x4ececfd8fbe2d05e, 0x3ef56f10b39935f0, + 0x0b22d6829cd619c6, 0x17fd460a74df2069, 0x6cf8cc8e8510ed40, 0xd6c824bf3a6ecaa7, + 0x61243d581a817049, 0x048bacb6bbc163a2, 0xd9a38ac27d44cc32, 0x7fddff5baaf410ab, + 0xad6d495aa804824b, 0xe1a6a74f2d8c9f94, 0xd4f7851235dee8e3, 0xfd4b7f886540d893, + 0x247c20042aa4bfda, 0x096ea1c517d1327c, 0xd56966b4361a6685, 0x277da5c31221057d, + 0x94d59893a43acff7, 0x64f0c51ccdc02281, 0x3d33bcc4ff6189db, 0xe005cb184ce66af1, + 0xff5ccd1d1db99bea, 0xb0b854a7fe42980f, 0x7bd46a6a718d4b9f, 0xd10fa8cc22a5fd8c, + 0xd31484952be4bd31, 0xc7fa975fcb243847, 0x4886ed1e5846c407, 0x28cddb791eb70b04, + 0xc2b00be2f573417f, 0x5c9590452180f877, 0x7a6bddfff370eb00, 0xce509e38d6d9d6a4, + 0xebeb0f00647fa702, 0x1dcc06cf76606f06, 0xe4d9f28ba286ff0a, 0xd85a305dc918c262, + 0x475b1d8732225f54, 0x2d4fb51668ccb5fe, 0xa679b9d9d72bba20, 0x53841c0d912d43a5, + 0x3b7eaa48bf12a4e8, 0x781e0e47f22f1ddf, 0xeff20ce60ab50973, 0x20d261d19dffb742, + 0x16a12b03062a2e39, 0x1960eb2239650495, 0x251c16fed50eb8b8, 0x9ac0c330f826016e, + 0xed152665953e7671, 0x02d63194a6369570, 0x5074f08394b1c987, 0x70ba598c90b25ce1, + 0x794a15810b9742f6, 0x0d5925e9fcaf8c6c, 0x3067716cd868744e, 0x910ab077e8d7731b, + 0x6a61bbdb5ac42f61, 0x93513efbf0851567, 0xf494724b9e83e9d5, 0xe887e1985c09648d, + 0x34b1d3c675370cfd, 0xdc35e433bc0d255d, 0xd0aab84234131be0, 0x08042a50b48b7eaf, + 0x9997c4ee44a3ab35, 0x829a7b49201799d0, 0x263b8307b7c54441, 0x752f95f4fd6a6ca6, + 0x927217402c08c6e5, 0x2a8ab754a795d9ee, 0xa442f7552f72943d, 0x2c31334e19781208, + 0x4fa98d7ceaee6291, 0x55c3862f665db309, 0xbd0610175d53b1f3, 0x46fe6cb840413f27, + 0x3fe03792df0cfa59, 0xcfe700372eb85e8f, 0xa7be29e7adbce118, 0xe544ee5cde8431dd, + 0x8a781b1b41f1873e, 0xa5c94c78a0d2f0e7, 0x39412e2877b60728, 0xa1265ef3afc9a62c, + 0xbcc2770c6a2506c5, 0x3ab66dd5dce1ce12, 0xe65499d04a675b37, 0x7d8f523481bfd216, + 0x0f6f64fcec15f389, 0x74efbe618b5b13c8, 0xacdc82b714273e1d, 0xdd40bfe003199d17, + 0x37e99257e7e061f8, 0xfa52626904775aaa, 0x8bbbf63a463d56f9, 0xf0013f1543a26e64, + 0xa8307e9f879ec898, 0xcc4c27a4150177cc, 0x1b432f2cca1d3348, 0xde1d1f8f9f6fa013, + 0x606602a047a7ddd6, 0xd237ab64cc1cb2c7, 0x9b938e7225fcd1d3, 0xec4e03708e0ff476, + 0xfeb2fbda3d03c12d, 0xae0bced2ee43889a, 0x22cb8923ebfb4f43, 0x69360d013cf7396d, + 0x855e3602d2d4e022, 0x073805bad01f784c, 0x33e17a133852f546, 0xdf4874058ac7b638, + 0xba92b29c678aa14a, 0x0ce89fc76cfaadcd, 0x5f9d4e0908339e34, 0xf1afe9291f5923b9, + 0x6e3480f60f4a265f, 0xeebf3a2ab29b841c, 0xe21938a88f91b4ad, 0x57dfeff845c6d3c3, + 0x2f006b0bf62caaf2, 0x62f479ef6f75ee78, 0x11a55ad41c8916a9, 0xf229d29084fed453, + 0x42f1c27b16b000e6, 0x2b1f76749823c074, 0x4b76eca3c2745360, 0x8c98f463b91691bd, + 0x14bcc93cf1ade66a, 0x8885213e6d458397, 0x8e177df0274d4711, 0xb49b73b5503f2951, + 0x10168168c3f96b6b, 0x0e3d963b63cab0ae, 0x8dfc4b5655a1db14, 0xf789f1356e14de5c, + 0x683e68af4e51dac1, 0xc9a84f9d8d4b0fd9, 0x3691e03f52a0f9d1, 0x5ed86e46e1878e80, + 0x3c711a0e99d07150, 0x5a0865b20c4e9310, 0x56fbfc1fe4f0682e, 0xea8d5de3105edf9b, + 0x71abfdb12379187a, 0x2eb99de1bee77b9c, 0x21ecc0ea33cf4523, 0x59a4d7521805c7a1, + 0x3896f5eb56ae7c72, 0xaa638f3db18f75dc, 0x9f39358dabe9808e, 0xb7defa91c00b72ac, + 0x6b5541fd62492d92, 0x6dc6dee8f92e4d5b, 0x353f57abc4beea7e, 0x735769d6da5690ce, + 0x0a234aa642391484, 0xf6f9508028f80d9d, 0xb8e319a27ab3f215, 0x31ad9c1151341a4d, + 0x773c22a57bef5805, 0x45c7561a07968633, 0xf913da9e249dbe36, 0xda652d9b78a64c68, + 0x4c27a97f3bc334ef, 0x76621220e66b17f4, 0x967743899acd7d0b, 0xf3ee5bcae0ed6782, + 0x409f753600c879fc, 0x06d09a39b5926db6, 0x6f83aeb0317ac588, 0x01e6ca4a86381f21, + 0x66ff3462d19f3025, 0x72207c24ddfd3bfb, 0x4af6b6d3e2ece2eb, 0x9c994dbec7ea08de, + 0x49ace597b09a8bc4, 0xb38c4766cf0797ba, 0x131b9373c57c2a75, 0xb1822cce61931e58, + 0x9d7555b909ba1c0c, 0x127fafdd937d11d2, 0x29da3badc66d92e4, 0xa2c1d57154c2ecbc, + 0x58c5134d82f6fe24, 0x1c3ae3515b62274f, 0xe907c82e01cb8126, 0xf8ed091913e37fcb, + 0x3249d8f9c80046c9, 0x80cf9bede388fb63, 0x1881539a116cf19e, 0x5103f3f76bd52457, + 0x15b7e6f5ae47f7a8, 0xdbd7c6ded47e9ccf, 0x44e55c410228bb1a, 0xb647d4255edb4e99, + 0x5d11882bb8aafc30, 0xf5098bbb29d3212a, 0x8fb5ea14e90296b3, 0x677b942157dd025a, + 0xfb58e7c0a390acb5, 0x89d3674c83bd4a01, 0x9e2da4df4bf3b93b, 0xfcc41e328cab4829, + 0x03f38c96ba582c52, 0xcad1bdbd7fd85db2, 0xbbb442c16082ae83, 0xb95fe86ba5da9ab0, + 0xb22e04673771a93f, 0x845358c9493152d8, 0xbe2a488697b4541e, 0x95a2dc2dd38e6966, + 0xc02c11ac923c852b, 0x2388b1990df2a87b, 0x7c8008fa1b4f37be, 0x1f70d0c84d54e503, + 0x5490adec7ece57d4, 0x002b3c27d9063a3a, 0x7eaea3848030a2bf, 0xc602326ded2003c0, + 0x83a7287d69a94086, 0xc57a5fcb30f57a8a, 0xb56844e479ebe779, 0xa373b40f05dcbce9, + 0xd71a786e88570ee2, 0x879cbacdbde8f6a0, 0x976ad1bcc164a32f, 0xab21e25e9666d78b, + 0x901063aae5e5c33c, 0x9818b34448698d90, 0xe36487ae3e1e8abb, 0xafbdf931893bdcb4, + 0x6345a0dc5fbbd519, 0x8628fe269b9465ca, 0x1e5d01603f9c51ec, 0x4de44006a15049b7, + 0xbf6c70e5f776cbb1, 0x411218f2ef552bed, 0xcb0c0708705a36a3, 0xe74d14754f986044, + 0xcd56d9430ea8280e, 0xc12591d7535f5065, 0xc83223f1720aef96, 0xc3a0396f7363a51f, +} diff --git a/vendor/github.com/cxmcc/tiger/tiger.go b/vendor/github.com/cxmcc/tiger/tiger.go new file mode 100644 index 0000000..504bd66 --- /dev/null +++ b/vendor/github.com/cxmcc/tiger/tiger.go @@ -0,0 +1,117 @@ +// Package tiger implements the Tiger hash algorithm +// https://github.com/cxmcc/tiger +package tiger + +import "hash" + +// The size of a Tiger hash value in bytes +const Size = 24 + +// The blocksize of Tiger hash function in bytes +const BlockSize = 64 + +const ( + chunk = 64 + initA = 0x0123456789abcdef + initB = 0xfedcba9876543210 + initC = 0xf096a5b4c3b2e187 +) + +type digest struct { + a uint64 + b uint64 + c uint64 + x [chunk]byte + nx int + length uint64 + ver int +} + +func (d *digest) Reset() { + d.a = initA + d.b = initB + d.c = initC + d.nx = 0 + d.length = 0 +} + +// New returns a new hash.Hash computing the Tiger hash value +func New() hash.Hash { + d := new(digest) + d.Reset() + d.ver = 1 + return d +} + +// New returns a new hash.Hash computing the Tiger2 hash value +func New2() hash.Hash { + d := new(digest) + d.Reset() + d.ver = 2 + return d +} + +func (d *digest) BlockSize() int { + return BlockSize +} + +func (d *digest) Size() int { + return Size +} + +func (d *digest) Write(p []byte) (length int, err error) { + length = len(p) + d.length += uint64(length) + if d.nx > 0 { + n := len(p) + if n > chunk-d.nx { + n = chunk - d.nx + } + copy(d.x[d.nx:d.nx+n], p[:n]) + d.nx += n + if d.nx == chunk { + d.compress(d.x[:chunk]) + d.nx = 0 + } + p = p[n:] + } + for len(p) >= chunk { + d.compress(p[:chunk]) + p = p[chunk:] + } + if len(p) > 0 { + d.nx = copy(d.x[:], p) + } + return +} + +func (d digest) Sum(in []byte) []byte { + length := d.length + var tmp [64]byte + if d.ver == 1 { + tmp[0] = 0x01 + } else { + tmp[0] = 0x80 + } + + size := length & 0x3f + if size < 56 { + d.Write(tmp[:56-size]) + } else { + d.Write(tmp[:64+56-size]) + } + + length <<= 3 + for i := uint(0); i < 8; i++ { + tmp[i] = byte(length >> (8 * i)) + } + d.Write(tmp[:8]) + + for i := uint(0); i < 8; i++ { + tmp[i] = byte(d.a >> (8 * i)) + tmp[i+8] = byte(d.b >> (8 * i)) + tmp[i+16] = byte(d.c >> (8 * i)) + } + + return append(in, tmp[:24]...) +} diff --git a/vendor/github.com/cxmcc/tiger/tiger_test.go b/vendor/github.com/cxmcc/tiger/tiger_test.go new file mode 100644 index 0000000..a7068aa --- /dev/null +++ b/vendor/github.com/cxmcc/tiger/tiger_test.go @@ -0,0 +1,144 @@ +package tiger + +import ( + "fmt" + "io" + "strings" + "testing" + "unsafe" +) + +type Test struct { + out string + in string +} + +var golden = []Test{ + {"3293ac630c13f0245f92bbb1766e16167a4e58492dde73f3", ""}, + {"77befbef2e7ef8ab2ec8f93bf587a7fc613e247f5f247809", "a"}, + {"2aab1484e8c158f2bfb8c5ff41b57a525129131c957b5f93", "abc"}, + {"d981f8cb78201a950dcf3048751e441c517fca1aa55a29f6", "message digest"}, + {"1714a472eee57d30040412bfcc55032a0b11602ff37beee9", "abcdefghijklmnopqrstuvwxyz"}, + {"0f7bf9a19b9c58f2b7610df7e84f0ac3a71c631e7b53f78e", "abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq"}, + {"8dcea680a17583ee502ba38a3c368651890ffbccdc49a8cc", "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"}, + {"1c14795529fd9f207a958f84c52f11e887fa0cabdfd91bfd", "12345678901234567890123456789012345678901234567890123456789012345678901234567890"}, + {"cdf0990c5c6b6b0bddd63a75ed20e2d448bf44e15fde0df4", strings.Repeat("A", 1024)}, + {"89292aee0f82842abc080c57b3aadd9ca84d66bf0cae77aa", strings.Repeat("A", 1025)}, +} + +func TestGolden(t *testing.T) { + for i := 0; i < len(golden); i++ { + g := golden[i] + c := New() + buf := make([]byte, len(g.in)+4) + for j := 0; j < 7; j++ { + if j < 2 { + io.WriteString(c, g.in) + } else if j == 2 { + io.WriteString(c, g.in[0:len(g.in)/2]) + c.Sum(nil) + io.WriteString(c, g.in[len(g.in)/2:]) + } else if j > 2 { + // test unaligned write + buf = buf[1:] + copy(buf, g.in) + c.Write(buf[:len(g.in)]) + } + s := fmt.Sprintf("%x", c.Sum(nil)) + if s != g.out { + t.Fatalf("tiger[%d](%s) = %s want %s", j, g.in, s, g.out) + } + c.Reset() + } + } +} + +type WriteTest struct { + out int + in string +} + +var writeTestVectors = []WriteTest{ + {0, ""}, + {1, "A"}, + {2, "AA"}, + {10, strings.Repeat("A", 10)}, + {1024, strings.Repeat("A", 1024)}, + {1025, strings.Repeat("A", 1025)}, + {0, ""}, +} + +func TestWriteReturnsCorrectSize(t *testing.T) { + c := New() + for i := 0; i < len(writeTestVectors); i++ { + v := writeTestVectors[i] + b := []byte(v.in) + length, err := c.Write(b[:len(v.in)]) + if length != v.out { + t.Fatalf("Write() = %d want %d", length, v.out) + } + if err != nil { + t.Fatalf("Write(%s) failed.", v.in) + } + } +} + +func ExampleNew() { + h := New() + io.WriteString(h, "It's the eye of the tiger, it's the thrill of the fight") + io.WriteString(h, "Rising up to the challenge of our rival!") + fmt.Printf("%x", h.Sum(nil)) + // Output: a7bbad36cc17918e399ae8ee893e4595e4d24e1639fe822c +} + +func ExampleNew2() { + h := New2() + io.WriteString(h, "It's the eye of the tiger, it's the thrill of the fight") + io.WriteString(h, "Rising up to the challenge of our rival!") + fmt.Printf("%x", h.Sum(nil)) + // Output: c86695c2a639506682de2c12c2d23b61a12db78ea1ee1001 +} + +var bench = New() +var buf = make([]byte, 8192+1) +var sum = make([]byte, bench.Size()) + +func benchmarkSize(b *testing.B, size int, unaligned bool) { + b.SetBytes(int64(size)) + buf := buf + if unaligned { + if uintptr(unsafe.Pointer(&buf[0]))&(unsafe.Alignof(uint32(0))-1) == 0 { + buf = buf[1:] + } + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + bench.Reset() + bench.Write(buf[:size]) + bench.Sum(sum[:0]) + } +} + +func BenchmarkHash8Bytes(b *testing.B) { + benchmarkSize(b, 8, false) +} + +func BenchmarkHash1K(b *testing.B) { + benchmarkSize(b, 1024, false) +} + +func BenchmarkHash8K(b *testing.B) { + benchmarkSize(b, 8192, false) +} + +func BenchmarkHash8BytesUnaligned(b *testing.B) { + benchmarkSize(b, 8, true) +} + +func BenchmarkHash1KUnaligned(b *testing.B) { + benchmarkSize(b, 1024, true) +} + +func BenchmarkHash8KUnaligned(b *testing.B) { + benchmarkSize(b, 8192, true) +} diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.gitignore b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.gitignore new file mode 100644 index 0000000..aa7ac80 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.gitignore @@ -0,0 +1,2 @@ +.idea/ +coverage.out diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.travis.yml b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.travis.yml new file mode 100644 index 0000000..8408fb7 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/.travis.yml @@ -0,0 +1,5 @@ +language: go + +go: + - 1.4 + - tip \ No newline at end of file diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/LICENSE.txt b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/LICENSE.txt new file mode 100644 index 0000000..b1fef93 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Syfaro + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/README.md b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/README.md new file mode 100644 index 0000000..d9a6873 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/README.md @@ -0,0 +1,118 @@ +# Golang bindings for the Telegram Bot API + +[![GoDoc](https://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api?status.svg)](http://godoc.org/github.com/go-telegram-bot-api/telegram-bot-api) +[![Travis](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api.svg)](https://travis-ci.org/go-telegram-bot-api/telegram-bot-api) + +All methods have been added, and all features should be available. +If you want a feature that hasn't been added yet or something is broken, +open an issue and I'll see what I can do. + +All methods are fairly self explanatory, and reading the godoc page should +explain everything. If something isn't clear, open an issue or submit +a pull request. + +The scope of this project is just to provide a wrapper around the API +without any additional features. There are other projects for creating +something with plugins and command handlers without having to design +all that yourself. + +Use `github.com/go-telegram-bot-api/telegram-bot-api` for the latest +version, or use `gopkg.in/telegram-bot-api.v4` for the stable build. + +Join [the development group](https://telegram.me/go_telegram_bot_api) if +you want to ask questions or discuss development. + +## Example + +This is a very simple bot that just displays any gotten updates, +then replies it to that chat. + +```go +package main + +import ( + "log" + "gopkg.in/telegram-bot-api.v4" +) + +func main() { + bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") + if err != nil { + log.Panic(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates, err := bot.GetUpdatesChan(u) + + for update := range updates { + if update.Message == nil { + continue + } + + log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) + + msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) + msg.ReplyToMessageID = update.Message.MessageID + + bot.Send(msg) + } +} +``` + +If you need to use webhooks (if you wish to run on Google App Engine), +you may use a slightly different method. + +```go +package main + +import ( + "gopkg.in/telegram-bot-api.v4" + "log" + "net/http" +) + +func main() { + bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") + if err != nil { + log.Fatal(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + _, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem")) + if err != nil { + log.Fatal(err) + } + info, err := bot.GetWebhookInfo() + if err != nil { + log.Fatal(err) + } + if info.LastErrorDate != 0 { + log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) + } + updates := bot.ListenForWebhook("/" + bot.Token) + go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) + + for update := range updates { + log.Printf("%+v\n", update) + } +} +``` + +If you need, you may generate a self signed certficate, as this requires +HTTPS / TLS. The above example tells Telegram that this is your +certificate and that it should be trusted, even though it is not +properly signed. + + openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 3560 -subj "//O=Org\CN=Test" -nodes + +Now that [Let's Encrypt](https://letsencrypt.org) has entered public beta, +you may wish to generate your free TLS certificate there. diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot.go new file mode 100644 index 0000000..8fb6200 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot.go @@ -0,0 +1,952 @@ +// Package tgbotapi has functions and types used for interacting with +// the Telegram Bot API. +package tgbotapi + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strconv" + "strings" + "time" + + "github.com/technoweenie/multipartstreamer" +) + +// BotAPI allows you to interact with the Telegram Bot API. +type BotAPI struct { + Token string `json:"token"` + Debug bool `json:"debug"` + Buffer int `json:"buffer"` + + Self User `json:"-"` + Client *http.Client `json:"-"` +} + +// NewBotAPI creates a new BotAPI instance. +// +// It requires a token, provided by @BotFather on Telegram. +func NewBotAPI(token string) (*BotAPI, error) { + return NewBotAPIWithClient(token, &http.Client{}) +} + +// NewBotAPIWithClient creates a new BotAPI instance +// and allows you to pass a http.Client. +// +// It requires a token, provided by @BotFather on Telegram. +func NewBotAPIWithClient(token string, client *http.Client) (*BotAPI, error) { + bot := &BotAPI{ + Token: token, + Client: client, + Buffer: 100, + } + + self, err := bot.GetMe() + if err != nil { + return nil, err + } + + bot.Self = self + + return bot, nil +} + +// MakeRequest makes a request to a specific endpoint with our token. +func (bot *BotAPI) MakeRequest(endpoint string, params url.Values) (APIResponse, error) { + method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint) + + resp, err := bot.Client.PostForm(method, params) + if err != nil { + return APIResponse{}, err + } + defer resp.Body.Close() + + var apiResp APIResponse + bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp) + if err != nil { + return apiResp, err + } + + if bot.Debug { + log.Printf("%s resp: %s", endpoint, bytes) + } + + if !apiResp.Ok { + parameters := ResponseParameters{} + if apiResp.Parameters != nil { + parameters = *apiResp.Parameters + } + return apiResp, Error{apiResp.Description, parameters} + } + + return apiResp, nil +} + +// decodeAPIResponse decode response and return slice of bytes if debug enabled. +// If debug disabled, just decode http.Response.Body stream to APIResponse struct +// for efficient memory usage +func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) (_ []byte, err error) { + if !bot.Debug { + dec := json.NewDecoder(responseBody) + err = dec.Decode(resp) + return + } + + // if debug, read reponse body + data, err := ioutil.ReadAll(responseBody) + if err != nil { + return + } + + err = json.Unmarshal(data, resp) + if err != nil { + return + } + + return data, nil +} + +// makeMessageRequest makes a request to a method that returns a Message. +func (bot *BotAPI) makeMessageRequest(endpoint string, params url.Values) (Message, error) { + resp, err := bot.MakeRequest(endpoint, params) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + bot.debugLog(endpoint, params, message) + + return message, nil +} + +// UploadFile makes a request to the API with a file. +// +// Requires the parameter to hold the file not be in the params. +// File should be a string to a file path, a FileBytes struct, +// a FileReader struct, or a url.URL. +// +// Note that if your FileReader has a size set to -1, it will read +// the file into memory to calculate a size. +func (bot *BotAPI) UploadFile(endpoint string, params map[string]string, fieldname string, file interface{}) (APIResponse, error) { + ms := multipartstreamer.New() + + switch f := file.(type) { + case string: + ms.WriteFields(params) + + fileHandle, err := os.Open(f) + if err != nil { + return APIResponse{}, err + } + defer fileHandle.Close() + + fi, err := os.Stat(f) + if err != nil { + return APIResponse{}, err + } + + ms.WriteReader(fieldname, fileHandle.Name(), fi.Size(), fileHandle) + case FileBytes: + ms.WriteFields(params) + + buf := bytes.NewBuffer(f.Bytes) + ms.WriteReader(fieldname, f.Name, int64(len(f.Bytes)), buf) + case FileReader: + ms.WriteFields(params) + + if f.Size != -1 { + ms.WriteReader(fieldname, f.Name, f.Size, f.Reader) + + break + } + + data, err := ioutil.ReadAll(f.Reader) + if err != nil { + return APIResponse{}, err + } + + buf := bytes.NewBuffer(data) + + ms.WriteReader(fieldname, f.Name, int64(len(data)), buf) + case url.URL: + params[fieldname] = f.String() + + ms.WriteFields(params) + default: + return APIResponse{}, errors.New(ErrBadFileType) + } + + method := fmt.Sprintf(APIEndpoint, bot.Token, endpoint) + + req, err := http.NewRequest("POST", method, nil) + if err != nil { + return APIResponse{}, err + } + + ms.SetupRequest(req) + + res, err := bot.Client.Do(req) + if err != nil { + return APIResponse{}, err + } + defer res.Body.Close() + + bytes, err := ioutil.ReadAll(res.Body) + if err != nil { + return APIResponse{}, err + } + + if bot.Debug { + log.Println(string(bytes)) + } + + var apiResp APIResponse + + err = json.Unmarshal(bytes, &apiResp) + if err != nil { + return APIResponse{}, err + } + + if !apiResp.Ok { + return APIResponse{}, errors.New(apiResp.Description) + } + + return apiResp, nil +} + +// GetFileDirectURL returns direct URL to file +// +// It requires the FileID. +func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) { + file, err := bot.GetFile(FileConfig{fileID}) + + if err != nil { + return "", err + } + + return file.Link(bot.Token), nil +} + +// GetMe fetches the currently authenticated bot. +// +// This method is called upon creation to validate the token, +// and so you may get this data from BotAPI.Self without the need for +// another request. +func (bot *BotAPI) GetMe() (User, error) { + resp, err := bot.MakeRequest("getMe", nil) + if err != nil { + return User{}, err + } + + var user User + json.Unmarshal(resp.Result, &user) + + bot.debugLog("getMe", nil, user) + + return user, nil +} + +// IsMessageToMe returns true if message directed to this bot. +// +// It requires the Message. +func (bot *BotAPI) IsMessageToMe(message Message) bool { + return strings.Contains(message.Text, "@"+bot.Self.UserName) +} + +// Send will send a Chattable item to Telegram. +// +// It requires the Chattable to send. +func (bot *BotAPI) Send(c Chattable) (Message, error) { + switch c.(type) { + case Fileable: + return bot.sendFile(c.(Fileable)) + default: + return bot.sendChattable(c) + } +} + +// debugLog checks if the bot is currently running in debug mode, and if +// so will display information about the request and response in the +// debug log. +func (bot *BotAPI) debugLog(context string, v url.Values, message interface{}) { + if bot.Debug { + log.Printf("%s req : %+v\n", context, v) + log.Printf("%s resp: %+v\n", context, message) + } +} + +// sendExisting will send a Message with an existing file to Telegram. +func (bot *BotAPI) sendExisting(method string, config Fileable) (Message, error) { + v, err := config.values() + + if err != nil { + return Message{}, err + } + + message, err := bot.makeMessageRequest(method, v) + if err != nil { + return Message{}, err + } + + return message, nil +} + +// uploadAndSend will send a Message with a new file to Telegram. +func (bot *BotAPI) uploadAndSend(method string, config Fileable) (Message, error) { + params, err := config.params() + if err != nil { + return Message{}, err + } + + file := config.getFile() + + resp, err := bot.UploadFile(method, params, config.name(), file) + if err != nil { + return Message{}, err + } + + var message Message + json.Unmarshal(resp.Result, &message) + + bot.debugLog(method, nil, message) + + return message, nil +} + +// sendFile determines if the file is using an existing file or uploading +// a new file, then sends it as needed. +func (bot *BotAPI) sendFile(config Fileable) (Message, error) { + if config.useExistingFile() { + return bot.sendExisting(config.method(), config) + } + + return bot.uploadAndSend(config.method(), config) +} + +// sendChattable sends a Chattable. +func (bot *BotAPI) sendChattable(config Chattable) (Message, error) { + v, err := config.values() + if err != nil { + return Message{}, err + } + + message, err := bot.makeMessageRequest(config.method(), v) + + if err != nil { + return Message{}, err + } + + return message, nil +} + +// GetUserProfilePhotos gets a user's profile photos. +// +// It requires UserID. +// Offset and Limit are optional. +func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) { + v := url.Values{} + v.Add("user_id", strconv.Itoa(config.UserID)) + if config.Offset != 0 { + v.Add("offset", strconv.Itoa(config.Offset)) + } + if config.Limit != 0 { + v.Add("limit", strconv.Itoa(config.Limit)) + } + + resp, err := bot.MakeRequest("getUserProfilePhotos", v) + if err != nil { + return UserProfilePhotos{}, err + } + + var profilePhotos UserProfilePhotos + json.Unmarshal(resp.Result, &profilePhotos) + + bot.debugLog("GetUserProfilePhoto", v, profilePhotos) + + return profilePhotos, nil +} + +// GetFile returns a File which can download a file from Telegram. +// +// Requires FileID. +func (bot *BotAPI) GetFile(config FileConfig) (File, error) { + v := url.Values{} + v.Add("file_id", config.FileID) + + resp, err := bot.MakeRequest("getFile", v) + if err != nil { + return File{}, err + } + + var file File + json.Unmarshal(resp.Result, &file) + + bot.debugLog("GetFile", v, file) + + return file, nil +} + +// GetUpdates fetches updates. +// If a WebHook is set, this will not return any data! +// +// Offset, Limit, and Timeout are optional. +// To avoid stale items, set Offset to one higher than the previous item. +// Set Timeout to a large number to reduce requests so you can get updates +// instantly instead of having to wait between requests. +func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) { + v := url.Values{} + if config.Offset != 0 { + v.Add("offset", strconv.Itoa(config.Offset)) + } + if config.Limit > 0 { + v.Add("limit", strconv.Itoa(config.Limit)) + } + if config.Timeout > 0 { + v.Add("timeout", strconv.Itoa(config.Timeout)) + } + + resp, err := bot.MakeRequest("getUpdates", v) + if err != nil { + return []Update{}, err + } + + var updates []Update + json.Unmarshal(resp.Result, &updates) + + bot.debugLog("getUpdates", v, updates) + + return updates, nil +} + +// RemoveWebhook unsets the webhook. +func (bot *BotAPI) RemoveWebhook() (APIResponse, error) { + return bot.MakeRequest("setWebhook", url.Values{}) +} + +// SetWebhook sets a webhook. +// +// If this is set, GetUpdates will not get any data! +// +// If you do not have a legitimate TLS certificate, you need to include +// your self signed certificate with the config. +func (bot *BotAPI) SetWebhook(config WebhookConfig) (APIResponse, error) { + + if config.Certificate == nil { + v := url.Values{} + v.Add("url", config.URL.String()) + if config.MaxConnections != 0 { + v.Add("max_connections", strconv.Itoa(config.MaxConnections)) + } + + return bot.MakeRequest("setWebhook", v) + } + + params := make(map[string]string) + params["url"] = config.URL.String() + if config.MaxConnections != 0 { + params["max_connections"] = strconv.Itoa(config.MaxConnections) + } + + resp, err := bot.UploadFile("setWebhook", params, "certificate", config.Certificate) + if err != nil { + return APIResponse{}, err + } + + return resp, nil +} + +// GetWebhookInfo allows you to fetch information about a webhook and if +// one currently is set, along with pending update count and error messages. +func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) { + resp, err := bot.MakeRequest("getWebhookInfo", url.Values{}) + if err != nil { + return WebhookInfo{}, err + } + + var info WebhookInfo + err = json.Unmarshal(resp.Result, &info) + + return info, err +} + +// GetUpdatesChan starts and returns a channel for getting updates. +func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) (UpdatesChannel, error) { + ch := make(chan Update, bot.Buffer) + + go func() { + for { + updates, err := bot.GetUpdates(config) + if err != nil { + log.Println(err) + log.Println("Failed to get updates, retrying in 3 seconds...") + time.Sleep(time.Second * 3) + + continue + } + + for _, update := range updates { + if update.UpdateID >= config.Offset { + config.Offset = update.UpdateID + 1 + ch <- update + } + } + } + }() + + return ch, nil +} + +// ListenForWebhook registers a http handler for a webhook. +func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel { + ch := make(chan Update, bot.Buffer) + + http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) { + bytes, _ := ioutil.ReadAll(r.Body) + + var update Update + json.Unmarshal(bytes, &update) + + ch <- update + }) + + return ch +} + +// AnswerInlineQuery sends a response to an inline query. +// +// Note that you must respond to an inline query within 30 seconds. +func (bot *BotAPI) AnswerInlineQuery(config InlineConfig) (APIResponse, error) { + v := url.Values{} + + v.Add("inline_query_id", config.InlineQueryID) + v.Add("cache_time", strconv.Itoa(config.CacheTime)) + v.Add("is_personal", strconv.FormatBool(config.IsPersonal)) + v.Add("next_offset", config.NextOffset) + data, err := json.Marshal(config.Results) + if err != nil { + return APIResponse{}, err + } + v.Add("results", string(data)) + v.Add("switch_pm_text", config.SwitchPMText) + v.Add("switch_pm_parameter", config.SwitchPMParameter) + + bot.debugLog("answerInlineQuery", v, nil) + + return bot.MakeRequest("answerInlineQuery", v) +} + +// AnswerCallbackQuery sends a response to an inline query callback. +func (bot *BotAPI) AnswerCallbackQuery(config CallbackConfig) (APIResponse, error) { + v := url.Values{} + + v.Add("callback_query_id", config.CallbackQueryID) + if config.Text != "" { + v.Add("text", config.Text) + } + v.Add("show_alert", strconv.FormatBool(config.ShowAlert)) + if config.URL != "" { + v.Add("url", config.URL) + } + v.Add("cache_time", strconv.Itoa(config.CacheTime)) + + bot.debugLog("answerCallbackQuery", v, nil) + + return bot.MakeRequest("answerCallbackQuery", v) +} + +// KickChatMember kicks a user from a chat. Note that this only will work +// in supergroups, and requires the bot to be an admin. Also note they +// will be unable to rejoin until they are unbanned. +func (bot *BotAPI) KickChatMember(config KickChatMemberConfig) (APIResponse, error) { + v := url.Values{} + + if config.SuperGroupUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.SuperGroupUsername) + } + v.Add("user_id", strconv.Itoa(config.UserID)) + + if config.UntilDate != 0 { + v.Add("until_date", strconv.FormatInt(config.UntilDate, 10)) + } + + bot.debugLog("kickChatMember", v, nil) + + return bot.MakeRequest("kickChatMember", v) +} + +// LeaveChat makes the bot leave the chat. +func (bot *BotAPI) LeaveChat(config ChatConfig) (APIResponse, error) { + v := url.Values{} + + if config.SuperGroupUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.SuperGroupUsername) + } + + bot.debugLog("leaveChat", v, nil) + + return bot.MakeRequest("leaveChat", v) +} + +// GetChat gets information about a chat. +func (bot *BotAPI) GetChat(config ChatConfig) (Chat, error) { + v := url.Values{} + + if config.SuperGroupUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.SuperGroupUsername) + } + + resp, err := bot.MakeRequest("getChat", v) + if err != nil { + return Chat{}, err + } + + var chat Chat + err = json.Unmarshal(resp.Result, &chat) + + bot.debugLog("getChat", v, chat) + + return chat, err +} + +// GetChatAdministrators gets a list of administrators in the chat. +// +// If none have been appointed, only the creator will be returned. +// Bots are not shown, even if they are an administrator. +func (bot *BotAPI) GetChatAdministrators(config ChatConfig) ([]ChatMember, error) { + v := url.Values{} + + if config.SuperGroupUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.SuperGroupUsername) + } + + resp, err := bot.MakeRequest("getChatAdministrators", v) + if err != nil { + return []ChatMember{}, err + } + + var members []ChatMember + err = json.Unmarshal(resp.Result, &members) + + bot.debugLog("getChatAdministrators", v, members) + + return members, err +} + +// GetChatMembersCount gets the number of users in a chat. +func (bot *BotAPI) GetChatMembersCount(config ChatConfig) (int, error) { + v := url.Values{} + + if config.SuperGroupUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.SuperGroupUsername) + } + + resp, err := bot.MakeRequest("getChatMembersCount", v) + if err != nil { + return -1, err + } + + var count int + err = json.Unmarshal(resp.Result, &count) + + bot.debugLog("getChatMembersCount", v, count) + + return count, err +} + +// GetChatMember gets a specific chat member. +func (bot *BotAPI) GetChatMember(config ChatConfigWithUser) (ChatMember, error) { + v := url.Values{} + + if config.SuperGroupUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.SuperGroupUsername) + } + v.Add("user_id", strconv.Itoa(config.UserID)) + + resp, err := bot.MakeRequest("getChatMember", v) + if err != nil { + return ChatMember{}, err + } + + var member ChatMember + err = json.Unmarshal(resp.Result, &member) + + bot.debugLog("getChatMember", v, member) + + return member, err +} + +// UnbanChatMember unbans a user from a chat. Note that this only will work +// in supergroups and channels, and requires the bot to be an admin. +func (bot *BotAPI) UnbanChatMember(config ChatMemberConfig) (APIResponse, error) { + v := url.Values{} + + if config.SuperGroupUsername != "" { + v.Add("chat_id", config.SuperGroupUsername) + } else if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } + v.Add("user_id", strconv.Itoa(config.UserID)) + + bot.debugLog("unbanChatMember", v, nil) + + return bot.MakeRequest("unbanChatMember", v) +} + +// RestrictChatMember to restrict a user in a supergroup. The bot must be an +//administrator in the supergroup for this to work and must have the +//appropriate admin rights. Pass True for all boolean parameters to lift +//restrictions from a user. Returns True on success. +func (bot *BotAPI) RestrictChatMember(config RestrictChatMemberConfig) (APIResponse, error) { + v := url.Values{} + + if config.SuperGroupUsername != "" { + v.Add("chat_id", config.SuperGroupUsername) + } else if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } + v.Add("user_id", strconv.Itoa(config.UserID)) + + if config.CanSendMessages != nil { + v.Add("can_send_messages", strconv.FormatBool(*config.CanSendMessages)) + } + if config.CanSendMediaMessages != nil { + v.Add("can_send_media_messages", strconv.FormatBool(*config.CanSendMediaMessages)) + } + if config.CanSendOtherMessages != nil { + v.Add("can_send_other_messages", strconv.FormatBool(*config.CanSendOtherMessages)) + } + if config.CanAddWebPagePreviews != nil { + v.Add("can_add_web_page_previews", strconv.FormatBool(*config.CanAddWebPagePreviews)) + } + if config.UntilDate != 0 { + v.Add("until_date", strconv.FormatInt(config.UntilDate, 10)) + } + + bot.debugLog("restrictChatMember", v, nil) + + return bot.MakeRequest("restrictChatMember", v) +} + +// PromoteChatMember add admin rights to user +func (bot *BotAPI) PromoteChatMember(config PromoteChatMemberConfig) (APIResponse, error) { + v := url.Values{} + + if config.SuperGroupUsername != "" { + v.Add("chat_id", config.SuperGroupUsername) + } else if config.ChannelUsername != "" { + v.Add("chat_id", config.ChannelUsername) + } else { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } + v.Add("user_id", strconv.Itoa(config.UserID)) + + if config.CanChangeInfo != nil { + v.Add("can_change_info", strconv.FormatBool(*config.CanChangeInfo)) + } + if config.CanPostMessages != nil { + v.Add("can_post_messages", strconv.FormatBool(*config.CanPostMessages)) + } + if config.CanEditMessages != nil { + v.Add("can_edit_messages", strconv.FormatBool(*config.CanEditMessages)) + } + if config.CanDeleteMessages != nil { + v.Add("can_delete_messages", strconv.FormatBool(*config.CanDeleteMessages)) + } + if config.CanInviteUsers != nil { + v.Add("can_invite_users", strconv.FormatBool(*config.CanInviteUsers)) + } + if config.CanRestrictMembers != nil { + v.Add("can_restrict_members", strconv.FormatBool(*config.CanRestrictMembers)) + } + if config.CanPinMessages != nil { + v.Add("can_pin_messages", strconv.FormatBool(*config.CanPinMessages)) + } + if config.CanPromoteMembers != nil { + v.Add("can_promote_members", strconv.FormatBool(*config.CanPromoteMembers)) + } + + bot.debugLog("promoteChatMember", v, nil) + + return bot.MakeRequest("promoteChatMember", v) +} + +// GetGameHighScores allows you to get the high scores for a game. +func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) { + v, _ := config.values() + + resp, err := bot.MakeRequest(config.method(), v) + if err != nil { + return []GameHighScore{}, err + } + + var highScores []GameHighScore + err = json.Unmarshal(resp.Result, &highScores) + + return highScores, err +} + +// AnswerShippingQuery allows you to reply to Update with shipping_query parameter. +func (bot *BotAPI) AnswerShippingQuery(config ShippingConfig) (APIResponse, error) { + v := url.Values{} + + v.Add("shipping_query_id", config.ShippingQueryID) + v.Add("ok", strconv.FormatBool(config.OK)) + if config.OK == true { + data, err := json.Marshal(config.ShippingOptions) + if err != nil { + return APIResponse{}, err + } + v.Add("shipping_options", string(data)) + } else { + v.Add("error_message", config.ErrorMessage) + } + + bot.debugLog("answerShippingQuery", v, nil) + + return bot.MakeRequest("answerShippingQuery", v) +} + +// AnswerPreCheckoutQuery allows you to reply to Update with pre_checkout_query. +func (bot *BotAPI) AnswerPreCheckoutQuery(config PreCheckoutConfig) (APIResponse, error) { + v := url.Values{} + + v.Add("pre_checkout_query_id", config.PreCheckoutQueryID) + v.Add("ok", strconv.FormatBool(config.OK)) + if config.OK != true { + v.Add("error", config.ErrorMessage) + } + + bot.debugLog("answerPreCheckoutQuery", v, nil) + + return bot.MakeRequest("answerPreCheckoutQuery", v) +} + +// DeleteMessage deletes a message in a chat +func (bot *BotAPI) DeleteMessage(config DeleteMessageConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} + +// GetInviteLink get InviteLink for a chat +func (bot *BotAPI) GetInviteLink(config ChatConfig) (string, error) { + v := url.Values{} + + if config.SuperGroupUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.SuperGroupUsername) + } + + resp, err := bot.MakeRequest("exportChatInviteLink", v) + if err != nil { + return "", err + } + + var inviteLink string + err = json.Unmarshal(resp.Result, &inviteLink) + + return inviteLink, err +} + +// PinChatMessage pin message in supergroup +func (bot *BotAPI) PinChatMessage(config PinChatMessageConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} + +// UnpinChatMessage unpin message in supergroup +func (bot *BotAPI) UnpinChatMessage(config UnpinChatMessageConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} + +// SetChatTitle change title of chat. +func (bot *BotAPI) SetChatTitle(config SetChatTitleConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} + +// SetChatDescription change description of chat. +func (bot *BotAPI) SetChatDescription(config SetChatDescriptionConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} + +// SetChatPhoto change photo of chat. +func (bot *BotAPI) SetChatPhoto(config SetChatPhotoConfig) (APIResponse, error) { + params, err := config.params() + if err != nil { + return APIResponse{}, err + } + + file := config.getFile() + + return bot.UploadFile(config.method(), params, config.name(), file) +} + +// DeleteChatPhoto delete photo of chat. +func (bot *BotAPI) DeleteChatPhoto(config DeleteChatPhotoConfig) (APIResponse, error) { + v, err := config.values() + if err != nil { + return APIResponse{}, err + } + + bot.debugLog(config.method(), v, nil) + + return bot.MakeRequest(config.method(), v) +} diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot_test.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot_test.go new file mode 100644 index 0000000..e7aa7ac --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/bot_test.go @@ -0,0 +1,682 @@ +package tgbotapi_test + +import ( + "io/ioutil" + "log" + "net/http" + "os" + "testing" + "time" + + "github.com/go-telegram-bot-api/telegram-bot-api" +) + +const ( + TestToken = "153667468:AAHlSHlMqSt1f_uFmVRJbm5gntu2HI4WW8I" + ChatID = 76918703 + SupergroupChatID = -1001120141283 + ReplyToMessageID = 35 + ExistingPhotoFileID = "AgADAgADw6cxG4zHKAkr42N7RwEN3IFShCoABHQwXEtVks4EH2wBAAEC" + ExistingDocumentFileID = "BQADAgADOQADjMcoCcioX1GrDvp3Ag" + ExistingAudioFileID = "BQADAgADRgADjMcoCdXg3lSIN49lAg" + ExistingVoiceFileID = "AwADAgADWQADjMcoCeul6r_q52IyAg" + ExistingVideoFileID = "BAADAgADZgADjMcoCav432kYe0FRAg" + ExistingVideoNoteFileID = "DQADAgADdQAD70cQSUK41dLsRMqfAg" + ExistingStickerFileID = "BQADAgADcwADjMcoCbdl-6eB--YPAg" +) + +func getBot(t *testing.T) (*tgbotapi.BotAPI, error) { + bot, err := tgbotapi.NewBotAPI(TestToken) + bot.Debug = true + + if err != nil { + t.Error(err) + t.Fail() + } + + return bot, err +} + +func TestNewBotAPI_notoken(t *testing.T) { + _, err := tgbotapi.NewBotAPI("") + + if err == nil { + t.Error(err) + t.Fail() + } +} + +func TestGetUpdates(t *testing.T) { + bot, _ := getBot(t) + + u := tgbotapi.NewUpdate(0) + + _, err := bot.GetUpdates(u) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = "markdown" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithMessageReply(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + msg.ReplyToMessageID = ReplyToMessageID + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithMessageForward(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewForward(ChatID, ChatID, ReplyToMessageID) + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewPhoto(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewPhotoUpload(ChatID, "tests/image.jpg") + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewPhotoWithFileBytes(t *testing.T) { + bot, _ := getBot(t) + + data, _ := ioutil.ReadFile("tests/image.jpg") + b := tgbotapi.FileBytes{Name: "image.jpg", Bytes: data} + + msg := tgbotapi.NewPhotoUpload(ChatID, b) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewPhotoWithFileReader(t *testing.T) { + bot, _ := getBot(t) + + f, _ := os.Open("tests/image.jpg") + reader := tgbotapi.FileReader{Name: "image.jpg", Reader: f, Size: -1} + + msg := tgbotapi.NewPhotoUpload(ChatID, reader) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewPhotoReply(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewPhotoUpload(ChatID, "tests/image.jpg") + msg.ReplyToMessageID = ReplyToMessageID + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithExistingPhoto(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewPhotoShare(ChatID, ExistingPhotoFileID) + msg.Caption = "Test" + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewDocument(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewDocumentUpload(ChatID, "tests/image.jpg") + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithExistingDocument(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewDocumentShare(ChatID, ExistingDocumentFileID) + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewAudio(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewAudioUpload(ChatID, "tests/audio.mp3") + msg.Title = "TEST" + msg.Duration = 10 + msg.Performer = "TEST" + msg.MimeType = "audio/mpeg" + msg.FileSize = 688 + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithExistingAudio(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewAudioShare(ChatID, ExistingAudioFileID) + msg.Title = "TEST" + msg.Duration = 10 + msg.Performer = "TEST" + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewVoice(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVoiceUpload(ChatID, "tests/voice.ogg") + msg.Duration = 10 + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithExistingVoice(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVoiceShare(ChatID, ExistingVoiceFileID) + msg.Duration = 10 + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithContact(t *testing.T) { + bot, _ := getBot(t) + + contact := tgbotapi.NewContact(ChatID, "5551234567", "Test") + + if _, err := bot.Send(contact); err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithLocation(t *testing.T) { + bot, _ := getBot(t) + + _, err := bot.Send(tgbotapi.NewLocation(ChatID, 40, 40)) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithVenue(t *testing.T) { + bot, _ := getBot(t) + + venue := tgbotapi.NewVenue(ChatID, "A Test Location", "123 Test Street", 40, 40) + + if _, err := bot.Send(venue); err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewVideo(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVideoUpload(ChatID, "tests/video.mp4") + msg.Duration = 10 + msg.Caption = "TEST" + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithExistingVideo(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVideoShare(ChatID, ExistingVideoFileID) + msg.Duration = 10 + msg.Caption = "TEST" + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewVideoNote(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVideoNoteUpload(ChatID, 240, "tests/videonote.mp4") + msg.Duration = 10 + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithExistingVideoNote(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewVideoNoteShare(ChatID, 240, ExistingVideoNoteFileID) + msg.Duration = 10 + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewSticker(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg") + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithExistingSticker(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID) + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithNewStickerAndKeyboardHide(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewStickerUpload(ChatID, "tests/image.jpg") + msg.ReplyMarkup = tgbotapi.ReplyKeyboardRemove{ + RemoveKeyboard: true, + Selective: false, + } + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendWithExistingStickerAndKeyboardHide(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewStickerShare(ChatID, ExistingStickerFileID) + msg.ReplyMarkup = tgbotapi.ReplyKeyboardRemove{ + RemoveKeyboard: true, + Selective: false, + } + + _, err := bot.Send(msg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestGetFile(t *testing.T) { + bot, _ := getBot(t) + + file := tgbotapi.FileConfig{FileID: ExistingPhotoFileID} + + _, err := bot.GetFile(file) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendChatConfig(t *testing.T) { + bot, _ := getBot(t) + + _, err := bot.Send(tgbotapi.NewChatAction(ChatID, tgbotapi.ChatTyping)) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSendEditMessage(t *testing.T) { + bot, _ := getBot(t) + + msg, err := bot.Send(tgbotapi.NewMessage(ChatID, "Testing editing.")) + if err != nil { + t.Error(err) + t.Fail() + } + + edit := tgbotapi.EditMessageTextConfig{ + BaseEdit: tgbotapi.BaseEdit{ + ChatID: ChatID, + MessageID: msg.MessageID, + }, + Text: "Updated text.", + } + + _, err = bot.Send(edit) + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestGetUserProfilePhotos(t *testing.T) { + bot, _ := getBot(t) + + _, err := bot.GetUserProfilePhotos(tgbotapi.NewUserProfilePhotos(ChatID)) + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestSetWebhookWithCert(t *testing.T) { + bot, _ := getBot(t) + + time.Sleep(time.Second * 2) + + bot.RemoveWebhook() + + wh := tgbotapi.NewWebhookWithCert("https://example.com/tgbotapi-test/"+bot.Token, "tests/cert.pem") + _, err := bot.SetWebhook(wh) + if err != nil { + t.Error(err) + t.Fail() + } + info, err := bot.GetWebhookInfo() + if err != nil { + t.Error(err) + } + if info.LastErrorDate != 0 { + t.Errorf("[Telegram callback failed]%s", info.LastErrorMessage) + } + bot.RemoveWebhook() +} + +func TestSetWebhookWithoutCert(t *testing.T) { + bot, _ := getBot(t) + + time.Sleep(time.Second * 2) + + bot.RemoveWebhook() + + wh := tgbotapi.NewWebhook("https://example.com/tgbotapi-test/" + bot.Token) + _, err := bot.SetWebhook(wh) + if err != nil { + t.Error(err) + t.Fail() + } + info, err := bot.GetWebhookInfo() + if err != nil { + t.Error(err) + } + if info.LastErrorDate != 0 { + t.Errorf("[Telegram callback failed]%s", info.LastErrorMessage) + } + bot.RemoveWebhook() +} + +func TestUpdatesChan(t *testing.T) { + bot, _ := getBot(t) + + var ucfg tgbotapi.UpdateConfig = tgbotapi.NewUpdate(0) + ucfg.Timeout = 60 + _, err := bot.GetUpdatesChan(ucfg) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func ExampleNewBotAPI() { + bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") + if err != nil { + log.Panic(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates, err := bot.GetUpdatesChan(u) + + // Optional: wait for updates and clear them if you don't want to handle + // a large backlog of old messages + time.Sleep(time.Millisecond * 500) + updates.Clear() + + for update := range updates { + if update.Message == nil { + continue + } + + log.Printf("[%s] %s", update.Message.From.UserName, update.Message.Text) + + msg := tgbotapi.NewMessage(update.Message.Chat.ID, update.Message.Text) + msg.ReplyToMessageID = update.Message.MessageID + + bot.Send(msg) + } +} + +func ExampleNewWebhook() { + bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") + if err != nil { + log.Fatal(err) + } + + bot.Debug = true + + log.Printf("Authorized on account %s", bot.Self.UserName) + + _, err = bot.SetWebhook(tgbotapi.NewWebhookWithCert("https://www.google.com:8443/"+bot.Token, "cert.pem")) + if err != nil { + log.Fatal(err) + } + info, err := bot.GetWebhookInfo() + if err != nil { + log.Fatal(err) + } + if info.LastErrorDate != 0 { + log.Printf("[Telegram callback failed]%s", info.LastErrorMessage) + } + updates := bot.ListenForWebhook("/" + bot.Token) + go http.ListenAndServeTLS("0.0.0.0:8443", "cert.pem", "key.pem", nil) + + for update := range updates { + log.Printf("%+v\n", update) + } +} + +func ExampleAnswerInlineQuery() { + bot, err := tgbotapi.NewBotAPI("MyAwesomeBotToken") // create new bot + if err != nil { + log.Panic(err) + } + + log.Printf("Authorized on account %s", bot.Self.UserName) + + u := tgbotapi.NewUpdate(0) + u.Timeout = 60 + + updates, err := bot.GetUpdatesChan(u) + + for update := range updates { + if update.InlineQuery == nil { // if no inline query, ignore it + continue + } + + article := tgbotapi.NewInlineQueryResultArticle(update.InlineQuery.ID, "Echo", update.InlineQuery.Query) + article.Description = update.InlineQuery.Query + + inlineConf := tgbotapi.InlineConfig{ + InlineQueryID: update.InlineQuery.ID, + IsPersonal: true, + CacheTime: 0, + Results: []interface{}{article}, + } + + if _, err := bot.AnswerInlineQuery(inlineConf); err != nil { + log.Println(err) + } + } +} + +func TestDeleteMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(ChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = "markdown" + message, _ := bot.Send(msg) + + deleteMessageConfig := tgbotapi.DeleteMessageConfig{ + ChatID: message.Chat.ID, + MessageID: message.MessageID, + } + _, err := bot.DeleteMessage(deleteMessageConfig) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestPinChatMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = "markdown" + message, _ := bot.Send(msg) + + pinChatMessageConfig := tgbotapi.PinChatMessageConfig{ + ChatID: message.Chat.ID, + MessageID: message.MessageID, + DisableNotification: false, + } + _, err := bot.PinChatMessage(pinChatMessageConfig) + + if err != nil { + t.Error(err) + t.Fail() + } +} + +func TestUnpinChatMessage(t *testing.T) { + bot, _ := getBot(t) + + msg := tgbotapi.NewMessage(SupergroupChatID, "A test message from the test library in telegram-bot-api") + msg.ParseMode = "markdown" + message, _ := bot.Send(msg) + + // We need pin message to unpin something + pinChatMessageConfig := tgbotapi.PinChatMessageConfig{ + ChatID: message.Chat.ID, + MessageID: message.MessageID, + DisableNotification: false, + } + _, err := bot.PinChatMessage(pinChatMessageConfig) + + unpinChatMessageConfig := tgbotapi.UnpinChatMessageConfig{ + ChatID: message.Chat.ID, + } + _, err = bot.UnpinChatMessage(unpinChatMessageConfig) + + if err != nil { + t.Error(err) + t.Fail() + } +} diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/configs.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/configs.go new file mode 100644 index 0000000..574b3dd --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/configs.go @@ -0,0 +1,1145 @@ +package tgbotapi + +import ( + "encoding/json" + "io" + "net/url" + "strconv" +) + +// Telegram constants +const ( + // APIEndpoint is the endpoint for all API methods, + // with formatting for Sprintf. + APIEndpoint = "https://api.telegram.org/bot%s/%s" + // FileEndpoint is the endpoint for downloading a file from Telegram. + FileEndpoint = "https://api.telegram.org/file/bot%s/%s" +) + +// Constant values for ChatActions +const ( + ChatTyping = "typing" + ChatUploadPhoto = "upload_photo" + ChatRecordVideo = "record_video" + ChatUploadVideo = "upload_video" + ChatRecordAudio = "record_audio" + ChatUploadAudio = "upload_audio" + ChatUploadDocument = "upload_document" + ChatFindLocation = "find_location" +) + +// API errors +const ( + // ErrAPIForbidden happens when a token is bad + ErrAPIForbidden = "forbidden" +) + +// Constant values for ParseMode in MessageConfig +const ( + ModeMarkdown = "Markdown" + ModeHTML = "HTML" +) + +// Library errors +const ( + // ErrBadFileType happens when you pass an unknown type + ErrBadFileType = "bad file type" + ErrBadURL = "bad or empty url" +) + +// Chattable is any config type that can be sent. +type Chattable interface { + values() (url.Values, error) + method() string +} + +// Fileable is any config type that can be sent that includes a file. +type Fileable interface { + Chattable + params() (map[string]string, error) + name() string + getFile() interface{} + useExistingFile() bool +} + +// BaseChat is base type for all chat config types. +type BaseChat struct { + ChatID int64 // required + ChannelUsername string + ReplyToMessageID int + ReplyMarkup interface{} + DisableNotification bool +} + +// values returns url.Values representation of BaseChat +func (chat *BaseChat) values() (url.Values, error) { + v := url.Values{} + if chat.ChannelUsername != "" { + v.Add("chat_id", chat.ChannelUsername) + } else { + v.Add("chat_id", strconv.FormatInt(chat.ChatID, 10)) + } + + if chat.ReplyToMessageID != 0 { + v.Add("reply_to_message_id", strconv.Itoa(chat.ReplyToMessageID)) + } + + if chat.ReplyMarkup != nil { + data, err := json.Marshal(chat.ReplyMarkup) + if err != nil { + return v, err + } + + v.Add("reply_markup", string(data)) + } + + v.Add("disable_notification", strconv.FormatBool(chat.DisableNotification)) + + return v, nil +} + +// BaseFile is a base type for all file config types. +type BaseFile struct { + BaseChat + File interface{} + FileID string + UseExisting bool + MimeType string + FileSize int +} + +// params returns a map[string]string representation of BaseFile. +func (file BaseFile) params() (map[string]string, error) { + params := make(map[string]string) + + if file.ChannelUsername != "" { + params["chat_id"] = file.ChannelUsername + } else { + params["chat_id"] = strconv.FormatInt(file.ChatID, 10) + } + + if file.ReplyToMessageID != 0 { + params["reply_to_message_id"] = strconv.Itoa(file.ReplyToMessageID) + } + + if file.ReplyMarkup != nil { + data, err := json.Marshal(file.ReplyMarkup) + if err != nil { + return params, err + } + + params["reply_markup"] = string(data) + } + + if file.MimeType != "" { + params["mime_type"] = file.MimeType + } + + if file.FileSize > 0 { + params["file_size"] = strconv.Itoa(file.FileSize) + } + + params["disable_notification"] = strconv.FormatBool(file.DisableNotification) + + return params, nil +} + +// getFile returns the file. +func (file BaseFile) getFile() interface{} { + return file.File +} + +// useExistingFile returns if the BaseFile has already been uploaded. +func (file BaseFile) useExistingFile() bool { + return file.UseExisting +} + +// BaseEdit is base type of all chat edits. +type BaseEdit struct { + ChatID int64 + ChannelUsername string + MessageID int + InlineMessageID string + ReplyMarkup *InlineKeyboardMarkup +} + +func (edit BaseEdit) values() (url.Values, error) { + v := url.Values{} + + if edit.InlineMessageID == "" { + if edit.ChannelUsername != "" { + v.Add("chat_id", edit.ChannelUsername) + } else { + v.Add("chat_id", strconv.FormatInt(edit.ChatID, 10)) + } + v.Add("message_id", strconv.Itoa(edit.MessageID)) + } else { + v.Add("inline_message_id", edit.InlineMessageID) + } + + if edit.ReplyMarkup != nil { + data, err := json.Marshal(edit.ReplyMarkup) + if err != nil { + return v, err + } + v.Add("reply_markup", string(data)) + } + + return v, nil +} + +// MessageConfig contains information about a SendMessage request. +type MessageConfig struct { + BaseChat + Text string + ParseMode string + DisableWebPagePreview bool +} + +// values returns a url.Values representation of MessageConfig. +func (config MessageConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + v.Add("text", config.Text) + v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview)) + if config.ParseMode != "" { + v.Add("parse_mode", config.ParseMode) + } + + return v, nil +} + +// method returns Telegram API method name for sending Message. +func (config MessageConfig) method() string { + return "sendMessage" +} + +// ForwardConfig contains information about a ForwardMessage request. +type ForwardConfig struct { + BaseChat + FromChatID int64 // required + FromChannelUsername string + MessageID int // required +} + +// values returns a url.Values representation of ForwardConfig. +func (config ForwardConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + v.Add("from_chat_id", strconv.FormatInt(config.FromChatID, 10)) + v.Add("message_id", strconv.Itoa(config.MessageID)) + return v, nil +} + +// method returns Telegram API method name for sending Forward. +func (config ForwardConfig) method() string { + return "forwardMessage" +} + +// PhotoConfig contains information about a SendPhoto request. +type PhotoConfig struct { + BaseFile + Caption string +} + +// Params returns a map[string]string representation of PhotoConfig. +func (config PhotoConfig) params() (map[string]string, error) { + params, _ := config.BaseFile.params() + + if config.Caption != "" { + params["caption"] = config.Caption + } + + return params, nil +} + +// Values returns a url.Values representation of PhotoConfig. +func (config PhotoConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add(config.name(), config.FileID) + if config.Caption != "" { + v.Add("caption", config.Caption) + } + return v, nil +} + +// name returns the field name for the Photo. +func (config PhotoConfig) name() string { + return "photo" +} + +// method returns Telegram API method name for sending Photo. +func (config PhotoConfig) method() string { + return "sendPhoto" +} + +// AudioConfig contains information about a SendAudio request. +type AudioConfig struct { + BaseFile + Caption string + Duration int + Performer string + Title string +} + +// values returns a url.Values representation of AudioConfig. +func (config AudioConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add(config.name(), config.FileID) + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + + if config.Performer != "" { + v.Add("performer", config.Performer) + } + if config.Title != "" { + v.Add("title", config.Title) + } + if config.Caption != "" { + v.Add("caption", config.Caption) + } + + return v, nil +} + +// params returns a map[string]string representation of AudioConfig. +func (config AudioConfig) params() (map[string]string, error) { + params, _ := config.BaseFile.params() + + if config.Duration != 0 { + params["duration"] = strconv.Itoa(config.Duration) + } + + if config.Performer != "" { + params["performer"] = config.Performer + } + if config.Title != "" { + params["title"] = config.Title + } + if config.Caption != "" { + params["caption"] = config.Caption + } + + return params, nil +} + +// name returns the field name for the Audio. +func (config AudioConfig) name() string { + return "audio" +} + +// method returns Telegram API method name for sending Audio. +func (config AudioConfig) method() string { + return "sendAudio" +} + +// DocumentConfig contains information about a SendDocument request. +type DocumentConfig struct { + BaseFile + Caption string +} + +// values returns a url.Values representation of DocumentConfig. +func (config DocumentConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add(config.name(), config.FileID) + if config.Caption != "" { + v.Add("caption", config.Caption) + } + + return v, nil +} + +// params returns a map[string]string representation of DocumentConfig. +func (config DocumentConfig) params() (map[string]string, error) { + params, _ := config.BaseFile.params() + + if config.Caption != "" { + params["caption"] = config.Caption + } + + return params, nil +} + +// name returns the field name for the Document. +func (config DocumentConfig) name() string { + return "document" +} + +// method returns Telegram API method name for sending Document. +func (config DocumentConfig) method() string { + return "sendDocument" +} + +// StickerConfig contains information about a SendSticker request. +type StickerConfig struct { + BaseFile +} + +// values returns a url.Values representation of StickerConfig. +func (config StickerConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add(config.name(), config.FileID) + + return v, nil +} + +// params returns a map[string]string representation of StickerConfig. +func (config StickerConfig) params() (map[string]string, error) { + params, _ := config.BaseFile.params() + + return params, nil +} + +// name returns the field name for the Sticker. +func (config StickerConfig) name() string { + return "sticker" +} + +// method returns Telegram API method name for sending Sticker. +func (config StickerConfig) method() string { + return "sendSticker" +} + +// VideoConfig contains information about a SendVideo request. +type VideoConfig struct { + BaseFile + Duration int + Caption string +} + +// values returns a url.Values representation of VideoConfig. +func (config VideoConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add(config.name(), config.FileID) + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + if config.Caption != "" { + v.Add("caption", config.Caption) + } + + return v, nil +} + +// params returns a map[string]string representation of VideoConfig. +func (config VideoConfig) params() (map[string]string, error) { + params, _ := config.BaseFile.params() + + if config.Caption != "" { + params["caption"] = config.Caption + } + + return params, nil +} + +// name returns the field name for the Video. +func (config VideoConfig) name() string { + return "video" +} + +// method returns Telegram API method name for sending Video. +func (config VideoConfig) method() string { + return "sendVideo" +} + +// VideoNoteConfig contains information about a SendVideoNote request. +type VideoNoteConfig struct { + BaseFile + Duration int + Length int +} + +// values returns a url.Values representation of VideoNoteConfig. +func (config VideoNoteConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add(config.name(), config.FileID) + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + + // Telegram API seems to have a bug, if no length is provided or it is 0, it will send an error response + if config.Length != 0 { + v.Add("length", strconv.Itoa(config.Length)) + } + + return v, nil +} + +// params returns a map[string]string representation of VideoNoteConfig. +func (config VideoNoteConfig) params() (map[string]string, error) { + params, _ := config.BaseFile.params() + + if config.Length != 0 { + params["length"] = strconv.Itoa(config.Length) + } + if config.Duration != 0 { + params["duration"] = strconv.Itoa(config.Duration) + } + + return params, nil +} + +// name returns the field name for the VideoNote. +func (config VideoNoteConfig) name() string { + return "video_note" +} + +// method returns Telegram API method name for sending VideoNote. +func (config VideoNoteConfig) method() string { + return "sendVideoNote" +} + +// VoiceConfig contains information about a SendVoice request. +type VoiceConfig struct { + BaseFile + Caption string + Duration int +} + +// values returns a url.Values representation of VoiceConfig. +func (config VoiceConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add(config.name(), config.FileID) + if config.Duration != 0 { + v.Add("duration", strconv.Itoa(config.Duration)) + } + if config.Caption != "" { + v.Add("caption", config.Caption) + } + + return v, nil +} + +// params returns a map[string]string representation of VoiceConfig. +func (config VoiceConfig) params() (map[string]string, error) { + params, _ := config.BaseFile.params() + + if config.Duration != 0 { + params["duration"] = strconv.Itoa(config.Duration) + } + if config.Caption != "" { + params["caption"] = config.Caption + } + + return params, nil +} + +// name returns the field name for the Voice. +func (config VoiceConfig) name() string { + return "voice" +} + +// method returns Telegram API method name for sending Voice. +func (config VoiceConfig) method() string { + return "sendVoice" +} + +// LocationConfig contains information about a SendLocation request. +type LocationConfig struct { + BaseChat + Latitude float64 // required + Longitude float64 // required +} + +// values returns a url.Values representation of LocationConfig. +func (config LocationConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64)) + v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64)) + + return v, nil +} + +// method returns Telegram API method name for sending Location. +func (config LocationConfig) method() string { + return "sendLocation" +} + +// VenueConfig contains information about a SendVenue request. +type VenueConfig struct { + BaseChat + Latitude float64 // required + Longitude float64 // required + Title string // required + Address string // required + FoursquareID string +} + +func (config VenueConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add("latitude", strconv.FormatFloat(config.Latitude, 'f', 6, 64)) + v.Add("longitude", strconv.FormatFloat(config.Longitude, 'f', 6, 64)) + v.Add("title", config.Title) + v.Add("address", config.Address) + if config.FoursquareID != "" { + v.Add("foursquare_id", config.FoursquareID) + } + + return v, nil +} + +func (config VenueConfig) method() string { + return "sendVenue" +} + +// ContactConfig allows you to send a contact. +type ContactConfig struct { + BaseChat + PhoneNumber string + FirstName string + LastName string +} + +func (config ContactConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add("phone_number", config.PhoneNumber) + v.Add("first_name", config.FirstName) + v.Add("last_name", config.LastName) + + return v, nil +} + +func (config ContactConfig) method() string { + return "sendContact" +} + +// GameConfig allows you to send a game. +type GameConfig struct { + BaseChat + GameShortName string +} + +func (config GameConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + + v.Add("game_short_name", config.GameShortName) + + return v, nil +} + +func (config GameConfig) method() string { + return "sendGame" +} + +// SetGameScoreConfig allows you to update the game score in a chat. +type SetGameScoreConfig struct { + UserID int + Score int + Force bool + DisableEditMessage bool + ChatID int64 + ChannelUsername string + MessageID int + InlineMessageID string +} + +func (config SetGameScoreConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("user_id", strconv.Itoa(config.UserID)) + v.Add("score", strconv.Itoa(config.Score)) + if config.InlineMessageID == "" { + if config.ChannelUsername == "" { + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + } else { + v.Add("chat_id", config.ChannelUsername) + } + v.Add("message_id", strconv.Itoa(config.MessageID)) + } else { + v.Add("inline_message_id", config.InlineMessageID) + } + v.Add("disable_edit_message", strconv.FormatBool(config.DisableEditMessage)) + + return v, nil +} + +func (config SetGameScoreConfig) method() string { + return "setGameScore" +} + +// GetGameHighScoresConfig allows you to fetch the high scores for a game. +type GetGameHighScoresConfig struct { + UserID int + ChatID int + ChannelUsername string + MessageID int + InlineMessageID string +} + +func (config GetGameHighScoresConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("user_id", strconv.Itoa(config.UserID)) + if config.InlineMessageID == "" { + if config.ChannelUsername == "" { + v.Add("chat_id", strconv.Itoa(config.ChatID)) + } else { + v.Add("chat_id", config.ChannelUsername) + } + v.Add("message_id", strconv.Itoa(config.MessageID)) + } else { + v.Add("inline_message_id", config.InlineMessageID) + } + + return v, nil +} + +func (config GetGameHighScoresConfig) method() string { + return "getGameHighScores" +} + +// ChatActionConfig contains information about a SendChatAction request. +type ChatActionConfig struct { + BaseChat + Action string // required +} + +// values returns a url.Values representation of ChatActionConfig. +func (config ChatActionConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + v.Add("action", config.Action) + return v, nil +} + +// method returns Telegram API method name for sending ChatAction. +func (config ChatActionConfig) method() string { + return "sendChatAction" +} + +// EditMessageTextConfig allows you to modify the text in a message. +type EditMessageTextConfig struct { + BaseEdit + Text string + ParseMode string + DisableWebPagePreview bool +} + +func (config EditMessageTextConfig) values() (url.Values, error) { + v, err := config.BaseEdit.values() + if err != nil { + return v, err + } + + v.Add("text", config.Text) + v.Add("parse_mode", config.ParseMode) + v.Add("disable_web_page_preview", strconv.FormatBool(config.DisableWebPagePreview)) + + return v, nil +} + +func (config EditMessageTextConfig) method() string { + return "editMessageText" +} + +// EditMessageCaptionConfig allows you to modify the caption of a message. +type EditMessageCaptionConfig struct { + BaseEdit + Caption string +} + +func (config EditMessageCaptionConfig) values() (url.Values, error) { + v, _ := config.BaseEdit.values() + + v.Add("caption", config.Caption) + + return v, nil +} + +func (config EditMessageCaptionConfig) method() string { + return "editMessageCaption" +} + +// EditMessageReplyMarkupConfig allows you to modify the reply markup +// of a message. +type EditMessageReplyMarkupConfig struct { + BaseEdit +} + +func (config EditMessageReplyMarkupConfig) values() (url.Values, error) { + return config.BaseEdit.values() +} + +func (config EditMessageReplyMarkupConfig) method() string { + return "editMessageReplyMarkup" +} + +// UserProfilePhotosConfig contains information about a +// GetUserProfilePhotos request. +type UserProfilePhotosConfig struct { + UserID int + Offset int + Limit int +} + +// FileConfig has information about a file hosted on Telegram. +type FileConfig struct { + FileID string +} + +// UpdateConfig contains information about a GetUpdates request. +type UpdateConfig struct { + Offset int + Limit int + Timeout int +} + +// WebhookConfig contains information about a SetWebhook request. +type WebhookConfig struct { + URL *url.URL + Certificate interface{} + MaxConnections int +} + +// FileBytes contains information about a set of bytes to upload +// as a File. +type FileBytes struct { + Name string + Bytes []byte +} + +// FileReader contains information about a reader to upload as a File. +// If Size is -1, it will read the entire Reader into memory to +// calculate a Size. +type FileReader struct { + Name string + Reader io.Reader + Size int64 +} + +// InlineConfig contains information on making an InlineQuery response. +type InlineConfig struct { + InlineQueryID string `json:"inline_query_id"` + Results []interface{} `json:"results"` + CacheTime int `json:"cache_time"` + IsPersonal bool `json:"is_personal"` + NextOffset string `json:"next_offset"` + SwitchPMText string `json:"switch_pm_text"` + SwitchPMParameter string `json:"switch_pm_parameter"` +} + +// CallbackConfig contains information on making a CallbackQuery response. +type CallbackConfig struct { + CallbackQueryID string `json:"callback_query_id"` + Text string `json:"text"` + ShowAlert bool `json:"show_alert"` + URL string `json:"url"` + CacheTime int `json:"cache_time"` +} + +// ChatMemberConfig contains information about a user in a chat for use +// with administrative functions such as kicking or unbanning a user. +type ChatMemberConfig struct { + ChatID int64 + SuperGroupUsername string + ChannelUsername string + UserID int +} + +// KickChatMemberConfig contains extra fields to kick user +type KickChatMemberConfig struct { + ChatMemberConfig + UntilDate int64 +} + +// RestrictChatMemberConfig contains fields to restrict members of chat +type RestrictChatMemberConfig struct { + ChatMemberConfig + UntilDate int64 + CanSendMessages *bool + CanSendMediaMessages *bool + CanSendOtherMessages *bool + CanAddWebPagePreviews *bool +} + +// PromoteChatMemberConfig contains fields to promote members of chat +type PromoteChatMemberConfig struct { + ChatMemberConfig + CanChangeInfo *bool + CanPostMessages *bool + CanEditMessages *bool + CanDeleteMessages *bool + CanInviteUsers *bool + CanRestrictMembers *bool + CanPinMessages *bool + CanPromoteMembers *bool +} + +// ChatConfig contains information about getting information on a chat. +type ChatConfig struct { + ChatID int64 + SuperGroupUsername string +} + +// ChatConfigWithUser contains information about getting information on +// a specific user within a chat. +type ChatConfigWithUser struct { + ChatID int64 + SuperGroupUsername string + UserID int +} + +// InvoiceConfig contains information for sendInvoice request. +type InvoiceConfig struct { + BaseChat + Title string // required + Description string // required + Payload string // required + ProviderToken string // required + StartParameter string // required + Currency string // required + Prices *[]LabeledPrice // required + PhotoURL string + PhotoSize int + PhotoWidth int + PhotoHeight int + NeedName bool + NeedPhoneNumber bool + NeedEmail bool + NeedShippingAddress bool + IsFlexible bool +} + +func (config InvoiceConfig) values() (url.Values, error) { + v, err := config.BaseChat.values() + if err != nil { + return v, err + } + v.Add("title", config.Title) + v.Add("description", config.Description) + v.Add("payload", config.Payload) + v.Add("provider_token", config.ProviderToken) + v.Add("start_parameter", config.StartParameter) + v.Add("currency", config.Currency) + data, err := json.Marshal(config.Prices) + if err != nil { + return v, err + } + v.Add("prices", string(data)) + if config.PhotoURL != "" { + v.Add("photo_url", config.PhotoURL) + } + if config.PhotoSize != 0 { + v.Add("photo_size", strconv.Itoa(config.PhotoSize)) + } + if config.PhotoWidth != 0 { + v.Add("photo_width", strconv.Itoa(config.PhotoWidth)) + } + if config.PhotoHeight != 0 { + v.Add("photo_height", strconv.Itoa(config.PhotoHeight)) + } + if config.NeedName != false { + v.Add("need_name", strconv.FormatBool(config.NeedName)) + } + if config.NeedPhoneNumber != false { + v.Add("need_phone_number", strconv.FormatBool(config.NeedPhoneNumber)) + } + if config.NeedEmail != false { + v.Add("need_email", strconv.FormatBool(config.NeedEmail)) + } + if config.NeedShippingAddress != false { + v.Add("need_shipping_address", strconv.FormatBool(config.NeedShippingAddress)) + } + if config.IsFlexible != false { + v.Add("is_flexible", strconv.FormatBool(config.IsFlexible)) + } + + return v, nil +} + +func (config InvoiceConfig) method() string { + return "sendInvoice" +} + +// ShippingConfig contains information for answerShippingQuery request. +type ShippingConfig struct { + ShippingQueryID string // required + OK bool // required + ShippingOptions *[]ShippingOption + ErrorMessage string +} + +// PreCheckoutConfig conatins information for answerPreCheckoutQuery request. +type PreCheckoutConfig struct { + PreCheckoutQueryID string // required + OK bool // required + ErrorMessage string +} + +// DeleteMessageConfig contains information of a message in a chat to delete. +type DeleteMessageConfig struct { + ChatID int64 + MessageID int +} + +func (config DeleteMessageConfig) method() string { + return "deleteMessage" +} + +func (config DeleteMessageConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + v.Add("message_id", strconv.Itoa(config.MessageID)) + + return v, nil +} + +// PinChatMessageConfig contains information of a message in a chat to pin. +type PinChatMessageConfig struct { + ChatID int64 + MessageID int + DisableNotification bool +} + +func (config PinChatMessageConfig) method() string { + return "pinChatMessage" +} + +func (config PinChatMessageConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + v.Add("message_id", strconv.Itoa(config.MessageID)) + v.Add("disable_notification", strconv.FormatBool(config.DisableNotification)) + + return v, nil +} + +// UnpinChatMessageConfig contains information of chat to unpin. +type UnpinChatMessageConfig struct { + ChatID int64 +} + +func (config UnpinChatMessageConfig) method() string { + return "unpinChatMessage" +} + +func (config UnpinChatMessageConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + + return v, nil +} + +// SetChatTitleConfig contains information for change chat title. +type SetChatTitleConfig struct { + ChatID int64 + Title string +} + +func (config SetChatTitleConfig) method() string { + return "setChatTitle" +} + +func (config SetChatTitleConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + v.Add("title", config.Title) + + return v, nil +} + +// SetChatDescriptionConfig contains information for change chat description. +type SetChatDescriptionConfig struct { + ChatID int64 + Description string +} + +func (config SetChatDescriptionConfig) method() string { + return "setChatDescription" +} + +func (config SetChatDescriptionConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + v.Add("description", config.Description) + + return v, nil +} + +// SetChatPhotoConfig contains information for change chat photo +type SetChatPhotoConfig struct { + BaseFile +} + +// name returns the field name for the Photo. +func (config SetChatPhotoConfig) name() string { + return "photo" +} + +// method returns Telegram API method name for sending Photo. +func (config SetChatPhotoConfig) method() string { + return "setChatPhoto" +} + +// DeleteChatPhotoConfig contains information for delete chat photo. +type DeleteChatPhotoConfig struct { + ChatID int64 +} + +func (config DeleteChatPhotoConfig) method() string { + return "deleteChatPhoto" +} + +func (config DeleteChatPhotoConfig) values() (url.Values, error) { + v := url.Values{} + + v.Add("chat_id", strconv.FormatInt(config.ChatID, 10)) + + return v, nil +} diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers.go new file mode 100644 index 0000000..c23a3bf --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers.go @@ -0,0 +1,686 @@ +package tgbotapi + +import ( + "log" + "net/url" +) + +// NewMessage creates a new Message. +// +// chatID is where to send it, text is the message text. +func NewMessage(chatID int64, text string) MessageConfig { + return MessageConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + ReplyToMessageID: 0, + }, + Text: text, + DisableWebPagePreview: false, + } +} + +// NewMessageToChannel creates a new Message that is sent to a channel +// by username. +// +// username is the username of the channel, text is the message text. +func NewMessageToChannel(username string, text string) MessageConfig { + return MessageConfig{ + BaseChat: BaseChat{ + ChannelUsername: username, + }, + Text: text, + } +} + +// NewForward creates a new forward. +// +// chatID is where to send it, fromChatID is the source chat, +// and messageID is the ID of the original message. +func NewForward(chatID int64, fromChatID int64, messageID int) ForwardConfig { + return ForwardConfig{ + BaseChat: BaseChat{ChatID: chatID}, + FromChatID: fromChatID, + MessageID: messageID, + } +} + +// NewPhotoUpload creates a new photo uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +// +// Note that you must send animated GIFs as a document. +func NewPhotoUpload(chatID int64, file interface{}) PhotoConfig { + return PhotoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewPhotoShare shares an existing photo. +// You may use this to reshare an existing photo without reuploading it. +// +// chatID is where to send it, fileID is the ID of the file +// already uploaded. +func NewPhotoShare(chatID int64, fileID string) PhotoConfig { + return PhotoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} + +// NewAudioUpload creates a new audio uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +func NewAudioUpload(chatID int64, file interface{}) AudioConfig { + return AudioConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewAudioShare shares an existing audio file. +// You may use this to reshare an existing audio file without +// reuploading it. +// +// chatID is where to send it, fileID is the ID of the audio +// already uploaded. +func NewAudioShare(chatID int64, fileID string) AudioConfig { + return AudioConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} + +// NewDocumentUpload creates a new document uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +func NewDocumentUpload(chatID int64, file interface{}) DocumentConfig { + return DocumentConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewDocumentShare shares an existing document. +// You may use this to reshare an existing document without +// reuploading it. +// +// chatID is where to send it, fileID is the ID of the document +// already uploaded. +func NewDocumentShare(chatID int64, fileID string) DocumentConfig { + return DocumentConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} + +// NewStickerUpload creates a new sticker uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +func NewStickerUpload(chatID int64, file interface{}) StickerConfig { + return StickerConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewStickerShare shares an existing sticker. +// You may use this to reshare an existing sticker without +// reuploading it. +// +// chatID is where to send it, fileID is the ID of the sticker +// already uploaded. +func NewStickerShare(chatID int64, fileID string) StickerConfig { + return StickerConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} + +// NewVideoUpload creates a new video uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +func NewVideoUpload(chatID int64, file interface{}) VideoConfig { + return VideoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewVideoShare shares an existing video. +// You may use this to reshare an existing video without reuploading it. +// +// chatID is where to send it, fileID is the ID of the video +// already uploaded. +func NewVideoShare(chatID int64, fileID string) VideoConfig { + return VideoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} + +// NewVideoNoteUpload creates a new video note uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +func NewVideoNoteUpload(chatID int64, length int, file interface{}) VideoNoteConfig { + return VideoNoteConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + Length: length, + } +} + +// NewVideoNoteShare shares an existing video. +// You may use this to reshare an existing video without reuploading it. +// +// chatID is where to send it, fileID is the ID of the video +// already uploaded. +func NewVideoNoteShare(chatID int64, length int, fileID string) VideoNoteConfig { + return VideoNoteConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + Length: length, + } +} + +// NewVoiceUpload creates a new voice uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +func NewVoiceUpload(chatID int64, file interface{}) VoiceConfig { + return VoiceConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewVoiceShare shares an existing voice. +// You may use this to reshare an existing voice without reuploading it. +// +// chatID is where to send it, fileID is the ID of the video +// already uploaded. +func NewVoiceShare(chatID int64, fileID string) VoiceConfig { + return VoiceConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} + +// NewContact allows you to send a shared contact. +func NewContact(chatID int64, phoneNumber, firstName string) ContactConfig { + return ContactConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + PhoneNumber: phoneNumber, + FirstName: firstName, + } +} + +// NewLocation shares your location. +// +// chatID is where to send it, latitude and longitude are coordinates. +func NewLocation(chatID int64, latitude float64, longitude float64) LocationConfig { + return LocationConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + Latitude: latitude, + Longitude: longitude, + } +} + +// NewVenue allows you to send a venue and its location. +func NewVenue(chatID int64, title, address string, latitude, longitude float64) VenueConfig { + return VenueConfig{ + BaseChat: BaseChat{ + ChatID: chatID, + }, + Title: title, + Address: address, + Latitude: latitude, + Longitude: longitude, + } +} + +// NewChatAction sets a chat action. +// Actions last for 5 seconds, or until your next action. +// +// chatID is where to send it, action should be set via Chat constants. +func NewChatAction(chatID int64, action string) ChatActionConfig { + return ChatActionConfig{ + BaseChat: BaseChat{ChatID: chatID}, + Action: action, + } +} + +// NewUserProfilePhotos gets user profile photos. +// +// userID is the ID of the user you wish to get profile photos from. +func NewUserProfilePhotos(userID int) UserProfilePhotosConfig { + return UserProfilePhotosConfig{ + UserID: userID, + Offset: 0, + Limit: 0, + } +} + +// NewUpdate gets updates since the last Offset. +// +// offset is the last Update ID to include. +// You likely want to set this to the last Update ID plus 1. +func NewUpdate(offset int) UpdateConfig { + return UpdateConfig{ + Offset: offset, + Limit: 0, + Timeout: 0, + } +} + +// NewWebhook creates a new webhook. +// +// link is the url parsable link you wish to get the updates. +func NewWebhook(link string) WebhookConfig { + u, _ := url.Parse(link) + + return WebhookConfig{ + URL: u, + } +} + +// NewWebhookWithCert creates a new webhook with a certificate. +// +// link is the url you wish to get webhooks, +// file contains a string to a file, FileReader, or FileBytes. +func NewWebhookWithCert(link string, file interface{}) WebhookConfig { + u, _ := url.Parse(link) + + return WebhookConfig{ + URL: u, + Certificate: file, + } +} + +// NewInlineQueryResultArticle creates a new inline query article. +func NewInlineQueryResultArticle(id, title, messageText string) InlineQueryResultArticle { + return InlineQueryResultArticle{ + Type: "article", + ID: id, + Title: title, + InputMessageContent: InputTextMessageContent{ + Text: messageText, + }, + } +} + +// NewInlineQueryResultArticleMarkdown creates a new inline query article with Markdown parsing. +func NewInlineQueryResultArticleMarkdown(id, title, messageText string) InlineQueryResultArticle { + return InlineQueryResultArticle{ + Type: "article", + ID: id, + Title: title, + InputMessageContent: InputTextMessageContent{ + Text: messageText, + ParseMode: "Markdown", + }, + } +} + +// NewInlineQueryResultArticleHTML creates a new inline query article with HTML parsing. +func NewInlineQueryResultArticleHTML(id, title, messageText string) InlineQueryResultArticle { + return InlineQueryResultArticle{ + Type: "article", + ID: id, + Title: title, + InputMessageContent: InputTextMessageContent{ + Text: messageText, + ParseMode: "HTML", + }, + } +} + +// NewInlineQueryResultGIF creates a new inline query GIF. +func NewInlineQueryResultGIF(id, url string) InlineQueryResultGIF { + return InlineQueryResultGIF{ + Type: "gif", + ID: id, + URL: url, + } +} + +// NewInlineQueryResultMPEG4GIF creates a new inline query MPEG4 GIF. +func NewInlineQueryResultMPEG4GIF(id, url string) InlineQueryResultMPEG4GIF { + return InlineQueryResultMPEG4GIF{ + Type: "mpeg4_gif", + ID: id, + URL: url, + } +} + +// NewInlineQueryResultPhoto creates a new inline query photo. +func NewInlineQueryResultPhoto(id, url string) InlineQueryResultPhoto { + return InlineQueryResultPhoto{ + Type: "photo", + ID: id, + URL: url, + } +} + +// NewInlineQueryResultPhotoWithThumb creates a new inline query photo. +func NewInlineQueryResultPhotoWithThumb(id, url, thumb string) InlineQueryResultPhoto { + return InlineQueryResultPhoto{ + Type: "photo", + ID: id, + URL: url, + ThumbURL: thumb, + } +} + +// NewInlineQueryResultVideo creates a new inline query video. +func NewInlineQueryResultVideo(id, url string) InlineQueryResultVideo { + return InlineQueryResultVideo{ + Type: "video", + ID: id, + URL: url, + } +} + +// NewInlineQueryResultAudio creates a new inline query audio. +func NewInlineQueryResultAudio(id, url, title string) InlineQueryResultAudio { + return InlineQueryResultAudio{ + Type: "audio", + ID: id, + URL: url, + Title: title, + } +} + +// NewInlineQueryResultVoice creates a new inline query voice. +func NewInlineQueryResultVoice(id, url, title string) InlineQueryResultVoice { + return InlineQueryResultVoice{ + Type: "voice", + ID: id, + URL: url, + Title: title, + } +} + +// NewInlineQueryResultDocument creates a new inline query document. +func NewInlineQueryResultDocument(id, url, title, mimeType string) InlineQueryResultDocument { + return InlineQueryResultDocument{ + Type: "document", + ID: id, + URL: url, + Title: title, + MimeType: mimeType, + } +} + +// NewInlineQueryResultLocation creates a new inline query location. +func NewInlineQueryResultLocation(id, title string, latitude, longitude float64) InlineQueryResultLocation { + return InlineQueryResultLocation{ + Type: "location", + ID: id, + Title: title, + Latitude: latitude, + Longitude: longitude, + } +} + +// NewEditMessageText allows you to edit the text of a message. +func NewEditMessageText(chatID int64, messageID int, text string) EditMessageTextConfig { + return EditMessageTextConfig{ + BaseEdit: BaseEdit{ + ChatID: chatID, + MessageID: messageID, + }, + Text: text, + } +} + +// NewEditMessageCaption allows you to edit the caption of a message. +func NewEditMessageCaption(chatID int64, messageID int, caption string) EditMessageCaptionConfig { + return EditMessageCaptionConfig{ + BaseEdit: BaseEdit{ + ChatID: chatID, + MessageID: messageID, + }, + Caption: caption, + } +} + +// NewEditMessageReplyMarkup allows you to edit the inline +// keyboard markup. +func NewEditMessageReplyMarkup(chatID int64, messageID int, replyMarkup InlineKeyboardMarkup) EditMessageReplyMarkupConfig { + return EditMessageReplyMarkupConfig{ + BaseEdit: BaseEdit{ + ChatID: chatID, + MessageID: messageID, + ReplyMarkup: &replyMarkup, + }, + } +} + +// NewHideKeyboard hides the keyboard, with the option for being selective +// or hiding for everyone. +func NewHideKeyboard(selective bool) ReplyKeyboardHide { + log.Println("NewHideKeyboard is deprecated, please use NewRemoveKeyboard") + + return ReplyKeyboardHide{ + HideKeyboard: true, + Selective: selective, + } +} + +// NewRemoveKeyboard hides the keyboard, with the option for being selective +// or hiding for everyone. +func NewRemoveKeyboard(selective bool) ReplyKeyboardRemove { + return ReplyKeyboardRemove{ + RemoveKeyboard: true, + Selective: selective, + } +} + +// NewKeyboardButton creates a regular keyboard button. +func NewKeyboardButton(text string) KeyboardButton { + return KeyboardButton{ + Text: text, + } +} + +// NewKeyboardButtonContact creates a keyboard button that requests +// user contact information upon click. +func NewKeyboardButtonContact(text string) KeyboardButton { + return KeyboardButton{ + Text: text, + RequestContact: true, + } +} + +// NewKeyboardButtonLocation creates a keyboard button that requests +// user location information upon click. +func NewKeyboardButtonLocation(text string) KeyboardButton { + return KeyboardButton{ + Text: text, + RequestLocation: true, + } +} + +// NewKeyboardButtonRow creates a row of keyboard buttons. +func NewKeyboardButtonRow(buttons ...KeyboardButton) []KeyboardButton { + var row []KeyboardButton + + row = append(row, buttons...) + + return row +} + +// NewReplyKeyboard creates a new regular keyboard with sane defaults. +func NewReplyKeyboard(rows ...[]KeyboardButton) ReplyKeyboardMarkup { + var keyboard [][]KeyboardButton + + keyboard = append(keyboard, rows...) + + return ReplyKeyboardMarkup{ + ResizeKeyboard: true, + Keyboard: keyboard, + } +} + +// NewInlineKeyboardButtonData creates an inline keyboard button with text +// and data for a callback. +func NewInlineKeyboardButtonData(text, data string) InlineKeyboardButton { + return InlineKeyboardButton{ + Text: text, + CallbackData: &data, + } +} + +// NewInlineKeyboardButtonURL creates an inline keyboard button with text +// which goes to a URL. +func NewInlineKeyboardButtonURL(text, url string) InlineKeyboardButton { + return InlineKeyboardButton{ + Text: text, + URL: &url, + } +} + +// NewInlineKeyboardButtonSwitch creates an inline keyboard button with +// text which allows the user to switch to a chat or return to a chat. +func NewInlineKeyboardButtonSwitch(text, sw string) InlineKeyboardButton { + return InlineKeyboardButton{ + Text: text, + SwitchInlineQuery: &sw, + } +} + +// NewInlineKeyboardRow creates an inline keyboard row with buttons. +func NewInlineKeyboardRow(buttons ...InlineKeyboardButton) []InlineKeyboardButton { + var row []InlineKeyboardButton + + row = append(row, buttons...) + + return row +} + +// NewInlineKeyboardMarkup creates a new inline keyboard. +func NewInlineKeyboardMarkup(rows ...[]InlineKeyboardButton) InlineKeyboardMarkup { + var keyboard [][]InlineKeyboardButton + + keyboard = append(keyboard, rows...) + + return InlineKeyboardMarkup{ + InlineKeyboard: keyboard, + } +} + +// NewCallback creates a new callback message. +func NewCallback(id, text string) CallbackConfig { + return CallbackConfig{ + CallbackQueryID: id, + Text: text, + ShowAlert: false, + } +} + +// NewCallbackWithAlert creates a new callback message that alerts +// the user. +func NewCallbackWithAlert(id, text string) CallbackConfig { + return CallbackConfig{ + CallbackQueryID: id, + Text: text, + ShowAlert: true, + } +} + +// NewInvoice creates a new Invoice request to the user. +func NewInvoice(chatID int64, title, description, payload, providerToken, startParameter, currency string, prices *[]LabeledPrice) InvoiceConfig { + return InvoiceConfig{ + BaseChat: BaseChat{ChatID: chatID}, + Title: title, + Description: description, + Payload: payload, + ProviderToken: providerToken, + StartParameter: startParameter, + Currency: currency, + Prices: prices} +} + +// NewSetChatPhotoUpload creates a new chat photo uploader. +// +// chatID is where to send it, file is a string path to the file, +// FileReader, or FileBytes. +// +// Note that you must send animated GIFs as a document. +func NewSetChatPhotoUpload(chatID int64, file interface{}) SetChatPhotoConfig { + return SetChatPhotoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + File: file, + UseExisting: false, + }, + } +} + +// NewSetChatPhotoShare shares an existing photo. +// You may use this to reshare an existing photo without reuploading it. +// +// chatID is where to send it, fileID is the ID of the file +// already uploaded. +func NewSetChatPhotoShare(chatID int64, fileID string) SetChatPhotoConfig { + return SetChatPhotoConfig{ + BaseFile: BaseFile{ + BaseChat: BaseChat{ChatID: chatID}, + FileID: fileID, + UseExisting: true, + }, + } +} diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers_test.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers_test.go new file mode 100644 index 0000000..9542f02 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/helpers_test.go @@ -0,0 +1,177 @@ +package tgbotapi_test + +import ( + "github.com/go-telegram-bot-api/telegram-bot-api" + "testing" +) + +func TestNewInlineQueryResultArticle(t *testing.T) { + result := tgbotapi.NewInlineQueryResultArticle("id", "title", "message") + + if result.Type != "article" || + result.ID != "id" || + result.Title != "title" || + result.InputMessageContent.(tgbotapi.InputTextMessageContent).Text != "message" { + t.Fail() + } +} + +func TestNewInlineQueryResultArticleMarkdown(t *testing.T) { + result := tgbotapi.NewInlineQueryResultArticleMarkdown("id", "title", "*message*") + + if result.Type != "article" || + result.ID != "id" || + result.Title != "title" || + result.InputMessageContent.(tgbotapi.InputTextMessageContent).Text != "*message*" || + result.InputMessageContent.(tgbotapi.InputTextMessageContent).ParseMode != "Markdown" { + t.Fail() + } +} + +func TestNewInlineQueryResultArticleHTML(t *testing.T) { + result := tgbotapi.NewInlineQueryResultArticleHTML("id", "title", "message") + + if result.Type != "article" || + result.ID != "id" || + result.Title != "title" || + result.InputMessageContent.(tgbotapi.InputTextMessageContent).Text != "message" || + result.InputMessageContent.(tgbotapi.InputTextMessageContent).ParseMode != "HTML" { + t.Fail() + } +} + +func TestNewInlineQueryResultGIF(t *testing.T) { + result := tgbotapi.NewInlineQueryResultGIF("id", "google.com") + + if result.Type != "gif" || + result.ID != "id" || + result.URL != "google.com" { + t.Fail() + } +} + +func TestNewInlineQueryResultMPEG4GIF(t *testing.T) { + result := tgbotapi.NewInlineQueryResultMPEG4GIF("id", "google.com") + + if result.Type != "mpeg4_gif" || + result.ID != "id" || + result.URL != "google.com" { + t.Fail() + } +} + +func TestNewInlineQueryResultPhoto(t *testing.T) { + result := tgbotapi.NewInlineQueryResultPhoto("id", "google.com") + + if result.Type != "photo" || + result.ID != "id" || + result.URL != "google.com" { + t.Fail() + } +} + +func TestNewInlineQueryResultPhotoWithThumb(t *testing.T) { + result := tgbotapi.NewInlineQueryResultPhotoWithThumb("id", "google.com", "thumb.com") + + if result.Type != "photo" || + result.ID != "id" || + result.URL != "google.com" || + result.ThumbURL != "thumb.com" { + t.Fail() + } +} + +func TestNewInlineQueryResultVideo(t *testing.T) { + result := tgbotapi.NewInlineQueryResultVideo("id", "google.com") + + if result.Type != "video" || + result.ID != "id" || + result.URL != "google.com" { + t.Fail() + } +} + +func TestNewInlineQueryResultAudio(t *testing.T) { + result := tgbotapi.NewInlineQueryResultAudio("id", "google.com", "title") + + if result.Type != "audio" || + result.ID != "id" || + result.URL != "google.com" || + result.Title != "title" { + t.Fail() + } +} + +func TestNewInlineQueryResultVoice(t *testing.T) { + result := tgbotapi.NewInlineQueryResultVoice("id", "google.com", "title") + + if result.Type != "voice" || + result.ID != "id" || + result.URL != "google.com" || + result.Title != "title" { + t.Fail() + } +} + +func TestNewInlineQueryResultDocument(t *testing.T) { + result := tgbotapi.NewInlineQueryResultDocument("id", "google.com", "title", "mime/type") + + if result.Type != "document" || + result.ID != "id" || + result.URL != "google.com" || + result.Title != "title" || + result.MimeType != "mime/type" { + t.Fail() + } +} + +func TestNewInlineQueryResultLocation(t *testing.T) { + result := tgbotapi.NewInlineQueryResultLocation("id", "name", 40, 50) + + if result.Type != "location" || + result.ID != "id" || + result.Title != "name" || + result.Latitude != 40 || + result.Longitude != 50 { + t.Fail() + } +} + +func TestNewEditMessageText(t *testing.T) { + edit := tgbotapi.NewEditMessageText(ChatID, ReplyToMessageID, "new text") + + if edit.Text != "new text" || + edit.BaseEdit.ChatID != ChatID || + edit.BaseEdit.MessageID != ReplyToMessageID { + t.Fail() + } +} + +func TestNewEditMessageCaption(t *testing.T) { + edit := tgbotapi.NewEditMessageCaption(ChatID, ReplyToMessageID, "new caption") + + if edit.Caption != "new caption" || + edit.BaseEdit.ChatID != ChatID || + edit.BaseEdit.MessageID != ReplyToMessageID { + t.Fail() + } +} + +func TestNewEditMessageReplyMarkup(t *testing.T) { + markup := tgbotapi.InlineKeyboardMarkup{ + InlineKeyboard: [][]tgbotapi.InlineKeyboardButton{ + []tgbotapi.InlineKeyboardButton{ + tgbotapi.InlineKeyboardButton{Text: "test"}, + }, + }, + } + + edit := tgbotapi.NewEditMessageReplyMarkup(ChatID, ReplyToMessageID, markup) + + if edit.ReplyMarkup.InlineKeyboard[0][0].Text != "test" || + edit.BaseEdit.ChatID != ChatID || + edit.BaseEdit.MessageID != ReplyToMessageID { + t.Fail() + } + +} diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/audio.mp3 b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/audio.mp3 new file mode 100644 index 0000000..06b0284 Binary files /dev/null and b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/audio.mp3 differ diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/cert.pem b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/cert.pem new file mode 100644 index 0000000..aa2bb94 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/cert.pem @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC0zCCAbugAwIBAgIJAPYfllX657axMA0GCSqGSIb3DQEBCwUAMAAwHhcNMTUx +MTIxMTExMDQxWhcNMjUwODIwMTExMDQxWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAoMMSIIgYx8pT8Kz1O8Ukd/JVyqBQYRSo0enqEzo7295VROXq +TUthbEbdi0OczUfl4IsAWppOSRrDwEguJZ0cJ/r6IxGsbrCdQr2MjgiomYtAXKKQ +GAGL5Wls+AzcRNV0OszVJzkDNFYZzgNejyitGJSNEQMyU8r2gyPyIWP9MQKQst8y +Mg91R/7l9jwf6AWwNxykZlYZurtsQ6XsBPZpF9YOFL7vZYPhKUFiNEm+74RpojC7 +Gt6nztYAUI2V/F+1uoXAr8nLpbj9SD0VSwyZLRG1uIVLBzhb0lfOIzAvJ45EKki9 +nejyoXfH1U5+iMzdSAdcy3MCBhpEZwJPqhDqeQIDAQABo1AwTjAdBgNVHQ4EFgQU +JE0RLM+ohLnlDz0Qk0McCxtDK2MwHwYDVR0jBBgwFoAUJE0RLM+ohLnlDz0Qk0Mc +CxtDK2MwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAEmgME00JYuYZ +4wNaGrJskZ05ZnP+TXJusmBui9ToQ4UoykuyY5rsdGQ3SdzXPLdmd2nfMsw63iK2 +D7rjcH/rmn6fRccZqN0o0SXd/EuHeIoeW1Xnnivbt71b6mcOAeNg1UsMYxnMAVl0 +ywdkta8gURltagSfXoUbqlnSxn/zCwqaxxcQXA/CnunvRsFtQrwWjDBPg/BPULHX +DEh2AactGtnGqEZ5iap/VCOVnmL6iPdJ1x5UIF/gS6U96wL+GHfcs1jCvPg+GEwR +3inh9oTXG9L21ge4lbGiPUIMBjtVcB3bXuQbOfec9Cr3ZhcQeZj680BIRxD/pNpA +O/XeCfjfkw== +-----END CERTIFICATE----- diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/image.jpg b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/image.jpg new file mode 100644 index 0000000..eddc186 Binary files /dev/null and b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/image.jpg differ diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/key.pem b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/key.pem new file mode 100644 index 0000000..034b703 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEwAIBADANBgkqhkiG9w0BAQEFAASCBKowggSmAgEAAoIBAQCgwxIgiBjHylPw +rPU7xSR38lXKoFBhFKjR6eoTOjvb3lVE5epNS2FsRt2LQ5zNR+XgiwBamk5JGsPA +SC4lnRwn+vojEaxusJ1CvYyOCKiZi0BcopAYAYvlaWz4DNxE1XQ6zNUnOQM0VhnO +A16PKK0YlI0RAzJTyvaDI/IhY/0xApCy3zIyD3VH/uX2PB/oBbA3HKRmVhm6u2xD +pewE9mkX1g4Uvu9lg+EpQWI0Sb7vhGmiMLsa3qfO1gBQjZX8X7W6hcCvyculuP1I +PRVLDJktEbW4hUsHOFvSV84jMC8njkQqSL2d6PKhd8fVTn6IzN1IB1zLcwIGGkRn +Ak+qEOp5AgMBAAECggEBAJ/dPCJzlEjhL5XPONLmGXzZ1Gx5/VR86eBMv0O9jhb3 +wk2QYO3aPxggZGD/rGcKz1L6hzCR77WM0wpb/N/Um1I6pxHGmnU8VjYvLh10CM0f +h7JWyfnFV+ubagxFJamhpkJuvKyTaldaI7EU8qxj47Xky18Wka53z6nbTgXcW8Sm +V4CJy9OHNgKJQnylX6zOAaxVngSGde3xLslLjsYK4w9b2+OkCSUST2XXdo+ZLXxl +cs0lEPFRM1Xh9/E6UrDrJMHHzio53L/W/+a8sIar1upgSY52pyD/tA7VSrAJ9nYC +RezOU81VTLfMO+TYmgZzSUQJYh0cR4yqJe+wgl4U550CgYEA1EcS6Z+PO5Pr3u2+ +XevawSAal6y9ONkkdOoASC977W37nn0E1wlQo41dR6DESCJfiSMeN0KbmXj5Wnc/ +ADu+73iGwC90G9Qs9sjD7KAFBJvuj0V8hxvpWRdIBBbf7rlOj3CV0iXRYjkJbyJa +cxuR0kiv4gTWmm5Cq+5ir8t1Oc8CgYEAwd+xOaDerNR481R+QmENFw+oR2EVMq3Q +B/vinLK0PemQWrh32iBcd+vhSilOSQtUm1nko1jLK8C4s8X2vZYua4m5tcK9VqCt +maCCq/ffxzsoW/GN8japnduz+qA+hKWJzW/aYR8tsOeqzjVqj4iIqPI4HuokrDi/ +UD/QLgq5UTcCgYEAk2ZC0Kx15dXB7AtDq63xOTcUoAtXXRkSgohV58npEKXVGWkQ +Kk0SjG7Fvc35XWlY0z3qZk6/AuOIqfOxcHUMEPatAtgwlH5RNo+T1EQNF/U6wotq +e9q6vp026XgEyJwt29Y+giy2ZrDaRywgiFs1d0H3t0bKyXMUopQmPJFXdesCgYEA +psCxXcDpZjxGX/zPsGZrbOdxtRtisTlg0k0rp93pO8tV90HtDHeDMT54g2ItzJPr +TMev6XOpJNPZyf6+8GhpOuO2EQkT85u2VYoCeslz95gBabvFfIzZrUZYcnw76bm8 +YjAP5DN+CEfq2PyG0Df+W1ojPSvlKSCSJQMOG1vr81cCgYEAkjPY5WR99uJxYBNI +OTFMSkETgDUbPXBu/E/h5Dtn79v8Moj9FvC7+q6sg9qXhrGhfK2xDev3/sTrbS/E +Gcf8UNIne3AXsoAS8MtkOwJXHkYaTIboIYgDX4LlDmbGQlIRaWgyh2POI6VtjLBT +ms6AdsdpIB6As9xNUBUwj/RnTZQ= +-----END PRIVATE KEY----- diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/video.mp4 b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/video.mp4 new file mode 100644 index 0000000..a203d0c Binary files /dev/null and b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/video.mp4 differ diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/videonote.mp4 b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/videonote.mp4 new file mode 100644 index 0000000..649d16f Binary files /dev/null and b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/videonote.mp4 differ diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/voice.ogg b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/voice.ogg new file mode 100644 index 0000000..0d7f43e Binary files /dev/null and b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/tests/voice.ogg differ diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types.go new file mode 100644 index 0000000..d3d30dc --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types.go @@ -0,0 +1,783 @@ +package tgbotapi + +import ( + "encoding/json" + "errors" + "fmt" + "net/url" + "strings" + "time" +) + +// APIResponse is a response from the Telegram API with the result +// stored raw. +type APIResponse struct { + Ok bool `json:"ok"` + Result json.RawMessage `json:"result"` + ErrorCode int `json:"error_code"` + Description string `json:"description"` + Parameters *ResponseParameters `json:"parameters"` +} + +// ResponseParameters are various errors that can be returned in APIResponse. +type ResponseParameters struct { + MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional + RetryAfter int `json:"retry_after"` // optional +} + +// Update is an update response, from GetUpdates. +type Update struct { + UpdateID int `json:"update_id"` + Message *Message `json:"message"` + EditedMessage *Message `json:"edited_message"` + ChannelPost *Message `json:"channel_post"` + EditedChannelPost *Message `json:"edited_channel_post"` + InlineQuery *InlineQuery `json:"inline_query"` + ChosenInlineResult *ChosenInlineResult `json:"chosen_inline_result"` + CallbackQuery *CallbackQuery `json:"callback_query"` + ShippingQuery *ShippingQuery `json:"shipping_query"` + PreCheckoutQuery *PreCheckoutQuery `json:"pre_checkout_query"` +} + +// UpdatesChannel is the channel for getting updates. +type UpdatesChannel <-chan Update + +// Clear discards all unprocessed incoming updates. +func (ch UpdatesChannel) Clear() { + for len(ch) != 0 { + <-ch + } +} + +// User is a user on Telegram. +type User struct { + ID int `json:"id"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` // optional + UserName string `json:"username"` // optional + LanguageCode string `json:"language_code"` // optional + IsBot bool `json:"is_bot"` // optional +} + +// String displays a simple text version of a user. +// +// It is normally a user's username, but falls back to a first/last +// name as available. +func (u *User) String() string { + if u.UserName != "" { + return u.UserName + } + + name := u.FirstName + if u.LastName != "" { + name += " " + u.LastName + } + + return name +} + +// GroupChat is a group chat. +type GroupChat struct { + ID int `json:"id"` + Title string `json:"title"` +} + +// ChatPhoto represents a chat photo. +type ChatPhoto struct { + SmallFileID string `json:"small_file_id"` + BigFileID string `json:"big_file_id"` +} + +// Chat contains information about the place a message was sent. +type Chat struct { + ID int64 `json:"id"` + Type string `json:"type"` + Title string `json:"title"` // optional + UserName string `json:"username"` // optional + FirstName string `json:"first_name"` // optional + LastName string `json:"last_name"` // optional + AllMembersAreAdmins bool `json:"all_members_are_administrators"` // optional + Photo *ChatPhoto `json:"photo"` + Description string `json:"description,omitempty"` // optional + InviteLink string `json:"invite_link,omitempty"` // optional +} + +// IsPrivate returns if the Chat is a private conversation. +func (c Chat) IsPrivate() bool { + return c.Type == "private" +} + +// IsGroup returns if the Chat is a group. +func (c Chat) IsGroup() bool { + return c.Type == "group" +} + +// IsSuperGroup returns if the Chat is a supergroup. +func (c Chat) IsSuperGroup() bool { + return c.Type == "supergroup" +} + +// IsChannel returns if the Chat is a channel. +func (c Chat) IsChannel() bool { + return c.Type == "channel" +} + +// ChatConfig returns a ChatConfig struct for chat related methods. +func (c Chat) ChatConfig() ChatConfig { + return ChatConfig{ChatID: c.ID} +} + +// Message is returned by almost every request, and contains data about +// almost anything. +type Message struct { + MessageID int `json:"message_id"` + From *User `json:"from"` // optional + Date int `json:"date"` + Chat *Chat `json:"chat"` + ForwardFrom *User `json:"forward_from"` // optional + ForwardFromChat *Chat `json:"forward_from_chat"` // optional + ForwardFromMessageID int `json:"forward_from_message_id"` // optional + ForwardDate int `json:"forward_date"` // optional + ReplyToMessage *Message `json:"reply_to_message"` // optional + EditDate int `json:"edit_date"` // optional + Text string `json:"text"` // optional + Entities *[]MessageEntity `json:"entities"` // optional + Audio *Audio `json:"audio"` // optional + Document *Document `json:"document"` // optional + Game *Game `json:"game"` // optional + Photo *[]PhotoSize `json:"photo"` // optional + Sticker *Sticker `json:"sticker"` // optional + Video *Video `json:"video"` // optional + VideoNote *VideoNote `json:"video_note"` // optional + Voice *Voice `json:"voice"` // optional + Caption string `json:"caption"` // optional + Contact *Contact `json:"contact"` // optional + Location *Location `json:"location"` // optional + Venue *Venue `json:"venue"` // optional + NewChatMembers *[]User `json:"new_chat_members"` // optional + LeftChatMember *User `json:"left_chat_member"` // optional + NewChatTitle string `json:"new_chat_title"` // optional + NewChatPhoto *[]PhotoSize `json:"new_chat_photo"` // optional + DeleteChatPhoto bool `json:"delete_chat_photo"` // optional + GroupChatCreated bool `json:"group_chat_created"` // optional + SuperGroupChatCreated bool `json:"supergroup_chat_created"` // optional + ChannelChatCreated bool `json:"channel_chat_created"` // optional + MigrateToChatID int64 `json:"migrate_to_chat_id"` // optional + MigrateFromChatID int64 `json:"migrate_from_chat_id"` // optional + PinnedMessage *Message `json:"pinned_message"` // optional + Invoice *Invoice `json:"invoice"` // optional + SuccessfulPayment *SuccessfulPayment `json:"successful_payment"` // optional +} + +// Time converts the message timestamp into a Time. +func (m *Message) Time() time.Time { + return time.Unix(int64(m.Date), 0) +} + +// IsCommand returns true if message starts with a "bot_command" entity. +func (m *Message) IsCommand() bool { + if m.Entities == nil || len(*m.Entities) == 0 { + return false + } + + entity := (*m.Entities)[0] + return entity.Offset == 0 && entity.Type == "bot_command" +} + +// Command checks if the message was a command and if it was, returns the +// command. If the Message was not a command, it returns an empty string. +// +// If the command contains the at name syntax, it is removed. Use +// CommandWithAt() if you do not want that. +func (m *Message) Command() string { + command := m.CommandWithAt() + + if i := strings.Index(command, "@"); i != -1 { + command = command[:i] + } + + return command +} + +// CommandWithAt checks if the message was a command and if it was, returns the +// command. If the Message was not a command, it returns an empty string. +// +// If the command contains the at name syntax, it is not removed. Use Command() +// if you want that. +func (m *Message) CommandWithAt() string { + if !m.IsCommand() { + return "" + } + + // IsCommand() checks that the message begins with a bot_command entity + entity := (*m.Entities)[0] + return m.Text[1:entity.Length] +} + +// CommandArguments checks if the message was a command and if it was, +// returns all text after the command name. If the Message was not a +// command, it returns an empty string. +// +// Note: The first character after the command name is omitted: +// - "/foo bar baz" yields "bar baz", not " bar baz" +// - "/foo-bar baz" yields "bar baz", too +// Even though the latter is not a command conforming to the spec, the API +// marks "/foo" as command entity. +func (m *Message) CommandArguments() string { + if !m.IsCommand() { + return "" + } + + // IsCommand() checks that the message begins with a bot_command entity + entity := (*m.Entities)[0] + if len(m.Text) == entity.Length { + return "" // The command makes up the whole message + } + + return m.Text[entity.Length+1:] +} + +// MessageEntity contains information about data in a Message. +type MessageEntity struct { + Type string `json:"type"` + Offset int `json:"offset"` + Length int `json:"length"` + URL string `json:"url"` // optional + User *User `json:"user"` // optional +} + +// ParseURL attempts to parse a URL contained within a MessageEntity. +func (entity MessageEntity) ParseURL() (*url.URL, error) { + if entity.URL == "" { + return nil, errors.New(ErrBadURL) + } + + return url.Parse(entity.URL) +} + +// PhotoSize contains information about photos. +type PhotoSize struct { + FileID string `json:"file_id"` + Width int `json:"width"` + Height int `json:"height"` + FileSize int `json:"file_size"` // optional +} + +// Audio contains information about audio. +type Audio struct { + FileID string `json:"file_id"` + Duration int `json:"duration"` + Performer string `json:"performer"` // optional + Title string `json:"title"` // optional + MimeType string `json:"mime_type"` // optional + FileSize int `json:"file_size"` // optional +} + +// Document contains information about a document. +type Document struct { + FileID string `json:"file_id"` + Thumbnail *PhotoSize `json:"thumb"` // optional + FileName string `json:"file_name"` // optional + MimeType string `json:"mime_type"` // optional + FileSize int `json:"file_size"` // optional +} + +// Sticker contains information about a sticker. +type Sticker struct { + FileID string `json:"file_id"` + Width int `json:"width"` + Height int `json:"height"` + Thumbnail *PhotoSize `json:"thumb"` // optional + Emoji string `json:"emoji"` // optional + FileSize int `json:"file_size"` // optional +} + +// Video contains information about a video. +type Video struct { + FileID string `json:"file_id"` + Width int `json:"width"` + Height int `json:"height"` + Duration int `json:"duration"` + Thumbnail *PhotoSize `json:"thumb"` // optional + MimeType string `json:"mime_type"` // optional + FileSize int `json:"file_size"` // optional +} + +// VideoNote contains information about a video. +type VideoNote struct { + FileID string `json:"file_id"` + Length int `json:"length"` + Duration int `json:"duration"` + Thumbnail *PhotoSize `json:"thumb"` // optional + FileSize int `json:"file_size"` // optional +} + +// Voice contains information about a voice. +type Voice struct { + FileID string `json:"file_id"` + Duration int `json:"duration"` + MimeType string `json:"mime_type"` // optional + FileSize int `json:"file_size"` // optional +} + +// Contact contains information about a contact. +// +// Note that LastName and UserID may be empty. +type Contact struct { + PhoneNumber string `json:"phone_number"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` // optional + UserID int `json:"user_id"` // optional +} + +// Location contains information about a place. +type Location struct { + Longitude float64 `json:"longitude"` + Latitude float64 `json:"latitude"` +} + +// Venue contains information about a venue, including its Location. +type Venue struct { + Location Location `json:"location"` + Title string `json:"title"` + Address string `json:"address"` + FoursquareID string `json:"foursquare_id"` // optional +} + +// UserProfilePhotos contains a set of user profile photos. +type UserProfilePhotos struct { + TotalCount int `json:"total_count"` + Photos [][]PhotoSize `json:"photos"` +} + +// File contains information about a file to download from Telegram. +type File struct { + FileID string `json:"file_id"` + FileSize int `json:"file_size"` // optional + FilePath string `json:"file_path"` // optional +} + +// Link returns a full path to the download URL for a File. +// +// It requires the Bot Token to create the link. +func (f *File) Link(token string) string { + return fmt.Sprintf(FileEndpoint, token, f.FilePath) +} + +// ReplyKeyboardMarkup allows the Bot to set a custom keyboard. +type ReplyKeyboardMarkup struct { + Keyboard [][]KeyboardButton `json:"keyboard"` + ResizeKeyboard bool `json:"resize_keyboard"` // optional + OneTimeKeyboard bool `json:"one_time_keyboard"` // optional + Selective bool `json:"selective"` // optional +} + +// KeyboardButton is a button within a custom keyboard. +type KeyboardButton struct { + Text string `json:"text"` + RequestContact bool `json:"request_contact"` + RequestLocation bool `json:"request_location"` +} + +// ReplyKeyboardHide allows the Bot to hide a custom keyboard. +type ReplyKeyboardHide struct { + HideKeyboard bool `json:"hide_keyboard"` + Selective bool `json:"selective"` // optional +} + +// ReplyKeyboardRemove allows the Bot to hide a custom keyboard. +type ReplyKeyboardRemove struct { + RemoveKeyboard bool `json:"remove_keyboard"` + Selective bool `json:"selective"` +} + +// InlineKeyboardMarkup is a custom keyboard presented for an inline bot. +type InlineKeyboardMarkup struct { + InlineKeyboard [][]InlineKeyboardButton `json:"inline_keyboard"` +} + +// InlineKeyboardButton is a button within a custom keyboard for +// inline query responses. +// +// Note that some values are references as even an empty string +// will change behavior. +// +// CallbackGame, if set, MUST be first button in first row. +type InlineKeyboardButton struct { + Text string `json:"text"` + URL *string `json:"url,omitempty"` // optional + CallbackData *string `json:"callback_data,omitempty"` // optional + SwitchInlineQuery *string `json:"switch_inline_query,omitempty"` // optional + SwitchInlineQueryCurrentChat *string `json:"switch_inline_query_current_chat,omitempty"` // optional + CallbackGame *CallbackGame `json:"callback_game,omitempty"` // optional + Pay bool `json:"pay,omitempty"` // optional +} + +// CallbackQuery is data sent when a keyboard button with callback data +// is clicked. +type CallbackQuery struct { + ID string `json:"id"` + From *User `json:"from"` + Message *Message `json:"message"` // optional + InlineMessageID string `json:"inline_message_id"` // optional + ChatInstance string `json:"chat_instance"` + Data string `json:"data"` // optional + GameShortName string `json:"game_short_name"` // optional +} + +// ForceReply allows the Bot to have users directly reply to it without +// additional interaction. +type ForceReply struct { + ForceReply bool `json:"force_reply"` + Selective bool `json:"selective"` // optional +} + +// ChatMember is information about a member in a chat. +type ChatMember struct { + User *User `json:"user"` + Status string `json:"status"` + UntilDate int64 `json:"until_date,omitempty"` // optional + CanBeEdited bool `json:"can_be_edited,omitempty"` // optional + CanChangeInfo bool `json:"can_change_info,omitempty"` // optional + CanPostMessages bool `json:"can_post_messages,omitempty"` // optional + CanEditMessages bool `json:"can_edit_messages,omitempty"` // optional + CanDeleteMessages bool `json:"can_delete_messages,omitempty"` // optional + CanInviteUsers bool `json:"can_invite_users,omitempty"` // optional + CanRestrictMembers bool `json:"can_restrict_members,omitempty"` // optional + CanPinMessages bool `json:"can_pin_messages,omitempty"` // optional + CanPromoteMembers bool `json:"can_promote_members,omitempty"` // optional + CanSendMessages bool `json:"can_send_messages,omitempty"` // optional + CanSendMediaMessages bool `json:"can_send_media_messages,omitempty"` // optional + CanSendOtherMessages bool `json:"can_send_other_messages,omitempty"` // optional + CanAddWebPagePreviews bool `json:"can_add_web_page_previews,omitempty"` // optional +} + +// IsCreator returns if the ChatMember was the creator of the chat. +func (chat ChatMember) IsCreator() bool { return chat.Status == "creator" } + +// IsAdministrator returns if the ChatMember is a chat administrator. +func (chat ChatMember) IsAdministrator() bool { return chat.Status == "administrator" } + +// IsMember returns if the ChatMember is a current member of the chat. +func (chat ChatMember) IsMember() bool { return chat.Status == "member" } + +// HasLeft returns if the ChatMember left the chat. +func (chat ChatMember) HasLeft() bool { return chat.Status == "left" } + +// WasKicked returns if the ChatMember was kicked from the chat. +func (chat ChatMember) WasKicked() bool { return chat.Status == "kicked" } + +// Game is a game within Telegram. +type Game struct { + Title string `json:"title"` + Description string `json:"description"` + Photo []PhotoSize `json:"photo"` + Text string `json:"text"` + TextEntities []MessageEntity `json:"text_entities"` + Animation Animation `json:"animation"` +} + +// Animation is a GIF animation demonstrating the game. +type Animation struct { + FileID string `json:"file_id"` + Thumb PhotoSize `json:"thumb"` + FileName string `json:"file_name"` + MimeType string `json:"mime_type"` + FileSize int `json:"file_size"` +} + +// GameHighScore is a user's score and position on the leaderboard. +type GameHighScore struct { + Position int `json:"position"` + User User `json:"user"` + Score int `json:"score"` +} + +// CallbackGame is for starting a game in an inline keyboard button. +type CallbackGame struct{} + +// WebhookInfo is information about a currently set webhook. +type WebhookInfo struct { + URL string `json:"url"` + HasCustomCertificate bool `json:"has_custom_certificate"` + PendingUpdateCount int `json:"pending_update_count"` + LastErrorDate int `json:"last_error_date"` // optional + LastErrorMessage string `json:"last_error_message"` // optional +} + +// IsSet returns true if a webhook is currently set. +func (info WebhookInfo) IsSet() bool { + return info.URL != "" +} + +// InlineQuery is a Query from Telegram for an inline request. +type InlineQuery struct { + ID string `json:"id"` + From *User `json:"from"` + Location *Location `json:"location"` // optional + Query string `json:"query"` + Offset string `json:"offset"` +} + +// InlineQueryResultArticle is an inline query response article. +type InlineQueryResultArticle struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + Title string `json:"title"` // required + InputMessageContent interface{} `json:"input_message_content,omitempty"` // required + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + URL string `json:"url"` + HideURL bool `json:"hide_url"` + Description string `json:"description"` + ThumbURL string `json:"thumb_url"` + ThumbWidth int `json:"thumb_width"` + ThumbHeight int `json:"thumb_height"` +} + +// InlineQueryResultPhoto is an inline query response photo. +type InlineQueryResultPhoto struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + URL string `json:"photo_url"` // required + MimeType string `json:"mime_type"` + Width int `json:"photo_width"` + Height int `json:"photo_height"` + ThumbURL string `json:"thumb_url"` + Title string `json:"title"` + Description string `json:"description"` + Caption string `json:"caption"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultGIF is an inline query response GIF. +type InlineQueryResultGIF struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + URL string `json:"gif_url"` // required + Width int `json:"gif_width"` + Height int `json:"gif_height"` + Duration int `json:"gif_duration"` + ThumbURL string `json:"thumb_url"` + Title string `json:"title"` + Caption string `json:"caption"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultMPEG4GIF is an inline query response MPEG4 GIF. +type InlineQueryResultMPEG4GIF struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + URL string `json:"mpeg4_url"` // required + Width int `json:"mpeg4_width"` + Height int `json:"mpeg4_height"` + Duration int `json:"mpeg4_duration"` + ThumbURL string `json:"thumb_url"` + Title string `json:"title"` + Caption string `json:"caption"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultVideo is an inline query response video. +type InlineQueryResultVideo struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + URL string `json:"video_url"` // required + MimeType string `json:"mime_type"` // required + ThumbURL string `json:"thumb_url"` + Title string `json:"title"` + Caption string `json:"caption"` + Width int `json:"video_width"` + Height int `json:"video_height"` + Duration int `json:"video_duration"` + Description string `json:"description"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultAudio is an inline query response audio. +type InlineQueryResultAudio struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + URL string `json:"audio_url"` // required + Title string `json:"title"` // required + Caption string `json:"caption"` + Performer string `json:"performer"` + Duration int `json:"audio_duration"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultVoice is an inline query response voice. +type InlineQueryResultVoice struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + URL string `json:"voice_url"` // required + Title string `json:"title"` // required + Caption string `json:"caption"` + Duration int `json:"voice_duration"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` +} + +// InlineQueryResultDocument is an inline query response document. +type InlineQueryResultDocument struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + Title string `json:"title"` // required + Caption string `json:"caption"` + URL string `json:"document_url"` // required + MimeType string `json:"mime_type"` // required + Description string `json:"description"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` + ThumbURL string `json:"thumb_url"` + ThumbWidth int `json:"thumb_width"` + ThumbHeight int `json:"thumb_height"` +} + +// InlineQueryResultLocation is an inline query response location. +type InlineQueryResultLocation struct { + Type string `json:"type"` // required + ID string `json:"id"` // required + Latitude float64 `json:"latitude"` // required + Longitude float64 `json:"longitude"` // required + Title string `json:"title"` // required + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` + InputMessageContent interface{} `json:"input_message_content,omitempty"` + ThumbURL string `json:"thumb_url"` + ThumbWidth int `json:"thumb_width"` + ThumbHeight int `json:"thumb_height"` +} + +// InlineQueryResultGame is an inline query response game. +type InlineQueryResultGame struct { + Type string `json:"type"` + ID string `json:"id"` + GameShortName string `json:"game_short_name"` + ReplyMarkup *InlineKeyboardMarkup `json:"reply_markup,omitempty"` +} + +// ChosenInlineResult is an inline query result chosen by a User +type ChosenInlineResult struct { + ResultID string `json:"result_id"` + From *User `json:"from"` + Location *Location `json:"location"` + InlineMessageID string `json:"inline_message_id"` + Query string `json:"query"` +} + +// InputTextMessageContent contains text for displaying +// as an inline query result. +type InputTextMessageContent struct { + Text string `json:"message_text"` + ParseMode string `json:"parse_mode"` + DisableWebPagePreview bool `json:"disable_web_page_preview"` +} + +// InputLocationMessageContent contains a location for displaying +// as an inline query result. +type InputLocationMessageContent struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` +} + +// InputVenueMessageContent contains a venue for displaying +// as an inline query result. +type InputVenueMessageContent struct { + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + Title string `json:"title"` + Address string `json:"address"` + FoursquareID string `json:"foursquare_id"` +} + +// InputContactMessageContent contains a contact for displaying +// as an inline query result. +type InputContactMessageContent struct { + PhoneNumber string `json:"phone_number"` + FirstName string `json:"first_name"` + LastName string `json:"last_name"` +} + +// Invoice contains basic information about an invoice. +type Invoice struct { + Title string `json:"title"` + Description string `json:"description"` + StartParameter string `json:"start_parameter"` + Currency string `json:"currency"` + TotalAmount int `json:"total_amount"` +} + +// LabeledPrice represents a portion of the price for goods or services. +type LabeledPrice struct { + Label string `json:"label"` + Amount int `json:"amount"` +} + +// ShippingAddress represents a shipping address. +type ShippingAddress struct { + CountryCode string `json:"country_code"` + State string `json:"state"` + City string `json:"city"` + StreetLine1 string `json:"street_line1"` + StreetLine2 string `json:"street_line2"` + PostCode string `json:"post_code"` +} + +// OrderInfo represents information about an order. +type OrderInfo struct { + Name string `json:"name,omitempty"` + PhoneNumber string `json:"phone_number,omitempty"` + Email string `json:"email,omitempty"` + ShippingAddress *ShippingAddress `json:"shipping_address,omitempty"` +} + +// ShippingOption represents one shipping option. +type ShippingOption struct { + ID string `json:"id"` + Title string `json:"title"` + Prices *[]LabeledPrice `json:"prices"` +} + +// SuccessfulPayment contains basic information about a successful payment. +type SuccessfulPayment struct { + Currency string `json:"currency"` + TotalAmount int `json:"total_amount"` + InvoicePayload string `json:"invoice_payload"` + ShippingOptionID string `json:"shipping_option_id,omitempty"` + OrderInfo *OrderInfo `json:"order_info,omitempty"` + TelegramPaymentChargeID string `json:"telegram_payment_charge_id"` + ProviderPaymentChargeID string `json:"provider_payment_charge_id"` +} + +// ShippingQuery contains information about an incoming shipping query. +type ShippingQuery struct { + ID string `json:"id"` + From *User `json:"from"` + InvoicePayload string `json:"invoice_payload"` + ShippingAddress *ShippingAddress `json:"shipping_address"` +} + +// PreCheckoutQuery contains information about an incoming pre-checkout query. +type PreCheckoutQuery struct { + ID string `json:"id"` + From *User `json:"from"` + Currency string `json:"currency"` + TotalAmount int `json:"total_amount"` + InvoicePayload string `json:"invoice_payload"` + ShippingOptionID string `json:"shipping_option_id,omitempty"` + OrderInfo *OrderInfo `json:"order_info,omitempty"` +} + +// Error is an error containing extra information returned by the Telegram API. +type Error struct { + Message string + ResponseParameters +} + +func (e Error) Error() string { + return e.Message +} diff --git a/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types_test.go b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types_test.go new file mode 100644 index 0000000..bb7bb64 --- /dev/null +++ b/vendor/github.com/go-telegram-bot-api/telegram-bot-api/types_test.go @@ -0,0 +1,200 @@ +package tgbotapi_test + +import ( + "testing" + "time" + + "github.com/go-telegram-bot-api/telegram-bot-api" +) + +func TestUserStringWith(t *testing.T) { + user := tgbotapi.User{ + ID: 0, + FirstName: "Test", + LastName: "Test", + UserName: "", + LanguageCode: "en", + IsBot: false, + } + + if user.String() != "Test Test" { + t.Fail() + } +} + +func TestUserStringWithUserName(t *testing.T) { + user := tgbotapi.User{ + ID: 0, + FirstName: "Test", + LastName: "Test", + UserName: "@test", + LanguageCode: "en", + } + + if user.String() != "@test" { + t.Fail() + } +} + +func TestMessageTime(t *testing.T) { + message := tgbotapi.Message{Date: 0} + + date := time.Unix(0, 0) + if message.Time() != date { + t.Fail() + } +} + +func TestMessageIsCommandWithCommand(t *testing.T) { + message := tgbotapi.Message{Text: "/command"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + + if message.IsCommand() != true { + t.Fail() + } +} + +func TestIsCommandWithText(t *testing.T) { + message := tgbotapi.Message{Text: "some text"} + + if message.IsCommand() != false { + t.Fail() + } +} + +func TestIsCommandWithEmptyText(t *testing.T) { + message := tgbotapi.Message{Text: ""} + + if message.IsCommand() != false { + t.Fail() + } +} + +func TestCommandWithCommand(t *testing.T) { + message := tgbotapi.Message{Text: "/command"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + + if message.Command() != "command" { + t.Fail() + } +} + +func TestCommandWithEmptyText(t *testing.T) { + message := tgbotapi.Message{Text: ""} + + if message.Command() != "" { + t.Fail() + } +} + +func TestCommandWithNonCommand(t *testing.T) { + message := tgbotapi.Message{Text: "test text"} + + if message.Command() != "" { + t.Fail() + } +} + +func TestCommandWithBotName(t *testing.T) { + message := tgbotapi.Message{Text: "/command@testbot"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} + + if message.Command() != "command" { + t.Fail() + } +} + +func TestCommandWithAtWithBotName(t *testing.T) { + message := tgbotapi.Message{Text: "/command@testbot"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 16}} + + if message.CommandWithAt() != "command@testbot" { + t.Fail() + } +} + +func TestMessageCommandArgumentsWithArguments(t *testing.T) { + message := tgbotapi.Message{Text: "/command with arguments"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + if message.CommandArguments() != "with arguments" { + t.Fail() + } +} + +func TestMessageCommandArgumentsWithMalformedArguments(t *testing.T) { + message := tgbotapi.Message{Text: "/command-without argument space"} + message.Entities = &[]tgbotapi.MessageEntity{{Type: "bot_command", Offset: 0, Length: 8}} + if message.CommandArguments() != "without argument space" { + t.Fail() + } +} + +func TestMessageCommandArgumentsWithoutArguments(t *testing.T) { + message := tgbotapi.Message{Text: "/command"} + if message.CommandArguments() != "" { + t.Fail() + } +} + +func TestMessageCommandArgumentsForNonCommand(t *testing.T) { + message := tgbotapi.Message{Text: "test text"} + if message.CommandArguments() != "" { + t.Fail() + } +} + +func TestMessageEntityParseURLGood(t *testing.T) { + entity := tgbotapi.MessageEntity{URL: "https://www.google.com"} + + if _, err := entity.ParseURL(); err != nil { + t.Fail() + } +} + +func TestMessageEntityParseURLBad(t *testing.T) { + entity := tgbotapi.MessageEntity{URL: ""} + + if _, err := entity.ParseURL(); err == nil { + t.Fail() + } +} + +func TestChatIsPrivate(t *testing.T) { + chat := tgbotapi.Chat{ID: 10, Type: "private"} + + if chat.IsPrivate() != true { + t.Fail() + } +} + +func TestChatIsGroup(t *testing.T) { + chat := tgbotapi.Chat{ID: 10, Type: "group"} + + if chat.IsGroup() != true { + t.Fail() + } +} + +func TestChatIsChannel(t *testing.T) { + chat := tgbotapi.Chat{ID: 10, Type: "channel"} + + if chat.IsChannel() != true { + t.Fail() + } +} + +func TestChatIsSuperGroup(t *testing.T) { + chat := tgbotapi.Chat{ID: 10, Type: "supergroup"} + + if !chat.IsSuperGroup() { + t.Fail() + } +} + +func TestFileLink(t *testing.T) { + file := tgbotapi.File{FilePath: "test/test.txt"} + + if file.Link("token") != "https://api.telegram.org/file/bottoken/test/test.txt" { + t.Fail() + } +} diff --git a/vendor/github.com/technoweenie/multipartstreamer/LICENSE b/vendor/github.com/technoweenie/multipartstreamer/LICENSE new file mode 100644 index 0000000..20d92fb --- /dev/null +++ b/vendor/github.com/technoweenie/multipartstreamer/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2013-* rick olson + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/technoweenie/multipartstreamer/README.md b/vendor/github.com/technoweenie/multipartstreamer/README.md new file mode 100644 index 0000000..dc1f824 --- /dev/null +++ b/vendor/github.com/technoweenie/multipartstreamer/README.md @@ -0,0 +1,47 @@ +# multipartstreamer + +Package multipartstreamer helps you encode large files in MIME multipart format +without reading the entire content into memory. It uses io.MultiReader to +combine an inner multipart.Reader with a file handle. + +```go +package main + +import ( + "github.com/technoweenie/multipartstreamer.go" + "net/http" +) + +func main() { + ms := multipartstreamer.New() + + ms.WriteFields(map[string]string{ + "key": "some-key", + "AWSAccessKeyId": "ABCDEF", + "acl": "some-acl", + }) + + // Add any io.Reader to the multipart.Reader. + ms.WriteReader("file", "filename", some_ioReader, size) + + // Shortcut for adding local file. + ms.WriteFile("file", "path/to/file") + + req, _ := http.NewRequest("POST", "someurl", nil) + ms.SetupRequest(req) + + res, _ := http.DefaultClient.Do(req) +} +``` + +One limitation: You can only write a single file. + +## TODO + +* Multiple files? + +## Credits + +Heavily inspired by James + +https://groups.google.com/forum/?fromgroups=#!topic/golang-nuts/Zjg5l4nKcQ0 diff --git a/vendor/github.com/technoweenie/multipartstreamer/examples/multipart.go b/vendor/github.com/technoweenie/multipartstreamer/examples/multipart.go new file mode 100644 index 0000000..971078b --- /dev/null +++ b/vendor/github.com/technoweenie/multipartstreamer/examples/multipart.go @@ -0,0 +1,31 @@ +package main + +import ( + "bytes" + "flag" + "fmt" + "io" + "mime/multipart" + "os" + "path/filepath" +) + +func main() { + defaultPath, _ := os.Getwd() + defaultFile := filepath.Join(defaultPath, "streamer.go") + fullpath := flag.String("path", defaultFile, "Path to the include in the multipart data.") + flag.Parse() + + buffer := bytes.NewBufferString("") + writer := multipart.NewWriter(buffer) + + fmt.Println("Adding the file to the multipart writer") + fileWriter, _ := writer.CreateFormFile("file", *fullpath) + fileData, _ := os.Open(*fullpath) + io.Copy(fileWriter, fileData) + writer.Close() + + fmt.Println("Writing the multipart data to a file") + output, _ := os.Create("multiparttest") + io.Copy(output, buffer) +} diff --git a/vendor/github.com/technoweenie/multipartstreamer/examples/streamer.go b/vendor/github.com/technoweenie/multipartstreamer/examples/streamer.go new file mode 100644 index 0000000..324f7e1 --- /dev/null +++ b/vendor/github.com/technoweenie/multipartstreamer/examples/streamer.go @@ -0,0 +1,27 @@ +package main + +import ( + "flag" + "fmt" + "github.com/technoweenie/multipartstreamer" + "io" + "os" + "path/filepath" +) + +func main() { + defaultPath, _ := os.Getwd() + defaultFile := filepath.Join(defaultPath, "streamer.go") + fullpath := flag.String("path", defaultFile, "Path to the include in the multipart data.") + flag.Parse() + + ms := multipartstreamer.New() + + fmt.Println("Adding the file to the multipart writer") + ms.WriteFile("file", *fullpath) + reader := ms.GetReader() + + fmt.Println("Writing the multipart data to a file") + file, _ := os.Create("streamtest") + io.Copy(file, reader) +} diff --git a/vendor/github.com/technoweenie/multipartstreamer/multipartstreamer.go b/vendor/github.com/technoweenie/multipartstreamer/multipartstreamer.go new file mode 100644 index 0000000..26d8e85 --- /dev/null +++ b/vendor/github.com/technoweenie/multipartstreamer/multipartstreamer.go @@ -0,0 +1,101 @@ +/* +Package multipartstreamer helps you encode large files in MIME multipart format +without reading the entire content into memory. It uses io.MultiReader to +combine an inner multipart.Reader with a file handle. +*/ +package multipartstreamer + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "path/filepath" +) + +type MultipartStreamer struct { + ContentType string + bodyBuffer *bytes.Buffer + bodyWriter *multipart.Writer + closeBuffer *bytes.Buffer + reader io.Reader + contentLength int64 +} + +// New initializes a new MultipartStreamer. +func New() (m *MultipartStreamer) { + m = &MultipartStreamer{bodyBuffer: new(bytes.Buffer)} + + m.bodyWriter = multipart.NewWriter(m.bodyBuffer) + boundary := m.bodyWriter.Boundary() + m.ContentType = "multipart/form-data; boundary=" + boundary + + closeBoundary := fmt.Sprintf("\r\n--%s--\r\n", boundary) + m.closeBuffer = bytes.NewBufferString(closeBoundary) + + return +} + +// WriteFields writes multiple form fields to the multipart.Writer. +func (m *MultipartStreamer) WriteFields(fields map[string]string) error { + var err error + + for key, value := range fields { + err = m.bodyWriter.WriteField(key, value) + if err != nil { + return err + } + } + + return nil +} + +// WriteReader adds an io.Reader to get the content of a file. The reader is +// not accessed until the multipart.Reader is copied to some output writer. +func (m *MultipartStreamer) WriteReader(key, filename string, size int64, reader io.Reader) (err error) { + m.reader = reader + m.contentLength = size + + _, err = m.bodyWriter.CreateFormFile(key, filename) + return +} + +// WriteFile is a shortcut for adding a local file as an io.Reader. +func (m *MultipartStreamer) WriteFile(key, filename string) error { + fh, err := os.Open(filename) + if err != nil { + return err + } + + stat, err := fh.Stat() + if err != nil { + return err + } + + return m.WriteReader(key, filepath.Base(filename), stat.Size(), fh) +} + +// SetupRequest sets up the http.Request body, and some crucial HTTP headers. +func (m *MultipartStreamer) SetupRequest(req *http.Request) { + req.Body = m.GetReader() + req.Header.Add("Content-Type", m.ContentType) + req.ContentLength = m.Len() +} + +func (m *MultipartStreamer) Boundary() string { + return m.bodyWriter.Boundary() +} + +// Len calculates the byte size of the multipart content. +func (m *MultipartStreamer) Len() int64 { + return m.contentLength + int64(m.bodyBuffer.Len()) + int64(m.closeBuffer.Len()) +} + +// GetReader gets an io.ReadCloser for passing to an http.Request. +func (m *MultipartStreamer) GetReader() io.ReadCloser { + reader := io.MultiReader(m.bodyBuffer, m.reader, m.closeBuffer) + return ioutil.NopCloser(reader) +} diff --git a/vendor/github.com/technoweenie/multipartstreamer/multipartstreamer_test.go b/vendor/github.com/technoweenie/multipartstreamer/multipartstreamer_test.go new file mode 100644 index 0000000..96f1bbe --- /dev/null +++ b/vendor/github.com/technoweenie/multipartstreamer/multipartstreamer_test.go @@ -0,0 +1,124 @@ +package multipartstreamer + +import ( + "bytes" + "io" + "io/ioutil" + "mime/multipart" + "os" + "path/filepath" + "testing" +) + +func TestMultipartFile(t *testing.T) { + path, _ := os.Getwd() + file := filepath.Join(path, "multipartstreamer.go") + stat, _ := os.Stat(file) + + ms := New() + err := ms.WriteFields(map[string]string{"a": "b"}) + if err != nil { + t.Fatalf("Error writing fields: %s", err) + } + + err = ms.WriteFile("file", file) + if err != nil { + t.Fatalf("Error writing file: %s", err) + } + + diff := ms.Len() - stat.Size() + if diff != 363 { + t.Error("Unexpected multipart size") + } + + data, err := ioutil.ReadAll(ms.GetReader()) + if err != nil { + t.Fatalf("Error reading multipart data: %s", err) + } + + buf := bytes.NewBuffer(data) + reader := multipart.NewReader(buf, ms.Boundary()) + + part, err := reader.NextPart() + if err != nil { + t.Fatalf("Expected form field: %s", err) + } + + if str := part.FileName(); str != "" { + t.Errorf("Unexpected filename: %s", str) + } + + if str := part.FormName(); str != "a" { + t.Errorf("Unexpected form name: %s", str) + } + + if by, _ := ioutil.ReadAll(part); string(by) != "b" { + t.Errorf("Unexpected form value: %s", string(by)) + } + + part, err = reader.NextPart() + if err != nil { + t.Fatalf("Expected file field: %s", err) + } + + if str := part.FileName(); str != "multipartstreamer.go" { + t.Errorf("Unexpected filename: %s", str) + } + + if str := part.FormName(); str != "file" { + t.Errorf("Unexpected form name: %s", str) + } + + src, _ := ioutil.ReadFile(file) + if by, _ := ioutil.ReadAll(part); string(by) != string(src) { + t.Errorf("Unexpected file value") + } + + part, err = reader.NextPart() + if err != io.EOF { + t.Errorf("Unexpected 3rd part: %s", part) + } +} + +func TestMultipartReader(t *testing.T) { + ms := New() + + err := ms.WriteReader("file", "code/bass", 3, bytes.NewBufferString("ABC")) + if err != nil { + t.Fatalf("Error writing reader: %s", err) + } + + if size := ms.Len(); size != 244 { + t.Errorf("Unexpected multipart size: %d", size) + } + + data, err := ioutil.ReadAll(ms.GetReader()) + if err != nil { + t.Fatalf("Error reading multipart data: %s", err) + } + + buf := bytes.NewBuffer(data) + reader := multipart.NewReader(buf, ms.Boundary()) + + part, err := reader.NextPart() + if err != nil { + t.Fatalf("Expected file field: %s", err) + } + + if str := part.FileName(); str != "code/bass" { + t.Errorf("Unexpected filename: %s", str) + } + + if str := part.FormName(); str != "file" { + t.Errorf("Unexpected form name: %s", str) + } + + if by, _ := ioutil.ReadAll(part); string(by) != "ABC" { + t.Errorf("Unexpected file value") + } + + part, err = reader.NextPart() + if err != io.EOF { + t.Errorf("Unexpected 2nd part: %s", part) + } +} diff --git a/vendor/github.com/technoweenie/multipartstreamer/script/test b/vendor/github.com/technoweenie/multipartstreamer/script/test new file mode 100755 index 0000000..d778968 --- /dev/null +++ b/vendor/github.com/technoweenie/multipartstreamer/script/test @@ -0,0 +1,2 @@ +gofmt -l -w . +go test \ No newline at end of file