diff --git a/discord.go b/discord.go index 681541efa..bae8fa8f2 100644 --- a/discord.go +++ b/discord.go @@ -14,9 +14,14 @@ package discordgo import ( + "context" "errors" "fmt" + "github.com/hashicorp/go-cleanhttp" + "log" "net/http" + "strconv" + "strings" "time" ) @@ -56,10 +61,15 @@ func New(args ...interface{}) (s *Session, err error) { ShouldReconnectOnError: true, ShardID: 0, ShardCount: 1, - MaxRestRetries: 3, - Client: &http.Client{Timeout: (20 * time.Second)}, - sequence: new(int64), + MaxRestRetries: 10, + Client: cleanhttp.DefaultPooledClient(), LastHeartbeatAck: time.Now().UTC(), + tokenInvalid: new(int32), + } + + s.GatewayManager = &GatewayConnectionManager{ + session: s, + voiceConnections: make(map[int64]*VoiceConnection), } // If no arguments are passed return the empty Session interface. @@ -144,3 +154,31 @@ func New(args ...interface{}) (s *Session, err error) { return } + +func CheckRetry(_ context.Context, resp *http.Response, err error) (bool, error) { + if err != nil { + return true, err + } + + return false, nil +} + +type retryableLogger struct{} + +func (r *retryableLogger) Printf(format string, args ...interface{}) { + s := fmt.Sprintf(format, args...) + + if strings.Contains(s, "[DEBUG]") { + return + } + + if strings.HasSuffix(s, "\n") { + s = s[:len(s)-1] + } + + log.Println(s) +} + +func StrID(id int64) string { + return strconv.FormatInt(id, 10) +} diff --git a/discord_test.go b/discord_test.go index 318bed0a3..4fa3cb5c1 100644 --- a/discord_test.go +++ b/discord_test.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "runtime" + "strconv" "sync/atomic" "testing" "time" @@ -15,13 +16,18 @@ var ( dg *Session // Stores a global discordgo user session dgBot *Session // Stores a global discordgo bot session - envToken = os.Getenv("DGU_TOKEN") // Token to use when authenticating the user account - envBotToken = os.Getenv("DGB_TOKEN") // Token to use when authenticating the bot account - envGuild = os.Getenv("DG_GUILD") // Guild ID to use for tests - envChannel = os.Getenv("DG_CHANNEL") // Channel ID to use for tests - envAdmin = os.Getenv("DG_ADMIN") // User ID of admin user to use for tests + envToken = os.Getenv("DGU_TOKEN") // Token to use when authenticating the user account + envBotToken = os.Getenv("DGB_TOKEN") // Token to use when authenticating the bot account + envGuild = parseID(os.Getenv("DG_GUILD")) // Guild ID to use for tests + envChannel = parseID(os.Getenv("DG_CHANNEL")) // Channel ID to use for tests + envAdmin = parseID(os.Getenv("DG_ADMIN")) // User ID of admin user to use for tests ) +func parseID(str string) int64 { + id, _ := strconv.ParseInt(str, 10, 64) + return id +} + func init() { fmt.Println("Init is being called.") if envBotToken != "" { @@ -103,12 +109,9 @@ func TestOpenClose(t *testing.T) { // this is totally gross. start := time.Now() for { - d.RLock() - if d.DataReady { - d.RUnlock() + if d.GatewayManager.Status() == GatewayStatusReady { break } - d.RUnlock() if time.Since(start) > 10*time.Second { t.Fatal("DataReady never became true.yy") diff --git a/emoji_codemap.go b/emoji_codemap.go new file mode 100644 index 000000000..c40464fa9 --- /dev/null +++ b/emoji_codemap.go @@ -0,0 +1,2630 @@ +package discordgo + +// NOTE: THIS FILE WAS PRODUCED BY THE +// EMOJICODEMAP CODE GENERATION TOOL (github.com/kyokomi/generateEmojiCodeMap) +// DO NOT EDIT + +// Mapping from character to concrete escape code. +var emojiCodeMap = map[string]string{ + "::": "\U0001f590", + ":100:": "\U0001f4af", + ":1234:": "\U0001f522", + ":8ball:": "\U0001f3b1", + ":a:": "\U0001f170", + ":ab:": "\U0001f18e", + ":abc:": "\U0001f524", + ":abcd:": "\U0001f521", + ":accept:": "\U0001f251", + ":adult:": "\U0001f9d1", + ":adult_tone1:": "\U0001f9d1\U0001f3fb", + ":adult_tone2:": "\U0001f9d1\U0001f3fc", + ":adult_tone3:": "\U0001f9d1\U0001f3fd", + ":adult_tone4:": "\U0001f9d1\U0001f3fe", + ":adult_tone5:": "\U0001f9d1\U0001f3ff", + ":aerial_tramway:": "\U0001f6a1", + ":airplane:": "\u2708", + ":airplane_arriving:": "\U0001f6ec", + ":airplane_departure:": "\U0001f6eb", + ":airplane_small:": "\U0001f6e9", + ":alarm_clock:": "\u23f0", + ":alembic:": "\u2697", + ":alien:": "\U0001f47d", + ":ambulance:": "\U0001f691", + ":amphora:": "\U0001f3fa", + ":anchor:": "\u2693", + ":angel:": "\U0001f47c", + ":angel_tone1:": "\U0001f47c\U0001f3fb", + ":angel_tone2:": "\U0001f47c\U0001f3fc", + ":angel_tone3:": "\U0001f47c\U0001f3fd", + ":angel_tone4:": "\U0001f47c\U0001f3fe", + ":angel_tone5:": "\U0001f47c\U0001f3ff", + ":anger:": "\U0001f4a2", + ":anger_right:": "\U0001f5ef", + ":angry:": "\U0001f620", + ":anguished:": "\U0001f627", + ":ant:": "\U0001f41c", + ":apple:": "\U0001f34e", + ":aquarius:": "\u2652", + ":aries:": "\u2648", + ":arrow_backward:": "\u25c0", + ":arrow_double_down:": "\u23ec", + ":arrow_double_up:": "\u23eb", + ":arrow_down:": "\u2b07", + ":arrow_down_small:": "\U0001f53d", + ":arrow_forward:": "\u25b6", + ":arrow_heading_down:": "\u2935", + ":arrow_heading_up:": "\u2934", + ":arrow_left:": "\u2b05", + ":arrow_lower_left:": "\u2199", + ":arrow_lower_right:": "\u2198", + ":arrow_right:": "\u27a1", + ":arrow_right_hook:": "\u21aa", + ":arrow_up:": "\u2b06", + ":arrow_up_down:": "\u2195", + ":arrow_up_small:": "\U0001f53c", + ":arrow_upper_left:": "\u2196", + ":arrow_upper_right:": "\u2197", + ":arrows_clockwise:": "\U0001f503", + ":arrows_counterclockwise:": "\U0001f504", + ":art:": "\U0001f3a8", + ":articulated_lorry:": "\U0001f69b", + ":asterisk:": "*\ufe0f\u20e3", + ":astonished:": "\U0001f632", + ":athletic_shoe:": "\U0001f45f", + ":atm:": "\U0001f3e7", + ":atom:": "\u269b", + ":avocado:": "\U0001f951", + ":b:": "\U0001f171", + ":baby:": "\U0001f476", + ":baby_bottle:": "\U0001f37c", + ":baby_chick:": "\U0001f424", + ":baby_symbol:": "\U0001f6bc", + ":baby_tone1:": "\U0001f476\U0001f3fb", + ":baby_tone2:": "\U0001f476\U0001f3fc", + ":baby_tone3:": "\U0001f476\U0001f3fd", + ":baby_tone4:": "\U0001f476\U0001f3fe", + ":baby_tone5:": "\U0001f476\U0001f3ff", + ":back:": "\U0001f519", + ":bacon:": "\U0001f953", + ":badminton:": "\U0001f3f8", + ":baggage_claim:": "\U0001f6c4", + ":balloon:": "\U0001f388", + ":ballot_box:": "\U0001f5f3", + ":ballot_box_with_check:": "\u2611", + ":bamboo:": "\U0001f38d", + ":banana:": "\U0001f34c", + ":bangbang:": "\u203c", + ":bank:": "\U0001f3e6", + ":bar_chart:": "\U0001f4ca", + ":barber:": "\U0001f488", + ":baseball:": "\u26be", + ":basketball:": "\U0001f3c0", + ":bat:": "\U0001f987", + ":bath:": "\U0001f6c0", + ":bath_tone1:": "\U0001f6c0\U0001f3fb", + ":bath_tone2:": "\U0001f6c0\U0001f3fc", + ":bath_tone3:": "\U0001f6c0\U0001f3fd", + ":bath_tone4:": "\U0001f6c0\U0001f3fe", + ":bath_tone5:": "\U0001f6c0\U0001f3ff", + ":bathtub:": "\U0001f6c1", + ":battery:": "\U0001f50b", + ":beach:": "\U0001f3d6", + ":beach_umbrella:": "\u26f1", + ":bear:": "\U0001f43b", + ":bearded_person:": "\U0001f9d4", + ":bearded_person_tone1:": "\U0001f9d4\U0001f3fb", + ":bearded_person_tone2:": "\U0001f9d4\U0001f3fc", + ":bearded_person_tone3:": "\U0001f9d4\U0001f3fd", + ":bearded_person_tone4:": "\U0001f9d4\U0001f3fe", + ":bearded_person_tone5:": "\U0001f9d4\U0001f3ff", + ":bed:": "\U0001f6cf", + ":bee:": "\U0001f41d", + ":beer:": "\U0001f37a", + ":beers:": "\U0001f37b", + ":beetle:": "\U0001f41e", + ":beginner:": "\U0001f530", + ":bell:": "\U0001f514", + ":bellhop:": "\U0001f6ce", + ":bento:": "\U0001f371", + ":bike:": "\U0001f6b2", + ":bikini:": "\U0001f459", + ":billed_cap:": "\U0001f9e2", + ":biohazard:": "\u2623", + ":bird:": "\U0001f426", + ":birthday:": "\U0001f382", + ":black_circle:": "\u26ab", + ":black_heart:": "\U0001f5a4", + ":black_joker:": "\U0001f0cf", + ":black_large_square:": "\u2b1b", + ":black_medium_small_square:": "\u25fe", + ":black_medium_square:": "\u25fc", + ":black_nib:": "\u2712", + ":black_small_square:": "\u25aa", + ":black_square_button:": "\U0001f532", + ":blond-haired_man:": "\U0001f471\u200d\u2642\ufe0f", + ":blond-haired_man_tone1:": "\U0001f471\U0001f3fb\u200d\u2642\ufe0f", + ":blond-haired_man_tone2:": "\U0001f471\U0001f3fc\u200d\u2642\ufe0f", + ":blond-haired_man_tone3:": "\U0001f471\U0001f3fd\u200d\u2642\ufe0f", + ":blond-haired_man_tone4:": "\U0001f471\U0001f3fe\u200d\u2642\ufe0f", + ":blond-haired_man_tone5:": "\U0001f471\U0001f3ff\u200d\u2642\ufe0f", + ":blond-haired_woman:": "\U0001f471\u200d\u2640\ufe0f", + ":blond-haired_woman_tone1:": "\U0001f471\U0001f3fb\u200d\u2640\ufe0f", + ":blond-haired_woman_tone2:": "\U0001f471\U0001f3fc\u200d\u2640\ufe0f", + ":blond-haired_woman_tone3:": "\U0001f471\U0001f3fd\u200d\u2640\ufe0f", + ":blond-haired_woman_tone4:": "\U0001f471\U0001f3fe\u200d\u2640\ufe0f", + ":blond-haired_woman_tone5:": "\U0001f471\U0001f3ff\u200d\u2640\ufe0f", + ":blond_haired_person:": "\U0001f471", + ":blond_haired_person_tone1:": "\U0001f471\U0001f3fb", + ":blond_haired_person_tone2:": "\U0001f471\U0001f3fc", + ":blond_haired_person_tone3:": "\U0001f471\U0001f3fd", + ":blond_haired_person_tone4:": "\U0001f471\U0001f3fe", + ":blond_haired_person_tone5:": "\U0001f471\U0001f3ff", + ":blossom:": "\U0001f33c", + ":blowfish:": "\U0001f421", + ":blue_book:": "\U0001f4d8", + ":blue_car:": "\U0001f699", + ":blue_circle:": "\U0001f535", + ":blue_heart:": "\U0001f499", + ":blush:": "\U0001f60a", + ":boar:": "\U0001f417", + ":bomb:": "\U0001f4a3", + ":book:": "\U0001f4d6", + ":bookmark:": "\U0001f516", + ":bookmark_tabs:": "\U0001f4d1", + ":books:": "\U0001f4da", + ":boom:": "\U0001f4a5", + ":boot:": "\U0001f462", + ":bouquet:": "\U0001f490", + ":bow_and_arrow:": "\U0001f3f9", + ":bowl_with_spoon:": "\U0001f963", + ":bowling:": "\U0001f3b3", + ":boxing_glove:": "\U0001f94a", + ":boy:": "\U0001f466", + ":boy_tone1:": "\U0001f466\U0001f3fb", + ":boy_tone2:": "\U0001f466\U0001f3fc", + ":boy_tone3:": "\U0001f466\U0001f3fd", + ":boy_tone4:": "\U0001f466\U0001f3fe", + ":boy_tone5:": "\U0001f466\U0001f3ff", + ":brain:": "\U0001f9e0", + ":bread:": "\U0001f35e", + ":breast_feeding:": "\U0001f931", + ":breast_feeding_tone1:": "\U0001f931\U0001f3fb", + ":breast_feeding_tone2:": "\U0001f931\U0001f3fc", + ":breast_feeding_tone3:": "\U0001f931\U0001f3fd", + ":breast_feeding_tone4:": "\U0001f931\U0001f3fe", + ":breast_feeding_tone5:": "\U0001f931\U0001f3ff", + ":bride_with_veil:": "\U0001f470", + ":bride_with_veil_tone1:": "\U0001f470\U0001f3fb", + ":bride_with_veil_tone2:": "\U0001f470\U0001f3fc", + ":bride_with_veil_tone3:": "\U0001f470\U0001f3fd", + ":bride_with_veil_tone4:": "\U0001f470\U0001f3fe", + ":bride_with_veil_tone5:": "\U0001f470\U0001f3ff", + ":bridge_at_night:": "\U0001f309", + ":briefcase:": "\U0001f4bc", + ":broccoli:": "\U0001f966", + ":broken_heart:": "\U0001f494", + ":bug:": "\U0001f41b", + ":bulb:": "\U0001f4a1", + ":bullettrain_front:": "\U0001f685", + ":bullettrain_side:": "\U0001f684", + ":burrito:": "\U0001f32f", + ":bus:": "\U0001f68c", + ":busstop:": "\U0001f68f", + ":bust_in_silhouette:": "\U0001f464", + ":busts_in_silhouette:": "\U0001f465", + ":butterfly:": "\U0001f98b", + ":cactus:": "\U0001f335", + ":cake:": "\U0001f370", + ":calendar:": "\U0001f4c6", + ":calendar_spiral:": "\U0001f5d3", + ":call_me:": "\U0001f919", + ":call_me_tone1:": "\U0001f919\U0001f3fb", + ":call_me_tone2:": "\U0001f919\U0001f3fc", + ":call_me_tone3:": "\U0001f919\U0001f3fd", + ":call_me_tone4:": "\U0001f919\U0001f3fe", + ":call_me_tone5:": "\U0001f919\U0001f3ff", + ":calling:": "\U0001f4f2", + ":camel:": "\U0001f42b", + ":camera:": "\U0001f4f7", + ":camera_with_flash:": "\U0001f4f8", + ":camping:": "\U0001f3d5", + ":cancer:": "\u264b", + ":candle:": "\U0001f56f", + ":candy:": "\U0001f36c", + ":canned_food:": "\U0001f96b", + ":canoe:": "\U0001f6f6", + ":capital_abcd:": "\U0001f520", + ":capricorn:": "\u2651", + ":card_box:": "\U0001f5c3", + ":card_index:": "\U0001f4c7", + ":carousel_horse:": "\U0001f3a0", + ":carrot:": "\U0001f955", + ":cat:": "\U0001f431", + ":cat2:": "\U0001f408", + ":cd:": "\U0001f4bf", + ":chains:": "\u26d3", + ":champagne:": "\U0001f37e", + ":champagne_glass:": "\U0001f942", + ":chart:": "\U0001f4b9", + ":chart_with_downwards_trend:": "\U0001f4c9", + ":chart_with_upwards_trend:": "\U0001f4c8", + ":checkered_flag:": "\U0001f3c1", + ":cheese:": "\U0001f9c0", + ":cherries:": "\U0001f352", + ":cherry_blossom:": "\U0001f338", + ":chestnut:": "\U0001f330", + ":chicken:": "\U0001f414", + ":child:": "\U0001f9d2", + ":child_tone1:": "\U0001f9d2\U0001f3fb", + ":child_tone2:": "\U0001f9d2\U0001f3fc", + ":child_tone3:": "\U0001f9d2\U0001f3fd", + ":child_tone4:": "\U0001f9d2\U0001f3fe", + ":child_tone5:": "\U0001f9d2\U0001f3ff", + ":children_crossing:": "\U0001f6b8", + ":chipmunk:": "\U0001f43f", + ":chocolate_bar:": "\U0001f36b", + ":chopsticks:": "\U0001f962", + ":christmas_tree:": "\U0001f384", + ":church:": "\u26ea", + ":cinema:": "\U0001f3a6", + ":circus_tent:": "\U0001f3aa", + ":city_dusk:": "\U0001f306", + ":city_sunset:": "\U0001f307", + ":cityscape:": "\U0001f3d9", + ":cl:": "\U0001f191", + ":clap:": "\U0001f44f", + ":clap_tone1:": "\U0001f44f\U0001f3fb", + ":clap_tone2:": "\U0001f44f\U0001f3fc", + ":clap_tone3:": "\U0001f44f\U0001f3fd", + ":clap_tone4:": "\U0001f44f\U0001f3fe", + ":clap_tone5:": "\U0001f44f\U0001f3ff", + ":clapper:": "\U0001f3ac", + ":classical_building:": "\U0001f3db", + ":clipboard:": "\U0001f4cb", + ":clock:": "\U0001f570", + ":clock1:": "\U0001f550", + ":clock10:": "\U0001f559", + ":clock1030:": "\U0001f565", + ":clock11:": "\U0001f55a", + ":clock1130:": "\U0001f566", + ":clock12:": "\U0001f55b", + ":clock1230:": "\U0001f567", + ":clock130:": "\U0001f55c", + ":clock2:": "\U0001f551", + ":clock230:": "\U0001f55d", + ":clock3:": "\U0001f552", + ":clock330:": "\U0001f55e", + ":clock4:": "\U0001f553", + ":clock430:": "\U0001f55f", + ":clock5:": "\U0001f554", + ":clock530:": "\U0001f560", + ":clock6:": "\U0001f555", + ":clock630:": "\U0001f561", + ":clock7:": "\U0001f556", + ":clock730:": "\U0001f562", + ":clock8:": "\U0001f557", + ":clock830:": "\U0001f563", + ":clock9:": "\U0001f558", + ":clock930:": "\U0001f564", + ":closed_book:": "\U0001f4d5", + ":closed_lock_with_key:": "\U0001f510", + ":closed_umbrella:": "\U0001f302", + ":cloud:": "\u2601", + ":cloud_lightning:": "\U0001f329", + ":cloud_rain:": "\U0001f327", + ":cloud_snow:": "\U0001f328", + ":cloud_tornado:": "\U0001f32a", + ":clown:": "\U0001f921", + ":clubs:": "\u2663", + ":coat:": "\U0001f9e5", + ":cocktail:": "\U0001f378", + ":coconut:": "\U0001f965", + ":coffee:": "\u2615", + ":coffin:": "\u26b0", + ":cold_sweat:": "\U0001f630", + ":comet:": "\u2604", + ":compression:": "\U0001f5dc", + ":computer:": "\U0001f4bb", + ":confetti_ball:": "\U0001f38a", + ":confounded:": "\U0001f616", + ":confused:": "\U0001f615", + ":congratulations:": "\u3297", + ":construction:": "\U0001f6a7", + ":construction_site:": "\U0001f3d7", + ":construction_worker:": "\U0001f477", + ":construction_worker_tone1:": "\U0001f477\U0001f3fb", + ":construction_worker_tone2:": "\U0001f477\U0001f3fc", + ":construction_worker_tone3:": "\U0001f477\U0001f3fd", + ":construction_worker_tone4:": "\U0001f477\U0001f3fe", + ":construction_worker_tone5:": "\U0001f477\U0001f3ff", + ":control_knobs:": "\U0001f39b", + ":convenience_store:": "\U0001f3ea", + ":cookie:": "\U0001f36a", + ":cooking:": "\U0001f373", + ":cool:": "\U0001f192", + ":copyright:": "\u00a9", + ":corn:": "\U0001f33d", + ":couch:": "\U0001f6cb", + ":couple:": "\U0001f46b", + ":couple_mm:": "\U0001f468\u200d\u2764\ufe0f\u200d\U0001f468", + ":couple_with_heart:": "\U0001f491", + ":couple_with_heart_woman_man:": "\U0001f469\u200d\u2764\ufe0f\u200d\U0001f468", + ":couple_ww:": "\U0001f469\u200d\u2764\ufe0f\u200d\U0001f469", + ":couplekiss:": "\U0001f48f", + ":cow:": "\U0001f42e", + ":cow2:": "\U0001f404", + ":cowboy:": "\U0001f920", + ":crab:": "\U0001f980", + ":crayon:": "\U0001f58d", + ":crazy_face:": "\U0001f92a", + ":credit_card:": "\U0001f4b3", + ":crescent_moon:": "\U0001f319", + ":cricket:": "\U0001f997", + ":cricket_game:": "\U0001f3cf", + ":crocodile:": "\U0001f40a", + ":croissant:": "\U0001f950", + ":cross:": "\u271d", + ":crossed_flags:": "\U0001f38c", + ":crossed_swords:": "\u2694", + ":crown:": "\U0001f451", + ":cruise_ship:": "\U0001f6f3", + ":cry:": "\U0001f622", + ":crying_cat_face:": "\U0001f63f", + ":crystal_ball:": "\U0001f52e", + ":cucumber:": "\U0001f952", + ":cup_with_straw:": "\U0001f964", + ":cupid:": "\U0001f498", + ":curling_stone:": "\U0001f94c", + ":curly_loop:": "\u27b0", + ":currency_exchange:": "\U0001f4b1", + ":curry:": "\U0001f35b", + ":custard:": "\U0001f36e", + ":customs:": "\U0001f6c3", + ":cut_of_meat:": "\U0001f969", + ":cyclone:": "\U0001f300", + ":dagger:": "\U0001f5e1", + ":dancer:": "\U0001f483", + ":dancer_tone1:": "\U0001f483\U0001f3fb", + ":dancer_tone2:": "\U0001f483\U0001f3fc", + ":dancer_tone3:": "\U0001f483\U0001f3fd", + ":dancer_tone4:": "\U0001f483\U0001f3fe", + ":dancer_tone5:": "\U0001f483\U0001f3ff", + ":dango:": "\U0001f361", + ":dark_sunglasses:": "\U0001f576", + ":dart:": "\U0001f3af", + ":dash:": "\U0001f4a8", + ":date:": "\U0001f4c5", + ":deciduous_tree:": "\U0001f333", + ":deer:": "\U0001f98c", + ":department_store:": "\U0001f3ec", + ":desert:": "\U0001f3dc", + ":desktop:": "\U0001f5a5", + ":detective:": "\U0001f575", + ":detective_tone1:": "\U0001f575\U0001f3fb", + ":detective_tone2:": "\U0001f575\U0001f3fc", + ":detective_tone3:": "\U0001f575\U0001f3fd", + ":detective_tone4:": "\U0001f575\U0001f3fe", + ":detective_tone5:": "\U0001f575\U0001f3ff", + ":diamond_shape_with_a_dot_inside:": "\U0001f4a0", + ":diamonds:": "\u2666", + ":disappointed:": "\U0001f61e", + ":disappointed_relieved:": "\U0001f625", + ":dividers:": "\U0001f5c2", + ":dizzy:": "\U0001f4ab", + ":dizzy_face:": "\U0001f635", + ":do_not_litter:": "\U0001f6af", + ":dog:": "\U0001f436", + ":dog2:": "\U0001f415", + ":dollar:": "\U0001f4b5", + ":dolls:": "\U0001f38e", + ":dolphin:": "\U0001f42c", + ":door:": "\U0001f6aa", + ":doughnut:": "\U0001f369", + ":dove:": "\U0001f54a", + ":dragon:": "\U0001f409", + ":dragon_face:": "\U0001f432", + ":dress:": "\U0001f457", + ":dromedary_camel:": "\U0001f42a", + ":drooling_face:": "\U0001f924", + ":droplet:": "\U0001f4a7", + ":drum:": "\U0001f941", + ":duck:": "\U0001f986", + ":dumpling:": "\U0001f95f", + ":dvd:": "\U0001f4c0", + ":e-mail:": "\U0001f4e7", + ":eagle:": "\U0001f985", + ":ear:": "\U0001f442", + ":ear_of_rice:": "\U0001f33e", + ":ear_tone1:": "\U0001f442\U0001f3fb", + ":ear_tone2:": "\U0001f442\U0001f3fc", + ":ear_tone3:": "\U0001f442\U0001f3fd", + ":ear_tone4:": "\U0001f442\U0001f3fe", + ":ear_tone5:": "\U0001f442\U0001f3ff", + ":earth_africa:": "\U0001f30d", + ":earth_americas:": "\U0001f30e", + ":earth_asia:": "\U0001f30f", + ":egg:": "\U0001f95a", + ":eggplant:": "\U0001f346", + ":eight:": "8\ufe0f\u20e3", + ":eight_pointed_black_star:": "\u2734", + ":eight_spoked_asterisk:": "\u2733", + ":eject:": "\u23cf", + ":electric_plug:": "\U0001f50c", + ":elephant:": "\U0001f418", + ":elf:": "\U0001f9dd", + ":elf_tone1:": "\U0001f9dd\U0001f3fb", + ":elf_tone2:": "\U0001f9dd\U0001f3fc", + ":elf_tone3:": "\U0001f9dd\U0001f3fd", + ":elf_tone4:": "\U0001f9dd\U0001f3fe", + ":elf_tone5:": "\U0001f9dd\U0001f3ff", + ":end:": "\U0001f51a", + ":england:": "\U0001f3f4\U000e0067\U000e0062\U000e0065\U000e006e\U000e0067\U000e007f", + ":envelope:": "\u2709", + ":envelope_with_arrow:": "\U0001f4e9", + ":euro:": "\U0001f4b6", + ":european_castle:": "\U0001f3f0", + ":european_post_office:": "\U0001f3e4", + ":evergreen_tree:": "\U0001f332", + ":exclamation:": "\u2757", + ":exploding_head:": "\U0001f92f", + ":expressionless:": "\U0001f611", + ":eye:": "\U0001f441", + ":eye_in_speech_bubble:": "\U0001f441\ufe0f\u200d\U0001f5e8\ufe0f", + ":eyeglasses:": "\U0001f453", + ":eyes:": "\U0001f440", + ":face_vomiting:": "\U0001f92e", + ":face_with_hand_over_mouth:": "\U0001f92d", + ":face_with_monocle:": "\U0001f9d0", + ":face_with_raised_eyebrow:": "\U0001f928", + ":face_with_symbols_over_mouth:": "\U0001f92c", + ":factory:": "\U0001f3ed", + ":fairy:": "\U0001f9da", + ":fairy_tone1:": "\U0001f9da\U0001f3fb", + ":fairy_tone2:": "\U0001f9da\U0001f3fc", + ":fairy_tone3:": "\U0001f9da\U0001f3fd", + ":fairy_tone4:": "\U0001f9da\U0001f3fe", + ":fairy_tone5:": "\U0001f9da\U0001f3ff", + ":fallen_leaf:": "\U0001f342", + ":family:": "\U0001f46a", + ":family_man_boy:": "\U0001f468\u200d\U0001f466", + ":family_man_boy_boy:": "\U0001f468\u200d\U0001f466\u200d\U0001f466", + ":family_man_girl:": "\U0001f468\u200d\U0001f467", + ":family_man_girl_boy:": "\U0001f468\u200d\U0001f467\u200d\U0001f466", + ":family_man_girl_girl:": "\U0001f468\u200d\U0001f467\u200d\U0001f467", + ":family_man_woman_boy:": "\U0001f468\u200d\U0001f469\u200d\U0001f466", + ":family_mmb:": "\U0001f468\u200d\U0001f468\u200d\U0001f466", + ":family_mmbb:": "\U0001f468\u200d\U0001f468\u200d\U0001f466\u200d\U0001f466", + ":family_mmg:": "\U0001f468\u200d\U0001f468\u200d\U0001f467", + ":family_mmgb:": "\U0001f468\u200d\U0001f468\u200d\U0001f467\u200d\U0001f466", + ":family_mmgg:": "\U0001f468\u200d\U0001f468\u200d\U0001f467\u200d\U0001f467", + ":family_mwbb:": "\U0001f468\u200d\U0001f469\u200d\U0001f466\u200d\U0001f466", + ":family_mwg:": "\U0001f468\u200d\U0001f469\u200d\U0001f467", + ":family_mwgb:": "\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466", + ":family_mwgg:": "\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f467", + ":family_woman_boy:": "\U0001f469\u200d\U0001f466", + ":family_woman_boy_boy:": "\U0001f469\u200d\U0001f466\u200d\U0001f466", + ":family_woman_girl:": "\U0001f469\u200d\U0001f467", + ":family_woman_girl_boy:": "\U0001f469\u200d\U0001f467\u200d\U0001f466", + ":family_woman_girl_girl:": "\U0001f469\u200d\U0001f467\u200d\U0001f467", + ":family_wwb:": "\U0001f469\u200d\U0001f469\u200d\U0001f466", + ":family_wwbb:": "\U0001f469\u200d\U0001f469\u200d\U0001f466\u200d\U0001f466", + ":family_wwg:": "\U0001f469\u200d\U0001f469\u200d\U0001f467", + ":family_wwgb:": "\U0001f469\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466", + ":family_wwgg:": "\U0001f469\u200d\U0001f469\u200d\U0001f467\u200d\U0001f467", + ":fast_forward:": "\u23e9", + ":fax:": "\U0001f4e0", + ":fearful:": "\U0001f628", + ":feet:": "\U0001f43e", + ":female_sign:": "\u2640", + ":ferris_wheel:": "\U0001f3a1", + ":ferry:": "\u26f4", + ":field_hockey:": "\U0001f3d1", + ":file_cabinet:": "\U0001f5c4", + ":file_folder:": "\U0001f4c1", + ":film_frames:": "\U0001f39e", + ":fingers_crossed:": "\U0001f91e", + ":fingers_crossed_tone1:": "\U0001f91e\U0001f3fb", + ":fingers_crossed_tone2:": "\U0001f91e\U0001f3fc", + ":fingers_crossed_tone3:": "\U0001f91e\U0001f3fd", + ":fingers_crossed_tone4:": "\U0001f91e\U0001f3fe", + ":fingers_crossed_tone5:": "\U0001f91e\U0001f3ff", + ":fire:": "\U0001f525", + ":fire_engine:": "\U0001f692", + ":fireworks:": "\U0001f386", + ":first_place:": "\U0001f947", + ":first_quarter_moon:": "\U0001f313", + ":first_quarter_moon_with_face:": "\U0001f31b", + ":fish:": "\U0001f41f", + ":fish_cake:": "\U0001f365", + ":fishing_pole_and_fish:": "\U0001f3a3", + ":fist:": "\u270a", + ":fist_tone1:": "\u270a\U0001f3fb", + ":fist_tone2:": "\u270a\U0001f3fc", + ":fist_tone3:": "\u270a\U0001f3fd", + ":fist_tone4:": "\u270a\U0001f3fe", + ":fist_tone5:": "\u270a\U0001f3ff", + ":five:": "5\ufe0f\u20e3", + ":flag_ac:": "\U0001f1e6\U0001f1e8", + ":flag_ad:": "\U0001f1e6\U0001f1e9", + ":flag_ae:": "\U0001f1e6\U0001f1ea", + ":flag_af:": "\U0001f1e6\U0001f1eb", + ":flag_ag:": "\U0001f1e6\U0001f1ec", + ":flag_ai:": "\U0001f1e6\U0001f1ee", + ":flag_al:": "\U0001f1e6\U0001f1f1", + ":flag_am:": "\U0001f1e6\U0001f1f2", + ":flag_ao:": "\U0001f1e6\U0001f1f4", + ":flag_aq:": "\U0001f1e6\U0001f1f6", + ":flag_ar:": "\U0001f1e6\U0001f1f7", + ":flag_as:": "\U0001f1e6\U0001f1f8", + ":flag_at:": "\U0001f1e6\U0001f1f9", + ":flag_au:": "\U0001f1e6\U0001f1fa", + ":flag_aw:": "\U0001f1e6\U0001f1fc", + ":flag_ax:": "\U0001f1e6\U0001f1fd", + ":flag_az:": "\U0001f1e6\U0001f1ff", + ":flag_ba:": "\U0001f1e7\U0001f1e6", + ":flag_bb:": "\U0001f1e7\U0001f1e7", + ":flag_bd:": "\U0001f1e7\U0001f1e9", + ":flag_be:": "\U0001f1e7\U0001f1ea", + ":flag_bf:": "\U0001f1e7\U0001f1eb", + ":flag_bg:": "\U0001f1e7\U0001f1ec", + ":flag_bh:": "\U0001f1e7\U0001f1ed", + ":flag_bi:": "\U0001f1e7\U0001f1ee", + ":flag_bj:": "\U0001f1e7\U0001f1ef", + ":flag_bl:": "\U0001f1e7\U0001f1f1", + ":flag_black:": "\U0001f3f4", + ":flag_bm:": "\U0001f1e7\U0001f1f2", + ":flag_bn:": "\U0001f1e7\U0001f1f3", + ":flag_bo:": "\U0001f1e7\U0001f1f4", + ":flag_bq:": "\U0001f1e7\U0001f1f6", + ":flag_br:": "\U0001f1e7\U0001f1f7", + ":flag_bs:": "\U0001f1e7\U0001f1f8", + ":flag_bt:": "\U0001f1e7\U0001f1f9", + ":flag_bv:": "\U0001f1e7\U0001f1fb", + ":flag_bw:": "\U0001f1e7\U0001f1fc", + ":flag_by:": "\U0001f1e7\U0001f1fe", + ":flag_bz:": "\U0001f1e7\U0001f1ff", + ":flag_ca:": "\U0001f1e8\U0001f1e6", + ":flag_cc:": "\U0001f1e8\U0001f1e8", + ":flag_cd:": "\U0001f1e8\U0001f1e9", + ":flag_cf:": "\U0001f1e8\U0001f1eb", + ":flag_cg:": "\U0001f1e8\U0001f1ec", + ":flag_ch:": "\U0001f1e8\U0001f1ed", + ":flag_ci:": "\U0001f1e8\U0001f1ee", + ":flag_ck:": "\U0001f1e8\U0001f1f0", + ":flag_cl:": "\U0001f1e8\U0001f1f1", + ":flag_cm:": "\U0001f1e8\U0001f1f2", + ":flag_cn:": "\U0001f1e8\U0001f1f3", + ":flag_co:": "\U0001f1e8\U0001f1f4", + ":flag_cp:": "\U0001f1e8\U0001f1f5", + ":flag_cr:": "\U0001f1e8\U0001f1f7", + ":flag_cu:": "\U0001f1e8\U0001f1fa", + ":flag_cv:": "\U0001f1e8\U0001f1fb", + ":flag_cw:": "\U0001f1e8\U0001f1fc", + ":flag_cx:": "\U0001f1e8\U0001f1fd", + ":flag_cy:": "\U0001f1e8\U0001f1fe", + ":flag_cz:": "\U0001f1e8\U0001f1ff", + ":flag_de:": "\U0001f1e9\U0001f1ea", + ":flag_dg:": "\U0001f1e9\U0001f1ec", + ":flag_dj:": "\U0001f1e9\U0001f1ef", + ":flag_dk:": "\U0001f1e9\U0001f1f0", + ":flag_dm:": "\U0001f1e9\U0001f1f2", + ":flag_do:": "\U0001f1e9\U0001f1f4", + ":flag_dz:": "\U0001f1e9\U0001f1ff", + ":flag_ea:": "\U0001f1ea\U0001f1e6", + ":flag_ec:": "\U0001f1ea\U0001f1e8", + ":flag_ee:": "\U0001f1ea\U0001f1ea", + ":flag_eg:": "\U0001f1ea\U0001f1ec", + ":flag_eh:": "\U0001f1ea\U0001f1ed", + ":flag_er:": "\U0001f1ea\U0001f1f7", + ":flag_es:": "\U0001f1ea\U0001f1f8", + ":flag_et:": "\U0001f1ea\U0001f1f9", + ":flag_eu:": "\U0001f1ea\U0001f1fa", + ":flag_fi:": "\U0001f1eb\U0001f1ee", + ":flag_fj:": "\U0001f1eb\U0001f1ef", + ":flag_fk:": "\U0001f1eb\U0001f1f0", + ":flag_fm:": "\U0001f1eb\U0001f1f2", + ":flag_fo:": "\U0001f1eb\U0001f1f4", + ":flag_fr:": "\U0001f1eb\U0001f1f7", + ":flag_ga:": "\U0001f1ec\U0001f1e6", + ":flag_gb:": "\U0001f1ec\U0001f1e7", + ":flag_gd:": "\U0001f1ec\U0001f1e9", + ":flag_ge:": "\U0001f1ec\U0001f1ea", + ":flag_gf:": "\U0001f1ec\U0001f1eb", + ":flag_gg:": "\U0001f1ec\U0001f1ec", + ":flag_gh:": "\U0001f1ec\U0001f1ed", + ":flag_gi:": "\U0001f1ec\U0001f1ee", + ":flag_gl:": "\U0001f1ec\U0001f1f1", + ":flag_gm:": "\U0001f1ec\U0001f1f2", + ":flag_gn:": "\U0001f1ec\U0001f1f3", + ":flag_gp:": "\U0001f1ec\U0001f1f5", + ":flag_gq:": "\U0001f1ec\U0001f1f6", + ":flag_gr:": "\U0001f1ec\U0001f1f7", + ":flag_gs:": "\U0001f1ec\U0001f1f8", + ":flag_gt:": "\U0001f1ec\U0001f1f9", + ":flag_gu:": "\U0001f1ec\U0001f1fa", + ":flag_gw:": "\U0001f1ec\U0001f1fc", + ":flag_gy:": "\U0001f1ec\U0001f1fe", + ":flag_hk:": "\U0001f1ed\U0001f1f0", + ":flag_hm:": "\U0001f1ed\U0001f1f2", + ":flag_hn:": "\U0001f1ed\U0001f1f3", + ":flag_hr:": "\U0001f1ed\U0001f1f7", + ":flag_ht:": "\U0001f1ed\U0001f1f9", + ":flag_hu:": "\U0001f1ed\U0001f1fa", + ":flag_ic:": "\U0001f1ee\U0001f1e8", + ":flag_id:": "\U0001f1ee\U0001f1e9", + ":flag_ie:": "\U0001f1ee\U0001f1ea", + ":flag_il:": "\U0001f1ee\U0001f1f1", + ":flag_im:": "\U0001f1ee\U0001f1f2", + ":flag_in:": "\U0001f1ee\U0001f1f3", + ":flag_io:": "\U0001f1ee\U0001f1f4", + ":flag_iq:": "\U0001f1ee\U0001f1f6", + ":flag_ir:": "\U0001f1ee\U0001f1f7", + ":flag_is:": "\U0001f1ee\U0001f1f8", + ":flag_it:": "\U0001f1ee\U0001f1f9", + ":flag_je:": "\U0001f1ef\U0001f1ea", + ":flag_jm:": "\U0001f1ef\U0001f1f2", + ":flag_jo:": "\U0001f1ef\U0001f1f4", + ":flag_jp:": "\U0001f1ef\U0001f1f5", + ":flag_ke:": "\U0001f1f0\U0001f1ea", + ":flag_kg:": "\U0001f1f0\U0001f1ec", + ":flag_kh:": "\U0001f1f0\U0001f1ed", + ":flag_ki:": "\U0001f1f0\U0001f1ee", + ":flag_km:": "\U0001f1f0\U0001f1f2", + ":flag_kn:": "\U0001f1f0\U0001f1f3", + ":flag_kp:": "\U0001f1f0\U0001f1f5", + ":flag_kr:": "\U0001f1f0\U0001f1f7", + ":flag_kw:": "\U0001f1f0\U0001f1fc", + ":flag_ky:": "\U0001f1f0\U0001f1fe", + ":flag_kz:": "\U0001f1f0\U0001f1ff", + ":flag_la:": "\U0001f1f1\U0001f1e6", + ":flag_lb:": "\U0001f1f1\U0001f1e7", + ":flag_lc:": "\U0001f1f1\U0001f1e8", + ":flag_li:": "\U0001f1f1\U0001f1ee", + ":flag_lk:": "\U0001f1f1\U0001f1f0", + ":flag_lr:": "\U0001f1f1\U0001f1f7", + ":flag_ls:": "\U0001f1f1\U0001f1f8", + ":flag_lt:": "\U0001f1f1\U0001f1f9", + ":flag_lu:": "\U0001f1f1\U0001f1fa", + ":flag_lv:": "\U0001f1f1\U0001f1fb", + ":flag_ly:": "\U0001f1f1\U0001f1fe", + ":flag_ma:": "\U0001f1f2\U0001f1e6", + ":flag_mc:": "\U0001f1f2\U0001f1e8", + ":flag_md:": "\U0001f1f2\U0001f1e9", + ":flag_me:": "\U0001f1f2\U0001f1ea", + ":flag_mf:": "\U0001f1f2\U0001f1eb", + ":flag_mg:": "\U0001f1f2\U0001f1ec", + ":flag_mh:": "\U0001f1f2\U0001f1ed", + ":flag_mk:": "\U0001f1f2\U0001f1f0", + ":flag_ml:": "\U0001f1f2\U0001f1f1", + ":flag_mm:": "\U0001f1f2\U0001f1f2", + ":flag_mn:": "\U0001f1f2\U0001f1f3", + ":flag_mo:": "\U0001f1f2\U0001f1f4", + ":flag_mp:": "\U0001f1f2\U0001f1f5", + ":flag_mq:": "\U0001f1f2\U0001f1f6", + ":flag_mr:": "\U0001f1f2\U0001f1f7", + ":flag_ms:": "\U0001f1f2\U0001f1f8", + ":flag_mt:": "\U0001f1f2\U0001f1f9", + ":flag_mu:": "\U0001f1f2\U0001f1fa", + ":flag_mv:": "\U0001f1f2\U0001f1fb", + ":flag_mw:": "\U0001f1f2\U0001f1fc", + ":flag_mx:": "\U0001f1f2\U0001f1fd", + ":flag_my:": "\U0001f1f2\U0001f1fe", + ":flag_mz:": "\U0001f1f2\U0001f1ff", + ":flag_na:": "\U0001f1f3\U0001f1e6", + ":flag_nc:": "\U0001f1f3\U0001f1e8", + ":flag_ne:": "\U0001f1f3\U0001f1ea", + ":flag_nf:": "\U0001f1f3\U0001f1eb", + ":flag_ng:": "\U0001f1f3\U0001f1ec", + ":flag_ni:": "\U0001f1f3\U0001f1ee", + ":flag_nl:": "\U0001f1f3\U0001f1f1", + ":flag_no:": "\U0001f1f3\U0001f1f4", + ":flag_np:": "\U0001f1f3\U0001f1f5", + ":flag_nr:": "\U0001f1f3\U0001f1f7", + ":flag_nu:": "\U0001f1f3\U0001f1fa", + ":flag_nz:": "\U0001f1f3\U0001f1ff", + ":flag_om:": "\U0001f1f4\U0001f1f2", + ":flag_pa:": "\U0001f1f5\U0001f1e6", + ":flag_pe:": "\U0001f1f5\U0001f1ea", + ":flag_pf:": "\U0001f1f5\U0001f1eb", + ":flag_pg:": "\U0001f1f5\U0001f1ec", + ":flag_ph:": "\U0001f1f5\U0001f1ed", + ":flag_pk:": "\U0001f1f5\U0001f1f0", + ":flag_pl:": "\U0001f1f5\U0001f1f1", + ":flag_pm:": "\U0001f1f5\U0001f1f2", + ":flag_pn:": "\U0001f1f5\U0001f1f3", + ":flag_pr:": "\U0001f1f5\U0001f1f7", + ":flag_ps:": "\U0001f1f5\U0001f1f8", + ":flag_pt:": "\U0001f1f5\U0001f1f9", + ":flag_pw:": "\U0001f1f5\U0001f1fc", + ":flag_py:": "\U0001f1f5\U0001f1fe", + ":flag_qa:": "\U0001f1f6\U0001f1e6", + ":flag_re:": "\U0001f1f7\U0001f1ea", + ":flag_ro:": "\U0001f1f7\U0001f1f4", + ":flag_rs:": "\U0001f1f7\U0001f1f8", + ":flag_ru:": "\U0001f1f7\U0001f1fa", + ":flag_rw:": "\U0001f1f7\U0001f1fc", + ":flag_sa:": "\U0001f1f8\U0001f1e6", + ":flag_sb:": "\U0001f1f8\U0001f1e7", + ":flag_sc:": "\U0001f1f8\U0001f1e8", + ":flag_sd:": "\U0001f1f8\U0001f1e9", + ":flag_se:": "\U0001f1f8\U0001f1ea", + ":flag_sg:": "\U0001f1f8\U0001f1ec", + ":flag_sh:": "\U0001f1f8\U0001f1ed", + ":flag_si:": "\U0001f1f8\U0001f1ee", + ":flag_sj:": "\U0001f1f8\U0001f1ef", + ":flag_sk:": "\U0001f1f8\U0001f1f0", + ":flag_sl:": "\U0001f1f8\U0001f1f1", + ":flag_sm:": "\U0001f1f8\U0001f1f2", + ":flag_sn:": "\U0001f1f8\U0001f1f3", + ":flag_so:": "\U0001f1f8\U0001f1f4", + ":flag_sr:": "\U0001f1f8\U0001f1f7", + ":flag_ss:": "\U0001f1f8\U0001f1f8", + ":flag_st:": "\U0001f1f8\U0001f1f9", + ":flag_sv:": "\U0001f1f8\U0001f1fb", + ":flag_sx:": "\U0001f1f8\U0001f1fd", + ":flag_sy:": "\U0001f1f8\U0001f1fe", + ":flag_sz:": "\U0001f1f8\U0001f1ff", + ":flag_ta:": "\U0001f1f9\U0001f1e6", + ":flag_tc:": "\U0001f1f9\U0001f1e8", + ":flag_td:": "\U0001f1f9\U0001f1e9", + ":flag_tf:": "\U0001f1f9\U0001f1eb", + ":flag_tg:": "\U0001f1f9\U0001f1ec", + ":flag_th:": "\U0001f1f9\U0001f1ed", + ":flag_tj:": "\U0001f1f9\U0001f1ef", + ":flag_tk:": "\U0001f1f9\U0001f1f0", + ":flag_tl:": "\U0001f1f9\U0001f1f1", + ":flag_tm:": "\U0001f1f9\U0001f1f2", + ":flag_tn:": "\U0001f1f9\U0001f1f3", + ":flag_to:": "\U0001f1f9\U0001f1f4", + ":flag_tr:": "\U0001f1f9\U0001f1f7", + ":flag_tt:": "\U0001f1f9\U0001f1f9", + ":flag_tv:": "\U0001f1f9\U0001f1fb", + ":flag_tw:": "\U0001f1f9\U0001f1fc", + ":flag_tz:": "\U0001f1f9\U0001f1ff", + ":flag_ua:": "\U0001f1fa\U0001f1e6", + ":flag_ug:": "\U0001f1fa\U0001f1ec", + ":flag_um:": "\U0001f1fa\U0001f1f2", + ":flag_us:": "\U0001f1fa\U0001f1f8", + ":flag_uy:": "\U0001f1fa\U0001f1fe", + ":flag_uz:": "\U0001f1fa\U0001f1ff", + ":flag_va:": "\U0001f1fb\U0001f1e6", + ":flag_vc:": "\U0001f1fb\U0001f1e8", + ":flag_ve:": "\U0001f1fb\U0001f1ea", + ":flag_vg:": "\U0001f1fb\U0001f1ec", + ":flag_vi:": "\U0001f1fb\U0001f1ee", + ":flag_vn:": "\U0001f1fb\U0001f1f3", + ":flag_vu:": "\U0001f1fb\U0001f1fa", + ":flag_wf:": "\U0001f1fc\U0001f1eb", + ":flag_white:": "\U0001f3f3", + ":flag_ws:": "\U0001f1fc\U0001f1f8", + ":flag_xk:": "\U0001f1fd\U0001f1f0", + ":flag_ye:": "\U0001f1fe\U0001f1ea", + ":flag_yt:": "\U0001f1fe\U0001f1f9", + ":flag_za:": "\U0001f1ff\U0001f1e6", + ":flag_zm:": "\U0001f1ff\U0001f1f2", + ":flag_zw:": "\U0001f1ff\U0001f1fc", + ":flags:": "\U0001f38f", + ":flashlight:": "\U0001f526", + ":fleur-de-lis:": "\u269c", + ":floppy_disk:": "\U0001f4be", + ":flower_playing_cards:": "\U0001f3b4", + ":flushed:": "\U0001f633", + ":flying_saucer:": "\U0001f6f8", + ":fog:": "\U0001f32b", + ":foggy:": "\U0001f301", + ":football:": "\U0001f3c8", + ":footprints:": "\U0001f463", + ":fork_and_knife:": "\U0001f374", + ":fork_knife_plate:": "\U0001f37d", + ":fortune_cookie:": "\U0001f960", + ":fountain:": "\u26f2", + ":four:": "4\ufe0f\u20e3", + ":four_leaf_clover:": "\U0001f340", + ":fox:": "\U0001f98a", + ":frame_photo:": "\U0001f5bc", + ":free:": "\U0001f193", + ":french_bread:": "\U0001f956", + ":fried_shrimp:": "\U0001f364", + ":fries:": "\U0001f35f", + ":frog:": "\U0001f438", + ":frowning:": "\U0001f626", + ":frowning2:": "\u2639", + ":fuelpump:": "\u26fd", + ":full_moon:": "\U0001f315", + ":full_moon_with_face:": "\U0001f31d", + ":game_die:": "\U0001f3b2", + ":gear:": "\u2699", + ":gem:": "\U0001f48e", + ":gemini:": "\u264a", + ":genie:": "\U0001f9de", + ":ghost:": "\U0001f47b", + ":gift:": "\U0001f381", + ":gift_heart:": "\U0001f49d", + ":giraffe:": "\U0001f992", + ":girl:": "\U0001f467", + ":girl_tone1:": "\U0001f467\U0001f3fb", + ":girl_tone2:": "\U0001f467\U0001f3fc", + ":girl_tone3:": "\U0001f467\U0001f3fd", + ":girl_tone4:": "\U0001f467\U0001f3fe", + ":girl_tone5:": "\U0001f467\U0001f3ff", + ":globe_with_meridians:": "\U0001f310", + ":gloves:": "\U0001f9e4", + ":goal:": "\U0001f945", + ":goat:": "\U0001f410", + ":golf:": "\u26f3", + ":gorilla:": "\U0001f98d", + ":grapes:": "\U0001f347", + ":green_apple:": "\U0001f34f", + ":green_book:": "\U0001f4d7", + ":green_heart:": "\U0001f49a", + ":grey_exclamation:": "\u2755", + ":grey_question:": "\u2754", + ":grimacing:": "\U0001f62c", + ":grin:": "\U0001f604", + ":grinning:": "\U0001f600", + ":guard:": "\U0001f482", + ":guard_tone1:": "\U0001f482\U0001f3fb", + ":guard_tone2:": "\U0001f482\U0001f3fc", + ":guard_tone3:": "\U0001f482\U0001f3fd", + ":guard_tone4:": "\U0001f482\U0001f3fe", + ":guard_tone5:": "\U0001f482\U0001f3ff", + ":guitar:": "\U0001f3b8", + ":gun:": "\U0001f52b", + ":hamburger:": "\U0001f354", + ":hammer:": "\U0001f528", + ":hammer_pick:": "\u2692", + ":hamster:": "\U0001f439", + ":hand_splayed_tone1:": "\U0001f590\U0001f3fb", + ":hand_splayed_tone2:": "\U0001f590\U0001f3fc", + ":hand_splayed_tone3:": "\U0001f590\U0001f3fd", + ":hand_splayed_tone4:": "\U0001f590\U0001f3fe", + ":hand_splayed_tone5:": "\U0001f590\U0001f3ff", + ":handbag:": "\U0001f45c", + ":handshake:": "\U0001f91d", + ":hash:": "#\ufe0f\u20e3", + ":hatched_chick:": "\U0001f425", + ":hatching_chick:": "\U0001f423", + ":head_bandage:": "\U0001f915", + ":headphones:": "\U0001f3a7", + ":hear_no_evil:": "\U0001f649", + ":heart:": "\u2764", + ":heart_decoration:": "\U0001f49f", + ":heart_exclamation:": "\u2763", + ":heart_eyes:": "\U0001f60d", + ":heart_eyes_cat:": "\U0001f63b", + ":heartbeat:": "\U0001f493", + ":heartpulse:": "\U0001f497", + ":hearts:": "\u2665", + ":heavy_check_mark:": "\u2714", + ":heavy_division_sign:": "\u2797", + ":heavy_dollar_sign:": "\U0001f4b2", + ":heavy_minus_sign:": "\u2796", + ":heavy_multiplication_x:": "\u2716", + ":heavy_plus_sign:": "\u2795", + ":hedgehog:": "\U0001f994", + ":helicopter:": "\U0001f681", + ":helmet_with_cross:": "\u26d1", + ":herb:": "\U0001f33f", + ":hibiscus:": "\U0001f33a", + ":high_brightness:": "\U0001f506", + ":high_heel:": "\U0001f460", + ":hockey:": "\U0001f3d2", + ":hole:": "\U0001f573", + ":homes:": "\U0001f3d8", + ":honey_pot:": "\U0001f36f", + ":horse:": "\U0001f434", + ":horse_racing:": "\U0001f3c7", + ":horse_racing_tone1:": "\U0001f3c7\U0001f3fb", + ":horse_racing_tone2:": "\U0001f3c7\U0001f3fc", + ":horse_racing_tone3:": "\U0001f3c7\U0001f3fd", + ":horse_racing_tone4:": "\U0001f3c7\U0001f3fe", + ":horse_racing_tone5:": "\U0001f3c7\U0001f3ff", + ":hospital:": "\U0001f3e5", + ":hot_pepper:": "\U0001f336", + ":hotdog:": "\U0001f32d", + ":hotel:": "\U0001f3e8", + ":hotsprings:": "\u2668", + ":hourglass:": "\u231b", + ":hourglass_flowing_sand:": "\u23f3", + ":house:": "\U0001f3e0", + ":house_abandoned:": "\U0001f3da", + ":house_with_garden:": "\U0001f3e1", + ":hugging:": "\U0001f917", + ":hushed:": "\U0001f62f", + ":ice_cream:": "\U0001f368", + ":ice_skate:": "\u26f8", + ":icecream:": "\U0001f366", + ":id:": "\U0001f194", + ":ideograph_advantage:": "\U0001f250", + ":imp:": "\U0001f47f", + ":inbox_tray:": "\U0001f4e5", + ":incoming_envelope:": "\U0001f4e8", + ":information_source:": "\u2139", + ":innocent:": "\U0001f607", + ":interrobang:": "\u2049", + ":iphone:": "\U0001f4f1", + ":island:": "\U0001f3dd", + ":izakaya_lantern:": "\U0001f3ee", + ":jack_o_lantern:": "\U0001f383", + ":japan:": "\U0001f5fe", + ":japanese_castle:": "\U0001f3ef", + ":japanese_goblin:": "\U0001f47a", + ":japanese_ogre:": "\U0001f479", + ":jeans:": "\U0001f456", + ":joy:": "\U0001f602", + ":joy_cat:": "\U0001f639", + ":joystick:": "\U0001f579", + ":kaaba:": "\U0001f54b", + ":key:": "\U0001f511", + ":key2:": "\U0001f5dd", + ":keyboard:": "\u2328", + ":keycap_ten:": "\U0001f51f", + ":kimono:": "\U0001f458", + ":kiss:": "\U0001f48b", + ":kiss_mm:": "\U0001f468\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", + ":kiss_woman_man:": "\U0001f469\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f468", + ":kiss_ww:": "\U0001f469\u200d\u2764\ufe0f\u200d\U0001f48b\u200d\U0001f469", + ":kissing:": "\U0001f617", + ":kissing_cat:": "\U0001f63d", + ":kissing_closed_eyes:": "\U0001f61a", + ":kissing_heart:": "\U0001f618", + ":kissing_smiling_eyes:": "\U0001f619", + ":kiwi:": "\U0001f95d", + ":knife:": "\U0001f52a", + ":koala:": "\U0001f428", + ":koko:": "\U0001f201", + ":label:": "\U0001f3f7", + ":large_blue_diamond:": "\U0001f537", + ":large_orange_diamond:": "\U0001f536", + ":last_quarter_moon:": "\U0001f317", + ":last_quarter_moon_with_face:": "\U0001f31c", + ":laughing:": "\U0001f606", + ":leaves:": "\U0001f343", + ":ledger:": "\U0001f4d2", + ":left_facing_fist:": "\U0001f91b", + ":left_facing_fist_tone1:": "\U0001f91b\U0001f3fb", + ":left_facing_fist_tone2:": "\U0001f91b\U0001f3fc", + ":left_facing_fist_tone3:": "\U0001f91b\U0001f3fd", + ":left_facing_fist_tone4:": "\U0001f91b\U0001f3fe", + ":left_facing_fist_tone5:": "\U0001f91b\U0001f3ff", + ":left_luggage:": "\U0001f6c5", + ":left_right_arrow:": "\u2194", + ":leftwards_arrow_with_hook:": "\u21a9", + ":lemon:": "\U0001f34b", + ":leo:": "\u264c", + ":leopard:": "\U0001f406", + ":level_slider:": "\U0001f39a", + ":libra:": "\u264e", + ":light_rail:": "\U0001f688", + ":link:": "\U0001f517", + ":lion_face:": "\U0001f981", + ":lips:": "\U0001f444", + ":lipstick:": "\U0001f484", + ":lizard:": "\U0001f98e", + ":lock:": "\U0001f512", + ":lock_with_ink_pen:": "\U0001f50f", + ":lollipop:": "\U0001f36d", + ":loop:": "\u27bf", + ":loud_sound:": "\U0001f50a", + ":loudspeaker:": "\U0001f4e2", + ":love_hotel:": "\U0001f3e9", + ":love_letter:": "\U0001f48c", + ":love_you_gesture:": "\U0001f91f", + ":love_you_gesture_tone1:": "\U0001f91f\U0001f3fb", + ":love_you_gesture_tone2:": "\U0001f91f\U0001f3fc", + ":love_you_gesture_tone3:": "\U0001f91f\U0001f3fd", + ":love_you_gesture_tone4:": "\U0001f91f\U0001f3fe", + ":love_you_gesture_tone5:": "\U0001f91f\U0001f3ff", + ":low_brightness:": "\U0001f505", + ":lying_face:": "\U0001f925", + ":m:": "\u24c2", + ":mag:": "\U0001f50d", + ":mag_right:": "\U0001f50e", + ":mage:": "\U0001f9d9", + ":mage_tone1:": "\U0001f9d9\U0001f3fb", + ":mage_tone2:": "\U0001f9d9\U0001f3fc", + ":mage_tone3:": "\U0001f9d9\U0001f3fd", + ":mage_tone4:": "\U0001f9d9\U0001f3fe", + ":mage_tone5:": "\U0001f9d9\U0001f3ff", + ":mahjong:": "\U0001f004", + ":mailbox:": "\U0001f4eb", + ":mailbox_closed:": "\U0001f4ea", + ":mailbox_with_mail:": "\U0001f4ec", + ":mailbox_with_no_mail:": "\U0001f4ed", + ":male_sign:": "\u2642", + ":man:": "\U0001f468", + ":man_artist:": "\U0001f468\u200d\U0001f3a8", + ":man_artist_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f3a8", + ":man_artist_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f3a8", + ":man_artist_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f3a8", + ":man_artist_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f3a8", + ":man_artist_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f3a8", + ":man_astronaut:": "\U0001f468\u200d\U0001f680", + ":man_astronaut_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f680", + ":man_astronaut_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f680", + ":man_astronaut_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f680", + ":man_astronaut_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f680", + ":man_astronaut_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f680", + ":man_biking:": "\U0001f6b4\u200d\u2642\ufe0f", + ":man_biking_tone1:": "\U0001f6b4\U0001f3fb\u200d\u2642\ufe0f", + ":man_biking_tone2:": "\U0001f6b4\U0001f3fc\u200d\u2642\ufe0f", + ":man_biking_tone3:": "\U0001f6b4\U0001f3fd\u200d\u2642\ufe0f", + ":man_biking_tone4:": "\U0001f6b4\U0001f3fe\u200d\u2642\ufe0f", + ":man_biking_tone5:": "\U0001f6b4\U0001f3ff\u200d\u2642\ufe0f", + ":man_bouncing_ball:": "\u26f9\ufe0f\u200d\u2642\ufe0f", + ":man_bouncing_ball_tone1:": "\u26f9\U0001f3fb\u200d\u2642\ufe0f", + ":man_bouncing_ball_tone2:": "\u26f9\U0001f3fc\u200d\u2642\ufe0f", + ":man_bouncing_ball_tone3:": "\u26f9\U0001f3fd\u200d\u2642\ufe0f", + ":man_bouncing_ball_tone4:": "\u26f9\U0001f3fe\u200d\u2642\ufe0f", + ":man_bouncing_ball_tone5:": "\u26f9\U0001f3ff\u200d\u2642\ufe0f", + ":man_bowing:": "\U0001f647\u200d\u2642\ufe0f", + ":man_bowing_tone1:": "\U0001f647\U0001f3fb\u200d\u2642\ufe0f", + ":man_bowing_tone2:": "\U0001f647\U0001f3fc\u200d\u2642\ufe0f", + ":man_bowing_tone3:": "\U0001f647\U0001f3fd\u200d\u2642\ufe0f", + ":man_bowing_tone4:": "\U0001f647\U0001f3fe\u200d\u2642\ufe0f", + ":man_bowing_tone5:": "\U0001f647\U0001f3ff\u200d\u2642\ufe0f", + ":man_cartwheeling:": "\U0001f938\u200d\u2642\ufe0f", + ":man_cartwheeling_tone1:": "\U0001f938\U0001f3fb\u200d\u2642\ufe0f", + ":man_cartwheeling_tone2:": "\U0001f938\U0001f3fc\u200d\u2642\ufe0f", + ":man_cartwheeling_tone3:": "\U0001f938\U0001f3fd\u200d\u2642\ufe0f", + ":man_cartwheeling_tone4:": "\U0001f938\U0001f3fe\u200d\u2642\ufe0f", + ":man_cartwheeling_tone5:": "\U0001f938\U0001f3ff\u200d\u2642\ufe0f", + ":man_climbing:": "\U0001f9d7\u200d\u2642\ufe0f", + ":man_climbing_tone1:": "\U0001f9d7\U0001f3fb\u200d\u2642\ufe0f", + ":man_climbing_tone2:": "\U0001f9d7\U0001f3fc\u200d\u2642\ufe0f", + ":man_climbing_tone3:": "\U0001f9d7\U0001f3fd\u200d\u2642\ufe0f", + ":man_climbing_tone4:": "\U0001f9d7\U0001f3fe\u200d\u2642\ufe0f", + ":man_climbing_tone5:": "\U0001f9d7\U0001f3ff\u200d\u2642\ufe0f", + ":man_construction_worker:": "\U0001f477\u200d\u2642\ufe0f", + ":man_construction_worker_tone1:": "\U0001f477\U0001f3fb\u200d\u2642\ufe0f", + ":man_construction_worker_tone2:": "\U0001f477\U0001f3fc\u200d\u2642\ufe0f", + ":man_construction_worker_tone3:": "\U0001f477\U0001f3fd\u200d\u2642\ufe0f", + ":man_construction_worker_tone4:": "\U0001f477\U0001f3fe\u200d\u2642\ufe0f", + ":man_construction_worker_tone5:": "\U0001f477\U0001f3ff\u200d\u2642\ufe0f", + ":man_cook:": "\U0001f468\u200d\U0001f373", + ":man_cook_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f373", + ":man_cook_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f373", + ":man_cook_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f373", + ":man_cook_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f373", + ":man_cook_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f373", + ":man_dancing:": "\U0001f57a", + ":man_dancing_tone1:": "\U0001f57a\U0001f3fb", + ":man_dancing_tone2:": "\U0001f57a\U0001f3fc", + ":man_dancing_tone3:": "\U0001f57a\U0001f3fd", + ":man_dancing_tone4:": "\U0001f57a\U0001f3fe", + ":man_dancing_tone5:": "\U0001f57a\U0001f3ff", + ":man_detective:": "\U0001f575\ufe0f\u200d\u2642\ufe0f", + ":man_detective_tone1:": "\U0001f575\U0001f3fb\u200d\u2642\ufe0f", + ":man_detective_tone2:": "\U0001f575\U0001f3fc\u200d\u2642\ufe0f", + ":man_detective_tone3:": "\U0001f575\U0001f3fd\u200d\u2642\ufe0f", + ":man_detective_tone4:": "\U0001f575\U0001f3fe\u200d\u2642\ufe0f", + ":man_detective_tone5:": "\U0001f575\U0001f3ff\u200d\u2642\ufe0f", + ":man_elf:": "\U0001f9dd\u200d\u2642\ufe0f", + ":man_elf_tone1:": "\U0001f9dd\U0001f3fb\u200d\u2642\ufe0f", + ":man_elf_tone2:": "\U0001f9dd\U0001f3fc\u200d\u2642\ufe0f", + ":man_elf_tone3:": "\U0001f9dd\U0001f3fd\u200d\u2642\ufe0f", + ":man_elf_tone4:": "\U0001f9dd\U0001f3fe\u200d\u2642\ufe0f", + ":man_elf_tone5:": "\U0001f9dd\U0001f3ff\u200d\u2642\ufe0f", + ":man_facepalming:": "\U0001f926\u200d\u2642\ufe0f", + ":man_facepalming_tone1:": "\U0001f926\U0001f3fb\u200d\u2642\ufe0f", + ":man_facepalming_tone2:": "\U0001f926\U0001f3fc\u200d\u2642\ufe0f", + ":man_facepalming_tone3:": "\U0001f926\U0001f3fd\u200d\u2642\ufe0f", + ":man_facepalming_tone4:": "\U0001f926\U0001f3fe\u200d\u2642\ufe0f", + ":man_facepalming_tone5:": "\U0001f926\U0001f3ff\u200d\u2642\ufe0f", + ":man_factory_worker:": "\U0001f468\u200d\U0001f3ed", + ":man_factory_worker_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f3ed", + ":man_factory_worker_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f3ed", + ":man_factory_worker_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f3ed", + ":man_factory_worker_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f3ed", + ":man_factory_worker_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f3ed", + ":man_fairy:": "\U0001f9da\u200d\u2642\ufe0f", + ":man_fairy_tone1:": "\U0001f9da\U0001f3fb\u200d\u2642\ufe0f", + ":man_fairy_tone2:": "\U0001f9da\U0001f3fc\u200d\u2642\ufe0f", + ":man_fairy_tone3:": "\U0001f9da\U0001f3fd\u200d\u2642\ufe0f", + ":man_fairy_tone4:": "\U0001f9da\U0001f3fe\u200d\u2642\ufe0f", + ":man_fairy_tone5:": "\U0001f9da\U0001f3ff\u200d\u2642\ufe0f", + ":man_farmer:": "\U0001f468\u200d\U0001f33e", + ":man_farmer_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f33e", + ":man_farmer_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f33e", + ":man_farmer_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f33e", + ":man_farmer_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f33e", + ":man_farmer_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f33e", + ":man_firefighter:": "\U0001f468\u200d\U0001f692", + ":man_firefighter_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f692", + ":man_firefighter_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f692", + ":man_firefighter_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f692", + ":man_firefighter_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f692", + ":man_firefighter_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f692", + ":man_frowning:": "\U0001f64d\u200d\u2642\ufe0f", + ":man_frowning_tone1:": "\U0001f64d\U0001f3fb\u200d\u2642\ufe0f", + ":man_frowning_tone2:": "\U0001f64d\U0001f3fc\u200d\u2642\ufe0f", + ":man_frowning_tone3:": "\U0001f64d\U0001f3fd\u200d\u2642\ufe0f", + ":man_frowning_tone4:": "\U0001f64d\U0001f3fe\u200d\u2642\ufe0f", + ":man_frowning_tone5:": "\U0001f64d\U0001f3ff\u200d\u2642\ufe0f", + ":man_genie:": "\U0001f9de\u200d\u2642\ufe0f", + ":man_gesturing_no:": "\U0001f645\u200d\u2642\ufe0f", + ":man_gesturing_no_tone1:": "\U0001f645\U0001f3fb\u200d\u2642\ufe0f", + ":man_gesturing_no_tone2:": "\U0001f645\U0001f3fc\u200d\u2642\ufe0f", + ":man_gesturing_no_tone3:": "\U0001f645\U0001f3fd\u200d\u2642\ufe0f", + ":man_gesturing_no_tone4:": "\U0001f645\U0001f3fe\u200d\u2642\ufe0f", + ":man_gesturing_no_tone5:": "\U0001f645\U0001f3ff\u200d\u2642\ufe0f", + ":man_gesturing_ok:": "\U0001f646\u200d\u2642\ufe0f", + ":man_gesturing_ok_tone1:": "\U0001f646\U0001f3fb\u200d\u2642\ufe0f", + ":man_gesturing_ok_tone2:": "\U0001f646\U0001f3fc\u200d\u2642\ufe0f", + ":man_gesturing_ok_tone3:": "\U0001f646\U0001f3fd\u200d\u2642\ufe0f", + ":man_gesturing_ok_tone4:": "\U0001f646\U0001f3fe\u200d\u2642\ufe0f", + ":man_gesturing_ok_tone5:": "\U0001f646\U0001f3ff\u200d\u2642\ufe0f", + ":man_getting_face_massage:": "\U0001f486\u200d\u2642\ufe0f", + ":man_getting_face_massage_tone1:": "\U0001f486\U0001f3fb\u200d\u2642\ufe0f", + ":man_getting_face_massage_tone2:": "\U0001f486\U0001f3fc\u200d\u2642\ufe0f", + ":man_getting_face_massage_tone3:": "\U0001f486\U0001f3fd\u200d\u2642\ufe0f", + ":man_getting_face_massage_tone4:": "\U0001f486\U0001f3fe\u200d\u2642\ufe0f", + ":man_getting_face_massage_tone5:": "\U0001f486\U0001f3ff\u200d\u2642\ufe0f", + ":man_getting_haircut:": "\U0001f487\u200d\u2642\ufe0f", + ":man_getting_haircut_tone1:": "\U0001f487\U0001f3fb\u200d\u2642\ufe0f", + ":man_getting_haircut_tone2:": "\U0001f487\U0001f3fc\u200d\u2642\ufe0f", + ":man_getting_haircut_tone3:": "\U0001f487\U0001f3fd\u200d\u2642\ufe0f", + ":man_getting_haircut_tone4:": "\U0001f487\U0001f3fe\u200d\u2642\ufe0f", + ":man_getting_haircut_tone5:": "\U0001f487\U0001f3ff\u200d\u2642\ufe0f", + ":man_golfing:": "\U0001f3cc\ufe0f\u200d\u2642\ufe0f", + ":man_golfing_tone1:": "\U0001f3cc\U0001f3fb\u200d\u2642\ufe0f", + ":man_golfing_tone2:": "\U0001f3cc\U0001f3fc\u200d\u2642\ufe0f", + ":man_golfing_tone3:": "\U0001f3cc\U0001f3fd\u200d\u2642\ufe0f", + ":man_golfing_tone4:": "\U0001f3cc\U0001f3fe\u200d\u2642\ufe0f", + ":man_golfing_tone5:": "\U0001f3cc\U0001f3ff\u200d\u2642\ufe0f", + ":man_guard:": "\U0001f482\u200d\u2642\ufe0f", + ":man_guard_tone1:": "\U0001f482\U0001f3fb\u200d\u2642\ufe0f", + ":man_guard_tone2:": "\U0001f482\U0001f3fc\u200d\u2642\ufe0f", + ":man_guard_tone3:": "\U0001f482\U0001f3fd\u200d\u2642\ufe0f", + ":man_guard_tone4:": "\U0001f482\U0001f3fe\u200d\u2642\ufe0f", + ":man_guard_tone5:": "\U0001f482\U0001f3ff\u200d\u2642\ufe0f", + ":man_health_worker:": "\U0001f468\u200d\u2695\ufe0f", + ":man_health_worker_tone1:": "\U0001f468\U0001f3fb\u200d\u2695\ufe0f", + ":man_health_worker_tone2:": "\U0001f468\U0001f3fc\u200d\u2695\ufe0f", + ":man_health_worker_tone3:": "\U0001f468\U0001f3fd\u200d\u2695\ufe0f", + ":man_health_worker_tone4:": "\U0001f468\U0001f3fe\u200d\u2695\ufe0f", + ":man_health_worker_tone5:": "\U0001f468\U0001f3ff\u200d\u2695\ufe0f", + ":man_in_business_suit_levitating_tone1:": "\U0001f574\U0001f3fb", + ":man_in_business_suit_levitating_tone2:": "\U0001f574\U0001f3fc", + ":man_in_business_suit_levitating_tone3:": "\U0001f574\U0001f3fd", + ":man_in_business_suit_levitating_tone4:": "\U0001f574\U0001f3fe", + ":man_in_business_suit_levitating_tone5:": "\U0001f574\U0001f3ff", + ":man_in_lotus_position:": "\U0001f9d8\u200d\u2642\ufe0f", + ":man_in_lotus_position_tone1:": "\U0001f9d8\U0001f3fb\u200d\u2642\ufe0f", + ":man_in_lotus_position_tone2:": "\U0001f9d8\U0001f3fc\u200d\u2642\ufe0f", + ":man_in_lotus_position_tone3:": "\U0001f9d8\U0001f3fd\u200d\u2642\ufe0f", + ":man_in_lotus_position_tone4:": "\U0001f9d8\U0001f3fe\u200d\u2642\ufe0f", + ":man_in_lotus_position_tone5:": "\U0001f9d8\U0001f3ff\u200d\u2642\ufe0f", + ":man_in_steamy_room:": "\U0001f9d6\u200d\u2642\ufe0f", + ":man_in_steamy_room_tone1:": "\U0001f9d6\U0001f3fb\u200d\u2642\ufe0f", + ":man_in_steamy_room_tone2:": "\U0001f9d6\U0001f3fc\u200d\u2642\ufe0f", + ":man_in_steamy_room_tone3:": "\U0001f9d6\U0001f3fd\u200d\u2642\ufe0f", + ":man_in_steamy_room_tone4:": "\U0001f9d6\U0001f3fe\u200d\u2642\ufe0f", + ":man_in_steamy_room_tone5:": "\U0001f9d6\U0001f3ff\u200d\u2642\ufe0f", + ":man_in_tuxedo:": "\U0001f935", + ":man_in_tuxedo_tone1:": "\U0001f935\U0001f3fb", + ":man_in_tuxedo_tone2:": "\U0001f935\U0001f3fc", + ":man_in_tuxedo_tone3:": "\U0001f935\U0001f3fd", + ":man_in_tuxedo_tone4:": "\U0001f935\U0001f3fe", + ":man_in_tuxedo_tone5:": "\U0001f935\U0001f3ff", + ":man_judge:": "\U0001f468\u200d\u2696\ufe0f", + ":man_judge_tone1:": "\U0001f468\U0001f3fb\u200d\u2696\ufe0f", + ":man_judge_tone2:": "\U0001f468\U0001f3fc\u200d\u2696\ufe0f", + ":man_judge_tone3:": "\U0001f468\U0001f3fd\u200d\u2696\ufe0f", + ":man_judge_tone4:": "\U0001f468\U0001f3fe\u200d\u2696\ufe0f", + ":man_judge_tone5:": "\U0001f468\U0001f3ff\u200d\u2696\ufe0f", + ":man_juggling:": "\U0001f939\u200d\u2642\ufe0f", + ":man_juggling_tone1:": "\U0001f939\U0001f3fb\u200d\u2642\ufe0f", + ":man_juggling_tone2:": "\U0001f939\U0001f3fc\u200d\u2642\ufe0f", + ":man_juggling_tone3:": "\U0001f939\U0001f3fd\u200d\u2642\ufe0f", + ":man_juggling_tone4:": "\U0001f939\U0001f3fe\u200d\u2642\ufe0f", + ":man_juggling_tone5:": "\U0001f939\U0001f3ff\u200d\u2642\ufe0f", + ":man_lifting_weights:": "\U0001f3cb\ufe0f\u200d\u2642\ufe0f", + ":man_lifting_weights_tone1:": "\U0001f3cb\U0001f3fb\u200d\u2642\ufe0f", + ":man_lifting_weights_tone2:": "\U0001f3cb\U0001f3fc\u200d\u2642\ufe0f", + ":man_lifting_weights_tone3:": "\U0001f3cb\U0001f3fd\u200d\u2642\ufe0f", + ":man_lifting_weights_tone4:": "\U0001f3cb\U0001f3fe\u200d\u2642\ufe0f", + ":man_lifting_weights_tone5:": "\U0001f3cb\U0001f3ff\u200d\u2642\ufe0f", + ":man_mage:": "\U0001f9d9\u200d\u2642\ufe0f", + ":man_mage_tone1:": "\U0001f9d9\U0001f3fb\u200d\u2642\ufe0f", + ":man_mage_tone2:": "\U0001f9d9\U0001f3fc\u200d\u2642\ufe0f", + ":man_mage_tone3:": "\U0001f9d9\U0001f3fd\u200d\u2642\ufe0f", + ":man_mage_tone4:": "\U0001f9d9\U0001f3fe\u200d\u2642\ufe0f", + ":man_mage_tone5:": "\U0001f9d9\U0001f3ff\u200d\u2642\ufe0f", + ":man_mechanic:": "\U0001f468\u200d\U0001f527", + ":man_mechanic_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f527", + ":man_mechanic_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f527", + ":man_mechanic_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f527", + ":man_mechanic_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f527", + ":man_mechanic_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f527", + ":man_mountain_biking:": "\U0001f6b5\u200d\u2642\ufe0f", + ":man_mountain_biking_tone1:": "\U0001f6b5\U0001f3fb\u200d\u2642\ufe0f", + ":man_mountain_biking_tone2:": "\U0001f6b5\U0001f3fc\u200d\u2642\ufe0f", + ":man_mountain_biking_tone3:": "\U0001f6b5\U0001f3fd\u200d\u2642\ufe0f", + ":man_mountain_biking_tone4:": "\U0001f6b5\U0001f3fe\u200d\u2642\ufe0f", + ":man_mountain_biking_tone5:": "\U0001f6b5\U0001f3ff\u200d\u2642\ufe0f", + ":man_office_worker:": "\U0001f468\u200d\U0001f4bc", + ":man_office_worker_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f4bc", + ":man_office_worker_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f4bc", + ":man_office_worker_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f4bc", + ":man_office_worker_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f4bc", + ":man_office_worker_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f4bc", + ":man_pilot:": "\U0001f468\u200d\u2708\ufe0f", + ":man_pilot_tone1:": "\U0001f468\U0001f3fb\u200d\u2708\ufe0f", + ":man_pilot_tone2:": "\U0001f468\U0001f3fc\u200d\u2708\ufe0f", + ":man_pilot_tone3:": "\U0001f468\U0001f3fd\u200d\u2708\ufe0f", + ":man_pilot_tone4:": "\U0001f468\U0001f3fe\u200d\u2708\ufe0f", + ":man_pilot_tone5:": "\U0001f468\U0001f3ff\u200d\u2708\ufe0f", + ":man_playing_handball:": "\U0001f93e\u200d\u2642\ufe0f", + ":man_playing_handball_tone1:": "\U0001f93e\U0001f3fb\u200d\u2642\ufe0f", + ":man_playing_handball_tone2:": "\U0001f93e\U0001f3fc\u200d\u2642\ufe0f", + ":man_playing_handball_tone3:": "\U0001f93e\U0001f3fd\u200d\u2642\ufe0f", + ":man_playing_handball_tone4:": "\U0001f93e\U0001f3fe\u200d\u2642\ufe0f", + ":man_playing_handball_tone5:": "\U0001f93e\U0001f3ff\u200d\u2642\ufe0f", + ":man_playing_water_polo:": "\U0001f93d\u200d\u2642\ufe0f", + ":man_playing_water_polo_tone1:": "\U0001f93d\U0001f3fb\u200d\u2642\ufe0f", + ":man_playing_water_polo_tone2:": "\U0001f93d\U0001f3fc\u200d\u2642\ufe0f", + ":man_playing_water_polo_tone3:": "\U0001f93d\U0001f3fd\u200d\u2642\ufe0f", + ":man_playing_water_polo_tone4:": "\U0001f93d\U0001f3fe\u200d\u2642\ufe0f", + ":man_playing_water_polo_tone5:": "\U0001f93d\U0001f3ff\u200d\u2642\ufe0f", + ":man_police_officer:": "\U0001f46e\u200d\u2642\ufe0f", + ":man_police_officer_tone1:": "\U0001f46e\U0001f3fb\u200d\u2642\ufe0f", + ":man_police_officer_tone2:": "\U0001f46e\U0001f3fc\u200d\u2642\ufe0f", + ":man_police_officer_tone3:": "\U0001f46e\U0001f3fd\u200d\u2642\ufe0f", + ":man_police_officer_tone4:": "\U0001f46e\U0001f3fe\u200d\u2642\ufe0f", + ":man_police_officer_tone5:": "\U0001f46e\U0001f3ff\u200d\u2642\ufe0f", + ":man_pouting:": "\U0001f64e\u200d\u2642\ufe0f", + ":man_pouting_tone1:": "\U0001f64e\U0001f3fb\u200d\u2642\ufe0f", + ":man_pouting_tone2:": "\U0001f64e\U0001f3fc\u200d\u2642\ufe0f", + ":man_pouting_tone3:": "\U0001f64e\U0001f3fd\u200d\u2642\ufe0f", + ":man_pouting_tone4:": "\U0001f64e\U0001f3fe\u200d\u2642\ufe0f", + ":man_pouting_tone5:": "\U0001f64e\U0001f3ff\u200d\u2642\ufe0f", + ":man_raising_hand:": "\U0001f64b\u200d\u2642\ufe0f", + ":man_raising_hand_tone1:": "\U0001f64b\U0001f3fb\u200d\u2642\ufe0f", + ":man_raising_hand_tone2:": "\U0001f64b\U0001f3fc\u200d\u2642\ufe0f", + ":man_raising_hand_tone3:": "\U0001f64b\U0001f3fd\u200d\u2642\ufe0f", + ":man_raising_hand_tone4:": "\U0001f64b\U0001f3fe\u200d\u2642\ufe0f", + ":man_raising_hand_tone5:": "\U0001f64b\U0001f3ff\u200d\u2642\ufe0f", + ":man_rowing_boat:": "\U0001f6a3\u200d\u2642\ufe0f", + ":man_rowing_boat_tone1:": "\U0001f6a3\U0001f3fb\u200d\u2642\ufe0f", + ":man_rowing_boat_tone2:": "\U0001f6a3\U0001f3fc\u200d\u2642\ufe0f", + ":man_rowing_boat_tone3:": "\U0001f6a3\U0001f3fd\u200d\u2642\ufe0f", + ":man_rowing_boat_tone4:": "\U0001f6a3\U0001f3fe\u200d\u2642\ufe0f", + ":man_rowing_boat_tone5:": "\U0001f6a3\U0001f3ff\u200d\u2642\ufe0f", + ":man_running:": "\U0001f3c3\u200d\u2642\ufe0f", + ":man_running_tone1:": "\U0001f3c3\U0001f3fb\u200d\u2642\ufe0f", + ":man_running_tone2:": "\U0001f3c3\U0001f3fc\u200d\u2642\ufe0f", + ":man_running_tone3:": "\U0001f3c3\U0001f3fd\u200d\u2642\ufe0f", + ":man_running_tone4:": "\U0001f3c3\U0001f3fe\u200d\u2642\ufe0f", + ":man_running_tone5:": "\U0001f3c3\U0001f3ff\u200d\u2642\ufe0f", + ":man_scientist:": "\U0001f468\u200d\U0001f52c", + ":man_scientist_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f52c", + ":man_scientist_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f52c", + ":man_scientist_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f52c", + ":man_scientist_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f52c", + ":man_scientist_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f52c", + ":man_shrugging:": "\U0001f937\u200d\u2642\ufe0f", + ":man_shrugging_tone1:": "\U0001f937\U0001f3fb\u200d\u2642\ufe0f", + ":man_shrugging_tone2:": "\U0001f937\U0001f3fc\u200d\u2642\ufe0f", + ":man_shrugging_tone3:": "\U0001f937\U0001f3fd\u200d\u2642\ufe0f", + ":man_shrugging_tone4:": "\U0001f937\U0001f3fe\u200d\u2642\ufe0f", + ":man_shrugging_tone5:": "\U0001f937\U0001f3ff\u200d\u2642\ufe0f", + ":man_singer:": "\U0001f468\u200d\U0001f3a4", + ":man_singer_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f3a4", + ":man_singer_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f3a4", + ":man_singer_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f3a4", + ":man_singer_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f3a4", + ":man_singer_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f3a4", + ":man_student:": "\U0001f468\u200d\U0001f393", + ":man_student_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f393", + ":man_student_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f393", + ":man_student_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f393", + ":man_student_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f393", + ":man_student_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f393", + ":man_surfing:": "\U0001f3c4\u200d\u2642\ufe0f", + ":man_surfing_tone1:": "\U0001f3c4\U0001f3fb\u200d\u2642\ufe0f", + ":man_surfing_tone2:": "\U0001f3c4\U0001f3fc\u200d\u2642\ufe0f", + ":man_surfing_tone3:": "\U0001f3c4\U0001f3fd\u200d\u2642\ufe0f", + ":man_surfing_tone4:": "\U0001f3c4\U0001f3fe\u200d\u2642\ufe0f", + ":man_surfing_tone5:": "\U0001f3c4\U0001f3ff\u200d\u2642\ufe0f", + ":man_swimming:": "\U0001f3ca\u200d\u2642\ufe0f", + ":man_swimming_tone1:": "\U0001f3ca\U0001f3fb\u200d\u2642\ufe0f", + ":man_swimming_tone2:": "\U0001f3ca\U0001f3fc\u200d\u2642\ufe0f", + ":man_swimming_tone3:": "\U0001f3ca\U0001f3fd\u200d\u2642\ufe0f", + ":man_swimming_tone4:": "\U0001f3ca\U0001f3fe\u200d\u2642\ufe0f", + ":man_swimming_tone5:": "\U0001f3ca\U0001f3ff\u200d\u2642\ufe0f", + ":man_teacher:": "\U0001f468\u200d\U0001f3eb", + ":man_teacher_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f3eb", + ":man_teacher_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f3eb", + ":man_teacher_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f3eb", + ":man_teacher_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f3eb", + ":man_teacher_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f3eb", + ":man_technologist:": "\U0001f468\u200d\U0001f4bb", + ":man_technologist_tone1:": "\U0001f468\U0001f3fb\u200d\U0001f4bb", + ":man_technologist_tone2:": "\U0001f468\U0001f3fc\u200d\U0001f4bb", + ":man_technologist_tone3:": "\U0001f468\U0001f3fd\u200d\U0001f4bb", + ":man_technologist_tone4:": "\U0001f468\U0001f3fe\u200d\U0001f4bb", + ":man_technologist_tone5:": "\U0001f468\U0001f3ff\u200d\U0001f4bb", + ":man_tipping_hand:": "\U0001f481\u200d\u2642\ufe0f", + ":man_tipping_hand_tone1:": "\U0001f481\U0001f3fb\u200d\u2642\ufe0f", + ":man_tipping_hand_tone2:": "\U0001f481\U0001f3fc\u200d\u2642\ufe0f", + ":man_tipping_hand_tone3:": "\U0001f481\U0001f3fd\u200d\u2642\ufe0f", + ":man_tipping_hand_tone4:": "\U0001f481\U0001f3fe\u200d\u2642\ufe0f", + ":man_tipping_hand_tone5:": "\U0001f481\U0001f3ff\u200d\u2642\ufe0f", + ":man_tone1:": "\U0001f468\U0001f3fb", + ":man_tone2:": "\U0001f468\U0001f3fc", + ":man_tone3:": "\U0001f468\U0001f3fd", + ":man_tone4:": "\U0001f468\U0001f3fe", + ":man_tone5:": "\U0001f468\U0001f3ff", + ":man_vampire:": "\U0001f9db\u200d\u2642\ufe0f", + ":man_vampire_tone1:": "\U0001f9db\U0001f3fb\u200d\u2642\ufe0f", + ":man_vampire_tone2:": "\U0001f9db\U0001f3fc\u200d\u2642\ufe0f", + ":man_vampire_tone3:": "\U0001f9db\U0001f3fd\u200d\u2642\ufe0f", + ":man_vampire_tone4:": "\U0001f9db\U0001f3fe\u200d\u2642\ufe0f", + ":man_vampire_tone5:": "\U0001f9db\U0001f3ff\u200d\u2642\ufe0f", + ":man_walking:": "\U0001f6b6\u200d\u2642\ufe0f", + ":man_walking_tone1:": "\U0001f6b6\U0001f3fb\u200d\u2642\ufe0f", + ":man_walking_tone2:": "\U0001f6b6\U0001f3fc\u200d\u2642\ufe0f", + ":man_walking_tone3:": "\U0001f6b6\U0001f3fd\u200d\u2642\ufe0f", + ":man_walking_tone4:": "\U0001f6b6\U0001f3fe\u200d\u2642\ufe0f", + ":man_walking_tone5:": "\U0001f6b6\U0001f3ff\u200d\u2642\ufe0f", + ":man_wearing_turban:": "\U0001f473\u200d\u2642\ufe0f", + ":man_wearing_turban_tone1:": "\U0001f473\U0001f3fb\u200d\u2642\ufe0f", + ":man_wearing_turban_tone2:": "\U0001f473\U0001f3fc\u200d\u2642\ufe0f", + ":man_wearing_turban_tone3:": "\U0001f473\U0001f3fd\u200d\u2642\ufe0f", + ":man_wearing_turban_tone4:": "\U0001f473\U0001f3fe\u200d\u2642\ufe0f", + ":man_wearing_turban_tone5:": "\U0001f473\U0001f3ff\u200d\u2642\ufe0f", + ":man_with_chinese_cap:": "\U0001f472", + ":man_with_chinese_cap_tone1:": "\U0001f472\U0001f3fb", + ":man_with_chinese_cap_tone2:": "\U0001f472\U0001f3fc", + ":man_with_chinese_cap_tone3:": "\U0001f472\U0001f3fd", + ":man_with_chinese_cap_tone4:": "\U0001f472\U0001f3fe", + ":man_with_chinese_cap_tone5:": "\U0001f472\U0001f3ff", + ":man_zombie:": "\U0001f9df\u200d\u2642\ufe0f", + ":mans_shoe:": "\U0001f45e", + ":map:": "\U0001f5fa", + ":maple_leaf:": "\U0001f341", + ":martial_arts_uniform:": "\U0001f94b", + ":mask:": "\U0001f637", + ":meat_on_bone:": "\U0001f356", + ":medal:": "\U0001f3c5", + ":medical_symbol:": "\u2695", + ":mega:": "\U0001f4e3", + ":melon:": "\U0001f348", + ":men_with_bunny_ears_partying:": "\U0001f46f\u200d\u2642\ufe0f", + ":men_wrestling:": "\U0001f93c\u200d\u2642\ufe0f", + ":menorah:": "\U0001f54e", + ":mens:": "\U0001f6b9", + ":mermaid:": "\U0001f9dc\u200d\u2640\ufe0f", + ":mermaid_tone1:": "\U0001f9dc\U0001f3fb\u200d\u2640\ufe0f", + ":mermaid_tone2:": "\U0001f9dc\U0001f3fc\u200d\u2640\ufe0f", + ":mermaid_tone3:": "\U0001f9dc\U0001f3fd\u200d\u2640\ufe0f", + ":mermaid_tone4:": "\U0001f9dc\U0001f3fe\u200d\u2640\ufe0f", + ":mermaid_tone5:": "\U0001f9dc\U0001f3ff\u200d\u2640\ufe0f", + ":merman:": "\U0001f9dc\u200d\u2642\ufe0f", + ":merman_tone1:": "\U0001f9dc\U0001f3fb\u200d\u2642\ufe0f", + ":merman_tone2:": "\U0001f9dc\U0001f3fc\u200d\u2642\ufe0f", + ":merman_tone3:": "\U0001f9dc\U0001f3fd\u200d\u2642\ufe0f", + ":merman_tone4:": "\U0001f9dc\U0001f3fe\u200d\u2642\ufe0f", + ":merman_tone5:": "\U0001f9dc\U0001f3ff\u200d\u2642\ufe0f", + ":merperson:": "\U0001f9dc", + ":merperson_tone1:": "\U0001f9dc\U0001f3fb", + ":merperson_tone2:": "\U0001f9dc\U0001f3fc", + ":merperson_tone3:": "\U0001f9dc\U0001f3fd", + ":merperson_tone4:": "\U0001f9dc\U0001f3fe", + ":merperson_tone5:": "\U0001f9dc\U0001f3ff", + ":metal:": "\U0001f918", + ":metal_tone1:": "\U0001f918\U0001f3fb", + ":metal_tone2:": "\U0001f918\U0001f3fc", + ":metal_tone3:": "\U0001f918\U0001f3fd", + ":metal_tone4:": "\U0001f918\U0001f3fe", + ":metal_tone5:": "\U0001f918\U0001f3ff", + ":metro:": "\U0001f687", + ":microphone:": "\U0001f3a4", + ":microphone2:": "\U0001f399", + ":microscope:": "\U0001f52c", + ":middle_finger:": "\U0001f595", + ":middle_finger_tone1:": "\U0001f595\U0001f3fb", + ":middle_finger_tone2:": "\U0001f595\U0001f3fc", + ":middle_finger_tone3:": "\U0001f595\U0001f3fd", + ":middle_finger_tone4:": "\U0001f595\U0001f3fe", + ":middle_finger_tone5:": "\U0001f595\U0001f3ff", + ":military_medal:": "\U0001f396", + ":milk:": "\U0001f95b", + ":milky_way:": "\U0001f30c", + ":minibus:": "\U0001f690", + ":minidisc:": "\U0001f4bd", + ":mobile_phone_off:": "\U0001f4f4", + ":money_mouth:": "\U0001f911", + ":money_with_wings:": "\U0001f4b8", + ":moneybag:": "\U0001f4b0", + ":monkey:": "\U0001f412", + ":monkey_face:": "\U0001f435", + ":monorail:": "\U0001f69d", + ":mortar_board:": "\U0001f393", + ":mosque:": "\U0001f54c", + ":motor_scooter:": "\U0001f6f5", + ":motorboat:": "\U0001f6e5", + ":motorcycle:": "\U0001f3cd", + ":motorway:": "\U0001f6e3", + ":mount_fuji:": "\U0001f5fb", + ":mountain:": "\u26f0", + ":mountain_cableway:": "\U0001f6a0", + ":mountain_railway:": "\U0001f69e", + ":mountain_snow:": "\U0001f3d4", + ":mouse:": "\U0001f42d", + ":mouse2:": "\U0001f401", + ":mouse_three_button:": "\U0001f5b1", + ":movie_camera:": "\U0001f3a5", + ":moyai:": "\U0001f5ff", + ":mrs_claus:": "\U0001f936", + ":mrs_claus_tone1:": "\U0001f936\U0001f3fb", + ":mrs_claus_tone2:": "\U0001f936\U0001f3fc", + ":mrs_claus_tone3:": "\U0001f936\U0001f3fd", + ":mrs_claus_tone4:": "\U0001f936\U0001f3fe", + ":mrs_claus_tone5:": "\U0001f936\U0001f3ff", + ":muscle:": "\U0001f4aa", + ":muscle_tone1:": "\U0001f4aa\U0001f3fb", + ":muscle_tone2:": "\U0001f4aa\U0001f3fc", + ":muscle_tone3:": "\U0001f4aa\U0001f3fd", + ":muscle_tone4:": "\U0001f4aa\U0001f3fe", + ":muscle_tone5:": "\U0001f4aa\U0001f3ff", + ":mushroom:": "\U0001f344", + ":musical_keyboard:": "\U0001f3b9", + ":musical_note:": "\U0001f3b5", + ":musical_score:": "\U0001f3bc", + ":mute:": "\U0001f507", + ":nail_care:": "\U0001f485", + ":nail_care_tone1:": "\U0001f485\U0001f3fb", + ":nail_care_tone2:": "\U0001f485\U0001f3fc", + ":nail_care_tone3:": "\U0001f485\U0001f3fd", + ":nail_care_tone4:": "\U0001f485\U0001f3fe", + ":nail_care_tone5:": "\U0001f485\U0001f3ff", + ":name_badge:": "\U0001f4db", + ":nauseated_face:": "\U0001f922", + ":necktie:": "\U0001f454", + ":negative_squared_cross_mark:": "\u274e", + ":nerd:": "\U0001f913", + ":neutral_face:": "\U0001f610", + ":new:": "\U0001f195", + ":new_moon:": "\U0001f311", + ":new_moon_with_face:": "\U0001f31a", + ":newspaper:": "\U0001f4f0", + ":newspaper2:": "\U0001f5de", + ":ng:": "\U0001f196", + ":night_with_stars:": "\U0001f303", + ":nine:": "9\ufe0f\u20e3", + ":no_bell:": "\U0001f515", + ":no_bicycles:": "\U0001f6b3", + ":no_entry:": "\u26d4", + ":no_entry_sign:": "\U0001f6ab", + ":no_mobile_phones:": "\U0001f4f5", + ":no_mouth:": "\U0001f636", + ":no_pedestrians:": "\U0001f6b7", + ":no_smoking:": "\U0001f6ad", + ":non-potable_water:": "\U0001f6b1", + ":nose:": "\U0001f443", + ":nose_tone1:": "\U0001f443\U0001f3fb", + ":nose_tone2:": "\U0001f443\U0001f3fc", + ":nose_tone3:": "\U0001f443\U0001f3fd", + ":nose_tone4:": "\U0001f443\U0001f3fe", + ":nose_tone5:": "\U0001f443\U0001f3ff", + ":notebook:": "\U0001f4d3", + ":notebook_with_decorative_cover:": "\U0001f4d4", + ":notepad_spiral:": "\U0001f5d2", + ":notes:": "\U0001f3b6", + ":nut_and_bolt:": "\U0001f529", + ":o:": "\u2b55", + ":o2:": "\U0001f17e", + ":ocean:": "\U0001f30a", + ":octagonal_sign:": "\U0001f6d1", + ":octopus:": "\U0001f419", + ":oden:": "\U0001f362", + ":office:": "\U0001f3e2", + ":oil:": "\U0001f6e2", + ":ok:": "\U0001f197", + ":ok_hand:": "\U0001f44c", + ":ok_hand_tone1:": "\U0001f44c\U0001f3fb", + ":ok_hand_tone2:": "\U0001f44c\U0001f3fc", + ":ok_hand_tone3:": "\U0001f44c\U0001f3fd", + ":ok_hand_tone4:": "\U0001f44c\U0001f3fe", + ":ok_hand_tone5:": "\U0001f44c\U0001f3ff", + ":older_adult:": "\U0001f9d3", + ":older_adult_tone1:": "\U0001f9d3\U0001f3fb", + ":older_adult_tone2:": "\U0001f9d3\U0001f3fc", + ":older_adult_tone3:": "\U0001f9d3\U0001f3fd", + ":older_adult_tone4:": "\U0001f9d3\U0001f3fe", + ":older_adult_tone5:": "\U0001f9d3\U0001f3ff", + ":older_man:": "\U0001f474", + ":older_man_tone1:": "\U0001f474\U0001f3fb", + ":older_man_tone2:": "\U0001f474\U0001f3fc", + ":older_man_tone3:": "\U0001f474\U0001f3fd", + ":older_man_tone4:": "\U0001f474\U0001f3fe", + ":older_man_tone5:": "\U0001f474\U0001f3ff", + ":older_woman:": "\U0001f475", + ":older_woman_tone1:": "\U0001f475\U0001f3fb", + ":older_woman_tone2:": "\U0001f475\U0001f3fc", + ":older_woman_tone3:": "\U0001f475\U0001f3fd", + ":older_woman_tone4:": "\U0001f475\U0001f3fe", + ":older_woman_tone5:": "\U0001f475\U0001f3ff", + ":om_symbol:": "\U0001f549", + ":on:": "\U0001f51b", + ":oncoming_automobile:": "\U0001f698", + ":oncoming_bus:": "\U0001f68d", + ":oncoming_police_car:": "\U0001f694", + ":oncoming_taxi:": "\U0001f696", + ":one:": "1\ufe0f\u20e3", + ":open_file_folder:": "\U0001f4c2", + ":open_hands:": "\U0001f450", + ":open_hands_tone1:": "\U0001f450\U0001f3fb", + ":open_hands_tone2:": "\U0001f450\U0001f3fc", + ":open_hands_tone3:": "\U0001f450\U0001f3fd", + ":open_hands_tone4:": "\U0001f450\U0001f3fe", + ":open_hands_tone5:": "\U0001f450\U0001f3ff", + ":open_mouth:": "\U0001f62e", + ":ophiuchus:": "\u26ce", + ":orange_book:": "\U0001f4d9", + ":orange_heart:": "\U0001f9e1", + ":orthodox_cross:": "\u2626", + ":outbox_tray:": "\U0001f4e4", + ":owl:": "\U0001f989", + ":ox:": "\U0001f402", + ":package:": "\U0001f4e6", + ":page_facing_up:": "\U0001f4c4", + ":page_with_curl:": "\U0001f4c3", + ":pager:": "\U0001f4df", + ":paintbrush:": "\U0001f58c", + ":palm_tree:": "\U0001f334", + ":palms_up_together:": "\U0001f932", + ":palms_up_together_tone1:": "\U0001f932\U0001f3fb", + ":palms_up_together_tone2:": "\U0001f932\U0001f3fc", + ":palms_up_together_tone3:": "\U0001f932\U0001f3fd", + ":palms_up_together_tone4:": "\U0001f932\U0001f3fe", + ":palms_up_together_tone5:": "\U0001f932\U0001f3ff", + ":pancakes:": "\U0001f95e", + ":panda_face:": "\U0001f43c", + ":paperclip:": "\U0001f4ce", + ":paperclips:": "\U0001f587", + ":park:": "\U0001f3de", + ":parking:": "\U0001f17f", + ":part_alternation_mark:": "\u303d", + ":partly_sunny:": "\u26c5", + ":passport_control:": "\U0001f6c2", + ":pause_button:": "\u23f8", + ":peace:": "\u262e", + ":peach:": "\U0001f351", + ":peanuts:": "\U0001f95c", + ":pear:": "\U0001f350", + ":pen_ballpoint:": "\U0001f58a", + ":pen_fountain:": "\U0001f58b", + ":pencil:": "\U0001f4dd", + ":pencil2:": "\u270f", + ":penguin:": "\U0001f427", + ":pensive:": "\U0001f614", + ":people_with_bunny_ears_partying:": "\U0001f46f", + ":people_wrestling:": "\U0001f93c", + ":performing_arts:": "\U0001f3ad", + ":persevere:": "\U0001f623", + ":person_biking:": "\U0001f6b4", + ":person_biking_tone1:": "\U0001f6b4\U0001f3fb", + ":person_biking_tone2:": "\U0001f6b4\U0001f3fc", + ":person_biking_tone3:": "\U0001f6b4\U0001f3fd", + ":person_biking_tone4:": "\U0001f6b4\U0001f3fe", + ":person_biking_tone5:": "\U0001f6b4\U0001f3ff", + ":person_bouncing_ball:": "\u26f9", + ":person_bouncing_ball_tone1:": "\u26f9\U0001f3fb", + ":person_bouncing_ball_tone2:": "\u26f9\U0001f3fc", + ":person_bouncing_ball_tone3:": "\u26f9\U0001f3fd", + ":person_bouncing_ball_tone4:": "\u26f9\U0001f3fe", + ":person_bouncing_ball_tone5:": "\u26f9\U0001f3ff", + ":person_bowing:": "\U0001f647", + ":person_bowing_tone1:": "\U0001f647\U0001f3fb", + ":person_bowing_tone2:": "\U0001f647\U0001f3fc", + ":person_bowing_tone3:": "\U0001f647\U0001f3fd", + ":person_bowing_tone4:": "\U0001f647\U0001f3fe", + ":person_bowing_tone5:": "\U0001f647\U0001f3ff", + ":person_climbing:": "\U0001f9d7", + ":person_climbing_tone1:": "\U0001f9d7\U0001f3fb", + ":person_climbing_tone2:": "\U0001f9d7\U0001f3fc", + ":person_climbing_tone3:": "\U0001f9d7\U0001f3fd", + ":person_climbing_tone4:": "\U0001f9d7\U0001f3fe", + ":person_climbing_tone5:": "\U0001f9d7\U0001f3ff", + ":person_doing_cartwheel:": "\U0001f938", + ":person_doing_cartwheel_tone1:": "\U0001f938\U0001f3fb", + ":person_doing_cartwheel_tone2:": "\U0001f938\U0001f3fc", + ":person_doing_cartwheel_tone3:": "\U0001f938\U0001f3fd", + ":person_doing_cartwheel_tone4:": "\U0001f938\U0001f3fe", + ":person_doing_cartwheel_tone5:": "\U0001f938\U0001f3ff", + ":person_facepalming:": "\U0001f926", + ":person_facepalming_tone1:": "\U0001f926\U0001f3fb", + ":person_facepalming_tone2:": "\U0001f926\U0001f3fc", + ":person_facepalming_tone3:": "\U0001f926\U0001f3fd", + ":person_facepalming_tone4:": "\U0001f926\U0001f3fe", + ":person_facepalming_tone5:": "\U0001f926\U0001f3ff", + ":person_fencing:": "\U0001f93a", + ":person_frowning:": "\U0001f64d", + ":person_frowning_tone1:": "\U0001f64d\U0001f3fb", + ":person_frowning_tone2:": "\U0001f64d\U0001f3fc", + ":person_frowning_tone3:": "\U0001f64d\U0001f3fd", + ":person_frowning_tone4:": "\U0001f64d\U0001f3fe", + ":person_frowning_tone5:": "\U0001f64d\U0001f3ff", + ":person_gesturing_no:": "\U0001f645", + ":person_gesturing_no_tone1:": "\U0001f645\U0001f3fb", + ":person_gesturing_no_tone2:": "\U0001f645\U0001f3fc", + ":person_gesturing_no_tone3:": "\U0001f645\U0001f3fd", + ":person_gesturing_no_tone4:": "\U0001f645\U0001f3fe", + ":person_gesturing_no_tone5:": "\U0001f645\U0001f3ff", + ":person_gesturing_ok:": "\U0001f646", + ":person_gesturing_ok_tone1:": "\U0001f646\U0001f3fb", + ":person_gesturing_ok_tone2:": "\U0001f646\U0001f3fc", + ":person_gesturing_ok_tone3:": "\U0001f646\U0001f3fd", + ":person_gesturing_ok_tone4:": "\U0001f646\U0001f3fe", + ":person_gesturing_ok_tone5:": "\U0001f646\U0001f3ff", + ":person_getting_haircut:": "\U0001f487", + ":person_getting_haircut_tone1:": "\U0001f487\U0001f3fb", + ":person_getting_haircut_tone2:": "\U0001f487\U0001f3fc", + ":person_getting_haircut_tone3:": "\U0001f487\U0001f3fd", + ":person_getting_haircut_tone4:": "\U0001f487\U0001f3fe", + ":person_getting_haircut_tone5:": "\U0001f487\U0001f3ff", + ":person_getting_massage:": "\U0001f486", + ":person_getting_massage_tone1:": "\U0001f486\U0001f3fb", + ":person_getting_massage_tone2:": "\U0001f486\U0001f3fc", + ":person_getting_massage_tone3:": "\U0001f486\U0001f3fd", + ":person_getting_massage_tone4:": "\U0001f486\U0001f3fe", + ":person_getting_massage_tone5:": "\U0001f486\U0001f3ff", + ":person_golfing:": "\U0001f3cc", + ":person_golfing_tone1:": "\U0001f3cc\U0001f3fb", + ":person_golfing_tone2:": "\U0001f3cc\U0001f3fc", + ":person_golfing_tone3:": "\U0001f3cc\U0001f3fd", + ":person_golfing_tone4:": "\U0001f3cc\U0001f3fe", + ":person_golfing_tone5:": "\U0001f3cc\U0001f3ff", + ":person_in_bed_tone1:": "\U0001f6cc\U0001f3fb", + ":person_in_bed_tone2:": "\U0001f6cc\U0001f3fc", + ":person_in_bed_tone3:": "\U0001f6cc\U0001f3fd", + ":person_in_bed_tone4:": "\U0001f6cc\U0001f3fe", + ":person_in_bed_tone5:": "\U0001f6cc\U0001f3ff", + ":person_in_lotus_position:": "\U0001f9d8", + ":person_in_lotus_position_tone1:": "\U0001f9d8\U0001f3fb", + ":person_in_lotus_position_tone2:": "\U0001f9d8\U0001f3fc", + ":person_in_lotus_position_tone3:": "\U0001f9d8\U0001f3fd", + ":person_in_lotus_position_tone4:": "\U0001f9d8\U0001f3fe", + ":person_in_lotus_position_tone5:": "\U0001f9d8\U0001f3ff", + ":person_in_steamy_room:": "\U0001f9d6", + ":person_in_steamy_room_tone1:": "\U0001f9d6\U0001f3fb", + ":person_in_steamy_room_tone2:": "\U0001f9d6\U0001f3fc", + ":person_in_steamy_room_tone3:": "\U0001f9d6\U0001f3fd", + ":person_in_steamy_room_tone4:": "\U0001f9d6\U0001f3fe", + ":person_in_steamy_room_tone5:": "\U0001f9d6\U0001f3ff", + ":person_juggling:": "\U0001f939", + ":person_juggling_tone1:": "\U0001f939\U0001f3fb", + ":person_juggling_tone2:": "\U0001f939\U0001f3fc", + ":person_juggling_tone3:": "\U0001f939\U0001f3fd", + ":person_juggling_tone4:": "\U0001f939\U0001f3fe", + ":person_juggling_tone5:": "\U0001f939\U0001f3ff", + ":person_lifting_weights:": "\U0001f3cb", + ":person_lifting_weights_tone1:": "\U0001f3cb\U0001f3fb", + ":person_lifting_weights_tone2:": "\U0001f3cb\U0001f3fc", + ":person_lifting_weights_tone3:": "\U0001f3cb\U0001f3fd", + ":person_lifting_weights_tone4:": "\U0001f3cb\U0001f3fe", + ":person_lifting_weights_tone5:": "\U0001f3cb\U0001f3ff", + ":person_mountain_biking:": "\U0001f6b5", + ":person_mountain_biking_tone1:": "\U0001f6b5\U0001f3fb", + ":person_mountain_biking_tone2:": "\U0001f6b5\U0001f3fc", + ":person_mountain_biking_tone3:": "\U0001f6b5\U0001f3fd", + ":person_mountain_biking_tone4:": "\U0001f6b5\U0001f3fe", + ":person_mountain_biking_tone5:": "\U0001f6b5\U0001f3ff", + ":person_playing_handball:": "\U0001f93e", + ":person_playing_handball_tone1:": "\U0001f93e\U0001f3fb", + ":person_playing_handball_tone2:": "\U0001f93e\U0001f3fc", + ":person_playing_handball_tone3:": "\U0001f93e\U0001f3fd", + ":person_playing_handball_tone4:": "\U0001f93e\U0001f3fe", + ":person_playing_handball_tone5:": "\U0001f93e\U0001f3ff", + ":person_playing_water_polo:": "\U0001f93d", + ":person_playing_water_polo_tone1:": "\U0001f93d\U0001f3fb", + ":person_playing_water_polo_tone2:": "\U0001f93d\U0001f3fc", + ":person_playing_water_polo_tone3:": "\U0001f93d\U0001f3fd", + ":person_playing_water_polo_tone4:": "\U0001f93d\U0001f3fe", + ":person_playing_water_polo_tone5:": "\U0001f93d\U0001f3ff", + ":person_pouting:": "\U0001f64e", + ":person_pouting_tone1:": "\U0001f64e\U0001f3fb", + ":person_pouting_tone2:": "\U0001f64e\U0001f3fc", + ":person_pouting_tone3:": "\U0001f64e\U0001f3fd", + ":person_pouting_tone4:": "\U0001f64e\U0001f3fe", + ":person_pouting_tone5:": "\U0001f64e\U0001f3ff", + ":person_raising_hand:": "\U0001f64b", + ":person_raising_hand_tone1:": "\U0001f64b\U0001f3fb", + ":person_raising_hand_tone2:": "\U0001f64b\U0001f3fc", + ":person_raising_hand_tone3:": "\U0001f64b\U0001f3fd", + ":person_raising_hand_tone4:": "\U0001f64b\U0001f3fe", + ":person_raising_hand_tone5:": "\U0001f64b\U0001f3ff", + ":person_rowing_boat:": "\U0001f6a3", + ":person_rowing_boat_tone1:": "\U0001f6a3\U0001f3fb", + ":person_rowing_boat_tone2:": "\U0001f6a3\U0001f3fc", + ":person_rowing_boat_tone3:": "\U0001f6a3\U0001f3fd", + ":person_rowing_boat_tone4:": "\U0001f6a3\U0001f3fe", + ":person_rowing_boat_tone5:": "\U0001f6a3\U0001f3ff", + ":person_running:": "\U0001f3c3", + ":person_running_tone1:": "\U0001f3c3\U0001f3fb", + ":person_running_tone2:": "\U0001f3c3\U0001f3fc", + ":person_running_tone3:": "\U0001f3c3\U0001f3fd", + ":person_running_tone4:": "\U0001f3c3\U0001f3fe", + ":person_running_tone5:": "\U0001f3c3\U0001f3ff", + ":person_shrugging:": "\U0001f937", + ":person_shrugging_tone1:": "\U0001f937\U0001f3fb", + ":person_shrugging_tone2:": "\U0001f937\U0001f3fc", + ":person_shrugging_tone3:": "\U0001f937\U0001f3fd", + ":person_shrugging_tone4:": "\U0001f937\U0001f3fe", + ":person_shrugging_tone5:": "\U0001f937\U0001f3ff", + ":person_surfing:": "\U0001f3c4", + ":person_surfing_tone1:": "\U0001f3c4\U0001f3fb", + ":person_surfing_tone2:": "\U0001f3c4\U0001f3fc", + ":person_surfing_tone3:": "\U0001f3c4\U0001f3fd", + ":person_surfing_tone4:": "\U0001f3c4\U0001f3fe", + ":person_surfing_tone5:": "\U0001f3c4\U0001f3ff", + ":person_swimming:": "\U0001f3ca", + ":person_swimming_tone1:": "\U0001f3ca\U0001f3fb", + ":person_swimming_tone2:": "\U0001f3ca\U0001f3fc", + ":person_swimming_tone3:": "\U0001f3ca\U0001f3fd", + ":person_swimming_tone4:": "\U0001f3ca\U0001f3fe", + ":person_swimming_tone5:": "\U0001f3ca\U0001f3ff", + ":person_tipping_hand:": "\U0001f481", + ":person_tipping_hand_tone1:": "\U0001f481\U0001f3fb", + ":person_tipping_hand_tone2:": "\U0001f481\U0001f3fc", + ":person_tipping_hand_tone3:": "\U0001f481\U0001f3fd", + ":person_tipping_hand_tone4:": "\U0001f481\U0001f3fe", + ":person_tipping_hand_tone5:": "\U0001f481\U0001f3ff", + ":person_walking:": "\U0001f6b6", + ":person_walking_tone1:": "\U0001f6b6\U0001f3fb", + ":person_walking_tone2:": "\U0001f6b6\U0001f3fc", + ":person_walking_tone3:": "\U0001f6b6\U0001f3fd", + ":person_walking_tone4:": "\U0001f6b6\U0001f3fe", + ":person_walking_tone5:": "\U0001f6b6\U0001f3ff", + ":person_wearing_turban:": "\U0001f473", + ":person_wearing_turban_tone1:": "\U0001f473\U0001f3fb", + ":person_wearing_turban_tone2:": "\U0001f473\U0001f3fc", + ":person_wearing_turban_tone3:": "\U0001f473\U0001f3fd", + ":person_wearing_turban_tone4:": "\U0001f473\U0001f3fe", + ":person_wearing_turban_tone5:": "\U0001f473\U0001f3ff", + ":pick:": "\u26cf", + ":pie:": "\U0001f967", + ":pig:": "\U0001f437", + ":pig2:": "\U0001f416", + ":pig_nose:": "\U0001f43d", + ":pill:": "\U0001f48a", + ":pineapple:": "\U0001f34d", + ":ping_pong:": "\U0001f3d3", + ":pisces:": "\u2653", + ":pizza:": "\U0001f355", + ":place_of_worship:": "\U0001f6d0", + ":play_pause:": "\u23ef", + ":point_down:": "\U0001f447", + ":point_down_tone1:": "\U0001f447\U0001f3fb", + ":point_down_tone2:": "\U0001f447\U0001f3fc", + ":point_down_tone3:": "\U0001f447\U0001f3fd", + ":point_down_tone4:": "\U0001f447\U0001f3fe", + ":point_down_tone5:": "\U0001f447\U0001f3ff", + ":point_left:": "\U0001f448", + ":point_left_tone1:": "\U0001f448\U0001f3fb", + ":point_left_tone2:": "\U0001f448\U0001f3fc", + ":point_left_tone3:": "\U0001f448\U0001f3fd", + ":point_left_tone4:": "\U0001f448\U0001f3fe", + ":point_left_tone5:": "\U0001f448\U0001f3ff", + ":point_right:": "\U0001f449", + ":point_right_tone1:": "\U0001f449\U0001f3fb", + ":point_right_tone2:": "\U0001f449\U0001f3fc", + ":point_right_tone3:": "\U0001f449\U0001f3fd", + ":point_right_tone4:": "\U0001f449\U0001f3fe", + ":point_right_tone5:": "\U0001f449\U0001f3ff", + ":point_up:": "\u261d", + ":point_up_2:": "\U0001f446", + ":point_up_2_tone1:": "\U0001f446\U0001f3fb", + ":point_up_2_tone2:": "\U0001f446\U0001f3fc", + ":point_up_2_tone3:": "\U0001f446\U0001f3fd", + ":point_up_2_tone4:": "\U0001f446\U0001f3fe", + ":point_up_2_tone5:": "\U0001f446\U0001f3ff", + ":point_up_tone1:": "\u261d\U0001f3fb", + ":point_up_tone2:": "\u261d\U0001f3fc", + ":point_up_tone3:": "\u261d\U0001f3fd", + ":point_up_tone4:": "\u261d\U0001f3fe", + ":point_up_tone5:": "\u261d\U0001f3ff", + ":police_car:": "\U0001f693", + ":police_officer:": "\U0001f46e", + ":police_officer_tone1:": "\U0001f46e\U0001f3fb", + ":police_officer_tone2:": "\U0001f46e\U0001f3fc", + ":police_officer_tone3:": "\U0001f46e\U0001f3fd", + ":police_officer_tone4:": "\U0001f46e\U0001f3fe", + ":police_officer_tone5:": "\U0001f46e\U0001f3ff", + ":poodle:": "\U0001f429", + ":poop:": "\U0001f4a9", + ":popcorn:": "\U0001f37f", + ":post_office:": "\U0001f3e3", + ":postal_horn:": "\U0001f4ef", + ":postbox:": "\U0001f4ee", + ":potable_water:": "\U0001f6b0", + ":potato:": "\U0001f954", + ":pouch:": "\U0001f45d", + ":poultry_leg:": "\U0001f357", + ":pound:": "\U0001f4b7", + ":pouting_cat:": "\U0001f63e", + ":pray:": "\U0001f64f", + ":pray_tone1:": "\U0001f64f\U0001f3fb", + ":pray_tone2:": "\U0001f64f\U0001f3fc", + ":pray_tone3:": "\U0001f64f\U0001f3fd", + ":pray_tone4:": "\U0001f64f\U0001f3fe", + ":pray_tone5:": "\U0001f64f\U0001f3ff", + ":prayer_beads:": "\U0001f4ff", + ":pregnant_woman:": "\U0001f930", + ":pregnant_woman_tone1:": "\U0001f930\U0001f3fb", + ":pregnant_woman_tone2:": "\U0001f930\U0001f3fc", + ":pregnant_woman_tone3:": "\U0001f930\U0001f3fd", + ":pregnant_woman_tone4:": "\U0001f930\U0001f3fe", + ":pregnant_woman_tone5:": "\U0001f930\U0001f3ff", + ":pretzel:": "\U0001f968", + ":prince:": "\U0001f934", + ":prince_tone1:": "\U0001f934\U0001f3fb", + ":prince_tone2:": "\U0001f934\U0001f3fc", + ":prince_tone3:": "\U0001f934\U0001f3fd", + ":prince_tone4:": "\U0001f934\U0001f3fe", + ":prince_tone5:": "\U0001f934\U0001f3ff", + ":princess:": "\U0001f478", + ":princess_tone1:": "\U0001f478\U0001f3fb", + ":princess_tone2:": "\U0001f478\U0001f3fc", + ":princess_tone3:": "\U0001f478\U0001f3fd", + ":princess_tone4:": "\U0001f478\U0001f3fe", + ":princess_tone5:": "\U0001f478\U0001f3ff", + ":printer:": "\U0001f5a8", + ":projector:": "\U0001f4fd", + ":punch:": "\U0001f44a", + ":punch_tone1:": "\U0001f44a\U0001f3fb", + ":punch_tone2:": "\U0001f44a\U0001f3fc", + ":punch_tone3:": "\U0001f44a\U0001f3fd", + ":punch_tone4:": "\U0001f44a\U0001f3fe", + ":punch_tone5:": "\U0001f44a\U0001f3ff", + ":purple_heart:": "\U0001f49c", + ":purse:": "\U0001f45b", + ":pushpin:": "\U0001f4cc", + ":put_litter_in_its_place:": "\U0001f6ae", + ":question:": "\u2753", + ":rabbit:": "\U0001f430", + ":rabbit2:": "\U0001f407", + ":race_car:": "\U0001f3ce", + ":racehorse:": "\U0001f40e", + ":radio:": "\U0001f4fb", + ":radio_button:": "\U0001f518", + ":radioactive:": "\u2622", + ":rage:": "\U0001f621", + ":railway_car:": "\U0001f683", + ":railway_track:": "\U0001f6e4", + ":rainbow:": "\U0001f308", + ":rainbow_flag:": "\U0001f3f3\ufe0f\u200d\U0001f308", + ":raised_back_of_hand:": "\U0001f91a", + ":raised_back_of_hand_tone1:": "\U0001f91a\U0001f3fb", + ":raised_back_of_hand_tone2:": "\U0001f91a\U0001f3fc", + ":raised_back_of_hand_tone3:": "\U0001f91a\U0001f3fd", + ":raised_back_of_hand_tone4:": "\U0001f91a\U0001f3fe", + ":raised_back_of_hand_tone5:": "\U0001f91a\U0001f3ff", + ":raised_hand:": "\u270b", + ":raised_hand_tone1:": "\u270b\U0001f3fb", + ":raised_hand_tone2:": "\u270b\U0001f3fc", + ":raised_hand_tone3:": "\u270b\U0001f3fd", + ":raised_hand_tone4:": "\u270b\U0001f3fe", + ":raised_hand_tone5:": "\u270b\U0001f3ff", + ":raised_hands:": "\U0001f64c", + ":raised_hands_tone1:": "\U0001f64c\U0001f3fb", + ":raised_hands_tone2:": "\U0001f64c\U0001f3fc", + ":raised_hands_tone3:": "\U0001f64c\U0001f3fd", + ":raised_hands_tone4:": "\U0001f64c\U0001f3fe", + ":raised_hands_tone5:": "\U0001f64c\U0001f3ff", + ":ram:": "\U0001f40f", + ":ramen:": "\U0001f35c", + ":rat:": "\U0001f400", + ":record_button:": "\u23fa", + ":recycle:": "\u267b", + ":red_car:": "\U0001f697", + ":red_circle:": "\U0001f534", + ":registered:": "\u00ae", + ":relaxed:": "\u263a", + ":relieved:": "\U0001f60c", + ":reminder_ribbon:": "\U0001f397", + ":repeat:": "\U0001f501", + ":repeat_one:": "\U0001f502", + ":restroom:": "\U0001f6bb", + ":revolving_hearts:": "\U0001f49e", + ":rewind:": "\u23ea", + ":rhino:": "\U0001f98f", + ":ribbon:": "\U0001f380", + ":rice:": "\U0001f35a", + ":rice_ball:": "\U0001f359", + ":rice_cracker:": "\U0001f358", + ":rice_scene:": "\U0001f391", + ":right_facing_fist:": "\U0001f91c", + ":right_facing_fist_tone1:": "\U0001f91c\U0001f3fb", + ":right_facing_fist_tone2:": "\U0001f91c\U0001f3fc", + ":right_facing_fist_tone3:": "\U0001f91c\U0001f3fd", + ":right_facing_fist_tone4:": "\U0001f91c\U0001f3fe", + ":right_facing_fist_tone5:": "\U0001f91c\U0001f3ff", + ":ring:": "\U0001f48d", + ":robot:": "\U0001f916", + ":rocket:": "\U0001f680", + ":rofl:": "\U0001f923", + ":roller_coaster:": "\U0001f3a2", + ":rolling_eyes:": "\U0001f644", + ":rooster:": "\U0001f413", + ":rose:": "\U0001f339", + ":rosette:": "\U0001f3f5", + ":rotating_light:": "\U0001f6a8", + ":round_pushpin:": "\U0001f4cd", + ":rugby_football:": "\U0001f3c9", + ":running_shirt_with_sash:": "\U0001f3bd", + ":sa:": "\U0001f202", + ":sagittarius:": "\u2650", + ":sailboat:": "\u26f5", + ":sake:": "\U0001f376", + ":salad:": "\U0001f957", + ":sandal:": "\U0001f461", + ":sandwich:": "\U0001f96a", + ":santa:": "\U0001f385", + ":santa_tone1:": "\U0001f385\U0001f3fb", + ":santa_tone2:": "\U0001f385\U0001f3fc", + ":santa_tone3:": "\U0001f385\U0001f3fd", + ":santa_tone4:": "\U0001f385\U0001f3fe", + ":santa_tone5:": "\U0001f385\U0001f3ff", + ":satellite:": "\U0001f4e1", + ":satellite_orbital:": "\U0001f6f0", + ":sauropod:": "\U0001f995", + ":saxophone:": "\U0001f3b7", + ":scales:": "\u2696", + ":scarf:": "\U0001f9e3", + ":school:": "\U0001f3eb", + ":school_satchel:": "\U0001f392", + ":scissors:": "\u2702", + ":scooter:": "\U0001f6f4", + ":scorpion:": "\U0001f982", + ":scorpius:": "\u264f", + ":scotland:": "\U0001f3f4\U000e0067\U000e0062\U000e0073\U000e0063\U000e0074\U000e007f", + ":scream:": "\U0001f631", + ":scream_cat:": "\U0001f640", + ":scroll:": "\U0001f4dc", + ":seat:": "\U0001f4ba", + ":second_place:": "\U0001f948", + ":secret:": "\u3299", + ":see_no_evil:": "\U0001f648", + ":seedling:": "\U0001f331", + ":selfie:": "\U0001f933", + ":selfie_tone1:": "\U0001f933\U0001f3fb", + ":selfie_tone2:": "\U0001f933\U0001f3fc", + ":selfie_tone3:": "\U0001f933\U0001f3fd", + ":selfie_tone4:": "\U0001f933\U0001f3fe", + ":selfie_tone5:": "\U0001f933\U0001f3ff", + ":seven:": "7\ufe0f\u20e3", + ":shallow_pan_of_food:": "\U0001f958", + ":shamrock:": "\u2618", + ":shark:": "\U0001f988", + ":shaved_ice:": "\U0001f367", + ":sheep:": "\U0001f411", + ":shell:": "\U0001f41a", + ":shield:": "\U0001f6e1", + ":shinto_shrine:": "\u26e9", + ":ship:": "\U0001f6a2", + ":shirt:": "\U0001f455", + ":shopping_bags:": "\U0001f6cd", + ":shopping_cart:": "\U0001f6d2", + ":shower:": "\U0001f6bf", + ":shrimp:": "\U0001f990", + ":shushing_face:": "\U0001f92b", + ":signal_strength:": "\U0001f4f6", + ":six:": "6\ufe0f\u20e3", + ":six_pointed_star:": "\U0001f52f", + ":ski:": "\U0001f3bf", + ":skier:": "\u26f7", + ":skull:": "\U0001f480", + ":skull_crossbones:": "\u2620", + ":sled:": "\U0001f6f7", + ":sleeping:": "\U0001f634", + ":sleeping_accommodation:": "\U0001f6cc", + ":sleepy:": "\U0001f62a", + ":slight_frown:": "\U0001f641", + ":slight_smile:": "\U0001f642", + ":slot_machine:": "\U0001f3b0", + ":small_blue_diamond:": "\U0001f539", + ":small_orange_diamond:": "\U0001f538", + ":small_red_triangle:": "\U0001f53a", + ":small_red_triangle_down:": "\U0001f53b", + ":smile_cat:": "\U0001f638", + ":smiley:": "\U0001f603", + ":smiley_cat:": "\U0001f63a", + ":smiling_imp:": "\U0001f608", + ":smirk:": "\U0001f60f", + ":smirk_cat:": "\U0001f63c", + ":smoking:": "\U0001f6ac", + ":snail:": "\U0001f40c", + ":snake:": "\U0001f40d", + ":sneezing_face:": "\U0001f927", + ":snowboarder:": "\U0001f3c2", + ":snowboarder_tone1:": "\U0001f3c2\U0001f3fb", + ":snowboarder_tone2:": "\U0001f3c2\U0001f3fc", + ":snowboarder_tone3:": "\U0001f3c2\U0001f3fd", + ":snowboarder_tone4:": "\U0001f3c2\U0001f3fe", + ":snowboarder_tone5:": "\U0001f3c2\U0001f3ff", + ":snowflake:": "\u2744", + ":snowman:": "\u26c4", + ":snowman2:": "\u2603", + ":sob:": "\U0001f62d", + ":soccer:": "\u26bd", + ":socks:": "\U0001f9e6", + ":soon:": "\U0001f51c", + ":sos:": "\U0001f198", + ":sound:": "\U0001f509", + ":space_invader:": "\U0001f47e", + ":spades:": "\u2660", + ":spaghetti:": "\U0001f35d", + ":sparkle:": "\u2747", + ":sparkler:": "\U0001f387", + ":sparkles:": "\u2728", + ":sparkling_heart:": "\U0001f496", + ":speak_no_evil:": "\U0001f64a", + ":speaker:": "\U0001f508", + ":speaking_head:": "\U0001f5e3", + ":speech_balloon:": "\U0001f4ac", + ":speech_left:": "\U0001f5e8", + ":speedboat:": "\U0001f6a4", + ":spider:": "\U0001f577", + ":spider_web:": "\U0001f578", + ":spoon:": "\U0001f944", + ":squid:": "\U0001f991", + ":stadium:": "\U0001f3df", + ":star:": "\u2b50", + ":star2:": "\U0001f31f", + ":star_and_crescent:": "\u262a", + ":star_of_david:": "\u2721", + ":star_struck:": "\U0001f929", + ":stars:": "\U0001f320", + ":station:": "\U0001f689", + ":statue_of_liberty:": "\U0001f5fd", + ":steam_locomotive:": "\U0001f682", + ":stew:": "\U0001f372", + ":stop_button:": "\u23f9", + ":stopwatch:": "\u23f1", + ":straight_ruler:": "\U0001f4cf", + ":strawberry:": "\U0001f353", + ":stuck_out_tongue:": "\U0001f61b", + ":stuck_out_tongue_closed_eyes:": "\U0001f61d", + ":stuck_out_tongue_winking_eye:": "\U0001f61c", + ":stuffed_flatbread:": "\U0001f959", + ":sun_with_face:": "\U0001f31e", + ":sunflower:": "\U0001f33b", + ":sunglasses:": "\U0001f60e", + ":sunny:": "\u2600", + ":sunrise:": "\U0001f305", + ":sunrise_over_mountains:": "\U0001f304", + ":sushi:": "\U0001f363", + ":suspension_railway:": "\U0001f69f", + ":sweat:": "\U0001f613", + ":sweat_drops:": "\U0001f4a6", + ":sweat_smile:": "\U0001f605", + ":sweet_potato:": "\U0001f360", + ":symbols:": "\U0001f523", + ":synagogue:": "\U0001f54d", + ":syringe:": "\U0001f489", + ":t_rex:": "\U0001f996", + ":taco:": "\U0001f32e", + ":tada:": "\U0001f389", + ":takeout_box:": "\U0001f961", + ":tanabata_tree:": "\U0001f38b", + ":tangerine:": "\U0001f34a", + ":taurus:": "\u2649", + ":taxi:": "\U0001f695", + ":tea:": "\U0001f375", + ":telephone:": "\u260e", + ":telephone_receiver:": "\U0001f4de", + ":telescope:": "\U0001f52d", + ":tennis:": "\U0001f3be", + ":tent:": "\u26fa", + ":thermometer:": "\U0001f321", + ":thermometer_face:": "\U0001f912", + ":thinking:": "\U0001f914", + ":third_place:": "\U0001f949", + ":thought_balloon:": "\U0001f4ad", + ":three:": "3\ufe0f\u20e3", + ":thumbsdown:": "\U0001f44e", + ":thumbsdown_tone1:": "\U0001f44e\U0001f3fb", + ":thumbsdown_tone2:": "\U0001f44e\U0001f3fc", + ":thumbsdown_tone3:": "\U0001f44e\U0001f3fd", + ":thumbsdown_tone4:": "\U0001f44e\U0001f3fe", + ":thumbsdown_tone5:": "\U0001f44e\U0001f3ff", + ":thumbsup:": "\U0001f44d", + ":thumbsup_tone1:": "\U0001f44d\U0001f3fb", + ":thumbsup_tone2:": "\U0001f44d\U0001f3fc", + ":thumbsup_tone3:": "\U0001f44d\U0001f3fd", + ":thumbsup_tone4:": "\U0001f44d\U0001f3fe", + ":thumbsup_tone5:": "\U0001f44d\U0001f3ff", + ":thunder_cloud_rain:": "\u26c8", + ":ticket:": "\U0001f3ab", + ":tickets:": "\U0001f39f", + ":tiger:": "\U0001f42f", + ":tiger2:": "\U0001f405", + ":timer:": "\u23f2", + ":tired_face:": "\U0001f62b", + ":tm:": "\u2122", + ":toilet:": "\U0001f6bd", + ":tokyo_tower:": "\U0001f5fc", + ":tomato:": "\U0001f345", + ":tongue:": "\U0001f445", + ":tools:": "\U0001f6e0", + ":top:": "\U0001f51d", + ":tophat:": "\U0001f3a9", + ":track_next:": "\u23ed", + ":track_previous:": "\u23ee", + ":trackball:": "\U0001f5b2", + ":tractor:": "\U0001f69c", + ":traffic_light:": "\U0001f6a5", + ":train:": "\U0001f68b", + ":train2:": "\U0001f686", + ":tram:": "\U0001f68a", + ":triangular_flag_on_post:": "\U0001f6a9", + ":triangular_ruler:": "\U0001f4d0", + ":trident:": "\U0001f531", + ":triumph:": "\U0001f624", + ":trolleybus:": "\U0001f68e", + ":trophy:": "\U0001f3c6", + ":tropical_drink:": "\U0001f379", + ":tropical_fish:": "\U0001f420", + ":truck:": "\U0001f69a", + ":trumpet:": "\U0001f3ba", + ":tulip:": "\U0001f337", + ":tumbler_glass:": "\U0001f943", + ":turkey:": "\U0001f983", + ":turtle:": "\U0001f422", + ":tv:": "\U0001f4fa", + ":twisted_rightwards_arrows:": "\U0001f500", + ":two:": "2\ufe0f\u20e3", + ":two_hearts:": "\U0001f495", + ":two_men_holding_hands:": "\U0001f46c", + ":two_women_holding_hands:": "\U0001f46d", + ":u5272:": "\U0001f239", + ":u5408:": "\U0001f234", + ":u55b6:": "\U0001f23a", + ":u6307:": "\U0001f22f", + ":u6708:": "\U0001f237", + ":u6709:": "\U0001f236", + ":u6e80:": "\U0001f235", + ":u7121:": "\U0001f21a", + ":u7533:": "\U0001f238", + ":u7981:": "\U0001f232", + ":u7a7a:": "\U0001f233", + ":umbrella:": "\u2614", + ":umbrella2:": "\u2602", + ":unamused:": "\U0001f612", + ":underage:": "\U0001f51e", + ":unicorn:": "\U0001f984", + ":united_nations:": "\U0001f1fa\U0001f1f3", + ":unlock:": "\U0001f513", + ":up:": "\U0001f199", + ":upside_down:": "\U0001f643", + ":urn:": "\u26b1", + ":v:": "\u270c", + ":v_tone1:": "\u270c\U0001f3fb", + ":v_tone2:": "\u270c\U0001f3fc", + ":v_tone3:": "\u270c\U0001f3fd", + ":v_tone4:": "\u270c\U0001f3fe", + ":v_tone5:": "\u270c\U0001f3ff", + ":vampire:": "\U0001f9db", + ":vampire_tone1:": "\U0001f9db\U0001f3fb", + ":vampire_tone2:": "\U0001f9db\U0001f3fc", + ":vampire_tone3:": "\U0001f9db\U0001f3fd", + ":vampire_tone4:": "\U0001f9db\U0001f3fe", + ":vampire_tone5:": "\U0001f9db\U0001f3ff", + ":vertical_traffic_light:": "\U0001f6a6", + ":vhs:": "\U0001f4fc", + ":vibration_mode:": "\U0001f4f3", + ":video_camera:": "\U0001f4f9", + ":video_game:": "\U0001f3ae", + ":violin:": "\U0001f3bb", + ":virgo:": "\u264d", + ":volcano:": "\U0001f30b", + ":volleyball:": "\U0001f3d0", + ":vs:": "\U0001f19a", + ":vulcan:": "\U0001f596", + ":vulcan_tone1:": "\U0001f596\U0001f3fb", + ":vulcan_tone2:": "\U0001f596\U0001f3fc", + ":vulcan_tone3:": "\U0001f596\U0001f3fd", + ":vulcan_tone4:": "\U0001f596\U0001f3fe", + ":vulcan_tone5:": "\U0001f596\U0001f3ff", + ":wales:": "\U0001f3f4\U000e0067\U000e0062\U000e0077\U000e006c\U000e0073\U000e007f", + ":waning_crescent_moon:": "\U0001f318", + ":waning_gibbous_moon:": "\U0001f316", + ":warning:": "\u26a0", + ":wastebasket:": "\U0001f5d1", + ":watch:": "\u231a", + ":water_buffalo:": "\U0001f403", + ":watermelon:": "\U0001f349", + ":wave:": "\U0001f44b", + ":wave_tone1:": "\U0001f44b\U0001f3fb", + ":wave_tone2:": "\U0001f44b\U0001f3fc", + ":wave_tone3:": "\U0001f44b\U0001f3fd", + ":wave_tone4:": "\U0001f44b\U0001f3fe", + ":wave_tone5:": "\U0001f44b\U0001f3ff", + ":wavy_dash:": "\u3030", + ":waxing_crescent_moon:": "\U0001f312", + ":waxing_gibbous_moon:": "\U0001f314", + ":wc:": "\U0001f6be", + ":weary:": "\U0001f629", + ":wedding:": "\U0001f492", + ":whale:": "\U0001f433", + ":whale2:": "\U0001f40b", + ":wheel_of_dharma:": "\u2638", + ":wheelchair:": "\u267f", + ":white_check_mark:": "\u2705", + ":white_circle:": "\u26aa", + ":white_flower:": "\U0001f4ae", + ":white_large_square:": "\u2b1c", + ":white_medium_small_square:": "\u25fd", + ":white_medium_square:": "\u25fb", + ":white_small_square:": "\u25ab", + ":white_square_button:": "\U0001f533", + ":white_sun_cloud:": "\U0001f325", + ":white_sun_rain_cloud:": "\U0001f326", + ":white_sun_small_cloud:": "\U0001f324", + ":wilted_rose:": "\U0001f940", + ":wind_blowing_face:": "\U0001f32c", + ":wind_chime:": "\U0001f390", + ":wine_glass:": "\U0001f377", + ":wink:": "\U0001f609", + ":wolf:": "\U0001f43a", + ":woman:": "\U0001f469", + ":woman_artist:": "\U0001f469\u200d\U0001f3a8", + ":woman_artist_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f3a8", + ":woman_artist_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f3a8", + ":woman_artist_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f3a8", + ":woman_artist_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f3a8", + ":woman_artist_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f3a8", + ":woman_astronaut:": "\U0001f469\u200d\U0001f680", + ":woman_astronaut_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f680", + ":woman_astronaut_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f680", + ":woman_astronaut_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f680", + ":woman_astronaut_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f680", + ":woman_astronaut_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f680", + ":woman_biking:": "\U0001f6b4\u200d\u2640\ufe0f", + ":woman_biking_tone1:": "\U0001f6b4\U0001f3fb\u200d\u2640\ufe0f", + ":woman_biking_tone2:": "\U0001f6b4\U0001f3fc\u200d\u2640\ufe0f", + ":woman_biking_tone3:": "\U0001f6b4\U0001f3fd\u200d\u2640\ufe0f", + ":woman_biking_tone4:": "\U0001f6b4\U0001f3fe\u200d\u2640\ufe0f", + ":woman_biking_tone5:": "\U0001f6b4\U0001f3ff\u200d\u2640\ufe0f", + ":woman_bouncing_ball:": "\u26f9\ufe0f\u200d\u2640\ufe0f", + ":woman_bouncing_ball_tone1:": "\u26f9\U0001f3fb\u200d\u2640\ufe0f", + ":woman_bouncing_ball_tone2:": "\u26f9\U0001f3fc\u200d\u2640\ufe0f", + ":woman_bouncing_ball_tone3:": "\u26f9\U0001f3fd\u200d\u2640\ufe0f", + ":woman_bouncing_ball_tone4:": "\u26f9\U0001f3fe\u200d\u2640\ufe0f", + ":woman_bouncing_ball_tone5:": "\u26f9\U0001f3ff\u200d\u2640\ufe0f", + ":woman_bowing:": "\U0001f647\u200d\u2640\ufe0f", + ":woman_bowing_tone1:": "\U0001f647\U0001f3fb\u200d\u2640\ufe0f", + ":woman_bowing_tone2:": "\U0001f647\U0001f3fc\u200d\u2640\ufe0f", + ":woman_bowing_tone3:": "\U0001f647\U0001f3fd\u200d\u2640\ufe0f", + ":woman_bowing_tone4:": "\U0001f647\U0001f3fe\u200d\u2640\ufe0f", + ":woman_bowing_tone5:": "\U0001f647\U0001f3ff\u200d\u2640\ufe0f", + ":woman_cartwheeling:": "\U0001f938\u200d\u2640\ufe0f", + ":woman_cartwheeling_tone1:": "\U0001f938\U0001f3fb\u200d\u2640\ufe0f", + ":woman_cartwheeling_tone2:": "\U0001f938\U0001f3fc\u200d\u2640\ufe0f", + ":woman_cartwheeling_tone3:": "\U0001f938\U0001f3fd\u200d\u2640\ufe0f", + ":woman_cartwheeling_tone4:": "\U0001f938\U0001f3fe\u200d\u2640\ufe0f", + ":woman_cartwheeling_tone5:": "\U0001f938\U0001f3ff\u200d\u2640\ufe0f", + ":woman_climbing:": "\U0001f9d7\u200d\u2640\ufe0f", + ":woman_climbing_tone1:": "\U0001f9d7\U0001f3fb\u200d\u2640\ufe0f", + ":woman_climbing_tone2:": "\U0001f9d7\U0001f3fc\u200d\u2640\ufe0f", + ":woman_climbing_tone3:": "\U0001f9d7\U0001f3fd\u200d\u2640\ufe0f", + ":woman_climbing_tone4:": "\U0001f9d7\U0001f3fe\u200d\u2640\ufe0f", + ":woman_climbing_tone5:": "\U0001f9d7\U0001f3ff\u200d\u2640\ufe0f", + ":woman_construction_worker:": "\U0001f477\u200d\u2640\ufe0f", + ":woman_construction_worker_tone1:": "\U0001f477\U0001f3fb\u200d\u2640\ufe0f", + ":woman_construction_worker_tone2:": "\U0001f477\U0001f3fc\u200d\u2640\ufe0f", + ":woman_construction_worker_tone3:": "\U0001f477\U0001f3fd\u200d\u2640\ufe0f", + ":woman_construction_worker_tone4:": "\U0001f477\U0001f3fe\u200d\u2640\ufe0f", + ":woman_construction_worker_tone5:": "\U0001f477\U0001f3ff\u200d\u2640\ufe0f", + ":woman_cook:": "\U0001f469\u200d\U0001f373", + ":woman_cook_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f373", + ":woman_cook_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f373", + ":woman_cook_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f373", + ":woman_cook_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f373", + ":woman_cook_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f373", + ":woman_detective:": "\U0001f575\ufe0f\u200d\u2640\ufe0f", + ":woman_detective_tone1:": "\U0001f575\U0001f3fb\u200d\u2640\ufe0f", + ":woman_detective_tone2:": "\U0001f575\U0001f3fc\u200d\u2640\ufe0f", + ":woman_detective_tone3:": "\U0001f575\U0001f3fd\u200d\u2640\ufe0f", + ":woman_detective_tone4:": "\U0001f575\U0001f3fe\u200d\u2640\ufe0f", + ":woman_detective_tone5:": "\U0001f575\U0001f3ff\u200d\u2640\ufe0f", + ":woman_elf:": "\U0001f9dd\u200d\u2640\ufe0f", + ":woman_elf_tone1:": "\U0001f9dd\U0001f3fb\u200d\u2640\ufe0f", + ":woman_elf_tone2:": "\U0001f9dd\U0001f3fc\u200d\u2640\ufe0f", + ":woman_elf_tone3:": "\U0001f9dd\U0001f3fd\u200d\u2640\ufe0f", + ":woman_elf_tone4:": "\U0001f9dd\U0001f3fe\u200d\u2640\ufe0f", + ":woman_elf_tone5:": "\U0001f9dd\U0001f3ff\u200d\u2640\ufe0f", + ":woman_facepalming:": "\U0001f926\u200d\u2640\ufe0f", + ":woman_facepalming_tone1:": "\U0001f926\U0001f3fb\u200d\u2640\ufe0f", + ":woman_facepalming_tone2:": "\U0001f926\U0001f3fc\u200d\u2640\ufe0f", + ":woman_facepalming_tone3:": "\U0001f926\U0001f3fd\u200d\u2640\ufe0f", + ":woman_facepalming_tone4:": "\U0001f926\U0001f3fe\u200d\u2640\ufe0f", + ":woman_facepalming_tone5:": "\U0001f926\U0001f3ff\u200d\u2640\ufe0f", + ":woman_factory_worker:": "\U0001f469\u200d\U0001f3ed", + ":woman_factory_worker_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f3ed", + ":woman_factory_worker_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f3ed", + ":woman_factory_worker_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f3ed", + ":woman_factory_worker_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f3ed", + ":woman_factory_worker_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f3ed", + ":woman_fairy:": "\U0001f9da\u200d\u2640\ufe0f", + ":woman_fairy_tone1:": "\U0001f9da\U0001f3fb\u200d\u2640\ufe0f", + ":woman_fairy_tone2:": "\U0001f9da\U0001f3fc\u200d\u2640\ufe0f", + ":woman_fairy_tone3:": "\U0001f9da\U0001f3fd\u200d\u2640\ufe0f", + ":woman_fairy_tone4:": "\U0001f9da\U0001f3fe\u200d\u2640\ufe0f", + ":woman_fairy_tone5:": "\U0001f9da\U0001f3ff\u200d\u2640\ufe0f", + ":woman_farmer:": "\U0001f469\u200d\U0001f33e", + ":woman_farmer_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f33e", + ":woman_farmer_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f33e", + ":woman_farmer_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f33e", + ":woman_farmer_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f33e", + ":woman_farmer_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f33e", + ":woman_firefighter:": "\U0001f469\u200d\U0001f692", + ":woman_firefighter_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f692", + ":woman_firefighter_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f692", + ":woman_firefighter_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f692", + ":woman_firefighter_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f692", + ":woman_firefighter_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f692", + ":woman_frowning:": "\U0001f64d\u200d\u2640\ufe0f", + ":woman_frowning_tone1:": "\U0001f64d\U0001f3fb\u200d\u2640\ufe0f", + ":woman_frowning_tone2:": "\U0001f64d\U0001f3fc\u200d\u2640\ufe0f", + ":woman_frowning_tone3:": "\U0001f64d\U0001f3fd\u200d\u2640\ufe0f", + ":woman_frowning_tone4:": "\U0001f64d\U0001f3fe\u200d\u2640\ufe0f", + ":woman_frowning_tone5:": "\U0001f64d\U0001f3ff\u200d\u2640\ufe0f", + ":woman_genie:": "\U0001f9de\u200d\u2640\ufe0f", + ":woman_gesturing_no:": "\U0001f645\u200d\u2640\ufe0f", + ":woman_gesturing_no_tone1:": "\U0001f645\U0001f3fb\u200d\u2640\ufe0f", + ":woman_gesturing_no_tone2:": "\U0001f645\U0001f3fc\u200d\u2640\ufe0f", + ":woman_gesturing_no_tone3:": "\U0001f645\U0001f3fd\u200d\u2640\ufe0f", + ":woman_gesturing_no_tone4:": "\U0001f645\U0001f3fe\u200d\u2640\ufe0f", + ":woman_gesturing_no_tone5:": "\U0001f645\U0001f3ff\u200d\u2640\ufe0f", + ":woman_gesturing_ok:": "\U0001f646\u200d\u2640\ufe0f", + ":woman_gesturing_ok_tone1:": "\U0001f646\U0001f3fb\u200d\u2640\ufe0f", + ":woman_gesturing_ok_tone2:": "\U0001f646\U0001f3fc\u200d\u2640\ufe0f", + ":woman_gesturing_ok_tone3:": "\U0001f646\U0001f3fd\u200d\u2640\ufe0f", + ":woman_gesturing_ok_tone4:": "\U0001f646\U0001f3fe\u200d\u2640\ufe0f", + ":woman_gesturing_ok_tone5:": "\U0001f646\U0001f3ff\u200d\u2640\ufe0f", + ":woman_getting_face_massage:": "\U0001f486\u200d\u2640\ufe0f", + ":woman_getting_face_massage_tone1:": "\U0001f486\U0001f3fb\u200d\u2640\ufe0f", + ":woman_getting_face_massage_tone2:": "\U0001f486\U0001f3fc\u200d\u2640\ufe0f", + ":woman_getting_face_massage_tone3:": "\U0001f486\U0001f3fd\u200d\u2640\ufe0f", + ":woman_getting_face_massage_tone4:": "\U0001f486\U0001f3fe\u200d\u2640\ufe0f", + ":woman_getting_face_massage_tone5:": "\U0001f486\U0001f3ff\u200d\u2640\ufe0f", + ":woman_getting_haircut:": "\U0001f487\u200d\u2640\ufe0f", + ":woman_getting_haircut_tone1:": "\U0001f487\U0001f3fb\u200d\u2640\ufe0f", + ":woman_getting_haircut_tone2:": "\U0001f487\U0001f3fc\u200d\u2640\ufe0f", + ":woman_getting_haircut_tone3:": "\U0001f487\U0001f3fd\u200d\u2640\ufe0f", + ":woman_getting_haircut_tone4:": "\U0001f487\U0001f3fe\u200d\u2640\ufe0f", + ":woman_getting_haircut_tone5:": "\U0001f487\U0001f3ff\u200d\u2640\ufe0f", + ":woman_golfing:": "\U0001f3cc\ufe0f\u200d\u2640\ufe0f", + ":woman_golfing_tone1:": "\U0001f3cc\U0001f3fb\u200d\u2640\ufe0f", + ":woman_golfing_tone2:": "\U0001f3cc\U0001f3fc\u200d\u2640\ufe0f", + ":woman_golfing_tone3:": "\U0001f3cc\U0001f3fd\u200d\u2640\ufe0f", + ":woman_golfing_tone4:": "\U0001f3cc\U0001f3fe\u200d\u2640\ufe0f", + ":woman_golfing_tone5:": "\U0001f3cc\U0001f3ff\u200d\u2640\ufe0f", + ":woman_guard:": "\U0001f482\u200d\u2640\ufe0f", + ":woman_guard_tone1:": "\U0001f482\U0001f3fb\u200d\u2640\ufe0f", + ":woman_guard_tone2:": "\U0001f482\U0001f3fc\u200d\u2640\ufe0f", + ":woman_guard_tone3:": "\U0001f482\U0001f3fd\u200d\u2640\ufe0f", + ":woman_guard_tone4:": "\U0001f482\U0001f3fe\u200d\u2640\ufe0f", + ":woman_guard_tone5:": "\U0001f482\U0001f3ff\u200d\u2640\ufe0f", + ":woman_health_worker:": "\U0001f469\u200d\u2695\ufe0f", + ":woman_health_worker_tone1:": "\U0001f469\U0001f3fb\u200d\u2695\ufe0f", + ":woman_health_worker_tone2:": "\U0001f469\U0001f3fc\u200d\u2695\ufe0f", + ":woman_health_worker_tone3:": "\U0001f469\U0001f3fd\u200d\u2695\ufe0f", + ":woman_health_worker_tone4:": "\U0001f469\U0001f3fe\u200d\u2695\ufe0f", + ":woman_health_worker_tone5:": "\U0001f469\U0001f3ff\u200d\u2695\ufe0f", + ":woman_in_lotus_position:": "\U0001f9d8\u200d\u2640\ufe0f", + ":woman_in_lotus_position_tone1:": "\U0001f9d8\U0001f3fb\u200d\u2640\ufe0f", + ":woman_in_lotus_position_tone2:": "\U0001f9d8\U0001f3fc\u200d\u2640\ufe0f", + ":woman_in_lotus_position_tone3:": "\U0001f9d8\U0001f3fd\u200d\u2640\ufe0f", + ":woman_in_lotus_position_tone4:": "\U0001f9d8\U0001f3fe\u200d\u2640\ufe0f", + ":woman_in_lotus_position_tone5:": "\U0001f9d8\U0001f3ff\u200d\u2640\ufe0f", + ":woman_in_steamy_room:": "\U0001f9d6\u200d\u2640\ufe0f", + ":woman_in_steamy_room_tone1:": "\U0001f9d6\U0001f3fb\u200d\u2640\ufe0f", + ":woman_in_steamy_room_tone2:": "\U0001f9d6\U0001f3fc\u200d\u2640\ufe0f", + ":woman_in_steamy_room_tone3:": "\U0001f9d6\U0001f3fd\u200d\u2640\ufe0f", + ":woman_in_steamy_room_tone4:": "\U0001f9d6\U0001f3fe\u200d\u2640\ufe0f", + ":woman_in_steamy_room_tone5:": "\U0001f9d6\U0001f3ff\u200d\u2640\ufe0f", + ":woman_judge:": "\U0001f469\u200d\u2696\ufe0f", + ":woman_judge_tone1:": "\U0001f469\U0001f3fb\u200d\u2696\ufe0f", + ":woman_judge_tone2:": "\U0001f469\U0001f3fc\u200d\u2696\ufe0f", + ":woman_judge_tone3:": "\U0001f469\U0001f3fd\u200d\u2696\ufe0f", + ":woman_judge_tone4:": "\U0001f469\U0001f3fe\u200d\u2696\ufe0f", + ":woman_judge_tone5:": "\U0001f469\U0001f3ff\u200d\u2696\ufe0f", + ":woman_juggling:": "\U0001f939\u200d\u2640\ufe0f", + ":woman_juggling_tone1:": "\U0001f939\U0001f3fb\u200d\u2640\ufe0f", + ":woman_juggling_tone2:": "\U0001f939\U0001f3fc\u200d\u2640\ufe0f", + ":woman_juggling_tone3:": "\U0001f939\U0001f3fd\u200d\u2640\ufe0f", + ":woman_juggling_tone4:": "\U0001f939\U0001f3fe\u200d\u2640\ufe0f", + ":woman_juggling_tone5:": "\U0001f939\U0001f3ff\u200d\u2640\ufe0f", + ":woman_lifting_weights:": "\U0001f3cb\ufe0f\u200d\u2640\ufe0f", + ":woman_lifting_weights_tone1:": "\U0001f3cb\U0001f3fb\u200d\u2640\ufe0f", + ":woman_lifting_weights_tone2:": "\U0001f3cb\U0001f3fc\u200d\u2640\ufe0f", + ":woman_lifting_weights_tone3:": "\U0001f3cb\U0001f3fd\u200d\u2640\ufe0f", + ":woman_lifting_weights_tone4:": "\U0001f3cb\U0001f3fe\u200d\u2640\ufe0f", + ":woman_lifting_weights_tone5:": "\U0001f3cb\U0001f3ff\u200d\u2640\ufe0f", + ":woman_mage:": "\U0001f9d9\u200d\u2640\ufe0f", + ":woman_mage_tone1:": "\U0001f9d9\U0001f3fb\u200d\u2640\ufe0f", + ":woman_mage_tone2:": "\U0001f9d9\U0001f3fc\u200d\u2640\ufe0f", + ":woman_mage_tone3:": "\U0001f9d9\U0001f3fd\u200d\u2640\ufe0f", + ":woman_mage_tone4:": "\U0001f9d9\U0001f3fe\u200d\u2640\ufe0f", + ":woman_mage_tone5:": "\U0001f9d9\U0001f3ff\u200d\u2640\ufe0f", + ":woman_mechanic:": "\U0001f469\u200d\U0001f527", + ":woman_mechanic_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f527", + ":woman_mechanic_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f527", + ":woman_mechanic_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f527", + ":woman_mechanic_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f527", + ":woman_mechanic_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f527", + ":woman_mountain_biking:": "\U0001f6b5\u200d\u2640\ufe0f", + ":woman_mountain_biking_tone1:": "\U0001f6b5\U0001f3fb\u200d\u2640\ufe0f", + ":woman_mountain_biking_tone2:": "\U0001f6b5\U0001f3fc\u200d\u2640\ufe0f", + ":woman_mountain_biking_tone3:": "\U0001f6b5\U0001f3fd\u200d\u2640\ufe0f", + ":woman_mountain_biking_tone4:": "\U0001f6b5\U0001f3fe\u200d\u2640\ufe0f", + ":woman_mountain_biking_tone5:": "\U0001f6b5\U0001f3ff\u200d\u2640\ufe0f", + ":woman_office_worker:": "\U0001f469\u200d\U0001f4bc", + ":woman_office_worker_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f4bc", + ":woman_office_worker_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f4bc", + ":woman_office_worker_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f4bc", + ":woman_office_worker_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f4bc", + ":woman_office_worker_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f4bc", + ":woman_pilot:": "\U0001f469\u200d\u2708\ufe0f", + ":woman_pilot_tone1:": "\U0001f469\U0001f3fb\u200d\u2708\ufe0f", + ":woman_pilot_tone2:": "\U0001f469\U0001f3fc\u200d\u2708\ufe0f", + ":woman_pilot_tone3:": "\U0001f469\U0001f3fd\u200d\u2708\ufe0f", + ":woman_pilot_tone4:": "\U0001f469\U0001f3fe\u200d\u2708\ufe0f", + ":woman_pilot_tone5:": "\U0001f469\U0001f3ff\u200d\u2708\ufe0f", + ":woman_playing_handball:": "\U0001f93e\u200d\u2640\ufe0f", + ":woman_playing_handball_tone1:": "\U0001f93e\U0001f3fb\u200d\u2640\ufe0f", + ":woman_playing_handball_tone2:": "\U0001f93e\U0001f3fc\u200d\u2640\ufe0f", + ":woman_playing_handball_tone3:": "\U0001f93e\U0001f3fd\u200d\u2640\ufe0f", + ":woman_playing_handball_tone4:": "\U0001f93e\U0001f3fe\u200d\u2640\ufe0f", + ":woman_playing_handball_tone5:": "\U0001f93e\U0001f3ff\u200d\u2640\ufe0f", + ":woman_playing_water_polo:": "\U0001f93d\u200d\u2640\ufe0f", + ":woman_playing_water_polo_tone1:": "\U0001f93d\U0001f3fb\u200d\u2640\ufe0f", + ":woman_playing_water_polo_tone2:": "\U0001f93d\U0001f3fc\u200d\u2640\ufe0f", + ":woman_playing_water_polo_tone3:": "\U0001f93d\U0001f3fd\u200d\u2640\ufe0f", + ":woman_playing_water_polo_tone4:": "\U0001f93d\U0001f3fe\u200d\u2640\ufe0f", + ":woman_playing_water_polo_tone5:": "\U0001f93d\U0001f3ff\u200d\u2640\ufe0f", + ":woman_police_officer:": "\U0001f46e\u200d\u2640\ufe0f", + ":woman_police_officer_tone1:": "\U0001f46e\U0001f3fb\u200d\u2640\ufe0f", + ":woman_police_officer_tone2:": "\U0001f46e\U0001f3fc\u200d\u2640\ufe0f", + ":woman_police_officer_tone3:": "\U0001f46e\U0001f3fd\u200d\u2640\ufe0f", + ":woman_police_officer_tone4:": "\U0001f46e\U0001f3fe\u200d\u2640\ufe0f", + ":woman_police_officer_tone5:": "\U0001f46e\U0001f3ff\u200d\u2640\ufe0f", + ":woman_pouting:": "\U0001f64e\u200d\u2640\ufe0f", + ":woman_pouting_tone1:": "\U0001f64e\U0001f3fb\u200d\u2640\ufe0f", + ":woman_pouting_tone2:": "\U0001f64e\U0001f3fc\u200d\u2640\ufe0f", + ":woman_pouting_tone3:": "\U0001f64e\U0001f3fd\u200d\u2640\ufe0f", + ":woman_pouting_tone4:": "\U0001f64e\U0001f3fe\u200d\u2640\ufe0f", + ":woman_pouting_tone5:": "\U0001f64e\U0001f3ff\u200d\u2640\ufe0f", + ":woman_raising_hand:": "\U0001f64b\u200d\u2640\ufe0f", + ":woman_raising_hand_tone1:": "\U0001f64b\U0001f3fb\u200d\u2640\ufe0f", + ":woman_raising_hand_tone2:": "\U0001f64b\U0001f3fc\u200d\u2640\ufe0f", + ":woman_raising_hand_tone3:": "\U0001f64b\U0001f3fd\u200d\u2640\ufe0f", + ":woman_raising_hand_tone4:": "\U0001f64b\U0001f3fe\u200d\u2640\ufe0f", + ":woman_raising_hand_tone5:": "\U0001f64b\U0001f3ff\u200d\u2640\ufe0f", + ":woman_rowing_boat:": "\U0001f6a3\u200d\u2640\ufe0f", + ":woman_rowing_boat_tone1:": "\U0001f6a3\U0001f3fb\u200d\u2640\ufe0f", + ":woman_rowing_boat_tone2:": "\U0001f6a3\U0001f3fc\u200d\u2640\ufe0f", + ":woman_rowing_boat_tone3:": "\U0001f6a3\U0001f3fd\u200d\u2640\ufe0f", + ":woman_rowing_boat_tone4:": "\U0001f6a3\U0001f3fe\u200d\u2640\ufe0f", + ":woman_rowing_boat_tone5:": "\U0001f6a3\U0001f3ff\u200d\u2640\ufe0f", + ":woman_running:": "\U0001f3c3\u200d\u2640\ufe0f", + ":woman_running_tone1:": "\U0001f3c3\U0001f3fb\u200d\u2640\ufe0f", + ":woman_running_tone2:": "\U0001f3c3\U0001f3fc\u200d\u2640\ufe0f", + ":woman_running_tone3:": "\U0001f3c3\U0001f3fd\u200d\u2640\ufe0f", + ":woman_running_tone4:": "\U0001f3c3\U0001f3fe\u200d\u2640\ufe0f", + ":woman_running_tone5:": "\U0001f3c3\U0001f3ff\u200d\u2640\ufe0f", + ":woman_scientist:": "\U0001f469\u200d\U0001f52c", + ":woman_scientist_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f52c", + ":woman_scientist_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f52c", + ":woman_scientist_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f52c", + ":woman_scientist_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f52c", + ":woman_scientist_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f52c", + ":woman_shrugging:": "\U0001f937\u200d\u2640\ufe0f", + ":woman_shrugging_tone1:": "\U0001f937\U0001f3fb\u200d\u2640\ufe0f", + ":woman_shrugging_tone2:": "\U0001f937\U0001f3fc\u200d\u2640\ufe0f", + ":woman_shrugging_tone3:": "\U0001f937\U0001f3fd\u200d\u2640\ufe0f", + ":woman_shrugging_tone4:": "\U0001f937\U0001f3fe\u200d\u2640\ufe0f", + ":woman_shrugging_tone5:": "\U0001f937\U0001f3ff\u200d\u2640\ufe0f", + ":woman_singer:": "\U0001f469\u200d\U0001f3a4", + ":woman_singer_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f3a4", + ":woman_singer_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f3a4", + ":woman_singer_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f3a4", + ":woman_singer_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f3a4", + ":woman_singer_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f3a4", + ":woman_student:": "\U0001f469\u200d\U0001f393", + ":woman_student_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f393", + ":woman_student_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f393", + ":woman_student_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f393", + ":woman_student_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f393", + ":woman_student_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f393", + ":woman_surfing:": "\U0001f3c4\u200d\u2640\ufe0f", + ":woman_surfing_tone1:": "\U0001f3c4\U0001f3fb\u200d\u2640\ufe0f", + ":woman_surfing_tone2:": "\U0001f3c4\U0001f3fc\u200d\u2640\ufe0f", + ":woman_surfing_tone3:": "\U0001f3c4\U0001f3fd\u200d\u2640\ufe0f", + ":woman_surfing_tone4:": "\U0001f3c4\U0001f3fe\u200d\u2640\ufe0f", + ":woman_surfing_tone5:": "\U0001f3c4\U0001f3ff\u200d\u2640\ufe0f", + ":woman_swimming:": "\U0001f3ca\u200d\u2640\ufe0f", + ":woman_swimming_tone1:": "\U0001f3ca\U0001f3fb\u200d\u2640\ufe0f", + ":woman_swimming_tone2:": "\U0001f3ca\U0001f3fc\u200d\u2640\ufe0f", + ":woman_swimming_tone3:": "\U0001f3ca\U0001f3fd\u200d\u2640\ufe0f", + ":woman_swimming_tone4:": "\U0001f3ca\U0001f3fe\u200d\u2640\ufe0f", + ":woman_swimming_tone5:": "\U0001f3ca\U0001f3ff\u200d\u2640\ufe0f", + ":woman_teacher:": "\U0001f469\u200d\U0001f3eb", + ":woman_teacher_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f3eb", + ":woman_teacher_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f3eb", + ":woman_teacher_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f3eb", + ":woman_teacher_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f3eb", + ":woman_teacher_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f3eb", + ":woman_technologist:": "\U0001f469\u200d\U0001f4bb", + ":woman_technologist_tone1:": "\U0001f469\U0001f3fb\u200d\U0001f4bb", + ":woman_technologist_tone2:": "\U0001f469\U0001f3fc\u200d\U0001f4bb", + ":woman_technologist_tone3:": "\U0001f469\U0001f3fd\u200d\U0001f4bb", + ":woman_technologist_tone4:": "\U0001f469\U0001f3fe\u200d\U0001f4bb", + ":woman_technologist_tone5:": "\U0001f469\U0001f3ff\u200d\U0001f4bb", + ":woman_tipping_hand:": "\U0001f481\u200d\u2640\ufe0f", + ":woman_tipping_hand_tone1:": "\U0001f481\U0001f3fb\u200d\u2640\ufe0f", + ":woman_tipping_hand_tone2:": "\U0001f481\U0001f3fc\u200d\u2640\ufe0f", + ":woman_tipping_hand_tone3:": "\U0001f481\U0001f3fd\u200d\u2640\ufe0f", + ":woman_tipping_hand_tone4:": "\U0001f481\U0001f3fe\u200d\u2640\ufe0f", + ":woman_tipping_hand_tone5:": "\U0001f481\U0001f3ff\u200d\u2640\ufe0f", + ":woman_tone1:": "\U0001f469\U0001f3fb", + ":woman_tone2:": "\U0001f469\U0001f3fc", + ":woman_tone3:": "\U0001f469\U0001f3fd", + ":woman_tone4:": "\U0001f469\U0001f3fe", + ":woman_tone5:": "\U0001f469\U0001f3ff", + ":woman_vampire:": "\U0001f9db\u200d\u2640\ufe0f", + ":woman_vampire_tone1:": "\U0001f9db\U0001f3fb\u200d\u2640\ufe0f", + ":woman_vampire_tone2:": "\U0001f9db\U0001f3fc\u200d\u2640\ufe0f", + ":woman_vampire_tone3:": "\U0001f9db\U0001f3fd\u200d\u2640\ufe0f", + ":woman_vampire_tone4:": "\U0001f9db\U0001f3fe\u200d\u2640\ufe0f", + ":woman_vampire_tone5:": "\U0001f9db\U0001f3ff\u200d\u2640\ufe0f", + ":woman_walking:": "\U0001f6b6\u200d\u2640\ufe0f", + ":woman_walking_tone1:": "\U0001f6b6\U0001f3fb\u200d\u2640\ufe0f", + ":woman_walking_tone2:": "\U0001f6b6\U0001f3fc\u200d\u2640\ufe0f", + ":woman_walking_tone3:": "\U0001f6b6\U0001f3fd\u200d\u2640\ufe0f", + ":woman_walking_tone4:": "\U0001f6b6\U0001f3fe\u200d\u2640\ufe0f", + ":woman_walking_tone5:": "\U0001f6b6\U0001f3ff\u200d\u2640\ufe0f", + ":woman_wearing_turban:": "\U0001f473\u200d\u2640\ufe0f", + ":woman_wearing_turban_tone1:": "\U0001f473\U0001f3fb\u200d\u2640\ufe0f", + ":woman_wearing_turban_tone2:": "\U0001f473\U0001f3fc\u200d\u2640\ufe0f", + ":woman_wearing_turban_tone3:": "\U0001f473\U0001f3fd\u200d\u2640\ufe0f", + ":woman_wearing_turban_tone4:": "\U0001f473\U0001f3fe\u200d\u2640\ufe0f", + ":woman_wearing_turban_tone5:": "\U0001f473\U0001f3ff\u200d\u2640\ufe0f", + ":woman_with_headscarf:": "\U0001f9d5", + ":woman_with_headscarf_tone1:": "\U0001f9d5\U0001f3fb", + ":woman_with_headscarf_tone2:": "\U0001f9d5\U0001f3fc", + ":woman_with_headscarf_tone3:": "\U0001f9d5\U0001f3fd", + ":woman_with_headscarf_tone4:": "\U0001f9d5\U0001f3fe", + ":woman_with_headscarf_tone5:": "\U0001f9d5\U0001f3ff", + ":woman_zombie:": "\U0001f9df\u200d\u2640\ufe0f", + ":womans_clothes:": "\U0001f45a", + ":womans_hat:": "\U0001f452", + ":women_with_bunny_ears_partying:": "\U0001f46f\u200d\u2640\ufe0f", + ":women_wrestling:": "\U0001f93c\u200d\u2640\ufe0f", + ":womens:": "\U0001f6ba", + ":worried:": "\U0001f61f", + ":wrench:": "\U0001f527", + ":writing_hand:": "\u270d", + ":writing_hand_tone1:": "\u270d\U0001f3fb", + ":writing_hand_tone2:": "\u270d\U0001f3fc", + ":writing_hand_tone3:": "\u270d\U0001f3fd", + ":writing_hand_tone4:": "\u270d\U0001f3fe", + ":writing_hand_tone5:": "\u270d\U0001f3ff", + ":x:": "\u274c", + ":yellow_heart:": "\U0001f49b", + ":yen:": "\U0001f4b4", + ":yin_yang:": "\u262f", + ":yum:": "\U0001f60b", + ":zap:": "\u26a1", + ":zebra:": "\U0001f993", + ":zero:": "0\ufe0f\u20e3", + ":zipper_mouth:": "\U0001f910", + ":zombie:": "\U0001f9df", + ":zzz:": "\U0001f4a4", +} diff --git a/endpoints.go b/endpoints.go index b96190897..e32d50477 100644 --- a/endpoints.go +++ b/endpoints.go @@ -61,74 +61,80 @@ var ( EndpointIntegrations = EndpointAPI + "integrations" EndpointUser = func(uID string) string { return EndpointUsers + uID } - EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } - EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" } - EndpointDefaultUserAvatar = func(uDiscriminator string) string { + EndpointUserAvatar = func(uID int64, aID string) string { return EndpointCDNAvatars + StrID(uID) + "/" + aID + ".png" } + EndpointUserAvatarAnimated = func(uID int64, aID string) string { return EndpointCDNAvatars + StrID(uID) + "/" + aID + ".gif" } + EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } + EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } + EndpointUserGuild = func(uID string, gID int64) string { return EndpointUsers + uID + "/guilds/" + StrID(gID) } + EndpointUserGuildSettings = func(uID string, gID int64) string { return EndpointUsers + uID + "/guilds/" + StrID(gID) + "/settings" } + EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } + EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } + EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } + EndpointUserNotes = func(uID int64) string { return EndpointUsers + "@me/notes/" + StrID(uID) } + + EndpointGuild = func(gID int64) string { return EndpointGuilds + StrID(gID) } + EndpointGuildChannels = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/channels" } + EndpointGuildMembers = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/members" } + EndpointGuildMember = func(gID int64, uID int64) string { return EndpointGuilds + StrID(gID) + "/members/" + StrID(uID) } + EndpointGuildMemberMe = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/members/@me" } + EndpointGuildMemberRole = func(gID, uID, rID int64) string { + return EndpointGuilds + StrID(gID) + "/members/" + StrID(uID) + "/roles/" + StrID(rID) + } + EndpointGuildBans = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/bans" } + EndpointGuildBan = func(gID, uID int64) string { return EndpointGuilds + StrID(gID) + "/bans/" + StrID(uID) } + EndpointGuildIntegrations = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/integrations" } + EndpointGuildIntegration = func(gID, iID int64) string { return EndpointGuilds + StrID(gID) + "/integrations/" + StrID(iID) } + EndpointGuildIntegrationSync = func(gID, iID int64) string { + return EndpointGuilds + StrID(gID) + "/integrations/" + StrID(iID) + "/sync" + } + EndpointGuildRoles = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/roles" } + EndpointGuildRole = func(gID, rID int64) string { return EndpointGuilds + StrID(gID) + "/roles/" + StrID(rID) } + EndpointGuildInvites = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/invites" } + EndpointGuildEmbed = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/embed" } + EndpointGuildPrune = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/prune" } + EndpointGuildIcon = func(gID int64, hash string) string { return EndpointCDNIcons + StrID(gID) + "/" + hash + ".png" } + EndpointGuildSplash = func(gID int64, hash string) string { return EndpointCDNSplashes + StrID(gID) + "/" + hash + ".png" } + EndpointGuildWebhooks = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/webhooks" } + EndpointGuildAuditLogs = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/audit-logs" } + EndpointGuildEmojis = func(gID int64) string { return EndpointGuilds + StrID(gID) + "/emojis" } + EndpointGuildEmoji = func(gID, eID int64) string { return EndpointGuilds + StrID(gID) + "/emojis/" + StrID(eID) } + + EndpointChannel = func(cID int64) string { return EndpointChannels + StrID(cID) } + EndpointChannelPermissions = func(cID int64) string { return EndpointChannels + StrID(cID) + "/permissions" } + EndpointChannelPermission = func(cID, tID int64) string { return EndpointChannels + StrID(cID) + "/permissions/" + StrID(tID) } + EndpointChannelInvites = func(cID int64) string { return EndpointChannels + StrID(cID) + "/invites" } + EndpointChannelTyping = func(cID int64) string { return EndpointChannels + StrID(cID) + "/typing" } + EndpointChannelMessages = func(cID int64) string { return EndpointChannels + StrID(cID) + "/messages" } + EndpointChannelMessage = func(cID, mID int64) string { return EndpointChannels + StrID(cID) + "/messages/" + StrID(mID) } + EndpointChannelMessageAck = func(cID, mID int64) string { return EndpointChannels + StrID(cID) + "/messages/" + StrID(mID) + "/ack" } + EndpointChannelMessagesBulkDelete = func(cID int64) string { return EndpointChannel(cID) + "/messages/bulk-delete" } + EndpointChannelMessagesPins = func(cID int64) string { return EndpointChannel(cID) + "/pins" } + EndpointChannelMessagePin = func(cID, mID int64) string { return EndpointChannel(cID) + "/pins/" + StrID(mID) } + + EndpointGroupIcon = func(cID int64, hash string) string { return EndpointCDNChannelIcons + StrID(cID) + "/" + hash + ".png" } + + EndpointChannelWebhooks = func(cID int64) string { return EndpointChannel(cID) + "/webhooks" } + EndpointWebhook = func(wID int64) string { return EndpointWebhooks + StrID(wID) } + EndpointWebhookToken = func(wID int64, token string) string { return EndpointWebhooks + StrID(wID) + "/" + token } + + EndpointDefaultUserAvatar = func(uDiscriminator string) string { uDiscriminatorInt, _ := strconv.Atoi(uDiscriminator) return EndpointCDN + "embed/avatars/" + strconv.Itoa(uDiscriminatorInt%5) + ".png" } - EndpointUserSettings = func(uID string) string { return EndpointUsers + uID + "/settings" } - EndpointUserGuilds = func(uID string) string { return EndpointUsers + uID + "/guilds" } - EndpointUserGuild = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID } - EndpointUserGuildSettings = func(uID, gID string) string { return EndpointUsers + uID + "/guilds/" + gID + "/settings" } - EndpointUserChannels = func(uID string) string { return EndpointUsers + uID + "/channels" } - EndpointUserDevices = func(uID string) string { return EndpointUsers + uID + "/devices" } - EndpointUserConnections = func(uID string) string { return EndpointUsers + uID + "/connections" } - EndpointUserNotes = func(uID string) string { return EndpointUsers + "@me/notes/" + uID } - - EndpointGuild = func(gID string) string { return EndpointGuilds + gID } - EndpointGuildChannels = func(gID string) string { return EndpointGuilds + gID + "/channels" } - EndpointGuildMembers = func(gID string) string { return EndpointGuilds + gID + "/members" } - EndpointGuildMember = func(gID, uID string) string { return EndpointGuilds + gID + "/members/" + uID } - EndpointGuildMemberRole = func(gID, uID, rID string) string { return EndpointGuilds + gID + "/members/" + uID + "/roles/" + rID } - EndpointGuildBans = func(gID string) string { return EndpointGuilds + gID + "/bans" } - EndpointGuildBan = func(gID, uID string) string { return EndpointGuilds + gID + "/bans/" + uID } - EndpointGuildIntegrations = func(gID string) string { return EndpointGuilds + gID + "/integrations" } - EndpointGuildIntegration = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID } - EndpointGuildIntegrationSync = func(gID, iID string) string { return EndpointGuilds + gID + "/integrations/" + iID + "/sync" } - EndpointGuildRoles = func(gID string) string { return EndpointGuilds + gID + "/roles" } - EndpointGuildRole = func(gID, rID string) string { return EndpointGuilds + gID + "/roles/" + rID } - EndpointGuildInvites = func(gID string) string { return EndpointGuilds + gID + "/invites" } - EndpointGuildEmbed = func(gID string) string { return EndpointGuilds + gID + "/embed" } - EndpointGuildPrune = func(gID string) string { return EndpointGuilds + gID + "/prune" } - EndpointGuildIcon = func(gID, hash string) string { return EndpointCDNIcons + gID + "/" + hash + ".png" } - EndpointGuildSplash = func(gID, hash string) string { return EndpointCDNSplashes + gID + "/" + hash + ".png" } - EndpointGuildWebhooks = func(gID string) string { return EndpointGuilds + gID + "/webhooks" } - EndpointGuildAuditLogs = func(gID string) string { return EndpointGuilds + gID + "/audit-logs" } - EndpointGuildEmojis = func(gID string) string { return EndpointGuilds + gID + "/emojis" } - EndpointGuildEmoji = func(gID, eID string) string { return EndpointGuilds + gID + "/emojis/" + eID } - - EndpointChannel = func(cID string) string { return EndpointChannels + cID } - EndpointChannelPermissions = func(cID string) string { return EndpointChannels + cID + "/permissions" } - EndpointChannelPermission = func(cID, tID string) string { return EndpointChannels + cID + "/permissions/" + tID } - EndpointChannelInvites = func(cID string) string { return EndpointChannels + cID + "/invites" } - EndpointChannelTyping = func(cID string) string { return EndpointChannels + cID + "/typing" } - EndpointChannelMessages = func(cID string) string { return EndpointChannels + cID + "/messages" } - EndpointChannelMessage = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID } - EndpointChannelMessageAck = func(cID, mID string) string { return EndpointChannels + cID + "/messages/" + mID + "/ack" } - EndpointChannelMessagesBulkDelete = func(cID string) string { return EndpointChannel(cID) + "/messages/bulk-delete" } - EndpointChannelMessagesPins = func(cID string) string { return EndpointChannel(cID) + "/pins" } - EndpointChannelMessagePin = func(cID, mID string) string { return EndpointChannel(cID) + "/pins/" + mID } - - EndpointGroupIcon = func(cID, hash string) string { return EndpointCDNChannelIcons + cID + "/" + hash + ".png" } - - EndpointChannelWebhooks = func(cID string) string { return EndpointChannel(cID) + "/webhooks" } - EndpointWebhook = func(wID string) string { return EndpointWebhooks + wID } - EndpointWebhookToken = func(wID, token string) string { return EndpointWebhooks + wID + "/" + token } - - EndpointMessageReactionsAll = func(cID, mID string) string { + + EndpointMessageReactionsAll = func(cID, mID int64) string { return EndpointChannelMessage(cID, mID) + "/reactions" } - EndpointMessageReactions = func(cID, mID, eID string) string { - return EndpointChannelMessage(cID, mID) + "/reactions/" + eID + EndpointMessageReactions = func(cID, mID int64, emoji EmojiName) string { + return EndpointChannelMessage(cID, mID) + "/reactions/" + emoji.String() } - EndpointMessageReaction = func(cID, mID, eID, uID string) string { - return EndpointMessageReactions(cID, mID, eID) + "/" + uID + EndpointMessageReaction = func(cID, mID int64, emoji EmojiName, uID string) string { + return EndpointMessageReactions(cID, mID, emoji) + "/" + uID } EndpointRelationships = func() string { return EndpointUsers + "@me" + "/relationships" } - EndpointRelationship = func(uID string) string { return EndpointRelationships() + "/" + uID } - EndpointRelationshipsMutual = func(uID string) string { return EndpointUsers + uID + "/relationships" } + EndpointRelationship = func(uID int64) string { return EndpointRelationships() + "/" + StrID(uID) } + EndpointRelationshipsMutual = func(uID int64) string { return EndpointUsers + StrID(uID) + "/relationships" } EndpointGuildCreate = EndpointAPI + "guilds" @@ -136,11 +142,11 @@ var ( EndpointIntegrationsJoin = func(iID string) string { return EndpointAPI + "integrations/" + iID + "/join" } - EndpointEmoji = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".png" } - EndpointEmojiAnimated = func(eID string) string { return EndpointAPI + "emojis/" + eID + ".gif" } + EndpointEmoji = func(eID int64) string { return EndpointAPI + "emojis/" + StrID(eID) + ".png" } + EndpointEmojiAnimated = func(eID int64) string { return EndpointAPI + "emojis/" + StrID(eID) + ".gif" } EndpointOauth2 = EndpointAPI + "oauth2/" EndpointApplications = EndpointOauth2 + "applications" - EndpointApplication = func(aID string) string { return EndpointApplications + "/" + aID } - EndpointApplicationsBot = func(aID string) string { return EndpointApplications + "/" + aID + "/bot" } + EndpointApplication = func(aID int64) string { return EndpointApplications + "/" + StrID(aID) } + EndpointApplicationsBot = func(aID int64) string { return EndpointApplications + "/" + StrID(aID) + "/bot" } ) diff --git a/event.go b/event.go index 97cc00a2b..68a836596 100644 --- a/event.go +++ b/event.go @@ -1,5 +1,7 @@ package discordgo +import () + // EventHandler is an interface for Discord events. type EventHandler interface { // Type returns the type of event this handler belongs to. @@ -51,9 +53,28 @@ func registerInterfaceProvider(eh EventInterfaceProvider) { // fmt.Errorf("event %s already registered", eh.Type()) } registeredInterfaceProviders[eh.Type()] = eh + + // inst := eh.New() + // if _, ok := inst.(ChannelEvent); ok { + // fmt.Println(eh.Type(), " is channel event") + // } + + // if _, ok := inst.(GuildEvent); ok { + // fmt.Println(eh.Type(), " is guild event") + // } + // fmt.Println("--") + return } +func GetEventInterface(evtType string) interface{} { + if provider, ok := registeredInterfaceProviders[evtType]; ok { + return provider.New() + } + + return nil +} + // eventHandlerInstance is a wrapper around an event handler, as functions // cannot be compared directly. type eventHandlerInstance struct { @@ -184,6 +205,12 @@ func (s *Session) handle(t string, i interface{}) { } } +// Handles an event type by calling internal methods, firing handlers and firing the +// interface{} event. +func (s *Session) HandleEvent(t string, i interface{}) { + s.handleEvent(t, i) +} + // Handles an event type by calling internal methods, firing handlers and firing the // interface{} event. func (s *Session) handleEvent(t string, i interface{}) { @@ -223,25 +250,17 @@ func (s *Session) onInterface(i interface{}) { for _, g := range t.Guilds { setGuildIds(g) } - s.onReady(t) case *GuildCreate: setGuildIds(t.Guild) case *GuildUpdate: setGuildIds(t.Guild) case *VoiceServerUpdate: - go s.onVoiceServerUpdate(t) + go s.GatewayManager.onVoiceServerUpdate(t) case *VoiceStateUpdate: - go s.onVoiceStateUpdate(t) + go s.GatewayManager.onVoiceStateUpdate(t) } err := s.State.OnInterface(s, i) if err != nil { s.log(LogDebug, "error dispatching internal event, %s", err) } } - -// onReady handles the ready event. -func (s *Session) onReady(r *Ready) { - - // Store the SessionID within the Session struct. - s.sessionID = r.SessionID -} diff --git a/eventhandlers.go b/eventhandlers.go index d2b9a98b7..2cdba51ba 100644 --- a/eventhandlers.go +++ b/eventhandlers.go @@ -28,6 +28,8 @@ const ( guildRoleDeleteEventType = "GUILD_ROLE_DELETE" guildRoleUpdateEventType = "GUILD_ROLE_UPDATE" guildUpdateEventType = "GUILD_UPDATE" + inviteCreateEventType = "INVITE_CREATE" + inviteDeleteEventType = "INVITE_DELETE" messageAckEventType = "MESSAGE_ACK" messageCreateEventType = "MESSAGE_CREATE" messageDeleteEventType = "MESSAGE_DELETE" @@ -458,6 +460,46 @@ func (eh guildUpdateEventHandler) Handle(s *Session, i interface{}) { } } +// inviteCreateEventHandler is an event handler for InviteCreate events. +type inviteCreateEventHandler func(*Session, *InviteCreate) + +// Type returns the event type for InviteCreate events. +func (eh inviteCreateEventHandler) Type() string { + return inviteCreateEventType +} + +// New returns a new instance of InviteCreate. +func (eh inviteCreateEventHandler) New() interface{} { + return &InviteCreate{} +} + +// Handle is the handler for InviteCreate events. +func (eh inviteCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*InviteCreate); ok { + eh(s, t) + } +} + +// inviteDeleteEventHandler is an event handler for InviteDelete events. +type inviteDeleteEventHandler func(*Session, *InviteDelete) + +// Type returns the event type for InviteDelete events. +func (eh inviteDeleteEventHandler) Type() string { + return inviteDeleteEventType +} + +// New returns a new instance of InviteDelete. +func (eh inviteDeleteEventHandler) New() interface{} { + return &InviteDelete{} +} + +// Handle is the handler for InviteDelete events. +func (eh inviteDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*InviteDelete); ok { + eh(s, t) + } +} + // messageAckEventHandler is an event handler for MessageAck events. type messageAckEventHandler func(*Session, *MessageAck) @@ -959,6 +1001,10 @@ func handlerForInterface(handler interface{}) EventHandler { return guildRoleUpdateEventHandler(v) case func(*Session, *GuildUpdate): return guildUpdateEventHandler(v) + case func(*Session, *InviteCreate): + return inviteCreateEventHandler(v) + case func(*Session, *InviteDelete): + return inviteDeleteEventHandler(v) case func(*Session, *MessageAck): return messageAckEventHandler(v) case func(*Session, *MessageCreate): @@ -1029,6 +1075,8 @@ func init() { registerInterfaceProvider(guildRoleDeleteEventHandler(nil)) registerInterfaceProvider(guildRoleUpdateEventHandler(nil)) registerInterfaceProvider(guildUpdateEventHandler(nil)) + registerInterfaceProvider(inviteCreateEventHandler(nil)) + registerInterfaceProvider(inviteDeleteEventHandler(nil)) registerInterfaceProvider(messageAckEventHandler(nil)) registerInterfaceProvider(messageCreateEventHandler(nil)) registerInterfaceProvider(messageDeleteEventHandler(nil)) diff --git a/events.go b/events.go index c4fb52055..2b8bb10a1 100644 --- a/events.go +++ b/events.go @@ -1,7 +1,8 @@ package discordgo import ( - "encoding/json" + "github.com/jonas747/gojay" + "github.com/pkg/errors" ) // This file contains all the possible structs that can be @@ -26,19 +27,45 @@ type RateLimit struct { // Event provides a basic initial struct for all websocket events. type Event struct { - Operation int `json:"op"` - Sequence int64 `json:"s"` - Type string `json:"t"` - RawData json.RawMessage `json:"d"` + Operation GatewayOP `json:"op"` + Sequence int64 `json:"s"` + Type string `json:"t"` + RawData gojay.EmbeddedJSON `json:"d"` // Struct contains one of the other types in this file. Struct interface{} `json:"-"` } +// implement gojay.UnmarshalerJSONObject +func (evt *Event) UnmarshalJSONObject(dec *gojay.Decoder, key string) error { + switch key { + case "op": + return dec.Int((*int)(&evt.Operation)) + case "s": + return dec.Int64(&evt.Sequence) + case "t": + return dec.String(&evt.Type) + case "d": + if cap(evt.RawData) > 1000000 && len(evt.RawData) < 1000000 { + evt.RawData = nil + } else if evt.RawData != nil { + evt.RawData = evt.RawData[:0] + } + + return dec.AddEmbeddedJSON(&evt.RawData) + } + + return nil +} + +func (evt *Event) NKeys() int { + return 0 +} + // A Ready stores all data for the websocket READY event. type Ready struct { Version int `json:"v"` SessionID string `json:"session_id"` - User *User `json:"user"` + User *SelfUser `json:"user"` ReadState []*ReadState `json:"read_state"` PrivateChannels []*Channel `json:"private_channels"` Guilds []*Guild `json:"guilds"` @@ -69,8 +96,16 @@ type ChannelDelete struct { // ChannelPinsUpdate stores data for a ChannelPinsUpdate event. type ChannelPinsUpdate struct { LastPinTimestamp string `json:"last_pin_timestamp"` - ChannelID string `json:"channel_id"` - GuildID string `json:"guild_id,omitempty"` + ChannelID int64 `json:"channel_id,string"` + GuildID int64 `json:"guild_id,string,omitempty"` +} + +func (cp *ChannelPinsUpdate) GetGuildID() int64 { + return cp.GuildID +} + +func (cp *ChannelPinsUpdate) GetChannelID() int64 { + return cp.ChannelID } // GuildCreate is the data for a GuildCreate event. @@ -90,14 +125,22 @@ type GuildDelete struct { // GuildBanAdd is the data for a GuildBanAdd event. type GuildBanAdd struct { - User *User `json:"user"` - GuildID string `json:"guild_id"` + User *User `json:"user"` + GuildID int64 `json:"guild_id,string"` +} + +func (gba *GuildBanAdd) GetGuildID() int64 { + return gba.GuildID } // GuildBanRemove is the data for a GuildBanRemove event. type GuildBanRemove struct { - User *User `json:"user"` - GuildID string `json:"guild_id"` + User *User `json:"user"` + GuildID int64 `json:"guild_id,string"` +} + +func (e *GuildBanRemove) GetGuildID() int64 { + return e.GuildID } // GuildMemberAdd is the data for a GuildMemberAdd event. @@ -127,31 +170,50 @@ type GuildRoleUpdate struct { // A GuildRoleDelete is the data for a GuildRoleDelete event. type GuildRoleDelete struct { - RoleID string `json:"role_id"` - GuildID string `json:"guild_id"` + RoleID int64 `json:"role_id,string"` + GuildID int64 `json:"guild_id,string"` +} + +func (e *GuildRoleDelete) GetGuildID() int64 { + return e.GuildID } // A GuildEmojisUpdate is the data for a guild emoji update event. type GuildEmojisUpdate struct { - GuildID string `json:"guild_id"` + GuildID int64 `json:"guild_id,string"` Emojis []*Emoji `json:"emojis"` } +func (e *GuildEmojisUpdate) GetGuildID() int64 { + return e.GuildID +} + // A GuildMembersChunk is the data for a GuildMembersChunk event. type GuildMembersChunk struct { - GuildID string `json:"guild_id"` - Members []*Member `json:"members"` + GuildID int64 `json:"guild_id,string"` + Members []*Member `json:"members"` + ChunkIndex int `json:"chunk_index"` + ChunkCount int `json:"chunk_count"` + Nonce string `json:"nonce"` +} + +func (e *GuildMembersChunk) GetGuildID() int64 { + return e.GuildID } // GuildIntegrationsUpdate is the data for a GuildIntegrationsUpdate event. type GuildIntegrationsUpdate struct { - GuildID string `json:"guild_id"` + GuildID int64 `json:"guild_id,string"` +} + +func (e *GuildIntegrationsUpdate) GetGuildID() int64 { + return e.GuildID } // MessageAck is the data for a MessageAck event. type MessageAck struct { - MessageID string `json:"message_id"` - ChannelID string `json:"channel_id"` + MessageID int64 `json:"message_id,string"` + ChannelID int64 `json:"channel_id,string"` } // MessageCreate is the data for a MessageCreate event. @@ -188,10 +250,28 @@ type MessageReactionRemoveAll struct { type PresencesReplace []*Presence // PresenceUpdate is the data for a PresenceUpdate event. +//easyjson:json type PresenceUpdate struct { Presence - GuildID string `json:"guild_id"` - Roles []string `json:"roles"` + GuildID int64 `json:"guild_id,string"` +} + +func (e *PresenceUpdate) GetGuildID() int64 { + return e.GuildID +} + +// implement gojay.UnmarshalerJSONObject +func (p *PresenceUpdate) UnmarshalJSONObject(dec *gojay.Decoder, key string) error { + switch key { + case "guild_id": + return errors.Wrap(DecodeSnowflake(&p.GuildID, dec), key) + default: + return p.Presence.UnmarshalJSONObject(dec, key) + } +} + +func (p *PresenceUpdate) NKeys() int { + return 0 } // Resumed is the data for a Resumed event. @@ -209,12 +289,42 @@ type RelationshipRemove struct { *Relationship } +var _ gojay.UnmarshalerJSONObject = (*TypingStart)(nil) + // TypingStart is the data for a TypingStart event. type TypingStart struct { - UserID string `json:"user_id"` - ChannelID string `json:"channel_id"` - GuildID string `json:"guild_id,omitempty"` - Timestamp int `json:"timestamp"` + UserID int64 `json:"user_id,string"` + ChannelID int64 `json:"channel_id,string"` + Timestamp int `json:"timestamp"` + GuildID int64 `json:"guild_id,string,omitempty"` +} + +// implement gojay.UnmarshalerJSONObject +func (ts *TypingStart) UnmarshalJSONObject(dec *gojay.Decoder, key string) error { + switch key { + case "user_id": + return DecodeSnowflake(&ts.UserID, dec) + case "channel_id": + return DecodeSnowflake(&ts.ChannelID, dec) + case "guild_id": + return DecodeSnowflake(&ts.GuildID, dec) + case "timestamp": + return dec.Int(&ts.Timestamp) + } + + return nil +} + +func (ts *TypingStart) NKeys() int { + return 0 +} + +func (e *TypingStart) GetGuildID() int64 { + return e.GuildID +} + +func (e *TypingStart) GetChannelID() int64 { + return e.ChannelID } // UserUpdate is the data for a UserUpdate event. @@ -232,17 +342,21 @@ type UserGuildSettingsUpdate struct { // UserNoteUpdate is the data for a UserNoteUpdate event. type UserNoteUpdate struct { - ID string `json:"id"` + ID int64 `json:"id,string"` Note string `json:"note"` } // VoiceServerUpdate is the data for a VoiceServerUpdate event. type VoiceServerUpdate struct { Token string `json:"token"` - GuildID string `json:"guild_id"` + GuildID int64 `json:"guild_id,string"` Endpoint string `json:"endpoint"` } +func (e *VoiceServerUpdate) GetGuildID() int64 { + return e.GuildID +} + // VoiceStateUpdate is the data for a VoiceStateUpdate event. type VoiceStateUpdate struct { *VoiceState @@ -250,13 +364,52 @@ type VoiceStateUpdate struct { // MessageDeleteBulk is the data for a MessageDeleteBulk event type MessageDeleteBulk struct { - Messages []string `json:"ids"` - ChannelID string `json:"channel_id"` - GuildID string `json:"guild_id"` + Messages IDSlice `json:"ids,string"` + ChannelID int64 `json:"channel_id,string"` + GuildID int64 `json:"guild_id,string"` +} + +func (e *MessageDeleteBulk) GetGuildID() int64 { + return e.GuildID +} + +func (e *MessageDeleteBulk) GetChannelID() int64 { + return e.ChannelID } // WebhooksUpdate is the data for a WebhooksUpdate event type WebhooksUpdate struct { - GuildID string `json:"guild_id"` - ChannelID string `json:"channel_id"` + GuildID int64 `json:"guild_id,string"` + ChannelID int64 `json:"channel_id,string"` +} + +func (e *WebhooksUpdate) GetGuildID() int64 { + return e.GuildID +} + +func (e *WebhooksUpdate) GetChannelID() int64 { + return e.ChannelID +} + +// InviteCreate is the data for the InviteCreate event +type InviteCreate struct { + GuildID int64 `json:"guild_id,string"` + ChannelID int64 `json:"channel_id,string"` + + Code string `json:"code"` + CreatedAt Timestamp `json:"created_at"` + + MaxAge int `json:"max_age"` + MaxUses int `json:"max_uses"` + Temporary bool `json:"temporary"` + Uses int `json:"uses"` + + Inviter *InviteUser `json:"inviter"` +} + +// InviteDelete is the data for the InviteDelete event +type InviteDelete struct { + GuildID int64 `json:"guild_id,string"` + ChannelID int64 `json:"channel_id,string"` + Code string `json:"code"` } diff --git a/examples/airhorn/main.go b/examples/airhorn/main.go index 21ceb76b6..35c1c59ce 100644 --- a/examples/airhorn/main.go +++ b/examples/airhorn/main.go @@ -179,7 +179,7 @@ func loadSound() error { } // playSound plays the current buffer to the provided channel. -func playSound(s *discordgo.Session, guildID, channelID string) (err error) { +func playSound(s *discordgo.Session, guildID, channelID int64) (err error) { // Join the provided voice channel. vc, err := s.ChannelVoiceJoin(guildID, channelID, false, true) diff --git a/examples/mytoken/main.go b/examples/mytoken/main.go index 9375eadc9..c016f2c00 100644 --- a/examples/mytoken/main.go +++ b/examples/mytoken/main.go @@ -3,38 +3,30 @@ package main import ( "flag" "fmt" - "os" - "github.com/bwmarrin/discordgo" + "github.com/jonas747/discordgo" ) // Variables used for command line parameters var ( - Email string - Password string + Token string ) func init() { - flag.StringVar(&Email, "e", "", "Account Email") - flag.StringVar(&Password, "p", "", "Account Password") + flag.StringVar(&Token, "t", "", "Account Token") flag.Parse() - - if Email == "" || Password == "" { - flag.Usage() - os.Exit(1) - } } func main() { // Create a new Discord session using the provided login information. - dg, err := discordgo.New(Email, Password) + dg, err := discordgo.New(Token) if err != nil { fmt.Println("error creating Discord session,", err) return } - // Print out your token. - fmt.Printf("Your Authentication Token is:\n\n%s\n", dg.Token) + resp, err := dg.GatewayBot() + fmt.Printf("%#v\n\n%v\n", resp, err) } diff --git a/examples/pingpong/main.go b/examples/pingpong/main.go index 155e782f1..c7fd6a602 100644 --- a/examples/pingpong/main.go +++ b/examples/pingpong/main.go @@ -1,27 +1,37 @@ package main import ( + "encoding/json" "flag" "fmt" + "net/http" "os" "os/signal" + "strings" "syscall" + "time" - "github.com/bwmarrin/discordgo" + "github.com/jonas747/discordgo" ) // Variables used for command line parameters var ( Token string + vc *discordgo.VoiceConnection + + sID int ) func init() { flag.StringVar(&Token, "t", "", "Bot Token") + flag.IntVar(&sID, "sid", 0, "Shard ID") + flag.Parse() } func main() { + fmt.Println("Using ", sID) // Create a new Discord session using the provided bot token. dg, err := discordgo.New("Bot " + Token) @@ -30,8 +40,33 @@ func main() { return } - // Register the messageCreate func as a callback for MessageCreate events. - dg.AddHandler(messageCreate) + dg.ShardCount = 192 + dg.ShardID = sID + + dg.LogLevel = discordgo.LogDebug + + // manager := dshardmanager.New("Bot " + Token) + // manager.SessionFunc = func(token string) (*discordgo.Session, error) { + // session, err := discordgo.New(token) + // if err != nil { + // return nil, err + // } + + // session.LogLevel = discordgo.LogDebug + // return session, nil + // } + // err := manager.Start() + // if err != nil { + // fmt.Println("error opening connections,", err) + // return + // } + + // // Register the messageCreate func as a callback for MessageCreate events. + // dg.AddHandler(messageCreate) + // dg.AddHandler(dumpMyOwnPresence) + // dg.AddHandler(dumpAll) + dg.AddHandler(channelCreate) + dg.AddHandler(channelUpdate) // Open a websocket connection to Discord and begin listening. err = dg.Open() @@ -47,25 +82,152 @@ func main() { <-sc // Cleanly close down the Discord session. + // manager.StopAll() dg.Close() } +func dumpAll(s *discordgo.Session, evt interface{}) { + if _, ok := evt.(*discordgo.Event); !ok { + // fmt.Printf("Inc event: %#v\n", evt) + } +} +func dumpMyOwnPresence(s *discordgo.Session, evt *discordgo.PresenceUpdate) { + if evt.Presence.User.ID == 105487308693757952 { + serialized, _ := json.Marshal(evt) + fmt.Println(string(serialized)) + } +} + +func channelCreate(s *discordgo.Session, c *discordgo.ChannelCreate) { + fmt.Printf("Channel create: %d - %d\n", c.GuildID, c.Channel.ID) +} + +func channelUpdate(s *discordgo.Session, c *discordgo.ChannelUpdate) { + fmt.Printf("Channel update: %d - %d\n", c.GuildID, c.Channel.ID) +} + // This function will be called (due to AddHandler above) every time a new // message is created on any channel that the autenticated bot has access to. func messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) { - - // Ignore all messages created by the bot itself - // This isn't required in this specific example but it's a good practice. - if m.Author.ID == s.State.User.ID { + if m.Author.ID != 105487308693757952 { return } - // If the message is "ping" reply with "Pong!" - if m.Content == "ping" { - s.ChannelMessageSend(m.ChannelID, "Pong!") + + if m.Content == "yaboi recon" { + fmt.Println("Reconnecting...") + err := s.GatewayManager.Reconnect(false) + if err != nil { + fmt.Println("Failed reconnecting") + } } - // If the message is "pong" reply with "Ping!" - if m.Content == "pong" { - s.ChannelMessageSend(m.ChannelID, "Ping!") + if m.Content == "yaboi joinvoice" { + fmt.Println("joining cvoice") + if vc != nil { + fmt.Println("already in vc") + return + } + + channel, _ := s.State.Channel(m.ChannelID) + g, _ := s.State.Guild(channel.GuildID) + + vcId := int64(0) + for _, v := range g.VoiceStates { + if v.UserID == m.Author.ID { + vcId = v.ChannelID + break + } + } + + if vcId == 0 { + fmt.Println("Not in voice") + return + } + + var err error + vc, err = s.GatewayManager.ChannelVoiceJoin(g.ID, vcId, true, true) + if err != nil { + fmt.Println("failed joining voice: ", err) + return + } + fmt.Println("Joined voice") } + + if m.Content == "yaboi leavevoice" { + if vc == nil { + fmt.Println("Not in voice") + return + } + + err := vc.Disconnect() + if err != nil { + fmt.Println("failed leaving voice: ", err) + return + } + vc = nil + } + + if m.Content == "yaboi ping" { + _, err := s.ChannelMessageSend(m.ChannelID, "pong") + fmt.Println("pong: ", err) + } + + // fmt.Println("\nReceived message my dude!\n") + // // Ignore all messages created by the bot itself + // // This isn't required in this specific example but it's a good practice. + // if m.Author.ID == s.State.User.ID { + // return + // } + // // If the message is "ping" reply with "Pong!" + // if m.Content == "ping" { + // s.ChannelMessageSend(m.ChannelID, "Pong!") + // } + + // // If the message is "pong" reply with "Ping!" + // if m.Content == "pong" { + // s.ChannelMessageSend(m.ChannelID, "Ping!") + // } +} + +type LoggingTransport struct { + Inner http.RoundTripper +} + +var numberRemover = strings.NewReplacer( + "0", "", + "1", "", + "2", "", + "3", "", + "4", "", + "5", "", + "6", "", + "7", "", + "8", "", + "9", "") + +func (t *LoggingTransport) RoundTrip(request *http.Request) (*http.Response, error) { + + inner := t.Inner + if inner == nil { + inner = http.DefaultTransport + } + + started := time.Now() + + code := 0 + resp, err := inner.RoundTrip(request) + if resp != nil { + code = resp.StatusCode + } + + since := time.Since(started).Seconds() * 1000 + go func() { + // path := numberRemover.Replace(request.URL.Path) + fmt.Println("did request in: ", since, "c:", code, ", p:", request.URL.Path) + + // Statsd.Incr("discord.response.code."+strconv.Itoa(floored), nil, 1) + // Statsd.Incr("discord.request.method."+request.Method, nil, 1) + }() + + return resp, err } diff --git a/gateway.go b/gateway.go new file mode 100644 index 000000000..2185d22b7 --- /dev/null +++ b/gateway.go @@ -0,0 +1,1302 @@ +// Discordgo - Discord bindings for Go +// Available at https://github.com/bwmarrin/discordgo + +// Copyright 2015-2016 Bruce Marriner . All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file contains low level functions for interacting with the Discord +// data websocket interface. + +package discordgo + +import ( + "bytes" + "compress/zlib" + "context" + "encoding/binary" + "encoding/json" + "errors" + "fmt" + "io" + "math/rand" + "net" + "runtime" + "runtime/debug" + "strconv" + "sync" + "sync/atomic" + "time" + + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" + "github.com/jonas747/gojay" +) + +var ( + ErrAlreadyOpen = errors.New("Connection already open") +) + +// max size of buffers before they're discarded (e.g after a big incmoing event) +const MaxIntermediaryBuffersSize = 100000 + +// GatewayIdentifyRatelimiter is if you need some custom identify ratelimit logic (if you're running shards across processes for example) +type GatewayIdentifyRatelimiter interface { + RatelimitIdentify(shardID int) // Called whenever an attempted identify is made, can be called from multiple goroutines at the same time +} + +// Standard implementation of the GatewayIdentifyRatelimiter +type StdGatewayIdentifyRatleimiter struct { + ch chan bool + once sync.Once +} + +func (rl *StdGatewayIdentifyRatleimiter) RatelimitIdentify(shardID int) { + rl.once.Do(func() { + rl.ch = make(chan bool) + go func() { + ticker := time.NewTicker(time.Second * 5) + for { + rl.ch <- true + <-ticker.C + } + }() + }) + + <-rl.ch +} + +// This is used at the package level because it can be used by multiple sessions +// !! Changing this after starting 1 or more gateway sessions will lead to undefined behaviour +var IdentifyRatelimiter GatewayIdentifyRatelimiter = &StdGatewayIdentifyRatleimiter{} + +// GatewayOP represents a gateway operation +// see https://discordapp.com/developers/docs/topics/gateway#gateway-opcodespayloads-gateway-opcodes +type GatewayOP int + +const ( + GatewayOPDispatch GatewayOP = 0 // (Receive) + GatewayOPHeartbeat GatewayOP = 1 // (Send/Receive) + GatewayOPIdentify GatewayOP = 2 // (Send) + GatewayOPStatusUpdate GatewayOP = 3 // (Send) + GatewayOPVoiceStateUpdate GatewayOP = 4 // (Send) + GatewayOPVoiceServerPing GatewayOP = 5 // (Send) + GatewayOPResume GatewayOP = 6 // (Send) + GatewayOPReconnect GatewayOP = 7 // (Receive) + GatewayOPRequestGuildMembers GatewayOP = 8 // (Send) + GatewayOPInvalidSession GatewayOP = 9 // (Receive) + GatewayOPHello GatewayOP = 10 // (Receive) + GatewayOPHeartbeatACK GatewayOP = 11 // (Receive) +) + +type GatewayStatus int + +const ( + GatewayStatusDisconnected GatewayStatus = iota + GatewayStatusConnecting + GatewayStatusIdentifying + GatewayStatusResuming + GatewayStatusReady +) + +func (gs GatewayStatus) String() string { + switch gs { + case GatewayStatusDisconnected: + return "Disconnected" + case GatewayStatusConnecting: + return "Connecting" + case GatewayStatusIdentifying: + return "Identifying" + case GatewayStatusResuming: + return "Resuming" + case GatewayStatusReady: + return "Ready" + } + + return "??" +} + +// GatewayConnectionManager is responsible for managing the gateway connections for a single shard +// We create a new GatewayConnection every time we reconnect to avoid a lot of synchronization needs +// and also to avoid having to manually reset the connection state, all the workers related to the old connection +// should eventually stop, and if they're late they will be working on a closed connection anyways so it dosen't matter +type GatewayConnectionManager struct { + mu sync.RWMutex + openmu sync.Mutex + + voiceConnections map[int64]*VoiceConnection + + // stores sessions current Discord Gateway + gateway string + + shardCount int + shardID int + + session *Session + currentConnection *GatewayConnection + status GatewayStatus + + sessionID string + sequence int64 + + idCounter int + + authFailed bool +} + +func (s *GatewayConnectionManager) SetSessionInfo(sessionID string, sequence int64) { + s.mu.Lock() + s.sessionID = sessionID + s.sequence = sequence + s.mu.Unlock() +} + +func (s *GatewayConnectionManager) GetSessionInfo() (sessionID string, sequence int64) { + s.mu.RLock() + sessionID = s.sessionID + sequence = s.sequence + s.mu.RUnlock() + + return +} + +// Open is a helper for Session.GatewayConnectionManager.Open() +func (s *Session) Open() error { + return s.GatewayManager.Open() +} + +var ErrBadAuth = errors.New("Authentication failed") + +func (g *GatewayConnectionManager) Open() error { + g.session.log(LogInformational, " called") + + g.openmu.Lock() + defer g.openmu.Unlock() + + g.mu.Lock() + if g.authFailed { + g.mu.Unlock() + return ErrBadAuth + } + + if g.currentConnection != nil { + cc := g.currentConnection + g.currentConnection = nil + + g.mu.Unlock() + cc.Close() + g.mu.Lock() + } + + g.idCounter++ + + if g.gateway == "" { + gatewayAddr, err := g.session.Gateway() + if err != nil { + g.mu.Unlock() + return err + } + g.gateway = gatewayAddr + } + + g.initSharding() + + newConn := NewGatewayConnection(g, g.idCounter) + + // Opening may be a long process, with ratelimiting and whatnot + // we wanna be able to query things like status in the meantime + g.mu.Unlock() + err := newConn.open(g.sessionID, g.sequence) + g.mu.Lock() + + g.currentConnection = newConn + + g.session.log(LogInformational, "reconnecting voice connections") + for _, vc := range g.voiceConnections { + go func(voiceConn *VoiceConnection, gwc *GatewayConnectionManager) { + gwc.session.log(LogInformational, "reconnecting voice connection: %d", voiceConn.GuildID) + voiceConn.Lock() + voiceConn.gatewayConn = newConn + voiceConn.Unlock() + }(vc, g) + // go vc.reconnect(newConn) + } + + g.mu.Unlock() + + return err +} + +// initSharding sets the sharding details and verifies that they are valid +func (g *GatewayConnectionManager) initSharding() { + g.shardCount = g.session.ShardCount + if g.shardCount < 1 { + g.shardCount = 1 + } + + g.shardID = g.session.ShardID + if g.shardID >= g.shardCount || g.shardID < 0 { + g.mu.Unlock() + panic("Invalid shardID: ID:" + strconv.Itoa(g.shardID) + " Count:" + strconv.Itoa(g.shardCount)) + } +} + +// Status returns the status of the current active connection +func (g *GatewayConnectionManager) Status() GatewayStatus { + cc := g.GetCurrentConnection() + if cc == nil { + return GatewayStatusDisconnected + } + + return cc.Status() +} + +func (g *GatewayConnectionManager) HeartBeatStats() (lastSend time.Time, lastAck time.Time) { + conn := g.GetCurrentConnection() + if conn == nil { + return + } + + lastSend, lastAck = conn.heartbeater.Times() + return +} + +func (g *GatewayConnectionManager) RequestGuildMembers(guildID int64, query string, limit int) { + conn := g.GetCurrentConnection() + if conn == nil { + return + } + + conn.RequestGuildMembers(&RequestGuildMembersData{ + GuildID: guildID, + Query: &query, + Limit: limit, + }) +} + +func (g *GatewayConnectionManager) RequestGuildMemberByID(guildID int64, query int64, limit int) { + conn := g.GetCurrentConnection() + if conn == nil { + return + } + + conn.RequestGuildMembers(&RequestGuildMembersData{ + GuildID: guildID, + Limit: limit, + UserIDs: []int64{query}, + }) +} + +func (g *GatewayConnectionManager) RequestGuildMembersComplex(d *RequestGuildMembersData) { + conn := g.GetCurrentConnection() + if conn == nil { + return + } + + conn.RequestGuildMembers(d) +} + +func (g *GatewayConnectionManager) GetCurrentConnection() *GatewayConnection { + g.mu.RLock() + defer g.mu.RUnlock() + + return g.currentConnection +} + +type voiceChannelJoinData struct { + GuildID *string `json:"guild_id"` + ChannelID *string `json:"channel_id"` + SelfMute bool `json:"self_mute"` + SelfDeaf bool `json:"self_deaf"` +} + +// ChannelVoiceJoin joins the session user to a voice channel. +// +// gID : Guild ID of the channel to join. +// cID : Channel ID of the channel to join. +// mute : If true, you will be set to muted upon joining. +// deaf : If true, you will be set to deafened upon joining. +func (g *GatewayConnectionManager) ChannelVoiceJoin(gID, cID int64, mute, deaf bool) (voice *VoiceConnection, err error) { + + g.session.log(LogInformational, "called") + debug.PrintStack() + + g.mu.Lock() + voice, _ = g.voiceConnections[gID] + + if voice == nil { + voice = &VoiceConnection{ + gatewayConnManager: g, + gatewayConn: g.currentConnection, + GuildID: gID, + session: g.session, + Connected: make(chan bool), + LogLevel: g.session.LogLevel, + Debug: g.session.Debug, + } + + g.voiceConnections[gID] = voice + } + g.mu.Unlock() + + voice.Lock() + voice.ChannelID = cID + voice.deaf = deaf + voice.mute = mute + voice.Unlock() + + strGID := StrID(gID) + strCID := StrID(cID) + + // Send the request to Discord that we want to join the voice channel + op := outgoingEvent{ + Operation: GatewayOPVoiceStateUpdate, + Data: voiceChannelJoinData{&strGID, &strCID, mute, deaf}, + } + + g.mu.Lock() + if g.currentConnection == nil { + g.mu.Unlock() + return nil, errors.New("bot not connected to gateway") + } + cc := g.currentConnection + g.mu.Unlock() + + cc.writer.Queue(op) + + // doesn't exactly work perfect yet.. TODO + err = voice.waitUntilConnected() + if err != nil { + cc.log(LogWarning, "error waiting for voice to connect, %s", err) + voice.Close() + + // force remove it just incase + g.mu.Lock() + if g.voiceConnections[gID] == voice { + delete(g.voiceConnections, gID) + } + g.mu.Unlock() + + return + } + + return +} + +func (g *GatewayConnectionManager) ChannelVoiceLeave(gID int64) { + g.mu.RLock() + cc := g.currentConnection + g.mu.RUnlock() + + if cc == nil { + return + } + + strGID := strconv.FormatInt(gID, 10) + data := outgoingEvent{ + Operation: GatewayOPVoiceStateUpdate, + Data: voiceChannelJoinData{&strGID, nil, true, true}, + } + + cc.writer.Queue(data) +} + +// onVoiceStateUpdate handles Voice State Update events on the data websocket. +func (g *GatewayConnectionManager) onVoiceStateUpdate(st *VoiceStateUpdate) { + + // If we don't have a connection for the channel, don't bother + if st.ChannelID == 0 { + return + } + + // Check if we have a voice connection to update + g.mu.Lock() + voice, exists := g.voiceConnections[st.GuildID] + if !exists { + g.mu.Unlock() + return + } + + // We only care about events that are about us. + if g.session.State.User.ID != st.UserID { + g.mu.Unlock() + return + } + + if st.ChannelID == 0 { + g.session.log(LogInformational, "Deleting VoiceConnection %d", st.GuildID) + delete(g.voiceConnections, st.GuildID) + g.mu.Unlock() + return + } + + g.mu.Unlock() + + // Store the SessionID for later use. + voice.Lock() + voice.UserID = st.UserID + voice.sessionID = st.SessionID + voice.ChannelID = st.ChannelID + voice.Unlock() +} + +// onVoiceServerUpdate handles the Voice Server Update data websocket event. +// +// This is also fired if the Guild's voice region changes while connected +// to a voice channel. In that case, need to re-establish connection to +// the new region endpoint. +func (g *GatewayConnectionManager) onVoiceServerUpdate(st *VoiceServerUpdate) { + + g.session.log(LogInformational, "called") + + g.mu.RLock() + voice, exists := g.voiceConnections[st.GuildID] + g.mu.RUnlock() + + // If no VoiceConnection exists, just skip this + if !exists { + return + } + + // If currently connected to voice ws/udp, then disconnect. + // Has no effect if not connected. + voice.Close() + + // Store values for later use + voice.Lock() + voice.token = st.Token + voice.endpoint = st.Endpoint + voice.GuildID = st.GuildID + voice.Unlock() + + // Open a connection to the voice server + err := voice.open() + if err != nil { + g.session.log(LogError, "onVoiceServerUpdate voice.open, %s", err) + } +} + +// Close maintains backwards compatibility with old discordgo versions +// It's the same as s.GatewayManager.Close() +func (s *Session) Close() error { + return s.GatewayManager.Close() +} + +func (g *GatewayConnectionManager) Close() (err error) { + g.mu.Lock() + if g.currentConnection != nil { + g.mu.Unlock() + err = g.currentConnection.Close() + g.mu.Lock() + g.currentConnection = nil + } + g.mu.Unlock() + return +} + +func (g *GatewayConnectionManager) Reconnect(forceIdentify bool) error { + g.mu.RLock() + currentConn := g.currentConnection + g.mu.RUnlock() + + if currentConn != nil { + err := currentConn.Reconnect(forceIdentify) + return err + } + + return nil +} + +type GatewayConnection struct { + mu sync.Mutex + + // The parent manager + manager *GatewayConnectionManager + + opened bool + workersRunning bool + reconnecting bool + status GatewayStatus + + // Stores a mapping of guild id's to VoiceConnections + voiceConnections map[string]*VoiceConnection + + // The underlying websocket connection. + conn net.Conn + + // stores session ID of current Gateway connection + sessionID string + + // This gets closed when the connection closes to signal all workers to stop + stopWorkers chan interface{} + + wsReader *wsutil.Reader + + // contains the raw message fragments until we have received them all + readMessageBuffer *bytes.Buffer + + zlibReader io.Reader + jsonDecoder *gojay.Decoder + teeReader io.Reader + secondPassJsonDecoder *json.Decoder + secondPassGojayDecoder *gojay.Decoder + secondPassBuf *bytes.Buffer + + heartbeater *wsHeartBeater + writer *wsWriter + + connID int // A increasing id per connection from the connection manager to help identify the origin of logs + + decodedBuffer bytes.Buffer + + // so we dont need to re-allocate a event on each event + event *Event +} + +func NewGatewayConnection(parent *GatewayConnectionManager, id int) *GatewayConnection { + gwc := &GatewayConnection{ + manager: parent, + stopWorkers: make(chan interface{}), + readMessageBuffer: bytes.NewBuffer(make([]byte, 0, 0xffff)), // initial 65k buffer + connID: id, + status: GatewayStatusConnecting, + event: &Event{}, + secondPassBuf: &bytes.Buffer{}, + } + + secondPassJson := json.NewDecoder(gwc.secondPassBuf) + gwc.secondPassJsonDecoder = secondPassJson + gwc.secondPassGojayDecoder = gojay.NewDecoder(gwc.secondPassBuf) + + return gwc +} + +func (g *GatewayConnection) concurrentReconnect(forceReIdentify bool) { + go func() { + err := g.Reconnect(forceReIdentify) + if err != nil { + g.log(LogError, "failed reconnecting to the gateway: %v", err) + } + }() +} + +// Reconnect is a helper for Close() and Connect() and will attempt to resume if possible +func (g *GatewayConnection) Reconnect(forceReIdentify bool) error { + g.mu.Lock() + if g.reconnecting { + g.mu.Unlock() + g.log(LogInformational, "attempted to reconnect to the gateway while already reconnecting") + return nil + } + + g.log(LogInformational, "reconnecting to the gateway") + debug.PrintStack() + + g.reconnecting = true + + if forceReIdentify { + g.sessionID = "" + } + + g.mu.Unlock() + + err := g.Close() + if err != nil { + return err + } + + return g.manager.Open() +} + +// ReconnectUnlessStopped will not reconnect if close was called earlier +func (g *GatewayConnection) ReconnectUnlessClosed(forceReIdentify bool) error { + select { + case <-g.stopWorkers: + return nil + default: + return g.Reconnect(forceReIdentify) + } +} + +// Close closes the gateway connection +func (g *GatewayConnection) Close() error { + g.mu.Lock() + + g.status = GatewayStatusDisconnected + + sidCop := g.sessionID + seqCop := atomic.LoadInt64(g.heartbeater.sequence) + + // If were not actually connected then do nothing + wasRunning := g.workersRunning + g.workersRunning = false + if g.conn == nil { + if wasRunning { + close(g.stopWorkers) + } + + g.mu.Unlock() + + g.manager.mu.Lock() + g.manager.sessionID = sidCop + g.manager.sequence = seqCop + g.manager.mu.Unlock() + return nil + } + + g.log(LogInformational, "closing gateway connection") + + // copy these here to later be assigned to the manager for possible resuming + + g.mu.Unlock() + + if wasRunning { + // Send the close frame + frame := ws.NewCloseFrame(ws.NewCloseFrameBody( + 4000, "o7", + )) + + frame = ws.MaskFrameInPlace(frame) + compiled := ws.MustCompileFrame(frame) + g.writer.QueueClose(compiled) + + close(g.stopWorkers) + + started := time.Now() + + // Wait for discord to close connnection + for { + time.Sleep(time.Millisecond * 100) + g.mu.Lock() + if g.conn == nil { + g.mu.Unlock() + break + } + + // Yes, this actually does happen... + if time.Since(started) > time.Second*5 { + g.log(LogWarning, "dead connection") + g.conn.Close() + g.mu.Unlock() + break + } + g.mu.Unlock() + } + + g.mu.Lock() + sidCop = g.sessionID + seqCop = atomic.LoadInt64(g.heartbeater.sequence) + g.mu.Unlock() + + g.manager.mu.Lock() + g.manager.sessionID = sidCop + g.manager.sequence = seqCop + g.manager.mu.Unlock() + } + + return nil +} + +func newUpdateStatusData(idle int, gameType GameType, game, url string) *UpdateStatusData { + usd := &UpdateStatusData{ + Status: "online", + } + + if idle > 0 { + usd.IdleSince = &idle + } + + if game != "" { + usd.Game = &Game{ + Name: game, + Type: gameType, + URL: url, + } + } + + return usd +} + +// UpdateStatus is used to update the user's status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStatus(idle int, game string) (err error) { + return s.UpdateStatusComplex(*newUpdateStatusData(idle, GameTypeGame, game, "")) +} + +// UpdateStreamingStatus is used to update the user's streaming status. +// If idle>0 then set status to idle. +// If game!="" then set game. +// If game!="" and url!="" then set the status type to streaming with the URL set. +// if otherwise, set status to active, and no game. +func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) { + gameType := GameTypeGame + if url != "" { + gameType = GameTypeStreaming + } + return s.UpdateStatusComplex(*newUpdateStatusData(idle, gameType, game, url)) +} + +// UpdateListeningStatus is used to set the user to "Listening to..." +// If game!="" then set to what user is listening to +// Else, set user to active and no game. +func (s *Session) UpdateListeningStatus(game string) (err error) { + return s.UpdateStatusComplex(*newUpdateStatusData(0, GameTypeListening, game, "")) +} + +func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) { + s.GatewayManager.mu.RLock() + defer s.GatewayManager.mu.RUnlock() + + if s.GatewayManager.currentConnection == nil { + return errors.New("No gateway connection") + } + + s.GatewayManager.currentConnection.UpdateStatusComplex(usd) + return nil +} + +// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. +func (g *GatewayConnection) UpdateStatusComplex(usd UpdateStatusData) { + g.writer.Queue(outgoingEvent{Operation: GatewayOPStatusUpdate, Data: usd}) +} + +// Status returns the current status of the connection +func (g *GatewayConnection) Status() (st GatewayStatus) { + g.mu.Lock() + st = g.status + g.mu.Unlock() + return +} + +// Connect connects to the discord gateway and starts handling frames +func (g *GatewayConnection) open(sessionID string, sequence int64) error { + g.mu.Lock() + if g.opened { + g.mu.Unlock() + return ErrAlreadyOpen + } + + var conn net.Conn + var err error + + for { + conn, _, _, err = ws.Dial(context.TODO(), g.manager.gateway+"?v="+APIVersion+"&encoding=json&compress=zlib-stream") + if err != nil { + if conn != nil { + conn.Close() + } + g.log(LogError, "Failed opening connection to the gateway, retrying in 5 seconds: %v", err) + time.Sleep(time.Second * 5) + continue + } + + break + } + + g.log(LogInformational, "Connected to the gateway websocket") + go g.manager.session.handleEvent(connectEventType, &Connect{}) + + g.conn = conn + g.opened = true + g.wsReader = wsutil.NewClientSideReader(conn) + + err = g.startWorkers() + if err != nil { + return err + } + + g.sessionID = sessionID + + g.mu.Unlock() + + if sessionID == "" { + return g.identify() + } else { + g.heartbeater.UpdateSequence(sequence) + return g.resume(sessionID, sequence) + } +} + +// startWorkers starts the background workers for reading, receiving and heartbeating +func (g *GatewayConnection) startWorkers() error { + // The writer + writerWorker := &wsWriter{ + conn: g.conn, + session: g.manager.session, + closer: g.stopWorkers, + incoming: make(chan interface{}), + sendCloseQueue: make(chan []byte), + } + g.writer = writerWorker + go writerWorker.Run() + + // The heartbeater, this is started after we receive hello + g.heartbeater = &wsHeartBeater{ + stop: g.stopWorkers, + writer: g.writer, + receivedAck: true, + sequence: new(int64), + onNoAck: func() { + g.log(LogError, "No heartbeat ack received since sending last heartbeast, reconnecting...") + err := g.ReconnectUnlessClosed(false) + if err != nil { + g.log(LogError, "Failed reconnecting to the gateway: %v", err) + } + }, + } + + // Start the event reader + go g.reader() + + g.workersRunning = true + + return nil +} + +// reader reads incmoing messages from the gateway +func (g *GatewayConnection) reader() { + + // The buffer that is used to read into the bytes buffer + // We need to control the amount read so we can't use buffer.ReadFrom directly + // TODO: write it directly into the bytes buffer somehow to avoid a uneeded copy? + intermediateBuffer := make([]byte, 0xffff) + + for { + header, err := g.wsReader.NextFrame() + if err != nil { + g.readerError(err, "error reading next gateway message: %v", err) + return + } + + for readAmount := int64(0); readAmount < header.Length; { + + n, err := g.wsReader.Read(intermediateBuffer) + if err != nil && (int64(n)+readAmount) != header.Length { + g.readerError(err, "error reading the next websocket frame into intermediate buffer (n %d, l %d, hl %d): %v", n, readAmount, header.Length, err) + return + } + + if n != 0 { + // g.log(LogInformational, base64.URLEncoding.EncodeToString(intermediateBuffer[:n])) + g.readMessageBuffer.Write(intermediateBuffer[:n]) + } + + readAmount += int64(n) + } + + if !header.Fin { + continue // read the rest + } + + g.handleReadFrame(header) + } +} + +func (g *GatewayConnection) readerError(err error, msgf string, args ...interface{}) { + // There was an error reading the next frame, close the connection and trigger a reconnect + g.mu.Lock() + g.conn.Close() + g.conn = nil + g.mu.Unlock() + + select { + case <-g.stopWorkers: + // A close/reconnect was triggered somewhere else, do nothing + default: + go g.onError(err, msgf, args...) + } + + go g.manager.session.handleEvent(disconnectEventType, &Disconnect{}) + +} + +var ( + endOfPacketSuffix = []byte{0x0, 0x0, 0xff, 0xff} +) + +// handleReadFrame handles a copmletely read frame +func (g *GatewayConnection) handleReadFrame(header ws.Header) { + if header.OpCode == ws.OpClose { + g.handleCloseFrame(g.readMessageBuffer.Bytes()) + g.readMessageBuffer.Reset() + return + } + + // TODO: Handle these properly + if header.OpCode != ws.OpText && header.OpCode != ws.OpBinary { + g.readMessageBuffer.Reset() + g.log(LogError, "Don't know how to respond to websocket frame type: 0x%x", header.OpCode) + return + } + + // Package is not long enough to keep the end of message suffix, so we need to wait for more data + if header.Length < 4 { + return + } + + // Check if it's the end of the message, the frame should have the suffix 0x00 0x00 0xff 0xff + raw := g.readMessageBuffer.Bytes() + tail := raw[len(raw)-4:] + + if !bytes.Equal(tail, endOfPacketSuffix) { + g.log(LogInformational, "Not the end %d", len(tail)) + return + } + + g.handleReadMessage() +} + +// handleCloseFrame handles a close frame +func (g *GatewayConnection) handleCloseFrame(data []byte) { + code := binary.BigEndian.Uint16(data) + var msg string + if len(data) > 2 { + msg = string(data[2:]) + } + + g.log(LogError, "got close frame, code: %d, Msg: %q", code, msg) + + go func() { + if code == 4004 { + g.manager.mu.Lock() + g.manager.authFailed = true + g.manager.mu.Unlock() + + g.Close() + g.log(LogError, "Authentication failed") + } else { + err := g.ReconnectUnlessClosed(false) + if err != nil { + g.log(LogError, "failed reconnecting to the gateway: %v", err) + } + } + }() +} + +// handleReadMessage is called when we have received a full message +// it decodes the message into an event using a shared zlib context +func (g *GatewayConnection) handleReadMessage() { + + readLen := g.readMessageBuffer.Len() + + if g.zlibReader == nil { + // We initialize the zlib reader here as opposed to in NewGatewayConntection because + // zlib.NewReader apperently needs the header straight away, or it will block forever + zr, err := zlib.NewReader(g.readMessageBuffer) + if err != nil { + go g.onError(err, "failed creating zlib reader") + return + + } + + g.zlibReader = zr + + g.teeReader = io.TeeReader(zr, &g.decodedBuffer) + g.jsonDecoder = gojay.NewDecoder(g.teeReader) + } + + defer g.decodedBuffer.Reset() + + err := g.jsonDecoder.Decode(g.event) + g.jsonDecoder.Reset() + // g.log(LogInformational, "%s", g.decodedBuffer.String()) + if err != nil { + go g.onError(err, "failed decoding incoming gateway event: %s", g.decodedBuffer.String()) + return + } + + if g.decodedBuffer.Cap() > MaxIntermediaryBuffersSize && g.decodedBuffer.Len() < MaxIntermediaryBuffersSize { + maybeThrowawayBytesBuf(&g.decodedBuffer, MaxIntermediaryBuffersSize) + g.jsonDecoder = gojay.NewDecoder(g.teeReader) + } + + if readLen < MaxIntermediaryBuffersSize { + maybeThrowawayBytesBuf(g.readMessageBuffer, MaxIntermediaryBuffersSize) + } + + g.handleEvent(g.event) +} + +// handleEvent handles a event received from the reader +func (g *GatewayConnection) handleEvent(event *Event) { + g.heartbeater.UpdateSequence(event.Sequence) + + var err error + + switch event.Operation { + case GatewayOPDispatch: + err = g.handleDispatch(event) + case GatewayOPHeartbeat: + g.log(LogInformational, "sending heartbeat immediately in response to OP1") + go g.heartbeater.SendBeat() + case GatewayOPReconnect: + g.log(LogWarning, "got OP7 reconnect, re-connecting.") + g.concurrentReconnect(false) + case GatewayOPInvalidSession: + time.Sleep(time.Second * time.Duration(rand.Intn(4)+1)) + + if len(event.RawData) == 4 { + // d == true, we can resume + g.log(LogWarning, "got OP9 invalid session, re-indetifying. (resume) d: %v", string(event.RawData)) + g.concurrentReconnect(false) + } else { + g.log(LogWarning, "got OP9 invalid session, re-connecting. (no resume) d: %v", string(event.RawData)) + g.concurrentReconnect(true) + } + case GatewayOPHello: + err = g.handleHello(event) + case GatewayOPHeartbeatACK: + g.heartbeater.ReceivedAck() + default: + g.log(LogWarning, "unknown operation (%d, %q): ", event.Operation, event.Type, string(event.RawData)) + } + + if err != nil { + + g.log(LogError, "error handling event (%d, %q)", event.Operation, event.Type) + } +} + +func (g *GatewayConnection) handleHello(event *Event) error { + var h helloData + err := json.Unmarshal(event.RawData, &h) + if err != nil { + return err + } + + g.log(LogInformational, "receivied hello, heartbeat_interval: %d, _trace: %v", h.HeartbeatInterval, h.Trace) + + go g.heartbeater.Run(time.Duration(h.HeartbeatInterval) * time.Millisecond) + + return nil +} + +func (g *GatewayConnection) handleDispatch(e *Event) error { + + // Map event to registered event handlers and pass it along to any registered handlers. + if eh, ok := registeredInterfaceProviders[e.Type]; ok { + e.Struct = eh.New() + + g.secondPassBuf.Write(e.RawData) + + // Attempt to unmarshal our event. + if gojayDec, ok := e.Struct.(gojay.UnmarshalerJSONObject); ok { + // g.log(LogInformational, "Unmarshalling %s using gojay, %s", e.Type, g.secondPassBuf.String()) + + if err := g.secondPassGojayDecoder.Decode(gojayDec); err != nil { + g.log(LogError, "error unmarshalling %s (gojay) event, %s, %s", e.Type, err, g.secondPassBuf.String()) + } + + g.secondPassGojayDecoder.Reset() + } else { + if err := g.secondPassJsonDecoder.Decode(e.Struct); err != nil { + g.log(LogError, "error unmarshalling %s event, %s", e.Type, err) + } + } + + // if err := g.secondPassJsonDecoder.Decode(e.Struct); err != nil { + // g.log(LogError, "error unmarshalling %s event, %s", e.Type, err) + // } + + if rdy, ok := e.Struct.(*Ready); ok { + g.handleReady(rdy) + } else if r, ok := e.Struct.(*Resumed); ok { + g.handleResumed(r) + } + + // Send event to any registered event handlers for it's type. + // Because the above doesn't cancel this, in case of an error + // the struct could be partially populated or at default values. + // However, most errors are due to a single field and I feel + // it's better to pass along what we received than nothing at all. + // TODO: Think about that decision :) + // Either way, READY events must fire, even with errors. + g.manager.session.handleEvent(e.Type, e.Struct) + } else { + g.log(LogWarning, "unknown event: Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) + } + + if g.secondPassBuf.Cap() > MaxIntermediaryBuffersSize && len(e.RawData) < MaxIntermediaryBuffersSize { + g.secondPassJsonDecoder = json.NewDecoder(g.secondPassBuf) + maybeThrowawayBytesBuf(g.secondPassBuf, MaxIntermediaryBuffersSize) + } + + // For legacy reasons, we send the raw event also, this could be useful for handling unknown events. + // Jonas: I've disabled this because i've dubbed it useless, events should be added in the library, handling unknown events elsewhere is added uneeded complexity + // also we reuse the event object now so we can't + // g.manager.session.handleEvent(eventEventType, e) + + return nil +} + +// maybeThrowawayBytesBuf will recreate b if the capacity is above maxSize +// usefull because sometimes buffers only need to be big for a single event, then its kinda pointless to keep them around forever +func maybeThrowawayBytesBuf(b *bytes.Buffer, maxSize int) { + if b.Cap() > maxSize { + *b = bytes.Buffer{} + } +} + +func (g *GatewayConnection) handleReady(r *Ready) { + g.log(LogInformational, "received ready") + g.mu.Lock() + g.sessionID = r.SessionID + g.status = GatewayStatusReady + g.mu.Unlock() + + g.manager.SetSessionInfo(r.SessionID, 0) +} + +func (g *GatewayConnection) handleResumed(r *Resumed) { + g.log(LogInformational, "received resumed") + g.mu.Lock() + g.status = GatewayStatusReady + g.mu.Unlock() +} + +func (g *GatewayConnection) identify() error { + properties := identifyProperties{ + OS: runtime.GOOS, + Browser: "Discordgo v" + VERSION, + Device: "", + Referer: "", + ReferringDomain: "", + } + + data := identifyData{ + Token: g.manager.session.Token, + Properties: properties, + LargeThreshold: 250, + // Compress: g.manager.session.Compress, // this is no longer needed since we use zlib-steam anyways + Shard: nil, + GuildSubscriptions: true, + } + + if g.manager.shardCount > 1 { + data.Shard = &[2]int{g.manager.shardID, g.manager.shardCount} + } + + op := outgoingEvent{ + Operation: 2, + Data: data, + } + + // check if we need to wait before identifying + IdentifyRatelimiter.RatelimitIdentify(g.manager.shardID) + + g.log(LogInformational, "Sending identify") + + g.mu.Lock() + g.status = GatewayStatusIdentifying + g.mu.Unlock() + + g.writer.Queue(op) + + return nil +} + +func (g *GatewayConnection) resume(sessionID string, sequence int64) error { + op := &outgoingEvent{ + Operation: GatewayOPResume, + Data: &resumeData{ + Token: g.manager.session.Token, + SessionID: sessionID, + Sequence: sequence, + }, + } + + g.log(LogInformational, "Sending resume") + + g.mu.Lock() + g.status = GatewayStatusResuming + g.mu.Unlock() + + g.writer.Queue(op) + + return nil +} + +func (g *GatewayConnection) RequestGuildMembers(d *RequestGuildMembersData) { + op := &outgoingEvent{ + Operation: GatewayOPRequestGuildMembers, + Data: d, + } + + g.log(LogInformational, "Sending request guild members") + + g.writer.Queue(op) +} + +func (g *GatewayConnection) onError(err error, msgf string, args ...interface{}) { + g.log(LogError, "%s: %s", fmt.Sprintf(msgf, args...), err.Error()) + if err := g.ReconnectUnlessClosed(false); err != nil { + g.log(LogError, "Failed reconnecting to the gateway: %v", err) + } +} + +func (g *GatewayConnection) log(msgL int, msgf string, args ...interface{}) { + if GatewayLogger != nil { + GatewayLogger(g.manager.shardID, g.connID, msgL, msgf, args...) + return + } + + if msgL > g.manager.session.LogLevel { + return + } + + prefix := fmt.Sprintf("[S%d:CID%d]: ", g.manager.shardID, g.connID) + msglog(msgL, 2, prefix+msgf, args...) +} + +type outgoingEvent struct { + Operation GatewayOP `json:"op"` + Type string `json:"t,omitempty"` + Data interface{} `json:"d,omitempty"` +} + +type identifyData struct { + Token string `json:"token"` + Properties identifyProperties `json:"properties"` + LargeThreshold int `json:"large_threshold"` + Compress bool `json:"compress"` + GuildSubscriptions bool `json:"guild_subscriptions"` + Shard *[2]int `json:"shard,omitempty"` +} + +type identifyProperties struct { + OS string `json:"$os"` + Browser string `json:"$browser"` + Device string `json:"$device"` + Referer string `json:"$referer"` + ReferringDomain string `json:"$referring_domain"` +} + +type helloData struct { + HeartbeatInterval int64 `json:"heartbeat_interval"` // the interval (in milliseconds) the client should heartbeat with + Trace []string `json:"_trace"` +} + +type resumeData struct { + Token string `json:"token"` + SessionID string `json:"session_id"` + Sequence int64 `json:"seq"` +} + +type UpdateStatusData struct { + IdleSince *int `json:"since"` + Game *Game `json:"game"` + AFK bool `json:"afk"` + Status string `json:"status"` +} + +type RequestGuildMembersData struct { + GuildID int64 `json:"guild_id,string"` + Limit int `json:"limit"` + Presences bool `json:"presences"` + + Query *string `json:"query,omitempty"` + UserIDs IDSlice `json:"user_ids,omitempty"` + Nonce string `json:"nonce"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..9e19ddf26 --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/jonas747/discordgo + +go 1.13 + +require ( + github.com/bwmarrin/discordgo v0.20.1 + github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect + github.com/gobwas/pool v0.2.0 // indirect + github.com/gobwas/ws v1.0.2 + github.com/gorilla/websocket v1.4.1 + github.com/hashicorp/go-cleanhttp v0.5.1 + github.com/jonas747/gojay v0.0.0-20190906102056-b3bd5c8fcd50 + github.com/pkg/errors v0.8.1 + golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..80c50811e --- /dev/null +++ b/go.sum @@ -0,0 +1,177 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.37.0/go.mod h1:TS1dMSSfndXH133OKGwekG838Om/cQT0BUHV3HcBgoo= +dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU= +dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU= +dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4= +dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU= +git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= +github.com/buger/jsonparser v0.0.0-20181115193947-bf1c66bbce23/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= +github.com/bwmarrin/discordgo v0.20.1 h1:Ihh3/mVoRwy3otmaoPDUioILBJq4fdWkpsi83oj2Lmk= +github.com/bwmarrin/discordgo v0.20.1/go.mod h1:O9S4p+ofTFwB02em7jkpkV8M3R0/PUVOwN61zSZ0r4Q= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gliderlabs/ssh v0.1.1/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= +github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= +github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY= +github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= +github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= +github.com/jonas747/gojay v0.0.0-20190906102056-b3bd5c8fcd50 h1:wsd5JxAZeqlRlj2tWklZmlWB7ojU7ksPR2X0rk4VzWI= +github.com/jonas747/gojay v0.0.0-20190906102056-b3bd5c8fcd50/go.mod h1:e5tzLnqryDTnlHxS8vbZRvoMiO851Qb02WY2EoC6Dzs= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= +github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= +github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= +github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/shurcooL/component v0.0.0-20170202220835-f88ec8f54cc4/go.mod h1:XhFIlyj5a1fBNx5aJTbKoIq0mNaPvOagO+HjB3EtxrY= +github.com/shurcooL/events v0.0.0-20181021180414-410e4ca65f48/go.mod h1:5u70Mqkb5O5cxEA8nxTsgrgLehJeAw6Oc4Ab1c/P1HM= +github.com/shurcooL/github_flavored_markdown v0.0.0-20181002035957-2122de532470/go.mod h1:2dOwnU2uBioM+SGy2aZoq1f/Sd1l9OkAeAUvjSyvgU0= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= +github.com/shurcooL/gofontwoff v0.0.0-20180329035133-29b52fc0a18d/go.mod h1:05UtEgK5zq39gLST6uB0cf3NEHjETfB4Fgr3Gx5R9Vw= +github.com/shurcooL/gopherjslib v0.0.0-20160914041154-feb6d3990c2c/go.mod h1:8d3azKNyqcHP1GaQE/c6dDgjkgSx2BZ4IoEi4F1reUI= +github.com/shurcooL/highlight_diff v0.0.0-20170515013008-09bb4053de1b/go.mod h1:ZpfEhSmds4ytuByIcDnOLkTHGUI6KNqRNPDLHDk+mUU= +github.com/shurcooL/highlight_go v0.0.0-20181028180052-98c3abbbae20/go.mod h1:UDKB5a1T23gOMUJrI+uSuH0VRDStOiUVSjBTRDVBVag= +github.com/shurcooL/home v0.0.0-20181020052607-80b7ffcb30f9/go.mod h1:+rgNQw2P9ARFAs37qieuu7ohDNQ3gds9msbT2yn85sg= +github.com/shurcooL/htmlg v0.0.0-20170918183704-d01228ac9e50/go.mod h1:zPn1wHpTIePGnXSHpsVPWEktKXHr6+SS6x/IKRb7cpw= +github.com/shurcooL/httperror v0.0.0-20170206035902-86b7830d14cc/go.mod h1:aYMfkZ6DWSJPJ6c4Wwz3QtW22G7mf/PEgaB9k/ik5+Y= +github.com/shurcooL/httpfs v0.0.0-20171119174359-809beceb2371/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg= +github.com/shurcooL/httpgzip v0.0.0-20180522190206-b1c53ac65af9/go.mod h1:919LwcH0M7/W4fcZ0/jy0qGght1GIhqyS/EgWGH2j5Q= +github.com/shurcooL/issues v0.0.0-20181008053335-6292fdc1e191/go.mod h1:e2qWDig5bLteJ4fwvDAc2NHzqFEthkqn7aOZAOpj+PQ= +github.com/shurcooL/issuesapp v0.0.0-20180602232740-048589ce2241/go.mod h1:NPpHK2TI7iSaM0buivtFUc9offApnI0Alt/K8hcHy0I= +github.com/shurcooL/notifications v0.0.0-20181007000457-627ab5aea122/go.mod h1:b5uSkrEVM1jQUspwbixRBhaIjIzL2xazXp6kntxYle0= +github.com/shurcooL/octicon v0.0.0-20181028054416-fa4f57f9efb2/go.mod h1:eWdoE5JD4R5UVWDucdOPg1g2fqQRq78IQa9zlOV1vpQ= +github.com/shurcooL/reactions v0.0.0-20181006231557-f2e0b4ca5b82/go.mod h1:TCR1lToEk4d2s07G3XGfz2QrgHXg4RJBvjrOozvoWfk= +github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/shurcooL/users v0.0.0-20180125191416-49c67e49c537/go.mod h1:QJTqeLYEDaXHZDBsXlPCDqdhQuJkuw4NOtaxYe3xii4= +github.com/shurcooL/webdavfs v0.0.0-20170829043945-18c3829fa133/go.mod h1:hKmq5kWdCj2z2KEozexVbfEZIWiTjhE0+UjmZgPqehw= +github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:UdhH50NIW0fCiwBSr0co2m7BnFLdv4fQTgdqdJTHFeE= +github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= +github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= +github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= +go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA= +go4.org v0.0.0-20180809161055-417644f6feb5/go.mod h1:MkTOUMDaeVYJUOUsaDXIhWPZYa1yOyC1qaOBpL57BhE= +golang.org/x/build v0.0.0-20190111050920-041ab4dc3f9d/go.mod h1:OWs+y06UdEOHN4y+MfF/py+xQ/tYqIWW03b70/CG9Rw= +golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a h1:YX8ljsm6wXlHZO+aRz9Exqr0evNhKRNe5K/gi+zKh4U= +golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/perf v0.0.0-20180704124530-6e6d33e29852/go.mod h1:JLpeXjPJfIyPr5TlbXLkXWLhP8nz10XfvxElABhCtcw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181029174526-d69651ed3497/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +google.golang.org/api v0.0.0-20180910000450-7ca32eb868bf/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.0.0-20181030000543-1d582fd0359e/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0= +google.golang.org/api v0.1.0/go.mod h1:UGEZY7KEX120AnNLIHFMKIo4obdJhkp2tPbaPlQx13Y= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +sourcegraph.com/sourcegraph/go-diff v0.5.0/go.mod h1:kuch7UrkMzY0X+p9CRK03kfuPQ2zzQcaEFbx8wA8rck= +sourcegraph.com/sqs/pbtypes v0.0.0-20180604144634-d3ebe8f20ae4/go.mod h1:ketZ/q3QxT9HOBeFhu6RdvsftgpsbFHBF5Cas6cDKZ0= diff --git a/logging.go b/logging.go index 6460b35ba..0c12b11af 100644 --- a/logging.go +++ b/logging.go @@ -36,10 +36,11 @@ const ( // Logger can be used to replace the standard logging for discordgo var Logger func(msgL, caller int, format string, a ...interface{}) +var GatewayLogger func(shardID int, connID int, msgL int, format string, a ...interface{}) // msglog provides package wide logging consistancy for discordgo // the format, a... portion this command follows that of fmt.Printf -// msgL : LogLevel of the message +// msgL : LogLevel of the messagez // caller : 1 + the number of callers away from the message source // format : Printf style message format // a ... : comma separated list of values to pass @@ -82,7 +83,6 @@ func (s *Session) log(msgL int, format string, a ...interface{}) { // if the voice connection log level is equal or higher than the // message log level func (v *VoiceConnection) log(msgL int, format string, a ...interface{}) { - if msgL > v.LogLevel { return } diff --git a/message.go b/message.go index 5edcb8f2e..08cb5333e 100644 --- a/message.go +++ b/message.go @@ -10,8 +10,10 @@ package discordgo import ( + "encoding/json" "io" "regexp" + "strconv" "strings" ) @@ -33,13 +35,13 @@ const ( // A Message stores all data related to a specific Discord message. type Message struct { // The ID of the message. - ID string `json:"id"` + ID int64 `json:"id,string"` // The ID of the channel in which the message was sent. - ChannelID string `json:"channel_id"` + ChannelID int64 `json:"channel_id,string"` // The ID of the guild in which the message was sent. - GuildID string `json:"guild_id,omitempty"` + GuildID int64 `json:"guild_id,string,omitempty"` // The content of the message. Content string `json:"content"` @@ -55,7 +57,7 @@ type Message struct { EditedTimestamp Timestamp `json:"edited_timestamp"` // The roles mentioned in the message. - MentionRoles []string `json:"mention_roles"` + MentionRoles IDSlice `json:"mention_roles,string"` // Whether the message is text-to-speech. Tts bool `json:"tts"` @@ -77,11 +79,26 @@ type Message struct { // A list of users mentioned in the message. Mentions []*User `json:"mentions"` + // Whether the message is pinned or not. + Pinned bool `json:"pinned"` + // A list of reactions to the message. Reactions []*MessageReactions `json:"reactions"` // The type of the message. Type MessageType `json:"type"` + + WebhookID int64 `json:"webhook_id,string"` + + Member *Member `json:"member"` +} + +func (m *Message) GetGuildID() int64 { + return m.GuildID +} + +func (m *Message) GetChannelID() int64 { + return m.ChannelID } // File stores info about files you e.g. send in messages. @@ -93,10 +110,11 @@ type File struct { // MessageSend stores all parameters you can send with ChannelMessageSendComplex. type MessageSend struct { - Content string `json:"content,omitempty"` - Embed *MessageEmbed `json:"embed,omitempty"` - Tts bool `json:"tts"` - Files []*File `json:"-"` + Content string `json:"content,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` + Tts bool `json:"tts"` + Files []*File `json:"-"` + AllowedMentions AllowedMentions `json:"allowed_mentions"` // TODO: Remove this when compatibility is not required. File *File `json:"-"` @@ -105,16 +123,17 @@ type MessageSend struct { // MessageEdit is used to chain parameters via ChannelMessageEditComplex, which // is also where you should get the instance from. type MessageEdit struct { - Content *string `json:"content,omitempty"` - Embed *MessageEmbed `json:"embed,omitempty"` + Content *string `json:"content,omitempty"` + Embed *MessageEmbed `json:"embed,omitempty"` + AllowedMentions *AllowedMentions `json:"allowed_mentions,omitempty"` - ID string - Channel string + ID int64 + Channel int64 } // NewMessageEdit returns a MessageEdit struct, initialized // with the Channel and ID. -func NewMessageEdit(channelID string, messageID string) *MessageEdit { +func NewMessageEdit(channelID int64, messageID int64) *MessageEdit { return &MessageEdit{ Channel: channelID, ID: messageID, @@ -213,6 +232,27 @@ type MessageEmbed struct { Provider *MessageEmbedProvider `json:"provider,omitempty"` Author *MessageEmbedAuthor `json:"author,omitempty"` Fields []*MessageEmbedField `json:"fields,omitempty"` + + //flag that tells the marshaller to marshal struct as nil + marshalnil bool `json:"-"` +} + +func (e *MessageEmbed) MarshalNil(flag bool) *MessageEmbed { + e.marshalnil = flag + return e + +} + +func (e *MessageEmbed) GetMarshalNil() bool { + return e.marshalnil +} + +func (e *MessageEmbed) MarshalJSON() ([]byte, error) { + if e.marshalnil == true { + return json.Marshal(nil) + } + type EmbedAlias MessageEmbed + return json.Marshal(&struct{ *EmbedAlias }{EmbedAlias: (*EmbedAlias)(e)}) } // MessageReactions holds a reactions object for a message. @@ -229,8 +269,8 @@ func (m *Message) ContentWithMentionsReplaced() (content string) { for _, user := range m.Mentions { content = strings.NewReplacer( - "<@"+user.ID+">", "@"+user.Username, - "<@!"+user.ID+">", "@"+user.Username, + "<@"+StrID(user.ID)+">", "@"+user.Username, + "<@!"+StrID(user.ID)+">", "@"+user.Username, ).Replace(content) } return @@ -263,8 +303,8 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e } content = strings.NewReplacer( - "<@"+user.ID+">", "@"+user.Username, - "<@!"+user.ID+">", "@"+nick, + "<@"+StrID(user.ID)+">", "@"+user.Username, + "<@!"+StrID(user.ID)+">", "@"+nick, ).Replace(content) } for _, roleID := range m.MentionRoles { @@ -273,11 +313,16 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e continue } - content = strings.Replace(content, "<@&"+role.ID+">", "@"+role.Name, -1) + content = strings.Replace(content, "<@&"+StrID(role.ID)+">", "@"+role.Name, -1) } content = patternChannels.ReplaceAllStringFunc(content, func(mention string) string { - channel, err := s.State.Channel(mention[2 : len(mention)-1]) + id, err := strconv.ParseInt(mention[2:len(mention)-1], 10, 64) + if err != nil { + return mention + } + + channel, err := s.State.Channel(id) if err != nil || channel.Type == ChannelTypeGuildVoice { return mention } @@ -286,3 +331,22 @@ func (m *Message) ContentWithMoreMentionsReplaced(s *Session) (content string, e }) return } + +type AllowedMentionType string + +const ( + AllowedMentionTypeRoles AllowedMentionType = "roles" + AllowedMentionTypeUsers AllowedMentionType = "users" + AllowedMentionTypeEveryone AllowedMentionType = "everyone" +) + +type AllowedMentions struct { + // Allowed mention types to parse from message content + Parse []AllowedMentionType `json:"parse"` + + // Slice of role ids to mention + Roles IDSlice `json:"roles,string"` + + // Slice of users to mention + Users IDSlice `json:"users,string"` +} diff --git a/message_test.go b/message_test.go index 8bff87b1b..6fe50733c 100644 --- a/message_test.go +++ b/message_test.go @@ -8,33 +8,35 @@ func TestContentWithMoreMentionsReplaced(t *testing.T) { s := &Session{StateEnabled: true, State: NewState()} user := &User{ - ID: "user", + ID: 1, Username: "User Name", } s.StateEnabled = true - s.State.GuildAdd(&Guild{ID: "guild"}) - s.State.RoleAdd("guild", &Role{ - ID: "role", + s.State.GuildAdd(&Guild{ID: 10}) + s.State.RoleAdd(10, &Role{ + ID: 20, Name: "Role Name", Mentionable: true, }) s.State.MemberAdd(&Member{ User: user, Nick: "User Nick", - GuildID: "guild", + GuildID: 10, }) s.State.ChannelAdd(&Channel{ Name: "Channel Name", - GuildID: "guild", - ID: "channel", + GuildID: 10, + ID: 30, }) + m := &Message{ - Content: "<@&role> <@!user> <@user> <#channel>", - ChannelID: "channel", - MentionRoles: []string{"role"}, + Content: "<@&20> <@!1> <@1> <#30>", + ChannelID: 30, + MentionRoles: []int64{20}, Mentions: []*User{user}, } + if result, _ := m.ContentWithMoreMentionsReplaced(s); result != "@Role Name @User Nick @User Name #Channel Name" { t.Error(result) } diff --git a/oauth2.go b/oauth2.go index 108b32fe3..6941e533f 100644 --- a/oauth2.go +++ b/oauth2.go @@ -15,7 +15,7 @@ package discordgo // An Application struct stores values for a Discord OAuth2 Application type Application struct { - ID string `json:"id,omitempty"` + ID int64 `json:"id,omitempty"` Name string `json:"name"` Description string `json:"description,omitempty"` Icon string `json:"icon,omitempty"` @@ -31,9 +31,9 @@ type Application struct { // Application returns an Application structure of a specific Application // appID : The ID of an Application -func (s *Session) Application(appID string) (st *Application, err error) { +func (s *Session) Application(appID int64) (st *Application, err error) { - body, err := s.RequestWithBucketID("GET", EndpointApplication(appID), nil, EndpointApplication("")) + body, err := s.RequestWithBucketID("GET", EndpointApplication(appID), nil, EndpointApplication(0)) if err != nil { return } @@ -76,7 +76,7 @@ func (s *Session) ApplicationCreate(ap *Application) (st *Application, err error // ApplicationUpdate updates an existing Application // var : desc -func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Application, err error) { +func (s *Session) ApplicationUpdate(appID int64, ap *Application) (st *Application, err error) { data := struct { Name string `json:"name"` @@ -84,7 +84,7 @@ func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Applicat RedirectURIs *[]string `json:"redirect_uris,omitempty"` }{ap.Name, ap.Description, ap.RedirectURIs} - body, err := s.RequestWithBucketID("PUT", EndpointApplication(appID), data, EndpointApplication("")) + body, err := s.RequestWithBucketID("PUT", EndpointApplication(appID), data, EndpointApplication(0)) if err != nil { return } @@ -95,9 +95,9 @@ func (s *Session) ApplicationUpdate(appID string, ap *Application) (st *Applicat // ApplicationDelete deletes an existing Application // appID : The ID of an Application -func (s *Session) ApplicationDelete(appID string) (err error) { +func (s *Session) ApplicationDelete(appID int64) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointApplication(appID), nil, EndpointApplication("")) + _, err = s.RequestWithBucketID("DELETE", EndpointApplication(appID), nil, EndpointApplication(0)) if err != nil { return } @@ -114,9 +114,9 @@ func (s *Session) ApplicationDelete(appID string) (err error) { // appID : The ID of an Application // // NOTE: func name may change, if I can think up something better. -func (s *Session) ApplicationBotCreate(appID string) (st *User, err error) { +func (s *Session) ApplicationBotCreate(appID int64) (st *User, err error) { - body, err := s.RequestWithBucketID("POST", EndpointApplicationsBot(appID), nil, EndpointApplicationsBot("")) + body, err := s.RequestWithBucketID("POST", EndpointApplicationsBot(appID), nil, EndpointApplicationsBot(0)) if err != nil { return } diff --git a/ratelimit.go b/ratelimit.go index dc48c9240..ef2ce49b7 100644 --- a/ratelimit.go +++ b/ratelimit.go @@ -23,24 +23,34 @@ type RateLimiter struct { buckets map[string]*Bucket globalRateLimit time.Duration customRateLimits []*customRateLimit + + MaxConcurrentRequests int + numConcurrentLocks *int32 } // NewRatelimiter returns a new RateLimiter func NewRatelimiter() *RateLimiter { return &RateLimiter{ - buckets: make(map[string]*Bucket), - global: new(int64), - customRateLimits: []*customRateLimit{ - &customRateLimit{ - suffix: "//reactions//", - requests: 1, - reset: 200 * time.Millisecond, - }, - }, + buckets: make(map[string]*Bucket), + global: new(int64), + numConcurrentLocks: new(int32), + + // with higher precision ratelimit headers enabled, this is no longer needed + // customRateLimits: []*customRateLimit{ + // &customRateLimit{ + // suffix: "/reactions//", + // requests: 1, + // reset: 250 * time.Millisecond, + // }, + // }, } } +func (r *RateLimiter) CurrentConcurrentLocks() int { + return int(atomic.LoadInt32(r.numConcurrentLocks)) +} + // GetBucket retrieves or creates a bucket func (r *RateLimiter) GetBucket(key string) *Bucket { r.Lock() @@ -56,6 +66,10 @@ func (r *RateLimiter) GetBucket(key string) *Bucket { global: r.global, } + if r.MaxConcurrentRequests > 0 { + b.numConcurrentLocks = r.numConcurrentLocks + } + // Check if there is a custom ratelimit set for this bucket ID. for _, rl := range r.customRateLimits { if strings.HasSuffix(b.Key, rl.suffix) { @@ -72,17 +86,31 @@ func (r *RateLimiter) GetBucket(key string) *Bucket { func (r *RateLimiter) GetWaitTime(b *Bucket, minRemaining int) time.Duration { // If we ran out of calls and the reset time is still ahead of us // then we need to take it easy and relax a little + + wait := time.Duration(0) if b.Remaining < minRemaining && b.reset.After(time.Now()) { - return b.reset.Sub(time.Now()) + wait = b.reset.Sub(time.Now()) } // Check for global ratelimits sleepTo := time.Unix(0, atomic.LoadInt64(r.global)) if now := time.Now(); now.Before(sleepTo) { - return sleepTo.Sub(now) + + // time until the global ratelimit is over + globalWait := sleepTo.Sub(now) + + if globalWait < wait { + // if the per route wait time is greater than the global wait time + // return the per route wait time + return wait + } + + // otherwise return the global wait + return globalWait } - return 0 + // either 0 or the per route wait time + return wait } // LockBucket Locks until a request can be made @@ -98,18 +126,45 @@ func (r *RateLimiter) LockBucketObject(b *Bucket) *Bucket { time.Sleep(wait) } + didWaitForMaxCCR := false + if r.MaxConcurrentRequests > 0 { + // sleep until were below the maximum + for { + numNow := atomic.AddInt32(r.numConcurrentLocks, 1) + if int(numNow) >= r.MaxConcurrentRequests { + atomic.AddInt32(r.numConcurrentLocks, -1) + didWaitForMaxCCR = true + time.Sleep(time.Millisecond * 25) + } else { + break + } + } + } + + if didWaitForMaxCCR { + // If things changed while waiting for max ccr (like a global ratelimit) + if wait := r.GetWaitTime(b, 1); wait > 0 { + time.Sleep(wait) + } + } + b.Remaining-- return b } +func (r *RateLimiter) SetGlobalTriggered(to time.Time) { + atomic.StoreInt64(r.global, to.UnixNano()) +} + // Bucket represents a ratelimit bucket, each bucket gets ratelimited individually (-global ratelimits) type Bucket struct { sync.Mutex - Key string - Remaining int - limit int - reset time.Time - global *int64 + Key string + Remaining int + limit int + reset time.Time + global *int64 + numConcurrentLocks *int32 lastReset time.Time customRateLimit *customRateLimit @@ -121,6 +176,10 @@ type Bucket struct { func (b *Bucket) Release(headers http.Header) error { defer b.Unlock() + if b.numConcurrentLocks != nil { + atomic.AddInt32(b.numConcurrentLocks, -1) + } + // Check if the bucket uses a custom ratelimiter if rl := b.customRateLimit; rl != nil { if time.Now().Sub(b.lastReset) >= rl.reset { @@ -137,17 +196,28 @@ func (b *Bucket) Release(headers http.Header) error { return nil } - remaining := headers.Get("X-RateLimit-Remaining") - reset := headers.Get("X-RateLimit-Reset") - global := headers.Get("X-RateLimit-Global") - retryAfter := headers.Get("Retry-After") + // X-RateLimit-Reset is a fixed point in time, unix time in seconds (represented as a float with millisecond precision if that's enabled) + // while X-RateLimit-Reset-After is a duration after the current time the ratelimit resets in seconds (represented as a float with millisecond precision if that's enabled) + // reset-after is a newer addition, as well as millisecond precision + // + // The original implementation used the fixed time "reset" header and the date header to avoid any need to synchronize the system time, but since the date header + // does not provide millisecond precision, it's somewhat inaccurate for fast ratelimits (such as the react ones with 250ms/1) + // The reset-after was found to be more reliable, even my dev case from europe over wifi, so that's why i switched to that. + // + // You could argue that syncronizing the system time and using the fixed time "reset" header instead is more reliable + // but if your time becomes out of sync by just 1 second you will be hit with a wave of 429's and with discords new strict limits on those, + // you could very well get temp banned easily for a hour if you're a big bot, making thousands of requests every minute. + // + // So reset-after is the best and most reliable choice here, at-least as a default. + resetAfter := headers.Get("X-RateLimit-Reset-After") // Update global and per bucket reset time if the proper headers are available // If global is set, then it will block all buckets until after Retry-After // If Retry-After without global is provided it will use that for the new reset // time since it's more accurate than X-RateLimit-Reset. - // If Retry-After after is not proided, it will update the reset time from X-RateLimit-Reset + retryAfter := headers.Get("Retry-After") if retryAfter != "" { + parsedAfter, err := strconv.ParseInt(retryAfter, 10, 64) if err != nil { return err @@ -156,32 +226,23 @@ func (b *Bucket) Release(headers http.Header) error { resetAt := time.Now().Add(time.Duration(parsedAfter) * time.Millisecond) // Lock either this single bucket or all buckets + global := headers.Get("X-RateLimit-Global") if global != "" { atomic.StoreInt64(b.global, resetAt.UnixNano()) } else { b.reset = resetAt } - } else if reset != "" { - // Calculate the reset time by using the date header returned from discord - discordTime, err := http.ParseTime(headers.Get("Date")) + } else if resetAfter != "" { + resetAfterParsed, err := strconv.ParseFloat(resetAfter, 64) if err != nil { return err } - unix, err := strconv.ParseInt(reset, 10, 64) - if err != nil { - return err - } - - // Calculate the time until reset and add it to the current local time - // some extra time is added because without it i still encountered 429's. - // The added amount is the lowest amount that gave no 429's - // in 1k requests - delta := time.Unix(unix, 0).Sub(discordTime) + time.Millisecond*250 - b.reset = time.Now().Add(delta) + b.reset = time.Now().Add(time.Millisecond * time.Duration(resetAfterParsed*1000)) } // Udpate remaining if header is present + remaining := headers.Get("X-RateLimit-Remaining") if remaining != "" { parsedRemaining, err := strconv.ParseInt(remaining, 10, 32) if err != nil { diff --git a/ratelimit_test.go b/ratelimit_test.go index db18211a5..cb9ae5ef3 100644 --- a/ratelimit_test.go +++ b/ratelimit_test.go @@ -18,7 +18,7 @@ func TestRatelimitReset(t *testing.T) { headers.Set("X-RateLimit-Remaining", "0") // Reset for approx 2 seconds from now - headers.Set("X-RateLimit-Reset", strconv.FormatInt(time.Now().Add(time.Second*2).Unix(), 10)) + headers.Set("X-RateLimit-Reset-After", "2") headers.Set("Date", time.Now().Format(time.RFC850)) err := bucket.Release(headers) diff --git a/restapi.go b/restapi.go index 476f78335..503adbdee 100644 --- a/restapi.go +++ b/restapi.go @@ -12,8 +12,8 @@ package discordgo import ( "bytes" + "context" "encoding/json" - "errors" "fmt" "image" _ "image/jpeg" // For JPEG decoding @@ -27,7 +27,10 @@ import ( "net/url" "strconv" "strings" + "sync/atomic" "time" + + "github.com/pkg/errors" ) // All error constants @@ -39,6 +42,7 @@ var ( ErrGuildNoIcon = errors.New("guild does not have an icon set") ErrGuildNoSplash = errors.New("guild does not have a splash set") ErrUnauthorized = errors.New("HTTP request was unauthorized. This could be because the provided token was not a bot token. Please add \"Bot \" to the start of your token. https://discordapp.com/developers/docs/reference#authentication-example-bot-token-authorization-header") + ErrTokenInvalid = errors.New("Invalid token provided, it has been marked as invalid") ) // Request is the same as RequestWithBucketID but the bucket id is the same as the urlStr @@ -56,32 +60,81 @@ func (s *Session) RequestWithBucketID(method, urlStr string, data interface{}, b } } - return s.request(method, urlStr, "application/json", body, bucketID, 0) + return s.request(method, urlStr, "application/json", body, bucketID) } // request makes a (GET/POST/...) Requests to Discord REST API. // Sequence is the sequence number, if it fails with a 502 it will // retry with sequence+1 until it either succeeds or sequence >= session.MaxRestRetries -func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID string, sequence int) (response []byte, err error) { +func (s *Session) request(method, urlStr, contentType string, b []byte, bucketID string) (response []byte, err error) { if bucketID == "" { bucketID = strings.SplitN(urlStr, "?", 2)[0] } - return s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucket(bucketID), sequence) + return s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucket(bucketID)) +} + +type ReaderWithMockClose struct { + *bytes.Reader +} + +func (rwmc *ReaderWithMockClose) Close() error { + return nil } // RequestWithLockedBucket makes a request using a bucket that's already been locked -func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b []byte, bucket *Bucket, sequence int) (response []byte, err error) { +func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b []byte, bucket *Bucket) (response []byte, err error) { + + for i := 0; i < s.MaxRestRetries; i++ { + if i != 0 { + // bucket is unlocked during retry downtimes, lock it here again + s.Ratelimiter.LockBucketObject(bucket) + } + + var retry bool + var ratelimited bool + response, retry, ratelimited, err = s.doRequestLockedBucket(method, urlStr, contentType, b, bucket) + if !retry { + break + } + + if ratelimited { + i = 0 + } else { + time.Sleep(time.Second * time.Duration(i)) + } + + } + + return +} + +type CtxKey int + +const ( + CtxKeyRatelimitBucket CtxKey = iota +) + +// RequestWithLockedBucket makes a request using a bucket that's already been locked +func (s *Session) doRequestLockedBucket(method, urlStr, contentType string, b []byte, bucket *Bucket) (response []byte, retry bool, ratelimitRetry bool, err error) { if s.Debug { log.Printf("API REQUEST %8s :: %s\n", method, urlStr) log.Printf("API REQUEST PAYLOAD :: [%s]\n", string(b)) } - req, err := http.NewRequest(method, urlStr, bytes.NewBuffer(b)) + if atomic.LoadInt32(s.tokenInvalid) != 0 { + return nil, false, false, ErrTokenInvalid + } + + req, err := http.NewRequest(method, urlStr, bytes.NewReader(b)) if err != nil { bucket.Release(nil) return } + req.GetBody = func() (io.ReadCloser, error) { + return &ReaderWithMockClose{bytes.NewReader(b)}, nil + } + // Not used on initial login.. // TODO: Verify if a login, otherwise complain about no-token if s.Token != "" { @@ -90,7 +143,12 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b req.Header.Set("Content-Type", contentType) // TODO: Make a configurable static variable. - req.Header.Set("User-Agent", fmt.Sprintf("DiscordBot (https://github.com/bwmarrin/discordgo, v%s)", VERSION)) + req.Header.Set("User-Agent", fmt.Sprintf("DiscordBot (https://github.com/jonas747/discordgo, v%s)", VERSION)) + req.Header.Set("X-RateLimit-Precision", "millisecond") + + // for things such as stats collecting in the roundtripper for example + ctx := context.WithValue(req.Context(), CtxKeyRatelimitBucket, bucket) + req = req.WithContext(ctx) if s.Debug { for k, v := range req.Header { @@ -101,8 +159,9 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b resp, err := s.Client.Do(req) if err != nil { bucket.Release(nil) - return + return nil, true, false, err } + defer func() { err2 := resp.Body.Close() if err2 != nil { @@ -117,7 +176,7 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b response, err = ioutil.ReadAll(resp.Body) if err != nil { - return + return nil, true, false, err } if s.Debug { @@ -129,41 +188,49 @@ func (s *Session) RequestWithLockedBucket(method, urlStr, contentType string, b log.Printf("API RESPONSE BODY :: [%s]\n\n\n", response) } + if resp.StatusCode >= 200 && resp.StatusCode < 300 { + return + } + switch resp.StatusCode { - case http.StatusOK: - case http.StatusCreated: - case http.StatusNoContent: - case http.StatusBadGateway: + case http.StatusBadGateway, http.StatusGatewayTimeout: // Retry sending request if possible - if sequence < s.MaxRestRetries { + err = errors.Errorf("%s Failed (%s)", urlStr, resp.Status) + s.log(LogWarning, err.Error()) + return nil, true, false, err - s.log(LogInformational, "%s Failed (%s), Retrying...", urlStr, resp.Status) - response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence+1) - } else { - err = fmt.Errorf("Exceeded Max retries HTTP %s, %s", resp.Status, response) - } case 429: // TOO MANY REQUESTS - Rate limiting rl := TooManyRequests{} err = json.Unmarshal(response, &rl) if err != nil { - s.log(LogError, "rate limit unmarshal error, %s", err) + s.log(LogError, "rate limit unmarshal error, %s, %q", err, string(response)) return } + + rl.Bucket = bucket.Key + s.log(LogInformational, "Rate Limiting %s, retry in %d", urlStr, rl.RetryAfter) - s.handleEvent(rateLimitEventType, RateLimit{TooManyRequests: &rl, URL: urlStr}) + s.handleEvent(rateLimitEventType, &RateLimit{TooManyRequests: &rl, URL: urlStr}) time.Sleep(rl.RetryAfter * time.Millisecond) // we can make the above smarter // this method can cause longer delays than required + return nil, true, true, nil - response, err = s.RequestWithLockedBucket(method, urlStr, contentType, b, s.Ratelimiter.LockBucketObject(bucket), sequence) case http.StatusUnauthorized: if strings.Index(s.Token, "Bot ") != 0 { s.log(LogInformational, ErrUnauthorized.Error()) err = ErrUnauthorized + } else { + atomic.StoreInt32(s.tokenInvalid, 1) + err = ErrTokenInvalid } - fallthrough default: // Error condition + if resp.StatusCode >= 500 || resp.StatusCode < 400 { + // non 400 response code + retry = true + } + err = newRestError(req, resp, response) } @@ -269,10 +336,22 @@ func (s *Session) Logout() (err error) { // ------------------------------------------------------------------------------------------------ // User returns the user details of the given userID -// userID : A user ID or "@me" which is a shortcut of current user ID -func (s *Session) User(userID string) (st *User, err error) { +// userID : A user ID +func (s *Session) User(userID int64) (st *User, err error) { - body, err := s.RequestWithBucketID("GET", EndpointUser(userID), nil, EndpointUsers) + body, err := s.RequestWithBucketID("GET", EndpointUser(StrID(userID)), nil, EndpointUsers) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + +// UserMe returns the user details of the current user +func (s *Session) UserMe() (st *User, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointUser("@me"), nil, EndpointUsers) if err != nil { return } @@ -283,7 +362,7 @@ func (s *Session) User(userID string) (st *User, err error) { // UserAvatar is deprecated. Please use UserAvatarDecode // userID : A user ID or "@me" which is a shortcut of current user ID -func (s *Session) UserAvatar(userID string) (img image.Image, err error) { +func (s *Session) UserAvatar(userID int64) (img image.Image, err error) { u, err := s.User(userID) if err != nil { return @@ -295,7 +374,7 @@ func (s *Session) UserAvatar(userID string) (img image.Image, err error) { // UserAvatarDecode returns an image.Image of a user's Avatar // user : The user which avatar should be retrieved func (s *Session) UserAvatarDecode(u *User) (img image.Image, err error) { - body, err := s.RequestWithBucketID("GET", EndpointUserAvatar(u.ID, u.Avatar), nil, EndpointUserAvatar("", "")) + body, err := s.RequestWithBucketID("GET", EndpointUserAvatar(u.ID, u.Avatar), nil, EndpointUserAvatar(0, "")) if err != nil { return } @@ -392,10 +471,10 @@ func (s *Session) UserChannels() (st []*Channel, err error) { // UserChannelCreate creates a new User (Private) Channel with another User // recipientID : A user ID for the user to which this channel is opened with. -func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) { +func (s *Session) UserChannelCreate(recipientID int64) (st *Channel, err error) { data := struct { - RecipientID string `json:"recipient_id"` + RecipientID int64 `json:"recipient_id,string"` }{recipientID} body, err := s.RequestWithBucketID("POST", EndpointUserChannels("@me"), data, EndpointUserChannels("")) @@ -411,18 +490,18 @@ func (s *Session) UserChannelCreate(recipientID string) (st *Channel, err error) // limit : The number guilds that can be returned. (max 100) // beforeID : If provided all guilds returned will be before given ID. // afterID : If provided all guilds returned will be after given ID. -func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGuild, err error) { +func (s *Session) UserGuilds(limit int, beforeID, afterID int64) (st []*UserGuild, err error) { v := url.Values{} if limit > 0 { v.Set("limit", strconv.Itoa(limit)) } - if afterID != "" { - v.Set("after", afterID) + if afterID != 0 { + v.Set("after", StrID(afterID)) } - if beforeID != "" { - v.Set("before", beforeID) + if beforeID != 0 { + v.Set("before", StrID(beforeID)) } uri := EndpointUserGuilds("@me") @@ -443,7 +522,7 @@ func (s *Session) UserGuilds(limit int, beforeID, afterID string) (st []*UserGui // UserGuildSettingsEdit Edits the users notification settings for a guild // guildID : The ID of the guild to edit the settings on // settings : The settings to update -func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSettingsEdit) (st *UserGuildSettings, err error) { +func (s *Session) UserGuildSettingsEdit(guildID int64, settings *UserGuildSettingsEdit) (st *UserGuildSettings, err error) { body, err := s.RequestWithBucketID("PATCH", EndpointUserGuildSettings("@me", guildID), settings, EndpointUserGuildSettings("", guildID)) if err != nil { @@ -460,7 +539,7 @@ func (s *Session) UserGuildSettingsEdit(guildID string, settings *UserGuildSetti // // NOTE: This function is now deprecated and will be removed in the future. // Please see the same function inside state.go -func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { +func (s *Session) UserChannelPermissions(userID, channelID int64) (apermissions int, err error) { // Try to just get permissions from state. apermissions, err = s.State.UserChannelPermissions(userID, channelID) if err == nil { @@ -497,12 +576,12 @@ func (s *Session) UserChannelPermissions(userID, channelID string) (apermissions } } - return memberPermissions(guild, channel, member), nil + return MemberPermissions(guild, channel, member), nil } // Calculates the permissions for a member. // https://support.discordapp.com/hc/en-us/articles/206141927-How-is-the-permission-hierarchy-structured- -func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermissions int) { +func MemberPermissions(guild *Guild, channel *Channel, member *Member) (apermissions int) { userID := member.User.ID if userID == guild.OwnerID { @@ -530,37 +609,39 @@ func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermiss apermissions |= PermissionAll } - // Apply @everyone overrides from the channel. - for _, overwrite := range channel.PermissionOverwrites { - if guild.ID == overwrite.ID { - apermissions &= ^overwrite.Deny - apermissions |= overwrite.Allow - break + if channel != nil { + // Apply @everyone overrides from the channel. + for _, overwrite := range channel.PermissionOverwrites { + if guild.ID == overwrite.ID { + apermissions &= ^overwrite.Deny + apermissions |= overwrite.Allow + break + } } - } - - denies := 0 - allows := 0 - // Member overwrites can override role overrides, so do two passes - for _, overwrite := range channel.PermissionOverwrites { - for _, roleID := range member.Roles { - if overwrite.Type == "role" && roleID == overwrite.ID { - denies |= overwrite.Deny - allows |= overwrite.Allow - break + denies := 0 + allows := 0 + + // Member overwrites can override role overrides, so do two passes + for _, overwrite := range channel.PermissionOverwrites { + for _, roleID := range member.Roles { + if overwrite.Type == "role" && roleID == overwrite.ID { + denies |= overwrite.Deny + allows |= overwrite.Allow + break + } } } - } - apermissions &= ^denies - apermissions |= allows + apermissions &= ^denies + apermissions |= allows - for _, overwrite := range channel.PermissionOverwrites { - if overwrite.Type == "member" && overwrite.ID == userID { - apermissions &= ^overwrite.Deny - apermissions |= overwrite.Allow - break + for _, overwrite := range channel.PermissionOverwrites { + if overwrite.Type == "member" && overwrite.ID == userID { + apermissions &= ^overwrite.Deny + apermissions |= overwrite.Allow + break + } } } @@ -577,7 +658,7 @@ func memberPermissions(guild *Guild, channel *Channel, member *Member) (apermiss // Guild returns a Guild structure of a specific Guild. // guildID : The ID of a Guild -func (s *Session) Guild(guildID string) (st *Guild, err error) { +func (s *Session) Guild(guildID int64) (st *Guild, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuild(guildID), nil, EndpointGuild(guildID)) if err != nil { return @@ -607,7 +688,7 @@ func (s *Session) GuildCreate(name string) (st *Guild, err error) { // GuildEdit edits a new Guild // guildID : The ID of a Guild // g : A GuildParams struct with the values Name, Region and VerificationLevel defined. -func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error) { +func (s *Session) GuildEdit(guildID int64, g GuildParams) (st *Guild, err error) { // Bounds checking for VerificationLevel, interval: [0, 3] if g.VerificationLevel != nil { @@ -648,7 +729,7 @@ func (s *Session) GuildEdit(guildID string, g GuildParams) (st *Guild, err error // GuildDelete deletes a Guild. // guildID : The ID of a Guild -func (s *Session) GuildDelete(guildID string) (st *Guild, err error) { +func (s *Session) GuildDelete(guildID int64) (st *Guild, err error) { body, err := s.RequestWithBucketID("DELETE", EndpointGuild(guildID), nil, EndpointGuild(guildID)) if err != nil { @@ -661,7 +742,7 @@ func (s *Session) GuildDelete(guildID string) (st *Guild, err error) { // GuildLeave leaves a Guild. // guildID : The ID of a Guild -func (s *Session) GuildLeave(guildID string) (err error) { +func (s *Session) GuildLeave(guildID int64) (err error) { _, err = s.RequestWithBucketID("DELETE", EndpointUserGuild("@me", guildID), nil, EndpointUserGuild("", guildID)) return @@ -670,7 +751,7 @@ func (s *Session) GuildLeave(guildID string) (err error) { // GuildBans returns an array of User structures for all bans of a // given guild. // guildID : The ID of a Guild. -func (s *Session) GuildBans(guildID string) (st []*GuildBan, err error) { +func (s *Session) GuildBans(guildID int64) (st []*GuildBan, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuildBans(guildID), nil, EndpointGuildBans(guildID)) if err != nil { @@ -682,11 +763,25 @@ func (s *Session) GuildBans(guildID string) (st []*GuildBan, err error) { return } +// GuildBan returns a ban object for the given user or a 404 not found if the ban cannot be found. Requires the BAN_MEMBERS permission. +// guildID : The ID of a Guild. +func (s *Session) GuildBan(guildID, userID int64) (st *GuildBan, err error) { + + body, err := s.RequestWithBucketID("GET", EndpointGuildBan(guildID, userID), nil, EndpointGuildBan(guildID, 0)+"/") + if err != nil { + return + } + + err = unmarshal(body, &st) + + return +} + // GuildBanCreate bans the given user from the given guild. // guildID : The ID of a Guild. // userID : The ID of a User // days : The number of days of previous comments to delete. -func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { +func (s *Session) GuildBanCreate(guildID, userID int64, days int) (err error) { return s.GuildBanCreateWithReason(guildID, userID, "", days) } @@ -695,7 +790,7 @@ func (s *Session) GuildBanCreate(guildID, userID string, days int) (err error) { // userID : The ID of a User // reason : The reason for this ban // days : The number of days of previous comments to delete. -func (s *Session) GuildBanCreateWithReason(guildID, userID, reason string, days int) (err error) { +func (s *Session) GuildBanCreateWithReason(guildID, userID int64, reason string, days int) (err error) { uri := EndpointGuildBan(guildID, userID) @@ -711,16 +806,16 @@ func (s *Session) GuildBanCreateWithReason(guildID, userID, reason string, days uri += "?" + queryParams.Encode() } - _, err = s.RequestWithBucketID("PUT", uri, nil, EndpointGuildBan(guildID, "")) + _, err = s.RequestWithBucketID("PUT", uri, nil, EndpointGuildBan(guildID, 0)) return } // GuildBanDelete removes the given user from the guild bans // guildID : The ID of a Guild. // userID : The ID of a User -func (s *Session) GuildBanDelete(guildID, userID string) (err error) { +func (s *Session) GuildBanDelete(guildID, userID int64) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointGuildBan(guildID, userID), nil, EndpointGuildBan(guildID, "")) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildBan(guildID, userID), nil, EndpointGuildBan(guildID, 0)) return } @@ -728,14 +823,14 @@ func (s *Session) GuildBanDelete(guildID, userID string) (err error) { // guildID : The ID of a Guild. // after : The id of the member to return members after // limit : max number of members to return (max 1000) -func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*Member, err error) { +func (s *Session) GuildMembers(guildID int64, after int64, limit int) (st []*Member, err error) { uri := EndpointGuildMembers(guildID) v := url.Values{} - if after != "" { - v.Set("after", after) + if after != 0 { + v.Set("after", StrID(after)) } if limit > 0 { @@ -758,9 +853,9 @@ func (s *Session) GuildMembers(guildID string, after string, limit int) (st []*M // GuildMember returns a member of a guild. // guildID : The ID of a Guild. // userID : The ID of a User -func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { +func (s *Session) GuildMember(guildID, userID int64) (st *Member, err error) { - body, err := s.RequestWithBucketID("GET", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, "")) + body, err := s.RequestWithBucketID("GET", EndpointGuildMember(guildID, userID), nil, EndpointGuildMember(guildID, 0)) if err != nil { return } @@ -777,17 +872,17 @@ func (s *Session) GuildMember(guildID, userID string) (st *Member, err error) { // roles : A list of role ID's to set on the member. // mute : If the user is muted. // deaf : If the user is deafened. -func (s *Session) GuildMemberAdd(accessToken, guildID, userID, nick string, roles []string, mute, deaf bool) (err error) { +func (s *Session) GuildMemberAdd(accessToken string, guildID, userID int64, nick string, roles []int64, mute, deaf bool) (err error) { data := struct { - AccessToken string `json:"access_token"` - Nick string `json:"nick,omitempty"` - Roles []string `json:"roles,omitempty"` - Mute bool `json:"mute,omitempty"` - Deaf bool `json:"deaf,omitempty"` + AccessToken string `json:"access_token"` + Nick string `json:"nick,omitempty"` + Roles IDSlice `json:"roles,omitempty"` + Mute bool `json:"mute,omitempty"` + Deaf bool `json:"deaf,omitempty"` }{accessToken, nick, roles, mute, deaf} - _, err = s.RequestWithBucketID("PUT", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + _, err = s.RequestWithBucketID("PUT", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, 0)) if err != nil { return err } @@ -798,7 +893,7 @@ func (s *Session) GuildMemberAdd(accessToken, guildID, userID, nick string, role // GuildMemberDelete removes the given user from the given guild. // guildID : The ID of a Guild. // userID : The ID of a User -func (s *Session) GuildMemberDelete(guildID, userID string) (err error) { +func (s *Session) GuildMemberDelete(guildID, userID int64) (err error) { return s.GuildMemberDeleteWithReason(guildID, userID, "") } @@ -807,14 +902,14 @@ func (s *Session) GuildMemberDelete(guildID, userID string) (err error) { // guildID : The ID of a Guild. // userID : The ID of a User // reason : The reason for the kick -func (s *Session) GuildMemberDeleteWithReason(guildID, userID, reason string) (err error) { +func (s *Session) GuildMemberDeleteWithReason(guildID, userID int64, reason string) (err error) { uri := EndpointGuildMember(guildID, userID) if reason != "" { uri += "?reason=" + url.QueryEscape(reason) } - _, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointGuildMember(guildID, "")) + _, err = s.RequestWithBucketID("DELETE", uri, nil, EndpointGuildMember(guildID, 0)) return } @@ -822,13 +917,13 @@ func (s *Session) GuildMemberDeleteWithReason(guildID, userID, reason string) (e // guildID : The ID of a Guild. // userID : The ID of a User. // roles : A list of role ID's to set on the member. -func (s *Session) GuildMemberEdit(guildID, userID string, roles []string) (err error) { +func (s *Session) GuildMemberEdit(guildID, userID int64, roles []string) (err error) { data := struct { Roles []string `json:"roles"` }{roles} - _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, 0)) if err != nil { return } @@ -842,13 +937,13 @@ func (s *Session) GuildMemberEdit(guildID, userID string, roles []string) (err e // channelID : The ID of a channel to move user to, or null? // NOTE : I am not entirely set on the name of this function and it may change // prior to the final 1.0.0 release of Discordgo -func (s *Session) GuildMemberMove(guildID, userID, channelID string) (err error) { +func (s *Session) GuildMemberMove(guildID, userID, channelID int64) (err error) { data := struct { - ChannelID string `json:"channel_id"` - }{channelID} + ChannelID nullIntID `json:"channel_id"` + }{nullIntID(channelID)} - _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, 0)) if err != nil { return } @@ -858,19 +953,28 @@ func (s *Session) GuildMemberMove(guildID, userID, channelID string) (err error) // GuildMemberNickname updates the nickname of a guild member // guildID : The ID of a guild -// userID : The ID of a user // userID : The ID of a user or "@me" which is a shortcut of the current user ID -func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err error) { +// nickname : The new nickname +func (s *Session) GuildMemberNickname(guildID, userID int64, nickname string) (err error) { data := struct { Nick string `json:"nick"` }{nickname} - if userID == "@me" { - userID += "/nick" - } + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, 0)) + return +} + +// GuildMemberNicknameMe updates the nickname the current user +// guildID : The ID of a guild +// nickname : The new nickname +func (s *Session) GuildMemberNicknameMe(guildID int64, nickname string) (err error) { + + data := struct { + Nick string `json:"nick"` + }{nickname} - _, err = s.RequestWithBucketID("PATCH", EndpointGuildMember(guildID, userID), data, EndpointGuildMember(guildID, "")) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildMemberMe(guildID)+"/nick", data, EndpointGuildMember(guildID, 0)) return } @@ -878,9 +982,9 @@ func (s *Session) GuildMemberNickname(guildID, userID, nickname string) (err err // guildID : The ID of a Guild. // userID : The ID of a User. // roleID : The ID of a Role to be assigned to the user. -func (s *Session) GuildMemberRoleAdd(guildID, userID, roleID string) (err error) { +func (s *Session) GuildMemberRoleAdd(guildID, userID, roleID int64) (err error) { - _, err = s.RequestWithBucketID("PUT", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, "", "")) + _, err = s.RequestWithBucketID("PUT", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, 0, 0)) return } @@ -889,9 +993,9 @@ func (s *Session) GuildMemberRoleAdd(guildID, userID, roleID string) (err error) // guildID : The ID of a Guild. // userID : The ID of a User. // roleID : The ID of a Role to be removed from the user. -func (s *Session) GuildMemberRoleRemove(guildID, userID, roleID string) (err error) { +func (s *Session) GuildMemberRoleRemove(guildID, userID, roleID int64) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, "", "")) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildMemberRole(guildID, userID, roleID), nil, EndpointGuildMemberRole(guildID, 0, 0)) return } @@ -899,9 +1003,9 @@ func (s *Session) GuildMemberRoleRemove(guildID, userID, roleID string) (err err // GuildChannels returns an array of Channel structures for all channels of a // given guild. // guildID : The ID of a Guild. -func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { +func (s *Session) GuildChannels(guildID int64) (st []*Channel, err error) { - body, err := s.request("GET", EndpointGuildChannels(guildID), "", nil, EndpointGuildChannels(guildID), 0) + body, err := s.request("GET", EndpointGuildChannels(guildID), "", nil, EndpointGuildChannels(guildID)) if err != nil { return } @@ -915,7 +1019,7 @@ func (s *Session) GuildChannels(guildID string) (st []*Channel, err error) { // guildID : The ID of a Guild. // name : Name of the channel (2-100 chars length) // ctype : Type of the channel -func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (st *Channel, err error) { +func (s *Session) GuildChannelCreate(guildID int64, name string, ctype ChannelType) (st *Channel, err error) { data := struct { Name string `json:"name"` @@ -931,14 +1035,37 @@ func (s *Session) GuildChannelCreate(guildID, name string, ctype ChannelType) (s return } +// GuildChannelCreateWithOverwrites creates a new channel in the given guild +// guildID : The ID of a Guild. +// name : Name of the channel (2-100 chars length) +// ctype : Type of the channel +// overwrites : slice of permission overwrites +func (s *Session) GuildChannelCreateWithOverwrites(guildID int64, name string, ctype ChannelType, parentID int64, overwrites []*PermissionOverwrite) (st *Channel, err error) { + + data := struct { + Name string `json:"name"` + Type ChannelType `json:"type"` + ParentID int64 `json:"parent_id,string"` + PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites"` + }{name, ctype, parentID, overwrites} + + body, err := s.RequestWithBucketID("POST", EndpointGuildChannels(guildID), data, EndpointGuildChannels(guildID)) + if err != nil { + return + } + + err = unmarshal(body, &st) + return +} + // GuildChannelsReorder updates the order of channels in a guild // guildID : The ID of a Guild. // channels : Updated channels. -func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err error) { +func (s *Session) GuildChannelsReorder(guildID int64, channels []*Channel) (err error) { data := make([]struct { - ID string `json:"id"` - Position int `json:"position"` + ID int64 `json:"id,string"` + Position int `json:"position"` }, len(channels)) for i, c := range channels { @@ -952,7 +1079,7 @@ func (s *Session) GuildChannelsReorder(guildID string, channels []*Channel) (err // GuildInvites returns an array of Invite structures for the given guild // guildID : The ID of a Guild. -func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) { +func (s *Session) GuildInvites(guildID int64) (st []*Invite, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuildInvites(guildID), nil, EndpointGuildInvites(guildID)) if err != nil { return @@ -964,7 +1091,7 @@ func (s *Session) GuildInvites(guildID string) (st []*Invite, err error) { // GuildRoles returns all roles for a given guild. // guildID : The ID of a Guild. -func (s *Session) GuildRoles(guildID string) (st []*Role, err error) { +func (s *Session) GuildRoles(guildID int64) (st []*Role, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuildRoles(guildID), nil, EndpointGuildRoles(guildID)) if err != nil { @@ -978,7 +1105,7 @@ func (s *Session) GuildRoles(guildID string) (st []*Role, err error) { // GuildRoleCreate returns a new Guild Role. // guildID: The ID of a Guild. -func (s *Session) GuildRoleCreate(guildID string) (st *Role, err error) { +func (s *Session) GuildRoleCreate(guildID int64) (st *Role, err error) { body, err := s.RequestWithBucketID("POST", EndpointGuildRoles(guildID), nil, EndpointGuildRoles(guildID)) if err != nil { @@ -998,7 +1125,7 @@ func (s *Session) GuildRoleCreate(guildID string) (st *Role, err error) { // hoist : Whether to display the role's users separately. // perm : The permissions for the role. // mention : Whether this role is mentionable -func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist bool, perm int, mention bool) (st *Role, err error) { +func (s *Session) GuildRoleEdit(guildID, roleID int64, name string, color int, hoist bool, perm int, mention bool) (st *Role, err error) { // Prevent sending a color int that is too big. if color > 0xFFFFFF { @@ -1014,7 +1141,7 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b Mentionable bool `json:"mentionable"` // Whether this role is mentionable }{name, color, hoist, perm, mention} - body, err := s.RequestWithBucketID("PATCH", EndpointGuildRole(guildID, roleID), data, EndpointGuildRole(guildID, "")) + body, err := s.RequestWithBucketID("PATCH", EndpointGuildRole(guildID, roleID), data, EndpointGuildRole(guildID, 0)) if err != nil { return } @@ -1027,7 +1154,7 @@ func (s *Session) GuildRoleEdit(guildID, roleID, name string, color int, hoist b // GuildRoleReorder reoders guild roles // guildID : The ID of a Guild. // roles : A list of ordered roles. -func (s *Session) GuildRoleReorder(guildID string, roles []*Role) (st []*Role, err error) { +func (s *Session) GuildRoleReorder(guildID int64, roles []*Role) (st []*Role, err error) { body, err := s.RequestWithBucketID("PATCH", EndpointGuildRoles(guildID), roles, EndpointGuildRoles(guildID)) if err != nil { @@ -1042,9 +1169,9 @@ func (s *Session) GuildRoleReorder(guildID string, roles []*Role) (st []*Role, e // GuildRoleDelete deletes an existing role. // guildID : The ID of a Guild. // roleID : The ID of a Role. -func (s *Session) GuildRoleDelete(guildID, roleID string) (err error) { +func (s *Session) GuildRoleDelete(guildID, roleID int64) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointGuildRole(guildID, roleID), nil, EndpointGuildRole(guildID, "")) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildRole(guildID, roleID), nil, EndpointGuildRole(guildID, 0)) return } @@ -1053,7 +1180,7 @@ func (s *Session) GuildRoleDelete(guildID, roleID string) (err error) { // Requires 'KICK_MEMBER' permission. // guildID : The ID of a Guild. // days : The number of days to count prune for (1 or more). -func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, err error) { +func (s *Session) GuildPruneCount(guildID int64, days uint32) (count uint32, err error) { count = 0 if days <= 0 { @@ -1085,7 +1212,7 @@ func (s *Session) GuildPruneCount(guildID string, days uint32) (count uint32, er // Returns an object with one 'pruned' key indicating the number of members that were removed in the prune operation. // guildID : The ID of a Guild. // days : The number of days to count prune for (1 or more). -func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err error) { +func (s *Session) GuildPrune(guildID int64, days uint32) (count uint32, err error) { count = 0 @@ -1119,7 +1246,7 @@ func (s *Session) GuildPrune(guildID string, days uint32) (count uint32, err err // GuildIntegrations returns an array of Integrations for a guild. // guildID : The ID of a Guild. -func (s *Session) GuildIntegrations(guildID string) (st []*Integration, err error) { +func (s *Session) GuildIntegrations(guildID int64) (st []*Integration, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuildIntegrations(guildID), nil, EndpointGuildIntegrations(guildID)) if err != nil { @@ -1135,11 +1262,11 @@ func (s *Session) GuildIntegrations(guildID string) (st []*Integration, err erro // guildID : The ID of a Guild. // integrationType : The Integration type. // integrationID : The ID of an integration. -func (s *Session) GuildIntegrationCreate(guildID, integrationType, integrationID string) (err error) { +func (s *Session) GuildIntegrationCreate(guildID int64, integrationType string, integrationID int64) (err error) { data := struct { Type string `json:"type"` - ID string `json:"id"` + ID int64 `json:"id,string"` }{integrationType, integrationID} _, err = s.RequestWithBucketID("POST", EndpointGuildIntegrations(guildID), data, EndpointGuildIntegrations(guildID)) @@ -1153,7 +1280,7 @@ func (s *Session) GuildIntegrationCreate(guildID, integrationType, integrationID // expireBehavior : The behavior when an integration subscription lapses (see the integration object documentation). // expireGracePeriod : Period (in seconds) where the integration will ignore lapsed subscriptions. // enableEmoticons : Whether emoticons should be synced for this integration (twitch only currently). -func (s *Session) GuildIntegrationEdit(guildID, integrationID string, expireBehavior, expireGracePeriod int, enableEmoticons bool) (err error) { +func (s *Session) GuildIntegrationEdit(guildID, integrationID int64, expireBehavior, expireGracePeriod int, enableEmoticons bool) (err error) { data := struct { ExpireBehavior int `json:"expire_behavior"` @@ -1161,31 +1288,31 @@ func (s *Session) GuildIntegrationEdit(guildID, integrationID string, expireBeha EnableEmoticons bool `json:"enable_emoticons"` }{expireBehavior, expireGracePeriod, enableEmoticons} - _, err = s.RequestWithBucketID("PATCH", EndpointGuildIntegration(guildID, integrationID), data, EndpointGuildIntegration(guildID, "")) + _, err = s.RequestWithBucketID("PATCH", EndpointGuildIntegration(guildID, integrationID), data, EndpointGuildIntegration(guildID, 0)) return } // GuildIntegrationDelete removes the given integration from the Guild. // guildID : The ID of a Guild. // integrationID : The ID of an integration. -func (s *Session) GuildIntegrationDelete(guildID, integrationID string) (err error) { +func (s *Session) GuildIntegrationDelete(guildID, integrationID int64) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointGuildIntegration(guildID, integrationID), nil, EndpointGuildIntegration(guildID, "")) + _, err = s.RequestWithBucketID("DELETE", EndpointGuildIntegration(guildID, integrationID), nil, EndpointGuildIntegration(guildID, 0)) return } // GuildIntegrationSync syncs an integration. // guildID : The ID of a Guild. // integrationID : The ID of an integration. -func (s *Session) GuildIntegrationSync(guildID, integrationID string) (err error) { +func (s *Session) GuildIntegrationSync(guildID, integrationID int64) (err error) { - _, err = s.RequestWithBucketID("POST", EndpointGuildIntegrationSync(guildID, integrationID), nil, EndpointGuildIntegration(guildID, "")) + _, err = s.RequestWithBucketID("POST", EndpointGuildIntegrationSync(guildID, integrationID), nil, EndpointGuildIntegration(guildID, 0)) return } // GuildIcon returns an image.Image of a guild icon. // guildID : The ID of a Guild. -func (s *Session) GuildIcon(guildID string) (img image.Image, err error) { +func (s *Session) GuildIcon(guildID int64) (img image.Image, err error) { g, err := s.Guild(guildID) if err != nil { return @@ -1207,7 +1334,7 @@ func (s *Session) GuildIcon(guildID string) (img image.Image, err error) { // GuildSplash returns an image.Image of a guild splash image. // guildID : The ID of a Guild. -func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { +func (s *Session) GuildSplash(guildID int64) (img image.Image, err error) { g, err := s.Guild(guildID) if err != nil { return @@ -1229,7 +1356,7 @@ func (s *Session) GuildSplash(guildID string) (img image.Image, err error) { // GuildEmbed returns the embed for a Guild. // guildID : The ID of a Guild. -func (s *Session) GuildEmbed(guildID string) (st *GuildEmbed, err error) { +func (s *Session) GuildEmbed(guildID int64) (st *GuildEmbed, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuildEmbed(guildID), nil, EndpointGuildEmbed(guildID)) if err != nil { @@ -1242,7 +1369,7 @@ func (s *Session) GuildEmbed(guildID string) (st *GuildEmbed, err error) { // GuildEmbedEdit returns the embed for a Guild. // guildID : The ID of a Guild. -func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string) (err error) { +func (s *Session) GuildEmbedEdit(guildID int64, enabled bool, channelID int64) (err error) { data := GuildEmbed{enabled, channelID} @@ -1256,16 +1383,16 @@ func (s *Session) GuildEmbedEdit(guildID string, enabled bool, channelID string) // beforeID : If provided all log entries returned will be before the given ID. // actionType : If provided the log will be filtered for the given Action Type. // limit : The number messages that can be returned. (default 50, min 1, max 100) -func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, limit int) (st *GuildAuditLog, err error) { +func (s *Session) GuildAuditLog(guildID, userID, beforeID int64, actionType, limit int) (st *GuildAuditLog, err error) { uri := EndpointGuildAuditLogs(guildID) v := url.Values{} - if userID != "" { - v.Set("user_id", userID) + if userID != 0 { + v.Set("user_id", StrID(userID)) } - if beforeID != "" { - v.Set("before", beforeID) + if beforeID != 0 { + v.Set("before", StrID(beforeID)) } if actionType > 0 { v.Set("action_type", strconv.Itoa(actionType)) @@ -1291,12 +1418,12 @@ func (s *Session) GuildAuditLog(guildID, userID, beforeID string, actionType, li // name : The Name of the Emoji. // image : The base64 encoded emoji image, has to be smaller than 256KB. // roles : The roles for which this emoji will be whitelisted, can be nil. -func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) (emoji *Emoji, err error) { +func (s *Session) GuildEmojiCreate(guildID int64, name, image string, roles []int64) (emoji *Emoji, err error) { data := struct { - Name string `json:"name"` - Image string `json:"image"` - Roles []string `json:"roles,omitempty"` + Name string `json:"name"` + Image string `json:"image"` + Roles IDSlice `json:"roles,omitempty"` }{name, image, roles} body, err := s.RequestWithBucketID("POST", EndpointGuildEmojis(guildID), data, EndpointGuildEmojis(guildID)) @@ -1313,11 +1440,11 @@ func (s *Session) GuildEmojiCreate(guildID, name, image string, roles []string) // emojiID : The ID of an Emoji. // name : The Name of the Emoji. // roles : The roles for which this emoji will be whitelisted, can be nil. -func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) (emoji *Emoji, err error) { +func (s *Session) GuildEmojiEdit(guildID, emojiID int64, name string, roles []int64) (emoji *Emoji, err error) { data := struct { - Name string `json:"name"` - Roles []string `json:"roles,omitempty"` + Name string `json:"name"` + Roles IDSlice `json:"roles,omitempty"` }{name, roles} body, err := s.RequestWithBucketID("PATCH", EndpointGuildEmoji(guildID, emojiID), data, EndpointGuildEmojis(guildID)) @@ -1332,7 +1459,7 @@ func (s *Session) GuildEmojiEdit(guildID, emojiID, name string, roles []string) // GuildEmojiDelete deletes an Emoji. // guildID : The ID of a Guild. // emojiID : The ID of an Emoji. -func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) { +func (s *Session) GuildEmojiDelete(guildID, emojiID int64) (err error) { _, err = s.RequestWithBucketID("DELETE", EndpointGuildEmoji(guildID, emojiID), nil, EndpointGuildEmojis(guildID)) return @@ -1344,7 +1471,7 @@ func (s *Session) GuildEmojiDelete(guildID, emojiID string) (err error) { // Channel returns a Channel structure of a specific Channel. // channelID : The ID of the Channel you want returned. -func (s *Session) Channel(channelID string) (st *Channel, err error) { +func (s *Session) Channel(channelID int64) (st *Channel, err error) { body, err := s.RequestWithBucketID("GET", EndpointChannel(channelID), nil, EndpointChannel(channelID)) if err != nil { return @@ -1357,7 +1484,7 @@ func (s *Session) Channel(channelID string) (st *Channel, err error) { // ChannelEdit edits the given channel // channelID : The ID of a Channel // name : The new name to assign the channel. -func (s *Session) ChannelEdit(channelID, name string) (*Channel, error) { +func (s *Session) ChannelEdit(channelID int64, name string) (*Channel, error) { return s.ChannelEditComplex(channelID, &ChannelEdit{ Name: name, }) @@ -1366,7 +1493,7 @@ func (s *Session) ChannelEdit(channelID, name string) (*Channel, error) { // ChannelEditComplex edits an existing channel, replacing the parameters entirely with ChannelEdit struct // channelID : The ID of a Channel // data : The channel struct to send -func (s *Session) ChannelEditComplex(channelID string, data *ChannelEdit) (st *Channel, err error) { +func (s *Session) ChannelEditComplex(channelID int64, data *ChannelEdit) (st *Channel, err error) { body, err := s.RequestWithBucketID("PATCH", EndpointChannel(channelID), data, EndpointChannel(channelID)) if err != nil { return @@ -1378,7 +1505,7 @@ func (s *Session) ChannelEditComplex(channelID string, data *ChannelEdit) (st *C // ChannelDelete deletes the given channel // channelID : The ID of a Channel -func (s *Session) ChannelDelete(channelID string) (st *Channel, err error) { +func (s *Session) ChannelDelete(channelID int64) (st *Channel, err error) { body, err := s.RequestWithBucketID("DELETE", EndpointChannel(channelID), nil, EndpointChannel(channelID)) if err != nil { @@ -1392,7 +1519,7 @@ func (s *Session) ChannelDelete(channelID string) (st *Channel, err error) { // ChannelTyping broadcasts to all members that authenticated user is typing in // the given channel. // channelID : The ID of a Channel -func (s *Session) ChannelTyping(channelID string) (err error) { +func (s *Session) ChannelTyping(channelID int64) (err error) { _, err = s.RequestWithBucketID("POST", EndpointChannelTyping(channelID), nil, EndpointChannelTyping(channelID)) return @@ -1405,7 +1532,7 @@ func (s *Session) ChannelTyping(channelID string) (err error) { // beforeID : If provided all messages returned will be before given ID. // afterID : If provided all messages returned will be after given ID. // aroundID : If provided all messages returned will be around given ID. -func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID, aroundID string) (st []*Message, err error) { +func (s *Session) ChannelMessages(channelID int64, limit int, beforeID, afterID, aroundID int64) (st []*Message, err error) { uri := EndpointChannelMessages(channelID) @@ -1413,15 +1540,16 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID if limit > 0 { v.Set("limit", strconv.Itoa(limit)) } - if afterID != "" { - v.Set("after", afterID) + if afterID != 0 { + v.Set("after", StrID(afterID)) } - if beforeID != "" { - v.Set("before", beforeID) + if beforeID != 0 { + v.Set("before", StrID(beforeID)) } - if aroundID != "" { - v.Set("around", aroundID) + if aroundID != 0 { + v.Set("around", StrID(aroundID)) } + if len(v) > 0 { uri = fmt.Sprintf("%s?%s", uri, v.Encode()) } @@ -1438,9 +1566,9 @@ func (s *Session) ChannelMessages(channelID string, limit int, beforeID, afterID // ChannelMessage gets a single message by ID from a given channel. // channeld : The ID of a Channel // messageID : the ID of a Message -func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err error) { +func (s *Session) ChannelMessage(channelID, messageID int64) (st *Message, err error) { - response, err := s.RequestWithBucketID("GET", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, "")) + response, err := s.RequestWithBucketID("GET", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, 0)) if err != nil { return } @@ -1453,9 +1581,9 @@ func (s *Session) ChannelMessage(channelID, messageID string) (st *Message, err // channeld : The ID of a Channel // messageID : the ID of a Message // lastToken : token returned by last ack -func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st *Ack, err error) { +func (s *Session) ChannelMessageAck(channelID, messageID int64, lastToken string) (st *Ack, err error) { - body, err := s.RequestWithBucketID("POST", EndpointChannelMessageAck(channelID, messageID), &Ack{Token: lastToken}, EndpointChannelMessageAck(channelID, "")) + body, err := s.RequestWithBucketID("POST", EndpointChannelMessageAck(channelID, messageID), &Ack{Token: lastToken}, EndpointChannelMessageAck(channelID, 0)) if err != nil { return } @@ -1467,7 +1595,7 @@ func (s *Session) ChannelMessageAck(channelID, messageID, lastToken string) (st // ChannelMessageSend sends a message to the given channel. // channelID : The ID of a Channel. // content : The message to send. -func (s *Session) ChannelMessageSend(channelID string, content string) (*Message, error) { +func (s *Session) ChannelMessageSend(channelID int64, content string) (*Message, error) { return s.ChannelMessageSendComplex(channelID, &MessageSend{ Content: content, }) @@ -1478,7 +1606,7 @@ var quoteEscaper = strings.NewReplacer("\\", "\\\\", `"`, "\\\"") // ChannelMessageSendComplex sends a message to the given channel. // channelID : The ID of a Channel. // data : The message struct to send. -func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) (st *Message, err error) { +func (s *Session) ChannelMessageSendComplex(channelID int64, data *MessageSend) (st *Message, err error) { if data.Embed != nil && data.Embed.Type == "" { data.Embed.Type = "rich" } @@ -1546,7 +1674,7 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) return } - response, err = s.request("POST", endpoint, bodywriter.FormDataContentType(), body.Bytes(), endpoint, 0) + response, err = s.request("POST", endpoint, bodywriter.FormDataContentType(), body.Bytes(), endpoint) } else { response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint) } @@ -1561,7 +1689,7 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend) // ChannelMessageSendTTS sends a message to the given channel with Text to Speech. // channelID : The ID of a Channel. // content : The message to send. -func (s *Session) ChannelMessageSendTTS(channelID string, content string) (*Message, error) { +func (s *Session) ChannelMessageSendTTS(channelID int64, content string) (*Message, error) { return s.ChannelMessageSendComplex(channelID, &MessageSend{ Content: content, Tts: true, @@ -1571,7 +1699,7 @@ func (s *Session) ChannelMessageSendTTS(channelID string, content string) (*Mess // ChannelMessageSendEmbed sends a message to the given channel with embedded data. // channelID : The ID of a Channel. // embed : The embed data to send. -func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) (*Message, error) { +func (s *Session) ChannelMessageSendEmbed(channelID int64, embed *MessageEmbed) (*Message, error) { return s.ChannelMessageSendComplex(channelID, &MessageSend{ Embed: embed, }) @@ -1582,7 +1710,7 @@ func (s *Session) ChannelMessageSendEmbed(channelID string, embed *MessageEmbed) // channelID : The ID of a Channel // messageID : The ID of a Message // content : The contents of the message -func (s *Session) ChannelMessageEdit(channelID, messageID, content string) (*Message, error) { +func (s *Session) ChannelMessageEdit(channelID, messageID int64, content string) (*Message, error) { return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetContent(content)) } @@ -1593,7 +1721,7 @@ func (s *Session) ChannelMessageEditComplex(m *MessageEdit) (st *Message, err er m.Embed.Type = "rich" } - response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(m.Channel, m.ID), m, EndpointChannelMessage(m.Channel, "")) + response, err := s.RequestWithBucketID("PATCH", EndpointChannelMessage(m.Channel, m.ID), m, EndpointChannelMessage(m.Channel, 0)) if err != nil { return } @@ -1606,14 +1734,14 @@ func (s *Session) ChannelMessageEditComplex(m *MessageEdit) (st *Message, err er // channelID : The ID of a Channel // messageID : The ID of a Message // embed : The embed data to send -func (s *Session) ChannelMessageEditEmbed(channelID, messageID string, embed *MessageEmbed) (*Message, error) { +func (s *Session) ChannelMessageEditEmbed(channelID, messageID int64, embed *MessageEmbed) (*Message, error) { return s.ChannelMessageEditComplex(NewMessageEdit(channelID, messageID).SetEmbed(embed)) } // ChannelMessageDelete deletes a message from the Channel. -func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error) { +func (s *Session) ChannelMessageDelete(channelID, messageID int64) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, "")) + _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessage(channelID, messageID), nil, EndpointChannelMessage(channelID, 0)) return } @@ -1621,8 +1749,8 @@ func (s *Session) ChannelMessageDelete(channelID, messageID string) (err error) // If only one messageID is in the slice call channelMessageDelete function. // If the slice is empty do nothing. // channelID : The ID of the channel for the messages to delete. -// messages : The IDs of the messages to be deleted. A slice of string IDs. A maximum of 100 messages. -func (s *Session) ChannelMessagesBulkDelete(channelID string, messages []string) (err error) { +// messages : The IDs of the messages to be deleted. A slice of message IDs. A maximum of 100 messages. +func (s *Session) ChannelMessagesBulkDelete(channelID int64, messages []int64) (err error) { if len(messages) == 0 { return @@ -1638,7 +1766,7 @@ func (s *Session) ChannelMessagesBulkDelete(channelID string, messages []string) } data := struct { - Messages []string `json:"messages"` + Messages IDSlice `json:"messages"` }{messages} _, err = s.RequestWithBucketID("POST", EndpointChannelMessagesBulkDelete(channelID), data, EndpointChannelMessagesBulkDelete(channelID)) @@ -1648,25 +1776,25 @@ func (s *Session) ChannelMessagesBulkDelete(channelID string, messages []string) // ChannelMessagePin pins a message within a given channel. // channelID: The ID of a channel. // messageID: The ID of a message. -func (s *Session) ChannelMessagePin(channelID, messageID string) (err error) { +func (s *Session) ChannelMessagePin(channelID, messageID int64) (err error) { - _, err = s.RequestWithBucketID("PUT", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, "")) + _, err = s.RequestWithBucketID("PUT", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, 0)) return } // ChannelMessageUnpin unpins a message within a given channel. // channelID: The ID of a channel. // messageID: The ID of a message. -func (s *Session) ChannelMessageUnpin(channelID, messageID string) (err error) { +func (s *Session) ChannelMessageUnpin(channelID, messageID int64) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, "")) + _, err = s.RequestWithBucketID("DELETE", EndpointChannelMessagePin(channelID, messageID), nil, EndpointChannelMessagePin(channelID, 0)) return } // ChannelMessagesPinned returns an array of Message structures for pinned messages // within a given channel // channelID : The ID of a Channel. -func (s *Session) ChannelMessagesPinned(channelID string) (st []*Message, err error) { +func (s *Session) ChannelMessagesPinned(channelID int64) (st []*Message, err error) { body, err := s.RequestWithBucketID("GET", EndpointChannelMessagesPins(channelID), nil, EndpointChannelMessagesPins(channelID)) @@ -1682,7 +1810,7 @@ func (s *Session) ChannelMessagesPinned(channelID string) (st []*Message, err er // channelID : The ID of a Channel. // name: The name of the file. // io.Reader : A reader for the file contents. -func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (*Message, error) { +func (s *Session) ChannelFileSend(channelID int64, name string, r io.Reader) (*Message, error) { return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}}) } @@ -1692,13 +1820,13 @@ func (s *Session) ChannelFileSend(channelID, name string, r io.Reader) (*Message // content: Optional Message content. // name: The name of the file. // io.Reader : A reader for the file contents. -func (s *Session) ChannelFileSendWithMessage(channelID, content string, name string, r io.Reader) (*Message, error) { +func (s *Session) ChannelFileSendWithMessage(channelID int64, content string, name string, r io.Reader) (*Message, error) { return s.ChannelMessageSendComplex(channelID, &MessageSend{File: &File{Name: name, Reader: r}, Content: content}) } // ChannelInvites returns an array of Invite structures for the given channel // channelID : The ID of a Channel -func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) { +func (s *Session) ChannelInvites(channelID int64) (st []*Invite, err error) { body, err := s.RequestWithBucketID("GET", EndpointChannelInvites(channelID), nil, EndpointChannelInvites(channelID)) if err != nil { @@ -1712,7 +1840,7 @@ func (s *Session) ChannelInvites(channelID string) (st []*Invite, err error) { // ChannelInviteCreate creates a new invite for the given channel. // channelID : The ID of a Channel // i : An Invite struct with the values MaxAge, MaxUses and Temporary defined. -func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, err error) { +func (s *Session) ChannelInviteCreate(channelID int64, i Invite) (st *Invite, err error) { data := struct { MaxAge int `json:"max_age"` @@ -1733,24 +1861,24 @@ func (s *Session) ChannelInviteCreate(channelID string, i Invite) (st *Invite, e // ChannelPermissionSet creates a Permission Override for the given channel. // NOTE: This func name may changed. Using Set instead of Create because // you can both create a new override or update an override with this function. -func (s *Session) ChannelPermissionSet(channelID, targetID, targetType string, allow, deny int) (err error) { +func (s *Session) ChannelPermissionSet(channelID, targetID int64, targetType string, allow, deny int) (err error) { data := struct { - ID string `json:"id"` + ID int64 `json:"id,string"` Type string `json:"type"` Allow int `json:"allow"` Deny int `json:"deny"` }{targetID, targetType, allow, deny} - _, err = s.RequestWithBucketID("PUT", EndpointChannelPermission(channelID, targetID), data, EndpointChannelPermission(channelID, "")) + _, err = s.RequestWithBucketID("PUT", EndpointChannelPermission(channelID, targetID), data, EndpointChannelPermission(channelID, 0)) return } // ChannelPermissionDelete deletes a specific permission override for the given channel. // NOTE: Name of this func may change. -func (s *Session) ChannelPermissionDelete(channelID, targetID string) (err error) { +func (s *Session) ChannelPermissionDelete(channelID, targetID int64) (err error) { - _, err = s.RequestWithBucketID("DELETE", EndpointChannelPermission(channelID, targetID), nil, EndpointChannelPermission(channelID, "")) + _, err = s.RequestWithBucketID("DELETE", EndpointChannelPermission(channelID, targetID), nil, EndpointChannelPermission(channelID, 0)) return } @@ -1898,7 +2026,7 @@ func (s *Session) GatewayBot() (st *GatewayBotResponse, err error) { // channelID: The ID of a Channel. // name : The name of the webhook. // avatar : The avatar of the webhook. -func (s *Session) WebhookCreate(channelID, name, avatar string) (st *Webhook, err error) { +func (s *Session) WebhookCreate(channelID int64, name, avatar string) (st *Webhook, err error) { data := struct { Name string `json:"name"` @@ -1917,7 +2045,7 @@ func (s *Session) WebhookCreate(channelID, name, avatar string) (st *Webhook, er // ChannelWebhooks returns all webhooks for a given channel. // channelID: The ID of a channel. -func (s *Session) ChannelWebhooks(channelID string) (st []*Webhook, err error) { +func (s *Session) ChannelWebhooks(channelID int64) (st []*Webhook, err error) { body, err := s.RequestWithBucketID("GET", EndpointChannelWebhooks(channelID), nil, EndpointChannelWebhooks(channelID)) if err != nil { @@ -1931,7 +2059,7 @@ func (s *Session) ChannelWebhooks(channelID string) (st []*Webhook, err error) { // GuildWebhooks returns all webhooks for a given guild. // guildID: The ID of a Guild. -func (s *Session) GuildWebhooks(guildID string) (st []*Webhook, err error) { +func (s *Session) GuildWebhooks(guildID int64) (st []*Webhook, err error) { body, err := s.RequestWithBucketID("GET", EndpointGuildWebhooks(guildID), nil, EndpointGuildWebhooks(guildID)) if err != nil { @@ -1945,7 +2073,7 @@ func (s *Session) GuildWebhooks(guildID string) (st []*Webhook, err error) { // Webhook returns a webhook for a given ID // webhookID: The ID of a webhook. -func (s *Session) Webhook(webhookID string) (st *Webhook, err error) { +func (s *Session) Webhook(webhookID int64) (st *Webhook, err error) { body, err := s.RequestWithBucketID("GET", EndpointWebhook(webhookID), nil, EndpointWebhooks) if err != nil { @@ -1960,9 +2088,9 @@ func (s *Session) Webhook(webhookID string) (st *Webhook, err error) { // WebhookWithToken returns a webhook for a given ID // webhookID: The ID of a webhook. // token : The auth token for the webhook. -func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err error) { +func (s *Session) WebhookWithToken(webhookID int64, token string) (st *Webhook, err error) { - body, err := s.RequestWithBucketID("GET", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken("", "")) + body, err := s.RequestWithBucketID("GET", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken(0, "")) if err != nil { return } @@ -1976,12 +2104,12 @@ func (s *Session) WebhookWithToken(webhookID, token string) (st *Webhook, err er // webhookID: The ID of a webhook. // name : The name of the webhook. // avatar : The avatar of the webhook. -func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string) (st *Role, err error) { +func (s *Session) WebhookEdit(webhookID int64, name, avatar string, channelID int64) (st *Role, err error) { data := struct { Name string `json:"name,omitempty"` Avatar string `json:"avatar,omitempty"` - ChannelID string `json:"channel_id,omitempty"` + ChannelID int64 `json:"channel_id,string,omitempty"` }{name, avatar, channelID} body, err := s.RequestWithBucketID("PATCH", EndpointWebhook(webhookID), data, EndpointWebhooks) @@ -1999,14 +2127,14 @@ func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string) (st *Ro // token : The auth token for the webhook. // name : The name of the webhook. // avatar : The avatar of the webhook. -func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (st *Role, err error) { +func (s *Session) WebhookEditWithToken(webhookID int64, token, name, avatar string) (st *Role, err error) { data := struct { Name string `json:"name,omitempty"` Avatar string `json:"avatar,omitempty"` }{name, avatar} - body, err := s.RequestWithBucketID("PATCH", EndpointWebhookToken(webhookID, token), data, EndpointWebhookToken("", "")) + body, err := s.RequestWithBucketID("PATCH", EndpointWebhookToken(webhookID, token), data, EndpointWebhookToken(0, "")) if err != nil { return } @@ -2018,8 +2146,7 @@ func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string) (s // WebhookDelete deletes a webhook for a given ID // webhookID: The ID of a webhook. -func (s *Session) WebhookDelete(webhookID string) (err error) { - +func (s *Session) WebhookDelete(webhookID int64) (err error) { _, err = s.RequestWithBucketID("DELETE", EndpointWebhook(webhookID), nil, EndpointWebhooks) return @@ -2028,9 +2155,9 @@ func (s *Session) WebhookDelete(webhookID string) (err error) { // WebhookDeleteWithToken deletes a webhook for a given ID with an auth token. // webhookID: The ID of a webhook. // token : The auth token for the webhook. -func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook, err error) { +func (s *Session) WebhookDeleteWithToken(webhookID int64, token string) (st *Webhook, err error) { - body, err := s.RequestWithBucketID("DELETE", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken("", "")) + body, err := s.RequestWithBucketID("DELETE", EndpointWebhookToken(webhookID, token), nil, EndpointWebhookToken(0, "")) if err != nil { return } @@ -2043,14 +2170,14 @@ func (s *Session) WebhookDeleteWithToken(webhookID, token string) (st *Webhook, // WebhookExecute executes a webhook. // webhookID: The ID of a webhook. // token : The auth token for the webhook -func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *WebhookParams) (err error) { +func (s *Session) WebhookExecute(webhookID int64, token string, wait bool, data *WebhookParams) (err error) { uri := EndpointWebhookToken(webhookID, token) if wait { uri += "?wait=true" } - _, err = s.RequestWithBucketID("POST", uri, data, EndpointWebhookToken("", "")) + _, err = s.RequestWithBucketID("POST", uri, data, EndpointWebhookToken(0, "")) return } @@ -2058,10 +2185,10 @@ func (s *Session) WebhookExecute(webhookID, token string, wait bool, data *Webho // MessageReactionAdd creates an emoji reaction to a message. // channelID : The channel ID. // messageID : The message ID. -// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. -func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error { +// emoji : Either the unicode emoji for the reaction, or a guild emoji identifier. +func (s *Session) MessageReactionAdd(channelID, messageID int64, emoji string) error { - _, err := s.RequestWithBucketID("PUT", EndpointMessageReaction(channelID, messageID, emojiID, "@me"), nil, EndpointMessageReaction(channelID, "", "", "")) + _, err := s.RequestWithBucketID("PUT", EndpointMessageReaction(channelID, messageID, EmojiName{emoji}, "@me"), nil, EndpointMessageReaction(channelID, 0, EmojiName{""}, "")) return err } @@ -2069,11 +2196,33 @@ func (s *Session) MessageReactionAdd(channelID, messageID, emojiID string) error // MessageReactionRemove deletes an emoji reaction to a message. // channelID : The channel ID. // messageID : The message ID. -// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. -// userID : @me or ID of the user to delete the reaction for. -func (s *Session) MessageReactionRemove(channelID, messageID, emojiID, userID string) error { +// emoji : Either the unicode emoji for the reaction, or a guild emoji identifier. +// userID : The ID of the user to delete the reaction for. +func (s *Session) MessageReactionRemove(channelID, messageID int64, emoji string, userID int64) error { - _, err := s.RequestWithBucketID("DELETE", EndpointMessageReaction(channelID, messageID, emojiID, userID), nil, EndpointMessageReaction(channelID, "", "", "")) + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReaction(channelID, messageID, EmojiName{emoji}, StrID(userID)), nil, EndpointMessageReaction(channelID, 0, EmojiName{""}, "")) + + return err +} + +// MessageReactionRemoveMe deletes an emoji reaction to a message the current user made. +// channelID : The channel ID. +// messageID : The message ID. +// emoji : Either the unicode emoji for the reaction, or a guild emoji identifier. +func (s *Session) MessageReactionRemoveMe(channelID, messageID int64, emoji string) error { + + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReaction(channelID, messageID, EmojiName{emoji}, "@me"), nil, EndpointMessageReaction(channelID, 0, EmojiName{""}, "")) + + return err +} + +// MessageReactionRemoveEmoji deletes all emoji reactions in a message. +// channelID : The channel ID. +// messageID : The message ID. +// emoji : Either the unicode emoji for the reaction, or a guild emoji identifier. +func (s *Session) MessageReactionRemoveEmoji(channelID, messageID int64, emoji string) error { + + _, err := s.RequestWithBucketID("DELETE", EndpointMessageReactions(channelID, messageID, EmojiName{emoji}), nil, EndpointMessageReactions(channelID, 0, EmojiName{""})) return err } @@ -2081,7 +2230,7 @@ func (s *Session) MessageReactionRemove(channelID, messageID, emojiID, userID st // MessageReactionsRemoveAll deletes all reactions from a message // channelID : The channel ID // messageID : The message ID. -func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error { +func (s *Session) MessageReactionsRemoveAll(channelID, messageID int64) error { _, err := s.RequestWithBucketID("DELETE", EndpointMessageReactionsAll(channelID, messageID), nil, EndpointMessageReactionsAll(channelID, messageID)) @@ -2091,22 +2240,32 @@ func (s *Session) MessageReactionsRemoveAll(channelID, messageID string) error { // MessageReactions gets all the users reactions for a specific emoji. // channelID : The channel ID. // messageID : The message ID. -// emojiID : Either the unicode emoji for the reaction, or a guild emoji identifier. -// limit : max number of users to return (max 100) -func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit int) (st []*User, err error) { - uri := EndpointMessageReactions(channelID, messageID, emojiID) +// emoji : Either the unicode emoji for the reaction, or a guild emoji identifier. +// limit : max number of users to return (max 100) +func (s *Session) MessageReactions(channelID, messageID int64, emoji string, limit int, before, after int64) (st []*User, err error) { + uri := EndpointMessageReactions(channelID, messageID, EmojiName{emoji}) v := url.Values{} if limit > 0 { + if limit > 100 { + limit = 100 + } + v.Set("limit", strconv.Itoa(limit)) } + if before != 0 { + v.Set("before", strconv.FormatInt(before, 10)) + } else if after != 0 { + v.Set("after", strconv.FormatInt(after, 10)) + } + if len(v) > 0 { uri = fmt.Sprintf("%s?%s", uri, v.Encode()) } - body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, "", "", "")) + body, err := s.RequestWithBucketID("GET", uri, nil, EndpointMessageReaction(channelID, 0, EmojiName{""}, "")) if err != nil { return } @@ -2120,12 +2279,12 @@ func (s *Session) MessageReactions(channelID, messageID, emojiID string, limit i // ------------------------------------------------------------------------------------------------ // UserNoteSet sets the note for a specific user. -func (s *Session) UserNoteSet(userID string, message string) (err error) { +func (s *Session) UserNoteSet(userID int64, message string) (err error) { data := struct { Note string `json:"note"` }{message} - _, err = s.RequestWithBucketID("PUT", EndpointUserNotes(userID), data, EndpointUserNotes("")) + _, err = s.RequestWithBucketID("PUT", EndpointUserNotes(userID), data, EndpointUserNotes(0)) return } @@ -2146,7 +2305,7 @@ func (s *Session) RelationshipsGet() (r []*Relationship, err error) { // relationshipCreate creates a new relationship. (I.e. send or accept a friend request, block a user.) // relationshipType : 1 = friend, 2 = blocked, 3 = incoming friend req, 4 = sent friend req -func (s *Session) relationshipCreate(userID string, relationshipType int) (err error) { +func (s *Session) relationshipCreate(userID int64, relationshipType int) (err error) { data := struct { Type int `json:"type"` }{relationshipType} @@ -2157,35 +2316,35 @@ func (s *Session) relationshipCreate(userID string, relationshipType int) (err e // RelationshipFriendRequestSend sends a friend request to a user. // userID: ID of the user. -func (s *Session) RelationshipFriendRequestSend(userID string) (err error) { +func (s *Session) RelationshipFriendRequestSend(userID int64) (err error) { err = s.relationshipCreate(userID, 4) return } // RelationshipFriendRequestAccept accepts a friend request from a user. // userID: ID of the user. -func (s *Session) RelationshipFriendRequestAccept(userID string) (err error) { +func (s *Session) RelationshipFriendRequestAccept(userID int64) (err error) { err = s.relationshipCreate(userID, 1) return } // RelationshipUserBlock blocks a user. // userID: ID of the user. -func (s *Session) RelationshipUserBlock(userID string) (err error) { +func (s *Session) RelationshipUserBlock(userID int64) (err error) { err = s.relationshipCreate(userID, 2) return } // RelationshipDelete removes the relationship with a user. // userID: ID of the user. -func (s *Session) RelationshipDelete(userID string) (err error) { +func (s *Session) RelationshipDelete(userID int64) (err error) { _, err = s.RequestWithBucketID("DELETE", EndpointRelationship(userID), nil, EndpointRelationships()) return } // RelationshipsMutualGet returns an array of all the users both @me and the given user is friends with. // userID: ID of the user. -func (s *Session) RelationshipsMutualGet(userID string) (mf []*User, err error) { +func (s *Session) RelationshipsMutualGet(userID int64) (mf []*User, err error) { body, err := s.RequestWithBucketID("GET", EndpointRelationshipsMutual(userID), nil, EndpointRelationshipsMutual(userID)) if err != nil { return diff --git a/restapi_test.go b/restapi_test.go index a2da6344e..c769e4bfc 100644 --- a/restapi_test.go +++ b/restapi_test.go @@ -10,7 +10,7 @@ import ( // TestChannelMessageSend tests the ChannelMessageSend() function. This should not return an error. func TestChannelMessageSend(t *testing.T) { - if envChannel == "" { + if envChannel == 0 { t.Skip("Skipping, DG_CHANNEL not set.") } @@ -86,7 +86,7 @@ func TestUserChannelCreate(t *testing.T) { t.Skip("Cannot TestUserChannelCreate, dg not set.") } - if envAdmin == "" { + if envAdmin == 0 { t.Skip("Skipped, DG_ADMIN not set.") } @@ -114,7 +114,7 @@ func TestUserGuilds(t *testing.T) { t.Skip("Cannot TestUserGuilds, dg not set.") } - _, err := dg.UserGuilds(10, "", "") + _, err := dg.UserGuilds(10, 0, 0) if err != nil { t.Errorf(err.Error()) } @@ -172,6 +172,7 @@ func TestGatewayBot(t *testing.T) { t.Skip("Skipping, dgBot not set.") } _, err := dgBot.GatewayBot() + if err != nil { t.Errorf("GatewayBot() returned error: %+v", err) } @@ -202,7 +203,7 @@ func TestVoiceRegions(t *testing.T) { } func TestGuildRoles(t *testing.T) { - if envGuild == "" { + if envGuild == 0 { t.Skip("Skipping, DG_GUILD not set.") } @@ -217,9 +218,9 @@ func TestGuildRoles(t *testing.T) { } -func TestGuildMemberNickname(t *testing.T) { +func TestGuildMemberNicknameMe(t *testing.T) { - if envGuild == "" { + if envGuild == 0 { t.Skip("Skipping, DG_GUILD not set.") } @@ -227,7 +228,7 @@ func TestGuildMemberNickname(t *testing.T) { t.Skip("Skipping, dg not set.") } - err := dg.GuildMemberNickname(envGuild, "@me/nick", "B1nzyRocks") + err := dg.GuildMemberNicknameMe(envGuild, "B1nzyRocks") if err != nil { t.Errorf("GuildNickname returned error: %+v", err) } @@ -236,7 +237,7 @@ func TestGuildMemberNickname(t *testing.T) { // TestChannelMessageSend2 tests the ChannelMessageSend() function. This should not return an error. func TestChannelMessageSend2(t *testing.T) { - if envChannel == "" { + if envChannel == 0 { t.Skip("Skipping, DG_CHANNEL not set.") } @@ -253,7 +254,7 @@ func TestChannelMessageSend2(t *testing.T) { // TestGuildPruneCount tests GuildPruneCount() function. This should not return an error. func TestGuildPruneCount(t *testing.T) { - if envGuild == "" { + if envGuild == 0 { t.Skip("Skipping, DG_GUILD not set.") } @@ -271,7 +272,7 @@ func TestGuildPruneCount(t *testing.T) { // TestGuildPrune tests GuildPrune() function. This should not return an error. func TestGuildPrune(t *testing.T) { - if envGuild == "" { + if envGuild == 0 { t.Skip("Skipping, DG_GUILD not set.") } diff --git a/state.go b/state.go index 3a4643fd2..eefdfc9f3 100644 --- a/state.go +++ b/state.go @@ -41,9 +41,9 @@ type State struct { TrackVoice bool TrackPresences bool - guildMap map[string]*Guild - channelMap map[string]*Channel - memberMap map[string]map[string]*Member + guildMap map[int64]*Guild + channelMap map[int64]*Channel + memberMap map[int64]map[int64]*Member } // NewState creates an empty state. @@ -59,14 +59,14 @@ func NewState() *State { TrackRoles: true, TrackVoice: true, TrackPresences: true, - guildMap: make(map[string]*Guild), - channelMap: make(map[string]*Channel), - memberMap: make(map[string]map[string]*Member), + guildMap: make(map[int64]*Guild), + channelMap: make(map[int64]*Channel), + memberMap: make(map[int64]map[int64]*Member), } } func (s *State) createMemberMap(guild *Guild) { - members := make(map[string]*Member) + members := make(map[int64]*Member) for _, m := range guild.Members { members[m.User.ID] = m } @@ -93,7 +93,7 @@ func (s *State) GuildAdd(guild *Guild) error { s.createMemberMap(guild) } else if _, ok := s.memberMap[guild.ID]; !ok { // Even if we have no new member slice, we still initialize the member map for this guild if it doesn't exist - s.memberMap[guild.ID] = make(map[string]*Member) + s.memberMap[guild.ID] = make(map[int64]*Member) } if g, ok := s.guildMap[guild.ID]; ok { @@ -160,7 +160,7 @@ func (s *State) GuildRemove(guild *Guild) error { // Useful for querying if @me is in a guild: // _, err := discordgo.Session.State.Guild(guildID) // isInGuild := err == nil -func (s *State) Guild(guildID string) (*Guild, error) { +func (s *State) Guild(guildID int64) (*Guild, error) { if s == nil { return nil, ErrNilState } @@ -177,7 +177,7 @@ func (s *State) Guild(guildID string) (*Guild, error) { // PresenceAdd adds a presence to the current world state, or // updates it if it already exists. -func (s *State) PresenceAdd(guildID string, presence *Presence) error { +func (s *State) PresenceAdd(guildID int64, presence *Presence) error { if s == nil { return ErrNilState } @@ -214,12 +214,6 @@ func (s *State) PresenceAdd(guildID string, presence *Presence) error { if presence.User.Discriminator != "" { guild.Presences[i].User.Discriminator = presence.User.Discriminator } - if presence.User.Email != "" { - guild.Presences[i].User.Email = presence.User.Email - } - if presence.User.Token != "" { - guild.Presences[i].User.Token = presence.User.Token - } if presence.User.Username != "" { guild.Presences[i].User.Username = presence.User.Username } @@ -233,7 +227,7 @@ func (s *State) PresenceAdd(guildID string, presence *Presence) error { } // PresenceRemove removes a presence from the current world state. -func (s *State) PresenceRemove(guildID string, presence *Presence) error { +func (s *State) PresenceRemove(guildID int64, presence *Presence) error { if s == nil { return ErrNilState } @@ -257,7 +251,7 @@ func (s *State) PresenceRemove(guildID string, presence *Presence) error { } // Presence gets a presence by ID from a guild. -func (s *State) Presence(guildID, userID string) (*Presence, error) { +func (s *State) Presence(guildID, userID int64) (*Presence, error) { if s == nil { return nil, ErrNilState } @@ -350,7 +344,7 @@ func (s *State) MemberRemove(member *Member) error { } // Member gets a member by ID from a guild. -func (s *State) Member(guildID, userID string) (*Member, error) { +func (s *State) Member(guildID, userID int64) (*Member, error) { if s == nil { return nil, ErrNilState } @@ -373,7 +367,7 @@ func (s *State) Member(guildID, userID string) (*Member, error) { // RoleAdd adds a role to the current world state, or // updates it if it already exists. -func (s *State) RoleAdd(guildID string, role *Role) error { +func (s *State) RoleAdd(guildID int64, role *Role) error { if s == nil { return ErrNilState } @@ -398,7 +392,7 @@ func (s *State) RoleAdd(guildID string, role *Role) error { } // RoleRemove removes a role from current world state by ID. -func (s *State) RoleRemove(guildID, roleID string) error { +func (s *State) RoleRemove(guildID, roleID int64) error { if s == nil { return ErrNilState } @@ -422,7 +416,7 @@ func (s *State) RoleRemove(guildID, roleID string) error { } // Role gets a role by ID from a guild. -func (s *State) Role(guildID, roleID string) (*Role, error) { +func (s *State) Role(guildID, roleID int64) (*Role, error) { if s == nil { return nil, ErrNilState } @@ -530,18 +524,18 @@ func (s *State) ChannelRemove(channel *Channel) error { // GuildChannel gets a channel by ID from a guild. // This method is Deprecated, use Channel(channelID) -func (s *State) GuildChannel(guildID, channelID string) (*Channel, error) { +func (s *State) GuildChannel(guildID, channelID int64) (*Channel, error) { return s.Channel(channelID) } // PrivateChannel gets a private channel by ID. // This method is Deprecated, use Channel(channelID) -func (s *State) PrivateChannel(channelID string) (*Channel, error) { +func (s *State) PrivateChannel(channelID int64) (*Channel, error) { return s.Channel(channelID) } // Channel gets a channel by ID, it will look in all guilds and private channels. -func (s *State) Channel(channelID string) (*Channel, error) { +func (s *State) Channel(channelID int64) (*Channel, error) { if s == nil { return nil, ErrNilState } @@ -557,7 +551,7 @@ func (s *State) Channel(channelID string) (*Channel, error) { } // Emoji returns an emoji for a guild and emoji id. -func (s *State) Emoji(guildID, emojiID string) (*Emoji, error) { +func (s *State) Emoji(guildID, emojiID int64) (*Emoji, error) { if s == nil { return nil, ErrNilState } @@ -580,7 +574,7 @@ func (s *State) Emoji(guildID, emojiID string) (*Emoji, error) { } // EmojiAdd adds an emoji to the current world state. -func (s *State) EmojiAdd(guildID string, emoji *Emoji) error { +func (s *State) EmojiAdd(guildID int64, emoji *Emoji) error { if s == nil { return ErrNilState } @@ -605,7 +599,7 @@ func (s *State) EmojiAdd(guildID string, emoji *Emoji) error { } // EmojisAdd adds multiple emojis to the world state. -func (s *State) EmojisAdd(guildID string, emojis []*Emoji) error { +func (s *State) EmojisAdd(guildID int64, emojis []*Emoji) error { for _, e := range emojis { if err := s.EmojiAdd(guildID, e); err != nil { return err @@ -677,7 +671,7 @@ func (s *State) MessageRemove(message *Message) error { } // messageRemoveByID removes a message by channelID and messageID from the world state. -func (s *State) messageRemoveByID(channelID, messageID string) error { +func (s *State) messageRemoveByID(channelID, messageID int64) error { c, err := s.Channel(channelID) if err != nil { return err @@ -706,7 +700,7 @@ func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error { defer s.Unlock() // Handle Leaving Channel - if update.ChannelID == "" { + if update.ChannelID == 0 { for i, state := range guild.VoiceStates { if state.UserID == update.UserID { guild.VoiceStates = append(guild.VoiceStates[:i], guild.VoiceStates[i+1:]...) @@ -728,7 +722,7 @@ func (s *State) voiceStateUpdate(update *VoiceStateUpdate) error { } // Message gets a message by channel and message ID. -func (s *State) Message(channelID, messageID string) (*Message, error) { +func (s *State) Message(channelID, messageID int64) (*Message, error) { if s == nil { return nil, ErrNilState } @@ -929,7 +923,7 @@ func (s *State) OnInterface(se *Session, i interface{}) (err error) { // UserChannelPermissions returns the permission of a user in a channel. // userID : The ID of the user to calculate permissions for. // channelID : The ID of the channel to calculate permission for. -func (s *State) UserChannelPermissions(userID, channelID string) (apermissions int, err error) { +func (s *State) UserChannelPermissions(userID, channelID int64) (apermissions int, err error) { if s == nil { return 0, ErrNilState } @@ -954,7 +948,7 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i return } - return memberPermissions(guild, channel, member), nil + return MemberPermissions(guild, channel, member), nil } // UserColor returns the color of a user in a channel. @@ -962,7 +956,7 @@ func (s *State) UserChannelPermissions(userID, channelID string) (apermissions i // 0 is returned in cases of error, which is the color of @everyone. // userID : The ID of the user to calculate the color for. // channelID : The ID of the channel to calculate the color for. -func (s *State) UserColor(userID, channelID string) int { +func (s *State) UserColor(userID, channelID int64) int { if s == nil { return 0 } diff --git a/structs.go b/structs.go index a7d298c8a..a71cf99cf 100644 --- a/structs.go +++ b/structs.go @@ -18,13 +18,12 @@ import ( "sync" "time" - "github.com/gorilla/websocket" + "github.com/jonas747/gojay" + "github.com/pkg/errors" ) // A Session represents a connection to the Discord API. type Session struct { - sync.RWMutex - // General configurable settings. // Authentication token for this session @@ -54,27 +53,9 @@ type Session struct { // e.g false = launch event handlers in their own goroutines. SyncEvents bool - // Exposed but should not be modified by User. - - // Whether the Data Websocket is ready - DataReady bool // NOTE: Maye be deprecated soon - // Max number of REST API retries MaxRestRetries int - // Status stores the currect status of the websocket connection - // this is being tested, may stay, may go away. - status int32 - - // Whether the Voice Websocket is ready - VoiceReady bool // NOTE: Deprecated. - - // Whether the UDP Connection is ready - UDPReady bool // NOTE: Deprecated - - // Stores a mapping of guild id's to VoiceConnections - VoiceConnections map[string]*VoiceConnection - // Managed state object, updated internally with events when // StateEnabled is true. State *State @@ -88,28 +69,15 @@ type Session struct { // used to deal with rate limits Ratelimiter *RateLimiter + // The gateway websocket connection + GatewayManager *GatewayConnectionManager + + tokenInvalid *int32 + // Event handlers handlersMu sync.RWMutex handlers map[string][]*eventHandlerInstance onceHandlers map[string][]*eventHandlerInstance - - // The websocket connection. - wsConn *websocket.Conn - - // When nil, the session is not listening. - listening chan interface{} - - // sequence tracks the current gateway api websocket sequence number - sequence *int64 - - // stores sessions current Discord Gateway - gateway string - - // stores session ID of current Gateway connection - sessionID string - - // used to make sure gateway websocket writes do not happen concurrently - wsMutex sync.Mutex } // UserConnection is a Connection returned from the UserConnections endpoint @@ -193,16 +161,18 @@ const ( ChannelTypeGuildVoice ChannelTypeGroupDM ChannelTypeGuildCategory + ChannelTypeGuildNews + ChannelTypeGuildStore ) // A Channel holds all data related to an individual Discord channel. type Channel struct { // The ID of the channel. - ID string `json:"id"` + ID int64 `json:"id,string"` // The ID of the guild to which the channel belongs, if it is in a guild. // Else, this ID is empty (e.g. DM channels). - GuildID string `json:"guild_id"` + GuildID int64 `json:"guild_id,string"` // The name of the channel. Name string `json:"name"` @@ -215,7 +185,7 @@ type Channel struct { // The ID of the last message sent in the channel. This is not // guaranteed to be an ID of a valid message. - LastMessageID string `json:"last_message_id"` + LastMessageID int64 `json:"last_message_id,string"` // Whether the channel is marked as NSFW. NSFW bool `json:"nsfw"` @@ -243,12 +213,22 @@ type Channel struct { UserLimit int `json:"user_limit"` // The ID of the parent channel, if the channel is under a category - ParentID string `json:"parent_id"` + ParentID int64 `json:"parent_id,string"` + + RateLimitPerUser int `json:"rate_limit_per_user"` +} + +func (c *Channel) GetChannelID() int64 { + return c.ID +} + +func (c *Channel) GetGuildID() int64 { + return c.GuildID } // Mention returns a string which mentions the channel func (c *Channel) Mention() string { - return fmt.Sprintf("<#%s>", c.ID) + return fmt.Sprintf("<#%d>", c.ID) } // A ChannelEdit holds Channel Feild data for a channel edit. @@ -256,16 +236,17 @@ type ChannelEdit struct { Name string `json:"name,omitempty"` Topic string `json:"topic,omitempty"` NSFW bool `json:"nsfw,omitempty"` - Position int `json:"position"` + Position *int `json:"position,omitempty"` Bitrate int `json:"bitrate,omitempty"` UserLimit int `json:"user_limit,omitempty"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` ParentID string `json:"parent_id,omitempty"` + RateLimitPerUser *int `json:"rate_limit_per_user,omitempty"` } // A PermissionOverwrite holds permission overwrite data for a Channel type PermissionOverwrite struct { - ID string `json:"id"` + ID int64 `json:"id,string"` Type string `json:"type"` Deny int `json:"deny"` Allow int `json:"allow"` @@ -273,17 +254,17 @@ type PermissionOverwrite struct { // Emoji struct holds data related to Emoji's type Emoji struct { - ID string `json:"id"` - Name string `json:"name"` - Roles []string `json:"roles"` - Managed bool `json:"managed"` - RequireColons bool `json:"require_colons"` - Animated bool `json:"animated"` + ID int64 `json:"id,string"` + Name string `json:"name"` + Roles IDSlice `json:"roles,string"` + Managed bool `json:"managed"` + RequireColons bool `json:"require_colons"` + Animated bool `json:"animated"` } // MessageFormat returns a correctly formatted Emoji for use in Message content and embeds func (e *Emoji) MessageFormat() string { - if e.ID != "" && e.Name != "" { + if e.ID != 0 && e.Name != "" { if e.Animated { return "" } @@ -296,13 +277,13 @@ func (e *Emoji) MessageFormat() string { // APIName returns an correctly formatted API name for use in the MessageReactions endpoints. func (e *Emoji) APIName() string { - if e.ID != "" && e.Name != "" { - return e.Name + ":" + e.ID + if e.ID != 0 && e.Name != "" { + return e.Name + ":" + StrID(e.ID) } if e.Name != "" { return e.Name } - return e.ID + return StrID(e.ID) } // VerificationLevel type definition @@ -339,7 +320,7 @@ const ( // sometimes referred to as Servers in the Discord client. type Guild struct { // The ID of the guild. - ID string `json:"id"` + ID int64 `json:"id,string"` // The name of the guild. (2–100 characters) Name string `json:"name"` @@ -352,13 +333,13 @@ type Guild struct { Region string `json:"region"` // The ID of the AFK voice channel. - AfkChannelID string `json:"afk_channel_id"` + AfkChannelID int64 `json:"afk_channel_id,string"` // The ID of the embed channel ID, used for embed widgets. - EmbedChannelID string `json:"embed_channel_id"` + EmbedChannelID int64 `json:"embed_channel_id,string"` // The user ID of the owner of the guild. - OwnerID string `json:"owner_id"` + OwnerID int64 `json:"owner_id,string"` // The time at which the current user joined the guild. // This field is only present in GUILD_CREATE events and websocket @@ -415,7 +396,9 @@ type Guild struct { // A list of voice states for the guild. // This field is only present in GUILD_CREATE events and websocket // update events, and thus is only present in state-cached guilds. - VoiceStates []*VoiceState `json:"voice_states"` + VoiceStates []*VoiceState `json:"voice_states"` + MaxPresences int `json:"max_presences"` + MaxMembers int `json:"max_members"` // Whether this guild is currently unavailable (most likely due to outage). // This field is only present in GUILD_CREATE events and websocket @@ -441,9 +424,33 @@ type Guild struct { SystemChannelID string `json:"system_channel_id"` } +func (g *Guild) GetGuildID() int64 { + return g.ID +} + +func (g *Guild) Role(id int64) *Role { + for _, v := range g.Roles { + if v.ID == id { + return v + } + } + + return nil +} + +func (g *Guild) Channel(id int64) *Channel { + for _, v := range g.Channels { + if v.ID == id { + return v + } + } + + return nil +} + // A UserGuild holds a brief version of a Guild type UserGuild struct { - ID string `json:"id"` + ID int64 `json:"id,string"` Name string `json:"name"` Icon string `json:"icon"` Owner bool `json:"owner"` @@ -456,17 +463,17 @@ type GuildParams struct { Region string `json:"region,omitempty"` VerificationLevel *VerificationLevel `json:"verification_level,omitempty"` DefaultMessageNotifications int `json:"default_message_notifications,omitempty"` // TODO: Separate type? - AfkChannelID string `json:"afk_channel_id,omitempty"` + AfkChannelID int64 `json:"afk_channel_id,omitempty,string"` AfkTimeout int `json:"afk_timeout,omitempty"` Icon string `json:"icon,omitempty"` - OwnerID string `json:"owner_id,omitempty"` + OwnerID int64 `json:"owner_id,omitempty,string"` Splash string `json:"splash,omitempty"` } // A Role stores information about Discord guild member roles. type Role struct { // The ID of the role. - ID string `json:"id"` + ID int64 `json:"id,string"` // The name of the role. Name string `json:"name"` @@ -495,7 +502,7 @@ type Role struct { // Mention returns a string which mentions the role func (r *Role) Mention() string { - return fmt.Sprintf("<@&%s>", r.ID) + return fmt.Sprintf("<@&%d>", r.ID) } // Roles are a collection of Role @@ -515,10 +522,10 @@ func (r Roles) Swap(i, j int) { // A VoiceState stores the voice states of Guilds type VoiceState struct { - UserID string `json:"user_id"` + UserID int64 `json:"user_id,string"` SessionID string `json:"session_id"` - ChannelID string `json:"channel_id"` - GuildID string `json:"guild_id"` + ChannelID int64 `json:"channel_id,string"` + GuildID int64 `json:"guild_id,string"` Suppress bool `json:"suppress"` SelfMute bool `json:"self_mute"` SelfDeaf bool `json:"self_deaf"` @@ -528,12 +535,60 @@ type VoiceState struct { // A Presence stores the online, offline, or idle and game status of Guild members. type Presence struct { - User *User `json:"user"` - Status Status `json:"status"` - Game *Game `json:"game"` - Nick string `json:"nick"` - Roles []string `json:"roles"` - Since *int `json:"since"` + User *User `json:"user"` + Status Status `json:"status"` + Game *Game `json:"game"` + Nick string `json:"nick"` + Roles IDSlice `json:"roles,string"` + + Activities Activities `json:"activities"` + + // not decoded + Since int64 `json:"since"` +} + +// implement gojay.UnmarshalerJSONObject +func (p *Presence) UnmarshalJSONObject(dec *gojay.Decoder, key string) error { + var err error + switch key { + case "user": + p.User = &User{} + err = dec.Object(p.User) + case "status": + err = dec.String((*string)(&p.Status)) + case "game": + p.Game = &Game{} + err = dec.Object(p.Game) + case "nick": + err = dec.String(&p.Nick) + case "roles": + err = dec.DecodeArray(&p.Roles) + case "activities": + err = dec.DecodeArray(&p.Activities) + default: + } + + if err != nil { + return errors.Wrap(err, key) + } + + return nil +} + +func (p *Presence) NKeys() int { + return 0 +} + +type Activities []*Game + +func (a *Activities) UnmarshalJSONArray(dec *gojay.Decoder) error { + instance := Game{} + err := dec.Object(&instance) + if err != nil { + return err + } + *a = append(*a, &instance) + return nil } // GameType is the type of "game" (see GameType* consts) in the Game struct @@ -561,6 +616,34 @@ type Game struct { // TODO: Party and Secrets (unknown structure) } +// implement gojay.UnmarshalerJSONObject +func (g *Game) UnmarshalJSONObject(dec *gojay.Decoder, key string) error { + switch key { + case "name": + return dec.String(&g.Name) + case "type": + return dec.Int((*int)(&g.Type)) + case "url": + return dec.String(&g.URL) + case "details": + return dec.String(&g.Details) + case "state": + return dec.String(&g.State) + case "timestamps": + case "assets": + case "application_id": + return dec.String(&g.ApplicationID) + case "instance": + return dec.Int8(&g.Instance) + } + + return nil +} + +func (g *Game) NKeys() int { + return 0 +} + // A TimeStamps struct contains start and end times used in the rich presence "playing .." Game type TimeStamps struct { EndTimestamp int64 `json:"end,omitempty"` @@ -570,15 +653,32 @@ type TimeStamps struct { // UnmarshalJSON unmarshals JSON into TimeStamps struct func (t *TimeStamps) UnmarshalJSON(b []byte) error { temp := struct { - End float64 `json:"end,omitempty"` - Start float64 `json:"start,omitempty"` + End json.Number `json:"end,omitempty"` + Start json.Number `json:"start,omitempty"` }{} err := json.Unmarshal(b, &temp) if err != nil { return err } - t.EndTimestamp = int64(temp.End) - t.StartTimestamp = int64(temp.Start) + + var endParsed float64 + if temp.End != "" { + endParsed, err = temp.End.Float64() + if err != nil { + return err + } + } + + var startParsed float64 + if temp.Start != "" { + startParsed, err = temp.Start.Float64() + if err != nil { + return err + } + } + + t.EndTimestamp = int64(endParsed) + t.StartTimestamp = int64(startParsed) return nil } @@ -594,10 +694,10 @@ type Assets struct { // member represents a certain user's presence in a guild. type Member struct { // The guild ID on which the member exists. - GuildID string `json:"guild_id"` + GuildID int64 `json:"guild_id,string"` // The time at which the member joined the guild, in ISO8601. - JoinedAt string `json:"joined_at"` + JoinedAt Timestamp `json:"joined_at"` // The nickname of the member, if they have one. Nick string `json:"nick"` @@ -612,7 +712,11 @@ type Member struct { User *User `json:"user"` // A list of IDs of the roles which are possessed by the member. - Roles []string `json:"roles"` + Roles IDSlice `json:"roles,string"` +} + +func (m *Member) GetGuildID() int64 { + return m.GuildID } // A Settings stores data for a specific users Discord client settings. @@ -626,8 +730,8 @@ type Settings struct { ConvertEmoticons bool `json:"convert_emoticons"` Locale string `json:"locale"` Theme string `json:"theme"` - GuildPositions []string `json:"guild_positions"` - RestrictedGuilds []string `json:"restricted_guilds"` + GuildPositions IDSlice `json:"guild_positions,string"` + RestrictedGuilds IDSlice `json:"restricted_guilds,string"` FriendSourceFlags *FriendSourceFlags `json:"friend_source_flags"` Status Status `json:"status"` DetectPlatformAccounts bool `json:"detect_platform_accounts"` @@ -666,13 +770,14 @@ type TooManyRequests struct { Bucket string `json:"bucket"` Message string `json:"message"` RetryAfter time.Duration `json:"retry_after"` + Global bool `json:"global"` } // A ReadState stores data on the read state of channels. type ReadState struct { - MentionCount int `json:"mention_count"` - LastMessageID string `json:"last_message_id"` - ID string `json:"id"` + MentionCount int `json:"mention_count"` + LastMessageID int64 `json:"last_message_id,string"` + ID int64 `json:"id,string"` } // An Ack is used to ack messages @@ -682,8 +787,12 @@ type Ack struct { // A GuildRole stores data for guild roles. type GuildRole struct { - Role *Role `json:"role"` - GuildID string `json:"guild_id"` + Role *Role `json:"role"` + GuildID int64 `json:"guild_id,string"` +} + +func (e *GuildRole) GetGuildID() int64 { + return e.GuildID } // A GuildBan stores data for a guild ban. @@ -694,47 +803,43 @@ type GuildBan struct { // A GuildEmbed stores data for a guild embed. type GuildEmbed struct { - Enabled bool `json:"enabled"` - ChannelID string `json:"channel_id"` + Enabled bool `json:"enabled"` + ChannelID int64 `json:"channel_id,string"` } // A GuildAuditLog stores data for a guild audit log. type GuildAuditLog struct { Webhooks []struct { - ChannelID string `json:"channel_id"` - GuildID string `json:"guild_id"` + ChannelID int64 `json:"channel_id,string"` + GuildID int64 `json:"guild_id,string"` ID string `json:"id"` Avatar string `json:"avatar"` Name string `json:"name"` } `json:"webhooks,omitempty"` - Users []struct { - Username string `json:"username"` - Discriminator string `json:"discriminator"` - Bot bool `json:"bot"` - ID string `json:"id"` - Avatar string `json:"avatar"` - } `json:"users,omitempty"` - AuditLogEntries []struct { - TargetID string `json:"target_id"` - Changes []struct { - NewValue interface{} `json:"new_value"` - OldValue interface{} `json:"old_value"` - Key string `json:"key"` - } `json:"changes,omitempty"` - UserID string `json:"user_id"` - ID string `json:"id"` - ActionType int `json:"action_type"` - Options struct { - DeleteMembersDay string `json:"delete_member_days"` - MembersRemoved string `json:"members_removed"` - ChannelID string `json:"channel_id"` - Count string `json:"count"` - ID string `json:"id"` - Type string `json:"type"` - RoleName string `json:"role_name"` - } `json:"options,omitempty"` - Reason string `json:"reason"` - } `json:"audit_log_entries"` + Users []*User `json:"users,omitempty"` + AuditLogEntries []*AuditLogEntry `json:"audit_log_entries"` +} + +type AuditLogEntry struct { + TargetID int64 `json:"target_id,string"` + Changes []struct { + NewValue interface{} `json:"new_value"` + OldValue interface{} `json:"old_value"` + Key string `json:"key"` + } `json:"changes,omitempty"` + UserID int64 `json:"user_id,string"` + ID int64 `json:"id,string"` + ActionType int `json:"action_type"` + Options struct { + DeleteMembersDay string `json:"delete_member_days"` + MembersRemoved string `json:"members_removed"` + ChannelID int64 `json:"channel_id,string"` + Count string `json:"count"` + ID int64 `json:"id,string"` + Type string `json:"type"` + RoleName string `json:"role_name"` + } `json:"options,omitempty"` + Reason string `json:"reason"` } // Block contains Discord Audit Log Action Types @@ -776,9 +881,9 @@ const ( // A UserGuildSettingsChannelOverride stores data for a channel override for a users guild settings. type UserGuildSettingsChannelOverride struct { - Muted bool `json:"muted"` - MessageNotifications int `json:"message_notifications"` - ChannelID string `json:"channel_id"` + Muted bool `json:"muted"` + MessageNotifications int `json:"message_notifications"` + ChannelID int64 `json:"channel_id,string"` } // A UserGuildSettings stores data for a users guild settings. @@ -787,7 +892,7 @@ type UserGuildSettings struct { Muted bool `json:"muted"` MobilePush bool `json:"mobile_push"` MessageNotifications int `json:"message_notifications"` - GuildID string `json:"guild_id"` + GuildID int64 `json:"guild_id,string"` ChannelOverrides []*UserGuildSettingsChannelOverride `json:"channel_overrides"` } @@ -808,9 +913,9 @@ type APIErrorMessage struct { // Webhook stores the data for a webhook. type Webhook struct { - ID string `json:"id"` - GuildID string `json:"guild_id"` - ChannelID string `json:"channel_id"` + ID int64 `json:"id,string"` + GuildID int64 `json:"guild_id,string"` + ChannelID int64 `json:"channel_id,string"` User *User `json:"user"` Name string `json:"name"` Avatar string `json:"avatar"` @@ -829,17 +934,32 @@ type WebhookParams struct { // MessageReaction stores the data for a message reaction. type MessageReaction struct { - UserID string `json:"user_id"` - MessageID string `json:"message_id"` - Emoji Emoji `json:"emoji"` - ChannelID string `json:"channel_id"` - GuildID string `json:"guild_id,omitempty"` + UserID int64 `json:"user_id,string"` + MessageID int64 `json:"message_id,string"` + Emoji Emoji `json:"emoji"` + ChannelID int64 `json:"channel_id,string"` + GuildID int64 `json:"guild_id,string,omitempty"` +} + +func (mr *MessageReaction) GetGuildID() int64 { + return mr.GuildID +} + +func (mr *MessageReaction) GetChannelID() int64 { + return mr.ChannelID } // GatewayBotResponse stores the data for the gateway/bot response type GatewayBotResponse struct { - URL string `json:"url"` - Shards int `json:"shards"` + URL string `json:"url"` + Shards int `json:"shards"` + SessionStartLimit SessionStartLimit `json:"session_start_limit"` +} + +type SessionStartLimit struct { + Total int `json:"total"` + Remaining int `json:"remaining"` + ResetAfter int64 `json:"reset_after"` } // Constants for the different bit offsets of text channel permissions @@ -905,12 +1025,15 @@ const ( PermissionManageRoles | PermissionManageChannels | PermissionAddReactions | - PermissionViewAuditLogs + PermissionViewAuditLogs | + PermissionManageWebhooks PermissionAll = PermissionAllChannel | PermissionKickMembers | PermissionBanMembers | PermissionManageServer | - PermissionAdministrator + PermissionAdministrator | + PermissionManageNicknames | + PermissionManageEmojis ) // Block contains Discord JSON Error Response codes @@ -966,3 +1089,11 @@ const ( ErrCodeReactionBlocked = 90001 ) + +// InviteUser is a partial user obejct from the invite event(s) +type InviteUser struct { + ID int64 `json:"id,string"` + Avatar string `json:"avatar"` + Discriminator string `json:"discriminator"` + Username string `json:"username"` +} diff --git a/types.go b/types.go index 780b6bb97..4bae0fe4c 100644 --- a/types.go +++ b/types.go @@ -1,3 +1,4 @@ +//go:generate generateEmojiCodeMap -pkg discordgo // Discordgo - Discord bindings for Go // Available at https://github.com/bwmarrin/discordgo @@ -10,10 +11,16 @@ package discordgo import ( + "bytes" "encoding/json" "fmt" "net/http" + "net/url" + "strconv" + "strings" "time" + + "github.com/jonas747/gojay" ) // Timestamp stores a timestamp, as sent by the Discord API. @@ -56,3 +63,188 @@ func newRestError(req *http.Request, resp *http.Response, body []byte) *RESTErro func (r RESTError) Error() string { return fmt.Sprintf("HTTP %s, %s", r.Response.Status, r.ResponseBody) } + +// nullIntID is a snowflake id that marshals to "null" if value is 0 and otherwise marshals as a string in the way discord expects ids to behave. +// It marshals to null or string representation of number according to value and unmarshals from both string and integer representations. + +type nullIntID int64 + +func(i nullIntID) MarshalJSON() ([]byte, error) { + if i == 0 { + return json.Marshal(nil) + } + return []byte(fmt.Sprintf(`"%d"`, i)), nil +} + +func(i *nullIntID) UnmarshalJSON(data []byte) error { + length := len(data) + if bytes.Equal(data, []byte("null")) { + *i = 0 + return nil + } + + if length > 2 && string(data[0]) == `"` && string(data[length -1]) == `"` { + data = data[1:length-1] + } + + var tempInt int64 + err := json.Unmarshal(data, &tempInt) + if err != nil { + return err + } + + *i = nullIntID(tempInt) + return nil +} + +// IDSlice Is a slice of snowflake id's that properly marshals and unmarshals the way discord expects them to +// They unmarshal from string arrays and marshals back to string arrays +type IDSlice []int64 + +func (ids *IDSlice) UnmarshalJSON(data []byte) error { + if len(data) < 3 { + return nil + } + + // Split and strip away "[" "]" + split := strings.Split(string(data[1:len(data)-1]), ",") + *ids = make([]int64, 0, len(split)) + for _, s := range split { + s = strings.TrimSpace(s) + if len(s) < 3 { + // Empty or invalid + continue + } + + // Strip away quotes and parse + parsed, err := strconv.ParseInt(s[1:len(s)-1], 10, 64) + if err != nil { + return err + } + + *ids = append(*ids, parsed) + } + + return nil +} + +func (ids IDSlice) MarshalJSON() ([]byte, error) { + // Capacity: + // 2 brackets + // each id is: + // 18 characters currently, but 1 extra added for the future, + // 1 comma + // 2 quotes + if len(ids) < 1 { + return []byte("[]"), nil + } + + outPut := make([]byte, 1, 2+(len(ids)*22)) + outPut[0] = '[' + + for i, id := range ids { + if i != 0 { + outPut = append(outPut, '"', ',', '"') + } else { + outPut = append(outPut, '"') + } + outPut = append(outPut, []byte(strconv.FormatInt(id, 10))...) + } + + outPut = append(outPut, '"', ']') + return outPut, nil +} + +// implement UnmarshalerJSONArray +func (ids *IDSlice) UnmarshalJSONArray(dec *gojay.Decoder) error { + str := "" + if err := dec.String(&str); err != nil { + return err + } + + parsed, err := strconv.ParseInt(str, 10, 64) + if err != nil { + return err + } + + *ids = append(*ids, parsed) + return nil +} + +type EmojiName struct { + string +} + +func (emoji EmojiName) String() string { + if codepoint, ok := emojiCodeMap[emoji.string]; ok { + emoji.string = codepoint + } + // Discord does not accept the emoji qualifier character. + // return strings.Replace(emoji.string, "\uFE0F", "", 1) + // this no longer the case? in fact its required? + return url.PathEscape(emoji.string) +} + +// Discord is super inconsistent with with types in some places (especially presence updates, +// might aswell change them to map[string]interface{} soon because there is 0 validation) +type DiscordFloat float64 + +func (df *DiscordFloat) UnmarshalJSON(data []byte) error { + var dst json.Number + err := json.Unmarshal(data, &dst) + if err != nil { + return err + } + + parsed, err := dst.Float64() + if err != nil { + return err + } + + *df = DiscordFloat(parsed) + return nil +} + +type DiscordInt64 int64 + +func (di *DiscordInt64) UnmarshalJSON(data []byte) error { + var dst json.Number + err := json.Unmarshal(data, &dst) + if err != nil { + return err + } + + parsed, err := dst.Int64() + if err != nil { + // Attempt to fallback to float, we lost some precision but eh, what can you do when discord is so freaking inconsistent + f, err := dst.Float64() + if err != nil { + return err + } + *di = DiscordInt64(int64(f)) + return nil + } + + *di = DiscordInt64(parsed) + return nil +} + +func DecodeSnowflake(dst *int64, dec *gojay.Decoder) error { + var str string + err := dec.String(&str) + if err != nil { + return err + } + + parsed, err := strconv.ParseInt(str, 10, 64) + *dst = parsed + return err +} + +type GuildEvent interface { + GetGuildID() int64 +} + +type ChannelEvent interface { + GetChannelID() int64 +} diff --git a/types_test.go b/types_test.go index 1d03aa32e..98afffd33 100644 --- a/types_test.go +++ b/types_test.go @@ -22,3 +22,23 @@ func TestTimestampParse(t *testing.T) { t.Error("Incorrect timezone") } } + +func TestEmojiNameUnqualify(t *testing.T) { + cases := []struct { + have string + want string + }{ + {"⚔️", "⚔"}, + {"⚔", "⚔"}, + {":crossed_swords:", "⚔"}, + {"1o57:442605016813928449", "1o57:442605016813928449"}, + } + + for _, c := range cases { + emoji := EmojiName{c.have} + if emoji.String() != c.want { + t.Errorf("Failed to strip emoji qualifier: '%s' -> '%s' not '%s'", + c.have, emoji.String(), c.want) + } + } +} diff --git a/user.go b/user.go index 618a7d59e..fceb6a3d3 100644 --- a/user.go +++ b/user.go @@ -2,17 +2,14 @@ package discordgo import ( "fmt" + "github.com/jonas747/gojay" "strings" ) // A User stores all data for an individual Discord user. type User struct { // The ID of the user. - ID string `json:"id"` - - // The email of the user. This is only present when - // the application possesses the email scope for the user. - Email string `json:"email"` + ID int64 `json:"id,string"` // The user's username. Username string `json:"username"` @@ -27,16 +24,6 @@ type User struct { // The discriminator of the user (4 numbers after name). Discriminator string `json:"discriminator"` - // The token of the user. This is only present for - // the user represented by the current session. - Token string `json:"token"` - - // Whether the user's email is verified. - Verified bool `json:"verified"` - - // Whether the user has multi-factor authentication enabled. - MFAEnabled bool `json:"mfa_enabled"` - // Whether the user is a bot. Bot bool `json:"bot"` } @@ -48,7 +35,31 @@ func (u *User) String() string { // Mention return a string which mentions the user func (u *User) Mention() string { - return fmt.Sprintf("<@%s>", u.ID) + return fmt.Sprintf("<@%d>", u.ID) +} + +// implement gojay.UnmarshalerJSONObject +func (u *User) UnmarshalJSONObject(dec *gojay.Decoder, key string) error { + switch key { + case "id": + return DecodeSnowflake(&u.ID, dec) + case "username": + return dec.String(&u.Username) + case "avatar": + return dec.String(&u.Avatar) + case "locale": + return dec.String(&u.Locale) + case "discriminator": + return dec.String(&u.Discriminator) + case "bot": + return dec.Bool(&u.Bot) + } + + return nil +} + +func (u *User) NKeys() int { + return 0 } // AvatarURL returns a URL to the user's avatar. @@ -70,3 +81,10 @@ func (u *User) AvatarURL(size string) string { } return URL } + +// A SelfUser stores user data about the token owner. +// Includes a few extra fields than a normal user struct. +type SelfUser struct { + *User + Token string `json:"token"` +} diff --git a/voice.go b/voice.go index 3bbf6212b..c5332cd49 100644 --- a/voice.go +++ b/voice.go @@ -12,6 +12,7 @@ package discordgo import ( "encoding/binary" "encoding/json" + "errors" "fmt" "net" "strings" @@ -19,6 +20,7 @@ import ( "time" "github.com/gorilla/websocket" + "github.com/jonas747/gojay" "golang.org/x/crypto/nacl/secretbox" ) @@ -33,9 +35,9 @@ type VoiceConnection struct { Debug bool // If true, print extra logging -- DEPRECATED LogLevel int Ready bool // If true, voice is ready to send/receive audio - UserID string - GuildID string - ChannelID string + UserID int64 + GuildID int64 + ChannelID int64 deaf bool mute bool speaking bool @@ -44,10 +46,12 @@ type VoiceConnection struct { OpusSend chan []byte // Chan for sending opus audio OpusRecv chan *Packet // Chan for receiving opus audio - wsConn *websocket.Conn - wsMutex sync.Mutex - udpConn *net.UDPConn - session *Session + wsConn *websocket.Conn + wsMutex sync.Mutex + udpConn *net.UDPConn + session *Session + gatewayConn *GatewayConnection + gatewayConnManager *GatewayConnectionManager sessionID string token string @@ -57,7 +61,7 @@ type VoiceConnection struct { close chan struct{} // Used to allow blocking until connected - connected chan bool + Connected chan bool // Used to pass the sessionid from onVoiceStateUpdate // sessionRecv chan string UNUSED ATM @@ -103,7 +107,7 @@ func (v *VoiceConnection) Speaking(b bool) (err error) { defer v.Unlock() if err != nil { v.speaking = false - v.log(LogError, "Speaking() write json error:", err) + v.log(LogError, "Speaking() write json error: %v", err) return } @@ -114,17 +118,23 @@ func (v *VoiceConnection) Speaking(b bool) (err error) { // ChangeChannel sends Discord a request to change channels within a Guild // !!! NOTE !!! This function may be removed in favour of just using ChannelVoiceJoin -func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err error) { +func (v *VoiceConnection) ChangeChannel(channelID int64, mute, deaf bool) (err error) { v.log(LogInformational, "called") - data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, &channelID, mute, deaf}} - v.wsMutex.Lock() - err = v.session.wsConn.WriteJSON(data) - v.wsMutex.Unlock() - if err != nil { - return + v.Lock() + + strGID := StrID(v.GuildID) + strCID := StrID(v.ChannelID) + + data := outgoingEvent{ + Operation: GatewayOPVoiceStateUpdate, + Data: voiceChannelJoinData{&strCID, &strGID, mute, deaf}, } + + v.gatewayConn.writer.Queue(data) + v.Unlock() + v.ChannelID = channelID v.deaf = deaf v.mute = mute @@ -138,23 +148,21 @@ func (v *VoiceConnection) ChangeChannel(channelID string, mute, deaf bool) (err // !!! NOTE !!! this function may be removed in favour of ChannelVoiceLeave func (v *VoiceConnection) Disconnect() (err error) { + v.Lock() // Send a OP4 with a nil channel to disconnect - if v.sessionID != "" { - data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} - v.session.wsMutex.Lock() - err = v.session.wsConn.WriteJSON(data) - v.session.wsMutex.Unlock() - v.sessionID = "" + strGID := StrID(v.GuildID) + + data := outgoingEvent{ + Operation: GatewayOPVoiceStateUpdate, + Data: voiceChannelJoinData{&strGID, nil, true, true}, } - // Close websocket and udp connections - v.Close() + v.gatewayConn.writer.Queue(data) - v.log(LogInformational, "Deleting VoiceConnection %s", v.GuildID) + v.Unlock() - v.session.Lock() - delete(v.session.VoiceConnections, v.GuildID) - v.session.Unlock() + // Close websocket and udp connections + v.Close() return } @@ -180,7 +188,7 @@ func (v *VoiceConnection) Close() { v.log(LogInformational, "closing udp") err := v.udpConn.Close() if err != nil { - v.log(LogError, "error closing udp connection: ", err) + v.log(LogError, "error closing udp connection: %v", err) } v.udpConn = nil } @@ -243,8 +251,11 @@ type voiceOP2 struct { Port int `json:"port"` Modes []string `json:"modes"` HeartbeatInterval time.Duration `json:"heartbeat_interval"` + IP string `json:"ip"` } +var ErrTimeoutWaitingForVoice = errors.New("timeout waiting for voice") + // WaitUntilConnected waits for the Voice Connection to // become ready, if it does not become ready it returns an err func (v *VoiceConnection) waitUntilConnected() error { @@ -261,7 +272,7 @@ func (v *VoiceConnection) waitUntilConnected() error { } if i > 10 { - return fmt.Errorf("timeout waiting for voice") + return ErrTimeoutWaitingForVoice } time.Sleep(1 * time.Second) @@ -309,8 +320,8 @@ func (v *VoiceConnection) open() (err error) { } type voiceHandshakeData struct { - ServerID string `json:"server_id"` - UserID string `json:"user_id"` + ServerID int64 `json:"server_id,string"` + UserID int64 `json:"user_id,string"` SessionID string `json:"session_id"` Token string `json:"token"` } @@ -356,7 +367,7 @@ func (v *VoiceConnection) wsListen(wsConn *websocket.Conn, close <-chan struct{} v.log(LogError, "voice endpoint %s websocket closed unexpectantly, %s", v.endpoint, err) // Start reconnect goroutine then exit. - go v.reconnect() + go v.reconnect(nil) } return } @@ -378,7 +389,7 @@ func (v *VoiceConnection) onEvent(message []byte) { v.log(LogDebug, "received: %s", string(message)) var e Event - if err := json.Unmarshal(message, &e); err != nil { + if err := gojay.UnmarshalJSONObject(message, &e); err != nil { v.log(LogError, "unmarshall error, %s", err) return } @@ -419,6 +430,8 @@ func (v *VoiceConnection) onEvent(message []byte) { go v.opusReceiver(v.udpConn, v.close, v.OpusRecv) } + // Send the ready event + v.Connected <- true return case 3: // HEARTBEAT response @@ -538,11 +551,11 @@ func (v *VoiceConnection) udpOpen() (err error) { return fmt.Errorf("nil close channel") } - if v.endpoint == "" { + if v.op2.IP == "" { return fmt.Errorf("empty endpoint") } - host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.endpoint, ":80"), v.op2.Port) + host := fmt.Sprintf("%s:%d", strings.TrimSuffix(v.op2.IP, ":80"), v.op2.Port) addr, err := net.ResolveUDPAddr("udp", host) if err != nil { v.log(LogWarning, "error resolving udp host %s, %s", host, err) @@ -560,6 +573,7 @@ func (v *VoiceConnection) udpOpen() (err error) { // into it. Then send that over the UDP connection to Discord sb := make([]byte, 70) binary.BigEndian.PutUint32(sb, v.op2.SSRC) + v.log(LogInformational, "op2 SSRC: %d", v.op2.SSRC) _, err = v.udpConn.Write(sb) if err != nil { v.log(LogWarning, "udp write error to %s, %s", addr.String(), err) @@ -571,15 +585,21 @@ func (v *VoiceConnection) udpOpen() (err error) { // of the response. This should be our public IP and PORT as Discord // saw us. rb := make([]byte, 70) - rlen, _, err := v.udpConn.ReadFromUDP(rb) - if err != nil { - v.log(LogWarning, "udp read error, %s, %s", addr.String(), err) + rChan := v.udpReadBackground(rb) + select { + case <-time.After(time.Second * 5): + go func() { + // empty the channel + <-rChan + }() + v.log(LogError, "timed out waiting for handshake after 5 seconds") return + case err = <-rChan: } - if rlen < 70 { - v.log(LogWarning, "received udp packet too small") - return fmt.Errorf("received udp packet too small") + if err != nil { + v.log(LogWarning, "udp read error, %s, %s", addr.String(), err) + return } // Loop over position 4 through 20 to grab the IP address @@ -599,6 +619,8 @@ func (v *VoiceConnection) udpOpen() (err error) { // the UDP connection handshake. data := voiceUDPOp{1, voiceUDPD{"udp", voiceUDPData{ip, port, "xsalsa20_poly1305"}}} + v.log(LogInformational, "External IP: %s, Port: %d", ip, port) + v.wsMutex.Lock() err = v.wsConn.WriteJSON(data) v.wsMutex.Unlock() @@ -614,6 +636,27 @@ func (v *VoiceConnection) udpOpen() (err error) { return } +func (v *VoiceConnection) udpReadBackground(dstBuf []byte) chan error { + c := make(chan error) + go func() { + + rlen, _, err := v.udpConn.ReadFromUDP(dstBuf) + if err != nil { + c <- err + return + } + + if rlen < len(dstBuf) { + c <- errors.New("received udp packet too small") + return + } + + c <- nil + }() + + return c +} + // udpKeepAlive sends a udp packet to keep the udp connection open // This is still a bit of a "proof of concept" func (v *VoiceConnection) udpKeepAlive(udpConn *net.UDPConn, close <-chan struct{}, i time.Duration) { @@ -782,7 +825,7 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct v.log(LogError, "udp read error, %s, %s", v.endpoint, err) v.log(LogDebug, "voice struct: %#v\n", v) - go v.reconnect() + go v.reconnect(nil) } return } @@ -829,20 +872,28 @@ func (v *VoiceConnection) opusReceiver(udpConn *net.UDPConn, close <-chan struct // NOTE : This func is messy and a WIP while I find what works. // It will be cleaned up once a proven stable option is flushed out. // aka: this is ugly shit code, please don't judge too harshly. -func (v *VoiceConnection) reconnect() { +func (v *VoiceConnection) reconnect(newGWConn *GatewayConnection) { v.log(LogInformational, "called") v.Lock() if v.reconnecting { - v.log(LogInformational, "already reconnecting to channel %s, exiting", v.ChannelID) + v.log(LogInformational, "already reconnecting to channel %d, exiting", v.ChannelID) v.Unlock() return } v.reconnecting = true + + if newGWConn != nil { + v.gatewayConn = newGWConn + } v.Unlock() - defer func() { v.reconnecting = false }() + defer func() { + v.Lock() + v.reconnecting = false + v.Unlock() + }() // Close any currently open connections v.Close() @@ -856,31 +907,39 @@ func (v *VoiceConnection) reconnect() { wait = 600 } - if v.session.DataReady == false || v.session.wsConn == nil { - v.log(LogInformational, "cannot reconnect to channel %s with unready session", v.ChannelID) + gwStatus := v.gatewayConn.Status() + if gwStatus == GatewayStatusDisconnected { + v.log(LogError, "Gateway closed, can't reconnect voice") + return + } + v.log(LogError, "status: %d", gwStatus) + if gwStatus != GatewayStatusReady { + v.log(LogInformational, "cannot reconnect to channel %d with unready gateway connection: %d", v.ChannelID, gwStatus) + continue } - v.log(LogInformational, "trying to reconnect to channel %s", v.ChannelID) + v.log(LogInformational, "trying to reconnect to channel %d", v.ChannelID) - _, err := v.session.ChannelVoiceJoin(v.GuildID, v.ChannelID, v.mute, v.deaf) + _, err := v.gatewayConn.manager.ChannelVoiceJoin(v.GuildID, v.ChannelID, v.mute, v.deaf) if err == nil { - v.log(LogInformational, "successfully reconnected to channel %s", v.ChannelID) + v.log(LogInformational, "successfully reconnected to channel %d", v.ChannelID) return } - v.log(LogInformational, "error reconnecting to channel %s, %s", v.ChannelID, err) + v.log(LogInformational, "error reconnecting to channel %d, %s", v.ChannelID, err) // if the reconnect above didn't work lets just send a disconnect // packet to reset things. // Send a OP4 with a nil channel to disconnect - data := voiceChannelJoinOp{4, voiceChannelJoinData{&v.GuildID, nil, true, true}} - v.session.wsMutex.Lock() - err = v.session.wsConn.WriteJSON(data) - v.session.wsMutex.Unlock() - if err != nil { - v.log(LogError, "error sending disconnect packet, %s", err) + strGID := StrID(v.GuildID) + data := outgoingEvent{ + Operation: GatewayOPVoiceStateUpdate, + Data: voiceChannelJoinData{&strGID, nil, true, true}, } + v.Lock() + v.gatewayConn.writer.Queue(data) + v.Unlock() } } diff --git a/wsapi.go b/wsapi.go index 80e85ac30..29fd54122 100644 --- a/wsapi.go +++ b/wsapi.go @@ -1,850 +1,195 @@ -// Discordgo - Discord bindings for Go -// Available at https://github.com/bwmarrin/discordgo - -// Copyright 2015-2016 Bruce Marriner . All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// This file contains low level functions for interacting with the Discord -// data websocket interface. - package discordgo import ( - "bytes" - "compress/zlib" "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "runtime" + "net" + "sync" "sync/atomic" "time" - "github.com/gorilla/websocket" + "github.com/gobwas/ws" + "github.com/gobwas/ws/wsutil" ) -// ErrWSAlreadyOpen is thrown when you attempt to open -// a websocket that already is open. -var ErrWSAlreadyOpen = errors.New("web socket already opened") - -// ErrWSNotFound is thrown when you attempt to use a websocket -// that doesn't exist -var ErrWSNotFound = errors.New("no websocket connection exists") - -// ErrWSShardBounds is thrown when you try to use a shard ID that is -// less than the total shard count -var ErrWSShardBounds = errors.New("ShardID must be less than ShardCount") - -type resumePacket struct { - Op int `json:"op"` - Data struct { - Token string `json:"token"` - SessionID string `json:"session_id"` - Sequence int64 `json:"seq"` - } `json:"d"` -} - -// Open creates a websocket connection to Discord. -// See: https://discordapp.com/developers/docs/topics/gateway#connecting -func (s *Session) Open() error { - s.log(LogInformational, "called") - - var err error - - // Prevent Open or other major Session functions from - // being called while Open is still running. - s.Lock() - defer s.Unlock() - - // If the websock is already open, bail out here. - if s.wsConn != nil { - return ErrWSAlreadyOpen - } - - // Get the gateway to use for the Websocket connection - if s.gateway == "" { - s.gateway, err = s.Gateway() - if err != nil { - return err - } - - // Add the version and encoding to the URL - s.gateway = s.gateway + "?v=" + APIVersion + "&encoding=json" - } - - // Connect to the Gateway - s.log(LogInformational, "connecting to gateway %s", s.gateway) - header := http.Header{} - header.Add("accept-encoding", "zlib") - s.wsConn, _, err = websocket.DefaultDialer.Dial(s.gateway, header) - if err != nil { - s.log(LogWarning, "error connecting to gateway %s, %s", s.gateway, err) - s.gateway = "" // clear cached gateway - s.wsConn = nil // Just to be safe. - return err - } - - defer func() { - // because of this, all code below must set err to the error - // when exiting with an error :) Maybe someone has a better - // way :) - if err != nil { - s.wsConn.Close() - s.wsConn = nil - } - }() - - // The first response from Discord should be an Op 10 (Hello) Packet. - // When processed by onEvent the heartbeat goroutine will be started. - mt, m, err := s.wsConn.ReadMessage() - if err != nil { - return err - } - e, err := s.onEvent(mt, m) - if err != nil { - return err - } - if e.Operation != 10 { - err = fmt.Errorf("expecting Op 10, got Op %d instead", e.Operation) - return err - } - s.log(LogInformational, "Op 10 Hello Packet received from Discord") - s.LastHeartbeatAck = time.Now().UTC() - var h helloOp - if err = json.Unmarshal(e.RawData, &h); err != nil { - err = fmt.Errorf("error unmarshalling helloOp, %s", err) - return err - } - - // Now we send either an Op 2 Identity if this is a brand new - // connection or Op 6 Resume if we are resuming an existing connection. - sequence := atomic.LoadInt64(s.sequence) - if s.sessionID == "" && sequence == 0 { +type wsWriter struct { + session *Session - // Send Op 2 Identity Packet - err = s.identify() - if err != nil { - err = fmt.Errorf("error sending identify packet to gateway, %s, %s", s.gateway, err) - return err - } - - } else { - - // Send Op 6 Resume Packet - p := resumePacket{} - p.Op = 6 - p.Data.Token = s.Token - p.Data.SessionID = s.sessionID - p.Data.Sequence = sequence - - s.log(LogInformational, "sending resume packet to gateway") - s.wsMutex.Lock() - err = s.wsConn.WriteJSON(p) - s.wsMutex.Unlock() - if err != nil { - err = fmt.Errorf("error sending gateway resume packet, %s, %s", s.gateway, err) - return err - } + conn net.Conn + closer chan interface{} + incoming chan interface{} + sendCloseQueue chan []byte - } + writer *wsutil.Writer - // A basic state is a hard requirement for Voice. - // We create it here so the below READY/RESUMED packet can populate - // the state :) - // XXX: Move to New() func? - if s.State == nil { - state := NewState() - state.TrackChannels = false - state.TrackEmojis = false - state.TrackMembers = false - state.TrackRoles = false - state.TrackVoice = false - s.State = state - } - - // Now Discord should send us a READY or RESUMED packet. - mt, m, err = s.wsConn.ReadMessage() - if err != nil { - return err - } - e, err = s.onEvent(mt, m) - if err != nil { - return err - } - if e.Type != `READY` && e.Type != `RESUMED` { - // This is not fatal, but it does not follow their API documentation. - s.log(LogWarning, "Expected READY/RESUMED, instead got:\n%#v\n", e) - } - s.log(LogInformational, "First Packet:\n%#v\n", e) - - s.log(LogInformational, "We are now connected to Discord, emitting connect event") - s.handleEvent(connectEventType, &Connect{}) - - // A VoiceConnections map is a hard requirement for Voice. - // XXX: can this be moved to when opening a voice connection? - if s.VoiceConnections == nil { - s.log(LogInformational, "creating new VoiceConnections map") - s.VoiceConnections = make(map[string]*VoiceConnection) - } - - // Create listening chan outside of listen, as it needs to happen inside the - // mutex lock and needs to exist before calling heartbeat and listen - // go rountines. - s.listening = make(chan interface{}) - - // Start sending heartbeats and reading messages from Discord. - go s.heartbeat(s.wsConn, s.listening, h.HeartbeatInterval) - go s.listen(s.wsConn, s.listening) - - s.log(LogInformational, "exiting") - return nil + sendRatelimiter chan bool } -// listen polls the websocket connection for events, it will stop when the -// listening channel is closed, or an error occurs. -func (s *Session) listen(wsConn *websocket.Conn, listening <-chan interface{}) { +func (w *wsWriter) Run() { + w.sendRatelimiter = make(chan bool) + go w.runSendRatelimiter() - s.log(LogInformational, "called") + w.writer = wsutil.NewWriter(w.conn, ws.StateClientSide, ws.OpText) for { - - messageType, message, err := wsConn.ReadMessage() - - if err != nil { - - // Detect if we have been closed manually. If a Close() has already - // happened, the websocket we are listening on will be different to - // the current session. - s.RLock() - sameConnection := s.wsConn == wsConn - s.RUnlock() - - if sameConnection { - - s.log(LogWarning, "error reading from gateway %s websocket, %s", s.gateway, err) - // There has been an error reading, close the websocket so that - // OnDisconnect event is emitted. - err := s.Close() - if err != nil { - s.log(LogWarning, "error closing session connection, %s", err) - } - - s.log(LogInformational, "calling reconnect() now") - s.reconnect() - } - - return - } - select { - - case <-listening: + case <-w.closer: + select { + // Ensure we send the close frame + case body := <-w.sendCloseQueue: + w.sendClose(body) + default: + } return + case msg := <-w.incoming: + var err error + switch t := msg.(type) { + case []byte: + err = w.writeRaw(t) + default: + err = w.writeJson(t) + } - default: - s.onEvent(messageType, message) - + if err != nil { + w.session.log(LogError, "Error writing to gateway: %s", err.Error()) + return + } + case body := <-w.sendCloseQueue: + w.sendClose(body) } } } -type heartbeatOp struct { - Op int `json:"op"` - Data int64 `json:"d"` -} - -type helloOp struct { - HeartbeatInterval time.Duration `json:"heartbeat_interval"` - Trace []string `json:"_trace"` -} - -// FailedHeartbeatAcks is the Number of heartbeat intervals to wait until forcing a connection restart. -const FailedHeartbeatAcks time.Duration = 5 * time.Millisecond - -// heartbeat sends regular heartbeats to Discord so it knows the client -// is still connected. If you do not send these heartbeats Discord will -// disconnect the websocket connection after a few seconds. -func (s *Session) heartbeat(wsConn *websocket.Conn, listening <-chan interface{}, heartbeatIntervalMsec time.Duration) { - - s.log(LogInformational, "called") - - if listening == nil || wsConn == nil { - return - } - - var err error - ticker := time.NewTicker(heartbeatIntervalMsec * time.Millisecond) - defer ticker.Stop() +func (w *wsWriter) runSendRatelimiter() { + tc := time.NewTicker(time.Millisecond * 500) + defer tc.Stop() + defer func() { + close(w.sendRatelimiter) + }() for { - s.RLock() - last := s.LastHeartbeatAck - s.RUnlock() - sequence := atomic.LoadInt64(s.sequence) - s.log(LogInformational, "sending gateway websocket heartbeat seq %d", sequence) - s.wsMutex.Lock() - err = wsConn.WriteJSON(heartbeatOp{1, sequence}) - s.wsMutex.Unlock() - if err != nil || time.Now().UTC().Sub(last) > (heartbeatIntervalMsec*FailedHeartbeatAcks) { - if err != nil { - s.log(LogError, "error sending heartbeat to gateway %s, %s", s.gateway, err) - } else { - s.log(LogError, "haven't gotten a heartbeat ACK in %v, triggering a reconnection", time.Now().UTC().Sub(last)) - } - s.Close() - s.reconnect() - return - } - s.Lock() - s.DataReady = true - s.Unlock() - select { - case <-ticker.C: - // continue loop and send heartbeat - case <-listening: + case <-tc.C: + select { + case w.sendRatelimiter <- true: + case <-w.closer: + return + } + case <-w.closer: return } } } -// UpdateStatusData ia provided to UpdateStatusComplex() -type UpdateStatusData struct { - IdleSince *int `json:"since"` - Game *Game `json:"game"` - AFK bool `json:"afk"` - Status string `json:"status"` -} - -type updateStatusOp struct { - Op int `json:"op"` - Data UpdateStatusData `json:"d"` -} - -func newUpdateStatusData(idle int, gameType GameType, game, url string) *UpdateStatusData { - usd := &UpdateStatusData{ - Status: "online", - } - - if idle > 0 { - usd.IdleSince = &idle - } - - if game != "" { - usd.Game = &Game{ - Name: game, - Type: gameType, - URL: url, - } - } - - return usd -} - -// UpdateStatus is used to update the user's status. -// If idle>0 then set status to idle. -// If game!="" then set game. -// if otherwise, set status to active, and no game. -func (s *Session) UpdateStatus(idle int, game string) (err error) { - return s.UpdateStatusComplex(*newUpdateStatusData(idle, GameTypeGame, game, "")) -} - -// UpdateStreamingStatus is used to update the user's streaming status. -// If idle>0 then set status to idle. -// If game!="" then set game. -// If game!="" and url!="" then set the status type to streaming with the URL set. -// if otherwise, set status to active, and no game. -func (s *Session) UpdateStreamingStatus(idle int, game string, url string) (err error) { - gameType := GameTypeGame - if url != "" { - gameType = GameTypeStreaming - } - return s.UpdateStatusComplex(*newUpdateStatusData(idle, gameType, game, url)) -} - -// UpdateListeningStatus is used to set the user to "Listening to..." -// If game!="" then set to what user is listening to -// Else, set user to active and no game. -func (s *Session) UpdateListeningStatus(game string) (err error) { - return s.UpdateStatusComplex(*newUpdateStatusData(0, GameTypeListening, game, "")) -} - -// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. -func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) { - - s.RLock() - defer s.RUnlock() - if s.wsConn == nil { - return ErrWSNotFound - } - - s.wsMutex.Lock() - err = s.wsConn.WriteJSON(updateStatusOp{3, usd}) - s.wsMutex.Unlock() - - return -} - -type requestGuildMembersData struct { - GuildID string `json:"guild_id"` - Query string `json:"query"` - Limit int `json:"limit"` -} - -type requestGuildMembersOp struct { - Op int `json:"op"` - Data requestGuildMembersData `json:"d"` -} - -// RequestGuildMembers requests guild members from the gateway -// The gateway responds with GuildMembersChunk events -// guildID : The ID of the guild to request members of -// query : String that username starts with, leave empty to return all members -// limit : Max number of items to return, or 0 to request all members matched -func (s *Session) RequestGuildMembers(guildID, query string, limit int) (err error) { - s.log(LogInformational, "called") - - s.RLock() - defer s.RUnlock() - if s.wsConn == nil { - return ErrWSNotFound - } - - data := requestGuildMembersData{ - GuildID: guildID, - Query: query, - Limit: limit, +func (w *wsWriter) writeJson(data interface{}) error { + serialized, err := json.Marshal(data) + if err != nil { + return err } - s.wsMutex.Lock() - err = s.wsConn.WriteJSON(requestGuildMembersOp{8, data}) - s.wsMutex.Unlock() - - return + return w.writeRaw(serialized) } -// onEvent is the "event handler" for all messages received on the -// Discord Gateway API websocket connection. -// -// If you use the AddHandler() function to register a handler for a -// specific event this function will pass the event along to that handler. -// -// If you use the AddHandler() function to register a handler for the -// "OnEvent" event then all events will be passed to that handler. -func (s *Session) onEvent(messageType int, message []byte) (*Event, error) { - - var err error - var reader io.Reader - reader = bytes.NewBuffer(message) - - // If this is a compressed message, uncompress it. - if messageType == websocket.BinaryMessage { - - z, err2 := zlib.NewReader(reader) - if err2 != nil { - s.log(LogError, "error uncompressing websocket message, %s", err) - return nil, err2 - } +func (w *wsWriter) writeRaw(data []byte) error { + <-w.sendRatelimiter - defer func() { - err3 := z.Close() - if err3 != nil { - s.log(LogWarning, "error closing zlib, %s", err) - } - }() - - reader = z - } - - // Decode the event into an Event struct. - var e *Event - decoder := json.NewDecoder(reader) - if err = decoder.Decode(&e); err != nil { - s.log(LogError, "error decoding websocket message, %s", err) - return e, err - } - - s.log(LogDebug, "Op: %d, Seq: %d, Type: %s, Data: %s\n\n", e.Operation, e.Sequence, e.Type, string(e.RawData)) - - // Ping request. - // Must respond with a heartbeat packet within 5 seconds - if e.Operation == 1 { - s.log(LogInformational, "sending heartbeat in response to Op1") - s.wsMutex.Lock() - err = s.wsConn.WriteJSON(heartbeatOp{1, atomic.LoadInt64(s.sequence)}) - s.wsMutex.Unlock() - if err != nil { - s.log(LogError, "error sending heartbeat in response to Op1") - return e, err - } - - return e, nil - } - - // Reconnect - // Must immediately disconnect from gateway and reconnect to new gateway. - if e.Operation == 7 { - s.log(LogInformational, "Closing and reconnecting in response to Op7") - s.Close() - s.reconnect() - return e, nil - } - - // Invalid Session - // Must respond with a Identify packet. - if e.Operation == 9 { - - s.log(LogInformational, "sending identify packet to gateway in response to Op9") - - err = s.identify() - if err != nil { - s.log(LogWarning, "error sending gateway identify packet, %s, %s", s.gateway, err) - return e, err - } - - return e, nil - } - - if e.Operation == 10 { - // Op10 is handled by Open() - return e, nil - } - - if e.Operation == 11 { - s.Lock() - s.LastHeartbeatAck = time.Now().UTC() - s.Unlock() - s.log(LogInformational, "got heartbeat ACK") - return e, nil - } - - // Do not try to Dispatch a non-Dispatch Message - if e.Operation != 0 { - // But we probably should be doing something with them. - // TEMP - s.log(LogWarning, "unknown Op: %d, Seq: %d, Type: %s, Data: %s, message: %s", e.Operation, e.Sequence, e.Type, string(e.RawData), string(message)) - return e, nil - } - - // Store the message sequence - atomic.StoreInt64(s.sequence, e.Sequence) - - // Map event to registered event handlers and pass it along to any registered handlers. - if eh, ok := registeredInterfaceProviders[e.Type]; ok { - e.Struct = eh.New() - - // Attempt to unmarshal our event. - if err = json.Unmarshal(e.RawData, e.Struct); err != nil { - s.log(LogError, "error unmarshalling %s event, %s", e.Type, err) - } - - // Send event to any registered event handlers for it's type. - // Because the above doesn't cancel this, in case of an error - // the struct could be partially populated or at default values. - // However, most errors are due to a single field and I feel - // it's better to pass along what we received than nothing at all. - // TODO: Think about that decision :) - // Either way, READY events must fire, even with errors. - s.handleEvent(e.Type, e.Struct) - } else { - s.log(LogWarning, "unknown event: Op: %d, Seq: %d, Type: %s, Data: %s", e.Operation, e.Sequence, e.Type, string(e.RawData)) + _, err := w.writer.WriteThrough(data) + if err != nil { + return err } - // For legacy reasons, we send the raw event also, this could be useful for handling unknown events. - s.handleEvent(eventEventType, e) - - return e, nil + return w.writer.Flush() } -// ------------------------------------------------------------------------------------------------ -// Code related to voice connections that initiate over the data websocket -// ------------------------------------------------------------------------------------------------ - -type voiceChannelJoinData struct { - GuildID *string `json:"guild_id"` - ChannelID *string `json:"channel_id"` - SelfMute bool `json:"self_mute"` - SelfDeaf bool `json:"self_deaf"` -} +func (w *wsWriter) sendClose(body []byte) error { + _, err := w.conn.Write(body) + return err -type voiceChannelJoinOp struct { - Op int `json:"op"` - Data voiceChannelJoinData `json:"d"` } -// ChannelVoiceJoin joins the session user to a voice channel. -// -// gID : Guild ID of the channel to join. -// cID : Channel ID of the channel to join. -// mute : If true, you will be set to muted upon joining. -// deaf : If true, you will be set to deafened upon joining. -func (s *Session) ChannelVoiceJoin(gID, cID string, mute, deaf bool) (voice *VoiceConnection, err error) { - - s.log(LogInformational, "called") - - s.RLock() - voice, _ = s.VoiceConnections[gID] - s.RUnlock() - - if voice == nil { - voice = &VoiceConnection{} - s.Lock() - s.VoiceConnections[gID] = voice - s.Unlock() - } - - voice.Lock() - voice.GuildID = gID - voice.ChannelID = cID - voice.deaf = deaf - voice.mute = mute - voice.session = s - voice.Unlock() - - // Send the request to Discord that we want to join the voice channel - data := voiceChannelJoinOp{4, voiceChannelJoinData{&gID, &cID, mute, deaf}} - s.wsMutex.Lock() - err = s.wsConn.WriteJSON(data) - s.wsMutex.Unlock() - if err != nil { - return - } - - // doesn't exactly work perfect yet.. TODO - err = voice.waitUntilConnected() - if err != nil { - s.log(LogWarning, "error waiting for voice to connect, %s", err) - voice.Close() - return +func (w *wsWriter) Queue(data interface{}) { + select { + case <-time.After(time.Second * 10): + case <-w.closer: + case w.incoming <- data: } - - return } -// onVoiceStateUpdate handles Voice State Update events on the data websocket. -func (s *Session) onVoiceStateUpdate(st *VoiceStateUpdate) { - - // If we don't have a connection for the channel, don't bother - if st.ChannelID == "" { - return - } - - // Check if we have a voice connection to update - s.RLock() - voice, exists := s.VoiceConnections[st.GuildID] - s.RUnlock() - if !exists { - return +func (w *wsWriter) QueueClose(body []byte) { + select { + case <-time.After(time.Second * 10): + case <-w.closer: + case w.sendCloseQueue <- body: } - - // We only care about events that are about us. - if s.State.User.ID != st.UserID { - return - } - - // Store the SessionID for later use. - voice.Lock() - voice.UserID = st.UserID - voice.sessionID = st.SessionID - voice.ChannelID = st.ChannelID - voice.Unlock() } -// onVoiceServerUpdate handles the Voice Server Update data websocket event. -// -// This is also fired if the Guild's voice region changes while connected -// to a voice channel. In that case, need to re-establish connection to -// the new region endpoint. -func (s *Session) onVoiceServerUpdate(st *VoiceServerUpdate) { - - s.log(LogInformational, "called") +type wsHeartBeater struct { + sync.Mutex - s.RLock() - voice, exists := s.VoiceConnections[st.GuildID] - s.RUnlock() + writer *wsWriter + sequence *int64 + receivedAck bool + missedAcks int + stop chan interface{} - // If no VoiceConnection exists, just skip this - if !exists { - return - } - - // If currently connected to voice ws/udp, then disconnect. - // Has no effect if not connected. - voice.Close() + // Called when we received no Ack from last heartbeat + onNoAck func() - // Store values for later use - voice.Lock() - voice.token = st.Token - voice.endpoint = st.Endpoint - voice.GuildID = st.GuildID - voice.Unlock() - - // Open a connection to the voice server - err := voice.open() - if err != nil { - s.log(LogError, "onVoiceServerUpdate voice.open, %s", err) - } -} - -type identifyProperties struct { - OS string `json:"$os"` - Browser string `json:"$browser"` - Device string `json:"$device"` - Referer string `json:"$referer"` - ReferringDomain string `json:"$referring_domain"` + lastAck time.Time + lastSend time.Time } -type identifyData struct { - Token string `json:"token"` - Properties identifyProperties `json:"properties"` - LargeThreshold int `json:"large_threshold"` - Compress bool `json:"compress"` - Shard *[2]int `json:"shard,omitempty"` +func (wh *wsHeartBeater) ReceivedAck() { + wh.Lock() + wh.receivedAck = true + wh.missedAcks = 0 + wh.lastAck = time.Now() + wh.Unlock() } -type identifyOp struct { - Op int `json:"op"` - Data identifyData `json:"d"` +func (wh *wsHeartBeater) UpdateSequence(seq int64) { + atomic.StoreInt64(wh.sequence, seq) } -// identify sends the identify packet to the gateway -func (s *Session) identify() error { - - properties := identifyProperties{runtime.GOOS, - "Discordgo v" + VERSION, - "", - "", - "", - } - - data := identifyData{s.Token, - properties, - 250, - s.Compress, - nil, - } - - if s.ShardCount > 1 { - - if s.ShardID >= s.ShardCount { - return ErrWSShardBounds - } - - data.Shard = &[2]int{s.ShardID, s.ShardCount} - } - - op := identifyOp{2, data} - - s.wsMutex.Lock() - err := s.wsConn.WriteJSON(op) - s.wsMutex.Unlock() - - return err -} - -func (s *Session) reconnect() { - - s.log(LogInformational, "called") - - var err error - - if s.ShouldReconnectOnError { +func (wh *wsHeartBeater) Run(interval time.Duration) { + ticker := time.NewTicker(interval) - wait := time.Duration(1) - - for { - s.log(LogInformational, "trying to reconnect to gateway") - - err = s.Open() - if err == nil { - s.log(LogInformational, "successfully reconnected to gateway") - - // I'm not sure if this is actually needed. - // if the gw reconnect works properly, voice should stay alive - // However, there seems to be cases where something "weird" - // happens. So we're doing this for now just to improve - // stability in those edge cases. - s.RLock() - defer s.RUnlock() - for _, v := range s.VoiceConnections { - - s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID) - go v.reconnect() + for { + select { + case <-ticker.C: - // This is here just to prevent violently spamming the - // voice reconnects - time.Sleep(1 * time.Second) + wh.Lock() + hasReceivedAck := wh.receivedAck + wh.receivedAck = false + wh.missedAcks++ + missed := wh.missedAcks - } - return - } + wh.lastSend = time.Now() + wh.Unlock() - // Certain race conditions can call reconnect() twice. If this happens, we - // just break out of the reconnect loop - if err == ErrWSAlreadyOpen { - s.log(LogInformational, "Websocket already exists, no need to reconnect") - return + if !hasReceivedAck && wh.onNoAck != nil && missed > 4 { + wh.onNoAck() } - s.log(LogError, "error reconnecting to gateway, %s", err) - - <-time.After(wait * time.Second) - wait *= 2 - if wait > 600 { - wait = 600 - } + wh.SendBeat() + case <-wh.stop: + return } } } -// Close closes a websocket and stops all listening/heartbeat goroutines. -// TODO: Add support for Voice WS/UDP connections -func (s *Session) Close() (err error) { - - s.log(LogInformational, "called") - s.Lock() +func (wh *wsHeartBeater) SendBeat() { + seq := atomic.LoadInt64(wh.sequence) - s.DataReady = false - - if s.listening != nil { - s.log(LogInformational, "closing listening channel") - close(s.listening) - s.listening = nil - } - - // TODO: Close all active Voice Connections too - // this should force stop any reconnecting voice channels too - - if s.wsConn != nil { - - s.log(LogInformational, "sending close frame") - // To cleanly close a connection, a client should send a close - // frame and wait for the server to close the connection. - s.wsMutex.Lock() - err := s.wsConn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")) - s.wsMutex.Unlock() - if err != nil { - s.log(LogInformational, "error closing websocket, %s", err) - } - - // TODO: Wait for Discord to actually close the connection. - time.Sleep(1 * time.Second) - - s.log(LogInformational, "closing gateway websocket") - err = s.wsConn.Close() - if err != nil { - s.log(LogInformational, "error closing websocket, %s", err) - } - - s.wsConn = nil - } - - s.Unlock() - - s.log(LogInformational, "emit disconnect event") - s.handleEvent(disconnectEventType, &Disconnect{}) + wh.writer.Queue(&outgoingEvent{ + Operation: GatewayOPHeartbeat, + Data: seq, + }) +} +func (wh *wsHeartBeater) Times() (send time.Time, ack time.Time) { + wh.Lock() + send = wh.lastSend + ack = wh.lastAck + wh.Unlock() return }