diff --git a/README.md b/README.md index 3779063..5d05bac 100644 --- a/README.md +++ b/README.md @@ -7,47 +7,53 @@ # snowflake snowflake is a golang library for parsing [snowflake IDs](https://docs.snowflake.com) from discord. -This package provides a custom `snowflake` type which has various utility methods for parsing snowflake IDs. +This package provides a custom `snowflake.ID` type which has various utility methods for parsing discord snowflakes. ### Installing ```sh -go get github.com/disgoorg/snowflake +go get github.com/disgoorg/snowflake/v2 ``` ## Usage ```go -id := Snowflake("123456789012345678") +id := snowflake.ID(123456789012345678) // deconstructs the snowflake ID into its components timestamp, worker ID, process ID, and increment id.Deconstruct() -// time.Time when the snowflake was generated +// the time.Time when the snowflake ID was generated id.Time() +// the worker ID which the snowflake ID was generated +id.WorkerID() + +// the process ID which the snowflake ID was generated +id.ProcessID() + +// tje sequence when the snowflake ID was generated +id.Sequence() + // returns the string representation of the snowflake ID id.String() -// returns the int64 representation of the snowflake ID -id.Int64() - -// returns a new snowflake with worker ID, process ID, and increment set to 0 +// returns a new snowflake ID with worker ID, process ID, and sequence set to 0 // this can be used for various pagination requests to the discord api -id = NewSnowflake(time.Now()) +id := New(time.Now()) -// returns the fmt.Stringer as a Snowflake -id = ParseString(...) +// returns a snowflake ID from an environment variable +id := GetEnv("guild_id") -// returns the int64 as a Snowflake -id = ParseInt64(123456789012345678) +// returns a snowflake ID from an environment variable and a bool indicating if the key was found +id, found := LookupEnv("guild_id") -// returns the uint64 as a Snowflake -id = ParseUInt64(123456789012345678) +// returns the string as a snowflake ID or an error +id, err := Parse("123456789012345678") -// returns a snowflake from an environment variable -id = GetSnowflakeEnv("guild_id") +// returns the string as a snowflake ID or panics if an error occurs +id := MustParse("123456789012345678") ``` ## License diff --git a/go.mod b/go.mod index 8295650..591e298 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/disgoorg/snowflake +module github.com/disgoorg/snowflake/v2 go 1.18 diff --git a/snowflake.go b/snowflake.go index a831b81..61422ca 100644 --- a/snowflake.go +++ b/snowflake.go @@ -1,81 +1,124 @@ package snowflake import ( - "fmt" + "bytes" "os" "strconv" "time" ) -var Epoch int64 = 1420070400000 +// Epoch is the discord epoch in milliseconds. +const Epoch = 1420070400000 -// NewSnowflake returns a new Snowflake based on the given time.Time -//goland:noinspection GoUnusedExportedFunction -func NewSnowflake(timestamp time.Time) Snowflake { - return Snowflake(strconv.FormatInt(((timestamp.UnixNano()/1_000_000)-Epoch)<<22, 10)) +// Parse parses a string into a snowflake ID. +// returns ID(0) if the string is "null" +func Parse(str string) (ID, error) { + if str == "null" { + return 0, nil + } + id, err := strconv.ParseUint(str, 10, 64) + if err != nil { + return 0, err + } + return ID(id), nil } -// ParseString parses a fmt.Stringer into a Snowflake -//goland:noinspection GoUnusedExportedFunction -func ParseString(str fmt.Stringer) Snowflake { - return Snowflake(str.String()) +// MustParse parses a string into a snowflake ID and panics on error. +// returns ID(0) if the string is "null" +func MustParse(str string) ID { + id, err := Parse(str) + if err != nil { + panic(err) + } + return id } -// ParseInt64 parses an int64 into a Snowflake -//goland:noinspection GoUnusedExportedFunction -func ParseInt64(i int64) Snowflake { - return Snowflake(strconv.FormatInt(i, 10)) +// GetEnv returns the value of the environment variable named by the key and parses it as a snowflake. +// returns ID(0) if the environment variable is not set. +func GetEnv(key string) ID { + snowflake, _ := LookupEnv(key) + return snowflake } -// ParseUInt64 parses an uint64 into a Snowflake -//goland:noinspection GoUnusedExportedFunction -func ParseUInt64(i uint64) Snowflake { - return Snowflake(strconv.FormatUint(i, 10)) +// LookupEnv returns the value of the environment variable named by the key and parses it as a snowflake. +// returns false if the environment variable is not set. +func LookupEnv(key string) (ID, bool) { + env, found := os.LookupEnv(key) + if !found { + return 0, false + } + snowflake, _ := Parse(env) + return snowflake, true } -// GetSnowflakeEnv returns a new Snowflake from an environment variable -//goland:noinspection GoUnusedExportedFunction -func GetSnowflakeEnv(key string) Snowflake { - return Snowflake(os.Getenv(key)) +// New creates a new snowflake ID from the provided timestamp with worker id and sequence 0. +func New(timestamp time.Time) ID { + return ID((timestamp.UnixMilli() - Epoch) << 22) } -// Snowflake is a general utility type around discord's IDs -type Snowflake string +// ID represents a unique snowflake ID. +type ID uint64 -// String returns the string representation of the Snowflake -func (s Snowflake) String() string { - return string(s) +// MarshalJSON marshals the snowflake ID into a JSON string. +func (id ID) MarshalJSON() ([]byte, error) { + return []byte(strconv.Quote(strconv.FormatUint(uint64(id), 10))), nil } -// Int64 returns the int64 representation of the Snowflake -func (s Snowflake) Int64() int64 { - snowflake, err := strconv.ParseInt(s.String(), 10, 64) +// UnmarshalJSON unmarshals the snowflake ID from a JSON string. +func (id *ID) UnmarshalJSON(data []byte) error { + if bytes.Equal(data, []byte("null")) { + return nil + } + snowflake, err := strconv.Unquote(string(data)) if err != nil { - panic(err.Error()) + return err } - return snowflake + i, err := strconv.ParseUint(snowflake, 10, 64) + if err != nil { + return err + } + *id = ID(i) + return nil } -// Deconstruct returns DeconstructedSnowflake (https://discord.com/developers/docs/reference#snowflakes-snowflake-id-format-structure-left-to-right) -func (s Snowflake) Deconstruct() DeconstructedSnowflake { - snowflake := s.Int64() - return DeconstructedSnowflake{ - Time: time.Unix(0, ((snowflake>>22)+Epoch)*1_000_000), - WorkerID: (snowflake & 0x3E0000) >> 17, - ProcessID: (snowflake & 0x1F000) >> 12, - Increment: snowflake & 0xFFF, - } +// String returns a string representation of the snowflake ID. +func (id ID) String() string { + return strconv.FormatUint(uint64(id), 10) +} + +// Time returns the time.Time the snowflake was created. +func (id ID) Time() time.Time { + return time.UnixMilli(int64(id>>22 + Epoch)) } -// Time returns the time.Time when the snowflake was created -func (s Snowflake) Time() time.Time { - return s.Deconstruct().Time +// WorkerID returns the id of the worker the snowflake was created on. +func (id ID) WorkerID() uint8 { + return uint8(id & 0x3E0000 >> 17) +} + +func (id ID) ProcessID() uint8 { + return uint8(id & 0x3E0000 >> 12) +} + +// Sequence returns the sequence of the snowflake. +func (id ID) Sequence() uint16 { + return uint16(id & 0xFFF) +} + +// Deconstruct returns DeconstructedID (https://discord.com/developers/docs/reference#snowflakes-snowflake-id-format-structure-left-to-right). +func (id ID) Deconstruct() DeconstructedSnowflake { + return DeconstructedSnowflake{ + Time: id.Time(), + WorkerID: id.WorkerID(), + ProcessID: id.ProcessID(), + Sequence: id.Sequence(), + } } -// DeconstructedSnowflake contains the properties used by Discord for each CommandID +// DeconstructedSnowflake contains the properties used by Discord for each snowflake ID. type DeconstructedSnowflake struct { Time time.Time - WorkerID int64 - ProcessID int64 - Increment int64 + WorkerID uint8 + ProcessID uint8 + Sequence uint16 }