Merge branch 'master' into master

This commit is contained in:
icez
2024-07-02 20:18:17 +07:00
committed by GitHub
2463 changed files with 3362159 additions and 2030553 deletions

View File

@@ -12,11 +12,11 @@ jobs:
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: latest version: latest
args: "-v --new-from-rev HEAD~5" args: "-v --new-from-rev HEAD~5 --timeout=5m"
test-build-upload: test-build-upload:
strategy: strategy:
matrix: matrix:
go-version: [1.20.x] go-version: [1.22.x]
platform: [ubuntu-latest] platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }} runs-on: ${{ matrix.platform }}
steps: steps:
@@ -39,19 +39,19 @@ jobs:
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/win/matterbridge-$VERSION-windows-amd64.exe
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64 CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -X github.com/42wim/matterbridge/version.GitHash=$(git log --pretty=format:'%h' -n 1)" -o output/mac/matterbridge-$VERSION-darwin-amd64
- name: Upload linux 64-bit - name: Upload linux 64-bit
if: startsWith(matrix.go-version,'1.20') if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: matterbridge-linux-64bit name: matterbridge-linux-64bit
path: output/lin path: output/lin
- name: Upload windows 64-bit - name: Upload windows 64-bit
if: startsWith(matrix.go-version,'1.20') if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: matterbridge-windows-64bit name: matterbridge-windows-64bit
path: output/win path: output/win
- name: Upload darwin 64-bit - name: Upload darwin 64-bit
if: startsWith(matrix.go-version,'1.20') if: startsWith(matrix.go-version,'1.22')
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
with: with:
name: matterbridge-darwin-64bit name: matterbridge-darwin-64bit

View File

@@ -212,6 +212,9 @@ linters:
- execinquery - execinquery
- nosnakecase - nosnakecase
- exhaustive - exhaustive
- testifylint
- mnd
- depguard
# rules to deal with reported isues # rules to deal with reported isues
issues: issues:
# List of regexps of issue texts to exclude, empty list by default. # List of regexps of issue texts to exclude, empty list by default.

View File

@@ -115,19 +115,21 @@ And more...
### 3rd party via matterbridge api ### 3rd party via matterbridge api
- [Delta Chat](https://github.com/deltachat-bot/matterdelta) - [Delta Chat](https://github.com/deltachat-bot/matterdelta)
- [Minecraft](https://github.com/raws/mattercraft)
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
#### Past 3rd party projects
- [Discourse](https://github.com/DeclanHoare/matterbabble) - [Discourse](https://github.com/DeclanHoare/matterbabble)
- [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio) - [Facebook messenger](https://github.com/powerjungle/fbridge-asyncio)
- [Facebook messenger](https://github.com/VictorNine/fbridge) - [Facebook messenger](https://github.com/VictorNine/fbridge)
- [Minecraft](https://github.com/elytra/MatterLink) - [Minecraft](https://github.com/elytra/MatterLink)
- [Minecraft](https://github.com/raws/mattercraft)
- [Minecraft](https://gitlab.com/Programie/MatterBukkit)
- [Reddit](https://github.com/bonehurtingjuice/mattereddit) - [Reddit](https://github.com/bonehurtingjuice/mattereddit)
- [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430) - [MatterAMXX](https://github.com/andrewlindberg/MatterAMXX): [Counter-Strike, half-life and more](https://forums.alliedmods.net/showthread.php?t=319430)
- [MatterAMXX](https://github.com/GabeIggy/MatterAMXX)
- [Vintage Story](https://github.com/NikkyAI/vs-matterbridge) - [Vintage Story](https://github.com/NikkyAI/vs-matterbridge)
- [Ultima Online Emulator](https://github.com/kuoushi/ServUO-Matterbridge) - [Ultima Online Emulator](https://github.com/kuoushi/ServUO-Matterbridge)
- [Teamspeak](https://github.com/Archeb/ts-matterbridge) - [Teamspeak](https://github.com/Archeb/ts-matterbridge)
### API ### API
The API is basic at the moment. The API is basic at the moment.

View File

@@ -121,6 +121,7 @@ type Protocol struct {
MessageLength int // IRC, max length of a message allowed MessageLength int // IRC, max length of a message allowed
MessageQueue int // IRC, size of message queue for flood control MessageQueue int // IRC, size of message queue for flood control
MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping MessageSplit bool // IRC, split long messages with newlines on MessageLength instead of clipping
MessageSplitMaxCount int // discord, split long messages into at most this many messages instead of clipping (MessageLength=1950 cannot be configured)
Muc string // xmpp Muc string // xmpp
MxID string // matrix MxID string // matrix
Name string // all protocols Name string // all protocols

View File

@@ -90,7 +90,7 @@ func (b *Bdiscord) Connect() error {
if err != nil { if err != nil {
return err return err
} }
guilds, err := b.c.UserGuilds(100, "", "") guilds, err := b.c.UserGuilds(100, "", "", false)
if err != nil { if err != nil {
return err return err
} }
@@ -316,6 +316,7 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
// Upload a file if it exists // Upload a file if it exists
if msg.Extra != nil { if msg.Extra != nil {
for _, rmsg := range helper.HandleExtra(msg, b.General) { for _, rmsg := range helper.HandleExtra(msg, b.General) {
// TODO: Use ClipOrSplitMessage
rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped")) rmsg.Text = helper.ClipMessage(rmsg.Text, MessageLength, b.GetString("MessageClipped"))
if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil { if _, err := b.c.ChannelMessageSend(channelID, rmsg.Username+rmsg.Text); err != nil {
b.Log.Errorf("Could not send message %#v: %s", rmsg, err) b.Log.Errorf("Could not send message %#v: %s", rmsg, err)
@@ -327,35 +328,53 @@ func (b *Bdiscord) handleEventBotUser(msg *config.Message, channelID string) (st
} }
} }
msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
msg.Text = b.replaceUserMentions(msg.Text)
// Edit message // Edit message
if msg.ID != "" { if msg.ID != "" {
_, err := b.c.ChannelMessageEdit(channelID, msg.ID, msg.Username+msg.Text) // Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
return msg.ID, err msgIds := strings.Split(msg.ID, ";")
} msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
for len(msgParts) < len(msgIds) {
m := discordgo.MessageSend{ msgParts = append(msgParts, "((obsoleted by edit))")
Content: msg.Username + msg.Text,
AllowedMentions: b.getAllowedMentions(),
}
if msg.ParentValid() {
m.Reference = &discordgo.MessageReference{
MessageID: msg.ParentID,
ChannelID: channelID,
GuildID: b.guildID,
} }
for i := range msgParts {
// In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
// TODO: Optimize away noop-updates of un-edited messages
// TODO: Use RemoteNickFormat instead of this broken concatenation
_, err := b.c.ChannelMessageEdit(channelID, msgIds[i], msg.Username+msgParts[i])
if err != nil {
return "", err
}
}
return msg.ID, nil
} }
// Post normal message msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount"))
res, err := b.c.ChannelMessageSendComplex(channelID, &m) msgIds := []string{}
if err != nil {
return "", err for _, msgPart := range msgParts {
m := discordgo.MessageSend{
Content: msg.Username + msgPart,
AllowedMentions: b.getAllowedMentions(),
}
if msg.ParentValid() {
m.Reference = &discordgo.MessageReference{
MessageID: msg.ParentID,
ChannelID: channelID,
GuildID: b.guildID,
}
}
// Post normal message
res, err := b.c.ChannelMessageSendComplex(channelID, &m)
if err != nil {
return "", err
}
msgIds = append(msgIds, res.ID)
} }
return res.ID, nil // Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";".
return strings.Join(msgIds, ";"), nil
} }
// handleUploadFile handles native upload of files // handleUploadFile handles native upload of files

View File

@@ -68,7 +68,7 @@ func (b *Bdiscord) getGuildMemberByNick(nick string) (*discordgo.Member, error)
b.membersMutex.RLock() b.membersMutex.RLock()
defer b.membersMutex.RUnlock() defer b.membersMutex.RUnlock()
if member, ok := b.nickMemberMap[nick]; ok { if member, ok := b.nickMemberMap[strings.TrimSpace(nick)]; ok {
return member, nil return member, nil
} }
return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller return nil, errors.New("Couldn't find guild member with nick " + nick) // This will most likely get ignored by the caller

View File

@@ -2,6 +2,7 @@ package bdiscord
import ( import (
"bytes" "bytes"
"strings"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
@@ -42,14 +43,66 @@ func (b *Bdiscord) maybeGetLocalAvatar(msg *config.Message) string {
return "" return ""
} }
func (b *Bdiscord) webhookSendTextOnly(msg *config.Message, channelID string) (string, error) {
msgParts := helper.ClipOrSplitMessage(msg.Text, MessageLength, b.GetString("MessageClipped"), b.GetInt("MessageSplitMaxCount"))
msgIds := []string{}
for _, msgPart := range msgParts {
res, err := b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Content: msgPart,
Username: msg.Username,
AvatarURL: msg.Avatar,
AllowedMentions: b.getAllowedMentions(),
},
)
if err != nil {
return "", err
} else {
msgIds = append(msgIds, res.ID)
}
}
// Exploit that a discord message ID is actually just a large number, so we encode a list of IDs by separating them with ";".
return strings.Join(msgIds, ";"), nil
}
func (b *Bdiscord) webhookSendFilesOnly(msg *config.Message, channelID string) error {
for _, f := range msg.Extra["file"] {
fi := f.(config.FileInfo) //nolint:forcetypeassert
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
content := fi.Comment
// Cannot use the resulting ID for any edits anyway, so throw it away.
// This has to be re-enabled when we implement message deletion.
_, err := b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Username: msg.Username,
AvatarURL: msg.Avatar,
Files: []*discordgo.File{&file},
Content: content,
AllowedMentions: b.getAllowedMentions(),
},
)
if err != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, err)
return err
}
}
return nil
}
// webhookSend send one or more message via webhook, taking care of file // webhookSend send one or more message via webhook, taking care of file
// uploads (from slack, telegram or mattermost). // uploads (from slack, telegram or mattermost).
// Returns messageID and error. // Returns messageID and error.
func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordgo.Message, error) { func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (string, error) {
var ( var (
res *discordgo.Message res string
res2 *discordgo.Message err error
err error
) )
// If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this) // If avatar is unset, mutate the message to include the local avatar (but only if settings say we should do this)
@@ -61,48 +114,11 @@ func (b *Bdiscord) webhookSend(msg *config.Message, channelID string) (*discordg
// We can't send empty messages. // We can't send empty messages.
if msg.Text != "" { if msg.Text != "" {
res, err = b.transmitter.Send( res, err = b.webhookSendTextOnly(msg, channelID)
channelID,
&discordgo.WebhookParams{
Content: msg.Text,
Username: msg.Username,
AvatarURL: msg.Avatar,
AllowedMentions: b.getAllowedMentions(),
},
)
if err != nil {
b.Log.Errorf("Could not send text (%s) for message %#v: %s", msg.Text, msg, err)
}
} }
if msg.Extra != nil { if err == nil && msg.Extra != nil {
for _, f := range msg.Extra["file"] { err = b.webhookSendFilesOnly(msg, channelID)
fi := f.(config.FileInfo)
file := discordgo.File{
Name: fi.Name,
ContentType: "",
Reader: bytes.NewReader(*fi.Data),
}
content := fi.Comment
res2, err = b.transmitter.Send(
channelID,
&discordgo.WebhookParams{
Username: msg.Username,
AvatarURL: msg.Avatar,
Files: []*discordgo.File{&file},
Content: content,
AllowedMentions: b.getAllowedMentions(),
},
)
if err != nil {
b.Log.Errorf("Could not send file %#v for message %#v: %s", file, msg, err)
}
}
}
if msg.Text == "" {
res = res2
} }
return res, err return res, err
@@ -120,35 +136,44 @@ func (b *Bdiscord) handleEventWebhook(msg *config.Message, channelID string) (st
return "", nil return "", nil
} }
msg.Text = helper.ClipMessage(msg.Text, MessageLength, b.GetString("MessageClipped"))
msg.Text = b.replaceUserMentions(msg.Text)
// discord username must be [0..32] max // discord username must be [0..32] max
if len(msg.Username) > 32 { if len(msg.Username) > 32 {
msg.Username = msg.Username[0:32] msg.Username = msg.Username[0:32]
} }
if msg.ID != "" { if msg.ID != "" {
// Exploit that a discord message ID is actually just a large number, and we encode a list of IDs by separating them with ";".
msgIds := strings.Split(msg.ID, ";")
msgParts := helper.ClipOrSplitMessage(b.replaceUserMentions(msg.Text), MessageLength, b.GetString("MessageClipped"), len(msgIds))
for len(msgParts) < len(msgIds) {
msgParts = append(msgParts, "((obsoleted by edit))")
}
b.Log.Debugf("Editing webhook message") b.Log.Debugf("Editing webhook message")
err := b.transmitter.Edit(channelID, msg.ID, &discordgo.WebhookParams{ var editErr error = nil
Content: msg.Text, for i := range msgParts {
Username: msg.Username, // In case of split-messages where some parts remain the same (i.e. only a typo-fix in a huge message), this causes some noop-updates.
AllowedMentions: b.getAllowedMentions(), // TODO: Optimize away noop-updates of un-edited messages
}) editErr = b.transmitter.Edit(channelID, msgIds[i], &discordgo.WebhookParams{
if err == nil { Content: msgParts[i],
Username: msg.Username,
AllowedMentions: b.getAllowedMentions(),
})
if editErr != nil {
break
}
}
if editErr == nil {
return msg.ID, nil return msg.ID, nil
} }
b.Log.Errorf("Could not edit webhook message: %s", err) b.Log.Errorf("Could not edit webhook message(s): %s; sending as new message(s) instead", editErr)
} }
b.Log.Debugf("Processing webhook sending for message %#v", msg) b.Log.Debugf("Processing webhook sending for message %#v", msg)
discordMsg, err := b.webhookSend(msg, channelID) msg.Text = b.replaceUserMentions(msg.Text)
msgID, err := b.webhookSend(msg, channelID)
if err != nil { if err != nil {
b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msg, err) b.Log.Errorf("Could not broadcast via webhook for message %#v: %s", msgID, err)
return "", err return "", err
} }
if discordMsg == nil { return msgID, nil
return "", nil
}
return discordMsg.ID, nil
} }

View File

@@ -211,14 +211,51 @@ func ClipMessage(text string, length int, clippingMessage string) string {
if len(text) > length { if len(text) > length {
text = text[:length-len(clippingMessage)] text = text[:length-len(clippingMessage)]
if r, size := utf8.DecodeLastRuneInString(text); r == utf8.RuneError { for len(text) > 0 {
text = text[:len(text)-size] if r, _ := utf8.DecodeLastRuneInString(text); r == utf8.RuneError {
text = text[:len(text)-1]
// Note: DecodeLastRuneInString only returns the constant value "1" in
// case of an error. We do not yet know whether the last rune is now
// actually valid. Example: "€" is 0xE2 0x82 0xAC. If we happen to split
// the string just before 0xAC, and go back only one byte, that would
// leave us with a string that ends in the byte 0xE2, which is not a valid
// rune, so we need to try again.
} else {
break
}
} }
text += clippingMessage text += clippingMessage
} }
return text return text
} }
func ClipOrSplitMessage(text string, length int, clippingMessage string, splitMax int) []string {
var msgParts []string
remainingText := text
// Invariant of this splitting loop: No text is lost (msgParts+remainingText is the original text),
// and all parts is guaranteed to satisfy the length requirement.
for len(msgParts) < splitMax-1 && len(remainingText) > length {
// Decision: The text needs to be split (again).
var chunk string
wasted := 0
// The longest UTF-8 encoding of a valid rune is 4 bytes (0xF4 0x8F 0xBF 0xBF, encoding U+10FFFF),
// so we should never need to waste 4 or more bytes at a time.
for wasted < 4 && wasted < length {
chunk = remainingText[:length-wasted]
if r, _ := utf8.DecodeLastRuneInString(chunk); r == utf8.RuneError {
wasted += 1
} else {
break
}
}
// Note: At this point, "chunk" might still be invalid, if "text" is very broken.
msgParts = append(msgParts, chunk)
remainingText = remainingText[len(chunk):]
}
msgParts = append(msgParts, ClipMessage(remainingText, length, clippingMessage))
return msgParts
}
// ParseMarkdown takes in an input string as markdown and parses it to html // ParseMarkdown takes in an input string as markdown and parses it to html
func ParseMarkdown(input string) string { func ParseMarkdown(input string) string {
extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode extensions := parser.HardLineBreak | parser.NoIntraEmphasis | parser.FencedCode

View File

@@ -88,6 +88,15 @@ var lineSplittingTestCases = map[string]struct {
}, },
nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"}, nonSplitOutput: []string{"不布人個我此而及單石業喜資富下我河下日沒一我臺空達的常景便物沒為……子大我別名解成?生賣的全直黑,我自我結毛分洲了世當,是政福那是東;斯說"},
}, },
"Long message, clip three-byte rune after two bytes": {
input: "x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。",
splitOutput: []string{
"x 人人生而自由,在尊嚴和權利上 <clipped message>",
"一律平等。 他們都具有理性和良知 <clipped message>",
",應該以兄弟情誼的精神對待彼此。",
},
nonSplitOutput: []string{"x 人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。"},
},
} }
func TestGetSubLines(t *testing.T) { func TestGetSubLines(t *testing.T) {
@@ -125,3 +134,105 @@ func TestConvertWebPToPNG(t *testing.T) {
t.Fail() t.Fail()
} }
} }
var clippingOrSplittingTestCases = map[string]struct {
inputText string
clipSplitLength int
clippingMessage string
splitMax int
expectedOutput []string
}{
"Short single-line message, split 3": {
inputText: "short",
clipSplitLength: 20,
clippingMessage: "?!?!",
splitMax: 3,
expectedOutput: []string{"short"},
},
"Short single-line message, split 1": {
inputText: "short",
clipSplitLength: 20,
clippingMessage: "?!?!",
splitMax: 1,
expectedOutput: []string{"short"},
},
"Short single-line message, split 0": {
// Mainly check that we don't crash.
inputText: "short",
clipSplitLength: 20,
clippingMessage: "?!?!",
splitMax: 0,
expectedOutput: []string{"short"},
},
"Long single-line message, noclip": {
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
clipSplitLength: 50,
clippingMessage: "?!?!",
splitMax: 10,
expectedOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing",
" elit, sed do eiusmod tempor incididunt ut labore ",
"et dolore magna aliqua.",
},
},
"Long single-line message, noclip tight": {
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
clipSplitLength: 50,
clippingMessage: "?!?!",
splitMax: 3,
expectedOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing",
" elit, sed do eiusmod tempor incididunt ut labore ",
"et dolore magna aliqua.",
},
},
"Long single-line message, clip custom": {
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
clipSplitLength: 50,
clippingMessage: "?!?!",
splitMax: 2,
expectedOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing",
" elit, sed do eiusmod tempor incididunt ut lab?!?!",
},
},
"Long single-line message, clip built-in": {
inputText: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
clipSplitLength: 50,
clippingMessage: "",
splitMax: 2,
expectedOutput: []string{
"Lorem ipsum dolor sit amet, consectetur adipiscing",
" elit, sed do eiusmod tempor inc <clipped message>",
},
},
"Short multi-line message": {
inputText: "I\ncan't\nget\nno\nsatisfaction!",
clipSplitLength: 50,
clippingMessage: "",
splitMax: 2,
expectedOutput: []string{"I\ncan't\nget\nno\nsatisfaction!"},
},
"Long message containing UTF-8 multi-byte runes": {
inputText: "人人生而自由,在尊嚴和權利上一律平等。 他們都具有理性和良知,應該以兄弟情誼的精神對待彼此。",
clipSplitLength: 50,
clippingMessage: "",
splitMax: 10,
expectedOutput: []string{
"人人生而自由,在尊嚴和權利上一律", // Note: only 48 bytes!
"平等。 他們都具有理性和良知,應該", // Note: only 49 bytes!
"以兄弟情誼的精神對待彼此。",
},
},
}
func TestClipOrSplitMessage(t *testing.T) {
for testname, testcase := range clippingOrSplittingTestCases {
actualOutput := ClipOrSplitMessage(testcase.inputText, testcase.clipSplitLength, testcase.clippingMessage, testcase.splitMax)
assert.Equalf(t, testcase.expectedOutput, actualOutput, "'%s' testcase should give expected lines with clipping+splitting.", testname)
for _, splitLine := range testcase.expectedOutput {
byteLength := len([]byte(splitLine))
assert.True(t, byteLength <= testcase.clipSplitLength, "Splitted line '%s' of testcase '%s' should not exceed the maximum byte-length (%d vs. %d).", splitLine, testname, testcase.clipSplitLength, byteLength)
}
}
}

View File

@@ -122,8 +122,18 @@ func (b *Birc) handleNewConnection(client *girc.Client, event girc.Event) {
i := b.i i := b.i
b.Nick = event.Params[0] b.Nick = event.Params[0]
b.Log.Debug("Clearing handlers before adding in case of BNC reconnect")
i.Handlers.Clear("PRIVMSG")
i.Handlers.Clear("CTCP_ACTION")
i.Handlers.Clear(girc.RPL_TOPICWHOTIME)
i.Handlers.Clear(girc.NOTICE)
i.Handlers.Clear("JOIN")
i.Handlers.Clear("PART")
i.Handlers.Clear("QUIT")
i.Handlers.Clear("KICK")
i.Handlers.Clear("INVITE")
i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg) i.Handlers.AddBg("PRIVMSG", b.handlePrivMsg)
i.Handlers.AddBg("CTCP_ACTION", b.handlePrivMsg)
i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime) i.Handlers.Add(girc.RPL_TOPICWHOTIME, b.handleTopicWhoTime)
i.Handlers.AddBg(girc.NOTICE, b.handleNotice) i.Handlers.AddBg(girc.NOTICE, b.handleNotice)
i.Handlers.AddBg("JOIN", b.handleJoinPart) i.Handlers.AddBg("JOIN", b.handleJoinPart)
@@ -195,7 +205,11 @@ func (b *Birc) handlePrivMsg(client *girc.Client, event girc.Event) {
b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event) b.Log.Debugf("== Receiving PRIVMSG: %s %s %#v", event.Source.Name, event.Last(), event)
// set action event // set action event
if event.IsAction() { if ok, ctcp := event.IsCTCP(); ok {
if ctcp.Command != girc.CTCP_ACTION {
b.Log.Debugf("dropping user ctcp, command: %s", ctcp.Command)
return
}
rmsg.Event = config.EventUserAction rmsg.Event = config.EventUserAction
} }

View File

@@ -1,10 +1,12 @@
package bmattermost package bmattermost
import ( import (
"context"
"github.com/42wim/matterbridge/bridge/config" "github.com/42wim/matterbridge/bridge/config"
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/matterbridge/matterclient" "github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost/server/public/model"
) )
// handleDownloadAvatar downloads the avatar of userid from channel // handleDownloadAvatar downloads the avatar of userid from channel
@@ -25,7 +27,7 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
data []byte data []byte
err error err error
) )
data, _, err = b.mc.Client.GetProfileImage(userid, "") data, _, err = b.mc.Client.GetProfileImage(context.TODO(), userid, "")
if err != nil { if err != nil {
b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err) b.Log.Errorf("ProfileImage download failed for %#v %s", userid, err)
return return
@@ -43,8 +45,8 @@ func (b *Bmattermost) handleDownloadAvatar(userid string, channel string) {
//nolint:wrapcheck //nolint:wrapcheck
func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error { func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error {
url, _, _ := b.mc.Client.GetFileLink(id) url, _, _ := b.mc.Client.GetFileLink(context.TODO(), id)
finfo, _, err := b.mc.Client.GetFileInfo(id) finfo, _, err := b.mc.Client.GetFileInfo(context.TODO(), id)
if err != nil { if err != nil {
return err return err
} }
@@ -52,7 +54,7 @@ func (b *Bmattermost) handleDownloadFile(rmsg *config.Message, id string) error
if err != nil { if err != nil {
return err return err
} }
data, _, err := b.mc.Client.DownloadFile(id, true) data, _, err := b.mc.Client.DownloadFile(context.TODO(), id, true)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -8,7 +8,7 @@ import (
"github.com/42wim/matterbridge/bridge/helper" "github.com/42wim/matterbridge/bridge/helper"
"github.com/42wim/matterbridge/matterhook" "github.com/42wim/matterbridge/matterhook"
"github.com/matterbridge/matterclient" "github.com/matterbridge/matterclient"
"github.com/mattermost/mattermost-server/v6/model" "github.com/mattermost/mattermost/server/public/model"
) )
func (b *Bmattermost) doConnectWebhookBind() error { func (b *Bmattermost) doConnectWebhookBind() error {
@@ -171,12 +171,23 @@ func (b *Bmattermost) sendWebhook(msg config.Message) (string, error) {
} }
// skipMessages returns true if this message should not be handled // skipMessages returns true if this message should not be handled
//
//nolint:gocyclo,cyclop //nolint:gocyclo,cyclop
func (b *Bmattermost) skipMessage(message *matterclient.Message) bool { func (b *Bmattermost) skipMessage(message *matterclient.Message) bool {
// Handle join/leave // Handle join/leave
if message.Type == "system_join_leave" || skipJoinMessageTypes := map[string]struct{}{
message.Type == "system_join_channel" || "system_join_leave": {}, // deprecated for system_add_to_channel
message.Type == "system_leave_channel" { "system_leave_channel": {}, // deprecated for system_remove_from_channel
"system_join_channel": {},
"system_add_to_channel": {},
"system_remove_from_channel": {},
"system_add_to_team": {},
"system_remove_from_team": {},
}
// dirty hack to efficiently check if this element is in the map without writing a contains func
// can be replaced with native slice.contains with go 1.21
if _, ok := skipJoinMessageTypes[message.Type]; ok {
if b.GetBool("nosendjoinpart") { if b.GetBool("nosendjoinpart") {
return true return true
} }

View File

@@ -1,6 +1,7 @@
package bmattermost package bmattermost
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@@ -157,7 +158,7 @@ func (b *Bmattermost) Send(msg config.Message) (string, error) {
// we only can reply to the root of the thread, not to a specific ID (like discord for example does) // we only can reply to the root of the thread, not to a specific ID (like discord for example does)
if msg.ParentID != "" { if msg.ParentID != "" {
post, _, err := b.mc.Client.GetPost(msg.ParentID, "") post, _, err := b.mc.Client.GetPost(context.TODO(), msg.ParentID, "")
if err != nil { if err != nil {
b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err) b.Log.Errorf("getting post %s failed: %s", msg.ParentID, err)
} }

View File

@@ -135,6 +135,7 @@ func (b *Brocketchat) uploadFile(fi *config.FileInfo, channel string) error {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := ioutil.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return err

View File

@@ -101,7 +101,9 @@ func (b *Bslack) populateMessageWithBotInfo(ev *slack.MessageEvent, rmsg *config
var err error var err error
var bot *slack.Bot var bot *slack.Bot
for { for {
bot, err = b.rtm.GetBotInfo(ev.BotID) bot, err = b.rtm.GetBotInfo(slack.GetBotInfoParameters{
Bot: ev.BotID,
})
if err == nil { if err == nil {
break break
} }

View File

@@ -136,7 +136,7 @@ func isGroupJid(identifier string) bool {
func (b *Bwhatsapp) getDevice() (*store.Device, error) { func (b *Bwhatsapp) getDevice() (*store.Device, error) {
device := &store.Device{} device := &store.Device{}
storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_foreign_keys=on&_pragma=busy_timeout=10000", nil) storeContainer, err := sqlstore.New("sqlite", "file:"+b.Config.GetString("sessionfile")+".db?_pragma=foreign_keys(1)&_pragma=busy_timeout=10000", nil)
if err != nil { if err != nil {
return device, fmt.Errorf("failed to connect to database: %v", err) return device, fmt.Errorf("failed to connect to database: %v", err)
} }
@@ -151,7 +151,6 @@ func (b *Bwhatsapp) getDevice() (*store.Device, error) {
func (b *Bwhatsapp) getNewReplyContext(parentID string) (*proto.ContextInfo, error) { func (b *Bwhatsapp) getNewReplyContext(parentID string) (*proto.ContextInfo, error) {
replyInfo, err := b.parseMessageID(parentID) replyInfo, err := b.parseMessageID(parentID)
if err != nil { if err != nil {
return nil, err return nil, err
} }

154
go.mod
View File

@@ -5,29 +5,29 @@ require (
github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f github.com/Benau/tgsconverter v0.0.0-20210809170556-99f4a4f6337f
github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560 github.com/Philipp15b/go-steam v1.0.1-0.20200727090957-6ae9b3c0a560
github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c github.com/Rhymen/go-whatsapp v0.1.2-0.20211102134409-31a2e740845c
github.com/SevereCloud/vksdk/v2 v2.16.0 github.com/SevereCloud/vksdk/v2 v2.16.1
github.com/bwmarrin/discordgo v0.27.1 github.com/bwmarrin/discordgo v0.28.1
github.com/d5/tengo/v2 v2.16.1 github.com/d5/tengo/v2 v2.17.0
github.com/davecgh/go-spew v1.1.1 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
github.com/fsnotify/fsnotify v1.6.0 github.com/fsnotify/fsnotify v1.7.0
github.com/gomarkdown/markdown v0.0.0-20230716120725-531d2d74bc12 github.com/gomarkdown/markdown v0.0.0-20240419095408-642f0ee99ae2
github.com/google/gops v0.3.27 github.com/google/gops v0.3.27
github.com/gorilla/schema v1.2.0 github.com/gorilla/schema v1.3.0
github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa github.com/harmony-development/shibshib v0.0.0-20220101224523-c98059d09cfa
github.com/hashicorp/golang-lru v0.6.0 github.com/hashicorp/golang-lru v1.0.2
github.com/jpillora/backoff v1.0.0 github.com/jpillora/backoff v1.0.0
github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20 github.com/keybase/go-keybase-chat-bot v0.0.0-20221220212439-e48d9abd2c20
github.com/kyokomi/emoji/v2 v2.2.12 github.com/kyokomi/emoji/v2 v2.2.13
github.com/labstack/echo/v4 v4.11.1 github.com/labstack/echo/v4 v4.12.0
github.com/lrstanley/girc v0.0.0-20230729130341-dd5853a5f1a6 github.com/lrstanley/girc v0.0.0-20240519163535-a518c5b87a79
github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696 github.com/matterbridge/Rocket.Chat.Go.SDK v0.0.0-20211016222428-79310a412696
github.com/matterbridge/go-xmpp v0.0.0-20211030125215-791a06c5f1be github.com/matterbridge/go-xmpp v0.0.0-20240523230155-7154bfeb76e8
github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27 github.com/matterbridge/gomatrix v0.0.0-20220411225302-271e5088ea27
github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75 github.com/matterbridge/gozulipbot v0.0.0-20211023205727-a19d6c1f3b75
github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba github.com/matterbridge/logrus-prefixed-formatter v0.5.3-0.20200523233437-d971309a77ba
github.com/matterbridge/matterclient v0.0.0-20230329213635-bc6e42a4a84a github.com/matterbridge/matterclient v0.0.0-20240523235056-57f299489168
github.com/matterbridge/telegram-bot-api/v6 v6.5.0 github.com/matterbridge/telegram-bot-api/v6 v6.5.0
github.com/mattermost/mattermost-server/v6 v6.7.2 github.com/mattermost/mattermost/server/public v0.1.3
github.com/mattn/godown v0.0.1 github.com/mattn/godown v0.0.1
github.com/mdp/qrterminal v1.0.1 github.com/mdp/qrterminal v1.0.1
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
@@ -39,21 +39,21 @@ require (
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d
github.com/shazow/ssh-chat v1.10.1 github.com/shazow/ssh-chat v1.10.1
github.com/sirupsen/logrus v1.9.3 github.com/sirupsen/logrus v1.9.3
github.com/slack-go/slack v0.12.2 github.com/slack-go/slack v0.13.0
github.com/spf13/viper v1.16.0 github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.8.4 github.com/stretchr/testify v1.9.0
github.com/vincent-petithory/dataurl v1.0.0 github.com/vincent-petithory/dataurl v1.0.0
github.com/writeas/go-strip-markdown v2.0.1+incompatible github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/yaegashi/msgraph.go v0.1.4 github.com/yaegashi/msgraph.go v0.1.4
github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289 github.com/zfjagann/golang-ring v0.0.0-20220330170733-19bcea1b6289
go.mau.fi/whatsmeow v0.0.0-20230805111647-405414b9b5c0 go.mau.fi/whatsmeow v0.0.0-20240520180327-81f8f07f1dfb
golang.org/x/image v0.11.0 golang.org/x/image v0.16.0
golang.org/x/oauth2 v0.11.0 golang.org/x/oauth2 v0.20.0
golang.org/x/text v0.12.0 golang.org/x/text v0.15.0
gomod.garykim.dev/nc-talk v0.3.0 gomod.garykim.dev/nc-talk v0.3.0
google.golang.org/protobuf v1.31.0 google.golang.org/protobuf v1.33.0
layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14 layeh.com/gumble v0.0.0-20221205141517-d1df60a3cc14
modernc.org/sqlite v1.25.0 modernc.org/sqlite v1.29.10
) )
require ( require (
@@ -62,90 +62,92 @@ require (
github.com/Jeffail/gabs v1.4.0 // indirect github.com/Jeffail/gabs v1.4.0 // indirect
github.com/apex/log v1.9.0 // indirect github.com/apex/log v1.9.0 // indirect
github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf // indirect github.com/av-elier/go-decimal-to-rational v0.0.0-20191127152832-89e6aad02ecf // indirect
github.com/blang/semver v3.5.1+incompatible // indirect github.com/blang/semver/v4 v4.0.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dyatlov/go-opengraph v0.0.0-20210112100619-dae8665a5b09 // indirect github.com/dyatlov/go-opengraph/opengraph v0.0.0-20220524092352-606d7b1e5f8a // indirect
github.com/fatih/color v1.16.0 // indirect
github.com/francoispqt/gojay v1.2.13 // indirect github.com/francoispqt/gojay v1.2.13 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.3 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/golang/protobuf v1.5.4 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gopackage/ddp v0.0.3 // indirect github.com/gopackage/ddp v0.0.3 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.1 // indirect
github.com/graph-gophers/graphql-go v1.3.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-hclog v1.6.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/hashicorp/yamux v0.1.1 // indirect
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect github.com/kettek/apng v0.0.0-20191108220231-414630eed80f // indirect
github.com/klauspost/compress v1.16.0 // indirect github.com/klauspost/compress v1.17.7 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/labstack/gommon v0.4.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect github.com/mattermost/go-i18n v1.11.1-0.20211013152124-5c415071e404 // indirect
github.com/mattermost/ldap v0.0.0-20201202150706-ee0e6284187d // indirect github.com/mattermost/ldap v0.0.0-20231116144001-0f480c025956 // indirect
github.com/mattermost/logr/v2 v2.0.15 // indirect github.com/mattermost/logr/v2 v2.0.21 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-runewidth v0.0.13 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/minio/minio-go/v7 v7.0.24 // indirect
github.com/minio/sha256-simd v1.0.0 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monaco-io/request v1.0.5 // indirect github.com/monaco-io/request v1.0.5 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/pborman/uuid v1.2.1 // indirect github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml v1.9.5 // indirect
github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/philhofer/fwd v1.1.1 // indirect github.com/philhofer/fwd v1.1.2 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rickb777/date v1.12.4 // indirect github.com/rickb777/date v1.12.4 // indirect
github.com/rickb777/plural v1.2.0 // indirect github.com/rickb777/plural v1.2.0 // indirect
github.com/rivo/uniseg v0.2.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect
github.com/rs/zerolog v1.32.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect github.com/shazow/rateio v0.0.0-20200113175441-4461efc8bdc4 // indirect
github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect github.com/sizeofint/webpanimation v0.0.0-20210809145948-1d2b32119882 // indirect
github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 // indirect github.com/skip2/go-qrcode v0.0.0-20190110000554-dc11ecdae0a9 // indirect
github.com/spf13/afero v1.9.5 // indirect github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/cast v1.5.1 // indirect github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/cast v1.6.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.4.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect
github.com/tinylib/msgp v1.1.6 // indirect github.com/tinylib/msgp v1.1.9 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
github.com/wiggin77/merror v1.0.3 // indirect github.com/wiggin77/merror v1.0.5 // indirect
github.com/wiggin77/srslog v1.0.1 // indirect github.com/wiggin77/srslog v1.0.1 // indirect
go.mau.fi/libsignal v0.1.0 // indirect go.mau.fi/libsignal v0.1.0 // indirect
golang.org/x/crypto v0.12.0 // indirect go.mau.fi/util v0.4.1 // indirect
golang.org/x/mod v0.8.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/net v0.14.0 // indirect golang.org/x/crypto v0.23.0 // indirect
golang.org/x/sys v0.11.0 // indirect golang.org/x/exp v0.0.0-20240314144324-c7f7c6466f7f // indirect
golang.org/x/term v0.11.0 // indirect golang.org/x/net v0.25.0 // indirect
golang.org/x/time v0.3.0 // indirect golang.org/x/sys v0.20.0 // indirect
golang.org/x/tools v0.6.0 // indirect golang.org/x/term v0.20.0 // indirect
google.golang.org/appengine v1.6.7 // indirect golang.org/x/time v0.5.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect
google.golang.org/grpc v1.62.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
lukechampine.com/uint128 v1.2.0 // indirect modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/cc/v3 v3.40.0 // indirect modernc.org/libc v1.49.3 // indirect
modernc.org/ccgo/v3 v3.16.13 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/libc v1.24.1 // indirect modernc.org/memory v1.8.0 // indirect
modernc.org/mathutil v1.5.0 // indirect modernc.org/strutil v1.2.0 // indirect
modernc.org/memory v1.6.0 // indirect modernc.org/token v1.1.0 // indirect
modernc.org/opt v0.1.3 // indirect
modernc.org/strutil v1.1.3 // indirect
modernc.org/token v1.0.1 // indirect
rsc.io/qr v0.2.0 // indirect rsc.io/qr v0.2.0 // indirect
) )
//replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419 //replace github.com/matrix-org/gomatrix => github.com/matterbridge/gomatrix v0.0.0-20220205235239-607eb9ee6419
go 1.19 go 1.22.0

2235
go.sum

File diff suppressed because it is too large Load Diff

View File

@@ -163,7 +163,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -301,7 +301,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#Nicks you want to replace. #Nicks you want to replace.
#See ReplaceMessages for syntaxA #See ReplaceMessages for syntax
#OPTIONAL (default empty) #OPTIONAL (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -468,7 +468,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -586,7 +586,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -725,7 +725,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -925,10 +925,17 @@ ShowTopicChange=false
# Supported from the following bridges: slack # Supported from the following bridges: slack
SyncTopic=false SyncTopic=false
#Message to show when a message is too big # Message to show when a message is too big
#Default "<clipped message>" # Default "<clipped message>"
MessageClipped="<clipped message>" MessageClipped="<clipped message>"
# Before clipping, try to split messages into at most this many parts. 0 is treated like 1.
# Be careful with large numbers, as this might cause flooding.
# Example: A maximum telegram message of 4096 bytes is received. This requires 3 Discord
# messages (each capped at a hardcoded 1950 bytes).
# Default 1
MessageSplitMaxCount=3
################################################################### ###################################################################
#telegram section #telegram section
################################################################### ###################################################################
@@ -1033,7 +1040,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -1174,7 +1181,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -1286,7 +1293,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -1388,7 +1395,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]
@@ -1597,7 +1604,7 @@ IgnoreMessages="^~~ badword"
ReplaceMessages=[ ["cat","dog"] ] ReplaceMessages=[ ["cat","dog"] ]
#nicks you want to replace. #nicks you want to replace.
#see replacemessages for syntaxa #see replacemessages for syntax
#optional (default empty) #optional (default empty)
ReplaceNicks=[ ["user--","user"] ] ReplaceNicks=[ ["user--","user"] ]

View File

@@ -7,6 +7,6 @@ package vksdk
// Module constants. // Module constants.
const ( const (
Version = "2.16.0" Version = "2.16.1"
API = "5.131" API = "5.131"
) )

View File

@@ -36,5 +36,5 @@ type BoardTopicPoll struct {
OwnerID int `json:"owner_id"` // Poll owner's ID OwnerID int `json:"owner_id"` // Poll owner's ID
PollID int `json:"poll_id"` // Poll ID PollID int `json:"poll_id"` // Poll ID
Question string `json:"question"` // Poll question Question string `json:"question"` // Poll question
Votes string `json:"votes"` // Votes number Votes int `json:"votes"` // Votes number
} }

View File

@@ -285,8 +285,8 @@ type BaseLinkProduct struct {
// BaseLinkRating struct. // BaseLinkRating struct.
type BaseLinkRating struct { type BaseLinkRating struct {
ReviewsCount int `json:"reviews_count"` ReviewsCount json.Number `json:"reviews_count"`
Stars float64 `json:"stars"` Stars float64 `json:"stars"`
} }
// BasePlace struct. // BasePlace struct.

View File

@@ -1,21 +0,0 @@
language: go
matrix:
include:
- go: 1.4.3
- go: 1.5.4
- go: 1.6.3
- go: 1.7
- go: tip
allow_failures:
- go: tip
install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/mattn/goveralls
script:
- echo "Test and track coverage" ; $HOME/gopath/bin/goveralls -package "." -service=travis-ci
-repotoken $COVERALLS_TOKEN
- echo "Build examples" ; cd examples && go build
- echo "Check if gofmt'd" ; diff -u <(echo -n) <(gofmt -d -s .)
env:
global:
secure: HroGEAUQpVq9zX1b1VIkraLiywhGbzvNnTZq2TMxgK7JHP8xqNplAeF1izrR2i4QLL9nsY+9WtYss4QuPvEtZcVHUobw6XnL6radF7jS1LgfYZ9Y7oF+zogZ2I5QUMRLGA7rcxQ05s7mKq3XZQfeqaNts4bms/eZRefWuaFZbkw=

View File

@@ -1,194 +0,0 @@
semver for golang [![Build Status](https://travis-ci.org/blang/semver.svg?branch=master)](https://travis-ci.org/blang/semver) [![GoDoc](https://godoc.org/github.com/blang/semver?status.png)](https://godoc.org/github.com/blang/semver) [![Coverage Status](https://img.shields.io/coveralls/blang/semver.svg)](https://coveralls.io/r/blang/semver?branch=master)
======
semver is a [Semantic Versioning](http://semver.org/) library written in golang. It fully covers spec version `2.0.0`.
Usage
-----
```bash
$ go get github.com/blang/semver
```
Note: Always vendor your dependencies or fix on a specific version tag.
```go
import github.com/blang/semver
v1, err := semver.Make("1.0.0-beta")
v2, err := semver.Make("2.0.0-beta")
v1.Compare(v2)
```
Also check the [GoDocs](http://godoc.org/github.com/blang/semver).
Why should I use this lib?
-----
- Fully spec compatible
- No reflection
- No regex
- Fully tested (Coverage >99%)
- Readable parsing/validation errors
- Fast (See [Benchmarks](#benchmarks))
- Only Stdlib
- Uses values instead of pointers
- Many features, see below
Features
-----
- Parsing and validation at all levels
- Comparator-like comparisons
- Compare Helper Methods
- InPlace manipulation
- Ranges `>=1.0.0 <2.0.0 || >=3.0.0 !3.0.1-beta.1`
- Wildcards `>=1.x`, `<=2.5.x`
- Sortable (implements sort.Interface)
- database/sql compatible (sql.Scanner/Valuer)
- encoding/json compatible (json.Marshaler/Unmarshaler)
Ranges
------
A `Range` is a set of conditions which specify which versions satisfy the range.
A condition is composed of an operator and a version. The supported operators are:
- `<1.0.0` Less than `1.0.0`
- `<=1.0.0` Less than or equal to `1.0.0`
- `>1.0.0` Greater than `1.0.0`
- `>=1.0.0` Greater than or equal to `1.0.0`
- `1.0.0`, `=1.0.0`, `==1.0.0` Equal to `1.0.0`
- `!1.0.0`, `!=1.0.0` Not equal to `1.0.0`. Excludes version `1.0.0`.
Note that spaces between the operator and the version will be gracefully tolerated.
A `Range` can link multiple `Ranges` separated by space:
Ranges can be linked by logical AND:
- `>1.0.0 <2.0.0` would match between both ranges, so `1.1.1` and `1.8.7` but not `1.0.0` or `2.0.0`
- `>1.0.0 <3.0.0 !2.0.3-beta.2` would match every version between `1.0.0` and `3.0.0` except `2.0.3-beta.2`
Ranges can also be linked by logical OR:
- `<2.0.0 || >=3.0.0` would match `1.x.x` and `3.x.x` but not `2.x.x`
AND has a higher precedence than OR. It's not possible to use brackets.
Ranges can be combined by both AND and OR
- `>1.0.0 <2.0.0 || >3.0.0 !4.2.1` would match `1.2.3`, `1.9.9`, `3.1.1`, but not `4.2.1`, `2.1.1`
Range usage:
```
v, err := semver.Parse("1.2.3")
range, err := semver.ParseRange(">1.0.0 <2.0.0 || >=3.0.0")
if range(v) {
//valid
}
```
Example
-----
Have a look at full examples in [examples/main.go](examples/main.go)
```go
import github.com/blang/semver
v, err := semver.Make("0.0.1-alpha.preview+123.github")
fmt.Printf("Major: %d\n", v.Major)
fmt.Printf("Minor: %d\n", v.Minor)
fmt.Printf("Patch: %d\n", v.Patch)
fmt.Printf("Pre: %s\n", v.Pre)
fmt.Printf("Build: %s\n", v.Build)
// Prerelease versions array
if len(v.Pre) > 0 {
fmt.Println("Prerelease versions:")
for i, pre := range v.Pre {
fmt.Printf("%d: %q\n", i, pre)
}
}
// Build meta data array
if len(v.Build) > 0 {
fmt.Println("Build meta data:")
for i, build := range v.Build {
fmt.Printf("%d: %q\n", i, build)
}
}
v001, err := semver.Make("0.0.1")
// Compare using helpers: v.GT(v2), v.LT, v.GTE, v.LTE
v001.GT(v) == true
v.LT(v001) == true
v.GTE(v) == true
v.LTE(v) == true
// Or use v.Compare(v2) for comparisons (-1, 0, 1):
v001.Compare(v) == 1
v.Compare(v001) == -1
v.Compare(v) == 0
// Manipulate Version in place:
v.Pre[0], err = semver.NewPRVersion("beta")
if err != nil {
fmt.Printf("Error parsing pre release version: %q", err)
}
fmt.Println("\nValidate versions:")
v.Build[0] = "?"
err = v.Validate()
if err != nil {
fmt.Printf("Validation failed: %s\n", err)
}
```
Benchmarks
-----
BenchmarkParseSimple-4 5000000 390 ns/op 48 B/op 1 allocs/op
BenchmarkParseComplex-4 1000000 1813 ns/op 256 B/op 7 allocs/op
BenchmarkParseAverage-4 1000000 1171 ns/op 163 B/op 4 allocs/op
BenchmarkStringSimple-4 20000000 119 ns/op 16 B/op 1 allocs/op
BenchmarkStringLarger-4 10000000 206 ns/op 32 B/op 2 allocs/op
BenchmarkStringComplex-4 5000000 324 ns/op 80 B/op 3 allocs/op
BenchmarkStringAverage-4 5000000 273 ns/op 53 B/op 2 allocs/op
BenchmarkValidateSimple-4 200000000 9.33 ns/op 0 B/op 0 allocs/op
BenchmarkValidateComplex-4 3000000 469 ns/op 0 B/op 0 allocs/op
BenchmarkValidateAverage-4 5000000 256 ns/op 0 B/op 0 allocs/op
BenchmarkCompareSimple-4 100000000 11.8 ns/op 0 B/op 0 allocs/op
BenchmarkCompareComplex-4 50000000 30.8 ns/op 0 B/op 0 allocs/op
BenchmarkCompareAverage-4 30000000 41.5 ns/op 0 B/op 0 allocs/op
BenchmarkSort-4 3000000 419 ns/op 256 B/op 2 allocs/op
BenchmarkRangeParseSimple-4 2000000 850 ns/op 192 B/op 5 allocs/op
BenchmarkRangeParseAverage-4 1000000 1677 ns/op 400 B/op 10 allocs/op
BenchmarkRangeParseComplex-4 300000 5214 ns/op 1440 B/op 30 allocs/op
BenchmarkRangeMatchSimple-4 50000000 25.6 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchAverage-4 30000000 56.4 ns/op 0 B/op 0 allocs/op
BenchmarkRangeMatchComplex-4 10000000 153 ns/op 0 B/op 0 allocs/op
See benchmark cases at [semver_test.go](semver_test.go)
Motivation
-----
I simply couldn't find any lib supporting the full spec. Others were just wrong or used reflection and regex which i don't like.
Contribution
-----
Feel free to make a pull request. For bigger changes create a issue first to discuss about it.
License
-----
See [LICENSE](LICENSE) file.

View File

@@ -1,17 +0,0 @@
{
"author": "blang",
"bugs": {
"URL": "https://github.com/blang/semver/issues",
"url": "https://github.com/blang/semver/issues"
},
"gx": {
"dvcsimport": "github.com/blang/semver"
},
"gxVersion": "0.10.0",
"language": "go",
"license": "MIT",
"name": "semver",
"releaseCmd": "git commit -a -m \"gx publish $VERSION\"",
"version": "3.5.1"
}

View File

@@ -327,7 +327,7 @@ func expandWildcardVersion(parts [][]string) ([][]string, error) {
for _, p := range parts { for _, p := range parts {
var newParts []string var newParts []string
for _, ap := range p { for _, ap := range p {
if strings.Index(ap, "x") != -1 { if strings.Contains(ap, "x") {
opStr, vStr, err := splitComparatorVersion(ap) opStr, vStr, err := splitComparatorVersion(ap)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -26,7 +26,7 @@ type Version struct {
Minor uint64 Minor uint64
Patch uint64 Patch uint64
Pre []PRVersion Pre []PRVersion
Build []string //No Precendence Build []string //No Precedence
} }
// Version to string // Version to string
@@ -61,6 +61,18 @@ func (v Version) String() string {
return string(b) return string(b)
} }
// FinalizeVersion discards prerelease and build number and only returns
// major, minor and patch number.
func (v Version) FinalizeVersion() string {
b := make([]byte, 0, 5)
b = strconv.AppendUint(b, v.Major, 10)
b = append(b, '.')
b = strconv.AppendUint(b, v.Minor, 10)
b = append(b, '.')
b = strconv.AppendUint(b, v.Patch, 10)
return string(b)
}
// Equals checks if v is equal to o. // Equals checks if v is equal to o.
func (v Version) Equals(o Version) bool { func (v Version) Equals(o Version) bool {
return (v.Compare(o) == 0) return (v.Compare(o) == 0)
@@ -161,6 +173,27 @@ func (v Version) Compare(o Version) int {
} }
// IncrementPatch increments the patch version
func (v *Version) IncrementPatch() error {
v.Patch++
return nil
}
// IncrementMinor increments the minor version
func (v *Version) IncrementMinor() error {
v.Minor++
v.Patch = 0
return nil
}
// IncrementMajor increments the major version
func (v *Version) IncrementMajor() error {
v.Major++
v.Minor = 0
v.Patch = 0
return nil
}
// Validate validates v and returns error in case // Validate validates v and returns error in case
func (v Version) Validate() error { func (v Version) Validate() error {
// Major, Minor, Patch already validated using uint64 // Major, Minor, Patch already validated using uint64
@@ -189,10 +222,10 @@ func (v Version) Validate() error {
} }
// New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error // New is an alias for Parse and returns a pointer, parses version string and returns a validated Version or error
func New(s string) (vp *Version, err error) { func New(s string) (*Version, error) {
v, err := Parse(s) v, err := Parse(s)
vp = &v vp := &v
return return vp, err
} }
// Make is an alias for Parse, parses version string and returns a validated Version or error // Make is an alias for Parse, parses version string and returns a validated Version or error
@@ -202,14 +235,25 @@ func Make(s string) (Version, error) {
// ParseTolerant allows for certain version specifications that do not strictly adhere to semver // ParseTolerant allows for certain version specifications that do not strictly adhere to semver
// specs to be parsed by this library. It does so by normalizing versions before passing them to // specs to be parsed by this library. It does so by normalizing versions before passing them to
// Parse(). It currently trims spaces, removes a "v" prefix, and adds a 0 patch number to versions // Parse(). It currently trims spaces, removes a "v" prefix, adds a 0 patch number to versions
// with only major and minor components specified // with only major and minor components specified, and removes leading 0s.
func ParseTolerant(s string) (Version, error) { func ParseTolerant(s string) (Version, error) {
s = strings.TrimSpace(s) s = strings.TrimSpace(s)
s = strings.TrimPrefix(s, "v") s = strings.TrimPrefix(s, "v")
// Split into major.minor.(patch+pr+meta) // Split into major.minor.(patch+pr+meta)
parts := strings.SplitN(s, ".", 3) parts := strings.SplitN(s, ".", 3)
// Remove leading zeros.
for i, p := range parts {
if len(p) > 1 {
p = strings.TrimLeft(p, "0")
if len(p) == 0 || !strings.ContainsAny(p[0:1], "0123456789") {
p = "0" + p
}
parts[i] = p
}
}
// Fill up shortened versions.
if len(parts) < 3 { if len(parts) < 3 {
if strings.ContainsAny(parts[len(parts)-1], "+-") { if strings.ContainsAny(parts[len(parts)-1], "+-") {
return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data") return Version{}, errors.New("Short version cannot contain PreRelease/Build meta data")
@@ -217,8 +261,8 @@ func ParseTolerant(s string) (Version, error) {
for len(parts) < 3 { for len(parts) < 3 {
parts = append(parts, "0") parts = append(parts, "0")
} }
s = strings.Join(parts, ".")
} }
s = strings.Join(parts, ".")
return Parse(s) return Parse(s)
} }
@@ -416,3 +460,17 @@ func NewBuildVersion(s string) (string, error) {
} }
return s, nil return s, nil
} }
// FinalizeVersion returns the major, minor and patch number only and discards
// prerelease and build number.
func FinalizeVersion(s string) (string, error) {
v, err := Parse(s)
if err != nil {
return "", err
}
v.Pre = nil
v.Build = nil
finalVer := v.String()
return finalVer, nil
}

View File

@@ -14,7 +14,7 @@ func (v *Version) Scan(src interface{}) (err error) {
case []byte: case []byte:
str = string(src) str = string(src)
default: default:
return fmt.Errorf("Version.Scan: cannot convert %T to string.", src) return fmt.Errorf("version.Scan: cannot convert %T to string", src)
} }
if t, err := Parse(str); err == nil { if t, err := Parse(str); err == nil {

View File

@@ -132,10 +132,10 @@ type ComponentEmoji struct {
// Button represents button component. // Button represents button component.
type Button struct { type Button struct {
Label string `json:"label"` Label string `json:"label"`
Style ButtonStyle `json:"style"` Style ButtonStyle `json:"style"`
Disabled bool `json:"disabled"` Disabled bool `json:"disabled"`
Emoji ComponentEmoji `json:"emoji"` Emoji *ComponentEmoji `json:"emoji,omitempty"`
// NOTE: Only button with LinkButton style can have link. Also, URL is mutually exclusive with CustomID. // NOTE: Only button with LinkButton style can have link. Also, URL is mutually exclusive with CustomID.
URL string `json:"url,omitempty"` URL string `json:"url,omitempty"`
@@ -166,14 +166,32 @@ func (Button) Type() ComponentType {
// SelectMenuOption represents an option for a select menu. // SelectMenuOption represents an option for a select menu.
type SelectMenuOption struct { type SelectMenuOption struct {
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
Value string `json:"value"` Value string `json:"value"`
Description string `json:"description"` Description string `json:"description"`
Emoji ComponentEmoji `json:"emoji"` Emoji *ComponentEmoji `json:"emoji,omitempty"`
// Determines whenever option is selected by default or not. // Determines whenever option is selected by default or not.
Default bool `json:"default"` Default bool `json:"default"`
} }
// SelectMenuDefaultValueType represents the type of an entity selected by default in auto-populated select menus.
type SelectMenuDefaultValueType string
// SelectMenuDefaultValue types.
const (
SelectMenuDefaultValueUser SelectMenuDefaultValueType = "user"
SelectMenuDefaultValueRole SelectMenuDefaultValueType = "role"
SelectMenuDefaultValueChannel SelectMenuDefaultValueType = "channel"
)
// SelectMenuDefaultValue represents an entity selected by default in auto-populated select menus.
type SelectMenuDefaultValue struct {
// ID of the entity.
ID string `json:"id"`
// Type of the entity.
Type SelectMenuDefaultValueType `json:"type"`
}
// SelectMenuType represents select menu type. // SelectMenuType represents select menu type.
type SelectMenuType ComponentType type SelectMenuType ComponentType
@@ -198,9 +216,13 @@ type SelectMenu struct {
MinValues *int `json:"min_values,omitempty"` MinValues *int `json:"min_values,omitempty"`
// This value determines the maximal amount of selected items in the menu. // This value determines the maximal amount of selected items in the menu.
// If MaxValues or MinValues are greater than one then the user can select multiple items in the component. // If MaxValues or MinValues are greater than one then the user can select multiple items in the component.
MaxValues int `json:"max_values,omitempty"` MaxValues int `json:"max_values,omitempty"`
Options []SelectMenuOption `json:"options,omitempty"` // List of default values for auto-populated select menus.
Disabled bool `json:"disabled"` // NOTE: Number of entries should be in the range defined by MinValues and MaxValues.
DefaultValues []SelectMenuDefaultValue `json:"default_values,omitempty"`
Options []SelectMenuOption `json:"options,omitempty"`
Disabled bool `json:"disabled"`
// NOTE: Can only be used in SelectMenu with Channel menu type. // NOTE: Can only be used in SelectMenu with Channel menu type.
ChannelTypes []ChannelType `json:"channel_types,omitempty"` ChannelTypes []ChannelType `json:"channel_types,omitempty"`

View File

@@ -22,7 +22,7 @@ import (
) )
// VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/) // VERSION of DiscordGo, follows Semantic Versioning. (http://semver.org/)
const VERSION = "0.27.1" const VERSION = "0.28.1"
// New creates a new Discord session with provided token. // New creates a new Discord session with provided token.
// If the token is for a bot, it must be prefixed with "Bot " // If the token is for a bot, it must be prefixed with "Bot "
@@ -33,20 +33,21 @@ func New(token string) (s *Session, err error) {
// Create an empty Session interface. // Create an empty Session interface.
s = &Session{ s = &Session{
State: NewState(), State: NewState(),
Ratelimiter: NewRatelimiter(), Ratelimiter: NewRatelimiter(),
StateEnabled: true, StateEnabled: true,
Compress: true, Compress: true,
ShouldReconnectOnError: true, ShouldReconnectOnError: true,
ShouldRetryOnRateLimit: true, ShouldReconnectVoiceOnSessionError: true,
ShardID: 0, ShouldRetryOnRateLimit: true,
ShardCount: 1, ShardID: 0,
MaxRestRetries: 3, ShardCount: 1,
Client: &http.Client{Timeout: (20 * time.Second)}, MaxRestRetries: 3,
Dialer: websocket.DefaultDialer, Client: &http.Client{Timeout: (20 * time.Second)},
UserAgent: "DiscordBot (https://github.com/bwmarrin/discordgo, v" + VERSION + ")", Dialer: websocket.DefaultDialer,
sequence: new(int64), UserAgent: "DiscordBot (https://github.com/bwmarrin/discordgo, v" + VERSION + ")",
LastHeartbeatAck: time.Now().UTC(), sequence: new(int64),
LastHeartbeatAck: time.Now().UTC(),
} }
// Initialize the Identify Package with defaults // Initialize the Identify Package with defaults

View File

@@ -42,6 +42,7 @@ var (
EndpointCDNChannelIcons = EndpointCDN + "channel-icons/" EndpointCDNChannelIcons = EndpointCDN + "channel-icons/"
EndpointCDNBanners = EndpointCDN + "banners/" EndpointCDNBanners = EndpointCDN + "banners/"
EndpointCDNGuilds = EndpointCDN + "guilds/" EndpointCDNGuilds = EndpointCDN + "guilds/"
EndpointCDNRoleIcons = EndpointCDN + "role-icons/"
EndpointVoice = EndpointAPI + "/voice/" EndpointVoice = EndpointAPI + "/voice/"
EndpointVoiceRegions = EndpointVoice + "regions" EndpointVoiceRegions = EndpointVoice + "regions"
@@ -49,9 +50,8 @@ var (
EndpointUser = func(uID string) string { return EndpointUsers + uID } EndpointUser = func(uID string) string { return EndpointUsers + uID }
EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" } EndpointUserAvatar = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".png" }
EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" } EndpointUserAvatarAnimated = func(uID, aID string) string { return EndpointCDNAvatars + uID + "/" + aID + ".gif" }
EndpointDefaultUserAvatar = func(uDiscriminator string) string { EndpointDefaultUserAvatar = func(idx int) string {
uDiscriminatorInt, _ := strconv.Atoi(uDiscriminator) return EndpointCDN + "embed/avatars/" + strconv.Itoa(idx) + ".png"
return EndpointCDN + "embed/avatars/" + strconv.Itoa(uDiscriminatorInt%5) + ".png"
} }
EndpointUserBanner = func(uID, cID string) string { EndpointUserBanner = func(uID, cID string) string {
return EndpointCDNBanners + uID + "/" + cID + ".png" return EndpointCDNBanners + uID + "/" + cID + ".png"
@@ -104,7 +104,8 @@ var (
EndpointGuildScheduledEvents = func(gID string) string { return EndpointGuilds + gID + "/scheduled-events" } EndpointGuildScheduledEvents = func(gID string) string { return EndpointGuilds + gID + "/scheduled-events" }
EndpointGuildScheduledEvent = func(gID, eID string) string { return EndpointGuilds + gID + "/scheduled-events/" + eID } EndpointGuildScheduledEvent = func(gID, eID string) string { return EndpointGuilds + gID + "/scheduled-events/" + eID }
EndpointGuildScheduledEventUsers = func(gID, eID string) string { return EndpointGuildScheduledEvent(gID, eID) + "/users" } EndpointGuildScheduledEventUsers = func(gID, eID string) string { return EndpointGuildScheduledEvent(gID, eID) + "/users" }
EndpointGuildTemplate = func(tID string) string { return EndpointGuilds + "/templates/" + tID } EndpointGuildOnboarding = func(gID string) string { return EndpointGuilds + gID + "/onboarding" }
EndpointGuildTemplate = func(tID string) string { return EndpointGuilds + "templates/" + tID }
EndpointGuildTemplates = func(gID string) string { return EndpointGuilds + gID + "/templates" } EndpointGuildTemplates = func(gID string) string { return EndpointGuilds + gID + "/templates" }
EndpointGuildTemplateSync = func(gID, tID string) string { return EndpointGuilds + gID + "/templates/" + tID } EndpointGuildTemplateSync = func(gID, tID string) string { return EndpointGuilds + gID + "/templates/" + tID }
EndpointGuildMemberAvatar = func(gId, uID, aID string) string { EndpointGuildMemberAvatar = func(gId, uID, aID string) string {
@@ -114,6 +115,10 @@ var (
return EndpointCDNGuilds + gId + "/users/" + uID + "/avatars/" + aID + ".gif" return EndpointCDNGuilds + gId + "/users/" + uID + "/avatars/" + aID + ".gif"
} }
EndpointRoleIcon = func(rID, hash string) string {
return EndpointCDNRoleIcons + rID + "/" + hash + ".png"
}
EndpointChannel = func(cID string) string { return EndpointChannels + cID } EndpointChannel = func(cID string) string { return EndpointChannels + cID }
EndpointChannelThreads = func(cID string) string { return EndpointChannel(cID) + "/threads" } EndpointChannelThreads = func(cID string) string { return EndpointChannel(cID) + "/threads" }
EndpointChannelActiveThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/active" } EndpointChannelActiveThreads = func(cID string) string { return EndpointChannelThreads(cID) + "/active" }

View File

@@ -19,6 +19,7 @@ const (
connectEventType = "__CONNECT__" connectEventType = "__CONNECT__"
disconnectEventType = "__DISCONNECT__" disconnectEventType = "__DISCONNECT__"
eventEventType = "__EVENT__" eventEventType = "__EVENT__"
guildAuditLogEntryCreateEventType = "GUILD_AUDIT_LOG_ENTRY_CREATE"
guildBanAddEventType = "GUILD_BAN_ADD" guildBanAddEventType = "GUILD_BAN_ADD"
guildBanRemoveEventType = "GUILD_BAN_REMOVE" guildBanRemoveEventType = "GUILD_BAN_REMOVE"
guildCreateEventType = "GUILD_CREATE" guildCreateEventType = "GUILD_CREATE"
@@ -294,6 +295,26 @@ func (eh eventEventHandler) Handle(s *Session, i interface{}) {
} }
} }
// guildAuditLogEntryCreateEventHandler is an event handler for GuildAuditLogEntryCreate events.
type guildAuditLogEntryCreateEventHandler func(*Session, *GuildAuditLogEntryCreate)
// Type returns the event type for GuildAuditLogEntryCreate events.
func (eh guildAuditLogEntryCreateEventHandler) Type() string {
return guildAuditLogEntryCreateEventType
}
// New returns a new instance of GuildAuditLogEntryCreate.
func (eh guildAuditLogEntryCreateEventHandler) New() interface{} {
return &GuildAuditLogEntryCreate{}
}
// Handle is the handler for GuildAuditLogEntryCreate events.
func (eh guildAuditLogEntryCreateEventHandler) Handle(s *Session, i interface{}) {
if t, ok := i.(*GuildAuditLogEntryCreate); ok {
eh(s, t)
}
}
// guildBanAddEventHandler is an event handler for GuildBanAdd events. // guildBanAddEventHandler is an event handler for GuildBanAdd events.
type guildBanAddEventHandler func(*Session, *GuildBanAdd) type guildBanAddEventHandler func(*Session, *GuildBanAdd)
@@ -1277,6 +1298,8 @@ func handlerForInterface(handler interface{}) EventHandler {
return disconnectEventHandler(v) return disconnectEventHandler(v)
case func(*Session, *Event): case func(*Session, *Event):
return eventEventHandler(v) return eventEventHandler(v)
case func(*Session, *GuildAuditLogEntryCreate):
return guildAuditLogEntryCreateEventHandler(v)
case func(*Session, *GuildBanAdd): case func(*Session, *GuildBanAdd):
return guildBanAddEventHandler(v) return guildBanAddEventHandler(v)
case func(*Session, *GuildBanRemove): case func(*Session, *GuildBanRemove):
@@ -1388,6 +1411,7 @@ func init() {
registerInterfaceProvider(channelDeleteEventHandler(nil)) registerInterfaceProvider(channelDeleteEventHandler(nil))
registerInterfaceProvider(channelPinsUpdateEventHandler(nil)) registerInterfaceProvider(channelPinsUpdateEventHandler(nil))
registerInterfaceProvider(channelUpdateEventHandler(nil)) registerInterfaceProvider(channelUpdateEventHandler(nil))
registerInterfaceProvider(guildAuditLogEntryCreateEventHandler(nil))
registerInterfaceProvider(guildBanAddEventHandler(nil)) registerInterfaceProvider(guildBanAddEventHandler(nil))
registerInterfaceProvider(guildBanRemoveEventHandler(nil)) registerInterfaceProvider(guildBanRemoveEventHandler(nil))
registerInterfaceProvider(guildCreateEventHandler(nil)) registerInterfaceProvider(guildCreateEventHandler(nil))

View File

@@ -401,3 +401,8 @@ type AutoModerationActionExecution struct {
MatchedKeyword string `json:"matched_keyword"` MatchedKeyword string `json:"matched_keyword"`
MatchedContent string `json:"matched_content"` MatchedContent string `json:"matched_content"`
} }
// GuildAuditLogEntryCreate is the data for a GuildAuditLogEntryCreate event.
type GuildAuditLogEntryCreate struct {
*AuditLogEntry
}

View File

@@ -314,9 +314,10 @@ type InteractionData interface {
// ApplicationCommandInteractionData contains the data of application command interaction. // ApplicationCommandInteractionData contains the data of application command interaction.
type ApplicationCommandInteractionData struct { type ApplicationCommandInteractionData struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Resolved *ApplicationCommandInteractionDataResolved `json:"resolved"` CommandType ApplicationCommandType `json:"type"`
Resolved *ApplicationCommandInteractionDataResolved `json:"resolved"`
// Slash command options // Slash command options
Options []*ApplicationCommandInteractionDataOption `json:"options"` Options []*ApplicationCommandInteractionDataOption `json:"options"`
@@ -553,6 +554,7 @@ type InteractionResponseData struct {
Embeds []*MessageEmbed `json:"embeds"` Embeds []*MessageEmbed `json:"embeds"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
Files []*File `json:"-"` Files []*File `json:"-"`
Attachments *[]*MessageAttachment `json:"attachments,omitempty"`
// NOTE: only MessageFlagsSuppressEmbeds and MessageFlagsEphemeral can be set. // NOTE: only MessageFlagsSuppressEmbeds and MessageFlagsEphemeral can be set.
Flags MessageFlags `json:"flags,omitempty"` Flags MessageFlags `json:"flags,omitempty"`

View File

@@ -39,6 +39,7 @@ const (
Romanian Locale = "ro" Romanian Locale = "ro"
Russian Locale = "ru" Russian Locale = "ru"
SpanishES Locale = "es-ES" SpanishES Locale = "es-ES"
SpanishLATAM Locale = "es-419"
Swedish Locale = "sv-SE" Swedish Locale = "sv-SE"
Thai Locale = "th" Thai Locale = "th"
Turkish Locale = "tr" Turkish Locale = "tr"
@@ -74,6 +75,7 @@ var Locales = map[Locale]string{
Romanian: "Romanian", Romanian: "Romanian",
Russian: "Russian", Russian: "Russian",
SpanishES: "Spanish (Spain)", SpanishES: "Spanish (Spain)",
SpanishLATAM: "Spanish (LATAM)",
Swedish: "Swedish", Swedish: "Swedish",
Thai: "Thai", Thai: "Thai",
Turkish: "Turkish", Turkish: "Turkish",

View File

@@ -148,8 +148,8 @@ type Message struct {
// The thread that was started from this message, includes thread member object // The thread that was started from this message, includes thread member object
Thread *Channel `json:"thread,omitempty"` Thread *Channel `json:"thread,omitempty"`
// An array of Sticker objects, if any were sent. // An array of StickerItem objects, representing sent stickers, if there were any.
StickerItems []*Sticker `json:"sticker_items"` StickerItems []*StickerItem `json:"sticker_items"`
} }
// UnmarshalJSON is a helper function to unmarshal the Message. // UnmarshalJSON is a helper function to unmarshal the Message.
@@ -215,6 +215,10 @@ const (
MessageFlagsLoading MessageFlags = 1 << 7 MessageFlagsLoading MessageFlags = 1 << 7
// MessageFlagsFailedToMentionSomeRolesInThread this message failed to mention some roles and add their members to the thread. // MessageFlagsFailedToMentionSomeRolesInThread this message failed to mention some roles and add their members to the thread.
MessageFlagsFailedToMentionSomeRolesInThread MessageFlags = 1 << 8 MessageFlagsFailedToMentionSomeRolesInThread MessageFlags = 1 << 8
// MessageFlagsSuppressNotifications this message will not trigger push and desktop notifications.
MessageFlagsSuppressNotifications MessageFlags = 1 << 12
// MessageFlagsIsVoiceMessage this message is a voice message.
MessageFlagsIsVoiceMessage MessageFlags = 1 << 13
) )
// File stores info about files you e.g. send in messages. // File stores info about files you e.g. send in messages.
@@ -233,6 +237,8 @@ type MessageSend struct {
Files []*File `json:"-"` Files []*File `json:"-"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
Reference *MessageReference `json:"message_reference,omitempty"` Reference *MessageReference `json:"message_reference,omitempty"`
StickerIDs []string `json:"sticker_ids"`
Flags MessageFlags `json:"flags,omitempty"`
// TODO: Remove this when compatibility is not required. // TODO: Remove this when compatibility is not required.
File *File `json:"-"` File *File `json:"-"`
@@ -245,8 +251,8 @@ type MessageSend struct {
// is also where you should get the instance from. // is also where you should get the instance from.
type MessageEdit struct { type MessageEdit struct {
Content *string `json:"content,omitempty"` Content *string `json:"content,omitempty"`
Components []MessageComponent `json:"components"` Components *[]MessageComponent `json:"components,omitempty"`
Embeds []*MessageEmbed `json:"embeds"` Embeds *[]*MessageEmbed `json:"embeds,omitempty"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
Flags MessageFlags `json:"flags,omitempty"` Flags MessageFlags `json:"flags,omitempty"`
// Files to append to the message // Files to append to the message
@@ -280,14 +286,14 @@ func (m *MessageEdit) SetContent(str string) *MessageEdit {
// SetEmbed is a convenience function for setting the embed, // SetEmbed is a convenience function for setting the embed,
// so you can chain commands. // so you can chain commands.
func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit { func (m *MessageEdit) SetEmbed(embed *MessageEmbed) *MessageEdit {
m.Embeds = []*MessageEmbed{embed} m.Embeds = &[]*MessageEmbed{embed}
return m return m
} }
// SetEmbeds is a convenience function for setting the embeds, // SetEmbeds is a convenience function for setting the embeds,
// so you can chain commands. // so you can chain commands.
func (m *MessageEdit) SetEmbeds(embeds []*MessageEmbed) *MessageEdit { func (m *MessageEdit) SetEmbeds(embeds []*MessageEmbed) *MessageEdit {
m.Embeds = embeds m.Embeds = &embeds
return m return m
} }
@@ -460,20 +466,32 @@ type MessageApplication struct {
// MessageReference contains reference data sent with crossposted messages // MessageReference contains reference data sent with crossposted messages
type MessageReference struct { type MessageReference struct {
MessageID string `json:"message_id"` MessageID string `json:"message_id"`
ChannelID string `json:"channel_id,omitempty"` ChannelID string `json:"channel_id,omitempty"`
GuildID string `json:"guild_id,omitempty"` GuildID string `json:"guild_id,omitempty"`
FailIfNotExists *bool `json:"fail_if_not_exists,omitempty"`
} }
// Reference returns MessageReference of given message func (m *Message) reference(failIfNotExists bool) *MessageReference {
func (m *Message) Reference() *MessageReference {
return &MessageReference{ return &MessageReference{
GuildID: m.GuildID, GuildID: m.GuildID,
ChannelID: m.ChannelID, ChannelID: m.ChannelID,
MessageID: m.ID, MessageID: m.ID,
FailIfNotExists: &failIfNotExists,
} }
} }
// Reference returns a MessageReference of the given message.
func (m *Message) Reference() *MessageReference {
return m.reference(true)
}
// SoftReference returns a MessageReference of the given message.
// If the message doesn't exist it will instead be sent as a non-reply message.
func (m *Message) SoftReference() *MessageReference {
return m.reference(false)
}
// ContentWithMentionsReplaced will replace all @<id> mentions with the // ContentWithMentionsReplaced will replace all @<id> mentions with the
// username of the mention. // username of the mention.
func (m *Message) ContentWithMentionsReplaced() (content string) { func (m *Message) ContentWithMentionsReplaced() (content string) {

View File

@@ -424,10 +424,11 @@ func (s *Session) UserGuildMember(guildID string, options ...RequestOption) (st
} }
// UserGuilds returns an array of UserGuild structures for all guilds. // UserGuilds returns an array of UserGuild structures for all guilds.
// limit : The number guilds that can be returned. (max 100) // limit : The number guilds that can be returned. (max 200)
// beforeID : If provided all guilds returned will be before given ID. // beforeID : If provided all guilds returned will be before given ID.
// afterID : If provided all guilds returned will be after given ID. // afterID : If provided all guilds returned will be after given ID.
func (s *Session) UserGuilds(limit int, beforeID, afterID string, options ...RequestOption) (st []*UserGuild, err error) { // withCounts : Whether to include approximate member and presence counts or not.
func (s *Session) UserGuilds(limit int, beforeID, afterID string, withCounts bool, options ...RequestOption) (st []*UserGuild, err error) {
v := url.Values{} v := url.Values{}
@@ -440,6 +441,9 @@ func (s *Session) UserGuilds(limit int, beforeID, afterID string, options ...Req
if beforeID != "" { if beforeID != "" {
v.Set("before", beforeID) v.Set("before", beforeID)
} }
if withCounts {
v.Set("with_counts", "true")
}
uri := EndpointUserGuilds("@me") uri := EndpointUserGuilds("@me")
@@ -672,14 +676,9 @@ func (s *Session) GuildEdit(guildID string, g *GuildParams, options ...RequestOp
// GuildDelete deletes a Guild. // GuildDelete deletes a Guild.
// guildID : The ID of a Guild // guildID : The ID of a Guild
func (s *Session) GuildDelete(guildID string, options ...RequestOption) (st *Guild, err error) { func (s *Session) GuildDelete(guildID string, options ...RequestOption) (err error) {
body, err := s.RequestWithBucketID("DELETE", EndpointGuild(guildID), nil, EndpointGuild(guildID), options...) _, err = s.RequestWithBucketID("DELETE", EndpointGuild(guildID), nil, EndpointGuild(guildID), options...)
if err != nil {
return
}
err = unmarshal(body, &st)
return return
} }
@@ -1700,13 +1699,19 @@ func (s *Session) ChannelMessageSendComplex(channelID string, data *MessageSend,
} }
} }
if data.StickerIDs != nil {
if len(data.StickerIDs) > 3 {
err = fmt.Errorf("cannot send more than 3 stickers")
return
}
}
var response []byte var response []byte
if len(files) > 0 { if len(files) > 0 {
contentType, body, encodeErr := MultipartBodyWithJSON(data, files) contentType, body, encodeErr := MultipartBodyWithJSON(data, files)
if encodeErr != nil { if encodeErr != nil {
return st, encodeErr return st, encodeErr
} }
response, err = s.request("POST", endpoint, contentType, body, endpoint, 0, options...) response, err = s.request("POST", endpoint, contentType, body, endpoint, 0, options...)
} else { } else {
response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint, options...) response, err = s.RequestWithBucketID("POST", endpoint, data, endpoint, options...)
@@ -1796,16 +1801,18 @@ func (s *Session) ChannelMessageEditComplex(m *MessageEdit, options ...RequestOp
// TODO: Remove this when compatibility is not required. // TODO: Remove this when compatibility is not required.
if m.Embed != nil { if m.Embed != nil {
if m.Embeds == nil { if m.Embeds == nil {
m.Embeds = []*MessageEmbed{m.Embed} m.Embeds = &[]*MessageEmbed{m.Embed}
} else { } else {
err = fmt.Errorf("cannot specify both Embed and Embeds") err = fmt.Errorf("cannot specify both Embed and Embeds")
return return
} }
} }
for _, embed := range m.Embeds { if m.Embeds != nil {
if embed.Type == "" { for _, embed := range *m.Embeds {
embed.Type = "rich" if embed.Type == "" {
embed.Type = "rich"
}
} }
} }
@@ -2267,7 +2274,7 @@ func (s *Session) WebhookWithToken(webhookID, token string, options ...RequestOp
// webhookID: The ID of a webhook. // webhookID: The ID of a webhook.
// name : The name of the webhook. // name : The name of the webhook.
// avatar : The avatar of the webhook. // avatar : The avatar of the webhook.
func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string, options ...RequestOption) (st *Role, err error) { func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string, options ...RequestOption) (st *Webhook, err error) {
data := struct { data := struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
@@ -2290,14 +2297,15 @@ func (s *Session) WebhookEdit(webhookID, name, avatar, channelID string, options
// token : The auth token for the webhook. // token : The auth token for the webhook.
// name : The name of the webhook. // name : The name of the webhook.
// avatar : The avatar of the webhook. // avatar : The avatar of the webhook.
func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string, options ...RequestOption) (st *Role, err error) { func (s *Session) WebhookEditWithToken(webhookID, token, name, avatar string, options ...RequestOption) (st *Webhook, err error) {
data := struct { data := struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Avatar string `json:"avatar,omitempty"` Avatar string `json:"avatar,omitempty"`
}{name, avatar} }{name, avatar}
body, err := s.RequestWithBucketID("PATCH", EndpointWebhookToken(webhookID, token), data, EndpointWebhookToken("", ""), options...) var body []byte
body, err = s.RequestWithBucketID("PATCH", EndpointWebhookToken(webhookID, token), data, EndpointWebhookToken("", ""), options...)
if err != nil { if err != nil {
return return
} }
@@ -2709,11 +2717,22 @@ func (s *Session) ThreadMemberRemove(threadID, memberID string, options ...Reque
return err return err
} }
// ThreadMember returns thread member object for the specified member of a thread // ThreadMember returns thread member object for the specified member of a thread.
func (s *Session) ThreadMember(threadID, memberID string, options ...RequestOption) (member *ThreadMember, err error) { // withMember : Whether to include a guild member object.
endpoint := EndpointThreadMember(threadID, memberID) func (s *Session) ThreadMember(threadID, memberID string, withMember bool, options ...RequestOption) (member *ThreadMember, err error) {
uri := EndpointThreadMember(threadID, memberID)
queryParams := url.Values{}
if withMember {
queryParams.Set("with_member", "true")
}
if len(queryParams) > 0 {
uri += "?" + queryParams.Encode()
}
var body []byte var body []byte
body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint, options...) body, err = s.RequestWithBucketID("GET", uri, nil, uri, options...)
if err != nil { if err != nil {
return return
@@ -2724,9 +2743,29 @@ func (s *Session) ThreadMember(threadID, memberID string, options ...RequestOpti
} }
// ThreadMembers returns all members of specified thread. // ThreadMembers returns all members of specified thread.
func (s *Session) ThreadMembers(threadID string, options ...RequestOption) (members []*ThreadMember, err error) { // limit : Max number of thread members to return (1-100). Defaults to 100.
// afterID : Get thread members after this user ID.
// withMember : Whether to include a guild member object for each thread member.
func (s *Session) ThreadMembers(threadID string, limit int, withMember bool, afterID string, options ...RequestOption) (members []*ThreadMember, err error) {
uri := EndpointThreadMembers(threadID)
queryParams := url.Values{}
if withMember {
queryParams.Set("with_member", "true")
}
if limit > 0 {
queryParams.Set("limit", strconv.Itoa(limit))
}
if afterID != "" {
queryParams.Set("after", afterID)
}
if len(queryParams) > 0 {
uri += "?" + queryParams.Encode()
}
var body []byte var body []byte
body, err = s.RequestWithBucketID("GET", EndpointThreadMembers(threadID), nil, EndpointThreadMembers(threadID), options...) body, err = s.RequestWithBucketID("GET", uri, nil, uri, options...)
if err != nil { if err != nil {
return return
@@ -3248,6 +3287,37 @@ func (s *Session) GuildScheduledEventUsers(guildID, eventID string, limit int, w
return return
} }
// GuildOnboarding returns onboarding configuration of a guild.
// guildID : The ID of the guild
func (s *Session) GuildOnboarding(guildID string, options ...RequestOption) (onboarding *GuildOnboarding, err error) {
endpoint := EndpointGuildOnboarding(guildID)
var body []byte
body, err = s.RequestWithBucketID("GET", endpoint, nil, endpoint, options...)
if err != nil {
return
}
err = unmarshal(body, &onboarding)
return
}
// GuildOnboardingEdit edits onboarding configuration of a guild.
// guildID : The ID of the guild
// o : New GuildOnboarding data
func (s *Session) GuildOnboardingEdit(guildID string, o *GuildOnboarding, options ...RequestOption) (onboarding *GuildOnboarding, err error) {
endpoint := EndpointGuildOnboarding(guildID)
var body []byte
body, err = s.RequestWithBucketID("PUT", endpoint, o, endpoint, options...)
if err != nil {
return
}
err = unmarshal(body, &onboarding)
return
}
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------
// Functions specific to auto moderation // Functions specific to auto moderation
// ---------------------------------------------------------------------- // ----------------------------------------------------------------------

View File

@@ -42,6 +42,9 @@ type Session struct {
// Should the session reconnect the websocket on errors. // Should the session reconnect the websocket on errors.
ShouldReconnectOnError bool ShouldReconnectOnError bool
// Should voice connections reconnect on a session reconnect.
ShouldReconnectVoiceOnSessionError bool
// Should the session retry requests when rate limited. // Should the session retry requests when rate limited.
ShouldRetryOnRateLimit bool ShouldRetryOnRateLimit bool
@@ -285,7 +288,9 @@ const (
ChannelTypeGuildPublicThread ChannelType = 11 ChannelTypeGuildPublicThread ChannelType = 11
ChannelTypeGuildPrivateThread ChannelType = 12 ChannelTypeGuildPrivateThread ChannelType = 12
ChannelTypeGuildStageVoice ChannelType = 13 ChannelTypeGuildStageVoice ChannelType = 13
ChannelTypeGuildDirectory ChannelType = 14
ChannelTypeGuildForum ChannelType = 15 ChannelTypeGuildForum ChannelType = 15
ChannelTypeGuildMedia ChannelType = 16
) )
// ChannelFlags represent flags of a channel/thread. // ChannelFlags represent flags of a channel/thread.
@@ -440,7 +445,7 @@ type ChannelEdit struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Topic string `json:"topic,omitempty"` Topic string `json:"topic,omitempty"`
NSFW *bool `json:"nsfw,omitempty"` NSFW *bool `json:"nsfw,omitempty"`
Position int `json:"position"` Position *int `json:"position,omitempty"`
Bitrate int `json:"bitrate,omitempty"` Bitrate int `json:"bitrate,omitempty"`
UserLimit int `json:"user_limit,omitempty"` UserLimit int `json:"user_limit,omitempty"`
PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"` PermissionOverwrites []*PermissionOverwrite `json:"permission_overwrites,omitempty"`
@@ -528,6 +533,10 @@ type ThreadMember struct {
JoinTimestamp time.Time `json:"join_timestamp"` JoinTimestamp time.Time `json:"join_timestamp"`
// Any user-thread settings, currently only used for notifications // Any user-thread settings, currently only used for notifications
Flags int `json:"flags"` Flags int `json:"flags"`
// Additional information about the user.
// NOTE: only present if the withMember parameter is set to true
// when calling Session.ThreadMembers or Session.ThreadMember.
Member *Member `json:"member,omitempty"`
} }
// ThreadsList represents a list of threads alongisde with thread member objects for the current user. // ThreadsList represents a list of threads alongisde with thread member objects for the current user.
@@ -649,6 +658,13 @@ type Sticker struct {
SortValue int `json:"sort_value"` SortValue int `json:"sort_value"`
} }
// StickerItem represents the smallest amount of data required to render a sticker. A partial sticker object.
type StickerItem struct {
ID string `json:"id"`
Name string `json:"name"`
FormatType StickerFormat `json:"format_type"`
}
// StickerPack represents a pack of standard stickers. // StickerPack represents a pack of standard stickers.
type StickerPack struct { type StickerPack struct {
ID string `json:"id"` ID string `json:"id"`
@@ -1067,6 +1083,109 @@ type GuildScheduledEventUser struct {
Member *Member `json:"member"` Member *Member `json:"member"`
} }
// GuildOnboardingMode defines the criteria used to satisfy constraints that are required for enabling onboarding.
// https://discord.com/developers/docs/resources/guild#guild-onboarding-object-onboarding-mode
type GuildOnboardingMode int
// Block containing known GuildOnboardingMode values.
const (
// GuildOnboardingModeDefault counts default channels towards constraints.
GuildOnboardingModeDefault GuildOnboardingMode = 0
// GuildOnboardingModeAdvanced counts default channels and questions towards constraints.
GuildOnboardingModeAdvanced GuildOnboardingMode = 1
)
// GuildOnboarding represents the onboarding flow for a guild.
// https://discord.com/developers/docs/resources/guild#guild-onboarding-object
type GuildOnboarding struct {
// ID of the guild this onboarding flow is part of.
GuildID string `json:"guild_id,omitempty"`
// Prompts shown during onboarding and in the customize community (Channels & Roles) tab.
Prompts *[]GuildOnboardingPrompt `json:"prompts,omitempty"`
// Channel IDs that members get opted into automatically.
DefaultChannelIDs []string `json:"default_channel_ids,omitempty"`
// Whether onboarding is enabled in the guild.
Enabled *bool `json:"enabled,omitempty"`
// Mode of onboarding.
Mode *GuildOnboardingMode `json:"mode,omitempty"`
}
// GuildOnboardingPromptType is the type of an onboarding prompt.
// https://discord.com/developers/docs/resources/guild#guild-onboarding-object-prompt-types
type GuildOnboardingPromptType int
// Block containing known GuildOnboardingPromptType values.
const (
GuildOnboardingPromptTypeMultipleChoice GuildOnboardingPromptType = 0
GuildOnboardingPromptTypeDropdown GuildOnboardingPromptType = 1
)
// GuildOnboardingPrompt is a prompt shown during onboarding and in the customize community (Channels & Roles) tab.
// https://discord.com/developers/docs/resources/guild#guild-onboarding-object-onboarding-prompt-structure
type GuildOnboardingPrompt struct {
// ID of the prompt.
// NOTE: always requires to be a valid snowflake (e.g. "0"), see
// https://github.com/discord/discord-api-docs/issues/6320 for more information.
ID string `json:"id,omitempty"`
// Type of the prompt.
Type GuildOnboardingPromptType `json:"type"`
// Options available within the prompt.
Options []GuildOnboardingPromptOption `json:"options"`
// Title of the prompt.
Title string `json:"title"`
// Indicates whether users are limited to selecting one option for the prompt.
SingleSelect bool `json:"single_select"`
// Indicates whether the prompt is required before a user completes the onboarding flow.
Required bool `json:"required"`
// Indicates whether the prompt is present in the onboarding flow.
// If false, the prompt will only appear in the customize community (Channels & Roles) tab.
InOnboarding bool `json:"in_onboarding"`
}
// GuildOnboardingPromptOption is an option available within an onboarding prompt.
// https://discord.com/developers/docs/resources/guild#guild-onboarding-object-prompt-option-structure
type GuildOnboardingPromptOption struct {
// ID of the prompt option.
ID string `json:"id,omitempty"`
// IDs for channels a member is added to when the option is selected.
ChannelIDs []string `json:"channel_ids"`
// IDs for roles assigned to a member when the option is selected.
RoleIDs []string `json:"role_ids"`
// Emoji of the option.
// NOTE: when creating or updating a prompt option
// EmojiID, EmojiName and EmojiAnimated should be used instead.
Emoji *Emoji `json:"emoji,omitempty"`
// Title of the option.
Title string `json:"title"`
// Description of the option.
Description string `json:"description"`
// ID of the option's emoji.
// NOTE: only used when creating or updating a prompt option.
EmojiID string `json:"emoji_id,omitempty"`
// Name of the option's emoji.
// NOTE: only used when creating or updating a prompt option.
EmojiName string `json:"emoji_name,omitempty"`
// Whether the option's emoji is animated.
// NOTE: only used when creating or updating a prompt option.
EmojiAnimated *bool `json:"emoji_animated,omitempty"`
}
// A GuildTemplate represents a replicable template for guild creation // A GuildTemplate represents a replicable template for guild creation
type GuildTemplate struct { type GuildTemplate struct {
// The unique code for the guild template // The unique code for the guild template
@@ -1157,6 +1276,14 @@ type UserGuild struct {
Owner bool `json:"owner"` Owner bool `json:"owner"`
Permissions int64 `json:"permissions,string"` Permissions int64 `json:"permissions,string"`
Features []GuildFeature `json:"features"` Features []GuildFeature `json:"features"`
// Approximate number of members in this guild.
// NOTE: this field is only filled when withCounts is true.
ApproximateMemberCount int `json:"approximate_member_count"`
// Approximate number of non-offline members in this guild.
// NOTE: this field is only filled when withCounts is true.
ApproximatePresenceCount int `json:"approximate_presence_count"`
} }
// GuildFeature indicates the presence of a feature in a guild // GuildFeature indicates the presence of a feature in a guild
@@ -1239,13 +1366,51 @@ type Role struct {
// This is a combination of bit masks; the presence of a certain permission can // This is a combination of bit masks; the presence of a certain permission can
// be checked by performing a bitwise AND between this int and the permission. // be checked by performing a bitwise AND between this int and the permission.
Permissions int64 `json:"permissions,string"` Permissions int64 `json:"permissions,string"`
// The hash of the role icon. Use Role.IconURL to retrieve the icon's URL.
Icon string `json:"icon"`
// The emoji assigned to this role.
UnicodeEmoji string `json:"unicode_emoji"`
// The flags of the role, which describe its extra features.
// This is a combination of bit masks; the presence of a certain flag can
// be checked by performing a bitwise AND between this int and the flag.
Flags RoleFlags `json:"flags"`
} }
// RoleFlags represent the flags of a Role.
// https://discord.com/developers/docs/topics/permissions#role-object-role-flags
type RoleFlags int
// Block containing known RoleFlags values.
const (
// RoleFlagInPrompt indicates whether the Role is selectable by members in an onboarding prompt.
RoleFlagInPrompt RoleFlags = 1 << 0
)
// Mention returns a string which mentions the role // Mention returns a string which mentions the role
func (r *Role) Mention() string { func (r *Role) Mention() string {
return fmt.Sprintf("<@&%s>", r.ID) return fmt.Sprintf("<@&%s>", r.ID)
} }
// IconURL returns the URL of the role's icon.
//
// size: The size of the desired role icon as a power of two
// Image size can be any power of two between 16 and 4096.
func (r *Role) IconURL(size string) string {
if r.Icon == "" {
return ""
}
URL := EndpointRoleIcon(r.ID, r.Icon)
if size != "" {
return URL + "?size=" + size
}
return URL
}
// RoleParams represents the parameters needed to create or update a Role // RoleParams represents the parameters needed to create or update a Role
type RoleParams struct { type RoleParams struct {
// The role's name // The role's name
@@ -1258,6 +1423,12 @@ type RoleParams struct {
Permissions *int64 `json:"permissions,omitempty,string"` Permissions *int64 `json:"permissions,omitempty,string"`
// Whether this role is mentionable // Whether this role is mentionable
Mentionable *bool `json:"mentionable,omitempty"` Mentionable *bool `json:"mentionable,omitempty"`
// The role's unicode emoji.
// NOTE: can only be set if the guild has the ROLE_ICONS feature.
UnicodeEmoji *string `json:"unicode_emoji,omitempty"`
// The role's icon image encoded in base64.
// NOTE: can only be set if the guild has the ROLE_ICONS feature.
Icon *string `json:"icon,omitempty"`
} }
// Roles are a collection of Role // Roles are a collection of Role
@@ -1330,6 +1501,22 @@ type Assets struct {
SmallText string `json:"small_text,omitempty"` SmallText string `json:"small_text,omitempty"`
} }
// MemberFlags represent flags of a guild member.
// https://discord.com/developers/docs/resources/guild#guild-member-object-guild-member-flags
type MemberFlags int
// Block containing known MemberFlags values.
const (
// MemberFlagDidRejoin indicates whether the Member has left and rejoined the guild.
MemberFlagDidRejoin MemberFlags = 1 << 0
// MemberFlagCompletedOnboarding indicates whether the Member has completed onboarding.
MemberFlagCompletedOnboarding MemberFlags = 1 << 1
// MemberFlagBypassesVerification indicates whether the Member is exempt from guild verification requirements.
MemberFlagBypassesVerification MemberFlags = 1 << 2
// MemberFlagStartedOnboarding indicates whether the Member has started onboarding.
MemberFlagStartedOnboarding MemberFlags = 1 << 3
)
// A Member stores user information for Guild members. A guild // A Member stores user information for Guild members. A guild
// member represents a certain user's presence in a guild. // member represents a certain user's presence in a guild.
type Member struct { type Member struct {
@@ -1360,6 +1547,10 @@ type Member struct {
// When the user used their Nitro boost on the server // When the user used their Nitro boost on the server
PremiumSince *time.Time `json:"premium_since"` PremiumSince *time.Time `json:"premium_since"`
// The flags of this member. This is a combination of bit masks; the presence of a certain
// flag can be checked by performing a bitwise AND between this int and the flag.
Flags MemberFlags `json:"flags"`
// Is true while the member hasn't accepted the membership screen. // Is true while the member hasn't accepted the membership screen.
Pending bool `json:"pending"` Pending bool `json:"pending"`
@@ -1391,6 +1582,15 @@ func (m *Member) AvatarURL(size string) string {
} }
// DisplayName returns the member's guild nickname if they have one,
// otherwise it returns their discord display name.
func (m *Member) DisplayName() string {
if m.Nick != "" {
return m.Nick
}
return m.User.GlobalName
}
// ClientStatus stores the online, offline, idle, or dnd status of each device of a Guild member. // ClientStatus stores the online, offline, idle, or dnd status of each device of a Guild member.
type ClientStatus struct { type ClientStatus struct {
Desktop Status `json:"desktop"` Desktop Status `json:"desktop"`
@@ -1738,14 +1938,18 @@ const (
// AuditLogOptions optional data for the AuditLog // AuditLogOptions optional data for the AuditLog
// https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info // https://discord.com/developers/docs/resources/audit-log#audit-log-entry-object-optional-audit-entry-info
type AuditLogOptions struct { type AuditLogOptions struct {
DeleteMemberDays string `json:"delete_member_days"` DeleteMemberDays string `json:"delete_member_days"`
MembersRemoved string `json:"members_removed"` MembersRemoved string `json:"members_removed"`
ChannelID string `json:"channel_id"` ChannelID string `json:"channel_id"`
MessageID string `json:"message_id"` MessageID string `json:"message_id"`
Count string `json:"count"` Count string `json:"count"`
ID string `json:"id"` ID string `json:"id"`
Type *AuditLogOptionsType `json:"type"` Type *AuditLogOptionsType `json:"type"`
RoleName string `json:"role_name"` RoleName string `json:"role_name"`
ApplicationID string `json:"application_id"`
AutoModerationRuleName string `json:"auto_moderation_rule_name"`
AutoModerationRuleTriggerType string `json:"auto_moderation_rule_trigger_type"`
IntegrationType string `json:"integration_type"`
} }
// AuditLogOptionsType of the AuditLogOption // AuditLogOptionsType of the AuditLogOption
@@ -1754,8 +1958,8 @@ type AuditLogOptionsType string
// Valid Types for AuditLogOptionsType // Valid Types for AuditLogOptionsType
const ( const (
AuditLogOptionsTypeMember AuditLogOptionsType = "member" AuditLogOptionsTypeRole AuditLogOptionsType = "0"
AuditLogOptionsTypeRole AuditLogOptionsType = "role" AuditLogOptionsTypeMember AuditLogOptionsType = "1"
) )
// AuditLogAction is the Action of the AuditLog (see AuditLogAction* consts) // AuditLogAction is the Action of the AuditLog (see AuditLogAction* consts)
@@ -1816,7 +2020,7 @@ const (
AuditLogActionStickerDelete AuditLogAction = 92 AuditLogActionStickerDelete AuditLogAction = 92
AuditLogGuildScheduledEventCreate AuditLogAction = 100 AuditLogGuildScheduledEventCreate AuditLogAction = 100
AuditLogGuildScheduledEventUpdare AuditLogAction = 101 AuditLogGuildScheduledEventUpdate AuditLogAction = 101
AuditLogGuildScheduledEventDelete AuditLogAction = 102 AuditLogGuildScheduledEventDelete AuditLogAction = 102
AuditLogActionThreadCreate AuditLogAction = 110 AuditLogActionThreadCreate AuditLogAction = 110
@@ -1824,6 +2028,16 @@ const (
AuditLogActionThreadDelete AuditLogAction = 112 AuditLogActionThreadDelete AuditLogAction = 112
AuditLogActionApplicationCommandPermissionUpdate AuditLogAction = 121 AuditLogActionApplicationCommandPermissionUpdate AuditLogAction = 121
AuditLogActionAutoModerationRuleCreate AuditLogAction = 140
AuditLogActionAutoModerationRuleUpdate AuditLogAction = 141
AuditLogActionAutoModerationRuleDelete AuditLogAction = 142
AuditLogActionAutoModerationBlockMessage AuditLogAction = 143
AuditLogActionAutoModerationFlagToChannel AuditLogAction = 144
AuditLogActionAutoModerationUserCommunicationDisabled AuditLogAction = 145
AuditLogActionCreatorMonetizationRequestCreated AuditLogAction = 150
AuditLogActionCreatorMonetizationTermsAccepted AuditLogAction = 151
) )
// GuildMemberParams stores data needed to update a member // GuildMemberParams stores data needed to update a member
@@ -2341,6 +2555,9 @@ const (
ErrCodeCannotUpdateAFinishedEvent = 180000 ErrCodeCannotUpdateAFinishedEvent = 180000
ErrCodeFailedToCreateStageNeededForStageEvent = 180002 ErrCodeFailedToCreateStageNeededForStageEvent = 180002
ErrCodeCannotEnableOnboardingRequirementsAreNotMet = 350000
ErrCodeCannotUpdateOnboardingWhileBelowRequirements = 350001
) )
// Intent is the type of a Gateway Intent // Intent is the type of a Gateway Intent
@@ -2351,7 +2568,7 @@ type Intent int
const ( const (
IntentGuilds Intent = 1 << 0 IntentGuilds Intent = 1 << 0
IntentGuildMembers Intent = 1 << 1 IntentGuildMembers Intent = 1 << 1
IntentGuildBans Intent = 1 << 2 IntentGuildModeration Intent = 1 << 2
IntentGuildEmojis Intent = 1 << 3 IntentGuildEmojis Intent = 1 << 3
IntentGuildIntegrations Intent = 1 << 4 IntentGuildIntegrations Intent = 1 << 4
IntentGuildWebhooks Intent = 1 << 5 IntentGuildWebhooks Intent = 1 << 5
@@ -2371,6 +2588,8 @@ const (
// TODO: remove when compatibility is not needed // TODO: remove when compatibility is not needed
IntentGuildBans Intent = IntentGuildModeration
IntentsGuilds Intent = 1 << 0 IntentsGuilds Intent = 1 << 0
IntentsGuildMembers Intent = 1 << 1 IntentsGuildMembers Intent = 1 << 1
IntentsGuildBans Intent = 1 << 2 IntentsGuildBans Intent = 1 << 2

View File

@@ -1,5 +1,9 @@
package discordgo package discordgo
import (
"strconv"
)
// UserFlags is the flags of "user" (see UserFlags* consts) // UserFlags is the flags of "user" (see UserFlags* consts)
// https://discord.com/developers/docs/resources/user#user-object-user-flags // https://discord.com/developers/docs/resources/user#user-object-user-flags
type UserFlags int type UserFlags int
@@ -20,6 +24,20 @@ const (
UserFlagVerifiedBot UserFlags = 1 << 16 UserFlagVerifiedBot UserFlags = 1 << 16
UserFlagVerifiedBotDeveloper UserFlags = 1 << 17 UserFlagVerifiedBotDeveloper UserFlags = 1 << 17
UserFlagDiscordCertifiedModerator UserFlags = 1 << 18 UserFlagDiscordCertifiedModerator UserFlags = 1 << 18
UserFlagBotHTTPInteractions UserFlags = 1 << 19
UserFlagActiveBotDeveloper UserFlags = 1 << 22
)
// UserPremiumType is the type of premium (nitro) subscription a user has (see UserPremiumType* consts).
// https://discord.com/developers/docs/resources/user#user-object-premium-types
type UserPremiumType int
// Valid UserPremiumType values.
const (
UserPremiumTypeNone UserPremiumType = 0
UserPremiumTypeNitroClassic UserPremiumType = 1
UserPremiumTypeNitro UserPremiumType = 2
UserPremiumTypeNitroBasic UserPremiumType = 3
) )
// A User stores all data for an individual Discord user. // A User stores all data for an individual Discord user.
@@ -44,6 +62,10 @@ type User struct {
// The discriminator of the user (4 numbers after name). // The discriminator of the user (4 numbers after name).
Discriminator string `json:"discriminator"` Discriminator string `json:"discriminator"`
// The user's display name, if it is set.
// For bots, this is the application name.
GlobalName string `json:"global_name"`
// The token of the user. This is only present for // The token of the user. This is only present for
// the user represented by the current session. // the user represented by the current session.
Token string `json:"token"` Token string `json:"token"`
@@ -70,7 +92,7 @@ type User struct {
// The type of Nitro subscription on a user's account. // The type of Nitro subscription on a user's account.
// Only available when the request is authorized via a Bearer token. // Only available when the request is authorized via a Bearer token.
PremiumType int `json:"premium_type"` PremiumType UserPremiumType `json:"premium_type"`
// Whether the user is an Official Discord System user (part of the urgent message system). // Whether the user is an Official Discord System user (part of the urgent message system).
System bool `json:"system"` System bool `json:"system"`
@@ -81,7 +103,14 @@ type User struct {
} }
// String returns a unique identifier of the form username#discriminator // String returns a unique identifier of the form username#discriminator
// or just username, if the discriminator is set to "0".
func (u *User) String() string { func (u *User) String() string {
// If the user has been migrated from the legacy username system, their discriminator is "0".
// See https://support-dev.discord.com/hc/en-us/articles/13667755828631
if u.Discriminator == "0" {
return u.Username
}
return u.Username + "#" + u.Discriminator return u.Username + "#" + u.Discriminator
} }
@@ -91,17 +120,35 @@ func (u *User) Mention() string {
} }
// AvatarURL returns a URL to the user's avatar. // AvatarURL returns a URL to the user's avatar.
// size: The size of the user's avatar as a power of two //
// if size is an empty string, no size parameter will // size: The size of the user's avatar as a power of two
// be added to the URL. // if size is an empty string, no size parameter will
// be added to the URL.
func (u *User) AvatarURL(size string) string { func (u *User) AvatarURL(size string) string {
return avatarURL(u.Avatar, EndpointDefaultUserAvatar(u.Discriminator), return avatarURL(
EndpointUserAvatar(u.ID, u.Avatar), EndpointUserAvatarAnimated(u.ID, u.Avatar), size) u.Avatar,
EndpointDefaultUserAvatar(u.DefaultAvatarIndex()),
EndpointUserAvatar(u.ID, u.Avatar),
EndpointUserAvatarAnimated(u.ID, u.Avatar),
size,
)
} }
// BannerURL returns the URL of the users's banner image. // BannerURL returns the URL of the users's banner image.
// size: The size of the desired banner image as a power of two //
// Image size can be any power of two between 16 and 4096. // size: The size of the desired banner image as a power of two
// Image size can be any power of two between 16 and 4096.
func (u *User) BannerURL(size string) string { func (u *User) BannerURL(size string) string {
return bannerURL(u.Banner, EndpointUserBanner(u.ID, u.Banner), EndpointUserBannerAnimated(u.ID, u.Banner), size) return bannerURL(u.Banner, EndpointUserBanner(u.ID, u.Banner), EndpointUserBannerAnimated(u.ID, u.Banner), size)
} }
// DefaultAvatarIndex returns the index of the user's default avatar.
func (u *User) DefaultAvatarIndex() int {
if u.Discriminator == "0" {
id, _ := strconv.ParseUint(u.ID, 10, 64)
return int((id >> 22) % 6)
}
id, _ := strconv.Atoi(u.Discriminator)
return id % 5
}

View File

@@ -76,7 +76,7 @@ type VoiceSpeakingUpdateHandler func(vc *VoiceConnection, vs *VoiceSpeakingUpdat
// Speaking sends a speaking notification to Discord over the voice websocket. // Speaking sends a speaking notification to Discord over the voice websocket.
// This must be sent as true prior to sending audio and should be set to false // This must be sent as true prior to sending audio and should be set to false
// once finished sending audio. // once finished sending audio.
// b : Send true if speaking, false if not. // b : Send true if speaking, false if not.
func (v *VoiceConnection) Speaking(b bool) (err error) { func (v *VoiceConnection) Speaking(b bool) (err error) {
v.log(LogDebug, "called (%t)", b) v.log(LogDebug, "called (%t)", b)
@@ -294,11 +294,15 @@ func (v *VoiceConnection) open() (err error) {
if v.sessionID != "" { if v.sessionID != "" {
break break
} }
if i > 20 { // only loop for up to 1 second total if i > 20 { // only loop for up to 1 second total
return fmt.Errorf("did not receive voice Session ID in time") return fmt.Errorf("did not receive voice Session ID in time")
} }
// Release the lock, so sessionID can be populated upon receiving a VoiceStateUpdate event.
v.Unlock()
time.Sleep(50 * time.Millisecond) time.Sleep(50 * time.Millisecond)
i++ i++
v.Lock()
} }
// Connect to VoiceConnection Websocket // Connect to VoiceConnection Websocket

View File

@@ -34,10 +34,14 @@ type WebhookParams struct {
Files []*File `json:"-"` Files []*File `json:"-"`
Components []MessageComponent `json:"components"` Components []MessageComponent `json:"components"`
Embeds []*MessageEmbed `json:"embeds,omitempty"` Embeds []*MessageEmbed `json:"embeds,omitempty"`
Attachments []*MessageAttachment `json:"attachments,omitempty"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
// Only MessageFlagsSuppressEmbeds and MessageFlagsEphemeral can be set. // Only MessageFlagsSuppressEmbeds and MessageFlagsEphemeral can be set.
// MessageFlagsEphemeral can only be set when using Followup Message Create endpoint. // MessageFlagsEphemeral can only be set when using Followup Message Create endpoint.
Flags MessageFlags `json:"flags,omitempty"` Flags MessageFlags `json:"flags,omitempty"`
// Name of the thread to create.
// NOTE: can only be set if the webhook channel is a forum.
ThreadName string `json:"thread_name,omitempty"`
} }
// WebhookEdit stores data for editing of a webhook message. // WebhookEdit stores data for editing of a webhook message.
@@ -46,5 +50,6 @@ type WebhookEdit struct {
Components *[]MessageComponent `json:"components,omitempty"` Components *[]MessageComponent `json:"components,omitempty"`
Embeds *[]*MessageEmbed `json:"embeds,omitempty"` Embeds *[]*MessageEmbed `json:"embeds,omitempty"`
Files []*File `json:"-"` Files []*File `json:"-"`
Attachments *[]*MessageAttachment `json:"attachments,omitempty"`
AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"` AllowedMentions *MessageAllowedMentions `json:"allowed_mentions,omitempty"`
} }

View File

@@ -389,6 +389,26 @@ func (s *Session) UpdateListeningStatus(name string) (err error) {
return s.UpdateStatusComplex(*newUpdateStatusData(0, ActivityTypeListening, name, "")) return s.UpdateStatusComplex(*newUpdateStatusData(0, ActivityTypeListening, name, ""))
} }
// UpdateCustomStatus is used to update the user's custom status.
// If state!="" then set the custom status.
// Else, set user to active and remove the custom status.
func (s *Session) UpdateCustomStatus(state string) (err error) {
data := UpdateStatusData{
Status: "online",
}
if state != "" {
// Discord requires a non-empty activity name, therefore we provide "Custom Status" as a placeholder.
data.Activities = []*Activity{{
Name: "Custom Status",
Type: ActivityTypeCustom,
State: state,
}}
}
return s.UpdateStatusComplex(data)
}
// UpdateStatusComplex allows for sending the raw status update data untouched by discordgo. // UpdateStatusComplex allows for sending the raw status update data untouched by discordgo.
func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) { func (s *Session) UpdateStatusComplex(usd UpdateStatusData) (err error) {
// The comment does say "untouched by discordgo", but we might need to lie a bit here. // The comment does say "untouched by discordgo", but we might need to lie a bit here.
@@ -862,17 +882,18 @@ func (s *Session) reconnect() {
// However, there seems to be cases where something "weird" // However, there seems to be cases where something "weird"
// happens. So we're doing this for now just to improve // happens. So we're doing this for now just to improve
// stability in those edge cases. // stability in those edge cases.
s.RLock() if s.ShouldReconnectVoiceOnSessionError {
defer s.RUnlock() s.RLock()
for _, v := range s.VoiceConnections { defer s.RUnlock()
for _, v := range s.VoiceConnections {
s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID) s.log(LogInformational, "reconnecting voice connection to guild %s", v.GuildID)
go v.reconnect() go v.reconnect()
// This is here just to prevent violently spamming the
// voice reconnects
time.Sleep(1 * time.Second)
// This is here just to prevent violently spamming the
// voice reconnects
time.Sleep(1 * time.Second)
}
} }
return return
} }

View File

@@ -1 +1,3 @@
dist/ dist/
.idea

View File

@@ -28,6 +28,12 @@ func MakeInstruction(opcode parser.Opcode, operands ...int) []byte {
n := uint16(o) n := uint16(o)
instruction[offset] = byte(n >> 8) instruction[offset] = byte(n >> 8)
instruction[offset+1] = byte(n) instruction[offset+1] = byte(n)
case 4:
n := uint32(o)
instruction[offset] = byte(n >> 24)
instruction[offset+1] = byte(n >> 16)
instruction[offset+2] = byte(n >> 8)
instruction[offset+3] = byte(n)
} }
offset += width offset += width
} }

View File

@@ -351,7 +351,7 @@ func (e *ImportExpr) End() Pos {
} }
func (e *ImportExpr) String() string { func (e *ImportExpr) String() string {
return `import("` + e.ModuleName + `")"` return `import("` + e.ModuleName + `")`
} }
// IndexExpr represents an index expression. // IndexExpr represents an index expression.

View File

@@ -106,10 +106,10 @@ var OpcodeOperands = [...][]int{
OpNotEqual: {}, OpNotEqual: {},
OpMinus: {}, OpMinus: {},
OpLNot: {}, OpLNot: {},
OpJumpFalsy: {2}, OpJumpFalsy: {4},
OpAndJump: {2}, OpAndJump: {4},
OpOrJump: {2}, OpOrJump: {4},
OpJump: {2}, OpJump: {4},
OpNull: {}, OpNull: {},
OpGetGlobal: {2}, OpGetGlobal: {2},
OpSetGlobal: {2}, OpSetGlobal: {2},
@@ -149,6 +149,8 @@ func ReadOperands(numOperands []int, ins []byte) (operands []int, offset int) {
operands = append(operands, int(ins[offset])) operands = append(operands, int(ins[offset]))
case 2: case 2:
operands = append(operands, int(ins[offset+1])|int(ins[offset])<<8) operands = append(operands, int(ins[offset+1])|int(ins[offset])<<8)
case 4:
operands = append(operands, int(ins[offset+3])|int(ins[offset+2])<<8|int(ins[offset+1])<<16|int(ins[offset])<<24)
} }
offset += width offset += width
} }

View File

@@ -6,11 +6,14 @@ import (
"io/ioutil" "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"runtime"
"github.com/d5/tengo/v2" "github.com/d5/tengo/v2"
) )
var osModule = map[string]tengo.Object{ var osModule = map[string]tengo.Object{
"platform": &tengo.String{Value: runtime.GOOS},
"arch": &tengo.String{Value: runtime.GOARCH},
"o_rdonly": &tengo.Int{Value: int64(os.O_RDONLY)}, "o_rdonly": &tengo.Int{Value: int64(os.O_RDONLY)},
"o_wronly": &tengo.Int{Value: int64(os.O_WRONLY)}, "o_wronly": &tengo.Int{Value: int64(os.O_WRONLY)},
"o_rdwr": &tengo.Int{Value: int64(os.O_RDWR)}, "o_rdwr": &tengo.Int{Value: int64(os.O_RDWR)},

17
vendor/github.com/d5/tengo/v2/vm.go generated vendored
View File

@@ -218,30 +218,30 @@ func (v *VM) run() {
return return
} }
case parser.OpJumpFalsy: case parser.OpJumpFalsy:
v.ip += 2 v.ip += 4
v.sp-- v.sp--
if v.stack[v.sp].IsFalsy() { if v.stack[v.sp].IsFalsy() {
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24
v.ip = pos - 1 v.ip = pos - 1
} }
case parser.OpAndJump: case parser.OpAndJump:
v.ip += 2 v.ip += 4
if v.stack[v.sp-1].IsFalsy() { if v.stack[v.sp-1].IsFalsy() {
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24
v.ip = pos - 1 v.ip = pos - 1
} else { } else {
v.sp-- v.sp--
} }
case parser.OpOrJump: case parser.OpOrJump:
v.ip += 2 v.ip += 4
if v.stack[v.sp-1].IsFalsy() { if v.stack[v.sp-1].IsFalsy() {
v.sp-- v.sp--
} else { } else {
pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 pos := int(v.curInsts[v.ip]) | int(v.curInsts[v.ip-1])<<8 | int(v.curInsts[v.ip-2])<<16 | int(v.curInsts[v.ip-3])<<24
v.ip = pos - 1 v.ip = pos - 1
} }
case parser.OpJump: case parser.OpJump:
pos := int(v.curInsts[v.ip+2]) | int(v.curInsts[v.ip+1])<<8 pos := int(v.curInsts[v.ip+4]) | int(v.curInsts[v.ip+3])<<8 | int(v.curInsts[v.ip+2])<<16 | int(v.curInsts[v.ip+1])<<24
v.ip = pos - 1 v.ip = pos - 1
case parser.OpSetGlobal: case parser.OpSetGlobal:
v.ip += 2 v.ip += 2
@@ -534,6 +534,9 @@ func (v *VM) run() {
} }
v.stack[v.sp] = val v.stack[v.sp] = val
v.sp++ v.sp++
default:
v.err = fmt.Errorf("not indexable: %s", left.TypeName())
return
} }
case parser.OpCall: case parser.OpCall:
numArgs := int(v.curInsts[v.ip+1]) numArgs := int(v.curInsts[v.ip+1])

View File

@@ -8,81 +8,37 @@ import (
"golang.org/x/net/html" "golang.org/x/net/html"
"golang.org/x/net/html/atom" "golang.org/x/net/html/atom"
"github.com/dyatlov/go-opengraph/opengraph/types/actor"
"github.com/dyatlov/go-opengraph/opengraph/types/article"
"github.com/dyatlov/go-opengraph/opengraph/types/audio"
"github.com/dyatlov/go-opengraph/opengraph/types/book"
"github.com/dyatlov/go-opengraph/opengraph/types/image"
"github.com/dyatlov/go-opengraph/opengraph/types/music"
"github.com/dyatlov/go-opengraph/opengraph/types/profile"
"github.com/dyatlov/go-opengraph/opengraph/types/video"
) )
// Image defines Open Graph Image type
type Image struct {
URL string `json:"url"`
SecureURL string `json:"secure_url"`
Type string `json:"type"`
Width uint64 `json:"width"`
Height uint64 `json:"height"`
draft bool `json:"-"`
}
// Video defines Open Graph Video type
type Video struct {
URL string `json:"url"`
SecureURL string `json:"secure_url"`
Type string `json:"type"`
Width uint64 `json:"width"`
Height uint64 `json:"height"`
draft bool `json:"-"`
}
// Audio defines Open Graph Audio Type
type Audio struct {
URL string `json:"url"`
SecureURL string `json:"secure_url"`
Type string `json:"type"`
draft bool `json:"-"`
}
// Article contain Open Graph Article structure
type Article struct {
PublishedTime *time.Time `json:"published_time"`
ModifiedTime *time.Time `json:"modified_time"`
ExpirationTime *time.Time `json:"expiration_time"`
Section string `json:"section"`
Tags []string `json:"tags"`
Authors []*Profile `json:"authors"`
}
// Profile contains Open Graph Profile structure
type Profile struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
Gender string `json:"gender"`
}
// Book contains Open Graph Book structure
type Book struct {
ISBN string `json:"isbn"`
ReleaseDate *time.Time `json:"release_date"`
Tags []string `json:"tags"`
Authors []*Profile `json:"authors"`
}
// OpenGraph contains facebook og data // OpenGraph contains facebook og data
type OpenGraph struct { type OpenGraph struct {
isArticle bool isArticle bool
isBook bool isBook bool
isProfile bool isProfile bool
Type string `json:"type"` Type string `json:"type"`
URL string `json:"url"` URL string `json:"url"`
Title string `json:"title"` Title string `json:"title"`
Description string `json:"description"` Description string `json:"description"`
Determiner string `json:"determiner"` Determiner string `json:"determiner"`
SiteName string `json:"site_name"` SiteName string `json:"site_name"`
Locale string `json:"locale"` Locale string `json:"locale"`
LocalesAlternate []string `json:"locales_alternate"` LocalesAlternate []string `json:"locales_alternate"`
Images []*Image `json:"images"` Images []*image.Image `json:"images"`
Audios []*Audio `json:"audios"` Audios []*audio.Audio `json:"audios"`
Videos []*Video `json:"videos"` Videos []*video.Video `json:"videos"`
Article *Article `json:"article,omitempty"` Article *article.Article `json:"article,omitempty"`
Book *Book `json:"book,omitempty"` Book *book.Book `json:"book,omitempty"`
Profile *Profile `json:"profile,omitempty"` Profile *profile.Profile `json:"profile,omitempty"`
Music *music.Music `json:"music,omitempty"`
} }
// NewOpenGraph returns new instance of Open Graph structure // NewOpenGraph returns new instance of Open Graph structure
@@ -137,21 +93,13 @@ func (og *OpenGraph) ensureHasVideo() {
if len(og.Videos) > 0 { if len(og.Videos) > 0 {
return return
} }
og.Videos = append(og.Videos, &Video{draft: true}) og.Videos = append(og.Videos, video.NewVideo())
} }
func (og *OpenGraph) ensureHasImage() { func (og *OpenGraph) ensureHasMusic() {
if len(og.Images) > 0 { if og.Music == nil {
return og.Music = music.NewMusic()
} }
og.Images = append(og.Images, &Image{draft: true})
}
func (og *OpenGraph) ensureHasAudio() {
if len(og.Audios) > 0 {
return
}
og.Audios = append(og.Audios, &Audio{draft: true})
} }
// ProcessMeta processes meta attributes and adds them to Open Graph structure if they are suitable for that // ProcessMeta processes meta attributes and adds them to Open Graph structure if they are suitable for that
@@ -182,73 +130,110 @@ func (og *OpenGraph) ProcessMeta(metaAttrs map[string]string) {
case "og:locale:alternate": case "og:locale:alternate":
og.LocalesAlternate = append(og.LocalesAlternate, metaAttrs["content"]) og.LocalesAlternate = append(og.LocalesAlternate, metaAttrs["content"])
case "og:audio": case "og:audio":
if len(og.Audios)>0 && og.Audios[len(og.Audios)-1].draft { og.Audios = audio.AddUrl(og.Audios, metaAttrs["content"])
og.Audios[len(og.Audios)-1].URL = metaAttrs["content"]
og.Audios[len(og.Audios)-1].draft = false
} else {
og.Audios = append(og.Audios, &Audio{URL: metaAttrs["content"]})
}
case "og:audio:secure_url": case "og:audio:secure_url":
og.ensureHasAudio() og.Audios = audio.AddSecureUrl(og.Audios, metaAttrs["content"])
og.Audios[len(og.Audios)-1].SecureURL = metaAttrs["content"]
case "og:audio:type": case "og:audio:type":
og.ensureHasAudio() og.Audios = audio.AddType(og.Audios, metaAttrs["content"])
og.Audios[len(og.Audios)-1].Type = metaAttrs["content"]
case "og:image": case "og:image":
if len(og.Images)>0 && og.Images[len(og.Images)-1].draft { og.Images = image.AddURL(og.Images, metaAttrs["content"])
og.Images[len(og.Images)-1].URL = metaAttrs["content"]
og.Images[len(og.Images)-1].draft = false
} else {
og.Images = append(og.Images, &Image{URL: metaAttrs["content"]})
}
case "og:image:url": case "og:image:url":
og.ensureHasImage() og.Images = image.AddURL(og.Images, metaAttrs["content"])
og.Images[len(og.Images)-1].URL = metaAttrs["content"]
case "og:image:secure_url": case "og:image:secure_url":
og.ensureHasImage() og.Images = image.AddSecureURL(og.Images, metaAttrs["content"])
og.Images[len(og.Images)-1].SecureURL = metaAttrs["content"]
case "og:image:type": case "og:image:type":
og.ensureHasImage() og.Images = image.AddType(og.Images, metaAttrs["content"])
og.Images[len(og.Images)-1].Type = metaAttrs["content"]
case "og:image:width": case "og:image:width":
w, err := strconv.ParseUint(metaAttrs["content"], 10, 64) w, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
if err == nil { if err == nil {
og.ensureHasImage() og.Images = image.AddWidth(og.Images, w)
og.Images[len(og.Images)-1].Width = w
} }
case "og:image:height": case "og:image:height":
h, err := strconv.ParseUint(metaAttrs["content"], 10, 64) h, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
if err == nil { if err == nil {
og.ensureHasImage() og.Images = image.AddHeight(og.Images, h)
og.Images[len(og.Images)-1].Height = h
} }
case "og:video": case "og:video":
if len(og.Videos)>0 && og.Videos[len(og.Videos)-1].draft { og.Videos = video.AddURL(og.Videos, metaAttrs["content"])
og.Videos[len(og.Videos)-1].URL = metaAttrs["content"] case "og:video:tag":
og.Videos[len(og.Videos)-1].draft = false og.Videos = video.AddTag(og.Videos, metaAttrs["content"])
} else { case "og:video:duration":
og.Videos = append(og.Videos, &Video{URL: metaAttrs["content"]}) if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil {
og.Videos = video.AddDuration(og.Videos, i)
}
case "og:video:release_date":
if t, err := time.Parse(time.RFC3339, metaAttrs["content"]); err == nil {
og.Videos = video.AddReleaseDate(og.Videos, &t)
} }
case "og:video:url": case "og:video:url":
og.ensureHasVideo() og.Videos = video.AddURL(og.Videos, metaAttrs["content"])
og.Videos[len(og.Videos)-1].URL = metaAttrs["content"]
case "og:video:secure_url": case "og:video:secure_url":
og.ensureHasVideo() og.Videos = video.AddSecureURL(og.Videos, metaAttrs["content"])
og.Videos[len(og.Videos)-1].SecureURL = metaAttrs["content"]
case "og:video:type": case "og:video:type":
og.ensureHasVideo() og.Videos = video.AddTag(og.Videos, metaAttrs["content"])
og.Videos[len(og.Videos)-1].Type = metaAttrs["content"]
case "og:video:width": case "og:video:width":
w, err := strconv.ParseUint(metaAttrs["content"], 10, 64) w, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
if err == nil { if err == nil {
og.ensureHasVideo() og.Videos = video.AddWidth(og.Videos, w)
og.Videos[len(og.Videos)-1].Width = w
} }
case "og:video:height": case "og:video:height":
h, err := strconv.ParseUint(metaAttrs["content"], 10, 64) h, err := strconv.ParseUint(metaAttrs["content"], 10, 64)
if err == nil { if err == nil {
og.ensureHasVideo() og.Videos = video.AddHeight(og.Videos, h)
og.Videos[len(og.Videos)-1].Height = h }
case "og:video:actor":
og.ensureHasVideo()
og.Videos[len(og.Videos)-1].Actors = actor.AddProfile(og.Videos[len(og.Videos)-1].Actors, metaAttrs["content"])
case "og:video:actor:role":
og.ensureHasVideo()
og.Videos[len(og.Videos)-1].Actors = actor.AddRole(og.Videos[len(og.Videos)-1].Actors, metaAttrs["content"])
case "og:video:director":
og.ensureHasVideo()
og.Videos[len(og.Videos)-1].Directors = append(og.Videos[len(og.Videos)-1].Directors, metaAttrs["content"])
case "og:video:writer":
og.ensureHasVideo()
og.Videos[len(og.Videos)-1].Writers = append(og.Videos[len(og.Videos)-1].Writers, metaAttrs["content"])
case "og:music:duration":
og.ensureHasMusic()
if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil {
og.Music.Duration = i
}
case "og:music:release_date":
og.ensureHasMusic()
if t, err := time.Parse(time.RFC3339, metaAttrs["content"]); err == nil {
og.Music.ReleaseDate = &t
}
case "og:music:album":
og.ensureHasMusic()
og.Music.Album.URL = metaAttrs["content"]
case "og:music:album:disc":
og.ensureHasMusic()
if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil {
og.Music.Album.Disc = i
}
case "og:music:album:track":
og.ensureHasMusic()
if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil {
og.Music.Album.Track = i
}
case "og:music:musician":
og.ensureHasMusic()
og.Music.Musicians = append(og.Music.Musicians, metaAttrs["content"])
case "og:music:creator":
og.ensureHasMusic()
og.Music.Creators = append(og.Music.Creators, metaAttrs["content"])
case "og:music:song":
og.ensureHasMusic()
og.Music.AddSongUrl(metaAttrs["content"])
case "og:music:disc":
og.ensureHasMusic()
if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil {
og.Music.AddSongDisc(i)
}
case "og:music:track":
og.ensureHasMusic()
if i, err := strconv.ParseUint(metaAttrs["content"], 10, 64); err == nil {
og.Music.AddSongTrack(i)
} }
default: default:
if og.isArticle { if og.isArticle {
@@ -263,100 +248,64 @@ func (og *OpenGraph) ProcessMeta(metaAttrs map[string]string) {
func (og *OpenGraph) processArticleMeta(metaAttrs map[string]string) { func (og *OpenGraph) processArticleMeta(metaAttrs map[string]string) {
if og.Article == nil { if og.Article == nil {
og.Article = &Article{} og.Article = &article.Article{}
} }
switch metaAttrs["property"] { switch metaAttrs["property"] {
case "article:published_time": case "og:article:published_time":
t, err := time.Parse(time.RFC3339, metaAttrs["content"]) t, err := time.Parse(time.RFC3339, metaAttrs["content"])
if err == nil { if err == nil {
og.Article.PublishedTime = &t og.Article.PublishedTime = &t
} }
case "article:modified_time": case "og:article:modified_time":
t, err := time.Parse(time.RFC3339, metaAttrs["content"]) t, err := time.Parse(time.RFC3339, metaAttrs["content"])
if err == nil { if err == nil {
og.Article.ModifiedTime = &t og.Article.ModifiedTime = &t
} }
case "article:expiration_time": case "og:article:expiration_time":
t, err := time.Parse(time.RFC3339, metaAttrs["content"]) t, err := time.Parse(time.RFC3339, metaAttrs["content"])
if err == nil { if err == nil {
og.Article.ExpirationTime = &t og.Article.ExpirationTime = &t
} }
case "article:section": case "og:article:section":
og.Article.Section = metaAttrs["content"] og.Article.Section = metaAttrs["content"]
case "article:tag": case "og:article:tag":
og.Article.Tags = append(og.Article.Tags, metaAttrs["content"]) og.Article.Tags = append(og.Article.Tags, metaAttrs["content"])
case "article:author:first_name": case "og:article:author":
if len(og.Article.Authors) == 0 { og.Article.Authors = append(og.Article.Authors, metaAttrs["content"])
og.Article.Authors = append(og.Article.Authors, &Profile{})
}
og.Article.Authors[len(og.Article.Authors)-1].FirstName = metaAttrs["content"]
case "article:author:last_name":
if len(og.Article.Authors) == 0 {
og.Article.Authors = append(og.Article.Authors, &Profile{})
}
og.Article.Authors[len(og.Article.Authors)-1].LastName = metaAttrs["content"]
case "article:author:username":
if len(og.Article.Authors) == 0 {
og.Article.Authors = append(og.Article.Authors, &Profile{})
}
og.Article.Authors[len(og.Article.Authors)-1].Username = metaAttrs["content"]
case "article:author:gender":
if len(og.Article.Authors) == 0 {
og.Article.Authors = append(og.Article.Authors, &Profile{})
}
og.Article.Authors[len(og.Article.Authors)-1].Gender = metaAttrs["content"]
} }
} }
func (og *OpenGraph) processBookMeta(metaAttrs map[string]string) { func (og *OpenGraph) processBookMeta(metaAttrs map[string]string) {
if og.Book == nil { if og.Book == nil {
og.Book = &Book{} og.Book = &book.Book{}
} }
switch metaAttrs["property"] { switch metaAttrs["property"] {
case "book:release_date": case "og:book:release_date":
t, err := time.Parse(time.RFC3339, metaAttrs["content"]) t, err := time.Parse(time.RFC3339, metaAttrs["content"])
if err == nil { if err == nil {
og.Book.ReleaseDate = &t og.Book.ReleaseDate = &t
} }
case "book:isbn": case "og:book:isbn":
og.Book.ISBN = metaAttrs["content"] og.Book.ISBN = metaAttrs["content"]
case "book:tag": case "og:book:tag":
og.Book.Tags = append(og.Book.Tags, metaAttrs["content"]) og.Book.Tags = append(og.Book.Tags, metaAttrs["content"])
case "book:author:first_name": case "og:book:author":
if len(og.Book.Authors) == 0 { og.Book.Authors = append(og.Book.Authors, metaAttrs["content"])
og.Book.Authors = append(og.Book.Authors, &Profile{})
}
og.Book.Authors[len(og.Book.Authors)-1].FirstName = metaAttrs["content"]
case "book:author:last_name":
if len(og.Book.Authors) == 0 {
og.Book.Authors = append(og.Book.Authors, &Profile{})
}
og.Book.Authors[len(og.Book.Authors)-1].LastName = metaAttrs["content"]
case "book:author:username":
if len(og.Book.Authors) == 0 {
og.Book.Authors = append(og.Book.Authors, &Profile{})
}
og.Book.Authors[len(og.Book.Authors)-1].Username = metaAttrs["content"]
case "book:author:gender":
if len(og.Book.Authors) == 0 {
og.Book.Authors = append(og.Book.Authors, &Profile{})
}
og.Book.Authors[len(og.Book.Authors)-1].Gender = metaAttrs["content"]
} }
} }
func (og *OpenGraph) processProfileMeta(metaAttrs map[string]string) { func (og *OpenGraph) processProfileMeta(metaAttrs map[string]string) {
if og.Profile == nil { if og.Profile == nil {
og.Profile = &Profile{} og.Profile = &profile.Profile{}
} }
switch metaAttrs["property"] { switch metaAttrs["property"] {
case "profile:first_name": case "og:profile:first_name":
og.Profile.FirstName = metaAttrs["content"] og.Profile.FirstName = metaAttrs["content"]
case "profile:last_name": case "og:profile:last_name":
og.Profile.LastName = metaAttrs["content"] og.Profile.LastName = metaAttrs["content"]
case "profile:username": case "og:profile:username":
og.Profile.Username = metaAttrs["content"] og.Profile.Username = metaAttrs["content"]
case "profile:gender": case "og:profile:gender":
og.Profile.Gender = metaAttrs["content"] og.Profile.Gender = metaAttrs["content"]
} }
} }

View File

@@ -0,0 +1,27 @@
package actor
// Actor contain Open Graph Actor structure
type Actor struct {
Profile string `json:"profile"`
Role string `json:"role"`
}
func NewActor() *Actor {
return &Actor{}
}
func AddProfile(actors []*Actor, v string) []*Actor {
if len(actors) == 0 || actors[len(actors)-1].Profile != "" {
actors = append(actors, &Actor{})
}
actors[len(actors)-1].Profile = v
return actors
}
func AddRole(actors []*Actor, v string) []*Actor {
if len(actors) == 0 || actors[len(actors)-1].Role != "" {
actors = append(actors, &Actor{})
}
actors[len(actors)-1].Role = v
return actors
}

View File

@@ -0,0 +1,15 @@
package article
import (
"time"
)
// Article contain Open Graph Article structure
type Article struct {
PublishedTime *time.Time `json:"published_time"`
ModifiedTime *time.Time `json:"modified_time"`
ExpirationTime *time.Time `json:"expiration_time"`
Section string `json:"section"`
Tags []string `json:"tags"`
Authors []string `json:"authors"`
}

View File

@@ -0,0 +1,36 @@
package audio
// Audio defines Open Graph Audio Type
type Audio struct {
URL string `json:"url"`
SecureURL string `json:"secure_url"`
Type string `json:"type"`
}
func NewAudio() *Audio {
return &Audio{}
}
func AddUrl(audios []*Audio, v string) []*Audio {
if len(audios) == 0 || audios[len(audios)-1].URL != "" {
audios = append(audios, &Audio{})
}
audios[len(audios)-1].URL = v
return audios
}
func AddSecureUrl(audios []*Audio, v string) []*Audio {
if len(audios) == 0 || audios[len(audios)-1].SecureURL != "" {
audios = append(audios, &Audio{})
}
audios[len(audios)-1].SecureURL = v
return audios
}
func AddType(audios []*Audio, v string) []*Audio {
if len(audios) == 0 || audios[len(audios)-1].Type != "" {
audios = append(audios, &Audio{})
}
audios[len(audios)-1].Type = v
return audios
}

View File

@@ -0,0 +1,13 @@
package book
import (
"time"
)
// Book contains Open Graph Book structure
type Book struct {
ISBN string `json:"isbn"`
ReleaseDate *time.Time `json:"release_date"`
Tags []string `json:"tags"`
Authors []string `json:"authors"`
}

View File

@@ -0,0 +1,53 @@
package image
// Image defines Open Graph Image type
type Image struct {
URL string `json:"url"`
SecureURL string `json:"secure_url"`
Type string `json:"type"`
Width uint64 `json:"width"`
Height uint64 `json:"height"`
}
func NewImage() *Image {
return &Image{}
}
func ensureHasImage(images []*Image) []*Image {
if len(images) == 0 {
images = append(images, NewImage())
}
return images
}
func AddURL(images []*Image, v string) []*Image {
if len(images) == 0 || (images[len(images)-1].URL != "" && images[len(images)-1].URL != v) {
images = append(images, NewImage())
}
images[len(images)-1].URL = v
return images
}
func AddSecureURL(images []*Image, v string) []*Image {
images = ensureHasImage(images)
images[len(images)-1].SecureURL = v
return images
}
func AddType(images []*Image, v string) []*Image {
images = ensureHasImage(images)
images[len(images)-1].Type = v
return images
}
func AddWidth(images []*Image, v uint64) []*Image {
images = ensureHasImage(images)
images[len(images)-1].Width = v
return images
}
func AddHeight(images []*Image, v uint64) []*Image {
images = ensureHasImage(images)
images[len(images)-1].Height = v
return images
}

View File

@@ -0,0 +1,52 @@
package music
import (
"time"
)
// Music defines Open Graph Music type
type Music struct {
Musicians []string `json:"musicians,omitempty"`
Creators []string `json:"creators,omitempty"`
Duration uint64 `json:"duration,omitempty"`
ReleaseDate *time.Time `json:"release_date,omitempty"`
Album *Album `json:"album"`
Songs []*Song `json:"songs"`
}
type Album struct {
URL string `json:"url,omitempty"`
Disc uint64 `json:"disc,omitempty"`
Track uint64 `json:"track,omitempty"`
}
type Song struct {
URL string `json:"url,omitempty"`
Disc uint64 `json:"disc,omitempty"`
Track uint64 `json:"track,omitempty"`
}
func NewMusic() *Music {
return &Music{Album: &Album{}}
}
func (m *Music) AddSongUrl(v string) {
if len(m.Songs) == 0 || m.Songs[len(m.Songs)-1].URL != "" {
m.Songs = append(m.Songs, &Song{})
}
m.Songs[len(m.Songs)-1].URL = v
}
func (m *Music) AddSongDisc(v uint64) {
if len(m.Songs) == 0 {
m.Songs = append(m.Songs, &Song{})
}
m.Songs[len(m.Songs)-1].Disc = v
}
func (m *Music) AddSongTrack(v uint64) {
if len(m.Songs) == 0 {
m.Songs = append(m.Songs, &Song{})
}
m.Songs[len(m.Songs)-1].Track = v
}

View File

@@ -0,0 +1,59 @@
package profile
import "strings"
// Profile contain Open Graph Profile structure
type Profile struct {
FirstName string `json:"first_name"`
LastName string `json:"last_name"`
Username string `json:"username"`
Gender string `json:"gender"`
}
func NewProfile() *Profile {
return &Profile{}
}
func AddBasicProfile(profiles []*Profile, v string) []*Profile {
parts := strings.SplitN(v, " ", 2)
if len(profiles) == 0 || profiles[len(profiles)-1].FirstName != "" {
profiles = append(profiles, &Profile{})
}
profiles[len(profiles)-1].FirstName = parts[0]
if len(parts) > 1 {
profiles[len(profiles)-1].LastName = parts[1]
}
return profiles
}
func AddFirstName(profiles []*Profile, v string) []*Profile {
if len(profiles) == 0 || profiles[len(profiles)-1].FirstName != "" {
profiles = append(profiles, &Profile{})
}
profiles[len(profiles)-1].FirstName = v
return profiles
}
func AddLastName(profiles []*Profile, v string) []*Profile {
if len(profiles) == 0 || profiles[len(profiles)-1].LastName != "" {
profiles = append(profiles, &Profile{})
}
profiles[len(profiles)-1].LastName = v
return profiles
}
func AddUsername(profiles []*Profile, v string) []*Profile {
if len(profiles) == 0 || profiles[len(profiles)-1].Username != "" {
profiles = append(profiles, &Profile{})
}
profiles[len(profiles)-1].Username = v
return profiles
}
func AddGender(profiles []*Profile, v string) []*Profile {
if len(profiles) == 0 || profiles[len(profiles)-1].Gender != "" {
profiles = append(profiles, &Profile{})
}
profiles[len(profiles)-1].Gender = v
return profiles
}

View File

@@ -0,0 +1,83 @@
package video
import (
"time"
"github.com/dyatlov/go-opengraph/opengraph/types/actor"
)
// Video defines Open Graph Video type
type Video struct {
URL string `json:"url"`
SecureURL string `json:"secure_url"`
Type string `json:"type"`
Width uint64 `json:"width"`
Height uint64 `json:"height"`
Actors []*actor.Actor `json:"actors,omitempty"`
Directors []string `json:"directors,omitempty"`
Writers []string `json:"writers,omitempty"`
Duration uint64 `json:"duration,omitempty"`
ReleaseDate *time.Time `json:"release_date,omitempty"`
Tags []string `json:"tags,omitempty"`
}
func NewVideo() *Video {
return &Video{}
}
func ensureHasVideo(videos []*Video) []*Video {
if len(videos) == 0 {
videos = append(videos, NewVideo())
}
return videos
}
func AddURL(videos []*Video, v string) []*Video {
if len(videos) == 0 || (videos[len(videos)-1].URL != "" && videos[len(videos)-1].URL != v) {
videos = append(videos, NewVideo())
}
videos[len(videos)-1].URL = v
return videos
}
func AddTag(videos []*Video, v string) []*Video {
videos = ensureHasVideo(videos)
videos[len(videos)-1].Tags = append(videos[len(videos)-1].Tags, v)
return videos
}
func AddDuration(videos []*Video, v uint64) []*Video {
videos = ensureHasVideo(videos)
videos[len(videos)-1].Duration = v
return videos
}
func AddReleaseDate(videos []*Video, v *time.Time) []*Video {
videos = ensureHasVideo(videos)
videos[len(videos)-1].ReleaseDate = v
return videos
}
func AddSecureURL(videos []*Video, v string) []*Video {
videos = ensureHasVideo(videos)
videos[len(videos)-1].SecureURL = v
return videos
}
func AddType(videos []*Video, v string) []*Video {
videos = ensureHasVideo(videos)
videos[len(videos)-1].Type = v
return videos
}
func AddWidth(videos []*Video, v uint64) []*Video {
videos = ensureHasVideo(videos)
videos[len(videos)-1].Width = v
return videos
}
func AddHeight(videos []*Video, v uint64) []*Video {
videos = ensureHasVideo(videos)
videos[len(videos)-1].Height = v
return videos
}

20
vendor/github.com/fatih/color/LICENSE.md generated vendored Normal file
View File

@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Fatih Arslan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

176
vendor/github.com/fatih/color/README.md generated vendored Normal file
View File

@@ -0,0 +1,176 @@
# color [![](https://github.com/fatih/color/workflows/build/badge.svg)](https://github.com/fatih/color/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/color)](https://pkg.go.dev/github.com/fatih/color)
Color lets you use colorized outputs in terms of [ANSI Escape
Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It
has support for Windows too! The API can be used in several ways, pick one that
suits you.
![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg)
## Install
```bash
go get github.com/fatih/color
```
## Examples
### Standard colors
```go
// Print with default helper functions
color.Cyan("Prints text in cyan.")
// A newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// These are using the default foreground colors
color.Red("We have red")
color.Magenta("And many others ..")
```
### Mix and reuse colors
```go
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with white background.")
```
### Use your own output (io.Writer)
```go
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(writer, "This will print text in blue.")
```
### Custom print functions (PrintFunc)
```go
// Create a custom print function for convenience
red := color.New(color.FgRed).PrintfFunc()
red("Warning")
red("Error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("Don't forget this...")
```
### Custom fprint functions (FprintFunc)
```go
blue := color.New(color.FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, "Don't forget this...")
```
### Insert into noncolor strings (SprintFunc)
```go
// Create SprintXxx functions to mix strings with other non-colorized strings:
yellow := color.New(color.FgYellow).SprintFunc()
red := color.New(color.FgRed).SprintFunc()
fmt.Printf("This is a %s and this is %s.\n", yellow("warning"), red("error"))
info := color.New(color.FgWhite, color.BgGreen).SprintFunc()
fmt.Printf("This %s rocks!\n", info("package"))
// Use helper functions
fmt.Println("This", color.RedString("warning"), "should be not neglected.")
fmt.Printf("%v %v\n", color.GreenString("Info:"), "an important message.")
// Windows supported too! Just don't forget to change the output to color.Output
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
```
### Plug into existing code
```go
// Use handy standard colors
color.Set(color.FgYellow)
fmt.Println("Existing text will now be in yellow")
fmt.Printf("This one %s\n", "too")
color.Unset() // Don't forget to unset
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // Use it in your function
fmt.Println("All text will now be bold magenta.")
```
### Disable/Enable color
There might be a case where you want to explicitly disable/enable color output. the
`go-isatty` package will automatically disable color output for non-tty output streams
(for example if the output were piped directly to `less`).
The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment
variable is set to a non-empty string.
`Color` has support to disable/enable colors programmatically both globally and
for single color definitions. For example suppose you have a CLI app and a
`-no-color` bool flag. You can easily disable the color output with:
```go
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor {
color.NoColor = true // disables colorized output
}
```
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
```go
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
```
## GitHub Actions
To output color in GitHub Actions (or other CI systems that support ANSI colors), make sure to set `color.NoColor = false` so that it bypasses the check for non-tty output streams.
## Todo
* Save/Return previous values
* Evaluate fmt.Formatter interface
## Credits
* [Fatih Arslan](https://github.com/fatih)
* Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable)
## License
The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details

650
vendor/github.com/fatih/color/color.go generated vendored Normal file
View File

@@ -0,0 +1,650 @@
package color
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
)
var (
// NoColor defines if the output is colorized or not. It's dynamically set to
// false or true based on the stdout's file descriptor referring to a terminal
// or not. It's also set to true if the NO_COLOR environment variable is
// set (regardless of its value). This is a global option and affects all
// colors. For more control over each color block use the methods
// DisableColor() individually.
NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
// Output defines the standard output of the print functions. By default,
// os.Stdout is used.
Output = colorable.NewColorableStdout()
// Error defines a color supporting writer for os.Stderr.
Error = colorable.NewColorableStderr()
// colorsCache is used to reduce the count of created Color objects and
// allows to reuse already created objects with required Attribute.
colorsCache = make(map[Attribute]*Color)
colorsCacheMu sync.Mutex // protects colorsCache
)
// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string.
func noColorIsSet() bool {
return os.Getenv("NO_COLOR") != ""
}
// Color defines a custom color object which is defined by SGR parameters.
type Color struct {
params []Attribute
noColor *bool
}
// Attribute defines a single SGR Code
type Attribute int
const escape = "\x1b"
// Base attributes
const (
Reset Attribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
ReverseVideo
Concealed
CrossedOut
)
const (
ResetBold Attribute = iota + 22
ResetItalic
ResetUnderline
ResetBlinking
_
ResetReversed
ResetConcealed
ResetCrossedOut
)
var mapResetAttributes map[Attribute]Attribute = map[Attribute]Attribute{
Bold: ResetBold,
Faint: ResetBold,
Italic: ResetItalic,
Underline: ResetUnderline,
BlinkSlow: ResetBlinking,
BlinkRapid: ResetBlinking,
ReverseVideo: ResetReversed,
Concealed: ResetConcealed,
CrossedOut: ResetCrossedOut,
}
// Foreground text colors
const (
FgBlack Attribute = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
)
// Foreground Hi-Intensity text colors
const (
FgHiBlack Attribute = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
// Background text colors
const (
BgBlack Attribute = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
)
// Background Hi-Intensity text colors
const (
BgHiBlack Attribute = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)
// New returns a newly created color object.
func New(value ...Attribute) *Color {
c := &Color{
params: make([]Attribute, 0),
}
if noColorIsSet() {
c.noColor = boolPtr(true)
}
c.Add(value...)
return c
}
// Set sets the given parameters immediately. It will change the color of
// output with the given SGR parameters until color.Unset() is called.
func Set(p ...Attribute) *Color {
c := New(p...)
c.Set()
return c
}
// Unset resets all escape attributes and clears the output. Usually should
// be called after Set().
func Unset() {
if NoColor {
return
}
fmt.Fprintf(Output, "%s[%dm", escape, Reset)
}
// Set sets the SGR sequence.
func (c *Color) Set() *Color {
if c.isNoColorSet() {
return c
}
fmt.Fprint(Output, c.format())
return c
}
func (c *Color) unset() {
if c.isNoColorSet() {
return
}
Unset()
}
// SetWriter is used to set the SGR sequence with the given io.Writer. This is
// a low-level function, and users should use the higher-level functions, such
// as color.Fprint, color.Print, etc.
func (c *Color) SetWriter(w io.Writer) *Color {
if c.isNoColorSet() {
return c
}
fmt.Fprint(w, c.format())
return c
}
// UnsetWriter resets all escape attributes and clears the output with the give
// io.Writer. Usually should be called after SetWriter().
func (c *Color) UnsetWriter(w io.Writer) {
if c.isNoColorSet() {
return
}
if NoColor {
return
}
fmt.Fprintf(w, "%s[%dm", escape, Reset)
}
// Add is used to chain SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: Add(color.FgRed, color.Underline).
func (c *Color) Add(value ...Attribute) *Color {
c.params = append(c.params, value...)
return c
}
// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
c.SetWriter(w)
defer c.UnsetWriter(w)
return fmt.Fprint(w, a...)
}
// Print formats using the default formats for its operands and writes to
// standard output. Spaces are added between operands when neither is a
// string. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Print(a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprint(Output, a...)
}
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
c.SetWriter(w)
defer c.UnsetWriter(w)
return fmt.Fprintf(w, format, a...)
}
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
// This is the standard fmt.Printf() method wrapped with the given color.
func (c *Color) Printf(format string, a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprintf(Output, format, a...)
}
// Fprintln formats using the default formats for its operands and writes to w.
// Spaces are always added between operands and a newline is appended.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, c.wrap(fmt.Sprint(a...)))
}
// Println formats using the default formats for its operands and writes to
// standard output. Spaces are always added between operands and a newline is
// appended. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Println(a ...interface{}) (n int, err error) {
return fmt.Fprintln(Output, c.wrap(fmt.Sprint(a...)))
}
// Sprint is just like Print, but returns a string instead of printing it.
func (c *Color) Sprint(a ...interface{}) string {
return c.wrap(fmt.Sprint(a...))
}
// Sprintln is just like Println, but returns a string instead of printing it.
func (c *Color) Sprintln(a ...interface{}) string {
return fmt.Sprintln(c.Sprint(a...))
}
// Sprintf is just like Printf, but returns a string instead of printing it.
func (c *Color) Sprintf(format string, a ...interface{}) string {
return c.wrap(fmt.Sprintf(format, a...))
}
// FprintFunc returns a new function that prints the passed arguments as
// colorized with color.Fprint().
func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) {
return func(w io.Writer, a ...interface{}) {
c.Fprint(w, a...)
}
}
// PrintFunc returns a new function that prints the passed arguments as
// colorized with color.Print().
func (c *Color) PrintFunc() func(a ...interface{}) {
return func(a ...interface{}) {
c.Print(a...)
}
}
// FprintfFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintf().
func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) {
return func(w io.Writer, format string, a ...interface{}) {
c.Fprintf(w, format, a...)
}
}
// PrintfFunc returns a new function that prints the passed arguments as
// colorized with color.Printf().
func (c *Color) PrintfFunc() func(format string, a ...interface{}) {
return func(format string, a ...interface{}) {
c.Printf(format, a...)
}
}
// FprintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintln().
func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) {
return func(w io.Writer, a ...interface{}) {
c.Fprintln(w, a...)
}
}
// PrintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Println().
func (c *Color) PrintlnFunc() func(a ...interface{}) {
return func(a ...interface{}) {
c.Println(a...)
}
}
// SprintFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprint(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output, example:
//
// put := New(FgYellow).SprintFunc()
// fmt.Fprintf(color.Output, "This is a %s", put("warning"))
func (c *Color) SprintFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return c.wrap(fmt.Sprint(a...))
}
}
// SprintfFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintf(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintfFunc() func(format string, a ...interface{}) string {
return func(format string, a ...interface{}) string {
return c.wrap(fmt.Sprintf(format, a...))
}
}
// SprintlnFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintln(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintlnFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return fmt.Sprintln(c.Sprint(a...))
}
}
// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m"
// an example output might be: "1;36" -> bold cyan
func (c *Color) sequence() string {
format := make([]string, len(c.params))
for i, v := range c.params {
format[i] = strconv.Itoa(int(v))
}
return strings.Join(format, ";")
}
// wrap wraps the s string with the colors attributes. The string is ready to
// be printed.
func (c *Color) wrap(s string) string {
if c.isNoColorSet() {
return s
}
return c.format() + s + c.unformat()
}
func (c *Color) format() string {
return fmt.Sprintf("%s[%sm", escape, c.sequence())
}
func (c *Color) unformat() string {
//return fmt.Sprintf("%s[%dm", escape, Reset)
//for each element in sequence let's use the speficic reset escape, ou the generic one if not found
format := make([]string, len(c.params))
for i, v := range c.params {
format[i] = strconv.Itoa(int(Reset))
ra, ok := mapResetAttributes[v]
if ok {
format[i] = strconv.Itoa(int(ra))
}
}
return fmt.Sprintf("%s[%sm", escape, strings.Join(format, ";"))
}
// DisableColor disables the color output. Useful to not change any existing
// code and still being able to output. Can be used for flags like
// "--no-color". To enable back use EnableColor() method.
func (c *Color) DisableColor() {
c.noColor = boolPtr(true)
}
// EnableColor enables the color output. Use it in conjunction with
// DisableColor(). Otherwise, this method has no side effects.
func (c *Color) EnableColor() {
c.noColor = boolPtr(false)
}
func (c *Color) isNoColorSet() bool {
// check first if we have user set action
if c.noColor != nil {
return *c.noColor
}
// if not return the global option, which is disabled by default
return NoColor
}
// Equals returns a boolean value indicating whether two colors are equal.
func (c *Color) Equals(c2 *Color) bool {
if c == nil && c2 == nil {
return true
}
if c == nil || c2 == nil {
return false
}
if len(c.params) != len(c2.params) {
return false
}
for _, attr := range c.params {
if !c2.attrExists(attr) {
return false
}
}
return true
}
func (c *Color) attrExists(a Attribute) bool {
for _, attr := range c.params {
if attr == a {
return true
}
}
return false
}
func boolPtr(v bool) *bool {
return &v
}
func getCachedColor(p Attribute) *Color {
colorsCacheMu.Lock()
defer colorsCacheMu.Unlock()
c, ok := colorsCache[p]
if !ok {
c = New(p)
colorsCache[p] = c
}
return c
}
func colorPrint(format string, p Attribute, a ...interface{}) {
c := getCachedColor(p)
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
if len(a) == 0 {
c.Print(format)
} else {
c.Printf(format, a...)
}
}
func colorString(format string, p Attribute, a ...interface{}) string {
c := getCachedColor(p)
if len(a) == 0 {
return c.SprintFunc()(format)
}
return c.SprintfFunc()(format, a...)
}
// Black is a convenient helper function to print with black foreground. A
// newline is appended to format by default.
func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) }
// Red is a convenient helper function to print with red foreground. A
// newline is appended to format by default.
func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) }
// Green is a convenient helper function to print with green foreground. A
// newline is appended to format by default.
func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) }
// Yellow is a convenient helper function to print with yellow foreground.
// A newline is appended to format by default.
func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) }
// Blue is a convenient helper function to print with blue foreground. A
// newline is appended to format by default.
func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) }
// Magenta is a convenient helper function to print with magenta foreground.
// A newline is appended to format by default.
func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) }
// Cyan is a convenient helper function to print with cyan foreground. A
// newline is appended to format by default.
func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) }
// White is a convenient helper function to print with white foreground. A
// newline is appended to format by default.
func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) }
// BlackString is a convenient helper function to return a string with black
// foreground.
func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) }
// RedString is a convenient helper function to return a string with red
// foreground.
func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) }
// GreenString is a convenient helper function to return a string with green
// foreground.
func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) }
// YellowString is a convenient helper function to return a string with yellow
// foreground.
func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) }
// BlueString is a convenient helper function to return a string with blue
// foreground.
func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) }
// MagentaString is a convenient helper function to return a string with magenta
// foreground.
func MagentaString(format string, a ...interface{}) string {
return colorString(format, FgMagenta, a...)
}
// CyanString is a convenient helper function to return a string with cyan
// foreground.
func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) }
// WhiteString is a convenient helper function to return a string with white
// foreground.
func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) }
// HiBlack is a convenient helper function to print with hi-intensity black foreground. A
// newline is appended to format by default.
func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) }
// HiRed is a convenient helper function to print with hi-intensity red foreground. A
// newline is appended to format by default.
func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) }
// HiGreen is a convenient helper function to print with hi-intensity green foreground. A
// newline is appended to format by default.
func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) }
// HiYellow is a convenient helper function to print with hi-intensity yellow foreground.
// A newline is appended to format by default.
func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) }
// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A
// newline is appended to format by default.
func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) }
// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground.
// A newline is appended to format by default.
func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) }
// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A
// newline is appended to format by default.
func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) }
// HiWhite is a convenient helper function to print with hi-intensity white foreground. A
// newline is appended to format by default.
func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) }
// HiBlackString is a convenient helper function to return a string with hi-intensity black
// foreground.
func HiBlackString(format string, a ...interface{}) string {
return colorString(format, FgHiBlack, a...)
}
// HiRedString is a convenient helper function to return a string with hi-intensity red
// foreground.
func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) }
// HiGreenString is a convenient helper function to return a string with hi-intensity green
// foreground.
func HiGreenString(format string, a ...interface{}) string {
return colorString(format, FgHiGreen, a...)
}
// HiYellowString is a convenient helper function to return a string with hi-intensity yellow
// foreground.
func HiYellowString(format string, a ...interface{}) string {
return colorString(format, FgHiYellow, a...)
}
// HiBlueString is a convenient helper function to return a string with hi-intensity blue
// foreground.
func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) }
// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta
// foreground.
func HiMagentaString(format string, a ...interface{}) string {
return colorString(format, FgHiMagenta, a...)
}
// HiCyanString is a convenient helper function to return a string with hi-intensity cyan
// foreground.
func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) }
// HiWhiteString is a convenient helper function to return a string with hi-intensity white
// foreground.
func HiWhiteString(format string, a ...interface{}) string {
return colorString(format, FgHiWhite, a...)
}

19
vendor/github.com/fatih/color/color_windows.go generated vendored Normal file
View File

@@ -0,0 +1,19 @@
package color
import (
"os"
"golang.org/x/sys/windows"
)
func init() {
// Opt-in for ansi color support for current process.
// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences
var outMode uint32
out := windows.Handle(os.Stdout.Fd())
if err := windows.GetConsoleMode(out, &outMode); err != nil {
return
}
outMode |= windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
_ = windows.SetConsoleMode(out, outMode)
}

134
vendor/github.com/fatih/color/doc.go generated vendored Normal file
View File

@@ -0,0 +1,134 @@
/*
Package color is an ANSI color package to output colorized or SGR defined
output to the standard output. The API can be used in several way, pick one
that suits you.
Use simple and default helper functions with predefined foreground colors:
color.Cyan("Prints text in cyan.")
// a newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// More default foreground colors..
color.Red("We have red")
color.Yellow("Yellow color too!")
color.Magenta("And many others ..")
// Hi-intensity colors
color.HiGreen("Bright green color.")
color.HiBlack("Bright black means gray..")
color.HiWhite("Shiny white color!")
However, there are times when custom color mixes are required. Below are some
examples to create custom color objects and use the print functions of each
separate color object.
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with White background.")
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(myWriter, "This will print text in blue.")
You can create PrintXxx functions to simplify even more:
// Create a custom print function for convenient
red := color.New(color.FgRed).PrintfFunc()
red("warning")
red("error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("don't forget this...")
You can also FprintXxx functions to pass your own io.Writer:
blue := color.New(FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, don't forget this...")
Or create SprintXxx functions to mix strings with other non-colorized strings:
yellow := New(FgYellow).SprintFunc()
red := New(FgRed).SprintFunc()
fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Printf("this %s rocks!\n", info("package"))
Windows support is enabled by default. All Print functions work as intended.
However, only for color.SprintXXX functions, user should use fmt.FprintXXX and
set the output to color.Output:
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package"))
Using with existing code is possible. Just use the Set() method to set the
standard output to the given parameters. That way a rewrite of an existing
code is not required.
// Use handy standard colors.
color.Set(color.FgYellow)
fmt.Println("Existing text will be now in Yellow")
fmt.Printf("This one %s\n", "too")
color.Unset() // don't forget to unset
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // use it in your function
fmt.Println("All text will be now bold magenta.")
There might be a case where you want to disable color output (for example to
pipe the standard output of your app to somewhere else). `Color` has support to
disable colors both globally and for single color definition. For example
suppose you have a CLI app and a `--no-color` bool flag. You can easily disable
the color output with:
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor {
color.NoColor = true // disables colorized output
}
You can also disable the color by setting the NO_COLOR environment variable to any value.
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
*/
package color

13
vendor/github.com/fsnotify/fsnotify/.cirrus.yml generated vendored Normal file
View File

@@ -0,0 +1,13 @@
freebsd_task:
name: 'FreeBSD'
freebsd_instance:
image_family: freebsd-13-2
install_script:
- pkg update -f
- pkg install -y go
test_script:
# run tests as user "cirrus" instead of root
- pw useradd cirrus -m
- chown -R cirrus:cirrus .
- FSNOTIFY_BUFFER=4096 sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...
- sudo --preserve-env=FSNOTIFY_BUFFER -u cirrus go test -parallel 1 -race ./...

View File

@@ -4,3 +4,4 @@
# Output of go build ./cmd/fsnotify # Output of go build ./cmd/fsnotify
/fsnotify /fsnotify
/fsnotify.exe

View File

@@ -1,16 +1,87 @@
# Changelog # Changelog
All notable changes to this project will be documented in this file. Unreleased
----------
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
Nothing yet. Nothing yet.
## [1.6.0] - 2022-10-13 1.7.0 - 2023-10-22
------------------
This version of fsnotify needs Go 1.17.
### Additions
- illumos: add FEN backend to support illumos and Solaris. ([#371])
- all: add `NewBufferedWatcher()` to use a buffered channel, which can be useful
in cases where you can't control the kernel buffer and receive a large number
of events in bursts. ([#550], [#572])
- all: add `AddWith()`, which is identical to `Add()` but allows passing
options. ([#521])
- windows: allow setting the ReadDirectoryChangesW() buffer size with
`fsnotify.WithBufferSize()`; the default of 64K is the highest value that
works on all platforms and is enough for most purposes, but in some cases a
highest buffer is needed. ([#521])
### Changes and fixes
- inotify: remove watcher if a watched path is renamed ([#518])
After a rename the reported name wasn't updated, or even an empty string.
Inotify doesn't provide any good facilities to update it, so just remove the
watcher. This is already how it worked on kqueue and FEN.
On Windows this does work, and remains working.
- windows: don't listen for file attribute changes ([#520])
File attribute changes are sent as `FILE_ACTION_MODIFIED` by the Windows API,
with no way to see if they're a file write or attribute change, so would show
up as a fsnotify.Write event. This is never useful, and could result in many
spurious Write events.
- windows: return `ErrEventOverflow` if the buffer is full ([#525])
Before it would merely return "short read", making it hard to detect this
error.
- kqueue: make sure events for all files are delivered properly when removing a
watched directory ([#526])
Previously they would get sent with `""` (empty string) or `"."` as the path
name.
- kqueue: don't emit spurious Create events for symbolic links ([#524])
The link would get resolved but kqueue would "forget" it already saw the link
itself, resulting on a Create for every Write event for the directory.
- all: return `ErrClosed` on `Add()` when the watcher is closed ([#516])
- other: add `Watcher.Errors` and `Watcher.Events` to the no-op `Watcher` in
`backend_other.go`, making it easier to use on unsupported platforms such as
WASM, AIX, etc. ([#528])
- other: use the `backend_other.go` no-op if the `appengine` build tag is set;
Google AppEngine forbids usage of the unsafe package so the inotify backend
won't compile there.
[#371]: https://github.com/fsnotify/fsnotify/pull/371
[#516]: https://github.com/fsnotify/fsnotify/pull/516
[#518]: https://github.com/fsnotify/fsnotify/pull/518
[#520]: https://github.com/fsnotify/fsnotify/pull/520
[#521]: https://github.com/fsnotify/fsnotify/pull/521
[#524]: https://github.com/fsnotify/fsnotify/pull/524
[#525]: https://github.com/fsnotify/fsnotify/pull/525
[#526]: https://github.com/fsnotify/fsnotify/pull/526
[#528]: https://github.com/fsnotify/fsnotify/pull/528
[#537]: https://github.com/fsnotify/fsnotify/pull/537
[#550]: https://github.com/fsnotify/fsnotify/pull/550
[#572]: https://github.com/fsnotify/fsnotify/pull/572
1.6.0 - 2022-10-13
------------------
This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1, This version of fsnotify needs Go 1.16 (this was already the case since 1.5.1,
but not documented). It also increases the minimum Linux version to 2.6.32. but not documented). It also increases the minimum Linux version to 2.6.32.

View File

@@ -1,29 +1,31 @@
fsnotify is a Go library to provide cross-platform filesystem notifications on fsnotify is a Go library to provide cross-platform filesystem notifications on
Windows, Linux, macOS, and BSD systems. Windows, Linux, macOS, BSD, and illumos.
Go 1.16 or newer is required; the full documentation is at Go 1.17 or newer is required; the full documentation is at
https://pkg.go.dev/github.com/fsnotify/fsnotify https://pkg.go.dev/github.com/fsnotify/fsnotify
**It's best to read the documentation at pkg.go.dev, as it's pinned to the last
released version, whereas this README is for the last development version which
may include additions/changes.**
--- ---
Platform support: Platform support:
| Adapter | OS | Status | | Backend | OS | Status |
| --------------------- | ---------------| -------------------------------------------------------------| | :-------------------- | :--------- | :------------------------------------------------------------------------ |
| inotify | Linux 2.6.32+ | Supported | | inotify | Linux | Supported |
| kqueue | BSD, macOS | Supported | | kqueue | BSD, macOS | Supported |
| ReadDirectoryChangesW | Windows | Supported | | ReadDirectoryChangesW | Windows | Supported |
| FSEvents | macOS | [Planned](https://github.com/fsnotify/fsnotify/issues/11) | | FEN | illumos | Supported |
| FEN | Solaris 11 | [In Progress](https://github.com/fsnotify/fsnotify/pull/371) | | fanotify | Linux 5.9+ | [Not yet](https://github.com/fsnotify/fsnotify/issues/114) |
| fanotify | Linux 5.9+ | [Maybe](https://github.com/fsnotify/fsnotify/issues/114) | | AHAFS | AIX | [aix branch]; experimental due to lack of maintainer and test environment |
| USN Journals | Windows | [Maybe](https://github.com/fsnotify/fsnotify/issues/53) | | FSEvents | macOS | [Needs support in x/sys/unix][fsevents] |
| Polling | *All* | [Maybe](https://github.com/fsnotify/fsnotify/issues/9) | | USN Journals | Windows | [Needs support in x/sys/windows][usn] |
| Polling | *All* | [Not yet](https://github.com/fsnotify/fsnotify/issues/9) |
Linux and macOS should include Android and iOS, but these are currently untested. Linux and illumos should include Android and Solaris, but these are currently
untested.
[fsevents]: https://github.com/fsnotify/fsnotify/issues/11#issuecomment-1279133120
[usn]: https://github.com/fsnotify/fsnotify/issues/53#issuecomment-1279829847
[aix branch]: https://github.com/fsnotify/fsnotify/issues/353#issuecomment-1284590129
Usage Usage
----- -----
@@ -83,20 +85,23 @@ run with:
% go run ./cmd/fsnotify % go run ./cmd/fsnotify
Further detailed documentation can be found in godoc:
https://pkg.go.dev/github.com/fsnotify/fsnotify
FAQ FAQ
--- ---
### Will a file still be watched when it's moved to another directory? ### Will a file still be watched when it's moved to another directory?
No, not unless you are watching the location it was moved to. No, not unless you are watching the location it was moved to.
### Are subdirectories watched too? ### Are subdirectories watched?
No, you must add watches for any directory you want to watch (a recursive No, you must add watches for any directory you want to watch (a recursive
watcher is on the roadmap: [#18]). watcher is on the roadmap: [#18]).
[#18]: https://github.com/fsnotify/fsnotify/issues/18 [#18]: https://github.com/fsnotify/fsnotify/issues/18
### Do I have to watch the Error and Event channels in a goroutine? ### Do I have to watch the Error and Event channels in a goroutine?
As of now, yes (you can read both channels in the same goroutine using `select`, Yes. You can read both channels in the same goroutine using `select` (you don't
you don't need a separate goroutine for both channels; see the example). need a separate goroutine for both channels; see the example).
### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys? ### Why don't notifications work with NFS, SMB, FUSE, /proc, or /sys?
fsnotify requires support from underlying OS to work. The current NFS and SMB fsnotify requires support from underlying OS to work. The current NFS and SMB
@@ -107,6 +112,32 @@ This could be fixed with a polling watcher ([#9]), but it's not yet implemented.
[#9]: https://github.com/fsnotify/fsnotify/issues/9 [#9]: https://github.com/fsnotify/fsnotify/issues/9
### Why do I get many Chmod events?
Some programs may generate a lot of attribute changes; for example Spotlight on
macOS, anti-virus programs, backup applications, and some others are known to do
this. As a rule, it's typically best to ignore Chmod events. They're often not
useful, and tend to cause problems.
Spotlight indexing on macOS can result in multiple events (see [#15]). A
temporary workaround is to add your folder(s) to the *Spotlight Privacy
settings* until we have a native FSEvents implementation (see [#11]).
[#11]: https://github.com/fsnotify/fsnotify/issues/11
[#15]: https://github.com/fsnotify/fsnotify/issues/15
### Watching a file doesn't work well
Watching individual files (rather than directories) is generally not recommended
as many programs (especially editors) update files atomically: it will write to
a temporary file which is then moved to to destination, overwriting the original
(or some variant thereof). The watcher on the original file is now lost, as that
no longer exists.
The upshot of this is that a power failure or crash won't leave a half-written
file.
Watch the parent directory and use `Event.Name` to filter out files you're not
interested in. There is an example of this in `cmd/fsnotify/file.go`.
Platform-specific notes Platform-specific notes
----------------------- -----------------------
### Linux ### Linux
@@ -151,11 +182,3 @@ these platforms.
The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to The sysctl variables `kern.maxfiles` and `kern.maxfilesperproc` can be used to
control the maximum number of open files. control the maximum number of open files.
### macOS
Spotlight indexing on macOS can result in multiple events (see [#15]). A temporary
workaround is to add your folder(s) to the *Spotlight Privacy settings* until we
have a native FSEvents implementation (see [#11]).
[#11]: https://github.com/fsnotify/fsnotify/issues/11
[#15]: https://github.com/fsnotify/fsnotify/issues/15

View File

@@ -1,10 +1,19 @@
//go:build solaris //go:build solaris
// +build solaris // +build solaris
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
import ( import (
"errors" "errors"
"fmt"
"os"
"path/filepath"
"sync"
"golang.org/x/sys/unix"
) )
// Watcher watches a set of paths, delivering events on a channel. // Watcher watches a set of paths, delivering events on a channel.
@@ -17,9 +26,9 @@ import (
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@@ -33,16 +42,16 @@ import (
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@@ -58,14 +67,20 @@ import (
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\path\to\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct { type Watcher struct {
// Events sends the filesystem change events. // Events sends the filesystem change events.
// //
@@ -92,44 +107,129 @@ type Watcher struct {
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
Events chan Event Events chan Event
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error Errors chan error
mu sync.Mutex
port *unix.EventPort
done chan struct{} // Channel for sending a "quit message" to the reader goroutine
dirs map[string]struct{} // Explicitly watched directories
watches map[string]struct{} // Explicitly watched non-directories
} }
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
return nil, errors.New("FEN based watcher not yet supported for fsnotify\n") return NewBufferedWatcher(0)
} }
// Close removes all watches and closes the events channel. // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
w := &Watcher{
Events: make(chan Event, sz),
Errors: make(chan error),
dirs: make(map[string]struct{}),
watches: make(map[string]struct{}),
done: make(chan struct{}),
}
var err error
w.port, err = unix.NewEventPort()
if err != nil {
return nil, fmt.Errorf("fsnotify.NewWatcher: %w", err)
}
go w.readEvents()
return w, nil
}
// sendEvent attempts to send an event to the user, returning true if the event
// was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendEvent(name string, op Op) (sent bool) {
select {
case w.Events <- Event{Name: name, Op: op}:
return true
case <-w.done:
return false
}
}
// sendError attempts to send an error to the user, returning true if the error
// was put in the channel successfully and false if the watcher has been closed.
func (w *Watcher) sendError(err error) (sent bool) {
select {
case w.Errors <- err:
return true
case <-w.done:
return false
}
}
func (w *Watcher) isClosed() bool {
select {
case <-w.done:
return true
default:
return false
}
}
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
return nil // Take the lock used by associateFile to prevent lingering events from
// being processed after the close
w.mu.Lock()
defer w.mu.Unlock()
if w.isClosed() {
return nil
}
close(w.done)
return w.port.Close()
} }
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@@ -139,15 +239,63 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}
if w.port.PathIsWatched(name) {
return nil
}
_ = getOptions(opts...)
// Currently we resolve symlinks that were explicitly requested to be
// watched. Otherwise we would use LStat here.
stat, err := os.Stat(name)
if err != nil {
return err
}
// Associate all files in the directory.
if stat.IsDir() {
err := w.handleDirectory(name, stat, true, w.associateFile)
if err != nil {
return err
}
w.mu.Lock()
w.dirs[name] = struct{}{}
w.mu.Unlock()
return nil
}
err = w.associateFile(name, stat, true)
if err != nil {
return err
}
w.mu.Lock()
w.watches[name] = struct{}{}
w.mu.Unlock()
return nil return nil
} }
@@ -157,6 +305,336 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
if w.isClosed() {
return nil
}
if !w.port.PathIsWatched(name) {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
}
// The user has expressed an intent. Immediately remove this name from
// whichever watch list it might be in. If it's not in there the delete
// doesn't cause harm.
w.mu.Lock()
delete(w.watches, name)
delete(w.dirs, name)
w.mu.Unlock()
stat, err := os.Stat(name)
if err != nil {
return err
}
// Remove associations for every file in the directory.
if stat.IsDir() {
err := w.handleDirectory(name, stat, false, w.dissociateFile)
if err != nil {
return err
}
return nil
}
err = w.port.DissociatePath(name)
if err != nil {
return err
}
return nil return nil
} }
// readEvents contains the main loop that runs in a goroutine watching for events.
func (w *Watcher) readEvents() {
// If this function returns, the watcher has been closed and we can close
// these channels
defer func() {
close(w.Errors)
close(w.Events)
}()
pevents := make([]unix.PortEvent, 8)
for {
count, err := w.port.Get(pevents, 1, nil)
if err != nil && err != unix.ETIME {
// Interrupted system call (count should be 0) ignore and continue
if errors.Is(err, unix.EINTR) && count == 0 {
continue
}
// Get failed because we called w.Close()
if errors.Is(err, unix.EBADF) && w.isClosed() {
return
}
// There was an error not caused by calling w.Close()
if !w.sendError(err) {
return
}
}
p := pevents[:count]
for _, pevent := range p {
if pevent.Source != unix.PORT_SOURCE_FILE {
// Event from unexpected source received; should never happen.
if !w.sendError(errors.New("Event from unexpected source received")) {
return
}
continue
}
err = w.handleEvent(&pevent)
if err != nil {
if !w.sendError(err) {
return
}
}
}
}
}
func (w *Watcher) handleDirectory(path string, stat os.FileInfo, follow bool, handler func(string, os.FileInfo, bool) error) error {
files, err := os.ReadDir(path)
if err != nil {
return err
}
// Handle all children of the directory.
for _, entry := range files {
finfo, err := entry.Info()
if err != nil {
return err
}
err = handler(filepath.Join(path, finfo.Name()), finfo, false)
if err != nil {
return err
}
}
// And finally handle the directory itself.
return handler(path, stat, follow)
}
// handleEvent might need to emit more than one fsnotify event if the events
// bitmap matches more than one event type (e.g. the file was both modified and
// had the attributes changed between when the association was created and the
// when event was returned)
func (w *Watcher) handleEvent(event *unix.PortEvent) error {
var (
events = event.Events
path = event.Path
fmode = event.Cookie.(os.FileMode)
reRegister = true
)
w.mu.Lock()
_, watchedDir := w.dirs[path]
_, watchedPath := w.watches[path]
w.mu.Unlock()
isWatched := watchedDir || watchedPath
if events&unix.FILE_DELETE != 0 {
if !w.sendEvent(path, Remove) {
return nil
}
reRegister = false
}
if events&unix.FILE_RENAME_FROM != 0 {
if !w.sendEvent(path, Rename) {
return nil
}
// Don't keep watching the new file name
reRegister = false
}
if events&unix.FILE_RENAME_TO != 0 {
// We don't report a Rename event for this case, because Rename events
// are interpreted as referring to the _old_ name of the file, and in
// this case the event would refer to the new name of the file. This
// type of rename event is not supported by fsnotify.
// inotify reports a Remove event in this case, so we simulate this
// here.
if !w.sendEvent(path, Remove) {
return nil
}
// Don't keep watching the file that was removed
reRegister = false
}
// The file is gone, nothing left to do.
if !reRegister {
if watchedDir {
w.mu.Lock()
delete(w.dirs, path)
w.mu.Unlock()
}
if watchedPath {
w.mu.Lock()
delete(w.watches, path)
w.mu.Unlock()
}
return nil
}
// If we didn't get a deletion the file still exists and we're going to have
// to watch it again. Let's Stat it now so that we can compare permissions
// and have what we need to continue watching the file
stat, err := os.Lstat(path)
if err != nil {
// This is unexpected, but we should still emit an event. This happens
// most often on "rm -r" of a subdirectory inside a watched directory We
// get a modify event of something happening inside, but by the time we
// get here, the sudirectory is already gone. Clearly we were watching
// this path but now it is gone. Let's tell the user that it was
// removed.
if !w.sendEvent(path, Remove) {
return nil
}
// Suppress extra write events on removed directories; they are not
// informative and can be confusing.
return nil
}
// resolve symlinks that were explicitly watched as we would have at Add()
// time. this helps suppress spurious Chmod events on watched symlinks
if isWatched {
stat, err = os.Stat(path)
if err != nil {
// The symlink still exists, but the target is gone. Report the
// Remove similar to above.
if !w.sendEvent(path, Remove) {
return nil
}
// Don't return the error
}
}
if events&unix.FILE_MODIFIED != 0 {
if fmode.IsDir() {
if watchedDir {
if err := w.updateDirectory(path); err != nil {
return err
}
} else {
if !w.sendEvent(path, Write) {
return nil
}
}
} else {
if !w.sendEvent(path, Write) {
return nil
}
}
}
if events&unix.FILE_ATTRIB != 0 && stat != nil {
// Only send Chmod if perms changed
if stat.Mode().Perm() != fmode.Perm() {
if !w.sendEvent(path, Chmod) {
return nil
}
}
}
if stat != nil {
// If we get here, it means we've hit an event above that requires us to
// continue watching the file or directory
return w.associateFile(path, stat, isWatched)
}
return nil
}
func (w *Watcher) updateDirectory(path string) error {
// The directory was modified, so we must find unwatched entities and watch
// them. If something was removed from the directory, nothing will happen,
// as everything else should still be watched.
files, err := os.ReadDir(path)
if err != nil {
return err
}
for _, entry := range files {
path := filepath.Join(path, entry.Name())
if w.port.PathIsWatched(path) {
continue
}
finfo, err := entry.Info()
if err != nil {
return err
}
err = w.associateFile(path, finfo, false)
if err != nil {
if !w.sendError(err) {
return nil
}
}
if !w.sendEvent(path, Create) {
return nil
}
}
return nil
}
func (w *Watcher) associateFile(path string, stat os.FileInfo, follow bool) error {
if w.isClosed() {
return ErrClosed
}
// This is primarily protecting the call to AssociatePath but it is
// important and intentional that the call to PathIsWatched is also
// protected by this mutex. Without this mutex, AssociatePath has been seen
// to error out that the path is already associated.
w.mu.Lock()
defer w.mu.Unlock()
if w.port.PathIsWatched(path) {
// Remove the old association in favor of this one If we get ENOENT,
// then while the x/sys/unix wrapper still thought that this path was
// associated, the underlying event port did not. This call will have
// cleared up that discrepancy. The most likely cause is that the event
// has fired but we haven't processed it yet.
err := w.port.DissociatePath(path)
if err != nil && err != unix.ENOENT {
return err
}
}
// FILE_NOFOLLOW means we watch symlinks themselves rather than their
// targets.
events := unix.FILE_MODIFIED | unix.FILE_ATTRIB | unix.FILE_NOFOLLOW
if follow {
// We *DO* follow symlinks for explicitly watched entries.
events = unix.FILE_MODIFIED | unix.FILE_ATTRIB
}
return w.port.AssociatePath(path, stat,
events,
stat.Mode())
}
func (w *Watcher) dissociateFile(path string, stat os.FileInfo, unused bool) error {
if !w.port.PathIsWatched(path) {
return nil
}
return w.port.DissociatePath(path)
}
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string {
if w.isClosed() {
return nil
}
w.mu.Lock()
defer w.mu.Unlock()
entries := make([]string, 0, len(w.watches)+len(w.dirs))
for pathname := range w.dirs {
entries = append(entries, pathname)
}
for pathname := range w.watches {
entries = append(entries, pathname)
}
return entries
}

View File

@@ -1,5 +1,8 @@
//go:build linux //go:build linux && !appengine
// +build linux // +build linux,!appengine
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
@@ -26,9 +29,9 @@ import (
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@@ -42,16 +45,16 @@ import (
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@@ -67,14 +70,20 @@ import (
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\path\to\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct { type Watcher struct {
// Events sends the filesystem change events. // Events sends the filesystem change events.
// //
@@ -101,36 +110,148 @@ type Watcher struct {
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
Events chan Event Events chan Event
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error Errors chan error
// Store fd here as os.File.Read() will no longer return on close after // Store fd here as os.File.Read() will no longer return on close after
// calling Fd(). See: https://github.com/golang/go/issues/26439 // calling Fd(). See: https://github.com/golang/go/issues/26439
fd int fd int
mu sync.Mutex // Map access
inotifyFile *os.File inotifyFile *os.File
watches map[string]*watch // Map of inotify watches (key: path) watches *watches
paths map[int]string // Map of watched paths (key: watch descriptor) done chan struct{} // Channel for sending a "quit message" to the reader goroutine
done chan struct{} // Channel for sending a "quit message" to the reader goroutine closeMu sync.Mutex
doneResp chan struct{} // Channel to respond to Close doneResp chan struct{} // Channel to respond to Close
}
type (
watches struct {
mu sync.RWMutex
wd map[uint32]*watch // wd → watch
path map[string]uint32 // pathname → wd
}
watch struct {
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
path string // Watch path.
}
)
func newWatches() *watches {
return &watches{
wd: make(map[uint32]*watch),
path: make(map[string]uint32),
}
}
func (w *watches) len() int {
w.mu.RLock()
defer w.mu.RUnlock()
return len(w.wd)
}
func (w *watches) add(ww *watch) {
w.mu.Lock()
defer w.mu.Unlock()
w.wd[ww.wd] = ww
w.path[ww.path] = ww.wd
}
func (w *watches) remove(wd uint32) {
w.mu.Lock()
defer w.mu.Unlock()
delete(w.path, w.wd[wd].path)
delete(w.wd, wd)
}
func (w *watches) removePath(path string) (uint32, bool) {
w.mu.Lock()
defer w.mu.Unlock()
wd, ok := w.path[path]
if !ok {
return 0, false
}
delete(w.path, path)
delete(w.wd, wd)
return wd, true
}
func (w *watches) byPath(path string) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[w.path[path]]
}
func (w *watches) byWd(wd uint32) *watch {
w.mu.RLock()
defer w.mu.RUnlock()
return w.wd[wd]
}
func (w *watches) updatePath(path string, f func(*watch) (*watch, error)) error {
w.mu.Lock()
defer w.mu.Unlock()
var existing *watch
wd, ok := w.path[path]
if ok {
existing = w.wd[wd]
}
upd, err := f(existing)
if err != nil {
return err
}
if upd != nil {
w.wd[upd.wd] = upd
w.path[upd.path] = upd.wd
if upd.wd != wd {
delete(w.wd, wd)
}
}
return nil
} }
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
// Create inotify fd return NewBufferedWatcher(0)
// Need to set the FD to nonblocking mode in order for SetDeadline methods to work }
// Otherwise, blocking i/o operations won't terminate on close
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
// Need to set nonblocking mode for SetDeadline to work, otherwise blocking
// I/O operations won't terminate on close.
fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK) fd, errno := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
if fd == -1 { if fd == -1 {
return nil, errno return nil, errno
@@ -139,9 +260,8 @@ func NewWatcher() (*Watcher, error) {
w := &Watcher{ w := &Watcher{
fd: fd, fd: fd,
inotifyFile: os.NewFile(uintptr(fd), ""), inotifyFile: os.NewFile(uintptr(fd), ""),
watches: make(map[string]*watch), watches: newWatches(),
paths: make(map[int]string), Events: make(chan Event, sz),
Events: make(chan Event),
Errors: make(chan error), Errors: make(chan error),
done: make(chan struct{}), done: make(chan struct{}),
doneResp: make(chan struct{}), doneResp: make(chan struct{}),
@@ -157,8 +277,8 @@ func (w *Watcher) sendEvent(e Event) bool {
case w.Events <- e: case w.Events <- e:
return true return true
case <-w.done: case <-w.done:
return false
} }
return false
} }
// Returns true if the error was sent, or false if watcher is closed. // Returns true if the error was sent, or false if watcher is closed.
@@ -180,17 +300,15 @@ func (w *Watcher) isClosed() bool {
} }
} }
// Close removes all watches and closes the events channel. // Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
w.mu.Lock() w.closeMu.Lock()
if w.isClosed() { if w.isClosed() {
w.mu.Unlock() w.closeMu.Unlock()
return nil return nil
} }
// Send 'close' signal to goroutine, and set the Watcher to closed.
close(w.done) close(w.done)
w.mu.Unlock() w.closeMu.Unlock()
// Causes any blocking reads to return with an error, provided the file // Causes any blocking reads to return with an error, provided the file
// still supports deadline operations. // still supports deadline operations.
@@ -207,17 +325,21 @@ func (w *Watcher) Close() error {
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@@ -227,44 +349,59 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
name = filepath.Clean(name) // Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() { if w.isClosed() {
return errors.New("inotify instance already closed") return ErrClosed
} }
name = filepath.Clean(name)
_ = getOptions(opts...)
var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM | var flags uint32 = unix.IN_MOVED_TO | unix.IN_MOVED_FROM |
unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY | unix.IN_CREATE | unix.IN_ATTRIB | unix.IN_MODIFY |
unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF unix.IN_MOVE_SELF | unix.IN_DELETE | unix.IN_DELETE_SELF
w.mu.Lock() return w.watches.updatePath(name, func(existing *watch) (*watch, error) {
defer w.mu.Unlock() if existing != nil {
watchEntry := w.watches[name] flags |= existing.flags | unix.IN_MASK_ADD
if watchEntry != nil { }
flags |= watchEntry.flags | unix.IN_MASK_ADD
}
wd, errno := unix.InotifyAddWatch(w.fd, name, flags)
if wd == -1 {
return errno
}
if watchEntry == nil { wd, err := unix.InotifyAddWatch(w.fd, name, flags)
w.watches[name] = &watch{wd: uint32(wd), flags: flags} if wd == -1 {
w.paths[wd] = name return nil, err
} else { }
watchEntry.wd = uint32(wd)
watchEntry.flags = flags
}
return nil if existing == nil {
return &watch{
wd: uint32(wd),
path: name,
flags: flags,
}, nil
}
existing.wd = uint32(wd)
existing.flags = flags
return existing, nil
})
} }
// Remove stops monitoring the path for changes. // Remove stops monitoring the path for changes.
@@ -273,32 +410,22 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
name = filepath.Clean(name) if w.isClosed() {
return nil
}
return w.remove(filepath.Clean(name))
}
// Fetch the watch. func (w *Watcher) remove(name string) error {
w.mu.Lock() wd, ok := w.watches.removePath(name)
defer w.mu.Unlock()
watch, ok := w.watches[name]
// Remove it from inotify.
if !ok { if !ok {
return fmt.Errorf("%w: %s", ErrNonExistentWatch, name) return fmt.Errorf("%w: %s", ErrNonExistentWatch, name)
} }
// We successfully removed the watch if InotifyRmWatch doesn't return an success, errno := unix.InotifyRmWatch(w.fd, wd)
// error, we need to clean up our internal state to ensure it matches
// inotify's kernel state.
delete(w.paths, int(watch.wd))
delete(w.watches, name)
// inotify_rm_watch will return EINVAL if the file has been deleted;
// the inotify will already have been removed.
// watches and pathes are deleted in ignoreLinux() implicitly and asynchronously
// by calling inotify_rm_watch() below. e.g. readEvents() goroutine receives IN_IGNORE
// so that EINVAL means that the wd is being rm_watch()ed or its file removed
// by another thread and we have not received IN_IGNORE event.
success, errno := unix.InotifyRmWatch(w.fd, watch.wd)
if success == -1 { if success == -1 {
// TODO: Perhaps it's not helpful to return an error here in every case; // TODO: Perhaps it's not helpful to return an error here in every case;
// The only two possible errors are: // The only two possible errors are:
@@ -312,26 +439,26 @@ func (w *Watcher) Remove(name string) error {
// are watching is deleted. // are watching is deleted.
return errno return errno
} }
return nil return nil
} }
// WatchList returns all paths added with [Add] (and are not yet removed). // WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { func (w *Watcher) WatchList() []string {
w.mu.Lock() if w.isClosed() {
defer w.mu.Unlock() return nil
entries := make([]string, 0, len(w.watches))
for pathname := range w.watches {
entries = append(entries, pathname)
} }
return entries entries := make([]string, 0, w.watches.len())
} w.watches.mu.RLock()
for pathname := range w.watches.path {
entries = append(entries, pathname)
}
w.watches.mu.RUnlock()
type watch struct { return entries
wd uint32 // Watch descriptor (as returned by the inotify_add_watch() syscall)
flags uint32 // inotify flags of this watch (see inotify(7) for the list of valid flags)
} }
// readEvents reads from the inotify file descriptor, converts the // readEvents reads from the inotify file descriptor, converts the
@@ -367,14 +494,11 @@ func (w *Watcher) readEvents() {
if n < unix.SizeofInotifyEvent { if n < unix.SizeofInotifyEvent {
var err error var err error
if n == 0 { if n == 0 {
// If EOF is received. This should really never happen. err = io.EOF // If EOF is received. This should really never happen.
err = io.EOF
} else if n < 0 { } else if n < 0 {
// If an error occurred while reading. err = errno // If an error occurred while reading.
err = errno
} else { } else {
// Read was too short. err = errors.New("notify: short read in readEvents()") // Read was too short.
err = errors.New("notify: short read in readEvents()")
} }
if !w.sendError(err) { if !w.sendError(err) {
return return
@@ -403,18 +527,29 @@ func (w *Watcher) readEvents() {
// doesn't append the filename to the event, but we would like to always fill the // doesn't append the filename to the event, but we would like to always fill the
// the "Name" field with a valid filename. We retrieve the path of the watch from // the "Name" field with a valid filename. We retrieve the path of the watch from
// the "paths" map. // the "paths" map.
w.mu.Lock() watch := w.watches.byWd(uint32(raw.Wd))
name, ok := w.paths[int(raw.Wd)]
// IN_DELETE_SELF occurs when the file/directory being watched is removed.
// This is a sign to clean up the maps, otherwise we are no longer in sync
// with the inotify kernel state which has already deleted the watch
// automatically.
if ok && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
delete(w.paths, int(raw.Wd))
delete(w.watches, name)
}
w.mu.Unlock()
// inotify will automatically remove the watch on deletes; just need
// to clean our state here.
if watch != nil && mask&unix.IN_DELETE_SELF == unix.IN_DELETE_SELF {
w.watches.remove(watch.wd)
}
// We can't really update the state when a watched path is moved;
// only IN_MOVE_SELF is sent and not IN_MOVED_{FROM,TO}. So remove
// the watch.
if watch != nil && mask&unix.IN_MOVE_SELF == unix.IN_MOVE_SELF {
err := w.remove(watch.path)
if err != nil && !errors.Is(err, ErrNonExistentWatch) {
if !w.sendError(err) {
return
}
}
}
var name string
if watch != nil {
name = watch.path
}
if nameLen > 0 { if nameLen > 0 {
// Point "bytes" at the first byte of the filename // Point "bytes" at the first byte of the filename
bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen] bytes := (*[unix.PathMax]byte)(unsafe.Pointer(&buf[offset+unix.SizeofInotifyEvent]))[:nameLen:nameLen]

View File

@@ -1,12 +1,14 @@
//go:build freebsd || openbsd || netbsd || dragonfly || darwin //go:build freebsd || openbsd || netbsd || dragonfly || darwin
// +build freebsd openbsd netbsd dragonfly darwin // +build freebsd openbsd netbsd dragonfly darwin
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@@ -24,9 +26,9 @@ import (
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@@ -40,16 +42,16 @@ import (
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@@ -65,14 +67,20 @@ import (
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\path\to\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct { type Watcher struct {
// Events sends the filesystem change events. // Events sends the filesystem change events.
// //
@@ -99,18 +107,27 @@ type Watcher struct {
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
Events chan Event Events chan Event
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error Errors chan error
done chan struct{} done chan struct{}
@@ -133,6 +150,18 @@ type pathInfo struct {
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
return NewBufferedWatcher(0)
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
kq, closepipe, err := newKqueue() kq, closepipe, err := newKqueue()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -147,7 +176,7 @@ func NewWatcher() (*Watcher, error) {
paths: make(map[int]pathInfo), paths: make(map[int]pathInfo),
fileExists: make(map[string]struct{}), fileExists: make(map[string]struct{}),
userWatches: make(map[string]struct{}), userWatches: make(map[string]struct{}),
Events: make(chan Event), Events: make(chan Event, sz),
Errors: make(chan error), Errors: make(chan error),
done: make(chan struct{}), done: make(chan struct{}),
} }
@@ -197,8 +226,8 @@ func (w *Watcher) sendEvent(e Event) bool {
case w.Events <- e: case w.Events <- e:
return true return true
case <-w.done: case <-w.done:
return false
} }
return false
} }
// Returns true if the error was sent, or false if watcher is closed. // Returns true if the error was sent, or false if watcher is closed.
@@ -207,11 +236,11 @@ func (w *Watcher) sendError(err error) bool {
case w.Errors <- err: case w.Errors <- err:
return true return true
case <-w.done: case <-w.done:
return false
} }
return false
} }
// Close removes all watches and closes the events channel. // Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
w.mu.Lock() w.mu.Lock()
if w.isClosed { if w.isClosed {
@@ -239,17 +268,21 @@ func (w *Watcher) Close() error {
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@@ -259,15 +292,28 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return w.AddWith(name) }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
_ = getOptions(opts...)
w.mu.Lock() w.mu.Lock()
w.userWatches[name] = struct{}{} w.userWatches[name] = struct{}{}
w.mu.Unlock() w.mu.Unlock()
@@ -281,9 +327,19 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
return w.remove(name, true)
}
func (w *Watcher) remove(name string, unwatchFiles bool) error {
name = filepath.Clean(name) name = filepath.Clean(name)
w.mu.Lock() w.mu.Lock()
if w.isClosed {
w.mu.Unlock()
return nil
}
watchfd, ok := w.watches[name] watchfd, ok := w.watches[name]
w.mu.Unlock() w.mu.Unlock()
if !ok { if !ok {
@@ -315,7 +371,7 @@ func (w *Watcher) Remove(name string) error {
w.mu.Unlock() w.mu.Unlock()
// Find all watched paths that are in this directory that are not external. // Find all watched paths that are in this directory that are not external.
if isDir { if unwatchFiles && isDir {
var pathsToRemove []string var pathsToRemove []string
w.mu.Lock() w.mu.Lock()
for fd := range w.watchesByDir[name] { for fd := range w.watchesByDir[name] {
@@ -326,20 +382,25 @@ func (w *Watcher) Remove(name string) error {
} }
w.mu.Unlock() w.mu.Unlock()
for _, name := range pathsToRemove { for _, name := range pathsToRemove {
// Since these are internal, not much sense in propagating error // Since these are internal, not much sense in propagating error to
// to the user, as that will just confuse them with an error about // the user, as that will just confuse them with an error about a
// a path they did not explicitly watch themselves. // path they did not explicitly watch themselves.
w.Remove(name) w.Remove(name)
} }
} }
return nil return nil
} }
// WatchList returns all paths added with [Add] (and are not yet removed). // WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { func (w *Watcher) WatchList() []string {
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
if w.isClosed {
return nil
}
entries := make([]string, 0, len(w.userWatches)) entries := make([]string, 0, len(w.userWatches))
for pathname := range w.userWatches { for pathname := range w.userWatches {
@@ -352,18 +413,18 @@ func (w *Watcher) WatchList() []string {
// Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE) // Watch all events (except NOTE_EXTEND, NOTE_LINK, NOTE_REVOKE)
const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME const noteAllEvents = unix.NOTE_DELETE | unix.NOTE_WRITE | unix.NOTE_ATTRIB | unix.NOTE_RENAME
// addWatch adds name to the watched file set. // addWatch adds name to the watched file set; the flags are interpreted as
// The flags are interpreted as described in kevent(2). // described in kevent(2).
// Returns the real path to the file which was added, if any, which may be different from the one passed in the case of symlinks. //
// Returns the real path to the file which was added, with symlinks resolved.
func (w *Watcher) addWatch(name string, flags uint32) (string, error) { func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
var isDir bool var isDir bool
// Make ./name and name equivalent
name = filepath.Clean(name) name = filepath.Clean(name)
w.mu.Lock() w.mu.Lock()
if w.isClosed { if w.isClosed {
w.mu.Unlock() w.mu.Unlock()
return "", errors.New("kevent instance already closed") return "", ErrClosed
} }
watchfd, alreadyWatching := w.watches[name] watchfd, alreadyWatching := w.watches[name]
// We already have a watch, but we can still override flags. // We already have a watch, but we can still override flags.
@@ -383,27 +444,30 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
return "", nil return "", nil
} }
// Follow Symlinks // Follow Symlinks.
//
// Linux can add unresolvable symlinks to the watch list without issue,
// and Windows can't do symlinks period. To maintain consistency, we
// will act like everything is fine if the link can't be resolved.
// There will simply be no file events for broken symlinks. Hence the
// returns of nil on errors.
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
name, err = filepath.EvalSymlinks(name) link, err := os.Readlink(name)
if err != nil { if err != nil {
// Return nil because Linux can add unresolvable symlinks to the
// watch list without problems, so maintain consistency with
// that. There will be no file events for broken symlinks.
// TODO: more specific check; returns os.PathError; ENOENT?
return "", nil return "", nil
} }
w.mu.Lock() w.mu.Lock()
_, alreadyWatching = w.watches[name] _, alreadyWatching = w.watches[link]
w.mu.Unlock() w.mu.Unlock()
if alreadyWatching { if alreadyWatching {
return name, nil // Add to watches so we don't get spurious Create events later
// on when we diff the directories.
w.watches[name] = 0
w.fileExists[name] = struct{}{}
return link, nil
} }
name = link
fi, err = os.Lstat(name) fi, err = os.Lstat(name)
if err != nil { if err != nil {
return "", nil return "", nil
@@ -411,7 +475,7 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
} }
// Retry on EINTR; open() can return EINTR in practice on macOS. // Retry on EINTR; open() can return EINTR in practice on macOS.
// See #354, and go issues 11180 and 39237. // See #354, and Go issues 11180 and 39237.
for { for {
watchfd, err = unix.Open(name, openMode, 0) watchfd, err = unix.Open(name, openMode, 0)
if err == nil { if err == nil {
@@ -444,14 +508,13 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
w.watchesByDir[parentName] = watchesByDir w.watchesByDir[parentName] = watchesByDir
} }
watchesByDir[watchfd] = struct{}{} watchesByDir[watchfd] = struct{}{}
w.paths[watchfd] = pathInfo{name: name, isDir: isDir} w.paths[watchfd] = pathInfo{name: name, isDir: isDir}
w.mu.Unlock() w.mu.Unlock()
} }
if isDir { if isDir {
// Watch the directory if it has not been watched before, // Watch the directory if it has not been watched before, or if it was
// or if it was watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles) // watched before, but perhaps only a NOTE_DELETE (watchDirectoryFiles)
w.mu.Lock() w.mu.Lock()
watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE && watchDir := (flags&unix.NOTE_WRITE) == unix.NOTE_WRITE &&
@@ -473,13 +536,10 @@ func (w *Watcher) addWatch(name string, flags uint32) (string, error) {
// Event values that it sends down the Events channel. // Event values that it sends down the Events channel.
func (w *Watcher) readEvents() { func (w *Watcher) readEvents() {
defer func() { defer func() {
err := unix.Close(w.kq)
if err != nil {
w.Errors <- err
}
unix.Close(w.closepipe[0])
close(w.Events) close(w.Events)
close(w.Errors) close(w.Errors)
_ = unix.Close(w.kq)
unix.Close(w.closepipe[0])
}() }()
eventBuffer := make([]unix.Kevent_t, 10) eventBuffer := make([]unix.Kevent_t, 10)
@@ -513,18 +573,8 @@ func (w *Watcher) readEvents() {
event := w.newEvent(path.name, mask) event := w.newEvent(path.name, mask)
if path.isDir && !event.Has(Remove) {
// Double check to make sure the directory exists. This can
// happen when we do a rm -fr on a recursively watched folders
// and we receive a modification event first but the folder has
// been deleted and later receive the delete event.
if _, err := os.Lstat(event.Name); os.IsNotExist(err) {
event.Op |= Remove
}
}
if event.Has(Rename) || event.Has(Remove) { if event.Has(Rename) || event.Has(Remove) {
w.Remove(event.Name) w.remove(event.Name, false)
w.mu.Lock() w.mu.Lock()
delete(w.fileExists, event.Name) delete(w.fileExists, event.Name)
w.mu.Unlock() w.mu.Unlock()
@@ -540,26 +590,30 @@ func (w *Watcher) readEvents() {
} }
if event.Has(Remove) { if event.Has(Remove) {
// Look for a file that may have overwritten this. // Look for a file that may have overwritten this; for example,
// For example, mv f1 f2 will delete f2, then create f2. // mv f1 f2 will delete f2, then create f2.
if path.isDir { if path.isDir {
fileDir := filepath.Clean(event.Name) fileDir := filepath.Clean(event.Name)
w.mu.Lock() w.mu.Lock()
_, found := w.watches[fileDir] _, found := w.watches[fileDir]
w.mu.Unlock() w.mu.Unlock()
if found { if found {
// make sure the directory exists before we watch for changes. When we err := w.sendDirectoryChangeEvents(fileDir)
// do a recursive watch and perform rm -fr, the parent directory might if err != nil {
// have gone missing, ignore the missing directory and let the if !w.sendError(err) {
// upcoming delete event remove the watch from the parent directory. closed = true
if _, err := os.Lstat(fileDir); err == nil { }
w.sendDirectoryChangeEvents(fileDir)
} }
} }
} else { } else {
filePath := filepath.Clean(event.Name) filePath := filepath.Clean(event.Name)
if fileInfo, err := os.Lstat(filePath); err == nil { if fi, err := os.Lstat(filePath); err == nil {
w.sendFileCreatedEventIfNew(filePath, fileInfo) err := w.sendFileCreatedEventIfNew(filePath, fi)
if err != nil {
if !w.sendError(err) {
closed = true
}
}
} }
} }
} }
@@ -582,21 +636,31 @@ func (w *Watcher) newEvent(name string, mask uint32) Event {
if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB { if mask&unix.NOTE_ATTRIB == unix.NOTE_ATTRIB {
e.Op |= Chmod e.Op |= Chmod
} }
// No point sending a write and delete event at the same time: if it's gone,
// then it's gone.
if e.Op.Has(Write) && e.Op.Has(Remove) {
e.Op &^= Write
}
return e return e
} }
// watchDirectoryFiles to mimic inotify when adding a watch on a directory // watchDirectoryFiles to mimic inotify when adding a watch on a directory
func (w *Watcher) watchDirectoryFiles(dirPath string) error { func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// Get all files // Get all files
files, err := ioutil.ReadDir(dirPath) files, err := os.ReadDir(dirPath)
if err != nil { if err != nil {
return err return err
} }
for _, fileInfo := range files { for _, f := range files {
path := filepath.Join(dirPath, fileInfo.Name()) path := filepath.Join(dirPath, f.Name())
cleanPath, err := w.internalWatch(path, fileInfo) fi, err := f.Info()
if err != nil {
return fmt.Errorf("%q: %w", path, err)
}
cleanPath, err := w.internalWatch(path, fi)
if err != nil { if err != nil {
// No permission to read the file; that's not a problem: just skip. // No permission to read the file; that's not a problem: just skip.
// But do add it to w.fileExists to prevent it from being picked up // But do add it to w.fileExists to prevent it from being picked up
@@ -606,7 +670,7 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM): case errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM):
cleanPath = filepath.Clean(path) cleanPath = filepath.Clean(path)
default: default:
return fmt.Errorf("%q: %w", filepath.Join(dirPath, fileInfo.Name()), err) return fmt.Errorf("%q: %w", path, err)
} }
} }
@@ -622,26 +686,37 @@ func (w *Watcher) watchDirectoryFiles(dirPath string) error {
// //
// This functionality is to have the BSD watcher match the inotify, which sends // This functionality is to have the BSD watcher match the inotify, which sends
// a create event for files created in a watched directory. // a create event for files created in a watched directory.
func (w *Watcher) sendDirectoryChangeEvents(dir string) { func (w *Watcher) sendDirectoryChangeEvents(dir string) error {
// Get all files files, err := os.ReadDir(dir)
files, err := ioutil.ReadDir(dir)
if err != nil { if err != nil {
if !w.sendError(fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)) { // Directory no longer exists: we can ignore this safely. kqueue will
return // still give us the correct events.
if errors.Is(err, os.ErrNotExist) {
return nil
} }
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
} }
// Search for new files for _, f := range files {
for _, fi := range files { fi, err := f.Info()
err := w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
if err != nil { if err != nil {
return return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
}
err = w.sendFileCreatedEventIfNew(filepath.Join(dir, fi.Name()), fi)
if err != nil {
// Don't need to send an error if this file isn't readable.
if errors.Is(err, unix.EACCES) || errors.Is(err, unix.EPERM) {
return nil
}
return fmt.Errorf("fsnotify.sendDirectoryChangeEvents: %w", err)
} }
} }
return nil
} }
// sendFileCreatedEvent sends a create event if the file isn't already being tracked. // sendFileCreatedEvent sends a create event if the file isn't already being tracked.
func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInfo) (err error) { func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fi os.FileInfo) (err error) {
w.mu.Lock() w.mu.Lock()
_, doesExist := w.fileExists[filePath] _, doesExist := w.fileExists[filePath]
w.mu.Unlock() w.mu.Unlock()
@@ -652,7 +727,7 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf
} }
// like watchDirectoryFiles (but without doing another ReadDir) // like watchDirectoryFiles (but without doing another ReadDir)
filePath, err = w.internalWatch(filePath, fileInfo) filePath, err = w.internalWatch(filePath, fi)
if err != nil { if err != nil {
return err return err
} }
@@ -664,10 +739,10 @@ func (w *Watcher) sendFileCreatedEventIfNew(filePath string, fileInfo os.FileInf
return nil return nil
} }
func (w *Watcher) internalWatch(name string, fileInfo os.FileInfo) (string, error) { func (w *Watcher) internalWatch(name string, fi os.FileInfo) (string, error) {
if fileInfo.IsDir() { if fi.IsDir() {
// mimic Linux providing delete events for subdirectories // mimic Linux providing delete events for subdirectories, but preserve
// but preserve the flags used if currently watching subdirectory // the flags used if currently watching subdirectory
w.mu.Lock() w.mu.Lock()
flags := w.dirFlags[name] flags := w.dirFlags[name]
w.mu.Unlock() w.mu.Unlock()

View File

@@ -1,39 +1,169 @@
//go:build !darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows //go:build appengine || (!darwin && !dragonfly && !freebsd && !openbsd && !linux && !netbsd && !solaris && !windows)
// +build !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows // +build appengine !darwin,!dragonfly,!freebsd,!openbsd,!linux,!netbsd,!solaris,!windows
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
import ( import "errors"
"fmt"
"runtime"
)
// Watcher watches a set of files, delivering events to a channel. // Watcher watches a set of paths, delivering events on a channel.
type Watcher struct{} //
// A watcher should not be copied (e.g. pass it by pointer, rather than by
// value).
//
// # Linux notes
//
// When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example:
//
// fp := os.Open("file")
// os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove
//
// This is the event that inotify sends, so not much can be changed about this.
//
// The fs.inotify.max_user_watches sysctl variable specifies the upper limit
// for the number of watches per user, and fs.inotify.max_user_instances
// specifies the maximum number of inotify instances per user. Every Watcher you
// create is an "instance", and every path you add is a "watch".
//
// These are also exposed in /proc as /proc/sys/fs/inotify/max_user_watches and
// /proc/sys/fs/inotify/max_user_instances
//
// To increase them you can use sysctl or write the value to the /proc file:
//
// # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128
//
// To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation):
//
// fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128
//
// Reaching the limit will result in a "no space left on device" or "too many open
// files" error.
//
// # kqueue notes (macOS, BSD)
//
// kqueue requires opening a file descriptor for every file that's being watched;
// so if you're watching a directory with five files then that's six file
// descriptors. You will run in to your system's "max open files" limit faster on
// these platforms.
//
// The sysctl variables kern.maxfiles and kern.maxfilesperproc can be used to
// control the maximum number of open files, as well as /etc/login.conf on BSD
// systems.
//
// # Windows notes
//
// Paths can be added as "C:\path\to\dir", but forward slashes
// ("C:/path/to/dir") will also work.
//
// When a watched directory is removed it will always send an event for the
// directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct {
// Events sends the filesystem change events.
//
// fsnotify can send the following events; a "path" here can refer to a
// file, directory, symbolic link, or special file like a FIFO.
//
// fsnotify.Create A new path was created; this may be followed by one
// or more Write events if data also gets written to a
// file.
//
// fsnotify.Remove A path was removed.
//
// fsnotify.Rename A path was renamed. A rename is always sent with the
// old path as Event.Name, and a Create event will be
// sent with the new name. Renames are only sent for
// paths that are currently watched; e.g. moving an
// unmonitored file into a monitored directory will
// show up as just a Create. Similarly, renaming a file
// to outside a monitored directory will show up as
// only a Rename.
//
// fsnotify.Write A file or named pipe was written to. A Truncate will
// also trigger a Write. A single "write action"
// initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program
// you may get hundreds of Write events, and you may
// want to wait until you've stopped receiving them
// (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
//
// fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent
// when a file is truncated. On Windows it's never
// sent.
Events chan Event
// Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error
}
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
return nil, fmt.Errorf("fsnotify not supported on %s", runtime.GOOS) return nil, errors.New("fsnotify not supported on the current platform")
} }
// Close removes all watches and closes the events channel. // NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
func (w *Watcher) Close() error { // channel.
return nil //
} // The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) { return NewWatcher() }
// Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { return nil }
// WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { return nil }
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@@ -43,17 +173,26 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
return nil // Watch the parent directory and use Event.Name to filter out files you're not
} // interested in. There is an example of this in cmd/fsnotify/file.go.
func (w *Watcher) Add(name string) error { return nil }
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error { return nil }
// Remove stops monitoring the path for changes. // Remove stops monitoring the path for changes.
// //
@@ -61,6 +200,6 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
func (w *Watcher) Remove(name string) error { //
return nil // Returns nil if [Watcher.Close] was called.
} func (w *Watcher) Remove(name string) error { return nil }

View File

@@ -1,6 +1,13 @@
//go:build windows //go:build windows
// +build windows // +build windows
// Windows backend based on ReadDirectoryChangesW()
//
// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
//
// Note: the documentation on the Watcher type and methods is generated from
// mkdoc.zsh
package fsnotify package fsnotify
import ( import (
@@ -27,9 +34,9 @@ import (
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@@ -43,16 +50,16 @@ import (
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@@ -68,14 +75,20 @@ import (
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\path\to\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
type Watcher struct { type Watcher struct {
// Events sends the filesystem change events. // Events sends the filesystem change events.
// //
@@ -102,31 +115,52 @@ type Watcher struct {
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
Events chan Event Events chan Event
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
Errors chan error Errors chan error
port windows.Handle // Handle to completion port port windows.Handle // Handle to completion port
input chan *input // Inputs to the reader are sent on this channel input chan *input // Inputs to the reader are sent on this channel
quit chan chan<- error quit chan chan<- error
mu sync.Mutex // Protects access to watches, isClosed mu sync.Mutex // Protects access to watches, closed
watches watchMap // Map of watches (key: i-number) watches watchMap // Map of watches (key: i-number)
isClosed bool // Set to true when Close() is first called closed bool // Set to true when Close() is first called
} }
// NewWatcher creates a new Watcher. // NewWatcher creates a new Watcher.
func NewWatcher() (*Watcher, error) { func NewWatcher() (*Watcher, error) {
return NewBufferedWatcher(50)
}
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
func NewBufferedWatcher(sz uint) (*Watcher, error) {
port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0) port, err := windows.CreateIoCompletionPort(windows.InvalidHandle, 0, 0, 0)
if err != nil { if err != nil {
return nil, os.NewSyscallError("CreateIoCompletionPort", err) return nil, os.NewSyscallError("CreateIoCompletionPort", err)
@@ -135,7 +169,7 @@ func NewWatcher() (*Watcher, error) {
port: port, port: port,
watches: make(watchMap), watches: make(watchMap),
input: make(chan *input, 1), input: make(chan *input, 1),
Events: make(chan Event, 50), Events: make(chan Event, sz),
Errors: make(chan error), Errors: make(chan error),
quit: make(chan chan<- error, 1), quit: make(chan chan<- error, 1),
} }
@@ -143,6 +177,12 @@ func NewWatcher() (*Watcher, error) {
return w, nil return w, nil
} }
func (w *Watcher) isClosed() bool {
w.mu.Lock()
defer w.mu.Unlock()
return w.closed
}
func (w *Watcher) sendEvent(name string, mask uint64) bool { func (w *Watcher) sendEvent(name string, mask uint64) bool {
if mask == 0 { if mask == 0 {
return false return false
@@ -167,14 +207,14 @@ func (w *Watcher) sendError(err error) bool {
return false return false
} }
// Close removes all watches and closes the events channel. // Close removes all watches and closes the Events channel.
func (w *Watcher) Close() error { func (w *Watcher) Close() error {
w.mu.Lock() if w.isClosed() {
if w.isClosed {
w.mu.Unlock()
return nil return nil
} }
w.isClosed = true
w.mu.Lock()
w.closed = true
w.mu.Unlock() w.mu.Unlock()
// Send "quit" message to the reader goroutine // Send "quit" message to the reader goroutine
@@ -188,17 +228,21 @@ func (w *Watcher) Close() error {
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@@ -208,27 +252,41 @@ func (w *Watcher) Close() error {
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
func (w *Watcher) Add(name string) error { //
w.mu.Lock() // Watch the parent directory and use Event.Name to filter out files you're not
if w.isClosed { // interested in. There is an example of this in cmd/fsnotify/file.go.
w.mu.Unlock() func (w *Watcher) Add(name string) error { return w.AddWith(name) }
return errors.New("watcher already closed")
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
func (w *Watcher) AddWith(name string, opts ...addOpt) error {
if w.isClosed() {
return ErrClosed
}
with := getOptions(opts...)
if with.bufsize < 4096 {
return fmt.Errorf("fsnotify.WithBufferSize: buffer size cannot be smaller than 4096 bytes")
} }
w.mu.Unlock()
in := &input{ in := &input{
op: opAddWatch, op: opAddWatch,
path: filepath.Clean(name), path: filepath.Clean(name),
flags: sysFSALLEVENTS, flags: sysFSALLEVENTS,
reply: make(chan error), reply: make(chan error),
bufsize: with.bufsize,
} }
w.input <- in w.input <- in
if err := w.wakeupReader(); err != nil { if err := w.wakeupReader(); err != nil {
@@ -243,7 +301,13 @@ func (w *Watcher) Add(name string) error {
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) Remove(name string) error { func (w *Watcher) Remove(name string) error {
if w.isClosed() {
return nil
}
in := &input{ in := &input{
op: opRemoveWatch, op: opRemoveWatch,
path: filepath.Clean(name), path: filepath.Clean(name),
@@ -256,8 +320,15 @@ func (w *Watcher) Remove(name string) error {
return <-in.reply return <-in.reply
} }
// WatchList returns all paths added with [Add] (and are not yet removed). // WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
func (w *Watcher) WatchList() []string { func (w *Watcher) WatchList() []string {
if w.isClosed() {
return nil
}
w.mu.Lock() w.mu.Lock()
defer w.mu.Unlock() defer w.mu.Unlock()
@@ -279,7 +350,6 @@ func (w *Watcher) WatchList() []string {
// This should all be removed at some point, and just use windows.FILE_NOTIFY_* // This should all be removed at some point, and just use windows.FILE_NOTIFY_*
const ( const (
sysFSALLEVENTS = 0xfff sysFSALLEVENTS = 0xfff
sysFSATTRIB = 0x4
sysFSCREATE = 0x100 sysFSCREATE = 0x100
sysFSDELETE = 0x200 sysFSDELETE = 0x200
sysFSDELETESELF = 0x400 sysFSDELETESELF = 0x400
@@ -305,9 +375,6 @@ func (w *Watcher) newEvent(name string, mask uint32) Event {
if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM { if mask&sysFSMOVE == sysFSMOVE || mask&sysFSMOVESELF == sysFSMOVESELF || mask&sysFSMOVEDFROM == sysFSMOVEDFROM {
e.Op |= Rename e.Op |= Rename
} }
if mask&sysFSATTRIB == sysFSATTRIB {
e.Op |= Chmod
}
return e return e
} }
@@ -321,10 +388,11 @@ const (
) )
type input struct { type input struct {
op int op int
path string path string
flags uint32 flags uint32
reply chan error bufsize int
reply chan error
} }
type inode struct { type inode struct {
@@ -334,13 +402,14 @@ type inode struct {
} }
type watch struct { type watch struct {
ov windows.Overlapped ov windows.Overlapped
ino *inode // i-number ino *inode // i-number
path string // Directory path recurse bool // Recursive watch?
mask uint64 // Directory itself is being watched with these notify flags path string // Directory path
names map[string]uint64 // Map of names being watched and their notify flags mask uint64 // Directory itself is being watched with these notify flags
rename string // Remembers the old name while renaming a file names map[string]uint64 // Map of names being watched and their notify flags
buf [65536]byte // 64K buffer rename string // Remembers the old name while renaming a file
buf []byte // buffer, allocated later
} }
type ( type (
@@ -413,7 +482,10 @@ func (m watchMap) set(ino *inode, watch *watch) {
} }
// Must run within the I/O thread. // Must run within the I/O thread.
func (w *Watcher) addWatch(pathname string, flags uint64) error { func (w *Watcher) addWatch(pathname string, flags uint64, bufsize int) error {
//pathname, recurse := recursivePath(pathname)
recurse := false
dir, err := w.getDir(pathname) dir, err := w.getDir(pathname)
if err != nil { if err != nil {
return err return err
@@ -433,9 +505,11 @@ func (w *Watcher) addWatch(pathname string, flags uint64) error {
return os.NewSyscallError("CreateIoCompletionPort", err) return os.NewSyscallError("CreateIoCompletionPort", err)
} }
watchEntry = &watch{ watchEntry = &watch{
ino: ino, ino: ino,
path: dir, path: dir,
names: make(map[string]uint64), names: make(map[string]uint64),
recurse: recurse,
buf: make([]byte, bufsize),
} }
w.mu.Lock() w.mu.Lock()
w.watches.set(ino, watchEntry) w.watches.set(ino, watchEntry)
@@ -465,6 +539,8 @@ func (w *Watcher) addWatch(pathname string, flags uint64) error {
// Must run within the I/O thread. // Must run within the I/O thread.
func (w *Watcher) remWatch(pathname string) error { func (w *Watcher) remWatch(pathname string) error {
pathname, recurse := recursivePath(pathname)
dir, err := w.getDir(pathname) dir, err := w.getDir(pathname)
if err != nil { if err != nil {
return err return err
@@ -478,6 +554,10 @@ func (w *Watcher) remWatch(pathname string) error {
watch := w.watches.get(ino) watch := w.watches.get(ino)
w.mu.Unlock() w.mu.Unlock()
if recurse && !watch.recurse {
return fmt.Errorf("can't use \\... with non-recursive watch %q", pathname)
}
err = windows.CloseHandle(ino.handle) err = windows.CloseHandle(ino.handle)
if err != nil { if err != nil {
w.sendError(os.NewSyscallError("CloseHandle", err)) w.sendError(os.NewSyscallError("CloseHandle", err))
@@ -535,8 +615,11 @@ func (w *Watcher) startRead(watch *watch) error {
return nil return nil
} }
rdErr := windows.ReadDirectoryChanges(watch.ino.handle, &watch.buf[0], // We need to pass the array, rather than the slice.
uint32(unsafe.Sizeof(watch.buf)), false, mask, nil, &watch.ov, 0) hdr := (*reflect.SliceHeader)(unsafe.Pointer(&watch.buf))
rdErr := windows.ReadDirectoryChanges(watch.ino.handle,
(*byte)(unsafe.Pointer(hdr.Data)), uint32(hdr.Len),
watch.recurse, mask, nil, &watch.ov, 0)
if rdErr != nil { if rdErr != nil {
err := os.NewSyscallError("ReadDirectoryChanges", rdErr) err := os.NewSyscallError("ReadDirectoryChanges", rdErr)
if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 { if rdErr == windows.ERROR_ACCESS_DENIED && watch.mask&provisional == 0 {
@@ -563,9 +646,8 @@ func (w *Watcher) readEvents() {
runtime.LockOSThread() runtime.LockOSThread()
for { for {
// This error is handled after the watch == nil check below.
qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE) qErr := windows.GetQueuedCompletionStatus(w.port, &n, &key, &ov, windows.INFINITE)
// This error is handled after the watch == nil check below. NOTE: this
// seems odd, note sure if it's correct.
watch := (*watch)(unsafe.Pointer(ov)) watch := (*watch)(unsafe.Pointer(ov))
if watch == nil { if watch == nil {
@@ -595,7 +677,7 @@ func (w *Watcher) readEvents() {
case in := <-w.input: case in := <-w.input:
switch in.op { switch in.op {
case opAddWatch: case opAddWatch:
in.reply <- w.addWatch(in.path, uint64(in.flags)) in.reply <- w.addWatch(in.path, uint64(in.flags), in.bufsize)
case opRemoveWatch: case opRemoveWatch:
in.reply <- w.remWatch(in.path) in.reply <- w.remWatch(in.path)
} }
@@ -605,6 +687,8 @@ func (w *Watcher) readEvents() {
} }
switch qErr { switch qErr {
case nil:
// No error
case windows.ERROR_MORE_DATA: case windows.ERROR_MORE_DATA:
if watch == nil { if watch == nil {
w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer")) w.sendError(errors.New("ERROR_MORE_DATA has unexpectedly null lpOverlapped buffer"))
@@ -626,13 +710,12 @@ func (w *Watcher) readEvents() {
default: default:
w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr)) w.sendError(os.NewSyscallError("GetQueuedCompletionPort", qErr))
continue continue
case nil:
} }
var offset uint32 var offset uint32
for { for {
if n == 0 { if n == 0 {
w.sendError(errors.New("short read in readEvents()")) w.sendError(ErrEventOverflow)
break break
} }
@@ -703,8 +786,9 @@ func (w *Watcher) readEvents() {
// Error! // Error!
if offset >= n { if offset >= n {
//lint:ignore ST1005 Windows should be capitalized
w.sendError(errors.New( w.sendError(errors.New(
"Windows system assumed buffer larger than it is, events have likely been missed.")) "Windows system assumed buffer larger than it is, events have likely been missed"))
break break
} }
} }
@@ -720,9 +804,6 @@ func (w *Watcher) toWindowsFlags(mask uint64) uint32 {
if mask&sysFSMODIFY != 0 { if mask&sysFSMODIFY != 0 {
m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE m |= windows.FILE_NOTIFY_CHANGE_LAST_WRITE
} }
if mask&sysFSATTRIB != 0 {
m |= windows.FILE_NOTIFY_CHANGE_ATTRIBUTES
}
if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 { if mask&(sysFSMOVE|sysFSCREATE|sysFSDELETE) != 0 {
m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME m |= windows.FILE_NOTIFY_CHANGE_FILE_NAME | windows.FILE_NOTIFY_CHANGE_DIR_NAME
} }

View File

@@ -1,13 +1,18 @@
//go:build !plan9
// +build !plan9
// Package fsnotify provides a cross-platform interface for file system // Package fsnotify provides a cross-platform interface for file system
// notifications. // notifications.
//
// Currently supported systems:
//
// Linux 2.6.32+ via inotify
// BSD, macOS via kqueue
// Windows via ReadDirectoryChangesW
// illumos via FEN
package fsnotify package fsnotify
import ( import (
"errors" "errors"
"fmt" "fmt"
"path/filepath"
"strings" "strings"
) )
@@ -33,34 +38,52 @@ type Op uint32
// The operations fsnotify can trigger; see the documentation on [Watcher] for a // The operations fsnotify can trigger; see the documentation on [Watcher] for a
// full description, and check them with [Event.Has]. // full description, and check them with [Event.Has].
const ( const (
// A new pathname was created.
Create Op = 1 << iota Create Op = 1 << iota
// The pathname was written to; this does *not* mean the write has finished,
// and a write can be followed by more writes.
Write Write
// The path was removed; any watches on it will be removed. Some "remove"
// operations may trigger a Rename if the file is actually moved (for
// example "remove to trash" is often a rename).
Remove Remove
// The path was renamed to something else; any watched on it will be
// removed.
Rename Rename
// File attributes were changed.
//
// It's generally not recommended to take action on this event, as it may
// get triggered very frequently by some software. For example, Spotlight
// indexing on macOS, anti-virus software, backup software, etc.
Chmod Chmod
) )
// Common errors that can be reported by a watcher // Common errors that can be reported.
var ( var (
ErrNonExistentWatch = errors.New("can't remove non-existent watcher") ErrNonExistentWatch = errors.New("fsnotify: can't remove non-existent watch")
ErrEventOverflow = errors.New("fsnotify queue overflow") ErrEventOverflow = errors.New("fsnotify: queue or buffer overflow")
ErrClosed = errors.New("fsnotify: watcher already closed")
) )
func (op Op) String() string { func (o Op) String() string {
var b strings.Builder var b strings.Builder
if op.Has(Create) { if o.Has(Create) {
b.WriteString("|CREATE") b.WriteString("|CREATE")
} }
if op.Has(Remove) { if o.Has(Remove) {
b.WriteString("|REMOVE") b.WriteString("|REMOVE")
} }
if op.Has(Write) { if o.Has(Write) {
b.WriteString("|WRITE") b.WriteString("|WRITE")
} }
if op.Has(Rename) { if o.Has(Rename) {
b.WriteString("|RENAME") b.WriteString("|RENAME")
} }
if op.Has(Chmod) { if o.Has(Chmod) {
b.WriteString("|CHMOD") b.WriteString("|CHMOD")
} }
if b.Len() == 0 { if b.Len() == 0 {
@@ -70,7 +93,7 @@ func (op Op) String() string {
} }
// Has reports if this operation has the given operation. // Has reports if this operation has the given operation.
func (o Op) Has(h Op) bool { return o&h == h } func (o Op) Has(h Op) bool { return o&h != 0 }
// Has reports if this event has the given operation. // Has reports if this event has the given operation.
func (e Event) Has(op Op) bool { return e.Op.Has(op) } func (e Event) Has(op Op) bool { return e.Op.Has(op) }
@@ -79,3 +102,45 @@ func (e Event) Has(op Op) bool { return e.Op.Has(op) }
func (e Event) String() string { func (e Event) String() string {
return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name) return fmt.Sprintf("%-13s %q", e.Op.String(), e.Name)
} }
type (
addOpt func(opt *withOpts)
withOpts struct {
bufsize int
}
)
var defaultOpts = withOpts{
bufsize: 65536, // 64K
}
func getOptions(opts ...addOpt) withOpts {
with := defaultOpts
for _, o := range opts {
o(&with)
}
return with
}
// WithBufferSize sets the [ReadDirectoryChangesW] buffer size.
//
// This only has effect on Windows systems, and is a no-op for other backends.
//
// The default value is 64K (65536 bytes) which is the highest value that works
// on all filesystems and should be enough for most applications, but if you
// have a large burst of events it may not be enough. You can increase it if
// you're hitting "queue or buffer overflow" errors ([ErrEventOverflow]).
//
// [ReadDirectoryChangesW]: https://learn.microsoft.com/en-gb/windows/win32/api/winbase/nf-winbase-readdirectorychangesw
func WithBufferSize(bytes int) addOpt {
return func(opt *withOpts) { opt.bufsize = bytes }
}
// Check if this path is recursive (ends with "/..." or "\..."), and return the
// path with the /... stripped.
func recursivePath(path string) (string, bool) {
if filepath.Base(path) == "..." {
return filepath.Dir(path), true
}
return path, false
}

View File

@@ -2,8 +2,8 @@
[ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1 [ "${ZSH_VERSION:-}" = "" ] && echo >&2 "Only works with zsh" && exit 1
setopt err_exit no_unset pipefail extended_glob setopt err_exit no_unset pipefail extended_glob
# Simple script to update the godoc comments on all watchers. Probably took me # Simple script to update the godoc comments on all watchers so you don't need
# more time to write this than doing it manually, but ah well 🙃 # to update the same comment 5 times.
watcher=$(<<EOF watcher=$(<<EOF
// Watcher watches a set of paths, delivering events on a channel. // Watcher watches a set of paths, delivering events on a channel.
@@ -16,9 +16,9 @@ watcher=$(<<EOF
// When a file is removed a Remove event won't be emitted until all file // When a file is removed a Remove event won't be emitted until all file
// descriptors are closed, and deletes will always emit a Chmod. For example: // descriptors are closed, and deletes will always emit a Chmod. For example:
// //
// fp := os.Open("file") // fp := os.Open("file")
// os.Remove("file") // Triggers Chmod // os.Remove("file") // Triggers Chmod
// fp.Close() // Triggers Remove // fp.Close() // Triggers Remove
// //
// This is the event that inotify sends, so not much can be changed about this. // This is the event that inotify sends, so not much can be changed about this.
// //
@@ -32,16 +32,16 @@ watcher=$(<<EOF
// //
// To increase them you can use sysctl or write the value to the /proc file: // To increase them you can use sysctl or write the value to the /proc file:
// //
// # Default values on Linux 5.18 // # Default values on Linux 5.18
// sysctl fs.inotify.max_user_watches=124983 // sysctl fs.inotify.max_user_watches=124983
// sysctl fs.inotify.max_user_instances=128 // sysctl fs.inotify.max_user_instances=128
// //
// To make the changes persist on reboot edit /etc/sysctl.conf or // To make the changes persist on reboot edit /etc/sysctl.conf or
// /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check // /usr/lib/sysctl.d/50-default.conf (details differ per Linux distro; check
// your distro's documentation): // your distro's documentation):
// //
// fs.inotify.max_user_watches=124983 // fs.inotify.max_user_watches=124983
// fs.inotify.max_user_instances=128 // fs.inotify.max_user_instances=128
// //
// Reaching the limit will result in a "no space left on device" or "too many open // Reaching the limit will result in a "no space left on device" or "too many open
// files" error. // files" error.
@@ -57,14 +57,20 @@ watcher=$(<<EOF
// control the maximum number of open files, as well as /etc/login.conf on BSD // control the maximum number of open files, as well as /etc/login.conf on BSD
// systems. // systems.
// //
// # macOS notes // # Windows notes
// //
// Spotlight indexing on macOS can result in multiple events (see [#15]). A // Paths can be added as "C:\\path\\to\\dir", but forward slashes
// temporary workaround is to add your folder(s) to the "Spotlight Privacy // ("C:/path/to/dir") will also work.
// Settings" until we have a native FSEvents implementation (see [#11]).
// //
// [#11]: https://github.com/fsnotify/fsnotify/issues/11 // When a watched directory is removed it will always send an event for the
// [#15]: https://github.com/fsnotify/fsnotify/issues/15 // directory itself, but may not send events for all files in that directory.
// Sometimes it will send events for all times, sometimes it will send no
// events, and often only for some files.
//
// The default ReadDirectoryChangesW() buffer size is 64K, which is the largest
// value that is guaranteed to work with SMB filesystems. If you have many
// events in quick succession this may not be enough, and you will have to use
// [WithBufferSize] to increase the value.
EOF EOF
) )
@@ -73,20 +79,36 @@ new=$(<<EOF
EOF EOF
) )
newbuffered=$(<<EOF
// NewBufferedWatcher creates a new Watcher with a buffered Watcher.Events
// channel.
//
// The main use case for this is situations with a very large number of events
// where the kernel buffer size can't be increased (e.g. due to lack of
// permissions). An unbuffered Watcher will perform better for almost all use
// cases, and whenever possible you will be better off increasing the kernel
// buffers instead of adding a large userspace buffer.
EOF
)
add=$(<<EOF add=$(<<EOF
// Add starts monitoring the path for changes. // Add starts monitoring the path for changes.
// //
// A path can only be watched once; attempting to watch it more than once will // A path can only be watched once; watching it more than once is a no-op and will
// return an error. Paths that do not yet exist on the filesystem cannot be // not return an error. Paths that do not yet exist on the filesystem cannot be
// added. A watch will be automatically removed if the path is deleted. // watched.
// //
// A path will remain watched if it gets renamed to somewhere else on the same // A watch will be automatically removed if the watched path is deleted or
// filesystem, but the monitor will get removed if the path gets deleted and // renamed. The exception is the Windows backend, which doesn't remove the
// re-created, or if it's moved to a different filesystem. // watcher on renames.
// //
// Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special // Notifications on network filesystems (NFS, SMB, FUSE, etc.) or special
// filesystems (/proc, /sys, etc.) generally don't work. // filesystems (/proc, /sys, etc.) generally don't work.
// //
// Returns [ErrClosed] if [Watcher.Close] was called.
//
// See [Watcher.AddWith] for a version that allows adding options.
//
// # Watching directories // # Watching directories
// //
// All files in a directory are monitored, including new files that are created // All files in a directory are monitored, including new files that are created
@@ -96,14 +118,27 @@ add=$(<<EOF
// # Watching files // # Watching files
// //
// Watching individual files (rather than directories) is generally not // Watching individual files (rather than directories) is generally not
// recommended as many tools update files atomically. Instead of "just" writing // recommended as many programs (especially editors) update files atomically: it
// to the file a temporary file will be written to first, and if successful the // will write to a temporary file which is then moved to to destination,
// temporary file is moved to to destination removing the original, or some // overwriting the original (or some variant thereof). The watcher on the
// variant thereof. The watcher on the original file is now lost, as it no // original file is now lost, as that no longer exists.
// longer exists.
// //
// Instead, watch the parent directory and use Event.Name to filter out files // The upshot of this is that a power failure or crash won't leave a
// you're not interested in. There is an example of this in [cmd/fsnotify/file.go]. // half-written file.
//
// Watch the parent directory and use Event.Name to filter out files you're not
// interested in. There is an example of this in cmd/fsnotify/file.go.
EOF
)
addwith=$(<<EOF
// AddWith is like [Watcher.Add], but allows adding options. When using Add()
// the defaults described below are used.
//
// Possible options are:
//
// - [WithBufferSize] sets the buffer size for the Windows backend; no-op on
// other platforms. The default is 64K (65536 bytes).
EOF EOF
) )
@@ -114,16 +149,21 @@ remove=$(<<EOF
// /tmp/dir and /tmp/dir/subdir then you will need to remove both. // /tmp/dir and /tmp/dir/subdir then you will need to remove both.
// //
// Removing a path that has not yet been added returns [ErrNonExistentWatch]. // Removing a path that has not yet been added returns [ErrNonExistentWatch].
//
// Returns nil if [Watcher.Close] was called.
EOF EOF
) )
close=$(<<EOF close=$(<<EOF
// Close removes all watches and closes the events channel. // Close removes all watches and closes the Events channel.
EOF EOF
) )
watchlist=$(<<EOF watchlist=$(<<EOF
// WatchList returns all paths added with [Add] (and are not yet removed). // WatchList returns all paths explicitly added with [Watcher.Add] (and are not
// yet removed).
//
// Returns nil if [Watcher.Close] was called.
EOF EOF
) )
@@ -153,20 +193,29 @@ events=$(<<EOF
// initiated by the user may show up as one or multiple // initiated by the user may show up as one or multiple
// writes, depending on when the system syncs things to // writes, depending on when the system syncs things to
// disk. For example when compiling a large Go program // disk. For example when compiling a large Go program
// you may get hundreds of Write events, so you // you may get hundreds of Write events, and you may
// probably want to wait until you've stopped receiving // want to wait until you've stopped receiving them
// them (see the dedup example in cmd/fsnotify). // (see the dedup example in cmd/fsnotify).
//
// Some systems may send Write event for directories
// when the directory content changes.
// //
// fsnotify.Chmod Attributes were changed. On Linux this is also sent // fsnotify.Chmod Attributes were changed. On Linux this is also sent
// when a file is removed (or more accurately, when a // when a file is removed (or more accurately, when a
// link to an inode is removed). On kqueue it's sent // link to an inode is removed). On kqueue it's sent
// and on kqueue when a file is truncated. On Windows // when a file is truncated. On Windows it's never
// it's never sent. // sent.
EOF EOF
) )
errors=$(<<EOF errors=$(<<EOF
// Errors sends any errors. // Errors sends any errors.
//
// ErrEventOverflow is used to indicate there are too many events:
//
// - inotify: There are too many queued events (fs.inotify.max_queued_events sysctl)
// - windows: The buffer size is too small; WithBufferSize() can be used to increase it.
// - kqueue, fen: Not used.
EOF EOF
) )
@@ -200,7 +249,9 @@ set-cmt() {
set-cmt '^type Watcher struct ' $watcher set-cmt '^type Watcher struct ' $watcher
set-cmt '^func NewWatcher(' $new set-cmt '^func NewWatcher(' $new
set-cmt '^func NewBufferedWatcher(' $newbuffered
set-cmt '^func (w \*Watcher) Add(' $add set-cmt '^func (w \*Watcher) Add(' $add
set-cmt '^func (w \*Watcher) AddWith(' $addwith
set-cmt '^func (w \*Watcher) Remove(' $remove set-cmt '^func (w \*Watcher) Remove(' $remove
set-cmt '^func (w \*Watcher) Close(' $close set-cmt '^func (w \*Watcher) Close(' $close
set-cmt '^func (w \*Watcher) WatchList(' $watchlist set-cmt '^func (w \*Watcher) WatchList(' $watchlist

View File

@@ -1,43 +0,0 @@
language: go
go:
- 1.2.x
- 1.6.x
- 1.9.x
- 1.10.x
- 1.11.x
- 1.12.x
- 1.14.x
- tip
os:
- linux
arch:
- amd64
- ppc64le
dist: xenial
env:
- GOARCH=amd64
jobs:
include:
- os: windows
go: 1.14.x
- os: osx
go: 1.14.x
- os: linux
go: 1.14.x
arch: arm64
- os: linux
go: 1.14.x
env:
- GOARCH=386
script:
- go test -v -cover ./... || go test -v ./...
matrix:
allowfailures:
go: 1.2.x

View File

@@ -170,12 +170,10 @@ func PrintPacket(p *Packet) {
printPacket(os.Stdout, p, 0, false) printPacket(os.Stdout, p, 0, false)
} }
func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) { // Return a string describing packet content. This is not recursive,
indentStr := "" // If the packet is a sequence, use `printPacket()`, or browse
// sequence yourself.
for len(indentStr) != indent { func DescribePacket(p *Packet) string {
indentStr += " "
}
classStr := ClassMap[p.ClassType] classStr := ClassMap[p.ClassType]
@@ -194,7 +192,17 @@ func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) {
description = p.Description + ": " description = p.Description + ": "
} }
_, _ = fmt.Fprintf(out, "%s%s(%s, %s, %s) Len=%d %q\n", indentStr, description, classStr, tagTypeStr, tagStr, p.Data.Len(), value) return fmt.Sprintf("%s(%s, %s, %s) Len=%d %q", description, classStr, tagTypeStr, tagStr, p.Data.Len(), value)
}
func printPacket(out io.Writer, p *Packet, indent int, printBytes bool) {
indentStr := ""
for len(indentStr) != indent {
indentStr += " "
}
_, _ = fmt.Fprintf(out, "%s%s\n", indentStr, DescribePacket(p))
if printBytes { if printBytes {
PrintBytes(out, p.Bytes(), indentStr) PrintBytes(out, p.Bytes(), indentStr)
@@ -317,7 +325,7 @@ func readPacket(reader io.Reader) (*Packet, int, error) {
// Read the next packet // Read the next packet
child, r, err := readPacket(reader) child, r, err := readPacket(reader)
if err != nil { if err != nil {
return nil, read, err return nil, read, unexpectedEOF(err)
} }
contentRead += r contentRead += r
read += r read += r
@@ -348,10 +356,7 @@ func readPacket(reader io.Reader) (*Packet, int, error) {
if length > 0 { if length > 0 {
_, err := io.ReadFull(reader, content) _, err := io.ReadFull(reader, content)
if err != nil { if err != nil {
if err == io.EOF { return nil, read, unexpectedEOF(err)
return nil, read, io.ErrUnexpectedEOF
}
return nil, read, err
} }
read += length read += length
} }

View File

@@ -37,7 +37,7 @@ func readIdentifier(reader io.Reader) (Identifier, int, error) {
if Debug { if Debug {
fmt.Printf("error reading high-tag-number tag byte %d: %v\n", tagBytes, err) fmt.Printf("error reading high-tag-number tag byte %d: %v\n", tagBytes, err)
} }
return Identifier{}, read, err return Identifier{}, read, unexpectedEOF(err)
} }
tagBytes++ tagBytes++
read++ read++

View File

@@ -13,7 +13,7 @@ func readLength(reader io.Reader) (length int, read int, err error) {
if Debug { if Debug {
fmt.Printf("error reading length byte: %v\n", err) fmt.Printf("error reading length byte: %v\n", err)
} }
return 0, 0, err return 0, 0, unexpectedEOF(err)
} }
read++ read++
@@ -47,7 +47,7 @@ func readLength(reader io.Reader) (length int, read int, err error) {
if Debug { if Debug {
fmt.Printf("error reading long-form length byte %d: %v\n", i, err) fmt.Printf("error reading long-form length byte %d: %v\n", i, err)
} }
return 0, read, err return 0, read, unexpectedEOF(err)
} }
read++ read++

View File

@@ -89,12 +89,18 @@ func parseBinaryFloat(v []byte) (float64, error) {
case 0x02: case 0x02:
expLen = 3 expLen = 3
case 0x03: case 0x03:
if len(v) < 2 {
return 0.0, errors.New("invalid data")
}
expLen = int(v[0]) expLen = int(v[0])
if expLen > 8 { if expLen > 8 {
return 0.0, errors.New("too big value of exponent") return 0.0, errors.New("too big value of exponent")
} }
v = v[1:] v = v[1:]
} }
if expLen > len(v) {
return 0.0, errors.New("too big value of exponent")
}
buf, v = v[:expLen], v[expLen:] buf, v = v[:expLen], v[expLen:]
exponent, err := ParseInt64(buf) exponent, err := ParseInt64(buf)
if err != nil { if err != nil {

View File

@@ -6,14 +6,18 @@ func readByte(reader io.Reader) (byte, error) {
bytes := make([]byte, 1) bytes := make([]byte, 1)
_, err := io.ReadFull(reader, bytes) _, err := io.ReadFull(reader, bytes)
if err != nil { if err != nil {
if err == io.EOF {
return 0, io.ErrUnexpectedEOF
}
return 0, err return 0, err
} }
return bytes[0], nil return bytes[0], nil
} }
func unexpectedEOF(err error) error {
if err == io.EOF {
return io.ErrUnexpectedEOF
}
return err
}
func isEOCPacket(p *Packet) bool { func isEOCPacket(p *Packet) bool {
return p != nil && return p != nil &&
p.Tag == TagEOC && p.Tag == TagEOC &&

View File

@@ -12,6 +12,31 @@ import (
// Symbols defined in public import of google/protobuf/descriptor.proto. // Symbols defined in public import of google/protobuf/descriptor.proto.
type Edition = descriptorpb.Edition
const Edition_EDITION_UNKNOWN = descriptorpb.Edition_EDITION_UNKNOWN
const Edition_EDITION_PROTO2 = descriptorpb.Edition_EDITION_PROTO2
const Edition_EDITION_PROTO3 = descriptorpb.Edition_EDITION_PROTO3
const Edition_EDITION_2023 = descriptorpb.Edition_EDITION_2023
const Edition_EDITION_2024 = descriptorpb.Edition_EDITION_2024
const Edition_EDITION_1_TEST_ONLY = descriptorpb.Edition_EDITION_1_TEST_ONLY
const Edition_EDITION_2_TEST_ONLY = descriptorpb.Edition_EDITION_2_TEST_ONLY
const Edition_EDITION_99997_TEST_ONLY = descriptorpb.Edition_EDITION_99997_TEST_ONLY
const Edition_EDITION_99998_TEST_ONLY = descriptorpb.Edition_EDITION_99998_TEST_ONLY
const Edition_EDITION_99999_TEST_ONLY = descriptorpb.Edition_EDITION_99999_TEST_ONLY
const Edition_EDITION_MAX = descriptorpb.Edition_EDITION_MAX
var Edition_name = descriptorpb.Edition_name
var Edition_value = descriptorpb.Edition_value
type ExtensionRangeOptions_VerificationState = descriptorpb.ExtensionRangeOptions_VerificationState
const ExtensionRangeOptions_DECLARATION = descriptorpb.ExtensionRangeOptions_DECLARATION
const ExtensionRangeOptions_UNVERIFIED = descriptorpb.ExtensionRangeOptions_UNVERIFIED
var ExtensionRangeOptions_VerificationState_name = descriptorpb.ExtensionRangeOptions_VerificationState_name
var ExtensionRangeOptions_VerificationState_value = descriptorpb.ExtensionRangeOptions_VerificationState_value
type FieldDescriptorProto_Type = descriptorpb.FieldDescriptorProto_Type type FieldDescriptorProto_Type = descriptorpb.FieldDescriptorProto_Type
const FieldDescriptorProto_TYPE_DOUBLE = descriptorpb.FieldDescriptorProto_TYPE_DOUBLE const FieldDescriptorProto_TYPE_DOUBLE = descriptorpb.FieldDescriptorProto_TYPE_DOUBLE
@@ -39,8 +64,8 @@ var FieldDescriptorProto_Type_value = descriptorpb.FieldDescriptorProto_Type_val
type FieldDescriptorProto_Label = descriptorpb.FieldDescriptorProto_Label type FieldDescriptorProto_Label = descriptorpb.FieldDescriptorProto_Label
const FieldDescriptorProto_LABEL_OPTIONAL = descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL const FieldDescriptorProto_LABEL_OPTIONAL = descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL
const FieldDescriptorProto_LABEL_REQUIRED = descriptorpb.FieldDescriptorProto_LABEL_REQUIRED
const FieldDescriptorProto_LABEL_REPEATED = descriptorpb.FieldDescriptorProto_LABEL_REPEATED const FieldDescriptorProto_LABEL_REPEATED = descriptorpb.FieldDescriptorProto_LABEL_REPEATED
const FieldDescriptorProto_LABEL_REQUIRED = descriptorpb.FieldDescriptorProto_LABEL_REQUIRED
var FieldDescriptorProto_Label_name = descriptorpb.FieldDescriptorProto_Label_name var FieldDescriptorProto_Label_name = descriptorpb.FieldDescriptorProto_Label_name
var FieldDescriptorProto_Label_value = descriptorpb.FieldDescriptorProto_Label_value var FieldDescriptorProto_Label_value = descriptorpb.FieldDescriptorProto_Label_value
@@ -72,6 +97,31 @@ const FieldOptions_JS_NUMBER = descriptorpb.FieldOptions_JS_NUMBER
var FieldOptions_JSType_name = descriptorpb.FieldOptions_JSType_name var FieldOptions_JSType_name = descriptorpb.FieldOptions_JSType_name
var FieldOptions_JSType_value = descriptorpb.FieldOptions_JSType_value var FieldOptions_JSType_value = descriptorpb.FieldOptions_JSType_value
type FieldOptions_OptionRetention = descriptorpb.FieldOptions_OptionRetention
const FieldOptions_RETENTION_UNKNOWN = descriptorpb.FieldOptions_RETENTION_UNKNOWN
const FieldOptions_RETENTION_RUNTIME = descriptorpb.FieldOptions_RETENTION_RUNTIME
const FieldOptions_RETENTION_SOURCE = descriptorpb.FieldOptions_RETENTION_SOURCE
var FieldOptions_OptionRetention_name = descriptorpb.FieldOptions_OptionRetention_name
var FieldOptions_OptionRetention_value = descriptorpb.FieldOptions_OptionRetention_value
type FieldOptions_OptionTargetType = descriptorpb.FieldOptions_OptionTargetType
const FieldOptions_TARGET_TYPE_UNKNOWN = descriptorpb.FieldOptions_TARGET_TYPE_UNKNOWN
const FieldOptions_TARGET_TYPE_FILE = descriptorpb.FieldOptions_TARGET_TYPE_FILE
const FieldOptions_TARGET_TYPE_EXTENSION_RANGE = descriptorpb.FieldOptions_TARGET_TYPE_EXTENSION_RANGE
const FieldOptions_TARGET_TYPE_MESSAGE = descriptorpb.FieldOptions_TARGET_TYPE_MESSAGE
const FieldOptions_TARGET_TYPE_FIELD = descriptorpb.FieldOptions_TARGET_TYPE_FIELD
const FieldOptions_TARGET_TYPE_ONEOF = descriptorpb.FieldOptions_TARGET_TYPE_ONEOF
const FieldOptions_TARGET_TYPE_ENUM = descriptorpb.FieldOptions_TARGET_TYPE_ENUM
const FieldOptions_TARGET_TYPE_ENUM_ENTRY = descriptorpb.FieldOptions_TARGET_TYPE_ENUM_ENTRY
const FieldOptions_TARGET_TYPE_SERVICE = descriptorpb.FieldOptions_TARGET_TYPE_SERVICE
const FieldOptions_TARGET_TYPE_METHOD = descriptorpb.FieldOptions_TARGET_TYPE_METHOD
var FieldOptions_OptionTargetType_name = descriptorpb.FieldOptions_OptionTargetType_name
var FieldOptions_OptionTargetType_value = descriptorpb.FieldOptions_OptionTargetType_value
type MethodOptions_IdempotencyLevel = descriptorpb.MethodOptions_IdempotencyLevel type MethodOptions_IdempotencyLevel = descriptorpb.MethodOptions_IdempotencyLevel
const MethodOptions_IDEMPOTENCY_UNKNOWN = descriptorpb.MethodOptions_IDEMPOTENCY_UNKNOWN const MethodOptions_IDEMPOTENCY_UNKNOWN = descriptorpb.MethodOptions_IDEMPOTENCY_UNKNOWN
@@ -81,10 +131,77 @@ const MethodOptions_IDEMPOTENT = descriptorpb.MethodOptions_IDEMPOTENT
var MethodOptions_IdempotencyLevel_name = descriptorpb.MethodOptions_IdempotencyLevel_name var MethodOptions_IdempotencyLevel_name = descriptorpb.MethodOptions_IdempotencyLevel_name
var MethodOptions_IdempotencyLevel_value = descriptorpb.MethodOptions_IdempotencyLevel_value var MethodOptions_IdempotencyLevel_value = descriptorpb.MethodOptions_IdempotencyLevel_value
type FeatureSet_FieldPresence = descriptorpb.FeatureSet_FieldPresence
const FeatureSet_FIELD_PRESENCE_UNKNOWN = descriptorpb.FeatureSet_FIELD_PRESENCE_UNKNOWN
const FeatureSet_EXPLICIT = descriptorpb.FeatureSet_EXPLICIT
const FeatureSet_IMPLICIT = descriptorpb.FeatureSet_IMPLICIT
const FeatureSet_LEGACY_REQUIRED = descriptorpb.FeatureSet_LEGACY_REQUIRED
var FeatureSet_FieldPresence_name = descriptorpb.FeatureSet_FieldPresence_name
var FeatureSet_FieldPresence_value = descriptorpb.FeatureSet_FieldPresence_value
type FeatureSet_EnumType = descriptorpb.FeatureSet_EnumType
const FeatureSet_ENUM_TYPE_UNKNOWN = descriptorpb.FeatureSet_ENUM_TYPE_UNKNOWN
const FeatureSet_OPEN = descriptorpb.FeatureSet_OPEN
const FeatureSet_CLOSED = descriptorpb.FeatureSet_CLOSED
var FeatureSet_EnumType_name = descriptorpb.FeatureSet_EnumType_name
var FeatureSet_EnumType_value = descriptorpb.FeatureSet_EnumType_value
type FeatureSet_RepeatedFieldEncoding = descriptorpb.FeatureSet_RepeatedFieldEncoding
const FeatureSet_REPEATED_FIELD_ENCODING_UNKNOWN = descriptorpb.FeatureSet_REPEATED_FIELD_ENCODING_UNKNOWN
const FeatureSet_PACKED = descriptorpb.FeatureSet_PACKED
const FeatureSet_EXPANDED = descriptorpb.FeatureSet_EXPANDED
var FeatureSet_RepeatedFieldEncoding_name = descriptorpb.FeatureSet_RepeatedFieldEncoding_name
var FeatureSet_RepeatedFieldEncoding_value = descriptorpb.FeatureSet_RepeatedFieldEncoding_value
type FeatureSet_Utf8Validation = descriptorpb.FeatureSet_Utf8Validation
const FeatureSet_UTF8_VALIDATION_UNKNOWN = descriptorpb.FeatureSet_UTF8_VALIDATION_UNKNOWN
const FeatureSet_VERIFY = descriptorpb.FeatureSet_VERIFY
const FeatureSet_NONE = descriptorpb.FeatureSet_NONE
var FeatureSet_Utf8Validation_name = descriptorpb.FeatureSet_Utf8Validation_name
var FeatureSet_Utf8Validation_value = descriptorpb.FeatureSet_Utf8Validation_value
type FeatureSet_MessageEncoding = descriptorpb.FeatureSet_MessageEncoding
const FeatureSet_MESSAGE_ENCODING_UNKNOWN = descriptorpb.FeatureSet_MESSAGE_ENCODING_UNKNOWN
const FeatureSet_LENGTH_PREFIXED = descriptorpb.FeatureSet_LENGTH_PREFIXED
const FeatureSet_DELIMITED = descriptorpb.FeatureSet_DELIMITED
var FeatureSet_MessageEncoding_name = descriptorpb.FeatureSet_MessageEncoding_name
var FeatureSet_MessageEncoding_value = descriptorpb.FeatureSet_MessageEncoding_value
type FeatureSet_JsonFormat = descriptorpb.FeatureSet_JsonFormat
const FeatureSet_JSON_FORMAT_UNKNOWN = descriptorpb.FeatureSet_JSON_FORMAT_UNKNOWN
const FeatureSet_ALLOW = descriptorpb.FeatureSet_ALLOW
const FeatureSet_LEGACY_BEST_EFFORT = descriptorpb.FeatureSet_LEGACY_BEST_EFFORT
var FeatureSet_JsonFormat_name = descriptorpb.FeatureSet_JsonFormat_name
var FeatureSet_JsonFormat_value = descriptorpb.FeatureSet_JsonFormat_value
type GeneratedCodeInfo_Annotation_Semantic = descriptorpb.GeneratedCodeInfo_Annotation_Semantic
const GeneratedCodeInfo_Annotation_NONE = descriptorpb.GeneratedCodeInfo_Annotation_NONE
const GeneratedCodeInfo_Annotation_SET = descriptorpb.GeneratedCodeInfo_Annotation_SET
const GeneratedCodeInfo_Annotation_ALIAS = descriptorpb.GeneratedCodeInfo_Annotation_ALIAS
var GeneratedCodeInfo_Annotation_Semantic_name = descriptorpb.GeneratedCodeInfo_Annotation_Semantic_name
var GeneratedCodeInfo_Annotation_Semantic_value = descriptorpb.GeneratedCodeInfo_Annotation_Semantic_value
type FileDescriptorSet = descriptorpb.FileDescriptorSet type FileDescriptorSet = descriptorpb.FileDescriptorSet
type FileDescriptorProto = descriptorpb.FileDescriptorProto type FileDescriptorProto = descriptorpb.FileDescriptorProto
type DescriptorProto = descriptorpb.DescriptorProto type DescriptorProto = descriptorpb.DescriptorProto
type ExtensionRangeOptions = descriptorpb.ExtensionRangeOptions type ExtensionRangeOptions = descriptorpb.ExtensionRangeOptions
const Default_ExtensionRangeOptions_Verification = descriptorpb.Default_ExtensionRangeOptions_Verification
type FieldDescriptorProto = descriptorpb.FieldDescriptorProto type FieldDescriptorProto = descriptorpb.FieldDescriptorProto
type OneofDescriptorProto = descriptorpb.OneofDescriptorProto type OneofDescriptorProto = descriptorpb.OneofDescriptorProto
type EnumDescriptorProto = descriptorpb.EnumDescriptorProto type EnumDescriptorProto = descriptorpb.EnumDescriptorProto
@@ -103,7 +220,6 @@ const Default_FileOptions_OptimizeFor = descriptorpb.Default_FileOptions_Optimiz
const Default_FileOptions_CcGenericServices = descriptorpb.Default_FileOptions_CcGenericServices const Default_FileOptions_CcGenericServices = descriptorpb.Default_FileOptions_CcGenericServices
const Default_FileOptions_JavaGenericServices = descriptorpb.Default_FileOptions_JavaGenericServices const Default_FileOptions_JavaGenericServices = descriptorpb.Default_FileOptions_JavaGenericServices
const Default_FileOptions_PyGenericServices = descriptorpb.Default_FileOptions_PyGenericServices const Default_FileOptions_PyGenericServices = descriptorpb.Default_FileOptions_PyGenericServices
const Default_FileOptions_PhpGenericServices = descriptorpb.Default_FileOptions_PhpGenericServices
const Default_FileOptions_Deprecated = descriptorpb.Default_FileOptions_Deprecated const Default_FileOptions_Deprecated = descriptorpb.Default_FileOptions_Deprecated
const Default_FileOptions_CcEnableArenas = descriptorpb.Default_FileOptions_CcEnableArenas const Default_FileOptions_CcEnableArenas = descriptorpb.Default_FileOptions_CcEnableArenas
@@ -118,8 +234,10 @@ type FieldOptions = descriptorpb.FieldOptions
const Default_FieldOptions_Ctype = descriptorpb.Default_FieldOptions_Ctype const Default_FieldOptions_Ctype = descriptorpb.Default_FieldOptions_Ctype
const Default_FieldOptions_Jstype = descriptorpb.Default_FieldOptions_Jstype const Default_FieldOptions_Jstype = descriptorpb.Default_FieldOptions_Jstype
const Default_FieldOptions_Lazy = descriptorpb.Default_FieldOptions_Lazy const Default_FieldOptions_Lazy = descriptorpb.Default_FieldOptions_Lazy
const Default_FieldOptions_UnverifiedLazy = descriptorpb.Default_FieldOptions_UnverifiedLazy
const Default_FieldOptions_Deprecated = descriptorpb.Default_FieldOptions_Deprecated const Default_FieldOptions_Deprecated = descriptorpb.Default_FieldOptions_Deprecated
const Default_FieldOptions_Weak = descriptorpb.Default_FieldOptions_Weak const Default_FieldOptions_Weak = descriptorpb.Default_FieldOptions_Weak
const Default_FieldOptions_DebugRedact = descriptorpb.Default_FieldOptions_DebugRedact
type OneofOptions = descriptorpb.OneofOptions type OneofOptions = descriptorpb.OneofOptions
type EnumOptions = descriptorpb.EnumOptions type EnumOptions = descriptorpb.EnumOptions
@@ -129,6 +247,7 @@ const Default_EnumOptions_Deprecated = descriptorpb.Default_EnumOptions_Deprecat
type EnumValueOptions = descriptorpb.EnumValueOptions type EnumValueOptions = descriptorpb.EnumValueOptions
const Default_EnumValueOptions_Deprecated = descriptorpb.Default_EnumValueOptions_Deprecated const Default_EnumValueOptions_Deprecated = descriptorpb.Default_EnumValueOptions_Deprecated
const Default_EnumValueOptions_DebugRedact = descriptorpb.Default_EnumValueOptions_DebugRedact
type ServiceOptions = descriptorpb.ServiceOptions type ServiceOptions = descriptorpb.ServiceOptions
@@ -140,12 +259,17 @@ const Default_MethodOptions_Deprecated = descriptorpb.Default_MethodOptions_Depr
const Default_MethodOptions_IdempotencyLevel = descriptorpb.Default_MethodOptions_IdempotencyLevel const Default_MethodOptions_IdempotencyLevel = descriptorpb.Default_MethodOptions_IdempotencyLevel
type UninterpretedOption = descriptorpb.UninterpretedOption type UninterpretedOption = descriptorpb.UninterpretedOption
type FeatureSet = descriptorpb.FeatureSet
type FeatureSetDefaults = descriptorpb.FeatureSetDefaults
type SourceCodeInfo = descriptorpb.SourceCodeInfo type SourceCodeInfo = descriptorpb.SourceCodeInfo
type GeneratedCodeInfo = descriptorpb.GeneratedCodeInfo type GeneratedCodeInfo = descriptorpb.GeneratedCodeInfo
type DescriptorProto_ExtensionRange = descriptorpb.DescriptorProto_ExtensionRange type DescriptorProto_ExtensionRange = descriptorpb.DescriptorProto_ExtensionRange
type DescriptorProto_ReservedRange = descriptorpb.DescriptorProto_ReservedRange type DescriptorProto_ReservedRange = descriptorpb.DescriptorProto_ReservedRange
type ExtensionRangeOptions_Declaration = descriptorpb.ExtensionRangeOptions_Declaration
type EnumDescriptorProto_EnumReservedRange = descriptorpb.EnumDescriptorProto_EnumReservedRange type EnumDescriptorProto_EnumReservedRange = descriptorpb.EnumDescriptorProto_EnumReservedRange
type FieldOptions_EditionDefault = descriptorpb.FieldOptions_EditionDefault
type UninterpretedOption_NamePart = descriptorpb.UninterpretedOption_NamePart type UninterpretedOption_NamePart = descriptorpb.UninterpretedOption_NamePart
type FeatureSetDefaults_FeatureSetEditionDefault = descriptorpb.FeatureSetDefaults_FeatureSetEditionDefault
type SourceCodeInfo_Location = descriptorpb.SourceCodeInfo_Location type SourceCodeInfo_Location = descriptorpb.SourceCodeInfo_Location
type GeneratedCodeInfo_Annotation = descriptorpb.GeneratedCodeInfo_Annotation type GeneratedCodeInfo_Annotation = descriptorpb.GeneratedCodeInfo_Annotation

View File

@@ -0,0 +1,62 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// source: github.com/golang/protobuf/ptypes/empty/empty.proto
package empty
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
emptypb "google.golang.org/protobuf/types/known/emptypb"
reflect "reflect"
)
// Symbols defined in public import of google/protobuf/empty.proto.
type Empty = emptypb.Empty
var File_github_com_golang_protobuf_ptypes_empty_empty_proto protoreflect.FileDescriptor
var file_github_com_golang_protobuf_ptypes_empty_empty_proto_rawDesc = []byte{
0x0a, 0x33, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x67, 0x6f, 0x6c,
0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x70, 0x74, 0x79,
0x70, 0x65, 0x73, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1b, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f,
0x74, 0x6f, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
0x2f, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
0x2f, 0x70, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2f, 0x65, 0x6d, 0x70, 0x74, 0x79, 0x3b, 0x65, 0x6d,
0x70, 0x74, 0x79, 0x50, 0x00, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var file_github_com_golang_protobuf_ptypes_empty_empty_proto_goTypes = []interface{}{}
var file_github_com_golang_protobuf_ptypes_empty_empty_proto_depIdxs = []int32{
0, // [0:0] is the sub-list for method output_type
0, // [0:0] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_github_com_golang_protobuf_ptypes_empty_empty_proto_init() }
func file_github_com_golang_protobuf_ptypes_empty_empty_proto_init() {
if File_github_com_golang_protobuf_ptypes_empty_empty_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_github_com_golang_protobuf_ptypes_empty_empty_proto_rawDesc,
NumEnums: 0,
NumMessages: 0,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_github_com_golang_protobuf_ptypes_empty_empty_proto_goTypes,
DependencyIndexes: file_github_com_golang_protobuf_ptypes_empty_empty_proto_depIdxs,
}.Build()
File_github_com_golang_protobuf_ptypes_empty_empty_proto = out.File
file_github_com_golang_protobuf_ptypes_empty_empty_proto_rawDesc = nil
file_github_com_golang_protobuf_ptypes_empty_empty_proto_goTypes = nil
file_github_com_golang_protobuf_ptypes_empty_empty_proto_depIdxs = nil
}

View File

@@ -9,11 +9,11 @@ It's very fast and supports common extensions.
Tutorial: https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html Tutorial: https://blog.kowalczyk.info/article/cxn3/advanced-markdown-processing-in-go.html
Code examples: Code examples:
* https://onlinetool.io/goplayground/#txO7hJ-ibeU : basic markdown => HTML * https://tools.arslexis.io/goplayground/#txO7hJ-ibeU : basic markdown => HTML
* https://onlinetool.io/goplayground/#yFRIWRiu-KL : customize HTML renderer * https://tools.arslexis.io/goplayground/#yFRIWRiu-KL : customize HTML renderer
* https://onlinetool.io/goplayground/#2yV5-HDKBUV : modify AST * https://tools.arslexis.io/goplayground/#2yV5-HDKBUV : modify AST
* https://onlinetool.io/goplayground/#9fqKwRbuJ04 : customize parser * https://tools.arslexis.io/goplayground/#9fqKwRbuJ04 : customize parser
* https://onlinetool.io/goplayground/#Bk0zTvrzUDR : syntax highlight * https://tools.arslexis.io/goplayground/#Bk0zTvrzUDR : syntax highlight
Those examples are also in [examples](./examples) directory. Those examples are also in [examples](./examples) directory.
@@ -226,7 +226,7 @@ implements the following extensions:
- **Hard line breaks**. With this extension enabled newlines in the input - **Hard line breaks**. With this extension enabled newlines in the input
translates into line breaks in the output. This extension is off by default. translates into line breaks in the output. This extension is off by default.
- **Non blocking space**. With this extension enabled spaces preceeded by a backslash - **Non blocking space**. With this extension enabled spaces preceded by a backslash
in the input translates non-blocking spaces in the output. This extension is off by default. in the input translates non-blocking spaces in the output. This extension is off by default.
- **Smart quotes**. Smartypants-style punctuation substitution is - **Smart quotes**. Smartypants-style punctuation substitution is

View File

@@ -191,6 +191,11 @@ func (p *Parser) Block(data []byte) {
// <div> // <div>
// ... // ...
// </div> // </div>
if len(data) == 0 {
continue
}
if data[0] == '<' { if data[0] == '<' {
if i := p.html(data, true); i > 0 { if i := p.html(data, true); i > 0 {
data = data[i:] data = data[i:]
@@ -393,7 +398,7 @@ func (p *Parser) AddBlock(n ast.Node) ast.Node {
} }
func (p *Parser) isPrefixHeading(data []byte) bool { func (p *Parser) isPrefixHeading(data []byte) bool {
if data[0] != '#' { if len(data) > 0 && data[0] != '#' {
return false return false
} }

View File

@@ -65,6 +65,11 @@ func citation(p *Parser, data []byte, offset int) (int, ast.Node) {
} }
citeType := ast.CitationTypeInformative citeType := ast.CitationTypeInformative
if len(citation) < 2 {
continue
}
j = 1 j = 1
switch citation[j] { switch citation[j] {
case '!': case '!':

View File

@@ -736,7 +736,7 @@ func leftAngle(p *Parser, data []byte, offset int) (int, ast.Node) {
} }
// '\\' backslash escape // '\\' backslash escape
var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~^") var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~^$")
func escape(p *Parser, data []byte, offset int) (int, ast.Node) { func escape(p *Parser, data []byte, offset int) (int, ast.Node) {
data = data[offset:] data = data[offset:]

View File

@@ -56,7 +56,7 @@ const (
) )
// for each character that triggers a response when parsing inline data. // for each character that triggers a response when parsing inline data.
type inlineParser func(p *Parser, data []byte, offset int) (int, ast.Node) type InlineParser func(p *Parser, data []byte, offset int) (int, ast.Node)
// ReferenceOverrideFunc is expected to be called with a reference string and // ReferenceOverrideFunc is expected to be called with a reference string and
// return either a valid Reference type that the reference string maps to or // return either a valid Reference type that the reference string maps to or
@@ -98,7 +98,7 @@ type Parser struct {
refs map[string]*reference refs map[string]*reference
refsRecord map[string]struct{} refsRecord map[string]struct{}
inlineCallback [256]inlineParser inlineCallback [256]InlineParser
nesting int nesting int
maxNesting int maxNesting int
insideLink bool insideLink bool
@@ -181,6 +181,12 @@ func NewWithExtensions(extension Extensions) *Parser {
return &p return &p
} }
func (p *Parser) RegisterInline(n byte, fn InlineParser) InlineParser {
prev := p.inlineCallback[n]
p.inlineCallback[n] = fn
return prev
}
func (p *Parser) getRef(refid string) (ref *reference, found bool) { func (p *Parser) getRef(refid string) (ref *reference, found bool) {
if p.ReferenceOverride != nil { if p.ReferenceOverride != nil {
r, overridden := p.ReferenceOverride(refid) r, overridden := p.ReferenceOverride(refid)
@@ -901,6 +907,9 @@ func isListItem(d ast.Node) bool {
} }
func NormalizeNewlines(d []byte) []byte { func NormalizeNewlines(d []byte) []byte {
res := make([]byte, len(d))
copy(res, d)
d = res
wi := 0 wi := 0
n := len(d) n := len(d)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {

View File

@@ -1,9 +0,0 @@
language: go
go:
- 1.4.3
- 1.5.3
- tip
script:
- go test -v ./...

41
vendor/github.com/google/uuid/CHANGELOG.md generated vendored Normal file
View File

@@ -0,0 +1,41 @@
# Changelog
## [1.6.0](https://github.com/google/uuid/compare/v1.5.0...v1.6.0) (2024-01-16)
### Features
* add Max UUID constant ([#149](https://github.com/google/uuid/issues/149)) ([c58770e](https://github.com/google/uuid/commit/c58770eb495f55fe2ced6284f93c5158a62e53e3))
### Bug Fixes
* fix typo in version 7 uuid documentation ([#153](https://github.com/google/uuid/issues/153)) ([016b199](https://github.com/google/uuid/commit/016b199544692f745ffc8867b914129ecb47ef06))
* Monotonicity in UUIDv7 ([#150](https://github.com/google/uuid/issues/150)) ([a2b2b32](https://github.com/google/uuid/commit/a2b2b32373ff0b1a312b7fdf6d38a977099698a6))
## [1.5.0](https://github.com/google/uuid/compare/v1.4.0...v1.5.0) (2023-12-12)
### Features
* Validate UUID without creating new UUID ([#141](https://github.com/google/uuid/issues/141)) ([9ee7366](https://github.com/google/uuid/commit/9ee7366e66c9ad96bab89139418a713dc584ae29))
## [1.4.0](https://github.com/google/uuid/compare/v1.3.1...v1.4.0) (2023-10-26)
### Features
* UUIDs slice type with Strings() convenience method ([#133](https://github.com/google/uuid/issues/133)) ([cd5fbbd](https://github.com/google/uuid/commit/cd5fbbdd02f3e3467ac18940e07e062be1f864b4))
### Fixes
* Clarify that Parse's job is to parse but not necessarily validate strings. (Documents current behavior)
## [1.3.1](https://github.com/google/uuid/compare/v1.3.0...v1.3.1) (2023-08-18)
### Bug Fixes
* Use .EqualFold() to parse urn prefixed UUIDs ([#118](https://github.com/google/uuid/issues/118)) ([574e687](https://github.com/google/uuid/commit/574e6874943741fb99d41764c705173ada5293f0))
## Changelog

View File

@@ -2,6 +2,22 @@
We definitely welcome patches and contribution to this project! We definitely welcome patches and contribution to this project!
### Tips
Commits must be formatted according to the [Conventional Commits Specification](https://www.conventionalcommits.org).
Always try to include a test case! If it is not possible or not necessary,
please explain why in the pull request description.
### Releasing
Commits that would precipitate a SemVer change, as described in the Conventional
Commits Specification, will trigger [`release-please`](https://github.com/google-github-actions/release-please-action)
to create a release candidate pull request. Once submitted, `release-please`
will create a release.
For tips on how to work with `release-please`, see its documentation.
### Legal requirements ### Legal requirements
In order to protect both you and ourselves, you will need to sign the In order to protect both you and ourselves, you will need to sign the

View File

@@ -1,6 +1,6 @@
# uuid ![build status](https://travis-ci.org/google/uuid.svg?branch=master) # uuid
The uuid package generates and inspects UUIDs based on The uuid package generates and inspects UUIDs based on
[RFC 4122](http://tools.ietf.org/html/rfc4122) [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122)
and DCE 1.1: Authentication and Security Services. and DCE 1.1: Authentication and Security Services.
This package is based on the github.com/pborman/uuid package (previously named This package is based on the github.com/pborman/uuid package (previously named
@@ -9,10 +9,12 @@ a UUID is a 16 byte array rather than a byte slice. One loss due to this
change is the ability to represent an invalid UUID (vs a NIL UUID). change is the ability to represent an invalid UUID (vs a NIL UUID).
###### Install ###### Install
`go get github.com/google/uuid` ```sh
go get github.com/google/uuid
```
###### Documentation ###### Documentation
[![GoDoc](https://godoc.org/github.com/google/uuid?status.svg)](http://godoc.org/github.com/google/uuid) [![Go Reference](https://pkg.go.dev/badge/github.com/google/uuid.svg)](https://pkg.go.dev/github.com/google/uuid)
Full `go doc` style documentation for the package can be viewed online without Full `go doc` style documentation for the package can be viewed online without
installing this package by using the GoDoc site here: installing this package by using the GoDoc site here:

View File

@@ -17,6 +17,12 @@ var (
NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8")) NameSpaceOID = Must(Parse("6ba7b812-9dad-11d1-80b4-00c04fd430c8"))
NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8")) NameSpaceX500 = Must(Parse("6ba7b814-9dad-11d1-80b4-00c04fd430c8"))
Nil UUID // empty UUID, all zeros Nil UUID // empty UUID, all zeros
// The Max UUID is special form of UUID that is specified to have all 128 bits set to 1.
Max = UUID{
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
}
) )
// NewHash returns a new UUID derived from the hash of space concatenated with // NewHash returns a new UUID derived from the hash of space concatenated with

View File

@@ -7,6 +7,6 @@
package uuid package uuid
// getHardwareInterface returns nil values for the JS version of the code. // getHardwareInterface returns nil values for the JS version of the code.
// This remvoves the "net" dependency, because it is not used in the browser. // This removes the "net" dependency, because it is not used in the browser.
// Using the "net" library inflates the size of the transpiled JS code by 673k bytes. // Using the "net" library inflates the size of the transpiled JS code by 673k bytes.
func getHardwareInterface(name string) (string, []byte) { return "", nil } func getHardwareInterface(name string) (string, []byte) { return "", nil }

View File

@@ -108,12 +108,23 @@ func setClockSequence(seq int) {
} }
// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in // Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
// uuid. The time is only defined for version 1 and 2 UUIDs. // uuid. The time is only defined for version 1, 2, 6 and 7 UUIDs.
func (uuid UUID) Time() Time { func (uuid UUID) Time() Time {
time := int64(binary.BigEndian.Uint32(uuid[0:4])) var t Time
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32 switch uuid.Version() {
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48 case 6:
return Time(time) time := binary.BigEndian.Uint64(uuid[:8]) // Ignore uuid[6] version b0110
t = Time(time)
case 7:
time := binary.BigEndian.Uint64(uuid[:8])
t = Time((time>>16)*10000 + g1582ns100)
default: // forward compatible
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
t = Time(time)
}
return t
} }
// ClockSequence returns the clock sequence encoded in uuid. // ClockSequence returns the clock sequence encoded in uuid.

View File

@@ -56,11 +56,15 @@ func IsInvalidLengthError(err error) bool {
return ok return ok
} }
// Parse decodes s into a UUID or returns an error. Both the standard UUID // Parse decodes s into a UUID or returns an error if it cannot be parsed. Both
// forms of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and // the standard UUID forms defined in RFC 4122
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx are decoded as well as the // (xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx and
// Microsoft encoding {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx} and the raw hex // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx) are decoded. In addition,
// encoding: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. // Parse accepts non-standard strings such as the raw hex encoding
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx and 38 byte "Microsoft style" encodings,
// e.g. {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}. Only the middle 36 bytes are
// examined in the latter case. Parse should not be used to validate strings as
// it parses non-standard encodings as indicated above.
func Parse(s string) (UUID, error) { func Parse(s string) (UUID, error) {
var uuid UUID var uuid UUID
switch len(s) { switch len(s) {
@@ -69,7 +73,7 @@ func Parse(s string) (UUID, error) {
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9: case 36 + 9:
if strings.ToLower(s[:9]) != "urn:uuid:" { if !strings.EqualFold(s[:9], "urn:uuid:") {
return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9]) return uuid, fmt.Errorf("invalid urn prefix: %q", s[:9])
} }
s = s[9:] s = s[9:]
@@ -101,7 +105,8 @@ func Parse(s string) (UUID, error) {
9, 11, 9, 11,
14, 16, 14, 16,
19, 21, 19, 21,
24, 26, 28, 30, 32, 34} { 24, 26, 28, 30, 32, 34,
} {
v, ok := xtob(s[x], s[x+1]) v, ok := xtob(s[x], s[x+1])
if !ok { if !ok {
return uuid, errors.New("invalid UUID format") return uuid, errors.New("invalid UUID format")
@@ -117,7 +122,7 @@ func ParseBytes(b []byte) (UUID, error) {
switch len(b) { switch len(b) {
case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx case 36: // xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx case 36 + 9: // urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
if !bytes.Equal(bytes.ToLower(b[:9]), []byte("urn:uuid:")) { if !bytes.EqualFold(b[:9], []byte("urn:uuid:")) {
return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9]) return uuid, fmt.Errorf("invalid urn prefix: %q", b[:9])
} }
b = b[9:] b = b[9:]
@@ -145,7 +150,8 @@ func ParseBytes(b []byte) (UUID, error) {
9, 11, 9, 11,
14, 16, 14, 16,
19, 21, 19, 21,
24, 26, 28, 30, 32, 34} { 24, 26, 28, 30, 32, 34,
} {
v, ok := xtob(b[x], b[x+1]) v, ok := xtob(b[x], b[x+1])
if !ok { if !ok {
return uuid, errors.New("invalid UUID format") return uuid, errors.New("invalid UUID format")
@@ -180,6 +186,59 @@ func Must(uuid UUID, err error) UUID {
return uuid return uuid
} }
// Validate returns an error if s is not a properly formatted UUID in one of the following formats:
// xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
// {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
// It returns an error if the format is invalid, otherwise nil.
func Validate(s string) error {
switch len(s) {
// Standard UUID format
case 36:
// UUID with "urn:uuid:" prefix
case 36 + 9:
if !strings.EqualFold(s[:9], "urn:uuid:") {
return fmt.Errorf("invalid urn prefix: %q", s[:9])
}
s = s[9:]
// UUID enclosed in braces
case 36 + 2:
if s[0] != '{' || s[len(s)-1] != '}' {
return fmt.Errorf("invalid bracketed UUID format")
}
s = s[1 : len(s)-1]
// UUID without hyphens
case 32:
for i := 0; i < len(s); i += 2 {
_, ok := xtob(s[i], s[i+1])
if !ok {
return errors.New("invalid UUID format")
}
}
default:
return invalidLengthError{len(s)}
}
// Check for standard UUID format
if len(s) == 36 {
if s[8] != '-' || s[13] != '-' || s[18] != '-' || s[23] != '-' {
return errors.New("invalid UUID format")
}
for _, x := range []int{0, 2, 4, 6, 9, 11, 14, 16, 19, 21, 24, 26, 28, 30, 32, 34} {
if _, ok := xtob(s[x], s[x+1]); !ok {
return errors.New("invalid UUID format")
}
}
}
return nil
}
// String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx // String returns the string form of uuid, xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
// , or "" if uuid is invalid. // , or "" if uuid is invalid.
func (uuid UUID) String() string { func (uuid UUID) String() string {
@@ -292,3 +351,15 @@ func DisableRandPool() {
poolMu.Lock() poolMu.Lock()
poolPos = randPoolSize poolPos = randPoolSize
} }
// UUIDs is a slice of UUID types.
type UUIDs []UUID
// Strings returns a string slice containing the string form of each UUID in uuids.
func (uuids UUIDs) Strings() []string {
var uuidStrs = make([]string, len(uuids))
for i, uuid := range uuids {
uuidStrs[i] = uuid.String()
}
return uuidStrs
}

56
vendor/github.com/google/uuid/version6.go generated vendored Normal file
View File

@@ -0,0 +1,56 @@
// Copyright 2023 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import "encoding/binary"
// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality.
// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs.
// Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead.
//
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#uuidv6
//
// NewV6 returns a Version 6 UUID based on the current NodeID and clock
// sequence, and the current time. If the NodeID has not been set by SetNodeID
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewV6 set NodeID is random bits automatically . If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewV6 returns Nil and an error.
func NewV6() (UUID, error) {
var uuid UUID
now, seq, err := GetTime()
if err != nil {
return uuid, err
}
/*
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_high |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_mid | time_low_and_version |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|clk_seq_hi_res | clk_seq_low | node (0-1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| node (2-5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
binary.BigEndian.PutUint64(uuid[0:], uint64(now))
binary.BigEndian.PutUint16(uuid[8:], seq)
uuid[6] = 0x60 | (uuid[6] & 0x0F)
uuid[8] = 0x80 | (uuid[8] & 0x3F)
nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()
return uuid, nil
}

104
vendor/github.com/google/uuid/version7.go generated vendored Normal file
View File

@@ -0,0 +1,104 @@
// Copyright 2023 Google Inc. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package uuid
import (
"io"
)
// UUID version 7 features a time-ordered value field derived from the widely
// implemented and well known Unix Epoch timestamp source,
// the number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded.
// As well as improved entropy characteristics over versions 1 or 6.
//
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#name-uuid-version-7
//
// Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible.
//
// NewV7 returns a Version 7 UUID based on the current time(Unix Epoch).
// Uses the randomness pool if it was enabled with EnableRandPool.
// On error, NewV7 returns Nil and an error
func NewV7() (UUID, error) {
uuid, err := NewRandom()
if err != nil {
return uuid, err
}
makeV7(uuid[:])
return uuid, nil
}
// NewV7FromReader returns a Version 7 UUID based on the current time(Unix Epoch).
// it use NewRandomFromReader fill random bits.
// On error, NewV7FromReader returns Nil and an error.
func NewV7FromReader(r io.Reader) (UUID, error) {
uuid, err := NewRandomFromReader(r)
if err != nil {
return uuid, err
}
makeV7(uuid[:])
return uuid, nil
}
// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6])
// uuid[8] already has the right version number (Variant is 10)
// see function NewV7 and NewV7FromReader
func makeV7(uuid []byte) {
/*
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a (12 bit seq) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
_ = uuid[15] // bounds check
t, s := getV7Time()
uuid[0] = byte(t >> 40)
uuid[1] = byte(t >> 32)
uuid[2] = byte(t >> 24)
uuid[3] = byte(t >> 16)
uuid[4] = byte(t >> 8)
uuid[5] = byte(t)
uuid[6] = 0x70 | (0x0F & byte(s>>8))
uuid[7] = byte(s)
}
// lastV7time is the last time we returned stored as:
//
// 52 bits of time in milliseconds since epoch
// 12 bits of (fractional nanoseconds) >> 8
var lastV7time int64
const nanoPerMilli = 1000000
// getV7Time returns the time in milliseconds and nanoseconds / 256.
// The returned (milli << 12 + seq) is guarenteed to be greater than
// (milli << 12 + seq) returned by any previous call to getV7Time.
func getV7Time() (milli, seq int64) {
timeMu.Lock()
defer timeMu.Unlock()
nano := timeNow().UnixNano()
milli = nano / nanoPerMilli
// Sequence number is between 0 and 3906 (nanoPerMilli>>8)
seq = (nano - milli*nanoPerMilli) >> 8
now := milli<<12 + seq
if now <= lastV7time {
now = lastV7time + 1
milli = now >> 12
seq = now & 0xfff
}
lastV7time = now
return milli, seq
}

Some files were not shown because too many files have changed in this diff Show More