pax_global_header00006660000000000000000000000064143074103310014506gustar00rootroot0000000000000052 comment=6f5eda2a6c1e50c8430bb07657bbe8be0def2b37 slack-0.11.3/000077500000000000000000000000001430741033100126655ustar00rootroot00000000000000slack-0.11.3/.github/000077500000000000000000000000001430741033100142255ustar00rootroot00000000000000slack-0.11.3/.github/ISSUE_TEMPLATE/000077500000000000000000000000001430741033100164105ustar00rootroot00000000000000slack-0.11.3/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000003271430741033100211040ustar00rootroot00000000000000--- name: Bug Report about: Create a report to help us improve --- ### What happened ### Expected behavior ### Steps to reproduce #### reproducible code #### manifest.yaml ### Versions - Go: - slack-go/slack: slack-0.11.3/.github/ISSUE_TEMPLATE/feature_request.md000066400000000000000000000001621430741033100221340ustar00rootroot00000000000000--- name: Feature Request about: Request an enhancement --- ### Description ### (Optional) Slack's documentation slack-0.11.3/.github/pull_request_template.md000066400000000000000000000021671430741033100211740ustar00rootroot00000000000000##### Pull Request Guidelines These are recommendations for pull requests. They are strictly guidelines to help manage expectations. ##### PR preparation Run `make pr-prep` from the root of the repository to run formatting, linting and tests. ##### Should this be an issue instead - [ ] is it a convenience method? (no new functionality, streamlines some use case) - [ ] exposes a previously private type, const, method, etc. - [ ] is it application specific (caching, retry logic, rate limiting, etc) - [ ] is it performance related. ##### API changes Since API changes have to be maintained they undergo a more detailed review and are more likely to require changes. - no tests, if you're adding to the API include at least a single test of the happy case. - If you can accomplish your goal without changing the API, then do so. - dependency changes. updates are okay. adding/removing need justification. ###### Examples of API changes that do not meet guidelines: - in library cache for users. caches are use case specific. - Convenience methods for Sending Messages, update, post, ephemeral, etc. consider opening an issue instead. slack-0.11.3/.github/workflows/000077500000000000000000000000001430741033100162625ustar00rootroot00000000000000slack-0.11.3/.github/workflows/test.yml000066400000000000000000000013311430741033100177620ustar00rootroot00000000000000name: Test on: push: branches: - master pull_request: jobs: test: runs-on: ubuntu-22.04 strategy: matrix: go: - '1.17' - '1.18' - '1.19' name: test go-${{ matrix.go }} steps: - uses: actions/checkout@v3 - uses: actions/setup-go@v3 with: go-version: ${{ matrix.go }} - name: run test run: go test -v -race ./... env: GO111MODULE: on lint: runs-on: ubuntu-22.04 name: lint steps: - uses: actions/checkout@v3 - name: golangci-lint uses: golangci/golangci-lint-action@537aa1903e5d359d0b27dbc19ddd22c5087f3fbc # v3.2.0 with: version: v1.48.0 slack-0.11.3/.gitignore000066400000000000000000000000211430741033100146460ustar00rootroot00000000000000*.test *~ .idea/ slack-0.11.3/.golangci.yml000066400000000000000000000003021430741033100152440ustar00rootroot00000000000000run: timeout: 6m issues-exit-code: 1 linters: disable-all: true enable: - goimports - govet - interfacer - misspell - structcheck - unconvert issues: new: true slack-0.11.3/CHANGELOG.md000066400000000000000000000122441430741033100145010ustar00rootroot00000000000000### v0.7.0 - October 2, 2020 full differences can be viewed using `git log --oneline --decorate --color v0.6.6..v0.7.0` Thank you for many contributions! #### Breaking Changes - Add ScheduledMessage type ([#753]) - Add description field to option block object ([#783]) - Fix wrong conditional branch ([#782]) - The behavior of the user's application may change.(The current behavior is incorrect) #### Highlights - example: fix to start up a server ([#773]) - example: Add explanation how the message could be sent in a proper way ([#787]) - example: fix typo in error log ([#779]) - refactor: Make GetConversationsParameters.ExcludeArchived optional ([#791]) - refactor: Unify variables to "config" ([#800]) - refactor: Rename wrong file name ([#810]) - feature: Add SetUserRealName for change user's realName([#755]) - feature: Add response metadata to slack response ([#772]) - feature: Add response metadata to slack response ([#778]) - feature: Add select block element conversations filter field ([#790]) - feature: Add Root field to MessageEvent to support thread_broadcast subtype ([#793]) - feature: Add bot_profile to messages ([#794]) - doc: Add logo to README ([#813]) - doc: Update current project status and Add changelog for v0.7.0 ([#814]) [#753]: https://github.com/slack-go/slack/pull/753 [#755]: https://github.com/slack-go/slack/pull/755 [#772]: https://github.com/slack-go/slack/pull/772 [#773]: https://github.com/slack-go/slack/pull/773 [#778]: https://github.com/slack-go/slack/pull/778 [#779]: https://github.com/slack-go/slack/pull/779 [#782]: https://github.com/slack-go/slack/pull/782 [#783]: https://github.com/slack-go/slack/pull/783 [#787]: https://github.com/slack-go/slack/pull/787 [#790]: https://github.com/slack-go/slack/pull/790 [#791]: https://github.com/slack-go/slack/pull/791 [#793]: https://github.com/slack-go/slack/pull/793 [#794]: https://github.com/slack-go/slack/pull/794 [#800]: https://github.com/slack-go/slack/pull/800 [#810]: https://github.com/slack-go/slack/pull/810 [#813]: https://github.com/slack-go/slack/pull/813 [#814]: https://github.com/slack-go/slack/pull/814 ### v0.6.0 - August 31, 2019 full differences can be viewed using `git log --oneline --decorate --color v0.5.0..v0.6.0` thanks to everyone who has contributed since January! #### Breaking Changes: - Info struct has had fields removed related to deprecated functionality by slack. - minor adjustments to some structs. - some internal default values have changed, usually to be more inline with slack defaults or to correct inability to set a particular value. (Message Parse for example.) ##### Highlights: - new slacktest package easy mocking for slack client. use, enjoy, please submit PRs for improvements and default behaviours! shamelessly taken from the [slack-test repo](https://github.com/lusis/slack-test) thank you lusis for letting us use it and bring it into the slack repo. - blocks, blocks, blocks. - RTM ManagedConnection has undergone a significant cleanup. in particular handles backoffs gracefully, removed many deadlocks, and Disconnect is now much more responsive. ### v0.5.0 - January 20, 2019 full differences can be viewed using `git log --oneline --decorate --color v0.4.0..v0.5.0` - Breaking changes: various old struct fields have been removed or updated to match slack's api. - deadlock fix in RTM disconnect. ### v0.4.0 - October 06, 2018 full differences can be viewed using `git log --oneline --decorate --color v0.3.0..v0.4.0` - Breaking Change: renamed ApplyMessageOption, to mark it as unsafe, this means it may break without warning in the future. - Breaking: Msg structure files field changed to an array. - General: implementation for new security headers. - RTM: deadlock fix between connect/disconnect. - Events: various new fields added. - Web: various fixes, new fields exposed, new methods added. - Interactions: minor additions expect breaking changes in next release for dialogs/button clicks. - Utils: new methods added. ### v0.3.0 - July 30, 2018 full differences can be viewed using `git log --oneline --decorate --color v0.2.0..v0.3.0` - slack events initial support added. (still considered experimental and undergoing changes, stability not promised) - vendored depedencies using dep, ensure using up to date tooling before filing issues. - RTM has improved its ability to identify dead connections and reconnect automatically (worth calling out in case it has unintended side effects). - bug fixes (various timestamp handling, error handling, RTM locking, etc). ### v0.2.0 - Feb 10, 2018 Release adds a bunch of functionality and improvements, mainly to give people a recent version to vendor against. Please check [0.2.0](https://github.com/nlopes/slack/releases/tag/v0.2.0) ### v0.1.0 - May 28, 2017 This is released before adding context support. As the used context package is the one from Go 1.7 this will be the last compatible with Go < 1.7. Please check [0.1.0](https://github.com/nlopes/slack/releases/tag/v0.1.0) ### v0.0.1 - Jul 26, 2015 If you just updated from master and it broke your implementation, please check [0.0.1](https://github.com/nlopes/slack/releases/tag/v0.0.1) slack-0.11.3/LICENSE000066400000000000000000000024151430741033100136740ustar00rootroot00000000000000Copyright (c) 2015, Norberto Lopes All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. slack-0.11.3/Makefile000066400000000000000000000017561430741033100143360ustar00rootroot00000000000000.PHONY: help deps fmt lint test test-race test-integration help: @echo "" @echo "Welcome to slack-go/slack make." @echo "The following commands are available:" @echo "" @echo " make deps : Fetch all dependencies" @echo " make fmt : Run go fmt to fix any formatting issues" @echo " make lint : Use go vet to check for linting issues" @echo " make test : Run all short tests" @echo " make test-race : Run all tests with race condition checking" @echo " make test-integration : Run all tests without limiting to short" @echo "" @echo " make pr-prep : Run this before making a PR to run fmt, lint and tests" @echo "" deps: @go mod tidy fmt: @go fmt . lint: @go vet . test: @go test -v -count=1 -timeout 300s -short ./... test-race: @go test -v -count=1 -timeout 300s -short -race ./... test-integration: @go test -v -count=1 -timeout 600s ./... pr-prep: fmt lint test-race test-integration slack-0.11.3/README.md000066400000000000000000000060471430741033100141530ustar00rootroot00000000000000Slack API in Go [![Go Reference](https://pkg.go.dev/badge/github.com/slack-go/slack.svg)](https://pkg.go.dev/github.com/slack-go/slack) =============== This is the original Slack library for Go created by Norberto Lopes, transferred to a GitHub organization. You can also chat with us on the #slack-go, #slack-go-ja Slack channel on the Gophers Slack. ![logo](logo.png "icon") This library supports most if not all of the `api.slack.com` REST calls, as well as the Real-Time Messaging protocol over websocket, in a fully managed way. ## Project Status There is currently no major version released. Therefore, minor version releases may include backward incompatible changes. See [CHANGELOG.md](https://github.com/slack-go/slack/blob/master/CHANGELOG.md) or [Releases](https://github.com/slack-go/slack/releases) for more information about the changes. ## Installing ### *go get* $ go get -u github.com/slack-go/slack ## Example ### Getting all groups ```golang import ( "fmt" "github.com/slack-go/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") // If you set debugging, it will log all requests to the console // Useful when encountering issues // slack.New("YOUR_TOKEN_HERE", slack.OptionDebug(true)) groups, err := api.GetUserGroups(slack.GetUserGroupsOptionIncludeUsers(false)) if err != nil { fmt.Printf("%s\n", err) return } for _, group := range groups { fmt.Printf("ID: %s, Name: %s\n", group.ID, group.Name) } } ``` ### Getting User Information ```golang import ( "fmt" "github.com/slack-go/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") user, err := api.GetUserInfo("U023BECGF") if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) } ``` ## Minimal Socket Mode usage: See https://github.com/slack-go/slack/blob/master/examples/socketmode/socketmode.go ## Minimal RTM usage: As mentioned in https://api.slack.com/rtm - for most applications, Socket Mode is a better way to communicate with Slack. See https://github.com/slack-go/slack/blob/master/examples/websocket/websocket.go ## Minimal EventsAPI usage: See https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go ## Socketmode Event Handler (Experimental) When using socket mode, dealing with an event can be pretty lengthy as it requires you to route the event to the right place. Instead, you can use `SocketmodeHandler` much like you use an HTTP handler to register which event you would like to listen to and what callback function will process that event when it occurs. See [./examples/socketmode_handler/socketmode_handler.go](./examples/socketmode_handler/socketmode_handler.go) ## Contributing You are more than welcome to contribute to this project. Fork and make a Pull Request, or create an Issue if you see any problem. Before making any Pull Request please run the following: ``` make pr-prep ``` This will check/update code formatting, linting and then run all tests ## License BSD 2 Clause license slack-0.11.3/TODO.txt000066400000000000000000000002051430741033100141700ustar00rootroot00000000000000- Add more tests!!! - Add support to have markdown hints - See section Message Formatting at https://api.slack.com/docs/formatting slack-0.11.3/admin.go000066400000000000000000000147551430741033100143200ustar00rootroot00000000000000package slack import ( "context" "fmt" "net/url" "strings" ) func (api *Client) adminRequest(ctx context.Context, method string, teamName string, values url.Values) error { resp := &SlackResponse{} err := parseAdminResponse(ctx, api.httpclient, method, teamName, values, resp, api) if err != nil { return err } return resp.Err() } // DisableUser disabled a user account, given a user ID func (api *Client) DisableUser(teamName string, uid string) error { return api.DisableUserContext(context.Background(), teamName, uid) } // DisableUserContext disabled a user account, given a user ID with a custom context func (api *Client) DisableUserContext(ctx context.Context, teamName string, uid string) error { values := url.Values{ "user": {uid}, "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } if err := api.adminRequest(ctx, "setInactive", teamName, values); err != nil { return fmt.Errorf("failed to disable user with id '%s': %s", uid, err) } return nil } // InviteGuest invites a user to Slack as a single-channel guest func (api *Client) InviteGuest(teamName, channel, firstName, lastName, emailAddress string) error { return api.InviteGuestContext(context.Background(), teamName, channel, firstName, lastName, emailAddress) } // InviteGuestContext invites a user to Slack as a single-channel guest with a custom context func (api *Client) InviteGuestContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error { values := url.Values{ "email": {emailAddress}, "channels": {channel}, "first_name": {firstName}, "last_name": {lastName}, "ultra_restricted": {"1"}, "token": {api.token}, "resend": {"true"}, "set_active": {"true"}, "_attempts": {"1"}, } err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { return fmt.Errorf("Failed to invite single-channel guest: %s", err) } return nil } // InviteRestricted invites a user to Slack as a restricted account func (api *Client) InviteRestricted(teamName, channel, firstName, lastName, emailAddress string) error { return api.InviteRestrictedContext(context.Background(), teamName, channel, firstName, lastName, emailAddress) } // InviteRestrictedContext invites a user to Slack as a restricted account with a custom context func (api *Client) InviteRestrictedContext(ctx context.Context, teamName, channel, firstName, lastName, emailAddress string) error { values := url.Values{ "email": {emailAddress}, "channels": {channel}, "first_name": {firstName}, "last_name": {lastName}, "restricted": {"1"}, "token": {api.token}, "resend": {"true"}, "set_active": {"true"}, "_attempts": {"1"}, } err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { return fmt.Errorf("Failed to restricted account: %s", err) } return nil } // InviteToTeam invites a user to a Slack team func (api *Client) InviteToTeam(teamName, firstName, lastName, emailAddress string) error { return api.InviteToTeamContext(context.Background(), teamName, firstName, lastName, emailAddress) } // InviteToTeamContext invites a user to a Slack team with a custom context func (api *Client) InviteToTeamContext(ctx context.Context, teamName, firstName, lastName, emailAddress string) error { values := url.Values{ "email": {emailAddress}, "first_name": {firstName}, "last_name": {lastName}, "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } err := api.adminRequest(ctx, "invite", teamName, values) if err != nil { return fmt.Errorf("Failed to invite to team: %s", err) } return nil } // SetRegular enables the specified user func (api *Client) SetRegular(teamName, user string) error { return api.SetRegularContext(context.Background(), teamName, user) } // SetRegularContext enables the specified user with a custom context func (api *Client) SetRegularContext(ctx context.Context, teamName, user string) error { values := url.Values{ "user": {user}, "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } err := api.adminRequest(ctx, "setRegular", teamName, values) if err != nil { return fmt.Errorf("Failed to change the user (%s) to a regular user: %s", user, err) } return nil } // SendSSOBindingEmail sends an SSO binding email to the specified user func (api *Client) SendSSOBindingEmail(teamName, user string) error { return api.SendSSOBindingEmailContext(context.Background(), teamName, user) } // SendSSOBindingEmailContext sends an SSO binding email to the specified user with a custom context func (api *Client) SendSSOBindingEmailContext(ctx context.Context, teamName, user string) error { values := url.Values{ "user": {user}, "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } err := api.adminRequest(ctx, "sendSSOBind", teamName, values) if err != nil { return fmt.Errorf("Failed to send SSO binding email for user (%s): %s", user, err) } return nil } // SetUltraRestricted converts a user into a single-channel guest func (api *Client) SetUltraRestricted(teamName, uid, channel string) error { return api.SetUltraRestrictedContext(context.Background(), teamName, uid, channel) } // SetUltraRestrictedContext converts a user into a single-channel guest with a custom context func (api *Client) SetUltraRestrictedContext(ctx context.Context, teamName, uid, channel string) error { values := url.Values{ "user": {uid}, "channel": {channel}, "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, } err := api.adminRequest(ctx, "setUltraRestricted", teamName, values) if err != nil { return fmt.Errorf("Failed to ultra-restrict account: %s", err) } return nil } // SetRestricted converts a user into a restricted account func (api *Client) SetRestricted(teamName, uid string, channelIds ...string) error { return api.SetRestrictedContext(context.Background(), teamName, uid, channelIds...) } // SetRestrictedContext converts a user into a restricted account with a custom context func (api *Client) SetRestrictedContext(ctx context.Context, teamName, uid string, channelIds ...string) error { values := url.Values{ "user": {uid}, "token": {api.token}, "set_active": {"true"}, "_attempts": {"1"}, "channels": {strings.Join(channelIds, ",")}, } err := api.adminRequest(ctx, "setRestricted", teamName, values) if err != nil { return fmt.Errorf("failed to restrict account: %s", err) } return nil } slack-0.11.3/apps.go000066400000000000000000000035501430741033100141620ustar00rootroot00000000000000package slack import ( "context" "encoding/json" "net/url" ) type listEventAuthorizationsResponse struct { SlackResponse Authorizations []EventAuthorization `json:"authorizations"` } type EventAuthorization struct { EnterpriseID string `json:"enterprise_id"` TeamID string `json:"team_id"` UserID string `json:"user_id"` IsBot bool `json:"is_bot"` IsEnterpriseInstall bool `json:"is_enterprise_install"` } func (api *Client) ListEventAuthorizations(eventContext string) ([]EventAuthorization, error) { return api.ListEventAuthorizationsContext(context.Background(), eventContext) } // ListEventAuthorizationsContext lists authed users and teams for the given event_context. You must provide an app-level token to the client using OptionAppLevelToken. More info: https://api.slack.com/methods/apps.event.authorizations.list func (api *Client) ListEventAuthorizationsContext(ctx context.Context, eventContext string) ([]EventAuthorization, error) { resp := &listEventAuthorizationsResponse{} request, _ := json.Marshal(map[string]string{ "event_context": eventContext, }) err := postJSON(ctx, api.httpclient, api.endpoint+"apps.event.authorizations.list", api.appLevelToken, request, &resp, api) if err != nil { return nil, err } if !resp.Ok { return nil, resp.Err() } return resp.Authorizations, nil } func (api *Client) UninstallApp(clientID, clientSecret string) error { return api.UninstallAppContext(context.Background(), clientID, clientSecret) } func (api *Client) UninstallAppContext(ctx context.Context, clientID, clientSecret string) error { values := url.Values{ "client_id": {clientID}, "client_secret": {clientSecret}, } response := SlackResponse{} err := api.getMethod(ctx, "apps.uninstall", api.token, values, &response) if err != nil { return err } return response.Err() } slack-0.11.3/apps_test.go000066400000000000000000000027421430741033100152230ustar00rootroot00000000000000package slack import ( "encoding/json" "net/http" "testing" ) func TestListEventAuthorizations(t *testing.T) { http.HandleFunc("/apps.event.authorizations.list", testListEventAuthorizationsHandler) once.Do(startServer) api := New("", OptionAppLevelToken("test-token"), OptionAPIURL("http://"+serverAddr+"/")) authorizations, err := api.ListEventAuthorizations("1-message-T012345678-DR12345678") if err != nil { t.Errorf("Failed, but should have succeeded") } else if len(authorizations) != 1 { t.Errorf("Didn't get 1 authorization") } else if authorizations[0].UserID != "U123456789" { t.Errorf("User ID is wrong") } } func testListEventAuthorizationsHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(listEventAuthorizationsResponse{ SlackResponse: SlackResponse{Ok: true}, Authorizations: []EventAuthorization{ { UserID: "U123456789", TeamID: "T012345678", }, }, }) w.Write(response) } func TestUninstallApp(t *testing.T) { http.HandleFunc("/apps.uninstall", testUninstallAppHandler) once.Do(startServer) api := New("test-token", OptionAPIURL("http://"+serverAddr+"/")) err := api.UninstallApp("", "") if err != nil { t.Errorf("Failed, but should have succeeded") } } func testUninstallAppHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(SlackResponse{Ok: true}) w.Write(response) } slack-0.11.3/attachments.go000066400000000000000000000107741430741033100155400ustar00rootroot00000000000000package slack import "encoding/json" // AttachmentField contains information for an attachment field // An Attachment can contain multiple of these type AttachmentField struct { Title string `json:"title"` Value string `json:"value"` Short bool `json:"short"` } // AttachmentAction is a button or menu to be included in the attachment. Required when // using message buttons or menus and otherwise not useful. A maximum of 5 actions may be // provided per attachment. type AttachmentAction struct { Name string `json:"name"` // Required. Text string `json:"text"` // Required. Style string `json:"style,omitempty"` // Optional. Allowed values: "default", "primary", "danger". Type ActionType `json:"type"` // Required. Must be set to "button" or "select". Value string `json:"value,omitempty"` // Optional. DataSource string `json:"data_source,omitempty"` // Optional. MinQueryLength int `json:"min_query_length,omitempty"` // Optional. Default value is 1. Options []AttachmentActionOption `json:"options,omitempty"` // Optional. Maximum of 100 options can be provided in each menu. SelectedOptions []AttachmentActionOption `json:"selected_options,omitempty"` // Optional. The first element of this array will be set as the pre-selected option for this menu. OptionGroups []AttachmentActionOptionGroup `json:"option_groups,omitempty"` // Optional. Confirm *ConfirmationField `json:"confirm,omitempty"` // Optional. URL string `json:"url,omitempty"` // Optional. } // actionType returns the type of the action func (a AttachmentAction) actionType() ActionType { return a.Type } // AttachmentActionOption the individual option to appear in action menu. type AttachmentActionOption struct { Text string `json:"text"` // Required. Value string `json:"value"` // Required. Description string `json:"description,omitempty"` // Optional. Up to 30 characters. } // AttachmentActionOptionGroup is a semi-hierarchal way to list available options to appear in action menu. type AttachmentActionOptionGroup struct { Text string `json:"text"` // Required. Options []AttachmentActionOption `json:"options"` // Required. } // AttachmentActionCallback is sent from Slack when a user clicks a button in an interactive message (aka AttachmentAction) // DEPRECATED: use InteractionCallback type AttachmentActionCallback InteractionCallback // ConfirmationField are used to ask users to confirm actions type ConfirmationField struct { Title string `json:"title,omitempty"` // Optional. Text string `json:"text"` // Required. OkText string `json:"ok_text,omitempty"` // Optional. Defaults to "Okay" DismissText string `json:"dismiss_text,omitempty"` // Optional. Defaults to "Cancel" } // Attachment contains all the information for an attachment type Attachment struct { Color string `json:"color,omitempty"` Fallback string `json:"fallback,omitempty"` CallbackID string `json:"callback_id,omitempty"` ID int `json:"id,omitempty"` AuthorID string `json:"author_id,omitempty"` AuthorName string `json:"author_name,omitempty"` AuthorSubname string `json:"author_subname,omitempty"` AuthorLink string `json:"author_link,omitempty"` AuthorIcon string `json:"author_icon,omitempty"` Title string `json:"title,omitempty"` TitleLink string `json:"title_link,omitempty"` Pretext string `json:"pretext,omitempty"` Text string `json:"text,omitempty"` ImageURL string `json:"image_url,omitempty"` ThumbURL string `json:"thumb_url,omitempty"` ServiceName string `json:"service_name,omitempty"` ServiceIcon string `json:"service_icon,omitempty"` FromURL string `json:"from_url,omitempty"` OriginalURL string `json:"original_url,omitempty"` Fields []AttachmentField `json:"fields,omitempty"` Actions []AttachmentAction `json:"actions,omitempty"` MarkdownIn []string `json:"mrkdwn_in,omitempty"` Blocks Blocks `json:"blocks,omitempty"` Footer string `json:"footer,omitempty"` FooterIcon string `json:"footer_icon,omitempty"` Ts json.Number `json:"ts,omitempty"` } slack-0.11.3/attachments_test.go000066400000000000000000000031771430741033100165760ustar00rootroot00000000000000package slack import ( "encoding/json" "strings" "testing" "github.com/go-test/deep" ) func TestAttachment_UnmarshalMarshalJSON_WithBlocks(t *testing.T) { originalAttachmentJson := `{ "id": 1, "blocks": [ { "type": "section", "block_id": "xxxx", "text": { "type": "mrkdwn", "text": "Pick something:", "verbatim": true }, "accessory": { "type": "static_select", "action_id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "placeholder": { "type": "plain_text", "text": "Select one item", "emoji": true }, "options": [ { "text": { "type": "plain_text", "text": "ghi", "emoji": true }, "value": "ghi" } ] } } ], "color": "#13A554", "fallback": "[no preview available]" }` attachment := new(Attachment) err := json.Unmarshal([]byte(originalAttachmentJson), attachment) if err != nil { t.Fatalf("expected no error unmarshaling attachment with blocks, got: %v", err) } actualAttachmentJson, err := json.Marshal(attachment) if err != nil { t.Fatal(err) } var ( actual interface{} expected interface{} ) if err = json.Unmarshal([]byte(originalAttachmentJson), &expected); err != nil { t.Fatal(err) } if err = json.Unmarshal(actualAttachmentJson, &actual); err != nil { t.Fatal(err) } if diff := deep.Equal(actual, expected); diff != nil { t.Fatal("actual does not match expected\n", strings.Join(diff, "\n")) } } slack-0.11.3/audit.go000066400000000000000000000104721430741033100143260ustar00rootroot00000000000000package slack import ( "context" "net/url" "strconv" ) type AuditLogResponse struct { Entries []AuditEntry `json:"entries"` SlackResponse } type AuditEntry struct { ID string `json:"id"` DateCreate int `json:"date_create"` Action string `json:"action"` Actor struct { Type string `json:"type"` User AuditUser `json:"user"` } `json:"actor"` Entity struct { Type string `json:"type"` // Only one of the below will be completed, based on the value of Type a user, a channel, a file, an app, a workspace, or an enterprise User AuditUser `json:"user"` Channel AuditChannel `json:"channel"` File AuditFile `json:"file"` App AuditApp `json:"app"` Workspace AuditWorkspace `json:"workspace"` Enterprise AuditEnterprise `json:"enterprise"` } `json:"entity"` Context struct { Location struct { Type string `json:"type"` ID string `json:"id"` Name string `json:"name"` Domain string `json:"domain"` } `json:"location"` UA string `json:"ua"` IPAddress string `json:"ip_address"` } `json:"context"` Details struct { NewValue interface{} `json:"new_value"` PreviousValue interface{} `json:"previous_value"` MobileOnly bool `json:"mobile_only"` WebOnly bool `json:"web_only"` NonSSOOnly bool `json:"non_sso_only"` ExportType string `json:"export_type"` ExportStart string `json:"export_start_ts"` ExportEnd string `json:"export_end_ts"` } `json:"details"` } type AuditUser struct { ID string `json:"id"` Name string `json:"name"` Email string `json:"email"` Team string `json:"team"` } type AuditChannel struct { ID string `json:"id"` Name string `json:"name"` Privacy string `json:"privacy"` IsShared bool `json:"is_shared"` IsOrgShared bool `json:"is_org_shared"` } type AuditFile struct { ID string `json:"id"` Name string `json:"name"` Filetype string `json:"filetype"` Title string `json:"title"` } type AuditApp struct { ID string `json:"id"` Name string `json:"name"` IsDistributed bool `json:"is_distributed"` IsDirectoryApproved bool `json:"is_directory_approved"` IsWorkflowApp bool `json:"is_workflow_app"` Scopes []string `json:"scopes"` } type AuditWorkspace struct { ID string `json:"id"` Name string `json:"name"` Domain string `json:"domain"` } type AuditEnterprise struct { ID string `json:"id"` Name string `json:"name"` Domain string `json:"domain"` } // AuditLogParameters contains all the parameters necessary (including the optional ones) for a GetAuditLogs() request type AuditLogParameters struct { Limit int Cursor string Latest int Oldest int Action string Actor string Entity string } func (api *Client) auditLogsRequest(ctx context.Context, path string, values url.Values) (*AuditLogResponse, error) { response := &AuditLogResponse{} err := api.getMethod(ctx, path, api.token, values, response) if err != nil { return nil, err } return response, response.Err() } // GetAuditLogs retrieves a page of audit entires according to the parameters given func (api *Client) GetAuditLogs(params AuditLogParameters) (entries []AuditEntry, nextCursor string, err error) { return api.GetAuditLogsContext(context.Background(), params) } // GetAuditLogsContext retrieves a page of audit entries according to the parameters given with a custom context func (api *Client) GetAuditLogsContext(ctx context.Context, params AuditLogParameters) (entries []AuditEntry, nextCursor string, err error) { values := url.Values{} if params.Limit != 0 { values.Add("limit", strconv.Itoa(params.Limit)) } if params.Oldest != 0 { values.Add("oldest", strconv.Itoa(params.Oldest)) } if params.Latest != 0 { values.Add("latest", strconv.Itoa(params.Latest)) } if params.Cursor != "" { values.Add("cursor", params.Cursor) } if params.Action != "" { values.Add("action", params.Action) } if params.Actor != "" { values.Add("actor", params.Actor) } if params.Entity != "" { values.Add("entity", params.Entity) } response, err := api.auditLogsRequest(ctx, "audit/v1/logs", values) if err != nil { return nil, "", err } return response.Entries, response.ResponseMetadata.Cursor, response.Err() } slack-0.11.3/audit_test.go000066400000000000000000000040141430741033100153600ustar00rootroot00000000000000package slack import ( "net/http" "testing" ) func getAuditLogs(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response := []byte(`{"entries": [ { "id": "0123a45b-6c7d-8900-e12f-3456789gh0i1", "date_create": 1521214343, "action": "user_login", "actor": { "type": "user", "user": { "id": "W123AB456", "name": "Charlie Parker", "email": "bird@slack.com" } }, "entity": { "type": "user", "user": { "id": "W123AB456", "name": "Charlie Parker", "email": "bird@slack.com" } }, "context": { "location": { "type": "enterprise", "id": "E1701NCCA", "name": "Birdland", "domain": "birdland" }, "ua": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.186 Safari/537.36", "ip_address": "1.23.45.678" } } ] }`) rw.Write(response) } func TestGetAuditLogs(t *testing.T) { http.HandleFunc("/audit/v1/logs", getAuditLogs) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) events, nextCursor, err := api.GetAuditLogs(AuditLogParameters{}) if err != nil { t.Errorf("Unexpected error: %s", err) return } if len(events) != 1 { t.Fatal("Should have been 1 event") } // test the first login event1 := events[0] if event1.Action != "user_login" { t.Fatal(ErrIncorrectResponse) } if event1.Entity.User.Email != "bird@slack.com" { t.Fatal(ErrIncorrectResponse) } if event1.Context.Location.Domain != "birdland" { t.Fatal(ErrIncorrectResponse) } if event1.DateCreate != 1521214343 { t.Fatal(ErrIncorrectResponse) } if event1.Context.IPAddress != "1.23.45.678" { t.Fatal(ErrIncorrectResponse) } if nextCursor != "" { t.Fatal(ErrIncorrectResponse) } } slack-0.11.3/auth.go000066400000000000000000000022151430741033100141550ustar00rootroot00000000000000package slack import ( "context" "net/url" ) // AuthRevokeResponse contains our Auth response from the auth.revoke endpoint type AuthRevokeResponse struct { SlackResponse // Contains the "ok", and "Error", if any Revoked bool `json:"revoked,omitempty"` } // authRequest sends the actual request, and unmarshals the response func (api *Client) authRequest(ctx context.Context, path string, values url.Values) (*AuthRevokeResponse, error) { response := &AuthRevokeResponse{} err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } return response, response.Err() } // SendAuthRevoke will send a revocation for our token func (api *Client) SendAuthRevoke(token string) (*AuthRevokeResponse, error) { return api.SendAuthRevokeContext(context.Background(), token) } // SendAuthRevokeContext will send a revocation request for our token to api.revoke with context func (api *Client) SendAuthRevokeContext(ctx context.Context, token string) (*AuthRevokeResponse, error) { if token == "" { token = api.token } values := url.Values{ "token": {token}, } return api.authRequest(ctx, "auth.revoke", values) } slack-0.11.3/block.go000066400000000000000000000061341430741033100143120ustar00rootroot00000000000000package slack // @NOTE: Blocks are in beta and subject to change. // More Information: https://api.slack.com/block-kit // MessageBlockType defines a named string type to define each block type // as a constant for use within the package. type MessageBlockType string const ( MBTSection MessageBlockType = "section" MBTDivider MessageBlockType = "divider" MBTImage MessageBlockType = "image" MBTAction MessageBlockType = "actions" MBTContext MessageBlockType = "context" MBTFile MessageBlockType = "file" MBTInput MessageBlockType = "input" MBTHeader MessageBlockType = "header" MBTRichText MessageBlockType = "rich_text" ) // Block defines an interface all block types should implement // to ensure consistency between blocks. type Block interface { BlockType() MessageBlockType } // Blocks is a convenience struct defined to allow dynamic unmarshalling of // the "blocks" value in Slack's JSON response, which varies depending on block type type Blocks struct { BlockSet []Block `json:"blocks,omitempty"` } // BlockAction is the action callback sent when a block is interacted with type BlockAction struct { ActionID string `json:"action_id"` BlockID string `json:"block_id"` Type ActionType `json:"type"` Text TextBlockObject `json:"text"` Value string `json:"value"` ActionTs string `json:"action_ts"` SelectedOption OptionBlockObject `json:"selected_option"` SelectedOptions []OptionBlockObject `json:"selected_options"` SelectedUser string `json:"selected_user"` SelectedUsers []string `json:"selected_users"` SelectedChannel string `json:"selected_channel"` SelectedChannels []string `json:"selected_channels"` SelectedConversation string `json:"selected_conversation"` SelectedConversations []string `json:"selected_conversations"` SelectedDate string `json:"selected_date"` SelectedTime string `json:"selected_time"` InitialOption OptionBlockObject `json:"initial_option"` InitialUser string `json:"initial_user"` InitialChannel string `json:"initial_channel"` InitialConversation string `json:"initial_conversation"` InitialDate string `json:"initial_date"` InitialTime string `json:"initial_time"` } // actionType returns the type of the action func (b BlockAction) actionType() ActionType { return b.Type } // NewBlockMessage creates a new Message that contains one or more blocks to be displayed func NewBlockMessage(blocks ...Block) Message { return Message{ Msg: Msg{ Blocks: Blocks{ BlockSet: blocks, }, }, } } // AddBlockMessage appends a block to the end of the existing list of blocks func AddBlockMessage(message Message, newBlk Block) Message { message.Msg.Blocks.BlockSet = append(message.Msg.Blocks.BlockSet, newBlk) return message } slack-0.11.3/block_action.go000066400000000000000000000013121430741033100156400ustar00rootroot00000000000000package slack // ActionBlock defines data that is used to hold interactive elements. // // More Information: https://api.slack.com/reference/messaging/blocks#actions type ActionBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` Elements *BlockElements `json:"elements"` } // BlockType returns the type of the block func (s ActionBlock) BlockType() MessageBlockType { return s.Type } // NewActionBlock returns a new instance of an Action Block func NewActionBlock(blockID string, elements ...BlockElement) *ActionBlock { return &ActionBlock{ Type: MBTAction, BlockID: blockID, Elements: &BlockElements{ ElementSet: elements, }, } } slack-0.11.3/block_action_test.go000066400000000000000000000007371430741033100167110ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewActionBlock(t *testing.T) { approveBtnTxt := NewTextBlockObject("plain_text", "Approve", false, false) approveBtn := NewButtonBlockElement("", "click_me_123", approveBtnTxt) actionBlock := NewActionBlock("test", approveBtn) assert.Equal(t, string(actionBlock.Type), "actions") assert.Equal(t, actionBlock.BlockID, "test") assert.Equal(t, len(actionBlock.Elements.ElementSet), 1) } slack-0.11.3/block_context.go000066400000000000000000000015761430741033100160630ustar00rootroot00000000000000package slack // ContextBlock defines data that is used to display message context, which can // include both images and text. // // More Information: https://api.slack.com/reference/messaging/blocks#context type ContextBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` ContextElements ContextElements `json:"elements"` } // BlockType returns the type of the block func (s ContextBlock) BlockType() MessageBlockType { return s.Type } type ContextElements struct { Elements []MixedElement } // NewContextBlock returns a new instance of a context block func NewContextBlock(blockID string, mixedElements ...MixedElement) *ContextBlock { elements := ContextElements{ Elements: mixedElements, } return &ContextBlock{ Type: MBTContext, BlockID: blockID, ContextElements: elements, } } slack-0.11.3/block_context_test.go000066400000000000000000000012131430741033100171060ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewContextBlock(t *testing.T) { locationPinImage := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Location Pin Icon") textExample := NewTextBlockObject("plain_text", "Location: Central Business District", true, false) elements := []MixedElement{locationPinImage, textExample} contextBlock := NewContextBlock("test", elements...) assert.Equal(t, string(contextBlock.Type), "context") assert.Equal(t, contextBlock.BlockID, "test") assert.Equal(t, len(contextBlock.ContextElements.Elements), 2) } slack-0.11.3/block_conv.go000066400000000000000000000243651430741033100153450ustar00rootroot00000000000000package slack import ( "encoding/json" "errors" "fmt" ) type sumtype struct { TypeVal string `json:"type"` } // MarshalJSON implements the Marshaller interface for Blocks so that any JSON // marshalling is delegated and proper type determination can be made before marshal func (b Blocks) MarshalJSON() ([]byte, error) { bytes, err := json.Marshal(b.BlockSet) if err != nil { return nil, err } return bytes, nil } // UnmarshalJSON implements the Unmarshaller interface for Blocks, so that any JSON // unmarshalling is delegated and proper type determination can be made before unmarshal func (b *Blocks) UnmarshalJSON(data []byte) error { var raw []json.RawMessage if string(data) == "{}" { return nil } err := json.Unmarshal(data, &raw) if err != nil { return err } var blocks Blocks for _, r := range raw { s := sumtype{} err := json.Unmarshal(r, &s) if err != nil { return err } var blockType string if s.TypeVal != "" { blockType = s.TypeVal } var block Block switch blockType { case "actions": block = &ActionBlock{} case "context": block = &ContextBlock{} case "divider": block = &DividerBlock{} case "file": block = &FileBlock{} case "header": block = &HeaderBlock{} case "image": block = &ImageBlock{} case "input": block = &InputBlock{} case "rich_text": block = &RichTextBlock{} case "section": block = &SectionBlock{} default: block = &UnknownBlock{} } err = json.Unmarshal(r, block) if err != nil { return err } blocks.BlockSet = append(blocks.BlockSet, block) } *b = blocks return nil } // UnmarshalJSON implements the Unmarshaller interface for InputBlock, so that any JSON // unmarshalling is delegated and proper type determination can be made before unmarshal func (b *InputBlock) UnmarshalJSON(data []byte) error { type alias InputBlock a := struct { Element json.RawMessage `json:"element"` *alias }{ alias: (*alias)(b), } if err := json.Unmarshal(data, &a); err != nil { return err } s := sumtype{} if err := json.Unmarshal(a.Element, &s); err != nil { return nil } var e BlockElement switch s.TypeVal { case "datepicker": e = &DatePickerBlockElement{} case "timepicker": e = &TimePickerBlockElement{} case "plain_text_input": e = &PlainTextInputBlockElement{} case "static_select", "external_select", "users_select", "conversations_select", "channels_select": e = &SelectBlockElement{} case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select": e = &MultiSelectBlockElement{} case "checkboxes": e = &CheckboxGroupsBlockElement{} case "overflow": e = &OverflowBlockElement{} case "radio_buttons": e = &RadioButtonsBlockElement{} default: return errors.New("unsupported block element type") } if err := json.Unmarshal(a.Element, e); err != nil { return err } b.Element = e return nil } // MarshalJSON implements the Marshaller interface for BlockElements so that any JSON // marshalling is delegated and proper type determination can be made before marshal func (b *BlockElements) MarshalJSON() ([]byte, error) { bytes, err := json.Marshal(b.ElementSet) if err != nil { return nil, err } return bytes, nil } // UnmarshalJSON implements the Unmarshaller interface for BlockElements, so that any JSON // unmarshalling is delegated and proper type determination can be made before unmarshal func (b *BlockElements) UnmarshalJSON(data []byte) error { var raw []json.RawMessage if string(data) == "{}" { return nil } err := json.Unmarshal(data, &raw) if err != nil { return err } var blockElements BlockElements for _, r := range raw { s := sumtype{} err := json.Unmarshal(r, &s) if err != nil { return err } var blockElementType string if s.TypeVal != "" { blockElementType = s.TypeVal } var blockElement BlockElement switch blockElementType { case "image": blockElement = &ImageBlockElement{} case "button": blockElement = &ButtonBlockElement{} case "overflow": blockElement = &OverflowBlockElement{} case "datepicker": blockElement = &DatePickerBlockElement{} case "timepicker": blockElement = &TimePickerBlockElement{} case "plain_text_input": blockElement = &PlainTextInputBlockElement{} case "checkboxes": blockElement = &CheckboxGroupsBlockElement{} case "radio_buttons": blockElement = &RadioButtonsBlockElement{} case "static_select", "external_select", "users_select", "conversations_select", "channels_select": blockElement = &SelectBlockElement{} default: return fmt.Errorf("unsupported block element type %v", blockElementType) } err = json.Unmarshal(r, blockElement) if err != nil { return err } blockElements.ElementSet = append(blockElements.ElementSet, blockElement) } *b = blockElements return nil } // MarshalJSON implements the Marshaller interface for Accessory so that any JSON // marshalling is delegated and proper type determination can be made before marshal func (a *Accessory) MarshalJSON() ([]byte, error) { bytes, err := json.Marshal(toBlockElement(a)) if err != nil { return nil, err } return bytes, nil } // UnmarshalJSON implements the Unmarshaller interface for Accessory, so that any JSON // unmarshalling is delegated and proper type determination can be made before unmarshal func (a *Accessory) UnmarshalJSON(data []byte) error { var r json.RawMessage if string(data) == "{\"accessory\":null}" { return nil } err := json.Unmarshal(data, &r) if err != nil { return err } s := sumtype{} err = json.Unmarshal(r, &s) if err != nil { return err } var blockElementType string if s.TypeVal != "" { blockElementType = s.TypeVal } switch blockElementType { case "image": element, err := unmarshalBlockElement(r, &ImageBlockElement{}) if err != nil { return err } a.ImageElement = element.(*ImageBlockElement) case "button": element, err := unmarshalBlockElement(r, &ButtonBlockElement{}) if err != nil { return err } a.ButtonElement = element.(*ButtonBlockElement) case "overflow": element, err := unmarshalBlockElement(r, &OverflowBlockElement{}) if err != nil { return err } a.OverflowElement = element.(*OverflowBlockElement) case "datepicker": element, err := unmarshalBlockElement(r, &DatePickerBlockElement{}) if err != nil { return err } a.DatePickerElement = element.(*DatePickerBlockElement) case "timepicker": element, err := unmarshalBlockElement(r, &TimePickerBlockElement{}) if err != nil { return err } a.TimePickerElement = element.(*TimePickerBlockElement) case "plain_text_input": element, err := unmarshalBlockElement(r, &PlainTextInputBlockElement{}) if err != nil { return err } a.PlainTextInputElement = element.(*PlainTextInputBlockElement) case "radio_buttons": element, err := unmarshalBlockElement(r, &RadioButtonsBlockElement{}) if err != nil { return err } a.RadioButtonsElement = element.(*RadioButtonsBlockElement) case "static_select", "external_select", "users_select", "conversations_select", "channels_select": element, err := unmarshalBlockElement(r, &SelectBlockElement{}) if err != nil { return err } a.SelectElement = element.(*SelectBlockElement) case "multi_static_select", "multi_external_select", "multi_users_select", "multi_conversations_select", "multi_channels_select": element, err := unmarshalBlockElement(r, &MultiSelectBlockElement{}) if err != nil { return err } a.MultiSelectElement = element.(*MultiSelectBlockElement) case "checkboxes": element, err := unmarshalBlockElement(r, &CheckboxGroupsBlockElement{}) if err != nil { return err } a.CheckboxGroupsBlockElement = element.(*CheckboxGroupsBlockElement) default: element, err := unmarshalBlockElement(r, &UnknownBlockElement{}) if err != nil { return err } a.UnknownElement = element.(*UnknownBlockElement) } return nil } func unmarshalBlockElement(r json.RawMessage, element BlockElement) (BlockElement, error) { err := json.Unmarshal(r, element) if err != nil { return nil, err } return element, nil } func toBlockElement(element *Accessory) BlockElement { if element.ImageElement != nil { return element.ImageElement } if element.ButtonElement != nil { return element.ButtonElement } if element.OverflowElement != nil { return element.OverflowElement } if element.DatePickerElement != nil { return element.DatePickerElement } if element.TimePickerElement != nil { return element.TimePickerElement } if element.PlainTextInputElement != nil { return element.PlainTextInputElement } if element.RadioButtonsElement != nil { return element.RadioButtonsElement } if element.CheckboxGroupsBlockElement != nil { return element.CheckboxGroupsBlockElement } if element.SelectElement != nil { return element.SelectElement } if element.MultiSelectElement != nil { return element.MultiSelectElement } return nil } // MarshalJSON implements the Marshaller interface for ContextElements so that any JSON // marshalling is delegated and proper type determination can be made before marshal func (e *ContextElements) MarshalJSON() ([]byte, error) { bytes, err := json.Marshal(e.Elements) if err != nil { return nil, err } return bytes, nil } // UnmarshalJSON implements the Unmarshaller interface for ContextElements, so that any JSON // unmarshalling is delegated and proper type determination can be made before unmarshal func (e *ContextElements) UnmarshalJSON(data []byte) error { var raw []json.RawMessage if string(data) == "{\"elements\":null}" { return nil } err := json.Unmarshal(data, &raw) if err != nil { return err } for _, r := range raw { s := sumtype{} err := json.Unmarshal(r, &s) if err != nil { return err } var contextElementType string if s.TypeVal != "" { contextElementType = s.TypeVal } switch contextElementType { case PlainTextType, MarkdownType: elem, err := unmarshalBlockObject(r, &TextBlockObject{}) if err != nil { return err } e.Elements = append(e.Elements, elem.(*TextBlockObject)) case "image": elem, err := unmarshalBlockElement(r, &ImageBlockElement{}) if err != nil { return err } e.Elements = append(e.Elements, elem.(*ImageBlockElement)) default: return errors.New("unsupported context element type") } } return nil } slack-0.11.3/block_divider.go000066400000000000000000000010761430741033100160200ustar00rootroot00000000000000package slack // DividerBlock for displaying a divider line between blocks (similar to
tag in html) // // More Information: https://api.slack.com/reference/messaging/blocks#divider type DividerBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` } // BlockType returns the type of the block func (s DividerBlock) BlockType() MessageBlockType { return s.Type } // NewDividerBlock returns a new instance of a divider block func NewDividerBlock() *DividerBlock { return &DividerBlock{ Type: MBTDivider, } } slack-0.11.3/block_divider_test.go000066400000000000000000000003241430741033100170520ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewDividerBlock(t *testing.T) { dividerBlock := NewDividerBlock() assert.Equal(t, string(dividerBlock.Type), "divider") } slack-0.11.3/block_element.go000066400000000000000000000454241430741033100160300ustar00rootroot00000000000000package slack // https://api.slack.com/reference/messaging/block-elements const ( METCheckboxGroups MessageElementType = "checkboxes" METImage MessageElementType = "image" METButton MessageElementType = "button" METOverflow MessageElementType = "overflow" METDatepicker MessageElementType = "datepicker" METTimepicker MessageElementType = "timepicker" METPlainTextInput MessageElementType = "plain_text_input" METRadioButtons MessageElementType = "radio_buttons" MixedElementImage MixedElementType = "mixed_image" MixedElementText MixedElementType = "mixed_text" OptTypeStatic string = "static_select" OptTypeExternal string = "external_select" OptTypeUser string = "users_select" OptTypeConversations string = "conversations_select" OptTypeChannels string = "channels_select" MultiOptTypeStatic string = "multi_static_select" MultiOptTypeExternal string = "multi_external_select" MultiOptTypeUser string = "multi_users_select" MultiOptTypeConversations string = "multi_conversations_select" MultiOptTypeChannels string = "multi_channels_select" ) type MessageElementType string type MixedElementType string // BlockElement defines an interface that all block element types should implement. type BlockElement interface { ElementType() MessageElementType } type MixedElement interface { MixedElementType() MixedElementType } type Accessory struct { ImageElement *ImageBlockElement ButtonElement *ButtonBlockElement OverflowElement *OverflowBlockElement DatePickerElement *DatePickerBlockElement TimePickerElement *TimePickerBlockElement PlainTextInputElement *PlainTextInputBlockElement RadioButtonsElement *RadioButtonsBlockElement SelectElement *SelectBlockElement MultiSelectElement *MultiSelectBlockElement CheckboxGroupsBlockElement *CheckboxGroupsBlockElement UnknownElement *UnknownBlockElement } // NewAccessory returns a new Accessory for a given block element func NewAccessory(element BlockElement) *Accessory { switch element.(type) { case *ImageBlockElement: return &Accessory{ImageElement: element.(*ImageBlockElement)} case *ButtonBlockElement: return &Accessory{ButtonElement: element.(*ButtonBlockElement)} case *OverflowBlockElement: return &Accessory{OverflowElement: element.(*OverflowBlockElement)} case *DatePickerBlockElement: return &Accessory{DatePickerElement: element.(*DatePickerBlockElement)} case *TimePickerBlockElement: return &Accessory{TimePickerElement: element.(*TimePickerBlockElement)} case *PlainTextInputBlockElement: return &Accessory{PlainTextInputElement: element.(*PlainTextInputBlockElement)} case *RadioButtonsBlockElement: return &Accessory{RadioButtonsElement: element.(*RadioButtonsBlockElement)} case *SelectBlockElement: return &Accessory{SelectElement: element.(*SelectBlockElement)} case *MultiSelectBlockElement: return &Accessory{MultiSelectElement: element.(*MultiSelectBlockElement)} case *CheckboxGroupsBlockElement: return &Accessory{CheckboxGroupsBlockElement: element.(*CheckboxGroupsBlockElement)} default: return &Accessory{UnknownElement: element.(*UnknownBlockElement)} } } // BlockElements is a convenience struct defined to allow dynamic unmarshalling of // the "elements" value in Slack's JSON response, which varies depending on BlockElement type type BlockElements struct { ElementSet []BlockElement `json:"elements,omitempty"` } // UnknownBlockElement any block element that this library does not directly support. // See the "Rich Elements" section at the following URL: // https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less // New block element types may be introduced by Slack at any time; this is a catch-all for any such block elements. type UnknownBlockElement struct { Type MessageElementType `json:"type"` Elements BlockElements } // ElementType returns the type of the Element func (s UnknownBlockElement) ElementType() MessageElementType { return s.Type } // ImageBlockElement An element to insert an image - this element can be used // in section and context blocks only. If you want a block with only an image // in it, you're looking for the image block. // // More Information: https://api.slack.com/reference/messaging/block-elements#image type ImageBlockElement struct { Type MessageElementType `json:"type"` ImageURL string `json:"image_url"` AltText string `json:"alt_text"` } // ElementType returns the type of the Element func (s ImageBlockElement) ElementType() MessageElementType { return s.Type } func (s ImageBlockElement) MixedElementType() MixedElementType { return MixedElementImage } // NewImageBlockElement returns a new instance of an image block element func NewImageBlockElement(imageURL, altText string) *ImageBlockElement { return &ImageBlockElement{ Type: METImage, ImageURL: imageURL, AltText: altText, } } // Style is a style of Button element // https://api.slack.com/reference/block-kit/block-elements#button__fields type Style string const ( StyleDefault Style = "" StylePrimary Style = "primary" StyleDanger Style = "danger" ) // ButtonBlockElement defines an interactive element that inserts a button. The // button can be a trigger for anything from opening a simple link to starting // a complex workflow. // // More Information: https://api.slack.com/reference/messaging/block-elements#button type ButtonBlockElement struct { Type MessageElementType `json:"type,omitempty"` Text *TextBlockObject `json:"text"` ActionID string `json:"action_id,omitempty"` URL string `json:"url,omitempty"` Value string `json:"value,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` Style Style `json:"style,omitempty"` } // ElementType returns the type of the element func (s ButtonBlockElement) ElementType() MessageElementType { return s.Type } // WithStyle adds styling to the button object and returns the modified ButtonBlockElement func (s *ButtonBlockElement) WithStyle(style Style) *ButtonBlockElement { s.Style = style return s } // WithConfirm adds a confirmation dialogue to the button object and returns the modified ButtonBlockElement func (s *ButtonBlockElement) WithConfirm(confirm *ConfirmationBlockObject) *ButtonBlockElement { s.Confirm = confirm return s } // NewButtonBlockElement returns an instance of a new button element to be used within a block func NewButtonBlockElement(actionID, value string, text *TextBlockObject) *ButtonBlockElement { return &ButtonBlockElement{ Type: METButton, ActionID: actionID, Text: text, Value: value, } } // OptionsResponse defines the response used for select block typahead. // // More Information: https://api.slack.com/reference/block-kit/block-elements#external_multi_select type OptionsResponse struct { Options []*OptionBlockObject `json:"options,omitempty"` } // OptionGroupsResponse defines the response used for select block typahead. // // More Information: https://api.slack.com/reference/block-kit/block-elements#external_multi_select type OptionGroupsResponse struct { OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` } // SelectBlockElement defines the simplest form of select menu, with a static list // of options passed in when defining the element. // // More Information: https://api.slack.com/reference/messaging/block-elements#select type SelectBlockElement struct { Type string `json:"type,omitempty"` Placeholder *TextBlockObject `json:"placeholder,omitempty"` ActionID string `json:"action_id,omitempty"` Options []*OptionBlockObject `json:"options,omitempty"` OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` InitialOption *OptionBlockObject `json:"initial_option,omitempty"` InitialUser string `json:"initial_user,omitempty"` InitialConversation string `json:"initial_conversation,omitempty"` InitialChannel string `json:"initial_channel,omitempty"` DefaultToCurrentConversation bool `json:"default_to_current_conversation,omitempty"` ResponseURLEnabled bool `json:"response_url_enabled,omitempty"` Filter *SelectBlockElementFilter `json:"filter,omitempty"` MinQueryLength *int `json:"min_query_length,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` } // SelectBlockElementFilter allows to filter select element conversation options by type. // // More Information: https://api.slack.com/reference/block-kit/composition-objects#filter_conversations type SelectBlockElementFilter struct { Include []string `json:"include,omitempty"` ExcludeExternalSharedChannels bool `json:"exclude_external_shared_channels,omitempty"` ExcludeBotUsers bool `json:"exclude_bot_users,omitempty"` } // ElementType returns the type of the Element func (s SelectBlockElement) ElementType() MessageElementType { return MessageElementType(s.Type) } // NewOptionsSelectBlockElement returns a new instance of SelectBlockElement for use with // the Options object only. func NewOptionsSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *SelectBlockElement { return &SelectBlockElement{ Type: optType, Placeholder: placeholder, ActionID: actionID, Options: options, } } // NewOptionsGroupSelectBlockElement returns a new instance of SelectBlockElement for use with // the Options object only. func NewOptionsGroupSelectBlockElement( optType string, placeholder *TextBlockObject, actionID string, optGroups ...*OptionGroupBlockObject, ) *SelectBlockElement { return &SelectBlockElement{ Type: optType, Placeholder: placeholder, ActionID: actionID, OptionGroups: optGroups, } } // MultiSelectBlockElement defines a multiselect menu, with a static list // of options passed in when defining the element. // // More Information: https://api.slack.com/reference/messaging/block-elements#multi_select type MultiSelectBlockElement struct { Type string `json:"type,omitempty"` Placeholder *TextBlockObject `json:"placeholder,omitempty"` ActionID string `json:"action_id,omitempty"` Options []*OptionBlockObject `json:"options,omitempty"` OptionGroups []*OptionGroupBlockObject `json:"option_groups,omitempty"` InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"` InitialUsers []string `json:"initial_users,omitempty"` InitialConversations []string `json:"initial_conversations,omitempty"` InitialChannels []string `json:"initial_channels,omitempty"` MinQueryLength *int `json:"min_query_length,omitempty"` MaxSelectedItems *int `json:"max_selected_items,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` } // ElementType returns the type of the Element func (s MultiSelectBlockElement) ElementType() MessageElementType { return MessageElementType(s.Type) } // NewOptionsMultiSelectBlockElement returns a new instance of SelectBlockElement for use with // the Options object only. func NewOptionsMultiSelectBlockElement(optType string, placeholder *TextBlockObject, actionID string, options ...*OptionBlockObject) *MultiSelectBlockElement { return &MultiSelectBlockElement{ Type: optType, Placeholder: placeholder, ActionID: actionID, Options: options, } } // NewOptionsGroupMultiSelectBlockElement returns a new instance of MultiSelectBlockElement for use with // the Options object only. func NewOptionsGroupMultiSelectBlockElement( optType string, placeholder *TextBlockObject, actionID string, optGroups ...*OptionGroupBlockObject, ) *MultiSelectBlockElement { return &MultiSelectBlockElement{ Type: optType, Placeholder: placeholder, ActionID: actionID, OptionGroups: optGroups, } } // OverflowBlockElement defines the fields needed to use an overflow element. // And Overflow Element is like a cross between a button and a select menu - // when a user clicks on this overflow button, they will be presented with a // list of options to choose from. // // More Information: https://api.slack.com/reference/messaging/block-elements#overflow type OverflowBlockElement struct { Type MessageElementType `json:"type"` ActionID string `json:"action_id,omitempty"` Options []*OptionBlockObject `json:"options"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` } // ElementType returns the type of the Element func (s OverflowBlockElement) ElementType() MessageElementType { return s.Type } // NewOverflowBlockElement returns an instance of a new Overflow Block Element func NewOverflowBlockElement(actionID string, options ...*OptionBlockObject) *OverflowBlockElement { return &OverflowBlockElement{ Type: METOverflow, ActionID: actionID, Options: options, } } // DatePickerBlockElement defines an element which lets users easily select a // date from a calendar style UI. Date picker elements can be used inside of // section and actions blocks. // // More Information: https://api.slack.com/reference/messaging/block-elements#datepicker type DatePickerBlockElement struct { Type MessageElementType `json:"type"` ActionID string `json:"action_id,omitempty"` Placeholder *TextBlockObject `json:"placeholder,omitempty"` InitialDate string `json:"initial_date,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` } // ElementType returns the type of the Element func (s DatePickerBlockElement) ElementType() MessageElementType { return s.Type } // NewDatePickerBlockElement returns an instance of a date picker element func NewDatePickerBlockElement(actionID string) *DatePickerBlockElement { return &DatePickerBlockElement{ Type: METDatepicker, ActionID: actionID, } } // TimePickerBlockElement defines an element which lets users easily select a // time from nice UI. Time picker elements can be used inside of // section and actions blocks. // // More Information: https://api.slack.com/reference/messaging/block-elements#timepicker type TimePickerBlockElement struct { Type MessageElementType `json:"type"` ActionID string `json:"action_id,omitempty"` Placeholder *TextBlockObject `json:"placeholder,omitempty"` InitialTime string `json:"initial_time,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` } // ElementType returns the type of the Element func (s TimePickerBlockElement) ElementType() MessageElementType { return s.Type } // NewTimePickerBlockElement returns an instance of a date picker element func NewTimePickerBlockElement(actionID string) *TimePickerBlockElement { return &TimePickerBlockElement{ Type: METTimepicker, ActionID: actionID, } } // PlainTextInputBlockElement creates a field where a user can enter freeform // data. // Plain-text input elements are currently only available in modals. // // More Information: https://api.slack.com/reference/block-kit/block-elements#input type PlainTextInputBlockElement struct { Type MessageElementType `json:"type"` ActionID string `json:"action_id,omitempty"` Placeholder *TextBlockObject `json:"placeholder,omitempty"` InitialValue string `json:"initial_value,omitempty"` Multiline bool `json:"multiline,omitempty"` MinLength int `json:"min_length,omitempty"` MaxLength int `json:"max_length,omitempty"` DispatchActionConfig *DispatchActionConfig `json:"dispatch_action_config,omitempty"` } type DispatchActionConfig struct { TriggerActionsOn []string `json:"trigger_actions_on,omitempty"` } // ElementType returns the type of the Element func (s PlainTextInputBlockElement) ElementType() MessageElementType { return s.Type } // NewPlainTextInputBlockElement returns an instance of a plain-text input // element func NewPlainTextInputBlockElement(placeholder *TextBlockObject, actionID string) *PlainTextInputBlockElement { return &PlainTextInputBlockElement{ Type: METPlainTextInput, ActionID: actionID, Placeholder: placeholder, } } // CheckboxGroupsBlockElement defines an element which allows users to choose // one or more items from a list of possible options. // // More Information: https://api.slack.com/reference/block-kit/block-elements#checkboxes type CheckboxGroupsBlockElement struct { Type MessageElementType `json:"type"` ActionID string `json:"action_id,omitempty"` Options []*OptionBlockObject `json:"options"` InitialOptions []*OptionBlockObject `json:"initial_options,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` } // ElementType returns the type of the Element func (c CheckboxGroupsBlockElement) ElementType() MessageElementType { return c.Type } // NewCheckboxGroupsBlockElement returns an instance of a checkbox-group block element func NewCheckboxGroupsBlockElement(actionID string, options ...*OptionBlockObject) *CheckboxGroupsBlockElement { return &CheckboxGroupsBlockElement{ Type: METCheckboxGroups, ActionID: actionID, Options: options, } } // RadioButtonsBlockElement defines an element which lets users choose one item // from a list of possible options. // // More Information: https://api.slack.com/reference/block-kit/block-elements#radio type RadioButtonsBlockElement struct { Type MessageElementType `json:"type"` ActionID string `json:"action_id,omitempty"` Options []*OptionBlockObject `json:"options"` InitialOption *OptionBlockObject `json:"initial_option,omitempty"` Confirm *ConfirmationBlockObject `json:"confirm,omitempty"` } // ElementType returns the type of the Element func (s RadioButtonsBlockElement) ElementType() MessageElementType { return s.Type } // NewRadioButtonsBlockElement returns an instance of a radio buttons element. func NewRadioButtonsBlockElement(actionID string, options ...*OptionBlockObject) *RadioButtonsBlockElement { return &RadioButtonsBlockElement{ Type: METRadioButtons, ActionID: actionID, Options: options, } } slack-0.11.3/block_element_test.go000066400000000000000000000167051430741033100170670ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewImageBlockElement(t *testing.T) { imageElement := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Location Pin Icon") assert.Equal(t, string(imageElement.Type), "image") assert.Contains(t, imageElement.ImageURL, "tripAgentLocationMarker") assert.Equal(t, imageElement.AltText, "Location Pin Icon") } func TestNewButtonBlockElement(t *testing.T) { btnTxt := NewTextBlockObject("plain_text", "Next 2 Results", false, false) btnElement := NewButtonBlockElement("test", "click_me_123", btnTxt) assert.Equal(t, string(btnElement.Type), "button") assert.Equal(t, btnElement.ActionID, "test") assert.Equal(t, btnElement.Value, "click_me_123") assert.Equal(t, btnElement.Text.Text, "Next 2 Results") } func TestWithStyleForButtonElement(t *testing.T) { // these values are irrelevant in this test btnTxt := NewTextBlockObject("plain_text", "Next 2 Results", false, false) btnElement := NewButtonBlockElement("test", "click_me_123", btnTxt) btnElement.WithStyle(StyleDefault) assert.Equal(t, btnElement.Style, Style("")) btnElement.WithStyle(StylePrimary) assert.Equal(t, btnElement.Style, Style("primary")) btnElement.WithStyle(StyleDanger) assert.Equal(t, btnElement.Style, Style("danger")) } func TestNewOptionsSelectBlockElement(t *testing.T) { testOptionText := NewTextBlockObject("plain_text", "Option One", false, false) testOption := NewOptionBlockObject("test", testOptionText, nil) option := NewOptionsSelectBlockElement("static_select", nil, "test", testOption) assert.Equal(t, option.Type, "static_select") assert.Equal(t, len(option.Options), 1) assert.Nil(t, option.OptionGroups) } func TestNewOptionsGroupSelectBlockElement(t *testing.T) { testOptionText := NewTextBlockObject("plain_text", "Option One", false, false) testOption := NewOptionBlockObject("test", testOptionText, nil) testLabel := NewTextBlockObject("plain_text", "Test Label", false, false) testGroupOption := NewOptionGroupBlockElement(testLabel, testOption) optGroup := NewOptionsGroupSelectBlockElement("static_select", nil, "test", testGroupOption) assert.Equal(t, optGroup.Type, "static_select") assert.Equal(t, optGroup.ActionID, "test") assert.Equal(t, len(optGroup.OptionGroups), 1) } func TestNewOptionsMultiSelectBlockElement(t *testing.T) { testOptionText := NewTextBlockObject("plain_text", "Option One", false, false) testDescriptionText := NewTextBlockObject("plain_text", "Description One", false, false) testOption := NewOptionBlockObject("test", testOptionText, testDescriptionText) option := NewOptionsMultiSelectBlockElement("static_select", nil, "test", testOption) assert.Equal(t, option.Type, "static_select") assert.Equal(t, len(option.Options), 1) assert.Nil(t, option.OptionGroups) } func TestNewOptionsGroupMultiSelectBlockElement(t *testing.T) { testOptionText := NewTextBlockObject("plain_text", "Option One", false, false) testOption := NewOptionBlockObject("test", testOptionText, nil) testLabel := NewTextBlockObject("plain_text", "Test Label", false, false) testGroupOption := NewOptionGroupBlockElement(testLabel, testOption) optGroup := NewOptionsGroupMultiSelectBlockElement("static_select", nil, "test", testGroupOption) assert.Equal(t, optGroup.Type, "static_select") assert.Equal(t, optGroup.ActionID, "test") assert.Equal(t, len(optGroup.OptionGroups), 1) } func TestNewOverflowBlockElement(t *testing.T) { // Build Text Objects associated with each option overflowOptionTextOne := NewTextBlockObject("plain_text", "Option One", false, false) overflowOptionTextTwo := NewTextBlockObject("plain_text", "Option Two", false, false) overflowOptionTextThree := NewTextBlockObject("plain_text", "Option Three", false, false) // Build each option, providing a value for the option overflowOptionOne := NewOptionBlockObject("value-0", overflowOptionTextOne, nil) overflowOptionTwo := NewOptionBlockObject("value-1", overflowOptionTextTwo, nil) overflowOptionThree := NewOptionBlockObject("value-2", overflowOptionTextThree, nil) // Build overflow section overflowElement := NewOverflowBlockElement("test", overflowOptionOne, overflowOptionTwo, overflowOptionThree) assert.Equal(t, string(overflowElement.Type), "overflow") assert.Equal(t, overflowElement.ActionID, "test") assert.Equal(t, len(overflowElement.Options), 3) } func TestNewDatePickerBlockElement(t *testing.T) { datepickerElement := NewDatePickerBlockElement("test") assert.Equal(t, string(datepickerElement.Type), "datepicker") assert.Equal(t, datepickerElement.ActionID, "test") } func TestNewTimePickerBlockElement(t *testing.T) { timepickerElement := NewTimePickerBlockElement("test") assert.Equal(t, string(timepickerElement.Type), "timepicker") assert.Equal(t, timepickerElement.ActionID, "test") } func TestNewPlainTextInputBlockElement(t *testing.T) { plainTextInputElement := NewPlainTextInputBlockElement(nil, "test") assert.Equal(t, string(plainTextInputElement.Type), "plain_text_input") assert.Equal(t, plainTextInputElement.ActionID, "test") } func TestNewCheckboxGroupsBlockElement(t *testing.T) { // Build Text Objects associated with each option checkBoxOptionTextOne := NewTextBlockObject("plain_text", "Check One", false, false) checkBoxOptionTextTwo := NewTextBlockObject("plain_text", "Check Two", false, false) checkBoxOptionTextThree := NewTextBlockObject("plain_text", "Check Three", false, false) checkBoxDescriptionTextOne := NewTextBlockObject("plain_text", "Description One", false, false) checkBoxDescriptionTextTwo := NewTextBlockObject("plain_text", "Description Two", false, false) checkBoxDescriptionTextThree := NewTextBlockObject("plain_text", "Description Three", false, false) // Build each option, providing a value for the option checkBoxOptionOne := NewOptionBlockObject("value-0", checkBoxOptionTextOne, checkBoxDescriptionTextOne) checkBoxOptionTwo := NewOptionBlockObject("value-1", checkBoxOptionTextTwo, checkBoxDescriptionTextTwo) checkBoxOptionThree := NewOptionBlockObject("value-2", checkBoxOptionTextThree, checkBoxDescriptionTextThree) // Build checkbox-group element checkBoxGroupElement := NewCheckboxGroupsBlockElement("test", checkBoxOptionOne, checkBoxOptionTwo, checkBoxOptionThree) assert.Equal(t, string(checkBoxGroupElement.Type), "checkboxes") assert.Equal(t, checkBoxGroupElement.ActionID, "test") assert.Equal(t, len(checkBoxGroupElement.Options), 3) } func TestNewRadioButtonsBlockElement(t *testing.T) { // Build Text Objects associated with each option radioButtonsOptionTextOne := NewTextBlockObject("plain_text", "Option One", false, false) radioButtonsOptionTextTwo := NewTextBlockObject("plain_text", "Option Two", false, false) radioButtonsOptionTextThree := NewTextBlockObject("plain_text", "Option Three", false, false) // Build each option, providing a value for the option radioButtonsOptionOne := NewOptionBlockObject("value-0", radioButtonsOptionTextOne, nil) radioButtonsOptionTwo := NewOptionBlockObject("value-1", radioButtonsOptionTextTwo, nil) radioButtonsOptionThree := NewOptionBlockObject("value-2", radioButtonsOptionTextThree, nil) // Build radio button element radioButtonsElement := NewRadioButtonsBlockElement("test", radioButtonsOptionOne, radioButtonsOptionTwo, radioButtonsOptionThree) assert.Equal(t, string(radioButtonsElement.Type), "radio_buttons") assert.Equal(t, radioButtonsElement.ActionID, "test") assert.Equal(t, len(radioButtonsElement.Options), 3) } slack-0.11.3/block_file.go000066400000000000000000000013521430741033100153060ustar00rootroot00000000000000package slack // FileBlock defines data that is used to display a remote file. // // More Information: https://api.slack.com/reference/block-kit/blocks#file type FileBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` ExternalID string `json:"external_id"` Source string `json:"source"` } // BlockType returns the type of the block func (s FileBlock) BlockType() MessageBlockType { return s.Type } // NewFileBlock returns a new instance of a file block func NewFileBlock(blockID string, externalID string, source string) *FileBlock { return &FileBlock{ Type: MBTFile, BlockID: blockID, ExternalID: externalID, Source: source, } } slack-0.11.3/block_file_test.go000066400000000000000000000005611430741033100163460ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewFileBlock(t *testing.T) { fileBlock := NewFileBlock("test", "external_id", "source") assert.Equal(t, string(fileBlock.Type), "file") assert.Equal(t, fileBlock.BlockID, "test") assert.Equal(t, fileBlock.ExternalID, "external_id") assert.Equal(t, fileBlock.Source, "source") } slack-0.11.3/block_header.go000066400000000000000000000017271430741033100156250ustar00rootroot00000000000000package slack // HeaderBlock defines a new block of type header // // More Information: https://api.slack.com/reference/messaging/blocks#header type HeaderBlock struct { Type MessageBlockType `json:"type"` Text *TextBlockObject `json:"text,omitempty"` BlockID string `json:"block_id,omitempty"` } // BlockType returns the type of the block func (s HeaderBlock) BlockType() MessageBlockType { return s.Type } // HeaderBlockOption allows configuration of options for a new header block type HeaderBlockOption func(*HeaderBlock) func HeaderBlockOptionBlockID(blockID string) HeaderBlockOption { return func(block *HeaderBlock) { block.BlockID = blockID } } // NewHeaderBlock returns a new instance of a header block to be rendered func NewHeaderBlock(textObj *TextBlockObject, options ...HeaderBlockOption) *HeaderBlock { block := HeaderBlock{ Type: MBTHeader, Text: textObj, } for _, option := range options { option(&block) } return &block } slack-0.11.3/block_header_test.go000066400000000000000000000010001430741033100166440ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewHeaderBlock(t *testing.T) { textInfo := NewTextBlockObject("plain_text", "This is quite the header", false, false) headerBlock := NewHeaderBlock(textInfo, HeaderBlockOptionBlockID("test_block")) assert.Equal(t, string(headerBlock.Type), "header") assert.Equal(t, headerBlock.BlockID, "test_block") assert.Equal(t, headerBlock.Text.Type, "plain_text") assert.Contains(t, headerBlock.Text.Text, "quite the header") } slack-0.11.3/block_image.go000066400000000000000000000015021430741033100154460ustar00rootroot00000000000000package slack // ImageBlock defines data required to display an image as a block element // // More Information: https://api.slack.com/reference/messaging/blocks#image type ImageBlock struct { Type MessageBlockType `json:"type"` ImageURL string `json:"image_url"` AltText string `json:"alt_text"` BlockID string `json:"block_id,omitempty"` Title *TextBlockObject `json:"title,omitempty"` } // BlockType returns the type of the block func (s ImageBlock) BlockType() MessageBlockType { return s.Type } // NewImageBlock returns an instance of a new Image Block type func NewImageBlock(imageURL, altText, blockID string, title *TextBlockObject) *ImageBlock { return &ImageBlock{ Type: MBTImage, ImageURL: imageURL, AltText: altText, BlockID: blockID, Title: title, } } slack-0.11.3/block_image_test.go000066400000000000000000000011451430741033100165100ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewImageBlock(t *testing.T) { imageText := NewTextBlockObject("plain_text", "Location", false, false) imageBlock := NewImageBlock("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Marker", "test", imageText) assert.Equal(t, string(imageBlock.Type), "image") assert.Equal(t, imageBlock.Title.Type, "plain_text") assert.Equal(t, imageBlock.BlockID, "test") assert.Contains(t, imageBlock.Title.Text, "Location") assert.Contains(t, imageBlock.ImageURL, "tripAgentLocationMarker.png") } slack-0.11.3/block_input.go000066400000000000000000000017211430741033100155260ustar00rootroot00000000000000package slack // InputBlock defines data that is used to display user input fields. // // More Information: https://api.slack.com/reference/block-kit/blocks#input type InputBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` Label *TextBlockObject `json:"label"` Element BlockElement `json:"element"` Hint *TextBlockObject `json:"hint,omitempty"` Optional bool `json:"optional,omitempty"` DispatchAction bool `json:"dispatch_action,omitempty"` } // BlockType returns the type of the block func (s InputBlock) BlockType() MessageBlockType { return s.Type } // NewInputBlock returns a new instance of an input block func NewInputBlock(blockID string, label, hint *TextBlockObject, element BlockElement) *InputBlock { return &InputBlock{ Type: MBTInput, BlockID: blockID, Label: label, Element: element, Hint: hint, } } slack-0.11.3/block_input_test.go000066400000000000000000000010361430741033100165640ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewInputBlock(t *testing.T) { label := NewTextBlockObject("plain_text", "label", false, false) element := NewDatePickerBlockElement("action_id") hint := NewTextBlockObject("plain_text", "hint", false, false) inputBlock := NewInputBlock("test", label, hint, element) assert.Equal(t, string(inputBlock.Type), "input") assert.Equal(t, inputBlock.BlockID, "test") assert.Equal(t, inputBlock.Label, label) assert.Equal(t, inputBlock.Element, element) } slack-0.11.3/block_object.go000066400000000000000000000163001430741033100156340ustar00rootroot00000000000000package slack import ( "encoding/json" "errors" ) // Block Objects are also known as Composition Objects // // For more information: https://api.slack.com/reference/messaging/composition-objects // BlockObject defines an interface that all block object types should // implement. // @TODO: Is this interface needed? // blockObject object types const ( MarkdownType = "mrkdwn" PlainTextType = "plain_text" // The following objects don't actually have types and their corresponding // const values are just for internal use motConfirmation = "confirm" motOption = "option" motOptionGroup = "option_group" ) type MessageObjectType string type blockObject interface { validateType() MessageObjectType } type BlockObjects struct { TextObjects []*TextBlockObject ConfirmationObjects []*ConfirmationBlockObject OptionObjects []*OptionBlockObject OptionGroupObjects []*OptionGroupBlockObject } // UnmarshalJSON implements the Unmarshaller interface for BlockObjects, so that any JSON // unmarshalling is delegated and proper type determination can be made before unmarshal func (b *BlockObjects) UnmarshalJSON(data []byte) error { var raw []json.RawMessage err := json.Unmarshal(data, &raw) if err != nil { return err } for _, r := range raw { var obj map[string]interface{} err := json.Unmarshal(r, &obj) if err != nil { return err } blockObjectType := getBlockObjectType(obj) switch blockObjectType { case PlainTextType, MarkdownType: object, err := unmarshalBlockObject(r, &TextBlockObject{}) if err != nil { return err } b.TextObjects = append(b.TextObjects, object.(*TextBlockObject)) case motConfirmation: object, err := unmarshalBlockObject(r, &ConfirmationBlockObject{}) if err != nil { return err } b.ConfirmationObjects = append(b.ConfirmationObjects, object.(*ConfirmationBlockObject)) case motOption: object, err := unmarshalBlockObject(r, &OptionBlockObject{}) if err != nil { return err } b.OptionObjects = append(b.OptionObjects, object.(*OptionBlockObject)) case motOptionGroup: object, err := unmarshalBlockObject(r, &OptionGroupBlockObject{}) if err != nil { return err } b.OptionGroupObjects = append(b.OptionGroupObjects, object.(*OptionGroupBlockObject)) } } return nil } // Ideally would have a better way to identify the block objects for // type casting at time of unmarshalling, should be adapted if possible // to accomplish in a more reliable manner. func getBlockObjectType(obj map[string]interface{}) string { if t, ok := obj["type"].(string); ok { return t } if _, ok := obj["confirm"].(string); ok { return "confirm" } if _, ok := obj["options"].(string); ok { return "option_group" } if _, ok := obj["text"].(string); ok { if _, ok := obj["value"].(string); ok { return "option" } } return "" } func unmarshalBlockObject(r json.RawMessage, object blockObject) (blockObject, error) { err := json.Unmarshal(r, object) if err != nil { return nil, err } return object, nil } // TextBlockObject defines a text element object to be used with blocks // // More Information: https://api.slack.com/reference/messaging/composition-objects#text type TextBlockObject struct { Type string `json:"type"` Text string `json:"text"` Emoji bool `json:"emoji,omitempty"` Verbatim bool `json:"verbatim,omitempty"` } // validateType enforces block objects for element and block parameters func (s TextBlockObject) validateType() MessageObjectType { return MessageObjectType(s.Type) } // validateType enforces block objects for element and block parameters func (s TextBlockObject) MixedElementType() MixedElementType { return MixedElementText } // Validate checks if TextBlockObject has valid values func (s TextBlockObject) Validate() error { if s.Type != "plain_text" && s.Type != "mrkdwn" { return errors.New("type must be either of plain_text or mrkdwn") } // https://github.com/slack-go/slack/issues/881 if s.Type == "mrkdwn" && s.Emoji { return errors.New("emoji cannot be true in mrkdown") } return nil } // NewTextBlockObject returns an instance of a new Text Block Object func NewTextBlockObject(elementType, text string, emoji, verbatim bool) *TextBlockObject { return &TextBlockObject{ Type: elementType, Text: text, Emoji: emoji, Verbatim: verbatim, } } // BlockType returns the type of the block func (t TextBlockObject) BlockType() MessageBlockType { if t.Type == "mrkdwn" { return MarkdownType } return PlainTextType } // ConfirmationBlockObject defines a dialog that provides a confirmation step to // any interactive element. This dialog will ask the user to confirm their action by // offering a confirm and deny buttons. // // More Information: https://api.slack.com/reference/messaging/composition-objects#confirm type ConfirmationBlockObject struct { Title *TextBlockObject `json:"title"` Text *TextBlockObject `json:"text"` Confirm *TextBlockObject `json:"confirm"` Deny *TextBlockObject `json:"deny"` Style Style `json:"style,omitempty"` } // validateType enforces block objects for element and block parameters func (s ConfirmationBlockObject) validateType() MessageObjectType { return motConfirmation } // WithStyle add styling to confirmation object func (s *ConfirmationBlockObject) WithStyle(style Style) *ConfirmationBlockObject { s.Style = style return s } // NewConfirmationBlockObject returns an instance of a new Confirmation Block Object func NewConfirmationBlockObject(title, text, confirm, deny *TextBlockObject) *ConfirmationBlockObject { return &ConfirmationBlockObject{ Title: title, Text: text, Confirm: confirm, Deny: deny, } } // OptionBlockObject represents a single selectable item in a select menu // // More Information: https://api.slack.com/reference/messaging/composition-objects#option type OptionBlockObject struct { Text *TextBlockObject `json:"text"` Value string `json:"value"` Description *TextBlockObject `json:"description,omitempty"` URL string `json:"url,omitempty"` } // NewOptionBlockObject returns an instance of a new Option Block Element func NewOptionBlockObject(value string, text, description *TextBlockObject) *OptionBlockObject { return &OptionBlockObject{ Text: text, Value: value, Description: description, } } // validateType enforces block objects for element and block parameters func (s OptionBlockObject) validateType() MessageObjectType { return motOption } // OptionGroupBlockObject Provides a way to group options in a select menu. // // More Information: https://api.slack.com/reference/messaging/composition-objects#option-group type OptionGroupBlockObject struct { Label *TextBlockObject `json:"label,omitempty"` Options []*OptionBlockObject `json:"options"` } // validateType enforces block objects for element and block parameters func (s OptionGroupBlockObject) validateType() MessageObjectType { return motOptionGroup } // NewOptionGroupBlockElement returns an instance of a new option group block element func NewOptionGroupBlockElement(label *TextBlockObject, options ...*OptionBlockObject) *OptionGroupBlockObject { return &OptionGroupBlockObject{ Label: label, Options: options, } } slack-0.11.3/block_object_test.go000066400000000000000000000073621430741033100167030ustar00rootroot00000000000000package slack import ( "errors" "testing" "github.com/stretchr/testify/assert" ) func TestNewImageBlockObject(t *testing.T) { imageObject := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/beagle.png", "Beagle") assert.Equal(t, string(imageObject.Type), "image") assert.Equal(t, imageObject.AltText, "Beagle") assert.Contains(t, imageObject.ImageURL, "beagle.png") } func TestNewTextBlockObject(t *testing.T) { textObject := NewTextBlockObject("plain_text", "test", true, false) assert.Equal(t, textObject.Type, "plain_text") assert.Equal(t, textObject.Text, "test") assert.True(t, textObject.Emoji, "Emoji property should be true") assert.False(t, textObject.Verbatim, "Verbatim should be false") } func TestNewConfirmationBlockObject(t *testing.T) { titleObj := NewTextBlockObject("plain_text", "testTitle", false, false) textObj := NewTextBlockObject("plain_text", "testText", false, false) confirmObj := NewTextBlockObject("plain_text", "testConfirm", false, false) confirmation := NewConfirmationBlockObject(titleObj, textObj, confirmObj, nil) assert.Equal(t, confirmation.Title.Text, "testTitle") assert.Equal(t, confirmation.Text.Text, "testText") assert.Equal(t, confirmation.Confirm.Text, "testConfirm") assert.Nil(t, confirmation.Deny, "Deny should be nil") } func TestWithStyleForConfirmation(t *testing.T) { // these values are irrelevant in this test titleObj := NewTextBlockObject("plain_text", "testTitle", false, false) textObj := NewTextBlockObject("plain_text", "testText", false, false) confirmObj := NewTextBlockObject("plain_text", "testConfirm", false, false) confirmation := NewConfirmationBlockObject(titleObj, textObj, confirmObj, nil) confirmation.WithStyle(StyleDefault) assert.Equal(t, confirmation.Style, Style("")) confirmation.WithStyle(StylePrimary) assert.Equal(t, confirmation.Style, Style("primary")) confirmation.WithStyle(StyleDanger) assert.Equal(t, confirmation.Style, Style("danger")) } func TestNewOptionBlockObject(t *testing.T) { valTextObj := NewTextBlockObject("plain_text", "testText", false, false) valDescriptionObj := NewTextBlockObject("plain_text", "testDescription", false, false) optObj := NewOptionBlockObject("testOpt", valTextObj, valDescriptionObj) assert.Equal(t, optObj.Text.Text, "testText") assert.Equal(t, optObj.Description.Text, "testDescription") assert.Equal(t, optObj.Value, "testOpt") } func TestNewOptionGroupBlockElement(t *testing.T) { labelObj := NewTextBlockObject("plain_text", "testLabel", false, false) valTextObj := NewTextBlockObject("plain_text", "testText", false, false) optObj := NewOptionBlockObject("testOpt", valTextObj, nil) optGroup := NewOptionGroupBlockElement(labelObj, optObj) assert.Equal(t, optGroup.Label.Text, "testLabel") assert.Len(t, optGroup.Options, 1, "Options should contain one element") } func TestValidateTextBlockObject(t *testing.T) { tests := []struct { input TextBlockObject expected error }{ { input: TextBlockObject{ Type: "plain_text", Text: "testText", Emoji: false, Verbatim: false, }, expected: nil, }, { input: TextBlockObject{ Type: "mrkdwn", Text: "testText", Emoji: false, Verbatim: false, }, expected: nil, }, { input: TextBlockObject{ Type: "invalid", Text: "testText", Emoji: false, Verbatim: false, }, expected: errors.New("type must be either of plain_text or mrkdwn"), }, { input: TextBlockObject{ Type: "mrkdwn", Text: "testText", Emoji: true, Verbatim: false, }, expected: errors.New("emoji cannot be true in mrkdown"), }, } for _, test := range tests { err := test.input.Validate() assert.Equal(t, err, test.expected) } } slack-0.11.3/block_rich_text.go000066400000000000000000000242721430741033100163660ustar00rootroot00000000000000package slack import ( "encoding/json" ) // RichTextBlock defines a new block of type rich_text. // More Information: https://api.slack.com/changelog/2019-09-what-they-see-is-what-you-get-and-more-and-less type RichTextBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` Elements []RichTextElement `json:"elements"` } func (b RichTextBlock) BlockType() MessageBlockType { return b.Type } func (e *RichTextBlock) UnmarshalJSON(b []byte) error { var raw struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id"` RawElements []json.RawMessage `json:"elements"` } if string(b) == "{}" { return nil } if err := json.Unmarshal(b, &raw); err != nil { return err } elems := make([]RichTextElement, 0, len(raw.RawElements)) for _, r := range raw.RawElements { var s struct { Type RichTextElementType `json:"type"` } if err := json.Unmarshal(r, &s); err != nil { return err } var elem RichTextElement switch s.Type { case RTESection: elem = &RichTextSection{} default: elems = append(elems, &RichTextUnknown{ Type: s.Type, Raw: string(r), }) continue } if err := json.Unmarshal(r, &elem); err != nil { return err } elems = append(elems, elem) } *e = RichTextBlock{ Type: raw.Type, BlockID: raw.BlockID, Elements: elems, } return nil } // NewRichTextBlock returns a new instance of RichText Block. func NewRichTextBlock(blockID string, elements ...RichTextElement) *RichTextBlock { return &RichTextBlock{ Type: MBTRichText, BlockID: blockID, Elements: elements, } } type RichTextElementType string type RichTextElement interface { RichTextElementType() RichTextElementType } const ( RTEList RichTextElementType = "rich_text_list" RTEPreformatted RichTextElementType = "rich_text_preformatted" RTEQuote RichTextElementType = "rich_text_quote" RTESection RichTextElementType = "rich_text_section" RTEUnknown RichTextElementType = "rich_text_unknown" ) type RichTextUnknown struct { Type RichTextElementType Raw string } func (u RichTextUnknown) RichTextElementType() RichTextElementType { return u.Type } type RichTextSection struct { Type RichTextElementType `json:"type"` Elements []RichTextSectionElement `json:"elements"` } // ElementType returns the type of the Element func (s RichTextSection) RichTextElementType() RichTextElementType { return s.Type } func (e *RichTextSection) UnmarshalJSON(b []byte) error { var raw struct { RawElements []json.RawMessage `json:"elements"` } if string(b) == "{}" { return nil } if err := json.Unmarshal(b, &raw); err != nil { return err } elems := make([]RichTextSectionElement, 0, len(raw.RawElements)) for _, r := range raw.RawElements { var s struct { Type RichTextSectionElementType `json:"type"` } if err := json.Unmarshal(r, &s); err != nil { return err } var elem RichTextSectionElement switch s.Type { case RTSEText: elem = &RichTextSectionTextElement{} case RTSEChannel: elem = &RichTextSectionChannelElement{} case RTSEUser: elem = &RichTextSectionUserElement{} case RTSEEmoji: elem = &RichTextSectionEmojiElement{} case RTSELink: elem = &RichTextSectionLinkElement{} case RTSETeam: elem = &RichTextSectionTeamElement{} case RTSEUserGroup: elem = &RichTextSectionUserGroupElement{} case RTSEDate: elem = &RichTextSectionDateElement{} case RTSEBroadcast: elem = &RichTextSectionBroadcastElement{} case RTSEColor: elem = &RichTextSectionColorElement{} default: elems = append(elems, &RichTextSectionUnknownElement{ Type: s.Type, Raw: string(r), }) continue } if err := json.Unmarshal(r, elem); err != nil { return err } elems = append(elems, elem) } *e = RichTextSection{ Type: RTESection, Elements: elems, } return nil } // NewRichTextSectionBlockElement . func NewRichTextSection(elements ...RichTextSectionElement) *RichTextSection { return &RichTextSection{ Type: RTESection, Elements: elements, } } type RichTextSectionElementType string const ( RTSEBroadcast RichTextSectionElementType = "broadcast" RTSEChannel RichTextSectionElementType = "channel" RTSEColor RichTextSectionElementType = "color" RTSEDate RichTextSectionElementType = "date" RTSEEmoji RichTextSectionElementType = "emoji" RTSELink RichTextSectionElementType = "link" RTSETeam RichTextSectionElementType = "team" RTSEText RichTextSectionElementType = "text" RTSEUser RichTextSectionElementType = "user" RTSEUserGroup RichTextSectionElementType = "usergroup" RTSEUnknown RichTextSectionElementType = "unknown" ) type RichTextSectionElement interface { RichTextSectionElementType() RichTextSectionElementType } type RichTextSectionTextStyle struct { Bold bool `json:"bold,omitempty"` Italic bool `json:"italic,omitempty"` Strike bool `json:"strike,omitempty"` Code bool `json:"code,omitempty"` } type RichTextSectionTextElement struct { Type RichTextSectionElementType `json:"type"` Text string `json:"text"` Style *RichTextSectionTextStyle `json:"style,omitempty"` } func (r RichTextSectionTextElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionTextElement(text string, style *RichTextSectionTextStyle) *RichTextSectionTextElement { return &RichTextSectionTextElement{ Type: RTSEText, Text: text, Style: style, } } type RichTextSectionChannelElement struct { Type RichTextSectionElementType `json:"type"` ChannelID string `json:"channel_id"` Style *RichTextSectionTextStyle `json:"style,omitempty"` } func (r RichTextSectionChannelElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionChannelElement(channelID string, style *RichTextSectionTextStyle) *RichTextSectionChannelElement { return &RichTextSectionChannelElement{ Type: RTSEText, ChannelID: channelID, Style: style, } } type RichTextSectionUserElement struct { Type RichTextSectionElementType `json:"type"` UserID string `json:"user_id"` Style *RichTextSectionTextStyle `json:"style,omitempty"` } func (r RichTextSectionUserElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionUserElement(userID string, style *RichTextSectionTextStyle) *RichTextSectionUserElement { return &RichTextSectionUserElement{ Type: RTSEUser, UserID: userID, Style: style, } } type RichTextSectionEmojiElement struct { Type RichTextSectionElementType `json:"type"` Name string `json:"name"` SkinTone int `json:"skin_tone"` Style *RichTextSectionTextStyle `json:"style,omitempty"` } func (r RichTextSectionEmojiElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionEmojiElement(name string, skinTone int, style *RichTextSectionTextStyle) *RichTextSectionEmojiElement { return &RichTextSectionEmojiElement{ Type: RTSEEmoji, Name: name, SkinTone: skinTone, Style: style, } } type RichTextSectionLinkElement struct { Type RichTextSectionElementType `json:"type"` URL string `json:"url"` Text string `json:"text"` Style *RichTextSectionTextStyle `json:"style,omitempty"` } func (r RichTextSectionLinkElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionLinkElement(url, text string, style *RichTextSectionTextStyle) *RichTextSectionLinkElement { return &RichTextSectionLinkElement{ Type: RTSELink, URL: url, Text: text, Style: style, } } type RichTextSectionTeamElement struct { Type RichTextSectionElementType `json:"type"` TeamID string `json:"team_id"` Style *RichTextSectionTextStyle `json:"style.omitempty"` } func (r RichTextSectionTeamElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionTeamElement(teamID string, style *RichTextSectionTextStyle) *RichTextSectionTeamElement { return &RichTextSectionTeamElement{ Type: RTSETeam, TeamID: teamID, Style: style, } } type RichTextSectionUserGroupElement struct { Type RichTextSectionElementType `json:"type"` UsergroupID string `json:"usergroup_id"` } func (r RichTextSectionUserGroupElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionUserGroupElement(usergroupID string) *RichTextSectionUserGroupElement { return &RichTextSectionUserGroupElement{ Type: RTSEUserGroup, UsergroupID: usergroupID, } } type RichTextSectionDateElement struct { Type RichTextSectionElementType `json:"type"` Timestamp string `json:"timestamp"` } func (r RichTextSectionDateElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionDateElement(timestamp string) *RichTextSectionDateElement { return &RichTextSectionDateElement{ Type: RTSEDate, Timestamp: timestamp, } } type RichTextSectionBroadcastElement struct { Type RichTextSectionElementType `json:"type"` Range string `json:"range"` } func (r RichTextSectionBroadcastElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionBroadcastElement(rangeStr string) *RichTextSectionBroadcastElement { return &RichTextSectionBroadcastElement{ Type: RTSEBroadcast, Range: rangeStr, } } type RichTextSectionColorElement struct { Type RichTextSectionElementType `json:"type"` Value string `json:"value"` } func (r RichTextSectionColorElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } func NewRichTextSectionColorElement(value string) *RichTextSectionColorElement { return &RichTextSectionColorElement{ Type: RTSEColor, Value: value, } } type RichTextSectionUnknownElement struct { Type RichTextSectionElementType `json:"type"` Raw string } func (r RichTextSectionUnknownElement) RichTextSectionElementType() RichTextSectionElementType { return r.Type } slack-0.11.3/block_rich_text_test.go000066400000000000000000000051511430741033100174200ustar00rootroot00000000000000package slack import ( "encoding/json" "testing" "github.com/go-test/deep" ) const ( dummyPayload = `{ "type":"rich_text", "block_id":"FaYCD", "elements": [ { "type":"rich_text_section", "elements": [ { "type":"channel", "channel_id":"C012345678" }, { "type":"text", "text":"dummy_text" } ] } ] }` ) func TestRichTextBlock_UnmarshalJSON(t *testing.T) { cases := []struct { raw []byte expected RichTextBlock err error }{ { []byte(`{"elements":[{"type":"rich_text_unknown"},{"type":"rich_text_section"}]}`), RichTextBlock{ Elements: []RichTextElement{ &RichTextUnknown{Type: RTEUnknown, Raw: `{"type":"rich_text_unknown"}`}, &RichTextSection{Type: RTESection, Elements: []RichTextSectionElement{}}, }, }, nil, }, { []byte(`{"type": "rich_text","block_id":"blk","elements":[]}`), RichTextBlock{ Type: MBTRichText, BlockID: "blk", Elements: []RichTextElement{}, }, nil, }, } for _, tc := range cases { var actual RichTextBlock err := json.Unmarshal(tc.raw, &actual) if err != nil { if tc.err == nil { t.Errorf("unexpected error: %s", err) } t.Errorf("expected error is %s, but got %s", tc.err, err) } if tc.err != nil { t.Errorf("expected to raise an error %s", tc.err) } if diff := deep.Equal(actual, tc.expected); diff != nil { t.Errorf("actual value does not match expected one\n%s", diff) } } } func TestRichTextSection_UnmarshalJSON(t *testing.T) { cases := []struct { raw []byte expected RichTextSection err error }{ { []byte(`{"elements":[{"type":"unknown","value":10},{"type":"text","text":"hi"}]}`), RichTextSection{ Type: RTESection, Elements: []RichTextSectionElement{ &RichTextSectionUnknownElement{Type: RTSEUnknown, Raw: `{"type":"unknown","value":10}`}, &RichTextSectionTextElement{Type: RTSEText, Text: "hi"}, }, }, nil, }, { []byte(`{"type": "rich_text_section","elements":[]}`), RichTextSection{ Type: RTESection, Elements: []RichTextSectionElement{}, }, nil, }, } for _, tc := range cases { var actual RichTextSection err := json.Unmarshal(tc.raw, &actual) if err != nil { if tc.err == nil { t.Errorf("unexpected error: %s", err) } t.Errorf("expected error is %s, but got %s", tc.err, err) } if tc.err != nil { t.Errorf("expected to raise an error %s", tc.err) } if diff := deep.Equal(actual, tc.expected); diff != nil { t.Errorf("actual value does not match expected one\n%s", diff) } } } slack-0.11.3/block_section.go000066400000000000000000000023211430741033100160300ustar00rootroot00000000000000package slack // SectionBlock defines a new block of type section // // More Information: https://api.slack.com/reference/messaging/blocks#section type SectionBlock struct { Type MessageBlockType `json:"type"` Text *TextBlockObject `json:"text,omitempty"` BlockID string `json:"block_id,omitempty"` Fields []*TextBlockObject `json:"fields,omitempty"` Accessory *Accessory `json:"accessory,omitempty"` } // BlockType returns the type of the block func (s SectionBlock) BlockType() MessageBlockType { return s.Type } // SectionBlockOption allows configuration of options for a new section block type SectionBlockOption func(*SectionBlock) func SectionBlockOptionBlockID(blockID string) SectionBlockOption { return func(block *SectionBlock) { block.BlockID = blockID } } // NewSectionBlock returns a new instance of a section block to be rendered func NewSectionBlock(textObj *TextBlockObject, fields []*TextBlockObject, accessory *Accessory, options ...SectionBlockOption) *SectionBlock { block := SectionBlock{ Type: MBTSection, Text: textObj, Fields: fields, Accessory: accessory, } for _, option := range options { option(&block) } return &block } slack-0.11.3/block_section_test.go000066400000000000000000000030071430741033100170710ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewSectionBlock(t *testing.T) { textInfo := NewTextBlockObject("mrkdwn", "**\n★★★★★\n$340 per night\nRated: 9.1 - Excellent", false, false) sectionBlock := NewSectionBlock(textInfo, nil, nil, SectionBlockOptionBlockID("test_block")) assert.Equal(t, string(sectionBlock.Type), "section") assert.Equal(t, sectionBlock.BlockID, "test_block") assert.Equal(t, len(sectionBlock.Fields), 0) assert.Nil(t, sectionBlock.Accessory) assert.Equal(t, sectionBlock.Text.Type, "mrkdwn") assert.Contains(t, sectionBlock.Text.Text, "New Orleans") } func TestNewBlockSectionContainsAddedTextBlockAndAccessory(t *testing.T) { textBlockObject := NewTextBlockObject("mrkdwn", "You have a new test: *Hi there* :wave:", true, false) conflictImage := NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/notificationsWarningIcon.png", "notifications warning icon") sectionBlock := NewSectionBlock(textBlockObject, nil, NewAccessory(conflictImage)) assert.Equal(t, sectionBlock.BlockType(), MBTSection) assert.Equal(t, len(sectionBlock.BlockID), 0) textBlockInSection := sectionBlock.Text assert.Equal(t, textBlockInSection.Text, textBlockObject.Text) assert.Equal(t, textBlockInSection.Type, textBlockObject.Type) assert.True(t, textBlockInSection.Emoji) assert.False(t, textBlockInSection.Verbatim) assert.Equal(t, sectionBlock.Accessory.ImageElement, conflictImage) } slack-0.11.3/block_test.go000066400000000000000000000004101430741033100153400ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewBlockMessage(t *testing.T) { dividerBlock := NewDividerBlock() blockMessage := NewBlockMessage(dividerBlock) assert.Equal(t, len(blockMessage.Msg.Blocks.BlockSet), 1) } slack-0.11.3/block_unknown.go000066400000000000000000000006541430741033100160720ustar00rootroot00000000000000package slack // UnknownBlock represents a block type that is not yet known. This block type exists to prevent Slack from introducing // new and unknown block types that break this library. type UnknownBlock struct { Type MessageBlockType `json:"type"` BlockID string `json:"block_id,omitempty"` } // BlockType returns the type of the block func (b UnknownBlock) BlockType() MessageBlockType { return b.Type } slack-0.11.3/bookmarks.go000066400000000000000000000107521430741033100152110ustar00rootroot00000000000000package slack import ( "context" "net/url" ) type Bookmark struct { ID string `json:"id"` ChannelID string `json:"channel_id"` Title string `json:"title"` Link string `json:"link"` Emoji string `json:"emoji"` IconURL string `json:"icon_url"` Type string `json:"type"` Created JSONTime `json:"date_created"` Updated JSONTime `json:"date_updated"` Rank string `json:"rank"` LastUpdatedByUserID string `json:"last_updated_by_user_id"` LastUpdatedByTeamID string `json:"last_updated_by_team_id"` ShortcutID string `json:"shortcut_id"` EntityID string `json:"entity_id"` AppID string `json:"app_id"` } type AddBookmarkParameters struct { Title string // A required title for the bookmark Type string // A required type for the bookmark Link string // URL required for type:link Emoji string // An optional emoji EntityID string ParentID string } type EditBookmarkParameters struct { Title *string // Change the title. Set to "" to clear Emoji *string // Change the emoji. Set to "" to clear Link string // Change the link } type addBookmarkResponse struct { Bookmark Bookmark `json:"bookmark"` SlackResponse } type editBookmarkResponse struct { Bookmark Bookmark `json:"bookmark"` SlackResponse } type listBookmarksResponse struct { Bookmarks []Bookmark `json:"bookmarks"` SlackResponse } // AddBookmark adds a bookmark in a channel func (api *Client) AddBookmark(channelID string, params AddBookmarkParameters) (Bookmark, error) { return api.AddBookmarkContext(context.Background(), channelID, params) } // AddBookmarkContext adds a bookmark in a channel with a custom context func (api *Client) AddBookmarkContext(ctx context.Context, channelID string, params AddBookmarkParameters) (Bookmark, error) { values := url.Values{ "channel_id": {channelID}, "token": {api.token}, "title": {params.Title}, "type": {params.Type}, } if params.Link != "" { values.Set("link", params.Link) } if params.Emoji != "" { values.Set("emoji", params.Emoji) } if params.EntityID != "" { values.Set("entity_id", params.EntityID) } if params.ParentID != "" { values.Set("parent_id", params.ParentID) } response := &addBookmarkResponse{} if err := api.postMethod(ctx, "bookmarks.add", values, response); err != nil { return Bookmark{}, err } return response.Bookmark, response.Err() } // RemoveBookmark removes a bookmark from a channel func (api *Client) RemoveBookmark(channelID, bookmarkID string) error { return api.RemoveBookmarkContext(context.Background(), channelID, bookmarkID) } // RemoveBookmarkContext removes a bookmark from a channel with a custom context func (api *Client) RemoveBookmarkContext(ctx context.Context, channelID, bookmarkID string) error { values := url.Values{ "channel_id": {channelID}, "token": {api.token}, "bookmark_id": {bookmarkID}, } response := &SlackResponse{} if err := api.postMethod(ctx, "bookmarks.remove", values, response); err != nil { return err } return response.Err() } // ListBookmarks returns all bookmarks for a channel. func (api *Client) ListBookmarks(channelID string) ([]Bookmark, error) { return api.ListBookmarksContext(context.Background(), channelID) } // ListBookmarksContext returns all bookmarks for a channel with a custom context. func (api *Client) ListBookmarksContext(ctx context.Context, channelID string) ([]Bookmark, error) { values := url.Values{ "channel_id": {channelID}, "token": {api.token}, } response := &listBookmarksResponse{} err := api.postMethod(ctx, "bookmarks.list", values, response) if err != nil { return nil, err } return response.Bookmarks, response.Err() } func (api *Client) EditBookmark(channelID, bookmarkID string, params EditBookmarkParameters) (Bookmark, error) { return api.EditBookmarkContext(context.Background(), channelID, bookmarkID, params) } func (api *Client) EditBookmarkContext(ctx context.Context, channelID, bookmarkID string, params EditBookmarkParameters) (Bookmark, error) { values := url.Values{ "channel_id": {channelID}, "token": {api.token}, "bookmark_id": {bookmarkID}, } if params.Link != "" { values.Set("link", params.Link) } if params.Emoji != nil { values.Set("emoji", *params.Emoji) } if params.Title != nil { values.Set("title", *params.Title) } response := &editBookmarkResponse{} if err := api.postMethod(ctx, "bookmarks.edit", values, response); err != nil { return Bookmark{}, err } return response.Bookmark, response.Err() } slack-0.11.3/bookmarks_test.go000066400000000000000000000151561430741033100162530ustar00rootroot00000000000000package slack import ( "encoding/json" "fmt" "net/http" "reflect" "testing" ) func getTestBookmark(channelID, bookmarkID string) Bookmark { return Bookmark{ ID: bookmarkID, ChannelID: channelID, Title: "bookmark", Type: "link", Link: "https://example.com", IconURL: "https://example.com/icon.png", } } func addBookmarkLinkHandler(rw http.ResponseWriter, r *http.Request) { channelID := r.FormValue("channel_id") title := r.FormValue("title") bookmarkType := r.FormValue("type") link := r.FormValue("link") rw.Header().Set("Content-Type", "application/json") if bookmarkType == "link" && link != "" && channelID != "" && title != "" { bookmark := getTestBookmark(channelID, "Bk123RBZG8GZ") bookmark.Title = title bookmark.Type = bookmarkType bookmark.Link = link resp, _ := json.Marshal(&addBookmarkResponse{ SlackResponse: SlackResponse{Ok: true}, Bookmark: bookmark}) rw.Write(resp) } else { rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) } } func TestAddBookmarkLink(t *testing.T) { http.HandleFunc("/bookmarks.add", addBookmarkLinkHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) params := AddBookmarkParameters{ Title: "test", Type: "link", Link: "https://example.com", } _, err := api.AddBookmark("CXXXXXXXX", params) if err != nil { t.Fatalf("Unexpected error: %s", err) } } func listBookmarksHandler(rw http.ResponseWriter, r *http.Request) { channelID := r.FormValue("channel_id") rw.Header().Set("Content-Type", "application/json") if channelID != "" { bookmarks := []Bookmark{ getTestBookmark(channelID, "Bk001"), getTestBookmark(channelID, "Bk002"), getTestBookmark(channelID, "Bk003"), getTestBookmark(channelID, "Bk004"), } resp, _ := json.Marshal(&listBookmarksResponse{ SlackResponse: SlackResponse{Ok: true}, Bookmarks: bookmarks}) rw.Write(resp) } else { rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) } } func TestListBookmarks(t *testing.T) { http.HandleFunc("/bookmarks.list", listBookmarksHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) channel := "CXXXXXXXX" bookmarks, err := api.ListBookmarks(channel) if err != nil { t.Fatalf("Unexpected error: %s", err) } if !reflect.DeepEqual([]Bookmark{ getTestBookmark(channel, "Bk001"), getTestBookmark(channel, "Bk002"), getTestBookmark(channel, "Bk003"), getTestBookmark(channel, "Bk004"), }, bookmarks) { t.Fatal(ErrIncorrectResponse) } } func removeBookmarkHandler(bookmark *Bookmark) func(rw http.ResponseWriter, r *http.Request) { return func(rw http.ResponseWriter, r *http.Request) { channelID := r.FormValue("channel_id") bookmarkID := r.FormValue("bookmark_id") rw.Header().Set("Content-Type", "application/json") if channelID == bookmark.ChannelID && bookmarkID == bookmark.ID { rw.Write([]byte(`{ "ok": true }`)) } else { rw.Write([]byte(`{ "ok": false, "error": "errored" }`)) } } } func TestRemoveBookmark(t *testing.T) { channel := "CXXXXXXXX" bookmark := getTestBookmark(channel, "BkXXXXX") http.HandleFunc("/bookmarks.remove", removeBookmarkHandler(&bookmark)) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) err := api.RemoveBookmark(channel, bookmark.ID) if err != nil { t.Fatalf("Unexpected error: %s", err) } } func editBookmarkHandler(bookmarks []Bookmark) func(rw http.ResponseWriter, r *http.Request) { return func(rw http.ResponseWriter, r *http.Request) { channelID := r.FormValue("channel_id") bookmarkID := r.FormValue("bookmark_id") rw.Header().Set("Content-Type", "application/json") if err := r.ParseForm(); err != nil { httpTestErrReply(rw, true, fmt.Sprintf("err parsing form: %s", err.Error())) return } for _, bookmark := range bookmarks { if bookmark.ID == bookmarkID && bookmark.ChannelID == channelID { if v := r.Form.Get("link"); v != "" { bookmark.Link = v } // Emoji and title require special handling since empty string sets to null if _, ok := r.Form["emoji"]; ok { bookmark.Emoji = r.Form.Get("emoji") } if _, ok := r.Form["title"]; ok { bookmark.Title = r.Form.Get("title") } resp, _ := json.Marshal(&editBookmarkResponse{ SlackResponse: SlackResponse{Ok: true}, Bookmark: bookmark}) rw.Write(resp) return } } // Fail if the bookmark doesn't exist rw.Write([]byte(`{ "ok": false, "error": "not_found" }`)) } } func TestEditBookmark(t *testing.T) { channel := "CXXXXXXXX" bookmarks := []Bookmark{ getTestBookmark(channel, "Bk001"), getTestBookmark(channel, "Bk002"), getTestBookmark(channel, "Bk003"), getTestBookmark(channel, "Bk004"), } http.HandleFunc("/bookmarks.edit", editBookmarkHandler(bookmarks)) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) smileEmoji := ":smile:" empty := "" title := "hello, world!" changes := []struct { ID string Params EditBookmarkParameters }{ { // add emoji ID: "Bk001", Params: EditBookmarkParameters{Emoji: &smileEmoji}, }, { // delete emoji ID: "Bk001", Params: EditBookmarkParameters{Emoji: &empty}, }, { // add title ID: "Bk002", Params: EditBookmarkParameters{Title: &title}, }, { // delete title ID: "Bk002", Params: EditBookmarkParameters{Title: &empty}, }, { // Change multiple fields at once ID: "Bk003", Params: EditBookmarkParameters{ Title: &title, Emoji: &empty, Link: "https://example.com/changed", }, }, { // noop ID: "Bk004", }, } for _, change := range changes { bookmark, err := api.EditBookmark(channel, change.ID, change.Params) if err != nil { t.Fatalf("Unexpected error: %s", err) } if change.ID != bookmark.ID { t.Fatalf("expected to modify bookmark with ID = %s, got %s", change.ID, bookmark.ID) } if change.Params.Emoji != nil && bookmark.Emoji != *change.Params.Emoji { t.Fatalf("expected bookmark.Emoji = %s, got %s", *change.Params.Emoji, bookmark.Emoji) } if change.Params.Title != nil && bookmark.Title != *change.Params.Title { t.Fatalf("expected bookmark.Title = %s, got %s", *change.Params.Title, bookmark.Emoji) } if change.Params.Link != "" && change.Params.Link != bookmark.Link { t.Fatalf("expected bookmark.Link = %s, got %s", change.Params.Link, bookmark.Link) } } // Cover the final case of trying to edit a bookmark which doesn't exist bookmark, err := api.EditBookmark(channel, "BkMissing", EditBookmarkParameters{}) if err == nil { t.Fatalf("Expected not found error, but got bookmark %s", bookmark.ID) } } slack-0.11.3/bots.go000066400000000000000000000024551430741033100141710ustar00rootroot00000000000000package slack import ( "context" "net/url" ) // Bot contains information about a bot type Bot struct { ID string `json:"id"` Name string `json:"name"` Deleted bool `json:"deleted"` UserID string `json:"user_id"` AppID string `json:"app_id"` Updated JSONTime `json:"updated"` Icons Icons `json:"icons"` } type botResponseFull struct { Bot `json:"bot,omitempty"` // GetBotInfo SlackResponse } func (api *Client) botRequest(ctx context.Context, path string, values url.Values) (*botResponseFull, error) { response := &botResponseFull{} err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } if err := response.Err(); err != nil { return nil, err } return response, nil } // GetBotInfo will retrieve the complete bot information func (api *Client) GetBotInfo(bot string) (*Bot, error) { return api.GetBotInfoContext(context.Background(), bot) } // GetBotInfoContext will retrieve the complete bot information using a custom context func (api *Client) GetBotInfoContext(ctx context.Context, bot string) (*Bot, error) { values := url.Values{ "token": {api.token}, } if bot != "" { values.Add("bot", bot) } response, err := api.botRequest(ctx, "bots.info", values) if err != nil { return nil, err } return &response.Bot, nil } slack-0.11.3/bots_test.go000066400000000000000000000030361430741033100152240ustar00rootroot00000000000000package slack import ( "net/http" "testing" ) func getBotInfo(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response := []byte(`{"ok": true, "bot": { "id":"B02875YLA", "deleted":false, "name":"github", "updated": 1449272004, "app_id":"A161CLERW", "user_id": "U012ABCDEF", "icons": { "image_36":"https:\/\/a.slack-edge.com\/2fac\/plugins\/github\/assets\/service_36.png", "image_48":"https:\/\/a.slack-edge.com\/2fac\/plugins\/github\/assets\/service_48.png", "image_72":"https:\/\/a.slack-edge.com\/2fac\/plugins\/github\/assets\/service_72.png" } }}`) rw.Write(response) } func TestGetBotInfo(t *testing.T) { http.HandleFunc("/bots.info", getBotInfo) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) bot, err := api.GetBotInfo("B02875YLA") if err != nil { t.Errorf("Unexpected error: %s", err) return } if bot.ID != "B02875YLA" { t.Fatal("Incorrect ID") } if bot.Name != "github" { t.Fatal("Incorrect Name") } if bot.Deleted { t.Fatal("Incorrect Deleted flag") } if bot.AppID != "A161CLERW" { t.Fatal("Incorrect App ID") } if bot.UserID != "U012ABCDEF" { t.Fatal("Incorrect User ID") } if bot.Updated != 1449272004 { t.Fatal("Incorrect Updated") } if len(bot.Icons.Image36) == 0 { t.Fatal("Missing Image36") } if len(bot.Icons.Image48) == 0 { t.Fatal("Missing Image38") } if len(bot.Icons.Image72) == 0 { t.Fatal("Missing Image72") } } slack-0.11.3/channels.go000066400000000000000000000016351430741033100150140ustar00rootroot00000000000000package slack import ( "context" "net/url" ) type channelResponseFull struct { Channel Channel `json:"channel"` Channels []Channel `json:"channels"` Purpose string `json:"purpose"` Topic string `json:"topic"` NotInChannel bool `json:"not_in_channel"` History SlackResponse Metadata ResponseMetadata `json:"response_metadata"` } // Channel contains information about the channel type Channel struct { GroupConversation IsChannel bool `json:"is_channel"` IsGeneral bool `json:"is_general"` IsMember bool `json:"is_member"` Locale string `json:"locale"` } func (api *Client) channelRequest(ctx context.Context, path string, values url.Values) (*channelResponseFull, error) { response := &channelResponseFull{} err := postForm(ctx, api.httpclient, api.endpoint+path, values, response, api) if err != nil { return nil, err } return response, response.Err() } slack-0.11.3/chat.go000066400000000000000000000666611430741033100141520ustar00rootroot00000000000000package slack import ( "bytes" "context" "encoding/json" "io/ioutil" "net/http" "net/url" "strconv" "github.com/slack-go/slack/slackutilsx" ) const ( DEFAULT_MESSAGE_USERNAME = "" DEFAULT_MESSAGE_REPLY_BROADCAST = false DEFAULT_MESSAGE_ASUSER = false DEFAULT_MESSAGE_PARSE = "" DEFAULT_MESSAGE_THREAD_TIMESTAMP = "" DEFAULT_MESSAGE_LINK_NAMES = 0 DEFAULT_MESSAGE_UNFURL_LINKS = false DEFAULT_MESSAGE_UNFURL_MEDIA = true DEFAULT_MESSAGE_ICON_URL = "" DEFAULT_MESSAGE_ICON_EMOJI = "" DEFAULT_MESSAGE_MARKDOWN = true DEFAULT_MESSAGE_ESCAPE_TEXT = true ) type chatResponseFull struct { Channel string `json:"channel"` Timestamp string `json:"ts"` //Regular message timestamp MessageTimeStamp string `json:"message_ts"` //Ephemeral message timestamp ScheduledMessageID string `json:"scheduled_message_id,omitempty"` //Scheduled message id Text string `json:"text"` SlackResponse } // getMessageTimestamp will inspect the `chatResponseFull` to ruturn a timestamp value // in `chat.postMessage` its under `ts` // in `chat.postEphemeral` its under `message_ts` func (c chatResponseFull) getMessageTimestamp() string { if len(c.Timestamp) > 0 { return c.Timestamp } return c.MessageTimeStamp } // PostMessageParameters contains all the parameters necessary (including the optional ones) for a PostMessage() request type PostMessageParameters struct { Username string `json:"username"` AsUser bool `json:"as_user"` Parse string `json:"parse"` ThreadTimestamp string `json:"thread_ts"` ReplyBroadcast bool `json:"reply_broadcast"` LinkNames int `json:"link_names"` UnfurlLinks bool `json:"unfurl_links"` UnfurlMedia bool `json:"unfurl_media"` IconURL string `json:"icon_url"` IconEmoji string `json:"icon_emoji"` Markdown bool `json:"mrkdwn,omitempty"` EscapeText bool `json:"escape_text"` // chat.postEphemeral support Channel string `json:"channel"` User string `json:"user"` // chat metadata support MetaData SlackMetadata `json:"metadata"` } // NewPostMessageParameters provides an instance of PostMessageParameters with all the sane default values set func NewPostMessageParameters() PostMessageParameters { return PostMessageParameters{ Username: DEFAULT_MESSAGE_USERNAME, User: DEFAULT_MESSAGE_USERNAME, AsUser: DEFAULT_MESSAGE_ASUSER, Parse: DEFAULT_MESSAGE_PARSE, ThreadTimestamp: DEFAULT_MESSAGE_THREAD_TIMESTAMP, LinkNames: DEFAULT_MESSAGE_LINK_NAMES, UnfurlLinks: DEFAULT_MESSAGE_UNFURL_LINKS, UnfurlMedia: DEFAULT_MESSAGE_UNFURL_MEDIA, IconURL: DEFAULT_MESSAGE_ICON_URL, IconEmoji: DEFAULT_MESSAGE_ICON_EMOJI, Markdown: DEFAULT_MESSAGE_MARKDOWN, EscapeText: DEFAULT_MESSAGE_ESCAPE_TEXT, } } // DeleteMessage deletes a message in a channel func (api *Client) DeleteMessage(channel, messageTimestamp string) (string, string, error) { return api.DeleteMessageContext(context.Background(), channel, messageTimestamp) } // DeleteMessageContext deletes a message in a channel with a custom context func (api *Client) DeleteMessageContext(ctx context.Context, channel, messageTimestamp string) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( ctx, channel, MsgOptionDelete(messageTimestamp), ) return respChannel, respTimestamp, err } // ScheduleMessage sends a message to a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) ScheduleMessage(channelID, postAt string, options ...MsgOption) (string, string, error) { return api.ScheduleMessageContext(context.Background(), channelID, postAt, options...) } // ScheduleMessageContext sends a message to a channel with a custom context // // For more details, see ScheduleMessage documentation. func (api *Client) ScheduleMessageContext(ctx context.Context, channelID, postAt string, options ...MsgOption) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( ctx, channelID, MsgOptionSchedule(postAt), MsgOptionCompose(options...), ) return respChannel, respTimestamp, err } // PostMessage sends a message to a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) PostMessage(channelID string, options ...MsgOption) (string, string, error) { return api.PostMessageContext(context.Background(), channelID, options...) } // PostMessageContext sends a message to a channel with a custom context // For more details, see PostMessage documentation. func (api *Client) PostMessageContext(ctx context.Context, channelID string, options ...MsgOption) (string, string, error) { respChannel, respTimestamp, _, err := api.SendMessageContext( ctx, channelID, MsgOptionPost(), MsgOptionCompose(options...), ) return respChannel, respTimestamp, err } // PostEphemeral sends an ephemeral message to a user in a channel. // Message is escaped by default according to https://api.slack.com/docs/formatting // Use http://davestevens.github.io/slack-message-builder/ to help crafting your message. func (api *Client) PostEphemeral(channelID, userID string, options ...MsgOption) (string, error) { return api.PostEphemeralContext(context.Background(), channelID, userID, options...) } // PostEphemeralContext sends an ephemeal message to a user in a channel with a custom context // For more details, see PostEphemeral documentation func (api *Client) PostEphemeralContext(ctx context.Context, channelID, userID string, options ...MsgOption) (timestamp string, err error) { _, timestamp, _, err = api.SendMessageContext( ctx, channelID, MsgOptionPostEphemeral(userID), MsgOptionCompose(options...), ) return timestamp, err } // UpdateMessage updates a message in a channel func (api *Client) UpdateMessage(channelID, timestamp string, options ...MsgOption) (string, string, string, error) { return api.UpdateMessageContext(context.Background(), channelID, timestamp, options...) } // UpdateMessageContext updates a message in a channel func (api *Client) UpdateMessageContext(ctx context.Context, channelID, timestamp string, options ...MsgOption) (string, string, string, error) { return api.SendMessageContext( ctx, channelID, MsgOptionUpdate(timestamp), MsgOptionCompose(options...), ) } // UnfurlMessage unfurls a message in a channel func (api *Client) UnfurlMessage(channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { return api.UnfurlMessageContext(context.Background(), channelID, timestamp, unfurls, options...) } // UnfurlMessageContext unfurls a message in a channel with a custom context func (api *Client) UnfurlMessageContext(ctx context.Context, channelID, timestamp string, unfurls map[string]Attachment, options ...MsgOption) (string, string, string, error) { return api.SendMessageContext(ctx, channelID, MsgOptionUnfurl(timestamp, unfurls), MsgOptionCompose(options...)) } // UnfurlMessageWithAuthURL sends an unfurl request containing an // authentication URL. // For more details see: // https://api.slack.com/reference/messaging/link-unfurling#authenticated_unfurls func (api *Client) UnfurlMessageWithAuthURL(channelID, timestamp string, userAuthURL string, options ...MsgOption) (string, string, string, error) { return api.UnfurlMessageWithAuthURLContext(context.Background(), channelID, timestamp, userAuthURL, options...) } // UnfurlMessageWithAuthURLContext sends an unfurl request containing an // authentication URL. // For more details see: // https://api.slack.com/reference/messaging/link-unfurling#authenticated_unfurls func (api *Client) UnfurlMessageWithAuthURLContext(ctx context.Context, channelID, timestamp string, userAuthURL string, options ...MsgOption) (string, string, string, error) { return api.SendMessageContext(ctx, channelID, MsgOptionUnfurlAuthURL(timestamp, userAuthURL), MsgOptionCompose(options...)) } // SendMessage more flexible method for configuring messages. func (api *Client) SendMessage(channel string, options ...MsgOption) (string, string, string, error) { return api.SendMessageContext(context.Background(), channel, options...) } // SendMessageContext more flexible method for configuring messages with a custom context. func (api *Client) SendMessageContext(ctx context.Context, channelID string, options ...MsgOption) (_channel string, _timestamp string, _text string, err error) { var ( req *http.Request parser func(*chatResponseFull) responseParser response chatResponseFull ) if req, parser, err = buildSender(api.endpoint, options...).BuildRequestContext(ctx, api.token, channelID); err != nil { return "", "", "", err } if api.Debug() { reqBody, err := ioutil.ReadAll(req.Body) if err != nil { return "", "", "", err } req.Body = ioutil.NopCloser(bytes.NewBuffer(reqBody)) api.Debugf("Sending request: %s", string(reqBody)) } if err = doPost(ctx, api.httpclient, req, parser(&response), api); err != nil { return "", "", "", err } return response.Channel, response.getMessageTimestamp(), response.Text, response.Err() } // UnsafeApplyMsgOptions utility function for debugging/testing chat requests. // NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this function // will be supported by the library. func UnsafeApplyMsgOptions(token, channel, apiurl string, options ...MsgOption) (string, url.Values, error) { config, err := applyMsgOptions(token, channel, apiurl, options...) return config.endpoint, config.values, err } func applyMsgOptions(token, channel, apiurl string, options ...MsgOption) (sendConfig, error) { config := sendConfig{ apiurl: apiurl, endpoint: apiurl + string(chatPostMessage), values: url.Values{ "token": {token}, "channel": {channel}, }, } for _, opt := range options { if err := opt(&config); err != nil { return config, err } } return config, nil } func buildSender(apiurl string, options ...MsgOption) sendConfig { return sendConfig{ apiurl: apiurl, options: options, } } type sendMode string const ( chatUpdate sendMode = "chat.update" chatPostMessage sendMode = "chat.postMessage" chatScheduleMessage sendMode = "chat.scheduleMessage" chatDelete sendMode = "chat.delete" chatPostEphemeral sendMode = "chat.postEphemeral" chatResponse sendMode = "chat.responseURL" chatMeMessage sendMode = "chat.meMessage" chatUnfurl sendMode = "chat.unfurl" ) type sendConfig struct { apiurl string options []MsgOption mode sendMode endpoint string values url.Values attachments []Attachment metadata SlackMetadata blocks Blocks responseType string replaceOriginal bool deleteOriginal bool } func (t sendConfig) BuildRequest(token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { return t.BuildRequestContext(context.Background(), token, channelID) } func (t sendConfig) BuildRequestContext(ctx context.Context, token, channelID string) (req *http.Request, _ func(*chatResponseFull) responseParser, err error) { if t, err = applyMsgOptions(token, channelID, t.apiurl, t.options...); err != nil { return nil, nil, err } switch t.mode { case chatResponse: return responseURLSender{ endpoint: t.endpoint, values: t.values, attachments: t.attachments, metadata: t.metadata, blocks: t.blocks, responseType: t.responseType, replaceOriginal: t.replaceOriginal, deleteOriginal: t.deleteOriginal, }.BuildRequestContext(ctx) default: return formSender{endpoint: t.endpoint, values: t.values}.BuildRequestContext(ctx) } } type formSender struct { endpoint string values url.Values } func (t formSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { return t.BuildRequestContext(context.Background()) } func (t formSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { req, err := formReq(ctx, t.endpoint, t.values) return req, func(resp *chatResponseFull) responseParser { return newJSONParser(resp) }, err } type responseURLSender struct { endpoint string values url.Values attachments []Attachment metadata SlackMetadata blocks Blocks responseType string replaceOriginal bool deleteOriginal bool } func (t responseURLSender) BuildRequest() (*http.Request, func(*chatResponseFull) responseParser, error) { return t.BuildRequestContext(context.Background()) } func (t responseURLSender) BuildRequestContext(ctx context.Context) (*http.Request, func(*chatResponseFull) responseParser, error) { req, err := jsonReq(ctx, t.endpoint, Msg{ Text: t.values.Get("text"), Timestamp: t.values.Get("ts"), Attachments: t.attachments, Blocks: t.blocks, Metadata: t.metadata, ResponseType: t.responseType, ReplaceOriginal: t.replaceOriginal, DeleteOriginal: t.deleteOriginal, }) return req, func(resp *chatResponseFull) responseParser { return newContentTypeParser(resp) }, err } // MsgOption option provided when sending a message. type MsgOption func(*sendConfig) error // MsgOptionSchedule schedules a messages. func MsgOptionSchedule(postAt string) MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatScheduleMessage) config.values.Add("post_at", postAt) return nil } } // MsgOptionPost posts a messages, this is the default. func MsgOptionPost() MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatPostMessage) config.values.Del("ts") return nil } } // MsgOptionPostEphemeral - posts an ephemeral message to the provided user. func MsgOptionPostEphemeral(userID string) MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatPostEphemeral) MsgOptionUser(userID)(config) config.values.Del("ts") return nil } } // MsgOptionMeMessage posts a "me message" type from the calling user func MsgOptionMeMessage() MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatMeMessage) return nil } } // MsgOptionUpdate updates a message based on the timestamp. func MsgOptionUpdate(timestamp string) MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatUpdate) config.values.Add("ts", timestamp) return nil } } // MsgOptionDelete deletes a message based on the timestamp. func MsgOptionDelete(timestamp string) MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatDelete) config.values.Add("ts", timestamp) return nil } } // MsgOptionUnfurl unfurls a message based on the timestamp. func MsgOptionUnfurl(timestamp string, unfurls map[string]Attachment) MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatUnfurl) config.values.Add("ts", timestamp) unfurlsStr, err := json.Marshal(unfurls) if err == nil { config.values.Add("unfurls", string(unfurlsStr)) } return err } } // MsgOptionUnfurlAuthURL unfurls a message using an auth url based on the timestamp. func MsgOptionUnfurlAuthURL(timestamp string, userAuthURL string) MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatUnfurl) config.values.Add("ts", timestamp) config.values.Add("user_auth_url", userAuthURL) return nil } } // MsgOptionUnfurlAuthRequired requests that the user installs the // Slack app for unfurling. func MsgOptionUnfurlAuthRequired(timestamp string) MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatUnfurl) config.values.Add("ts", timestamp) config.values.Add("user_auth_required", "true") return nil } } // MsgOptionUnfurlAuthMessage attaches a message inviting the user to // authenticate. func MsgOptionUnfurlAuthMessage(timestamp string, msg string) MsgOption { return func(config *sendConfig) error { config.endpoint = config.apiurl + string(chatUnfurl) config.values.Add("ts", timestamp) config.values.Add("user_auth_message", msg) return nil } } // MsgOptionResponseURL supplies a url to use as the endpoint. func MsgOptionResponseURL(url string, responseType string) MsgOption { return func(config *sendConfig) error { config.mode = chatResponse config.endpoint = url config.responseType = responseType config.values.Del("ts") return nil } } // MsgOptionReplaceOriginal replaces original message with response url func MsgOptionReplaceOriginal(responseURL string) MsgOption { return func(config *sendConfig) error { config.mode = chatResponse config.endpoint = responseURL config.replaceOriginal = true return nil } } // MsgOptionDeleteOriginal deletes original message with response url func MsgOptionDeleteOriginal(responseURL string) MsgOption { return func(config *sendConfig) error { config.mode = chatResponse config.endpoint = responseURL config.deleteOriginal = true return nil } } // MsgOptionAsUser whether or not to send the message as the user. func MsgOptionAsUser(b bool) MsgOption { return func(config *sendConfig) error { if b != DEFAULT_MESSAGE_ASUSER { config.values.Set("as_user", "true") } return nil } } // MsgOptionUser set the user for the message. func MsgOptionUser(userID string) MsgOption { return func(config *sendConfig) error { config.values.Set("user", userID) return nil } } // MsgOptionUsername set the username for the message. func MsgOptionUsername(username string) MsgOption { return func(config *sendConfig) error { config.values.Set("username", username) return nil } } // MsgOptionText provide the text for the message, optionally escape the provided // text. func MsgOptionText(text string, escape bool) MsgOption { return func(config *sendConfig) error { if escape { text = slackutilsx.EscapeMessage(text) } config.values.Add("text", text) return nil } } // MsgOptionAttachments provide attachments for the message. func MsgOptionAttachments(attachments ...Attachment) MsgOption { return func(config *sendConfig) error { if attachments == nil { return nil } config.attachments = attachments // FIXME: We are setting the attachments on the message twice: above for // the json version, and below for the html version. The marshalled bytes // we put into config.values below don't work directly in the Msg version. attachmentBytes, err := json.Marshal(attachments) if err == nil { config.values.Set("attachments", string(attachmentBytes)) } return err } } // MsgOptionBlocks sets blocks for the message func MsgOptionBlocks(blocks ...Block) MsgOption { return func(config *sendConfig) error { if blocks == nil { return nil } config.blocks.BlockSet = append(config.blocks.BlockSet, blocks...) blocks, err := json.Marshal(blocks) if err == nil { config.values.Set("blocks", string(blocks)) } return err } } // MsgOptionEnableLinkUnfurl enables link unfurling func MsgOptionEnableLinkUnfurl() MsgOption { return func(config *sendConfig) error { config.values.Set("unfurl_links", "true") return nil } } // MsgOptionDisableLinkUnfurl disables link unfurling func MsgOptionDisableLinkUnfurl() MsgOption { return func(config *sendConfig) error { config.values.Set("unfurl_links", "false") return nil } } // MsgOptionDisableMediaUnfurl disables media unfurling. func MsgOptionDisableMediaUnfurl() MsgOption { return func(config *sendConfig) error { config.values.Set("unfurl_media", "false") return nil } } // MsgOptionDisableMarkdown disables markdown. func MsgOptionDisableMarkdown() MsgOption { return func(config *sendConfig) error { config.values.Set("mrkdwn", "false") return nil } } // MsgOptionTS sets the thread TS of the message to enable creating or replying to a thread func MsgOptionTS(ts string) MsgOption { return func(config *sendConfig) error { config.values.Set("thread_ts", ts) return nil } } // MsgOptionBroadcast sets reply_broadcast to true func MsgOptionBroadcast() MsgOption { return func(config *sendConfig) error { config.values.Set("reply_broadcast", "true") return nil } } // MsgOptionCompose combines multiple options into a single option. func MsgOptionCompose(options ...MsgOption) MsgOption { return func(config *sendConfig) error { for _, opt := range options { if err := opt(config); err != nil { return err } } return nil } } // MsgOptionParse set parse option. func MsgOptionParse(b bool) MsgOption { return func(config *sendConfig) error { var v string if b { v = "full" } else { v = "none" } config.values.Set("parse", v) return nil } } // MsgOptionIconURL sets an icon URL func MsgOptionIconURL(iconURL string) MsgOption { return func(config *sendConfig) error { config.values.Set("icon_url", iconURL) return nil } } // MsgOptionIconEmoji sets an icon emoji func MsgOptionIconEmoji(iconEmoji string) MsgOption { return func(config *sendConfig) error { config.values.Set("icon_emoji", iconEmoji) return nil } } // MsgOptionMetadata sets message metadata func MsgOptionMetadata(metadata SlackMetadata) MsgOption { return func(config *sendConfig) error { config.metadata = metadata meta, err := json.Marshal(metadata) if err == nil { config.values.Set("metadata", string(meta)) } return err } } // UnsafeMsgOptionEndpoint deliver the message to the specified endpoint. // NOTE: USE AT YOUR OWN RISK: No issues relating to the use of this Option // will be supported by the library, it is subject to change without notice that // may result in compilation errors or runtime behaviour changes. func UnsafeMsgOptionEndpoint(endpoint string, update func(url.Values)) MsgOption { return func(config *sendConfig) error { config.endpoint = endpoint update(config.values) return nil } } // MsgOptionPostMessageParameters maintain backwards compatibility. func MsgOptionPostMessageParameters(params PostMessageParameters) MsgOption { return func(config *sendConfig) error { if params.Username != DEFAULT_MESSAGE_USERNAME { config.values.Set("username", params.Username) } // chat.postEphemeral support if params.User != DEFAULT_MESSAGE_USERNAME { config.values.Set("user", params.User) } // never generates an error. MsgOptionAsUser(params.AsUser)(config) if params.Parse != DEFAULT_MESSAGE_PARSE { config.values.Set("parse", params.Parse) } if params.LinkNames != DEFAULT_MESSAGE_LINK_NAMES { config.values.Set("link_names", "1") } if params.UnfurlLinks != DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "true") } // I want to send a message with explicit `as_user` `true` and `unfurl_links` `false` in request. // Because setting `as_user` to `true` will change the default value for `unfurl_links` to `true` on Slack API side. if params.AsUser != DEFAULT_MESSAGE_ASUSER && params.UnfurlLinks == DEFAULT_MESSAGE_UNFURL_LINKS { config.values.Set("unfurl_links", "false") } if params.UnfurlMedia != DEFAULT_MESSAGE_UNFURL_MEDIA { config.values.Set("unfurl_media", "false") } if params.IconURL != DEFAULT_MESSAGE_ICON_URL { config.values.Set("icon_url", params.IconURL) } if params.IconEmoji != DEFAULT_MESSAGE_ICON_EMOJI { config.values.Set("icon_emoji", params.IconEmoji) } if params.Markdown != DEFAULT_MESSAGE_MARKDOWN { config.values.Set("mrkdwn", "false") } if params.ThreadTimestamp != DEFAULT_MESSAGE_THREAD_TIMESTAMP { config.values.Set("thread_ts", params.ThreadTimestamp) } if params.ReplyBroadcast != DEFAULT_MESSAGE_REPLY_BROADCAST { config.values.Set("reply_broadcast", "true") } return nil } } // PermalinkParameters are the parameters required to get a permalink to a // message. Slack documentation can be found here: // https://api.slack.com/methods/chat.getPermalink type PermalinkParameters struct { Channel string Ts string } // GetPermalink returns the permalink for a message. It takes // PermalinkParameters and returns a string containing the permalink. It // returns an error if unable to retrieve the permalink. func (api *Client) GetPermalink(params *PermalinkParameters) (string, error) { return api.GetPermalinkContext(context.Background(), params) } // GetPermalinkContext returns the permalink for a message using a custom context. func (api *Client) GetPermalinkContext(ctx context.Context, params *PermalinkParameters) (string, error) { values := url.Values{ "channel": {params.Channel}, "message_ts": {params.Ts}, } response := struct { Channel string `json:"channel"` Permalink string `json:"permalink"` SlackResponse }{} err := api.getMethod(ctx, "chat.getPermalink", api.token, values, &response) if err != nil { return "", err } return response.Permalink, response.Err() } type GetScheduledMessagesParameters struct { Channel string Cursor string Latest string Limit int Oldest string } // GetScheduledMessages returns the list of scheduled messages based on params func (api *Client) GetScheduledMessages(params *GetScheduledMessagesParameters) (channels []ScheduledMessage, nextCursor string, err error) { return api.GetScheduledMessagesContext(context.Background(), params) } // GetScheduledMessagesContext returns the list of scheduled messages in a Slack team with a custom context func (api *Client) GetScheduledMessagesContext(ctx context.Context, params *GetScheduledMessagesParameters) (channels []ScheduledMessage, nextCursor string, err error) { values := url.Values{ "token": {api.token}, } if params.Channel != "" { values.Add("channel", params.Channel) } if params.Cursor != "" { values.Add("cursor", params.Cursor) } if params.Limit != 0 { values.Add("limit", strconv.Itoa(params.Limit)) } if params.Latest != "" { values.Add("latest", params.Latest) } if params.Oldest != "" { values.Add("oldest", params.Oldest) } response := struct { Messages []ScheduledMessage `json:"scheduled_messages"` ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} err = api.postMethod(ctx, "chat.scheduledMessages.list", values, &response) if err != nil { return nil, "", err } return response.Messages, response.ResponseMetaData.NextCursor, response.Err() } type DeleteScheduledMessageParameters struct { Channel string ScheduledMessageID string AsUser bool } // DeleteScheduledMessage returns the list of scheduled messages based on params func (api *Client) DeleteScheduledMessage(params *DeleteScheduledMessageParameters) (bool, error) { return api.DeleteScheduledMessageContext(context.Background(), params) } // DeleteScheduledMessageContext returns the list of scheduled messages in a Slack team with a custom context func (api *Client) DeleteScheduledMessageContext(ctx context.Context, params *DeleteScheduledMessageParameters) (bool, error) { values := url.Values{ "token": {api.token}, "channel": {params.Channel}, "scheduled_message_id": {params.ScheduledMessageID}, "as_user": {strconv.FormatBool(params.AsUser)}, } response := struct { SlackResponse }{} err := api.postMethod(ctx, "chat.deleteScheduledMessage", values, &response) if err != nil { return false, err } return response.Ok, response.Err() } slack-0.11.3/chat_test.go000066400000000000000000000211071430741033100151730ustar00rootroot00000000000000package slack import ( "encoding/json" "io/ioutil" "net/http" "net/url" "reflect" "testing" ) func postMessageInvalidChannelHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(chatResponseFull{ SlackResponse: SlackResponse{Ok: false, Error: "channel_not_found"}, }) rw.Write(response) } func TestPostMessageInvalidChannel(t *testing.T) { http.DefaultServeMux = new(http.ServeMux) http.HandleFunc("/chat.postMessage", postMessageInvalidChannelHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) _, _, err := api.PostMessage("CXXXXXXXX", MsgOptionText("hello", false)) if err == nil { t.Errorf("Expected error: channel_not_found; instead succeeded") return } if err.Error() != "channel_not_found" { t.Errorf("Expected error: channel_not_found; received: %s", err) return } } func TestGetPermalink(t *testing.T) { channel := "C1H9RESGA" timeStamp := "p135854651500008" http.HandleFunc("/chat.getPermalink", func(rw http.ResponseWriter, r *http.Request) { if got, want := r.Header.Get("Content-Type"), "application/x-www-form-urlencoded"; got != want { t.Errorf("request uses unexpected content type: got %s, want %s", got, want) } if got, want := r.URL.Query().Get("channel"), channel; got != want { t.Errorf("request contains unexpected channel: got %s, want %s", got, want) } if got, want := r.URL.Query().Get("message_ts"), timeStamp; got != want { t.Errorf("request contains unexpected message timestamp: got %s, want %s", got, want) } rw.Header().Set("Content-Type", "application/json") response := []byte("{\"ok\": true, \"channel\": \"" + channel + "\", \"permalink\": \"https://ghostbusters.slack.com/archives/" + channel + "/" + timeStamp + "\"}") rw.Write(response) }) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) pp := PermalinkParameters{Channel: channel, Ts: timeStamp} pl, err := api.GetPermalink(&pp) if got, want := pl, "https://ghostbusters.slack.com/archives/C1H9RESGA/p135854651500008"; got != want { t.Errorf("unexpected permalink: got %s, want %s", got, want) } if err != nil { t.Errorf("unexpected error returned: %v", err) } } func TestPostMessage(t *testing.T) { type messageTest struct { endpoint string opt []MsgOption expected url.Values } blocks := []Block{NewContextBlock("context", NewTextBlockObject(PlainTextType, "hello", false, false))} blockStr := `[{"type":"context","block_id":"context","elements":[{"type":"plain_text","text":"hello"}]}]` tests := map[string]messageTest{ "OnlyBasicProperties": { endpoint: "/chat.postMessage", opt: []MsgOption{}, expected: url.Values{ "channel": []string{"CXXX"}, "token": []string{"testing-token"}, }, }, "Blocks": { endpoint: "/chat.postMessage", opt: []MsgOption{ MsgOptionBlocks(blocks...), MsgOptionText("text", false), }, expected: url.Values{ "blocks": []string{blockStr}, "channel": []string{"CXXX"}, "text": []string{"text"}, "token": []string{"testing-token"}, }, }, "Attachment": { endpoint: "/chat.postMessage", opt: []MsgOption{ MsgOptionAttachments( Attachment{ Blocks: Blocks{BlockSet: blocks}, }), }, expected: url.Values{ "attachments": []string{`[{"blocks":` + blockStr + `}]`}, "channel": []string{"CXXX"}, "token": []string{"testing-token"}, }, }, "Metadata": { endpoint: "/chat.postMessage", opt: []MsgOption{ MsgOptionMetadata( SlackMetadata{ EventType: "testing-event", EventPayload: map[string]interface{}{ "id": 13, "name": "testing-name", }, }), }, expected: url.Values{ "metadata": []string{`{"event_type":"testing-event","event_payload":{"id":13,"name":"testing-name"}}`}, "channel": []string{"CXXX"}, "token": []string{"testing-token"}, }, }, "Unfurl": { endpoint: "/chat.unfurl", opt: []MsgOption{ MsgOptionUnfurl("123", map[string]Attachment{"something": {Text: "attachment-test"}}), }, expected: url.Values{ "channel": []string{"CXXX"}, "token": []string{"testing-token"}, "ts": []string{"123"}, "unfurls": []string{`{"something":{"text":"attachment-test","blocks":null}}`}, }, }, "UnfurlAuthURL": { endpoint: "/chat.unfurl", opt: []MsgOption{ MsgOptionUnfurlAuthURL("123", "https://auth-url.com"), }, expected: url.Values{ "channel": []string{"CXXX"}, "token": []string{"testing-token"}, "ts": []string{"123"}, "user_auth_url": []string{"https://auth-url.com"}, }, }, "UnfurlAuthRequired": { endpoint: "/chat.unfurl", opt: []MsgOption{ MsgOptionUnfurlAuthRequired("123"), }, expected: url.Values{ "channel": []string{"CXXX"}, "token": []string{"testing-token"}, "ts": []string{"123"}, "user_auth_required": []string{"true"}, }, }, "UnfurlAuthMessage": { endpoint: "/chat.unfurl", opt: []MsgOption{ MsgOptionUnfurlAuthMessage("123", "Please!"), }, expected: url.Values{ "channel": []string{"CXXX"}, "token": []string{"testing-token"}, "ts": []string{"123"}, "user_auth_message": []string{"Please!"}, }, }, } once.Do(startServer) api := New(validToken, OptionAPIURL("http://"+serverAddr+"/")) for name, test := range tests { t.Run(name, func(t *testing.T) { http.DefaultServeMux = new(http.ServeMux) http.HandleFunc(test.endpoint, func(rw http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { t.Errorf("unexpected error: %v", err) return } actual, err := url.ParseQuery(string(body)) if err != nil { t.Errorf("unexpected error: %v", err) return } if !reflect.DeepEqual(actual, test.expected) { t.Errorf("\nexpected: %s\n actual: %s", test.expected, actual) return } }) _, _, _ = api.PostMessage("CXXX", test.opt...) }) } } func TestPostMessageWithBlocksWhenMsgOptionResponseURLApplied(t *testing.T) { expectedBlocks := []Block{NewContextBlock("context", NewTextBlockObject(PlainTextType, "hello", false, false))} http.DefaultServeMux = new(http.ServeMux) http.HandleFunc("/response-url", func(rw http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { t.Errorf("unexpected error: %v", err) return } var msg Msg if err := json.Unmarshal(body, &msg); err != nil { t.Errorf("unexpected error: %v", err) return } actualBlocks := msg.Blocks.BlockSet if !reflect.DeepEqual(expectedBlocks, actualBlocks) { t.Errorf("expected: %#v, got: %#v", expectedBlocks, actualBlocks) return } }) once.Do(startServer) api := New(validToken, OptionAPIURL("http://"+serverAddr+"/")) responseURL := api.endpoint + "response-url" _, _, _ = api.PostMessage("CXXX", MsgOptionBlocks(expectedBlocks...), MsgOptionText("text", false), MsgOptionResponseURL(responseURL, ResponseTypeInChannel)) } func TestPostMessageWhenMsgOptionReplaceOriginalApplied(t *testing.T) { http.DefaultServeMux = new(http.ServeMux) http.HandleFunc("/response-url", func(rw http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { t.Errorf("unexpected error: %v", err) return } var msg Msg if err := json.Unmarshal(body, &msg); err != nil { t.Errorf("unexpected error: %v", err) return } if msg.ReplaceOriginal != true { t.Errorf("expected: true, got: %v", msg.ReplaceOriginal) return } }) once.Do(startServer) api := New(validToken, OptionAPIURL("http://"+serverAddr+"/")) responseURL := api.endpoint + "response-url" _, _, _ = api.PostMessage("CXXX", MsgOptionText("text", false), MsgOptionReplaceOriginal(responseURL)) } func TestPostMessageWhenMsgOptionDeleteOriginalApplied(t *testing.T) { http.DefaultServeMux = new(http.ServeMux) http.HandleFunc("/response-url", func(rw http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { t.Errorf("unexpected error: %v", err) return } var msg Msg if err := json.Unmarshal(body, &msg); err != nil { t.Errorf("unexpected error: %v", err) return } if msg.DeleteOriginal != true { t.Errorf("expected: true, got: %v", msg.DeleteOriginal) return } }) once.Do(startServer) api := New(validToken, OptionAPIURL("http://"+serverAddr+"/")) responseURL := api.endpoint + "response-url" _, _, _ = api.PostMessage("CXXX", MsgOptionDeleteOriginal(responseURL)) } slack-0.11.3/comment.go000066400000000000000000000005121430741033100146540ustar00rootroot00000000000000package slack // Comment contains all the information relative to a comment type Comment struct { ID string `json:"id,omitempty"` Created JSONTime `json:"created,omitempty"` Timestamp JSONTime `json:"timestamp,omitempty"` User string `json:"user,omitempty"` Comment string `json:"comment,omitempty"` } slack-0.11.3/conversation.go000066400000000000000000000504761430741033100157420ustar00rootroot00000000000000package slack import ( "context" "net/url" "strconv" "strings" ) // Conversation is the foundation for IM and BaseGroupConversation type Conversation struct { ID string `json:"id"` Created JSONTime `json:"created"` IsOpen bool `json:"is_open"` LastRead string `json:"last_read,omitempty"` Latest *Message `json:"latest,omitempty"` UnreadCount int `json:"unread_count,omitempty"` UnreadCountDisplay int `json:"unread_count_display,omitempty"` IsGroup bool `json:"is_group"` IsShared bool `json:"is_shared"` IsIM bool `json:"is_im"` IsExtShared bool `json:"is_ext_shared"` IsOrgShared bool `json:"is_org_shared"` IsPendingExtShared bool `json:"is_pending_ext_shared"` IsPrivate bool `json:"is_private"` IsMpIM bool `json:"is_mpim"` Unlinked int `json:"unlinked"` NameNormalized string `json:"name_normalized"` NumMembers int `json:"num_members"` Priority float64 `json:"priority"` User string `json:"user"` // TODO support pending_shared // TODO support previous_names } // GroupConversation is the foundation for Group and Channel type GroupConversation struct { Conversation Name string `json:"name"` Creator string `json:"creator"` IsArchived bool `json:"is_archived"` Members []string `json:"members"` Topic Topic `json:"topic"` Purpose Purpose `json:"purpose"` } // Topic contains information about the topic type Topic struct { Value string `json:"value"` Creator string `json:"creator"` LastSet JSONTime `json:"last_set"` } // Purpose contains information about the purpose type Purpose struct { Value string `json:"value"` Creator string `json:"creator"` LastSet JSONTime `json:"last_set"` } type GetUsersInConversationParameters struct { ChannelID string Cursor string Limit int } type GetConversationsForUserParameters struct { UserID string Cursor string Types []string Limit int ExcludeArchived bool } type responseMetaData struct { NextCursor string `json:"next_cursor"` } // GetUsersInConversation returns the list of users in a conversation func (api *Client) GetUsersInConversation(params *GetUsersInConversationParameters) ([]string, string, error) { return api.GetUsersInConversationContext(context.Background(), params) } // GetUsersInConversationContext returns the list of users in a conversation with a custom context func (api *Client) GetUsersInConversationContext(ctx context.Context, params *GetUsersInConversationParameters) ([]string, string, error) { values := url.Values{ "token": {api.token}, "channel": {params.ChannelID}, } if params.Cursor != "" { values.Add("cursor", params.Cursor) } if params.Limit != 0 { values.Add("limit", strconv.Itoa(params.Limit)) } response := struct { Members []string `json:"members"` ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} err := api.postMethod(ctx, "conversations.members", values, &response) if err != nil { return nil, "", err } if err := response.Err(); err != nil { return nil, "", err } return response.Members, response.ResponseMetaData.NextCursor, nil } // GetConversationsForUser returns the list conversations for a given user func (api *Client) GetConversationsForUser(params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { return api.GetConversationsForUserContext(context.Background(), params) } // GetConversationsForUserContext returns the list conversations for a given user with a custom context func (api *Client) GetConversationsForUserContext(ctx context.Context, params *GetConversationsForUserParameters) (channels []Channel, nextCursor string, err error) { values := url.Values{ "token": {api.token}, } if params.UserID != "" { values.Add("user", params.UserID) } if params.Cursor != "" { values.Add("cursor", params.Cursor) } if params.Limit != 0 { values.Add("limit", strconv.Itoa(params.Limit)) } if params.Types != nil { values.Add("types", strings.Join(params.Types, ",")) } if params.ExcludeArchived { values.Add("exclude_archived", "true") } response := struct { Channels []Channel `json:"channels"` ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} err = api.postMethod(ctx, "users.conversations", values, &response) if err != nil { return nil, "", err } return response.Channels, response.ResponseMetaData.NextCursor, response.Err() } // ArchiveConversation archives a conversation func (api *Client) ArchiveConversation(channelID string) error { return api.ArchiveConversationContext(context.Background(), channelID) } // ArchiveConversationContext archives a conversation with a custom context func (api *Client) ArchiveConversationContext(ctx context.Context, channelID string) error { values := url.Values{ "token": {api.token}, "channel": {channelID}, } response := SlackResponse{} err := api.postMethod(ctx, "conversations.archive", values, &response) if err != nil { return err } return response.Err() } // UnArchiveConversation reverses conversation archival func (api *Client) UnArchiveConversation(channelID string) error { return api.UnArchiveConversationContext(context.Background(), channelID) } // UnArchiveConversationContext reverses conversation archival with a custom context func (api *Client) UnArchiveConversationContext(ctx context.Context, channelID string) error { values := url.Values{ "token": {api.token}, "channel": {channelID}, } response := SlackResponse{} err := api.postMethod(ctx, "conversations.unarchive", values, &response) if err != nil { return err } return response.Err() } // SetTopicOfConversation sets the topic for a conversation func (api *Client) SetTopicOfConversation(channelID, topic string) (*Channel, error) { return api.SetTopicOfConversationContext(context.Background(), channelID, topic) } // SetTopicOfConversationContext sets the topic for a conversation with a custom context func (api *Client) SetTopicOfConversationContext(ctx context.Context, channelID, topic string) (*Channel, error) { values := url.Values{ "token": {api.token}, "channel": {channelID}, "topic": {topic}, } response := struct { SlackResponse Channel *Channel `json:"channel"` }{} err := api.postMethod(ctx, "conversations.setTopic", values, &response) if err != nil { return nil, err } return response.Channel, response.Err() } // SetPurposeOfConversation sets the purpose for a conversation func (api *Client) SetPurposeOfConversation(channelID, purpose string) (*Channel, error) { return api.SetPurposeOfConversationContext(context.Background(), channelID, purpose) } // SetPurposeOfConversationContext sets the purpose for a conversation with a custom context func (api *Client) SetPurposeOfConversationContext(ctx context.Context, channelID, purpose string) (*Channel, error) { values := url.Values{ "token": {api.token}, "channel": {channelID}, "purpose": {purpose}, } response := struct { SlackResponse Channel *Channel `json:"channel"` }{} err := api.postMethod(ctx, "conversations.setPurpose", values, &response) if err != nil { return nil, err } return response.Channel, response.Err() } // RenameConversation renames a conversation func (api *Client) RenameConversation(channelID, channelName string) (*Channel, error) { return api.RenameConversationContext(context.Background(), channelID, channelName) } // RenameConversationContext renames a conversation with a custom context func (api *Client) RenameConversationContext(ctx context.Context, channelID, channelName string) (*Channel, error) { values := url.Values{ "token": {api.token}, "channel": {channelID}, "name": {channelName}, } response := struct { SlackResponse Channel *Channel `json:"channel"` }{} err := api.postMethod(ctx, "conversations.rename", values, &response) if err != nil { return nil, err } return response.Channel, response.Err() } // InviteUsersToConversation invites users to a channel func (api *Client) InviteUsersToConversation(channelID string, users ...string) (*Channel, error) { return api.InviteUsersToConversationContext(context.Background(), channelID, users...) } // InviteUsersToConversationContext invites users to a channel with a custom context func (api *Client) InviteUsersToConversationContext(ctx context.Context, channelID string, users ...string) (*Channel, error) { values := url.Values{ "token": {api.token}, "channel": {channelID}, "users": {strings.Join(users, ",")}, } response := struct { SlackResponse Channel *Channel `json:"channel"` }{} err := api.postMethod(ctx, "conversations.invite", values, &response) if err != nil { return nil, err } return response.Channel, response.Err() } // KickUserFromConversation removes a user from a conversation func (api *Client) KickUserFromConversation(channelID string, user string) error { return api.KickUserFromConversationContext(context.Background(), channelID, user) } // KickUserFromConversationContext removes a user from a conversation with a custom context func (api *Client) KickUserFromConversationContext(ctx context.Context, channelID string, user string) error { values := url.Values{ "token": {api.token}, "channel": {channelID}, "user": {user}, } response := SlackResponse{} err := api.postMethod(ctx, "conversations.kick", values, &response) if err != nil { return err } return response.Err() } // CloseConversation closes a direct message or multi-person direct message func (api *Client) CloseConversation(channelID string) (noOp bool, alreadyClosed bool, err error) { return api.CloseConversationContext(context.Background(), channelID) } // CloseConversationContext closes a direct message or multi-person direct message with a custom context func (api *Client) CloseConversationContext(ctx context.Context, channelID string) (noOp bool, alreadyClosed bool, err error) { values := url.Values{ "token": {api.token}, "channel": {channelID}, } response := struct { SlackResponse NoOp bool `json:"no_op"` AlreadyClosed bool `json:"already_closed"` }{} err = api.postMethod(ctx, "conversations.close", values, &response) if err != nil { return false, false, err } return response.NoOp, response.AlreadyClosed, response.Err() } // CreateConversation initiates a public or private channel-based conversation func (api *Client) CreateConversation(channelName string, isPrivate bool) (*Channel, error) { return api.CreateConversationContext(context.Background(), channelName, isPrivate) } // CreateConversationContext initiates a public or private channel-based conversation with a custom context func (api *Client) CreateConversationContext(ctx context.Context, channelName string, isPrivate bool) (*Channel, error) { values := url.Values{ "token": {api.token}, "name": {channelName}, "is_private": {strconv.FormatBool(isPrivate)}, } response, err := api.channelRequest(ctx, "conversations.create", values) if err != nil { return nil, err } return &response.Channel, nil } // GetConversationInfo retrieves information about a conversation func (api *Client) GetConversationInfo(channelID string, includeLocale bool) (*Channel, error) { return api.GetConversationInfoContext(context.Background(), channelID, includeLocale) } // GetConversationInfoContext retrieves information about a conversation with a custom context func (api *Client) GetConversationInfoContext(ctx context.Context, channelID string, includeLocale bool) (*Channel, error) { values := url.Values{ "token": {api.token}, "channel": {channelID}, "include_locale": {strconv.FormatBool(includeLocale)}, } response, err := api.channelRequest(ctx, "conversations.info", values) if err != nil { return nil, err } return &response.Channel, response.Err() } // LeaveConversation leaves a conversation func (api *Client) LeaveConversation(channelID string) (bool, error) { return api.LeaveConversationContext(context.Background(), channelID) } // LeaveConversationContext leaves a conversation with a custom context func (api *Client) LeaveConversationContext(ctx context.Context, channelID string) (bool, error) { values := url.Values{ "token": {api.token}, "channel": {channelID}, } response, err := api.channelRequest(ctx, "conversations.leave", values) if err != nil { return false, err } return response.NotInChannel, err } type GetConversationRepliesParameters struct { ChannelID string Timestamp string Cursor string Inclusive bool Latest string Limit int Oldest string } // GetConversationReplies retrieves a thread of messages posted to a conversation func (api *Client) GetConversationReplies(params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) { return api.GetConversationRepliesContext(context.Background(), params) } // GetConversationRepliesContext retrieves a thread of messages posted to a conversation with a custom context func (api *Client) GetConversationRepliesContext(ctx context.Context, params *GetConversationRepliesParameters) (msgs []Message, hasMore bool, nextCursor string, err error) { values := url.Values{ "token": {api.token}, "channel": {params.ChannelID}, "ts": {params.Timestamp}, } if params.Cursor != "" { values.Add("cursor", params.Cursor) } if params.Latest != "" { values.Add("latest", params.Latest) } if params.Limit != 0 { values.Add("limit", strconv.Itoa(params.Limit)) } if params.Oldest != "" { values.Add("oldest", params.Oldest) } if params.Inclusive { values.Add("inclusive", "1") } else { values.Add("inclusive", "0") } response := struct { SlackResponse HasMore bool `json:"has_more"` ResponseMetaData struct { NextCursor string `json:"next_cursor"` } `json:"response_metadata"` Messages []Message `json:"messages"` }{} err = api.postMethod(ctx, "conversations.replies", values, &response) if err != nil { return nil, false, "", err } return response.Messages, response.HasMore, response.ResponseMetaData.NextCursor, response.Err() } type GetConversationsParameters struct { Cursor string ExcludeArchived bool Limit int Types []string TeamID string } // GetConversations returns the list of channels in a Slack team func (api *Client) GetConversations(params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { return api.GetConversationsContext(context.Background(), params) } // GetConversationsContext returns the list of channels in a Slack team with a custom context func (api *Client) GetConversationsContext(ctx context.Context, params *GetConversationsParameters) (channels []Channel, nextCursor string, err error) { values := url.Values{ "token": {api.token}, } if params.Cursor != "" { values.Add("cursor", params.Cursor) } if params.Limit != 0 { values.Add("limit", strconv.Itoa(params.Limit)) } if params.Types != nil { values.Add("types", strings.Join(params.Types, ",")) } if params.ExcludeArchived { values.Add("exclude_archived", strconv.FormatBool(params.ExcludeArchived)) } if params.TeamID != "" { values.Add("team_id", params.TeamID) } response := struct { Channels []Channel `json:"channels"` ResponseMetaData responseMetaData `json:"response_metadata"` SlackResponse }{} err = api.postMethod(ctx, "conversations.list", values, &response) if err != nil { return nil, "", err } return response.Channels, response.ResponseMetaData.NextCursor, response.Err() } type OpenConversationParameters struct { ChannelID string ReturnIM bool Users []string } // OpenConversation opens or resumes a direct message or multi-person direct message func (api *Client) OpenConversation(params *OpenConversationParameters) (*Channel, bool, bool, error) { return api.OpenConversationContext(context.Background(), params) } // OpenConversationContext opens or resumes a direct message or multi-person direct message with a custom context func (api *Client) OpenConversationContext(ctx context.Context, params *OpenConversationParameters) (*Channel, bool, bool, error) { values := url.Values{ "token": {api.token}, "return_im": {strconv.FormatBool(params.ReturnIM)}, } if params.ChannelID != "" { values.Add("channel", params.ChannelID) } if params.Users != nil { values.Add("users", strings.Join(params.Users, ",")) } response := struct { Channel *Channel `json:"channel"` NoOp bool `json:"no_op"` AlreadyOpen bool `json:"already_open"` SlackResponse }{} err := api.postMethod(ctx, "conversations.open", values, &response) if err != nil { return nil, false, false, err } return response.Channel, response.NoOp, response.AlreadyOpen, response.Err() } // JoinConversation joins an existing conversation func (api *Client) JoinConversation(channelID string) (*Channel, string, []string, error) { return api.JoinConversationContext(context.Background(), channelID) } // JoinConversationContext joins an existing conversation with a custom context func (api *Client) JoinConversationContext(ctx context.Context, channelID string) (*Channel, string, []string, error) { values := url.Values{"token": {api.token}, "channel": {channelID}} response := struct { Channel *Channel `json:"channel"` Warning string `json:"warning"` ResponseMetaData *struct { Warnings []string `json:"warnings"` } `json:"response_metadata"` SlackResponse }{} err := api.postMethod(ctx, "conversations.join", values, &response) if err != nil { return nil, "", nil, err } if response.Err() != nil { return nil, "", nil, response.Err() } var warnings []string if response.ResponseMetaData != nil { warnings = response.ResponseMetaData.Warnings } return response.Channel, response.Warning, warnings, nil } type GetConversationHistoryParameters struct { ChannelID string Cursor string Inclusive bool Latest string Limit int Oldest string IncludeAllMetadata bool } type GetConversationHistoryResponse struct { SlackResponse HasMore bool `json:"has_more"` PinCount int `json:"pin_count"` Latest string `json:"latest"` ResponseMetaData struct { NextCursor string `json:"next_cursor"` } `json:"response_metadata"` Messages []Message `json:"messages"` } // GetConversationHistory joins an existing conversation func (api *Client) GetConversationHistory(params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { return api.GetConversationHistoryContext(context.Background(), params) } // GetConversationHistoryContext joins an existing conversation with a custom context func (api *Client) GetConversationHistoryContext(ctx context.Context, params *GetConversationHistoryParameters) (*GetConversationHistoryResponse, error) { values := url.Values{"token": {api.token}, "channel": {params.ChannelID}} if params.Cursor != "" { values.Add("cursor", params.Cursor) } if params.Inclusive { values.Add("inclusive", "1") } else { values.Add("inclusive", "0") } if params.Latest != "" { values.Add("latest", params.Latest) } if params.Limit != 0 { values.Add("limit", strconv.Itoa(params.Limit)) } if params.Oldest != "" { values.Add("oldest", params.Oldest) } if params.IncludeAllMetadata { values.Add("include_all_metadata", "1") } else { values.Add("include_all_metadata", "0") } response := GetConversationHistoryResponse{} err := api.postMethod(ctx, "conversations.history", values, &response) if err != nil { return nil, err } return &response, response.Err() } // MarkConversation sets the read mark of a conversation to a specific point func (api *Client) MarkConversation(channel, ts string) (err error) { return api.MarkConversationContext(context.Background(), channel, ts) } // MarkConversationContext sets the read mark of a conversation to a specific point with a custom context func (api *Client) MarkConversationContext(ctx context.Context, channel, ts string) error { values := url.Values{ "token": {api.token}, "channel": {channel}, "ts": {ts}, } response := &SlackResponse{} err := api.postMethod(ctx, "conversations.mark", values, response) if err != nil { return err } return response.Err() } slack-0.11.3/conversation_test.go000066400000000000000000000374601430741033100167770ustar00rootroot00000000000000package slack import ( "encoding/json" "net/http" "reflect" "testing" "github.com/stretchr/testify/assert" ) // Channel var simpleChannel = `{ "id": "C024BE91L", "name": "fun", "is_channel": true, "created": 1360782804, "creator": "U024BE7LH", "is_archived": false, "is_general": false, "members": [ "U024BE7LH" ], "topic": { "value": "Fun times", "creator": "U024BE7LV", "last_set": 1369677212 }, "purpose": { "value": "This channel is for fun", "creator": "U024BE7LH", "last_set": 1360782804 }, "is_member": true, "last_read": "1401383885.000061", "unread_count": 0, "unread_count_display": 0 }` func unmarshalChannel(j string) (*Channel, error) { channel := &Channel{} if err := json.Unmarshal([]byte(j), &channel); err != nil { return nil, err } return channel, nil } func TestSimpleChannel(t *testing.T) { channel, err := unmarshalChannel(simpleChannel) assert.Nil(t, err) assertSimpleChannel(t, channel) } func assertSimpleChannel(t *testing.T, channel *Channel) { assert.NotNil(t, channel) assert.Equal(t, "C024BE91L", channel.ID) assert.Equal(t, "fun", channel.Name) assert.Equal(t, true, channel.IsChannel) assert.Equal(t, JSONTime(1360782804), channel.Created) assert.Equal(t, "U024BE7LH", channel.Creator) assert.Equal(t, false, channel.IsArchived) assert.Equal(t, false, channel.IsGeneral) assert.Equal(t, true, channel.IsMember) assert.Equal(t, "1401383885.000061", channel.LastRead) assert.Equal(t, 0, channel.UnreadCount) assert.Equal(t, 0, channel.UnreadCountDisplay) } func TestCreateSimpleChannel(t *testing.T) { channel := &Channel{} channel.ID = "C024BE91L" channel.Name = "fun" channel.IsChannel = true channel.Created = JSONTime(1360782804) channel.Creator = "U024BE7LH" channel.IsArchived = false channel.IsGeneral = false channel.IsMember = true channel.LastRead = "1401383885.000061" channel.UnreadCount = 0 channel.UnreadCountDisplay = 0 assertSimpleChannel(t, channel) } // Group var simpleGroup = `{ "id": "G024BE91L", "name": "secretplans", "is_group": true, "created": 1360782804, "creator": "U024BE7LH", "is_archived": false, "members": [ "U024BE7LH" ], "topic": { "value": "Secret plans on hold", "creator": "U024BE7LV", "last_set": 1369677212 }, "purpose": { "value": "Discuss secret plans that no-one else should know", "creator": "U024BE7LH", "last_set": 1360782804 }, "last_read": "1401383885.000061", "unread_count": 0, "unread_count_display": 0 }` func unmarshalGroup(j string) (*Group, error) { group := &Group{} if err := json.Unmarshal([]byte(j), &group); err != nil { return nil, err } return group, nil } func TestSimpleGroup(t *testing.T) { group, err := unmarshalGroup(simpleGroup) assert.Nil(t, err) assertSimpleGroup(t, group) } func assertSimpleGroup(t *testing.T, group *Group) { assert.NotNil(t, group) assert.Equal(t, "G024BE91L", group.ID) assert.Equal(t, "secretplans", group.Name) assert.Equal(t, true, group.IsGroup) assert.Equal(t, JSONTime(1360782804), group.Created) assert.Equal(t, "U024BE7LH", group.Creator) assert.Equal(t, false, group.IsArchived) assert.Equal(t, "1401383885.000061", group.LastRead) assert.Equal(t, 0, group.UnreadCount) assert.Equal(t, 0, group.UnreadCountDisplay) } func TestCreateSimpleGroup(t *testing.T) { group := &Group{} group.ID = "G024BE91L" group.Name = "secretplans" group.IsGroup = true group.Created = JSONTime(1360782804) group.Creator = "U024BE7LH" group.IsArchived = false group.LastRead = "1401383885.000061" group.UnreadCount = 0 group.UnreadCountDisplay = 0 assertSimpleGroup(t, group) } // IM var simpleIM = `{ "id": "D024BFF1M", "is_im": true, "user": "U024BE7LH", "created": 1360782804, "is_user_deleted": false, "is_open": true, "last_read": "1401383885.000061", "unread_count": 0, "unread_count_display": 0 }` func unmarshalIM(j string) (*IM, error) { im := &IM{} if err := json.Unmarshal([]byte(j), &im); err != nil { return nil, err } return im, nil } func TestSimpleIM(t *testing.T) { im, err := unmarshalIM(simpleIM) assert.Nil(t, err) assertSimpleIM(t, im) } func assertSimpleIM(t *testing.T, im *IM) { assert.NotNil(t, im) assert.Equal(t, "D024BFF1M", im.ID) assert.Equal(t, true, im.IsIM) assert.Equal(t, "U024BE7LH", im.User) assert.Equal(t, JSONTime(1360782804), im.Created) assert.Equal(t, false, im.IsUserDeleted) assert.Equal(t, true, im.IsOpen) assert.Equal(t, "1401383885.000061", im.LastRead) assert.Equal(t, 0, im.UnreadCount) assert.Equal(t, 0, im.UnreadCountDisplay) } func TestCreateSimpleIM(t *testing.T) { im := &IM{} im.ID = "D024BFF1M" im.IsIM = true im.User = "U024BE7LH" im.Created = JSONTime(1360782804) im.IsUserDeleted = false im.IsOpen = true im.LastRead = "1401383885.000061" im.UnreadCount = 0 im.UnreadCountDisplay = 0 assertSimpleIM(t, im) } func getTestMembers() []string { return []string{"test"} } func getUsersInConversation(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { SlackResponse Members []string `json:"members"` ResponseMetaData responseMetaData `json:"response_metadata"` }{ SlackResponse: SlackResponse{Ok: true}, Members: getTestMembers(), ResponseMetaData: responseMetaData{NextCursor: ""}, }) rw.Write(response) } func TestGetUsersInConversation(t *testing.T) { http.HandleFunc("/conversations.members", getUsersInConversation) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) params := GetUsersInConversationParameters{ ChannelID: "CXXXXXXXX", } expectedMembers := getTestMembers() members, _, err := api.GetUsersInConversation(¶ms) if err != nil { t.Errorf("Unexpected error: %s", err) return } if !reflect.DeepEqual(expectedMembers, members) { t.Fatal(ErrIncorrectResponse) } } func TestArchiveConversation(t *testing.T) { http.HandleFunc("/conversations.archive", okJSONHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) err := api.ArchiveConversation("CXXXXXXXX") if err != nil { t.Errorf("Unexpected error: %s", err) return } } func TestUnArchiveConversation(t *testing.T) { http.HandleFunc("/conversations.unarchive", okJSONHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) err := api.UnArchiveConversation("CXXXXXXXX") if err != nil { t.Errorf("Unexpected error: %s", err) return } } func getTestChannel() *Channel { return &Channel{ GroupConversation: GroupConversation{ Topic: Topic{ Value: "response topic", }, Purpose: Purpose{ Value: "response purpose", }, }} } func okChannelJsonHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { SlackResponse Channel *Channel `json:"channel"` }{ SlackResponse: SlackResponse{Ok: true}, Channel: getTestChannel(), }) rw.Write(response) } func TestSetTopicOfConversation(t *testing.T) { http.HandleFunc("/conversations.setTopic", okChannelJsonHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) inputChannel := getTestChannel() channel, err := api.SetTopicOfConversation("CXXXXXXXX", inputChannel.Topic.Value) if err != nil { t.Errorf("Unexpected error: %s", err) return } if channel.Topic.Value != inputChannel.Topic.Value { t.Fatalf(`topic = '%s', want '%s'`, channel.Topic.Value, inputChannel.Topic.Value) } } func TestSetPurposeOfConversation(t *testing.T) { http.HandleFunc("/conversations.setPurpose", okChannelJsonHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) inputChannel := getTestChannel() channel, err := api.SetPurposeOfConversation("CXXXXXXXX", inputChannel.Purpose.Value) if err != nil { t.Errorf("Unexpected error: %s", err) return } if channel.Purpose.Value != inputChannel.Purpose.Value { t.Fatalf(`purpose = '%s', want '%s'`, channel.Purpose.Value, inputChannel.Purpose.Value) } } func TestRenameConversation(t *testing.T) { http.HandleFunc("/conversations.rename", okChannelJsonHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) inputChannel := getTestChannel() channel, err := api.RenameConversation("CXXXXXXXX", inputChannel.Name) if err != nil { t.Errorf("Unexpected error: %s", err) return } if channel.Name != inputChannel.Name { t.Fatalf(`channelName = '%s', want '%s'`, channel.Name, inputChannel.Name) } } func TestInviteUsersToConversation(t *testing.T) { http.HandleFunc("/conversations.invite", okChannelJsonHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) users := []string{"UXXXXXXX1", "UXXXXXXX2"} channel, err := api.InviteUsersToConversation("CXXXXXXXX", users...) if err != nil { t.Errorf("Unexpected error: %s", err) return } if channel == nil { t.Error("channel should not be nil") return } } func TestKickUserFromConversation(t *testing.T) { http.HandleFunc("/conversations.kick", okJSONHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) err := api.KickUserFromConversation("CXXXXXXXX", "UXXXXXXXX") if err != nil { t.Errorf("Unexpected error: %s", err) return } } func closeConversationHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { SlackResponse NoOp bool `json:"no_op"` AlreadyClosed bool `json:"already_closed"` }{ SlackResponse: SlackResponse{Ok: true}}) rw.Write(response) } func TestCloseConversation(t *testing.T) { http.HandleFunc("/conversations.close", closeConversationHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) _, _, err := api.CloseConversation("CXXXXXXXX") if err != nil { t.Errorf("Unexpected error: %s", err) return } } func TestCreateConversation(t *testing.T) { http.HandleFunc("/conversations.create", okChannelJsonHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) channel, err := api.CreateConversation("CXXXXXXXX", false) if err != nil { t.Errorf("Unexpected error: %s", err) return } if channel == nil { t.Error("channel should not be nil") return } } func TestGetConversationInfo(t *testing.T) { http.HandleFunc("/conversations.info", okChannelJsonHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) channel, err := api.GetConversationInfo("CXXXXXXXX", false) if err != nil { t.Errorf("Unexpected error: %s", err) return } if channel == nil { t.Error("channel should not be nil") return } } func leaveConversationHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { SlackResponse NotInChannel bool `json:"not_in_channel"` }{ SlackResponse: SlackResponse{Ok: true}}) rw.Write(response) } func TestLeaveConversation(t *testing.T) { http.HandleFunc("/conversations.leave", leaveConversationHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) _, err := api.LeaveConversation("CXXXXXXXX") if err != nil { t.Errorf("Unexpected error: %s", err) return } } func getConversationRepliesHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { SlackResponse HasMore bool `json:"has_more"` ResponseMetaData struct { NextCursor string `json:"next_cursor"` } `json:"response_metadata"` Messages []Message `json:"messages"` }{ SlackResponse: SlackResponse{Ok: true}, Messages: []Message{}}) rw.Write(response) } func TestGetConversationReplies(t *testing.T) { http.HandleFunc("/conversations.replies", getConversationRepliesHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) params := GetConversationRepliesParameters{ ChannelID: "CXXXXXXXX", Timestamp: "1234567890.123456", } _, _, _, err := api.GetConversationReplies(¶ms) if err != nil { t.Errorf("Unexpected error: %s", err) return } } func getConversationsHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { SlackResponse ResponseMetaData struct { NextCursor string `json:"next_cursor"` } `json:"response_metadata"` Channels []Channel `json:"channels"` }{ SlackResponse: SlackResponse{Ok: true}, Channels: []Channel{}}) rw.Write(response) } func TestGetConversations(t *testing.T) { http.HandleFunc("/conversations.list", getConversationsHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) params := GetConversationsParameters{} _, _, err := api.GetConversations(¶ms) if err != nil { t.Errorf("Unexpected error: %s", err) return } } func openConversationHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { SlackResponse NoOp bool `json:"no_op"` AlreadyOpen bool `json:"already_open"` Channel *Channel `json:"channel"` }{ SlackResponse: SlackResponse{Ok: true}}) rw.Write(response) } func TestOpenConversation(t *testing.T) { http.HandleFunc("/conversations.open", openConversationHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) params := OpenConversationParameters{ChannelID: "CXXXXXXXX"} _, _, _, err := api.OpenConversation(¶ms) if err != nil { t.Errorf("Unexpected error: %s", err) return } } func joinConversationHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { Channel *Channel `json:"channel"` Warning string `json:"warning"` ResponseMetaData *struct { Warnings []string `json:"warnings"` } `json:"response_metadata"` SlackResponse }{ SlackResponse: SlackResponse{Ok: true}}) rw.Write(response) } func TestJoinConversation(t *testing.T) { http.HandleFunc("/conversations.join", joinConversationHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) _, _, _, err := api.JoinConversation("CXXXXXXXX") if err != nil { t.Errorf("Unexpected error: %s", err) return } } func getConversationHistoryHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(GetConversationHistoryResponse{ SlackResponse: SlackResponse{Ok: true}}) rw.Write(response) } func TestGetConversationHistory(t *testing.T) { http.HandleFunc("/conversations.history", getConversationHistoryHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) params := GetConversationHistoryParameters{ChannelID: "CXXXXXXXX"} _, err := api.GetConversationHistory(¶ms) if err != nil { t.Errorf("Unexpected error: %s", err) return } } func markConversationHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(GetConversationHistoryResponse{ SlackResponse: SlackResponse{Ok: true}}) w.Write(response) } func TestMarkConversation(t *testing.T) { http.HandleFunc("/conversations.mark", markConversationHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) err := api.MarkConversation("CXXXXXXXX", "1401383885.000061") if err != nil { t.Errorf("Unexpected error: %s", err) return } } slack-0.11.3/dialog.go000066400000000000000000000074361430741033100144650ustar00rootroot00000000000000package slack import ( "context" "encoding/json" "strings" ) // InputType is the type of the dialog input type type InputType string const ( // InputTypeText textfield input InputTypeText InputType = "text" // InputTypeTextArea textarea input InputTypeTextArea InputType = "textarea" // InputTypeSelect select menus input InputTypeSelect InputType = "select" ) // DialogInput for dialogs input type text or menu type DialogInput struct { Type InputType `json:"type"` Label string `json:"label"` Name string `json:"name"` Placeholder string `json:"placeholder"` Optional bool `json:"optional"` Hint string `json:"hint"` } // DialogTrigger ... type DialogTrigger struct { TriggerID string `json:"trigger_id"` //Required. Must respond within 3 seconds. Dialog Dialog `json:"dialog"` //Required. } // Dialog as in Slack dialogs // https://api.slack.com/dialogs#option_element_attributes#top-level_dialog_attributes type Dialog struct { TriggerID string `json:"trigger_id"` // Required CallbackID string `json:"callback_id"` // Required State string `json:"state,omitempty"` // Optional Title string `json:"title"` SubmitLabel string `json:"submit_label,omitempty"` NotifyOnCancel bool `json:"notify_on_cancel"` Elements []DialogElement `json:"elements"` } // DialogElement abstract type for dialogs. type DialogElement interface{} // DialogCallback DEPRECATED use InteractionCallback type DialogCallback InteractionCallback // DialogSubmissionCallback is sent from Slack when a user submits a form from within a dialog type DialogSubmissionCallback struct { // NOTE: State is only used with the dialog_submission type. // You should use InteractionCallback.BlockActionsState for block_actions type. State string `json:"-"` Submission map[string]string `json:"submission"` } // DialogOpenResponse response from `dialog.open` type DialogOpenResponse struct { SlackResponse DialogResponseMetadata DialogResponseMetadata `json:"response_metadata"` } // DialogResponseMetadata lists the error messages type DialogResponseMetadata struct { Messages []string `json:"messages"` } // DialogInputValidationError is an error when user inputs incorrect value to form from within a dialog type DialogInputValidationError struct { Name string `json:"name"` Error string `json:"error"` } // DialogInputValidationErrors lists the name of field and that error messages type DialogInputValidationErrors struct { Errors []DialogInputValidationError `json:"errors"` } // OpenDialog opens a dialog window where the triggerID originated from. // EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. func (api *Client) OpenDialog(triggerID string, dialog Dialog) (err error) { return api.OpenDialogContext(context.Background(), triggerID, dialog) } // OpenDialogContext opens a dialog window where the triggerId originated from with a custom context // EXPERIMENTAL: dialog functionality is currently experimental, api is not considered stable. func (api *Client) OpenDialogContext(ctx context.Context, triggerID string, dialog Dialog) (err error) { if triggerID == "" { return ErrParametersMissing } req := DialogTrigger{ TriggerID: triggerID, Dialog: dialog, } encoded, err := json.Marshal(req) if err != nil { return err } response := &DialogOpenResponse{} endpoint := api.endpoint + "dialog.open" if err := postJSON(ctx, api.httpclient, endpoint, api.token, encoded, response, api); err != nil { return err } if len(response.DialogResponseMetadata.Messages) > 0 { response.Ok = false response.Error += "\n" + strings.Join(response.DialogResponseMetadata.Messages, "\n") } return response.Err() } slack-0.11.3/dialog_select.go000066400000000000000000000101531430741033100160120ustar00rootroot00000000000000package slack // SelectDataSource types of select datasource type SelectDataSource string const ( // DialogDataSourceStatic menu with static Options/OptionGroups DialogDataSourceStatic SelectDataSource = "static" // DialogDataSourceExternal dynamic datasource DialogDataSourceExternal SelectDataSource = "external" // DialogDataSourceConversations provides a list of conversations DialogDataSourceConversations SelectDataSource = "conversations" // DialogDataSourceChannels provides a list of channels DialogDataSourceChannels SelectDataSource = "channels" // DialogDataSourceUsers provides a list of users DialogDataSourceUsers SelectDataSource = "users" ) // DialogInputSelect dialog support for select boxes. type DialogInputSelect struct { DialogInput Value string `json:"value,omitempty"` //Optional. DataSource SelectDataSource `json:"data_source,omitempty"` //Optional. Allowed values: "users", "channels", "conversations", "external". SelectedOptions []DialogSelectOption `json:"selected_options,omitempty"` //Optional. May hold at most one element, for use with "external" only. Options []DialogSelectOption `json:"options,omitempty"` //One of options or option_groups is required. OptionGroups []DialogOptionGroup `json:"option_groups,omitempty"` //Provide up to 100 options. MinQueryLength int `json:"min_query_length,omitempty"` //Optional. minimum characters before query is sent. Hint string `json:"hint,omitempty"` //Optional. Additional hint text. } // DialogSelectOption is an option for the user to select from the menu type DialogSelectOption struct { Label string `json:"label"` Value string `json:"value"` } // DialogOptionGroup is a collection of options for creating a segmented table type DialogOptionGroup struct { Label string `json:"label"` Options []DialogSelectOption `json:"options"` } // NewStaticSelectDialogInput constructor for a `static` datasource menu input func NewStaticSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect { return &DialogInputSelect{ DialogInput: DialogInput{ Type: InputTypeSelect, Name: name, Label: label, Optional: true, }, DataSource: DialogDataSourceStatic, Options: options, } } // NewExternalSelectDialogInput constructor for a `external` datasource menu input func NewExternalSelectDialogInput(name, label string, options []DialogSelectOption) *DialogInputSelect { return &DialogInputSelect{ DialogInput: DialogInput{ Type: InputTypeSelect, Name: name, Label: label, Optional: true, }, DataSource: DialogDataSourceExternal, Options: options, } } // NewGroupedSelectDialogInput creates grouped options select input for Dialogs. func NewGroupedSelectDialogInput(name, label string, options []DialogOptionGroup) *DialogInputSelect { return &DialogInputSelect{ DialogInput: DialogInput{ Type: InputTypeSelect, Name: name, Label: label, }, DataSource: DialogDataSourceStatic, OptionGroups: options} } // NewDialogOptionGroup creates a DialogOptionGroup from several select options func NewDialogOptionGroup(label string, options ...DialogSelectOption) DialogOptionGroup { return DialogOptionGroup{ Label: label, Options: options, } } // NewConversationsSelect returns a `Conversations` select func NewConversationsSelect(name, label string) *DialogInputSelect { return newPresetSelect(name, label, DialogDataSourceConversations) } // NewChannelsSelect returns a `Channels` select func NewChannelsSelect(name, label string) *DialogInputSelect { return newPresetSelect(name, label, DialogDataSourceChannels) } // NewUsersSelect returns a `Users` select func NewUsersSelect(name, label string) *DialogInputSelect { return newPresetSelect(name, label, DialogDataSourceUsers) } func newPresetSelect(name, label string, dataSourceType SelectDataSource) *DialogInputSelect { return &DialogInputSelect{ DialogInput: DialogInput{ Type: InputTypeSelect, Label: label, Name: name, }, DataSource: dataSourceType, } } slack-0.11.3/dialog_select_test.go000066400000000000000000000073511430741033100170570ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func selectOptionsFromArray(options ...string) []DialogSelectOption { selectOptions := make([]DialogSelectOption, len(options)) for idx, value := range options { selectOptions[idx] = DialogSelectOption{ Label: value, Value: value, } } return selectOptions } func selectOptionsFromMap(options map[string]string) []DialogSelectOption { selectOptions := make([]DialogSelectOption, len(options)) idx := 0 var option DialogSelectOption for key, value := range options { option = DialogSelectOption{ Label: key, Value: value, } selectOptions[idx] = option idx++ } return selectOptions } func TestSelectOptionsFromArray(t *testing.T) { options := []string{"opt 1"} expectedOptions := selectOptionsFromArray(options...) assert.Equal(t, len(options), len(expectedOptions)) firstOption := expectedOptions[0] assert.Equal(t, "opt 1", firstOption.Label) assert.Equal(t, "opt 1", firstOption.Value) } func TestOptionsFromMap(t *testing.T) { options := make(map[string]string) options["key"] = "myValue" selectOptions := selectOptionsFromMap(options) assert.Equal(t, 1, len(selectOptions)) firstOption := selectOptions[0] assert.Equal(t, "key", firstOption.Label) assert.Equal(t, "myValue", firstOption.Value) } func TestStaticSelectFromArray(t *testing.T) { name := "static select" label := "Static Select Label" expectedOptions := selectOptionsFromArray("opt 1", "opt 2", "opt 3") selectInput := NewStaticSelectDialogInput(name, label, expectedOptions) assert.Equal(t, name, selectInput.Name) assert.Equal(t, label, selectInput.Label) assert.Equal(t, expectedOptions, selectInput.Options) } func TestStaticSelectFromDictionary(t *testing.T) { name := "static select" label := "Static Select Label" optionsMap := make(map[string]string) optionsMap["option_1"] = "First" optionsMap["option_2"] = "Second" optionsMap["option_3"] = "Third" expectedOptions := selectOptionsFromMap(optionsMap) selectInput := NewStaticSelectDialogInput(name, label, expectedOptions) assert.Equal(t, name, selectInput.Name) assert.Equal(t, label, selectInput.Label) assert.Equal(t, expectedOptions, selectInput.Options) } func TestNewDialogOptionGroup(t *testing.T) { expectedOptions := selectOptionsFromArray("option_1", "option_2") label := "GroupLabel" optionGroup := NewDialogOptionGroup(label, expectedOptions...) assert.Equal(t, label, optionGroup.Label) assert.Equal(t, expectedOptions, optionGroup.Options) } func TestStaticGroupedSelect(t *testing.T) { groupOpt1 := NewDialogOptionGroup("group1", selectOptionsFromArray("G1_01", "G1_02")...) groupOpt2 := NewDialogOptionGroup("group2", selectOptionsFromArray("G2_01", "G2_02", "G2_03")...) options := []DialogOptionGroup{groupOpt1, groupOpt2} groupSelect := NewGroupedSelectDialogInput("groupSelect", "User Label", options) assert.Equal(t, InputTypeSelect, groupSelect.Type) assert.Equal(t, "groupSelect", groupSelect.Name) assert.Equal(t, "User Label", groupSelect.Label) assert.Nil(t, groupSelect.Options) assert.NotNil(t, groupSelect.OptionGroups) assert.Equal(t, 2, len(groupSelect.OptionGroups)) } func TestConversationSelect(t *testing.T) { convoSelect := NewConversationsSelect("", "") assert.Equal(t, InputTypeSelect, convoSelect.Type) assert.Equal(t, DialogDataSourceConversations, convoSelect.DataSource) } func TestChannelSelect(t *testing.T) { convoSelect := NewChannelsSelect("", "") assert.Equal(t, InputTypeSelect, convoSelect.Type) assert.Equal(t, DialogDataSourceChannels, convoSelect.DataSource) } func TestUserSelect(t *testing.T) { convoSelect := NewUsersSelect("", "") assert.Equal(t, InputTypeSelect, convoSelect.Type) assert.Equal(t, DialogDataSourceUsers, convoSelect.DataSource) } slack-0.11.3/dialog_test.go000066400000000000000000000233641430741033100155220ustar00rootroot00000000000000package slack import ( "encoding/json" "fmt" "testing" "net/http" "github.com/stretchr/testify/assert" ) // Dialogs var simpleDialog = `{ "callback_id":"ryde-46e2b0", "title":"Request a Ride", "submit_label":"Request", "notify_on_cancel":true }` var simpleTextElement = `{ "label": "testing label", "name": "testing name", "type": "text", "placeholder": "testing placeholder", "optional": true, "value": "testing value", "max_length": 1000, "min_length": 10, "hint": "testing hint", "subtype": "email" }` var simpleSelectElement = `{ "label": "testing label", "name": "testing name", "type": "select", "placeholder": "testing placeholder", "optional": true, "value": "testing value", "data_source": "users", "selected_options": [], "options": [{"label": "option 1", "value": "1"}], "option_groups": [] }` func unmarshalDialog() (*Dialog, error) { dialog := &Dialog{} // Unmarshall the simple dialog json if err := json.Unmarshal([]byte(simpleDialog), &dialog); err != nil { return nil, err } // Unmarshall and append the text element textElement := &TextInputElement{} if err := json.Unmarshal([]byte(simpleTextElement), &textElement); err != nil { return nil, err } // Unmarshall and append the select element selectElement := &DialogInputSelect{} if err := json.Unmarshal([]byte(simpleSelectElement), &selectElement); err != nil { return nil, err } dialog.Elements = []DialogElement{ textElement, selectElement, } return dialog, nil } func TestSimpleDialog(t *testing.T) { dialog, err := unmarshalDialog() assert.Nil(t, err) assertSimpleDialog(t, dialog) } func TestCreateSimpleDialog(t *testing.T) { dialog := &Dialog{} dialog.CallbackID = "ryde-46e2b0" dialog.Title = "Request a Ride" dialog.SubmitLabel = "Request" dialog.NotifyOnCancel = true textElement := &TextInputElement{} textElement.Label = "testing label" textElement.Name = "testing name" textElement.Type = "text" textElement.Placeholder = "testing placeholder" textElement.Optional = true textElement.Value = "testing value" textElement.MaxLength = 1000 textElement.MinLength = 10 textElement.Hint = "testing hint" textElement.Subtype = "email" selectElement := &DialogInputSelect{} selectElement.Label = "testing label" selectElement.Name = "testing name" selectElement.Type = "select" selectElement.Placeholder = "testing placeholder" selectElement.Optional = true selectElement.Value = "testing value" selectElement.DataSource = "users" selectElement.SelectedOptions = []DialogSelectOption{} selectElement.Options = []DialogSelectOption{ {Label: "option 1", Value: "1"}, } selectElement.OptionGroups = []DialogOptionGroup{} dialog.Elements = []DialogElement{ textElement, selectElement, } assertSimpleDialog(t, dialog) } func assertSimpleDialog(t *testing.T, dialog *Dialog) { assert.NotNil(t, dialog) // Test the main dialog fields assert.Equal(t, "ryde-46e2b0", dialog.CallbackID) assert.Equal(t, "Request a Ride", dialog.Title) assert.Equal(t, "Request", dialog.SubmitLabel) assert.Equal(t, true, dialog.NotifyOnCancel) // Test the text element is correctly parsed textElement := dialog.Elements[0].(*TextInputElement) assert.Equal(t, "testing label", textElement.Label) assert.Equal(t, "testing name", textElement.Name) assert.Equal(t, InputTypeText, textElement.Type) assert.Equal(t, "testing placeholder", textElement.Placeholder) assert.Equal(t, true, textElement.Optional) assert.Equal(t, "testing value", textElement.Value) assert.Equal(t, 1000, textElement.MaxLength) assert.Equal(t, 10, textElement.MinLength) assert.Equal(t, "testing hint", textElement.Hint) assert.Equal(t, InputSubtypeEmail, textElement.Subtype) // Test the select element is correctly parsed selectElement := dialog.Elements[1].(*DialogInputSelect) assert.Equal(t, "testing label", selectElement.Label) assert.Equal(t, "testing name", selectElement.Name) assert.Equal(t, InputTypeSelect, selectElement.Type) assert.Equal(t, "testing placeholder", selectElement.Placeholder) assert.Equal(t, true, selectElement.Optional) assert.Equal(t, "testing value", selectElement.Value) assert.Equal(t, DialogDataSourceUsers, selectElement.DataSource) assert.Equal(t, []DialogSelectOption{}, selectElement.SelectedOptions) assert.Equal(t, "option 1", selectElement.Options[0].Label) assert.Equal(t, "1", selectElement.Options[0].Value) assert.Equal(t, 0, len(selectElement.OptionGroups)) } // Callbacks var simpleCallback = `{ "type": "dialog_submission", "submission": { "name": "Sigourney Dreamweaver", "email": "sigdre@example.com", "phone": "+1 800-555-1212", "meal": "burrito", "comment": "No sour cream please", "team_channel": "C0LFFBKPB", "who_should_sing": "U0MJRG1AL" }, "callback_id": "employee_offsite_1138b", "team": { "id": "T1ABCD2E12", "domain": "coverbands" }, "user": { "id": "W12A3BCDEF", "name": "dreamweaver" }, "channel": { "id": "C1AB2C3DE", "name": "coverthon-1999" }, "action_ts": "936893340.702759", "token": "M1AqUUw3FqayAbqNtsGMch72", "response_url": "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ" }` func unmarshalCallback(j string) (*DialogCallback, error) { callback := &DialogCallback{} if err := json.Unmarshal([]byte(j), &callback); err != nil { return nil, err } return callback, nil } func TestSimpleCallback(t *testing.T) { callback, err := unmarshalCallback(simpleCallback) assert.Nil(t, err) assertSimpleCallback(t, callback) } func assertSimpleCallback(t *testing.T, callback *DialogCallback) { assert.NotNil(t, callback) assert.Equal(t, InteractionTypeDialogSubmission, callback.Type) assert.Equal(t, "employee_offsite_1138b", callback.CallbackID) assert.Equal(t, "T1ABCD2E12", callback.Team.ID) assert.Equal(t, "coverbands", callback.Team.Domain) assert.Equal(t, "C1AB2C3DE", callback.Channel.ID) assert.Equal(t, "coverthon-1999", callback.Channel.Name) assert.Equal(t, "W12A3BCDEF", callback.User.ID) assert.Equal(t, "dreamweaver", callback.User.Name) assert.Equal(t, "936893340.702759", callback.ActionTs) assert.Equal(t, "M1AqUUw3FqayAbqNtsGMch72", callback.Token) assert.Equal(t, "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ", callback.ResponseURL) assert.Equal(t, "Sigourney Dreamweaver", callback.Submission["name"]) assert.Equal(t, "sigdre@example.com", callback.Submission["email"]) assert.Equal(t, "+1 800-555-1212", callback.Submission["phone"]) assert.Equal(t, "burrito", callback.Submission["meal"]) assert.Equal(t, "No sour cream please", callback.Submission["comment"]) assert.Equal(t, "C0LFFBKPB", callback.Submission["team_channel"]) assert.Equal(t, "U0MJRG1AL", callback.Submission["who_should_sing"]) } // Suggestion Callbacks var simpleSuggestionCallback = `{ "type": "dialog_suggestion", "token": "W3VDvuzi2nRLsiaDOsmJranO", "action_ts": "1528203589.238335", "team": { "id": "T24BK35ML", "domain": "hooli-hq" }, "user": { "id": "U900MV5U7", "name": "gbelson" }, "channel": { "id": "C012AB3CD", "name": "triage-platform" }, "name": "external_data", "value": "test", "callback_id": "bugs" }` func unmarshalSuggestionCallback(j string) (*InteractionCallback, error) { callback := &InteractionCallback{} if err := json.Unmarshal([]byte(j), &callback); err != nil { return nil, err } return callback, nil } func TestSimpleSuggestionCallback(t *testing.T) { callback, err := unmarshalSuggestionCallback(simpleSuggestionCallback) assert.Nil(t, err) assertSimpleSuggestionCallback(t, callback) } func assertSimpleSuggestionCallback(t *testing.T, callback *InteractionCallback) { assert.NotNil(t, callback) assert.Equal(t, InteractionTypeDialogSuggestion, callback.Type) assert.Equal(t, "W3VDvuzi2nRLsiaDOsmJranO", callback.Token) assert.Equal(t, "1528203589.238335", callback.ActionTs) assert.Equal(t, "T24BK35ML", callback.Team.ID) assert.Equal(t, "hooli-hq", callback.Team.Domain) assert.Equal(t, "U900MV5U7", callback.User.ID) assert.Equal(t, "gbelson", callback.User.Name) assert.Equal(t, "C012AB3CD", callback.Channel.ID) assert.Equal(t, "triage-platform", callback.Channel.Name) assert.Equal(t, "external_data", callback.Name) assert.Equal(t, "test", callback.Value) assert.Equal(t, "bugs", callback.CallbackID) } func openDialogHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(struct { SlackResponse }{ SlackResponse: SlackResponse{Ok: true}, }) rw.Write(response) } func TestOpenDialog(t *testing.T) { http.HandleFunc("/dialog.open", openDialogHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) dialog, err := unmarshalDialog() if err != nil { t.Errorf("Unexpected error: %s", err) return } err = api.OpenDialog("TXXXXXXXX", *dialog) if err != nil { t.Errorf("Unexpected error: %s", err) return } err = api.OpenDialog("", *dialog) if err == nil { t.Errorf("Did not error with empty trigger, %s", err) return } } const ( triggerID = "trigger_xyz" callbackID = "callback_xyz" notifyOnCancel = false title = "Dialog_title" submitLabel = "Send" token = "xoxa-123-123-123-213" ) func _mocDialog() *Dialog { triggerID := triggerID callbackID := callbackID notifyOnCancel := notifyOnCancel title := title submitLabel := submitLabel return &Dialog{ TriggerID: triggerID, CallbackID: callbackID, NotifyOnCancel: notifyOnCancel, Title: title, SubmitLabel: submitLabel, } } func TestDialogCreate(t *testing.T) { dialog := _mocDialog() if dialog == nil { t.Errorf("Should be able to construct a dialog") t.Fail() } } func ExampleDialog() { dialog := _mocDialog() fmt.Println(*dialog) // Output: // {trigger_xyz callback_xyz Dialog_title Send false []} } slack-0.11.3/dialog_text.go000066400000000000000000000031331430741033100155170ustar00rootroot00000000000000package slack // TextInputSubtype Accepts email, number, tel, or url. In some form factors, optimized input is provided for this subtype. type TextInputSubtype string // TextInputOption handle to extra inputs options. type TextInputOption func(*TextInputElement) const ( // InputSubtypeEmail email keyboard InputSubtypeEmail TextInputSubtype = "email" // InputSubtypeNumber numeric keyboard InputSubtypeNumber TextInputSubtype = "number" // InputSubtypeTel Phone keyboard InputSubtypeTel TextInputSubtype = "tel" // InputSubtypeURL Phone keyboard InputSubtypeURL TextInputSubtype = "url" ) // TextInputElement subtype of DialogInput // https://api.slack.com/dialogs#option_element_attributes#text_element_attributes type TextInputElement struct { DialogInput MaxLength int `json:"max_length,omitempty"` MinLength int `json:"min_length,omitempty"` Hint string `json:"hint,omitempty"` Subtype TextInputSubtype `json:"subtype"` Value string `json:"value"` } // NewTextInput constructor for a `text` input func NewTextInput(name, label, text string, options ...TextInputOption) *TextInputElement { t := &TextInputElement{ DialogInput: DialogInput{ Type: InputTypeText, Name: name, Label: label, }, Value: text, } for _, opt := range options { opt(t) } return t } // NewTextAreaInput constructor for a `textarea` input func NewTextAreaInput(name, label, text string) *TextInputElement { return &TextInputElement{ DialogInput: DialogInput{ Type: InputTypeTextArea, Name: name, Label: label, }, Value: text, } } slack-0.11.3/dialog_text_test.go000066400000000000000000000013701430741033100165570ustar00rootroot00000000000000package slack import ( "testing" "github.com/stretchr/testify/assert" ) func TestNewTextInput(t *testing.T) { name := "internalName" label := "Human Readable" value := "Pre filled text" textInput := NewTextInput(name, label, value) assert.Equal(t, InputTypeText, textInput.Type) assert.Equal(t, name, textInput.Name) assert.Equal(t, label, textInput.Label) assert.Equal(t, value, textInput.Value) } func TestNewTextAreaInput(t *testing.T) { name := "internalName" label := "Human Readable" value := "Pre filled text" textInput := NewTextAreaInput(name, label, value) assert.Equal(t, InputTypeTextArea, textInput.Type) assert.Equal(t, name, textInput.Name) assert.Equal(t, label, textInput.Label) assert.Equal(t, value, textInput.Value) } slack-0.11.3/dnd.go000066400000000000000000000102671430741033100137670ustar00rootroot00000000000000package slack import ( "context" "net/url" "strconv" "strings" ) type SnoozeDebug struct { SnoozeEndDate string `json:"snooze_end_date"` } type SnoozeInfo struct { SnoozeEnabled bool `json:"snooze_enabled,omitempty"` SnoozeEndTime int `json:"snooze_endtime,omitempty"` SnoozeRemaining int `json:"snooze_remaining,omitempty"` SnoozeDebug SnoozeDebug `json:"snooze_debug,omitempty"` } type DNDStatus struct { Enabled bool `json:"dnd_enabled"` NextStartTimestamp int `json:"next_dnd_start_ts"` NextEndTimestamp int `json:"next_dnd_end_ts"` SnoozeInfo } type dndResponseFull struct { DNDStatus SlackResponse } type dndTeamInfoResponse struct { Users map[string]DNDStatus `json:"users"` SlackResponse } func (api *Client) dndRequest(ctx context.Context, path string, values url.Values) (*dndResponseFull, error) { response := &dndResponseFull{} err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } return response, response.Err() } // EndDND ends the user's scheduled Do Not Disturb session func (api *Client) EndDND() error { return api.EndDNDContext(context.Background()) } // EndDNDContext ends the user's scheduled Do Not Disturb session with a custom context func (api *Client) EndDNDContext(ctx context.Context) error { values := url.Values{ "token": {api.token}, } response := &SlackResponse{} if err := api.postMethod(ctx, "dnd.endDnd", values, response); err != nil { return err } return response.Err() } // EndSnooze ends the current user's snooze mode func (api *Client) EndSnooze() (*DNDStatus, error) { return api.EndSnoozeContext(context.Background()) } // EndSnoozeContext ends the current user's snooze mode with a custom context func (api *Client) EndSnoozeContext(ctx context.Context) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, } response, err := api.dndRequest(ctx, "dnd.endSnooze", values) if err != nil { return nil, err } return &response.DNDStatus, nil } // GetDNDInfo provides information about a user's current Do Not Disturb settings. func (api *Client) GetDNDInfo(user *string) (*DNDStatus, error) { return api.GetDNDInfoContext(context.Background(), user) } // GetDNDInfoContext provides information about a user's current Do Not Disturb settings with a custom context. func (api *Client) GetDNDInfoContext(ctx context.Context, user *string) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, } if user != nil { values.Set("user", *user) } response, err := api.dndRequest(ctx, "dnd.info", values) if err != nil { return nil, err } return &response.DNDStatus, nil } // GetDNDTeamInfo provides information about a user's current Do Not Disturb settings. func (api *Client) GetDNDTeamInfo(users []string) (map[string]DNDStatus, error) { return api.GetDNDTeamInfoContext(context.Background(), users) } // GetDNDTeamInfoContext provides information about a user's current Do Not Disturb settings with a custom context. func (api *Client) GetDNDTeamInfoContext(ctx context.Context, users []string) (map[string]DNDStatus, error) { values := url.Values{ "token": {api.token}, "users": {strings.Join(users, ",")}, } response := &dndTeamInfoResponse{} if err := api.postMethod(ctx, "dnd.teamInfo", values, response); err != nil { return nil, err } if response.Err() != nil { return nil, response.Err() } return response.Users, nil } // SetSnooze adjusts the snooze duration for a user's Do Not Disturb // settings. If a snooze session is not already active for the user, invoking // this method will begin one for the specified duration. func (api *Client) SetSnooze(minutes int) (*DNDStatus, error) { return api.SetSnoozeContext(context.Background(), minutes) } // SetSnoozeContext adjusts the snooze duration for a user's Do Not Disturb settings with a custom context. // For more information see the SetSnooze docs func (api *Client) SetSnoozeContext(ctx context.Context, minutes int) (*DNDStatus, error) { values := url.Values{ "token": {api.token}, "num_minutes": {strconv.Itoa(minutes)}, } response, err := api.dndRequest(ctx, "dnd.setSnooze", values) if err != nil { return nil, err } return &response.DNDStatus, nil } slack-0.11.3/dnd_test.go000066400000000000000000000105501430741033100150210ustar00rootroot00000000000000package slack import ( "net/http" "reflect" "testing" ) func TestSlack_EndDND(t *testing.T) { http.HandleFunc("/dnd.endDnd", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true }`)) }) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) err := api.EndDND() if err != nil { t.Fatalf("Unexpected error: %s", err) } } func TestSlack_EndSnooze(t *testing.T) { http.HandleFunc("/dnd.endSnooze", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true, "dnd_enabled": true, "next_dnd_start_ts": 1450418400, "next_dnd_end_ts": 1450454400, "snooze_enabled": false }`)) }) state := DNDStatus{ Enabled: true, NextStartTimestamp: 1450418400, NextEndTimestamp: 1450454400, SnoozeInfo: SnoozeInfo{SnoozeEnabled: false}, } once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) snoozeState, err := api.EndSnooze() if err != nil { t.Fatalf("Unexpected error: %s", err) } eq := reflect.DeepEqual(snoozeState, &state) if !eq { t.Errorf("got %v; want %v", snoozeState, &state) } } func TestSlack_GetDNDInfo(t *testing.T) { http.HandleFunc("/dnd.info", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true, "dnd_enabled": true, "next_dnd_start_ts": 1450416600, "next_dnd_end_ts": 1450452600, "snooze_enabled": true, "snooze_endtime": 1450416600, "snooze_remaining": 1196 }`)) }) userDNDInfo := DNDStatus{ Enabled: true, NextStartTimestamp: 1450416600, NextEndTimestamp: 1450452600, SnoozeInfo: SnoozeInfo{ SnoozeEnabled: true, SnoozeEndTime: 1450416600, SnoozeRemaining: 1196, }, } once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) userDNDInfoResponse, err := api.GetDNDInfo(nil) if err != nil { t.Fatalf("Unexpected error: %s", err) } eq := reflect.DeepEqual(userDNDInfoResponse, &userDNDInfo) if !eq { t.Errorf("got %v; want %v", userDNDInfoResponse, &userDNDInfo) } } func TestSlack_GetDNDTeamInfo(t *testing.T) { http.HandleFunc("/dnd.teamInfo", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true, "users": { "U023BECGF": { "dnd_enabled": true, "next_dnd_start_ts": 1450387800, "next_dnd_end_ts": 1450423800 }, "U058CJVAA": { "dnd_enabled": false, "next_dnd_start_ts": 1, "next_dnd_end_ts": 1 } } }`)) }) usersDNDInfo := map[string]DNDStatus{ "U023BECGF": { Enabled: true, NextStartTimestamp: 1450387800, NextEndTimestamp: 1450423800, }, "U058CJVAA": { Enabled: false, NextStartTimestamp: 1, NextEndTimestamp: 1, }, } once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) usersDNDInfoResponse, err := api.GetDNDTeamInfo(nil) if err != nil { t.Fatalf("Unexpected error: %s", err) } eq := reflect.DeepEqual(usersDNDInfoResponse, usersDNDInfo) if !eq { t.Errorf("got %v; want %v", usersDNDInfoResponse, usersDNDInfo) } } func TestSlack_SetSnooze(t *testing.T) { http.HandleFunc("/dnd.setSnooze", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.Write([]byte(`{ "ok": true, "dnd_enabled": true, "snooze_endtime": 1450373897, "snooze_remaining": 60 }`)) }) snooze := DNDStatus{ Enabled: true, SnoozeInfo: SnoozeInfo{ SnoozeEndTime: 1450373897, SnoozeRemaining: 60, }, } once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) snoozeResponse, err := api.SetSnooze(60) if err != nil { t.Fatalf("Unexpected error: %s", err) } eq := reflect.DeepEqual(snoozeResponse, &snooze) if !eq { t.Errorf("got %v; want %v", snoozeResponse, &snooze) } } slack-0.11.3/emoji.go000066400000000000000000000013121430741033100143140ustar00rootroot00000000000000package slack import ( "context" "net/url" ) type emojiResponseFull struct { Emoji map[string]string `json:"emoji"` SlackResponse } // GetEmoji retrieves all the emojis func (api *Client) GetEmoji() (map[string]string, error) { return api.GetEmojiContext(context.Background()) } // GetEmojiContext retrieves all the emojis with a custom context func (api *Client) GetEmojiContext(ctx context.Context) (map[string]string, error) { values := url.Values{ "token": {api.token}, } response := &emojiResponseFull{} err := api.postMethod(ctx, "emoji.list", values, response) if err != nil { return nil, err } if response.Err() != nil { return nil, response.Err() } return response.Emoji, nil } slack-0.11.3/emoji_test.go000066400000000000000000000017721430741033100153650ustar00rootroot00000000000000package slack import ( "net/http" "reflect" "testing" ) func getEmojiHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response := []byte(`{"ok": true, "emoji": { "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png", "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", "shipit": "alias:squirrel" }}`) rw.Write(response) } func TestGetEmoji(t *testing.T) { http.HandleFunc("/emoji.list", getEmojiHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) emojisResponse := map[string]string{ "bowtie": "https://my.slack.com/emoji/bowtie/46ec6f2bb0.png", "squirrel": "https://my.slack.com/emoji/squirrel/f35f40c0e0.png", "shipit": "alias:squirrel", } emojis, err := api.GetEmoji() if err != nil { t.Errorf("Unexpected error: %s", err) return } eq := reflect.DeepEqual(emojis, emojisResponse) if !eq { t.Errorf("got %v; want %v", emojis, emojisResponse) } } slack-0.11.3/errors.go000066400000000000000000000015551430741033100145360ustar00rootroot00000000000000package slack import "github.com/slack-go/slack/internal/errorsx" // Errors returned by various methods. const ( ErrAlreadyDisconnected = errorsx.String("Invalid call to Disconnect - Slack API is already disconnected") ErrRTMDisconnected = errorsx.String("disconnect received while trying to connect") ErrRTMGoodbye = errorsx.String("goodbye detected") ErrRTMDeadman = errorsx.String("deadman switch triggered") ErrParametersMissing = errorsx.String("received empty parameters") ErrBlockIDNotUnique = errorsx.String("Block ID needs to be unique") ErrInvalidConfiguration = errorsx.String("invalid configuration") ErrMissingHeaders = errorsx.String("missing headers") ErrExpiredTimestamp = errorsx.String("timestamp is too old") ) // internal errors const ( errPaginationComplete = errorsx.String("pagination complete") ) slack-0.11.3/examples/000077500000000000000000000000001430741033100145035ustar00rootroot00000000000000slack-0.11.3/examples/blocks/000077500000000000000000000000001430741033100157605ustar00rootroot00000000000000slack-0.11.3/examples/blocks/README.md000066400000000000000000000737551430741033100172600ustar00rootroot00000000000000### Block Examples The examples provided replicate the template examples provided by slack. The template builder can be found at https://api.slack.com/tools/block-kit-builder. Due to the nature how slack expects different components to be configured, building complex block using the provided functions can be very verbose, but allows for maximum flexibility. The examples below should cover implementing most supported block elements. For additional information on Blocks, see the [Block Kit website](https://api.slack.com/block-kit). ### Using examples with the Block Kit Builder website When generating examples, they will be printed to the screen as a complete message that is meant to be sent back to slack as a direct response, or throuogh the ResponseURL provided. To test your examples in the Block Kit Builder, you must take the contents of the `blocks` property and paste the results into the builder. For example, when printing a simple header, the output will be ``` { "replace_original": false, "delete_original": false, "blocks": [ { "type": "section", "text": { "type": "plain_text", "text": "Example Header Text" } }, { "type": "divider" } ] } ``` To preview this block on the builder website, you should copy just the contents of the blocks: ``` [ { "type": "section", "text": { "type": "plain_text", "text": "Example Header Text" } }, { "type": "divider" } ] ``` #### Example 1 - Approval The first example demonstrates usage of Sections, Fields and Action buttons. You can view the [Approval Example](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22You%20have%20a%20new%20request%3A%5Cn*%3CfakeLink.toEmployeeProfile.com%7CFred%20Enriquez%20-%20New%20device%20request%3E*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22fields%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Type%3A*%5CnComputer%20(laptop)%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*When%3A*%5CnSubmitted%20Aut%2010%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Last%20Update%3A*%5CnMar%2010%2C%202015%20(3%20years%2C%205%20months)%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Reason%3A*%5CnAll%20vowel%20keys%20aren%27t%20working.%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Specs%3A*%5Cn%5C%22Cheetah%20Pro%2015%5C%22%20-%20Fast%2C%20really%20fast%5C%22%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Approve%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Deny%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. This example can be generated with the function named `exampleOne`. #### Example 2 - Approval - With Images The secoond example adds additional complexity by introducing images as accessories to main blocks of text. You can view this [Approval Example with Images](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22You%20have%20a%20new%20request%3A%5Cn*%3Cgoogle.com%7CFred%20Enriquez%20-%20Time%20Off%20request%3E*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Type%3A*%5CnPaid%20time%20off%5Cn*When%3A*%5CnAug%2010-Aug%2013%5Cn*Hours%3A*%2016.0%20(2%20days)%5Cn*Remaining%20balance%3A*%2032.0%20hours%20(4%20days)%5Cn*Comments%3A*%20%5C%22Family%20in%20town%2C%20going%20camping!%5C%22%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FapprovalsNewDevice.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22computer%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Approve%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Deny%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. This example can be generated with the function named `exampleTwo`. #### Example 3 - Notifications This example shows how to add actions to your block that will trigger an interactive message to your application. You can view the rendered example for [Notifications](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%22text%22%3A%20%22Looks%20like%20you%20have%20a%20scheduling%20conflict%20with%20this%20event%3A%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toUserProfiles.com%7CIris%20%2F%20Zelda%201-1%3E*%5CnTuesday%2C%20January%2021%204%3A00-4%3A30pm%5CnBuilding%202%20-%20Havarti%20Cheese%20(3)%5Cn2%20guests%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fnotifications.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22calendar%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FnotificationsWarningIcon.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22notifications%20warning%20icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22*Conflicts%20with%20Team%20Huddle%3A%204%3A15-4%3A30pm*%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Propose%20a%20new%20time%3A*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Today%20-%204%3A30-5pm*%5CnEveryone%20is%20available%3A%20%40iris%2C%20%40zelda%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Tomorrow%20-%204-4%3A30pm*%5CnEveryone%20is%20available%3A%20%40iris%2C%20%40zelda%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Tomorrow%20-%206-6%3A30pm*%5CnSome%20people%20aren%27t%20available%3A%20%40iris%2C%20~%40zelda~%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Choose%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3Cfakelink.ToMoreTimes.com%7CShow%20more%20times%3E*%22%0A%09%09%7D%0A%09%7D%0A%5D) on the block builder website. Refer to the function `exampleThree` for details on how this block can be generated. #### Example 4 - Polls The Polls example displays results and allows the end user to vote, displaying a count and images of recent voters. You can view the rendered [Poll Example](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*Where%20should%20we%20order%20lunch%20from%3F*%20Poll%20by%20%3CfakeLink.toUser.com%7CMark%3E%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22%3Asushi%3A%20*Ace%20Wasabi%20Rock-n-Roll%20Sushi%20Bar*%5CnThe%20best%20landlocked%20sushi%20restaurant.%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Vote%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_1.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Michael%20Scott%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_2.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Dwight%20Schrute%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_3.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Pam%20Beasely%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%223%20votes%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22%3Ahamburger%3A%20*Super%20Hungryman%20Hamburgers*%5CnOnly%20for%20the%20hungriest%20of%20the%20hungry.%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Vote%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_4.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Angela%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2Fprofile_2.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Dwight%20Schrute%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%222%20votes%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22%3Aramen%3A%20*Kagawa-Ya%20Udon%20Noodle%20Shop*%5CnDo%20you%20like%20to%20shop%20for%20noodles%3F%20We%20have%20noodles.%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Vote%22%0A%09%09%09%7D%2C%0A%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%09%22text%22%3A%20%22No%20votes%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Add%20a%20suggestion%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. Refer to the function named `exampleFour` for more information on generating this block type. #### Example 5 - Search Results This example introduces overflow elements, allowing you to populate a select style dropdown with fields. These fields can be static, loaded from an external source. You can view the rendered [Search Results Example](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22We%20found%20*205%20Hotels*%20in%20New%20Orleans%2C%20LA%20from%20*12%2F14%20to%2012%2F17*%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22overflow%22%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Option%20One%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Option%20Two%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Option%20Three%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Option%20Four%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-3%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toHotelPage.com%7CWindsor%20Court%20Hotel%3E*%5Cn%E2%98%85%E2%98%85%E2%98%85%E2%98%85%E2%98%85%5Cn%24340%20per%20night%5CnRated%3A%209.4%20-%20Excellent%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgent_1.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22Windsor%20Court%20Hotel%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgentLocationMarker.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Location%20Pin%20Icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Location%3A%20Central%20Business%20District%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toHotelPage.com%7CThe%20Ritz-Carlton%20New%20Orleans%3E*%5Cn%E2%98%85%E2%98%85%E2%98%85%E2%98%85%E2%98%85%5Cn%24340%20per%20night%5CnRated%3A%209.1%20-%20Excellent%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgent_2.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22Ritz-Carlton%20New%20Orleans%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgentLocationMarker.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Location%20Pin%20Icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Location%3A%20French%20Quarter%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toHotelPage.com%7COmni%20Royal%20Orleans%20Hotel%3E*%5Cn%E2%98%85%E2%98%85%E2%98%85%E2%98%85%E2%98%85%5Cn%24419%20per%20night%5CnRated%3A%208.8%20-%20Excellent%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgent_3.png%22%2C%0A%09%09%09%22alt_text%22%3A%20%22Omni%20Royal%20Orleans%20Hotel%20thumbnail%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22context%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22image%22%2C%0A%09%09%09%09%22image_url%22%3A%20%22https%3A%2F%2Fapi.slack.com%2Fimg%2Fblocks%2Fbkb_template_images%2FtripAgentLocationMarker.png%22%2C%0A%09%09%09%09%22alt_text%22%3A%20%22Location%20Pin%20Icon%22%0A%09%09%09%7D%2C%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Location%3A%20French%20Quarter%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Next%202%20Results%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) on the block kit builder website. Refer to the function named `exampleFive` for more information on generating this block. #### Example 6 - Search Results with Options and Actions Using a combination of overflow elements containing selectable options and actions, this examples allows you to prompt the user with multiple actions in a single response. You can view the rendered [Search Results with Actions](https://api.slack.com/tools/block-kit-builder?blocks=%5B%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22%3Amag%3A%20Search%20results%20for%20*Cata*%22%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CUse%20Case%20Catalogue%3E*%5CnUse%20Case%20Catalogue%20for%20the%20following%20departments%2Froles...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Edit%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CCustomer%20Support%20-%20Workflow%20Diagram%20Catalogue%3E*%5CnThis%20resource%20was%20put%20together%20by%20members%20of...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Manage%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CSelf-Serve%20Learning%20Options%20Catalogue%3E*%5CnSee%20the%20learning%20and%20development%20options%20we...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Manage%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CUse%20Case%20Catalogue%20-%20CF%20Presentation%20-%20%5BJune%2012%2C%202018%5D%3E*%5CnThis%20is%20presentation%20will%20continue%20to%20be%20updated%20as...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Manage%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22section%22%2C%0A%09%09%22text%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22mrkdwn%22%2C%0A%09%09%09%22text%22%3A%20%22*%3CfakeLink.toYourApp.com%7CComprehensive%20Benefits%20Catalogue%20-%202019%3E*%5CnInformation%20about%20all%20the%20benfits%20we%20offer%20is...%22%0A%09%09%7D%2C%0A%09%09%22accessory%22%3A%20%7B%0A%09%09%09%22type%22%3A%20%22static_select%22%2C%0A%09%09%09%22placeholder%22%3A%20%7B%0A%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%22text%22%3A%20%22Manage%22%0A%09%09%09%7D%2C%0A%09%09%09%22options%22%3A%20%5B%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Manage%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-0%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Read%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-1%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%7B%0A%09%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%09%22text%22%3A%20%22Save%20it%22%0A%09%09%09%09%09%7D%2C%0A%09%09%09%09%09%22value%22%3A%20%22value-2%22%0A%09%09%09%09%7D%0A%09%09%09%5D%0A%09%09%7D%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22divider%22%0A%09%7D%2C%0A%09%7B%0A%09%09%22type%22%3A%20%22actions%22%2C%0A%09%09%22elements%22%3A%20%5B%0A%09%09%09%7B%0A%09%09%09%09%22type%22%3A%20%22button%22%2C%0A%09%09%09%09%22text%22%3A%20%7B%0A%09%09%09%09%09%22type%22%3A%20%22plain_text%22%2C%0A%09%09%09%09%09%22emoji%22%3A%20true%2C%0A%09%09%09%09%09%22text%22%3A%20%22Next%205%20Results%22%0A%09%09%09%09%7D%2C%0A%09%09%09%09%22value%22%3A%20%22click_me_123%22%0A%09%09%09%7D%0A%09%09%5D%0A%09%7D%0A%5D) example on the block kit builder website. Refer to the function named `exampleSix` for more information on building this block.slack-0.11.3/examples/blocks/blocks.go000066400000000000000000000537671430741033100176060ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "github.com/slack-go/slack" ) // The functions below mock the different templates slack has as examples on their website. // // Refer to README.md for more information on the examples and how to use them. func main() { fmt.Println("--- Begin Example One ---") exampleOne() fmt.Println("--- End Example One ---") fmt.Println("--- Begin Example Two ---") exampleTwo() fmt.Println("--- End Example Two ---") fmt.Println("--- Begin Example Three ---") exampleThree() fmt.Println("--- End Example Three ---") fmt.Println("--- Begin Example Four ---") exampleFour() fmt.Println("--- End Example Four ---") fmt.Println("--- Begin Example Five ---") exampleFive() fmt.Println("--- End Example Five ---") fmt.Println("--- Begin Example Six ---") exampleSix() fmt.Println("--- End Example Six ---") fmt.Println("--- Begin Example Unmarshalling ---") unmarshalExample() fmt.Println("--- End Example Unmarshalling ---") } // approvalRequest mocks the simple "Approval" template located on block kit builder website func exampleOne() { // Header Section headerText := slack.NewTextBlockObject("mrkdwn", "You have a new request:\n**", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) // Fields typeField := slack.NewTextBlockObject("mrkdwn", "*Type:*\nComputer (laptop)", false, false) whenField := slack.NewTextBlockObject("mrkdwn", "*When:*\nSubmitted Aut 10", false, false) lastUpdateField := slack.NewTextBlockObject("mrkdwn", "*Last Update:*\nMar 10, 2015 (3 years, 5 months)", false, false) reasonField := slack.NewTextBlockObject("mrkdwn", "*Reason:*\nAll vowel keys aren't working.", false, false) specsField := slack.NewTextBlockObject("mrkdwn", "*Specs:*\n\"Cheetah Pro 15\" - Fast, really fast\"", false, false) fieldSlice := make([]*slack.TextBlockObject, 0) fieldSlice = append(fieldSlice, typeField) fieldSlice = append(fieldSlice, whenField) fieldSlice = append(fieldSlice, lastUpdateField) fieldSlice = append(fieldSlice, reasonField) fieldSlice = append(fieldSlice, specsField) fieldsSection := slack.NewSectionBlock(nil, fieldSlice, nil) // Approve and Deny Buttons approveBtnTxt := slack.NewTextBlockObject("plain_text", "Approve", false, false) approveBtn := slack.NewButtonBlockElement("", "click_me_123", approveBtnTxt) denyBtnTxt := slack.NewTextBlockObject("plain_text", "Deny", false, false) denyBtn := slack.NewButtonBlockElement("", "click_me_123", denyBtnTxt) actionBlock := slack.NewActionBlock("", approveBtn, denyBtn) // Build Message with blocks created above msg := slack.NewBlockMessage( headerSection, fieldsSection, actionBlock, ) b, err := json.MarshalIndent(msg, "", " ") if err != nil { fmt.Println(err) return } fmt.Println(string(b)) } // exampleTwo mocks the more complex "Approval" template located on block kit builder website // which includes an accessory image next to the approval request func exampleTwo() { // Header Section headerText := slack.NewTextBlockObject("mrkdwn", "You have a new request:\n**", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) approvalText := slack.NewTextBlockObject("mrkdwn", "*Type:*\nPaid time off\n*When:*\nAug 10-Aug 13\n*Hours:* 16.0 (2 days)\n*Remaining balance:* 32.0 hours (4 days)\n*Comments:* \"Family in town, going camping!\"", false, false) approvalImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/approvalsNewDevice.png", "computer thumbnail") fieldsSection := slack.NewSectionBlock(approvalText, nil, slack.NewAccessory(approvalImage)) // Approve and Deny Buttons approveBtnTxt := slack.NewTextBlockObject("plain_text", "Approve", false, false) approveBtn := slack.NewButtonBlockElement("", "click_me_123", approveBtnTxt) denyBtnTxt := slack.NewTextBlockObject("plain_text", "Deny", false, false) denyBtn := slack.NewButtonBlockElement("", "click_me_123", denyBtnTxt) actionBlock := slack.NewActionBlock("", approveBtn, denyBtn) // Build Message with blocks created above msg := slack.NewBlockMessage( headerSection, fieldsSection, actionBlock, ) b, err := json.MarshalIndent(msg, "", " ") if err != nil { fmt.Println(err) return } fmt.Println(string(b)) } // exampleThree generates the notification example from the block kit builder website func exampleThree() { // Shared Assets for example chooseBtnText := slack.NewTextBlockObject("plain_text", "Choose", true, false) chooseBtnEle := slack.NewButtonBlockElement("", "click_me_123", chooseBtnText) divSection := slack.NewDividerBlock() // Header Section headerText := slack.NewTextBlockObject("plain_text", "Looks like you have a scheduling conflict with this event:", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) // Schedule Info Section scheduleText := slack.NewTextBlockObject("mrkdwn", "**\nTuesday, January 21 4:00-4:30pm\nBuilding 2 - Havarti Cheese (3)\n2 guests", false, false) scheduleAccessory := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/notifications.png", "calendar thumbnail") schedeuleSection := slack.NewSectionBlock(scheduleText, nil, slack.NewAccessory(scheduleAccessory)) // Conflict Section conflictImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/notificationsWarningIcon.png", "notifications warning icon") conflictText := slack.NewTextBlockObject("mrkdwn", "*Conflicts with Team Huddle: 4:15-4:30pm*", false, false) conflictSection := slack.NewContextBlock( "", []slack.MixedElement{conflictImage, conflictText}..., ) // Proposese Text proposeText := slack.NewTextBlockObject("mrkdwn", "*Propose a new time:*", false, false) proposeSection := slack.NewSectionBlock(proposeText, nil, nil) // Option 1 optionOneText := slack.NewTextBlockObject("mrkdwn", "*Today - 4:30-5pm*\nEveryone is available: @iris, @zelda", false, false) optionOneSection := slack.NewSectionBlock(optionOneText, nil, slack.NewAccessory(chooseBtnEle)) // Option 2 optionTwoText := slack.NewTextBlockObject("mrkdwn", "*Tomorrow - 4-4:30pm*\nEveryone is available: @iris, @zelda", false, false) optionTwoSection := slack.NewSectionBlock(optionTwoText, nil, slack.NewAccessory(chooseBtnEle)) // Option 3 optionThreeText := slack.NewTextBlockObject("mrkdwn", "*Tomorrow - 6-6:30pm*\nSome people aren't available: @iris, ~@zelda~", false, false) optionThreeSection := slack.NewSectionBlock(optionThreeText, nil, slack.NewAccessory(chooseBtnEle)) // Show More Times Link showMoreText := slack.NewTextBlockObject("mrkdwn", "**", false, false) showMoreSection := slack.NewSectionBlock(showMoreText, nil, nil) // Build Message with blocks created above msg := slack.NewBlockMessage( headerSection, divSection, schedeuleSection, conflictSection, divSection, proposeSection, optionOneSection, optionTwoSection, optionThreeSection, showMoreSection, ) b, err := json.MarshalIndent(msg, "", " ") if err != nil { fmt.Println(err) return } fmt.Println(string(b)) } // exampleFour profiles a poll example block func exampleFour() { // Shared Assets for example divSection := slack.NewDividerBlock() voteBtnText := slack.NewTextBlockObject("plain_text", "Vote", true, false) voteBtnEle := slack.NewButtonBlockElement("", "click_me_123", voteBtnText) profileOne := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", "Michael Scott") profileTwo := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", "Dwight Schrute") profileThree := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_3.png", "Pam Beasely") profileFour := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_4.png", "Angela") // Header Section headerText := slack.NewTextBlockObject("mrkdwn", "*Where should we order lunch from?* Poll by ", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) // Option One Info optOneText := slack.NewTextBlockObject("mrkdwn", ":sushi: *Ace Wasabi Rock-n-Roll Sushi Bar*\nThe best landlocked sushi restaurant.", false, false) optOneSection := slack.NewSectionBlock(optOneText, nil, slack.NewAccessory(voteBtnEle)) // Option One Votes optOneVoteText := slack.NewTextBlockObject("plain_text", "3 votes", true, false) optOneContext := slack.NewContextBlock("", []slack.MixedElement{profileOne, profileTwo, profileThree, optOneVoteText}...) // Option Two Info optTwoText := slack.NewTextBlockObject("mrkdwn", ":hamburger: *Super Hungryman Hamburgers*\nOnly for the hungriest of the hungry.", false, false) optTwoSection := slack.NewSectionBlock(optTwoText, nil, slack.NewAccessory(voteBtnEle)) // Option Two Votes optTwoVoteText := slack.NewTextBlockObject("plain_text", "2 votes", true, false) optTwoContext := slack.NewContextBlock("", []slack.MixedElement{profileFour, profileTwo, optTwoVoteText}...) // Option Three Info optThreeText := slack.NewTextBlockObject("mrkdwn", ":ramen: *Kagawa-Ya Udon Noodle Shop*\nDo you like to shop for noodles? We have noodles.", false, false) optThreeSection := slack.NewSectionBlock(optThreeText, nil, slack.NewAccessory(voteBtnEle)) // Option Three Votes optThreeVoteText := slack.NewTextBlockObject("plain_text", "No votes", true, false) optThreeContext := slack.NewContextBlock("", []slack.MixedElement{optThreeVoteText}...) // Suggestions Action btnTxt := slack.NewTextBlockObject("plain_text", "Add a suggestion", false, false) nextBtn := slack.NewButtonBlockElement("", "click_me_123", btnTxt) actionBlock := slack.NewActionBlock("", nextBtn) // Build Message with blocks created above msg := slack.NewBlockMessage( headerSection, divSection, optOneSection, optOneContext, optTwoSection, optTwoContext, optThreeSection, optThreeContext, divSection, actionBlock, ) b, err := json.MarshalIndent(msg, "", " ") if err != nil { fmt.Println(err) return } fmt.Println(string(b)) } func exampleFive() { // Build Header Section Block, includes text and overflow menu headerText := slack.NewTextBlockObject("mrkdwn", "We found *205 Hotels* in New Orleans, LA from *12/14 to 12/17*", false, false) // Build Text Objects associated with each option overflowOptionTextOne := slack.NewTextBlockObject("plain_text", "Option One", false, false) overflowOptionTextTwo := slack.NewTextBlockObject("plain_text", "Option Two", false, false) overflowOptionTextThree := slack.NewTextBlockObject("plain_text", "Option Three", false, false) // Build each option, providing a value for the option overflowOptionOne := slack.NewOptionBlockObject("value-0", overflowOptionTextOne, nil) overflowOptionTwo := slack.NewOptionBlockObject("value-1", overflowOptionTextTwo, nil) overflowOptionThree := slack.NewOptionBlockObject("value-2", overflowOptionTextThree, nil) // Build overflow section overflow := slack.NewOverflowBlockElement("", overflowOptionOne, overflowOptionTwo, overflowOptionThree) // Create the header section headerSection := slack.NewSectionBlock(headerText, nil, slack.NewAccessory(overflow)) // Shared Divider divSection := slack.NewDividerBlock() // Shared Objects locationPinImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgentLocationMarker.png", "Location Pin Icon") // First Hotel Listing hotelOneInfo := slack.NewTextBlockObject("mrkdwn", "**\n★★★★★\n$340 per night\nRated: 9.4 - Excellent", false, false) hotelOneImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgent_1.png", "Windsor Court Hotel thumbnail") hotelOneLoc := slack.NewTextBlockObject("plain_text", "Location: Central Business District", true, false) hotelOneSection := slack.NewSectionBlock(hotelOneInfo, nil, slack.NewAccessory(hotelOneImage)) hotelOneContext := slack.NewContextBlock("", []slack.MixedElement{locationPinImage, hotelOneLoc}...) // Second Hotel Listing hotelTwoInfo := slack.NewTextBlockObject("mrkdwn", "**\n★★★★★\n$340 per night\nRated: 9.1 - Excellent", false, false) hotelTwoImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgent_2.png", "Ritz-Carlton New Orleans thumbnail") hotelTwoLoc := slack.NewTextBlockObject("plain_text", "Location: French Quarter", true, false) hotelTwoSection := slack.NewSectionBlock(hotelTwoInfo, nil, slack.NewAccessory(hotelTwoImage)) hotelTwoContext := slack.NewContextBlock("", []slack.MixedElement{locationPinImage, hotelTwoLoc}...) // Third Hotel Listing hotelThreeInfo := slack.NewTextBlockObject("mrkdwn", "**\n★★★★★\n$419 per night\nRated: 8.8 - Excellent", false, false) hotelThreeImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/tripAgent_3.png", "https://api.slack.com/img/blocks/bkb_template_images/tripAgent_3.png") hotelThreeLoc := slack.NewTextBlockObject("plain_text", "Location: French Quarter", true, false) hotelThreeSection := slack.NewSectionBlock(hotelThreeInfo, nil, slack.NewAccessory(hotelThreeImage)) hotelThreeContext := slack.NewContextBlock("", []slack.MixedElement{locationPinImage, hotelThreeLoc}...) // Action button btnTxt := slack.NewTextBlockObject("plain_text", "Next 2 Results", false, false) nextBtn := slack.NewButtonBlockElement("", "click_me_123", btnTxt) actionBlock := slack.NewActionBlock("", nextBtn) // Build Message with blocks created above msg := slack.NewBlockMessage( headerSection, divSection, hotelOneSection, hotelOneContext, divSection, hotelTwoSection, hotelTwoContext, divSection, hotelThreeSection, hotelThreeContext, divSection, actionBlock, ) b, err := json.MarshalIndent(msg, "", " ") if err != nil { fmt.Println(err) return } fmt.Println(string(b)) } func exampleSix() { // Shared Assets for example divSection := slack.NewDividerBlock() // Shared Available Options manageTxt := slack.NewTextBlockObject("plain_text", "Manage", true, false) editTxt := slack.NewTextBlockObject("plain_text", "Edit it", false, false) readTxt := slack.NewTextBlockObject("plain_text", "Read it", false, false) saveTxt := slack.NewTextBlockObject("plain_text", "Save it", false, false) editOpt := slack.NewOptionBlockObject("value-0", editTxt, nil) readOpt := slack.NewOptionBlockObject("value-1", readTxt, nil) saveOpt := slack.NewOptionBlockObject("value-2", saveTxt, nil) availableOption := slack.NewOptionsSelectBlockElement("static_select", manageTxt, "", editOpt, readOpt, saveOpt) // Header Section headerText := slack.NewTextBlockObject("mrkdwn", ":mag: Search results for *Cata*", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) // Result One resultOneTxt := slack.NewTextBlockObject("mrkdwn", "**\nUse Case Catalogue for the following departments/roles...", false, false) resultOneSection := slack.NewSectionBlock(resultOneTxt, nil, slack.NewAccessory(availableOption)) // Result Two resultTwoTxt := slack.NewTextBlockObject("mrkdwn", "**\nThis resource was put together by members of...", false, false) resultTwoSection := slack.NewSectionBlock(resultTwoTxt, nil, slack.NewAccessory(availableOption)) // Result Three resultThreeTxt := slack.NewTextBlockObject("mrkdwn", "**\nSee the learning and development options we...", false, false) resultThreeSection := slack.NewSectionBlock(resultThreeTxt, nil, slack.NewAccessory(availableOption)) // Result Four resultFourTxt := slack.NewTextBlockObject("mrkdwn", "**\nThis is presentation will continue to be updated as...", false, false) resultFourSection := slack.NewSectionBlock(resultFourTxt, nil, slack.NewAccessory(availableOption)) // Result Five resultFiveTxt := slack.NewTextBlockObject("mrkdwn", "**\nInformation about all the benfits we offer is...", false, false) resultFiveSection := slack.NewSectionBlock(resultFiveTxt, nil, slack.NewAccessory(availableOption)) // Next Results Button // Suggestions Action btnTxt := slack.NewTextBlockObject("plain_text", "Next 5 Results", false, false) nextBtn := slack.NewButtonBlockElement("", "click_me_123", btnTxt) actionBlock := slack.NewActionBlock("", nextBtn) // Build Message with blocks created above msg := slack.NewBlockMessage( headerSection, divSection, resultOneSection, resultTwoSection, resultThreeSection, resultFourSection, resultFiveSection, divSection, actionBlock, ) b, err := json.MarshalIndent(msg, "", " ") if err != nil { fmt.Println(err) return } fmt.Println(string(b)) } func unmarshalExample() { var msgBlocks []slack.Block // Append ActionBlock for marshalling btnTxt := slack.NewTextBlockObject("plain_text", "Add a suggestion", false, false) nextBtn := slack.NewButtonBlockElement("", "click_me_123", btnTxt) approveBtnTxt := slack.NewTextBlockObject("plain_text", "Approve", false, false) approveBtn := slack.NewButtonBlockElement("", "click_me_123", approveBtnTxt) msgBlocks = append(msgBlocks, slack.NewActionBlock("", nextBtn, approveBtn)) // Append ContextBlock for marshalling profileOne := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_1.png", "Michael Scott") profileTwo := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", "Dwight Schrute") textBlockObj := slack.NewTextBlockObject("mrkdwn", "**\n★★★★★\n$419 per night\nRated: 8.8 - Excellent", false, false) msgBlocks = append(msgBlocks, slack.NewContextBlock("", []slack.MixedElement{profileOne, profileTwo, textBlockObj}...)) // Append ImageBlock for marshalling msgBlocks = append(msgBlocks, slack.NewImageBlock("https://api.slack.com/img/blocks/bkb_template_images/profile_2.png", "some profile", "image-block", textBlockObj)) // Append DividerBlock for marshalling msgBlocks = append(msgBlocks, slack.NewDividerBlock()) // Append SectionBlock for marshalling approvalText := slack.NewTextBlockObject("mrkdwn", "*Type:*\nPaid time off\n*When:*\nAug 10-Aug 13\n*Hours:* 16.0 (2 days)\n*Remaining balance:* 32.0 hours (4 days)\n*Comments:* \"Family in town, going camping!\"", false, false) approvalImage := slack.NewImageBlockElement("https://api.slack.com/img/blocks/bkb_template_images/approvalsNewDevice.png", "computer thumbnail") msgBlocks = append(msgBlocks, slack.NewSectionBlock(approvalText, nil, slack.NewAccessory(approvalImage)), nil) // Build Message with blocks created above msg := slack.NewBlockMessage(msgBlocks...) b, err := json.Marshal(&msg) if err != nil { fmt.Println(err) return } fmt.Println(string(b)) // Unmarshal message m := slack.Message{} if err := json.Unmarshal(b, &m); err != nil { fmt.Println(err) return } var respBlocks []slack.Block for _, block := range m.Blocks.BlockSet { // Need to implement a type switch to determine Block type since the // response from Slack could include any/all types under "blocks" key switch block.BlockType() { case slack.MBTContext: var respMixedElements []slack.MixedElement contextElements := block.(*slack.ContextBlock).ContextElements.Elements // Need to implement a type switch for ContextElements for same reason as Blocks for _, elem := range contextElements { switch elem.MixedElementType() { case slack.MixedElementImage: // Assert the block's type to manipulate/extract values imageBlockElem := elem.(*slack.ImageBlockElement) imageBlockElem.ImageURL = "https://api.slack.com/img/blocks/bkb_template_images/profile_1.png" imageBlockElem.AltText = "MichaelScott" respMixedElements = append(respMixedElements, imageBlockElem) case slack.MixedElementText: textBlockElem := elem.(*slack.TextBlockObject) textBlockElem.Text = "go go go go go" respMixedElements = append(respMixedElements, textBlockElem) } } respBlocks = append(respBlocks, slack.NewContextBlock("new block", respMixedElements...)) case slack.MBTAction: actionBlock := block.(*slack.ActionBlock) // Need to implement a type switch for BlockElements for same reason as Blocks for _, elem := range actionBlock.Elements.ElementSet { switch elem.ElementType() { case slack.METImage: imageElem := elem.(*slack.ImageBlockElement) fmt.Printf("do something with image block element: %v\n", imageElem) case slack.METButton: buttonElem := elem.(*slack.ButtonBlockElement) fmt.Printf("do something with button block element: %v\n", buttonElem) case slack.METOverflow: overflowElem := elem.(*slack.OverflowBlockElement) fmt.Printf("do something with overflow block element: %v\n", overflowElem) case slack.METDatepicker: datepickerElem := elem.(*slack.DatePickerBlockElement) fmt.Printf("do something with datepicker block element: %v\n", datepickerElem) case slack.METTimepicker: timepickerElem := elem.(*slack.TimePickerBlockElement) fmt.Printf("do something with timepicker block element: %v\n", timepickerElem) } } respBlocks = append(respBlocks, block) case slack.MBTImage: // Simply re-append the block if you want to include it in the response respBlocks = append(respBlocks, block) case slack.MBTSection: respBlocks = append(respBlocks, block) case slack.MBTDivider: respBlocks = append(respBlocks, block) } } // Build new Message with Blocks obtained/edited from callback respMsg := slack.NewBlockMessage(respBlocks...) b, err = json.Marshal(&respMsg) if err != nil { fmt.Println(err) return } fmt.Println(string(b)) } slack-0.11.3/examples/buttons/000077500000000000000000000000001430741033100162015ustar00rootroot00000000000000slack-0.11.3/examples/buttons/buttons.go000066400000000000000000000032051430741033100202260ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "net/http" "os" "github.com/slack-go/slack" ) func main() { var token, channel string var ok bool token, ok = os.LookupEnv("SLACK_TOKEN") if !ok { fmt.Println("Missing SLACK_TOKEN in environment") os.Exit(1) } channel, ok = os.LookupEnv("SLACK_CHANNEL") if !ok { fmt.Println("Missing SLACK_CHANNEL in environment") os.Exit(1) } api := slack.New(token) attachment := slack.Attachment{ Pretext: "pretext", Fallback: "We don't currently support your client", CallbackID: "accept_or_reject", Color: "#3AA3E3", Actions: []slack.AttachmentAction{ slack.AttachmentAction{ Name: "accept", Text: "Accept", Type: "button", Value: "accept", }, slack.AttachmentAction{ Name: "reject", Text: "Reject", Type: "button", Value: "reject", Style: "danger", }, }, } message := slack.MsgOptionAttachments(attachment) channelID, timestamp, err := api.PostMessage(channel, slack.MsgOptionText("", false), message) if err != nil { fmt.Printf("Could not send message: %v", err) } fmt.Printf("Message with buttons sucessfully sent to channel %s at %s", channelID, timestamp) http.HandleFunc("/actions", actionHandler) http.ListenAndServe(":3000", nil) } func actionHandler(w http.ResponseWriter, r *http.Request) { var payload slack.InteractionCallback err := json.Unmarshal([]byte(r.FormValue("payload")), &payload) if err != nil { fmt.Printf("Could not parse action response JSON: %v", err) } fmt.Printf("Message button pressed by user %s with value %s", payload.User.Name, payload.ActionCallback.AttachmentActions[0].Value) } slack-0.11.3/examples/connparams/000077500000000000000000000000001430741033100166445ustar00rootroot00000000000000slack-0.11.3/examples/connparams/connparams.go000066400000000000000000000027541430741033100213440ustar00rootroot00000000000000package main import ( "fmt" "log" "net/url" "os" "github.com/slack-go/slack" ) func main() { token, ok := os.LookupEnv("SLACK_TOKEN") if !ok { fmt.Println("Missing SLACK_TOKEN in environment") os.Exit(1) } api := slack.New( token, slack.OptionDebug(true), slack.OptionLog(log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)), ) // turn on the batch_presence_aware option rtm := api.NewRTM(slack.RTMOptionConnParams(url.Values{ "batch_presence_aware": {"1"}, })) go rtm.ManageConnection() for msg := range rtm.IncomingEvents { fmt.Print("Event Received: ") switch ev := msg.Data.(type) { case *slack.HelloEvent: // Replace USER-ID-N here with your User IDs rtm.SendMessage(rtm.NewSubscribeUserPresence([]string{ "USER-ID-1", "USER-ID-2", })) case *slack.ConnectedEvent: fmt.Println("Infos:", ev.Info) fmt.Println("Connection counter:", ev.ConnectionCount) // Replace C2147483705 with your Channel ID rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "C2147483705")) case *slack.MessageEvent: fmt.Printf("Message: %v\n", ev) case *slack.PresenceChangeEvent: fmt.Printf("Presence Change: %v\n", ev) case *slack.LatencyReport: fmt.Printf("Current latency: %v\n", ev.Value) case *slack.RTMError: fmt.Printf("Error: %s\n", ev.Error()) case *slack.InvalidAuthEvent: fmt.Printf("Invalid credentials") return default: // Ignore other events.. // fmt.Printf("Unexpected: %v\n", msg.Data) } } } slack-0.11.3/examples/dialog/000077500000000000000000000000001430741033100157425ustar00rootroot00000000000000slack-0.11.3/examples/dialog/dialog.go000066400000000000000000000050721430741033100175340ustar00rootroot00000000000000package main import ( "encoding/json" "io/ioutil" "log" "net/http" "net/url" "strings" "github.com/slack-go/slack" ) var api = slack.New("YOUR_TOKEN") var signingSecret = "YOUR_SIGNING_SECRET" // You can open a dialog with a user interaction. (like pushing buttons, slash commands ...) // https://api.slack.com/surfaces/modals // https://api.slack.com/interactivity/entry-points func main() { http.HandleFunc("/", handler) http.ListenAndServe(":3000", nil) } func handler(w http.ResponseWriter, r *http.Request) { // Read request body defer r.Body.Close() body, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) log.Printf("[ERROR] Fail to read request body: %v", err) return } // Verify signing secret sv, err := slack.NewSecretsVerifier(r.Header, signingSecret) if err != nil { w.WriteHeader(http.StatusUnauthorized) log.Printf("[ERROR] Fail to verify SigningSecret: %v", err) return } sv.Write(body) if err := sv.Ensure(); err != nil { w.WriteHeader(http.StatusUnauthorized) log.Printf("[ERROR] Fail to verify SigningSecret: %v", err) return } // Parse request body str, _ := url.QueryUnescape(string(body)) str = strings.Replace(str, "payload=", "", 1) var message slack.InteractionCallback if err := json.Unmarshal([]byte(str), &message); err != nil { log.Printf("[ERROR] Fail to unmarshal json: %v", err) return } switch message.Type { case slack.InteractionTypeInteractionMessage: // Make new dialog components and open a dialog. // Component-Text textInput := slack.NewTextInput("TextSample", "Sample label - Text", "Default value") // Component-TextArea textareaInput := slack.NewTextAreaInput("TexaAreaSample", "Sample label - TextArea", "Default value") // Component-Select menu option1 := slack.DialogSelectOption{ Label: "Display name 1", Value: "Inner value 1", } option2 := slack.DialogSelectOption{ Label: "Display name 2", Value: "Inner value 2", } options := []slack.DialogSelectOption{option1, option2} selectInput := slack.NewStaticSelectDialogInput("SelectSample", "Sample label - Select", options) // Open a dialog elements := []slack.DialogElement{ textInput, textareaInput, selectInput, } dialog := slack.Dialog{ CallbackID: "Callback_ID", Title: "Dialog title", SubmitLabel: "Submit", Elements: elements, } api.OpenDialog(message.TriggerID, dialog) case slack.InteractionTypeDialogSubmission: // Receive a notification of a dialog submission log.Printf("Successfully receive a dialog submission.") } } slack-0.11.3/examples/eventsapi/000077500000000000000000000000001430741033100165015ustar00rootroot00000000000000slack-0.11.3/examples/eventsapi/events.go000066400000000000000000000032501430741033100203340ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "io/ioutil" "net/http" "os" "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" ) // You more than likely want your "Bot User OAuth Access Token" which starts with "xoxb-" var api = slack.New("TOKEN") func main() { signingSecret := os.Getenv("SLACK_SIGNING_SECRET") http.HandleFunc("/events-endpoint", func(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) return } sv, err := slack.NewSecretsVerifier(r.Header, signingSecret) if err != nil { w.WriteHeader(http.StatusBadRequest) return } if _, err := sv.Write(body); err != nil { w.WriteHeader(http.StatusInternalServerError) return } if err := sv.Ensure(); err != nil { w.WriteHeader(http.StatusUnauthorized) return } eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken()) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if eventsAPIEvent.Type == slackevents.URLVerification { var r *slackevents.ChallengeResponse err := json.Unmarshal([]byte(body), &r) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text") w.Write([]byte(r.Challenge)) } if eventsAPIEvent.Type == slackevents.CallbackEvent { innerEvent := eventsAPIEvent.InnerEvent switch ev := innerEvent.Data.(type) { case *slackevents.AppMentionEvent: api.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false)) } } }) fmt.Println("[INFO] Server listening") http.ListenAndServe(":3000", nil) } slack-0.11.3/examples/files/000077500000000000000000000000001430741033100156055ustar00rootroot00000000000000slack-0.11.3/examples/files/example.txt000066400000000000000000000000471430741033100200020ustar00rootroot00000000000000Nan Nan Nan Nan Nan Nan Nan Nan Batman slack-0.11.3/examples/files/files.go000066400000000000000000000011021430741033100172300ustar00rootroot00000000000000package main import ( "fmt" "github.com/slack-go/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") params := slack.FileUploadParameters{ Title: "Batman Example", //Filetype: "txt", File: "example.txt", //Content: "Nan Nan Nan Nan Nan Nan Nan Nan Batman", } file, err := api.UploadFile(params) if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("Name: %s, URL: %s\n", file.Name, file.URL) err = api.DeleteFile(file.ID) if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("File %s deleted successfully.\n", file.Name) } slack-0.11.3/examples/messages/000077500000000000000000000000001430741033100163125ustar00rootroot00000000000000slack-0.11.3/examples/messages/messages.go000066400000000000000000000014711430741033100204530ustar00rootroot00000000000000package main import ( "fmt" "github.com/slack-go/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") attachment := slack.Attachment{ Pretext: "some pretext", Text: "some text", // Uncomment the following part to send a field too /* Fields: []slack.AttachmentField{ slack.AttachmentField{ Title: "a", Value: "no", }, }, */ } channelID, timestamp, err := api.PostMessage( "CHANNEL_ID", slack.MsgOptionText("Some text", false), slack.MsgOptionAttachments(attachment), slack.MsgOptionAsUser(true), // Add this if you want that the bot would post message as a user, otherwise it will send response using the default slackbot ) if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("Message successfully sent to channel %s at %s", channelID, timestamp) } slack-0.11.3/examples/modal/000077500000000000000000000000001430741033100155775ustar00rootroot00000000000000slack-0.11.3/examples/modal/modal.go000066400000000000000000000121061430741033100172220ustar00rootroot00000000000000// Modal example - How to respond to a slash command with an interactive modal and parse the response // The flow of this example: // 1. User trigers your app with a slash command (e.g. /modaltest) that will send a request to http://URL/slash and respond with a request to open a modal // 2. User fills out fields first and last name in modal and hits submit // 3. This will send a request to http://URL/modal and send a greeting message to the user // Note: Within your slack app you will need to enable and provide a URL for "Interactivity & Shortcuts" and "Slash Commands" // Note: Be sure to update YOUR_SIGNING_SECRET_HERE and YOUR_TOKEN_HERE // You can use ngrok to test this example: https://api.slack.com/tutorials/tunneling-with-ngrok // Helpful slack documentation to learn more: https://api.slack.com/interactivity/handling package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "net/http" "github.com/slack-go/slack" ) func generateModalRequest() slack.ModalViewRequest { // Create a ModalViewRequest with a header and two inputs titleText := slack.NewTextBlockObject("plain_text", "My App", false, false) closeText := slack.NewTextBlockObject("plain_text", "Close", false, false) submitText := slack.NewTextBlockObject("plain_text", "Submit", false, false) headerText := slack.NewTextBlockObject("mrkdwn", "Please enter your name", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) firstNameText := slack.NewTextBlockObject("plain_text", "First Name", false, false) firstNameHint := slack.NewTextBlockObject("plain_text", "First Name Hint", false, false) firstNamePlaceholder := slack.NewTextBlockObject("plain_text", "Enter your first name", false, false) firstNameElement := slack.NewPlainTextInputBlockElement(firstNamePlaceholder, "firstName") // Notice that blockID is a unique identifier for a block firstName := slack.NewInputBlock("First Name", firstNameText, firstNameHint, firstNameElement) lastNameText := slack.NewTextBlockObject("plain_text", "Last Name", false, false) lastNameHint := slack.NewTextBlockObject("plain_text", "Last Name Hint", false, false) lastNamePlaceholder := slack.NewTextBlockObject("plain_text", "Enter your first name", false, false) lastNameElement := slack.NewPlainTextInputBlockElement(lastNamePlaceholder, "lastName") lastName := slack.NewInputBlock("Last Name", lastNameText, lastNameHint, lastNameElement) blocks := slack.Blocks{ BlockSet: []slack.Block{ headerSection, firstName, lastName, }, } var modalRequest slack.ModalViewRequest modalRequest.Type = slack.ViewType("modal") modalRequest.Title = titleText modalRequest.Close = closeText modalRequest.Submit = submitText modalRequest.Blocks = blocks return modalRequest } // This was taken from the slash example // https://github.com/slack-go/slack/blob/master/examples/slash/slash.go func verifySigningSecret(r *http.Request) error { signingSecret := "YOUR_SIGNING_SECRET_HERE" verifier, err := slack.NewSecretsVerifier(r.Header, signingSecret) if err != nil { fmt.Println(err.Error()) return err } body, err := ioutil.ReadAll(r.Body) if err != nil { fmt.Println(err.Error()) return err } // Need to use r.Body again when unmarshalling SlashCommand and InteractionCallback r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) verifier.Write(body) if err = verifier.Ensure(); err != nil { fmt.Println(err.Error()) return err } return nil } func handleSlash(w http.ResponseWriter, r *http.Request) { err := verifySigningSecret(r) if err != nil { fmt.Printf(err.Error()) w.WriteHeader(http.StatusUnauthorized) return } s, err := slack.SlashCommandParse(r) if err != nil { w.WriteHeader(http.StatusInternalServerError) fmt.Println(err.Error()) return } switch s.Command { case "/humboldttest": api := slack.New("YOUR_TOKEN_HERE") modalRequest := generateModalRequest() _, err = api.OpenView(s.TriggerID, modalRequest) if err != nil { fmt.Printf("Error opening view: %s", err) } default: w.WriteHeader(http.StatusInternalServerError) return } } func handleModal(w http.ResponseWriter, r *http.Request) { err := verifySigningSecret(r) if err != nil { fmt.Printf(err.Error()) w.WriteHeader(http.StatusUnauthorized) return } var i slack.InteractionCallback err = json.Unmarshal([]byte(r.FormValue("payload")), &i) if err != nil { fmt.Printf(err.Error()) w.WriteHeader(http.StatusUnauthorized) return } // Note there might be a better way to get this info, but I figured this structure out from looking at the json response firstName := i.View.State.Values["First Name"]["firstName"].Value lastName := i.View.State.Values["Last Name"]["lastName"].Value msg := fmt.Sprintf("Hello %s %s, nice to meet you!", firstName, lastName) api := slack.New("YOUR_TOKEN_HERE") _, _, err = api.PostMessage(i.User.ID, slack.MsgOptionText(msg, false), slack.MsgOptionAttachments()) if err != nil { fmt.Printf(err.Error()) w.WriteHeader(http.StatusUnauthorized) return } } func main() { http.HandleFunc("/slash", handleSlash) http.HandleFunc("/modal", handleModal) http.ListenAndServe(":4390", nil) } slack-0.11.3/examples/modal_users/000077500000000000000000000000001430741033100170205ustar00rootroot00000000000000slack-0.11.3/examples/modal_users/users.go000066400000000000000000000131541430741033100205140ustar00rootroot00000000000000package main import ( "fmt" "github.com/slack-go/slack" ) // An example how to open a modal with different kinds of input fields func main() { // Create a ModalViewRequest with a header and two inputs titleText := slack.NewTextBlockObject(slack.PlainTextType, "Create channel demo", false, false) closeText := slack.NewTextBlockObject(slack.PlainTextType, "Close", false, false) submitText := slack.NewTextBlockObject(slack.PlainTextType, "Submit", false, false) contextText := slack.NewTextBlockObject(slack.MarkdownType, "This app demonstrates the use of different fields", false, false) contextBlock := slack.NewContextBlock("context", contextText) // Only the inputs in input blocks will be included in view_submission’s view.state.values: https://slack.dev/java-slack-sdk/guides/modals // This means the inputs will not be interactive either because they do not trigger block_actions messages: https://api.slack.com/surfaces/modals/using#interactions channelNameText := slack.NewTextBlockObject(slack.PlainTextType, "Channel Name", false, false) channelNameHint := slack.NewTextBlockObject(slack.PlainTextType, "Channel names may only contain lowercase letters, numbers, hyphens, and underscores, and must be 80 characters or less", false, false) channelPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "New channel name", false, false) channelNameElement := slack.NewPlainTextInputBlockElement(channelPlaceholder, "channel_name") // Slack channel names can be maximum 80 characters: https://api.slack.com/methods/conversations.create channelNameElement.MaxLength = 80 channelNameBlock := slack.NewInputBlock("channel_name", channelNameText, channelNameHint, channelNameElement) // Provide a static list of users to choose from, those provided now are just made up user IDs // Get user IDs by right clicking on them in Slack, select "Copy link", and inspect the last part of the link // The user ID should start with "U" followed by 8 random characters memberOptions := createOptionBlockObjects([]string{"U9911MMAA", "U2233KKNN", "U00112233"}, true) inviteeText := slack.NewTextBlockObject(slack.PlainTextType, "Invitee from static list", false, false) inviteeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeStatic, nil, "invitee", memberOptions...) inviteeBlock := slack.NewInputBlock("invitee", inviteeText, nil, inviteeOption) // Section with users select - this input will not be included in the view_submission's view.state.values, // but instead be sent as a "block_actions" request additionalInviteeText := slack.NewTextBlockObject(slack.PlainTextType, "Invitee from complete list of users", false, false) additionalInviteeHintText := slack.NewTextBlockObject(slack.PlainTextType, "", false, false) additionalInviteeOption := slack.NewOptionsSelectBlockElement(slack.OptTypeUser, additionalInviteeText, "user") additionalInviteeSection := slack.NewSectionBlock(additionalInviteeText, nil, slack.NewAccessory(additionalInviteeOption)) // Input with users select - this input will be included in the view_submission's view.state.values // It can be fetched as for example "payload.View.State.Values["user"]["user"].SelectedUser" additionalInviteeBlock := slack.NewInputBlock("user", additionalInviteeText, additionalInviteeHintText, additionalInviteeOption) checkboxTxt := slack.NewTextBlockObject(slack.PlainTextType, "Checkbox", false, false) checkboxOptions := createOptionBlockObjects([]string{"option 1", "option 2", "option 3"}, false) checkboxOptionsBlock := slack.NewCheckboxGroupsBlockElement("chkbox", checkboxOptions...) checkboxBlock := slack.NewInputBlock("chkbox", checkboxTxt, nil, checkboxOptionsBlock) summaryText := slack.NewTextBlockObject(slack.PlainTextType, "Summary", false, false) summaryHint := slack.NewTextBlockObject(slack.PlainTextType, "Summary Hint", false, false) summaryPlaceholder := slack.NewTextBlockObject(slack.PlainTextType, "Summary of reason for creating channel", false, false) summaryElement := slack.NewPlainTextInputBlockElement(summaryPlaceholder, "summary") // Just set an arbitrary max length to avoid too prose summary summaryElement.MaxLength = 200 summaryElement.Multiline = true summaryBlock := slack.NewInputBlock("summary", summaryText, summaryHint, summaryElement) blocks := slack.Blocks{ BlockSet: []slack.Block{ contextBlock, channelNameBlock, inviteeBlock, additionalInviteeSection, additionalInviteeBlock, checkboxBlock, summaryBlock, }, } var modalRequest slack.ModalViewRequest modalRequest.Type = slack.ViewType("modal") modalRequest.Title = titleText modalRequest.Close = closeText modalRequest.Submit = submitText modalRequest.Blocks = blocks modalRequest.CallbackID = "create_channel" api := slack.New("YOUR_BOT_TOKEN_HERE") // Using a trigger ID you can open a modal // The trigger ID is provided through certain events and interactions // More information can be found here: https://api.slack.com/interactivity/handling#modal_responses _, err := api.OpenView("YOUR_TRIGGERID_HERE", modalRequest) if err != nil { fmt.Printf("Error opening view: %s", err) } } // createOptionBlockObjects - utility function for generating option block objects func createOptionBlockObjects(options []string, users bool) []*slack.OptionBlockObject { optionBlockObjects := make([]*slack.OptionBlockObject, 0, len(options)) var text string for _, o := range options { if users { text = fmt.Sprintf("<@%s>", o) } else { text = o } optionText := slack.NewTextBlockObject(slack.PlainTextType, text, false, false) optionBlockObjects = append(optionBlockObjects, slack.NewOptionBlockObject(o, optionText, nil)) } return optionBlockObjects } slack-0.11.3/examples/pins/000077500000000000000000000000001430741033100154545ustar00rootroot00000000000000slack-0.11.3/examples/pins/pins.go000066400000000000000000000054711430741033100167630ustar00rootroot00000000000000package main import ( "flag" "fmt" "github.com/slack-go/slack" ) /* WARNING: This example is destructive in the sense that it create a channel called testpinning */ func main() { var ( apiToken string debug bool ) flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token") flag.BoolVar(&debug, "debug", false, "Show JSON output") flag.Parse() api := slack.New(apiToken, slack.OptionDebug(debug)) var ( postAsUserName string postAsUserID string postToChannelID string channels []slack.Channel ) // Find the user to post as. authTest, err := api.AuthTest() if err != nil { fmt.Printf("Error getting channels: %s\n", err) return } channelName := "testpinning" // Post as the authenticated user. postAsUserName = authTest.User postAsUserID = authTest.UserID // Create a temporary channel channel, err := api.CreateConversation(channelName, false) if err != nil { // If the channel exists, that means we just need to unarchive it if err.Error() == "name_taken" { err = nil params := &slack.GetConversationsParameters{ExcludeArchived: false} if channels, _, err = api.GetConversations(params); err != nil { fmt.Println("Could not retrieve channels") return } for _, archivedChannel := range channels { if archivedChannel.Name == channelName { if archivedChannel.IsArchived { err = api.UnArchiveConversation(archivedChannel.ID) if err != nil { fmt.Printf("Could not unarchive %s: %s\n", archivedChannel.ID, err) return } } channel = &archivedChannel break } } } if err != nil { fmt.Printf("Error setting test channel for pinning: %s\n", err) return } } postToChannelID = channel.ID fmt.Printf("Posting as %s (%s) in channel %s\n", postAsUserName, postAsUserID, postToChannelID) // Post a message. channelID, timestamp, err := api.PostMessage(postToChannelID, slack.MsgOptionText("Is this any good?", false)) if err != nil { fmt.Printf("Error posting message: %s\n", err) return } // Grab a reference to the message. msgRef := slack.NewRefToMessage(channelID, timestamp) // Add message pin to channel if err = api.AddPin(channelID, msgRef); err != nil { fmt.Printf("Error adding pin: %s\n", err) return } // List all of the users pins. listPins, _, err := api.ListPins(channelID) if err != nil { fmt.Printf("Error listing pins: %s\n", err) return } fmt.Printf("\n") fmt.Printf("All pins by %s...\n", authTest.User) for _, item := range listPins { fmt.Printf(" > Item type: %s\n", item.Type) } // Remove the pin. err = api.RemovePin(channelID, msgRef) if err != nil { fmt.Printf("Error remove pin: %s\n", err) return } if err = api.UnArchiveConversation(channelID); err != nil { fmt.Printf("Error archiving channel: %s\n", err) return } } slack-0.11.3/examples/reactions/000077500000000000000000000000001430741033100164725ustar00rootroot00000000000000slack-0.11.3/examples/reactions/reactions.go000066400000000000000000000061741430741033100210200ustar00rootroot00000000000000package main import ( "flag" "fmt" "github.com/slack-go/slack" ) func main() { var ( apiToken string debug bool ) flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token") flag.BoolVar(&debug, "debug", false, "Show JSON output") flag.Parse() api := slack.New(apiToken, slack.OptionDebug(debug)) var ( postAsUserName string postAsUserID string postToUserName string postToUserID string postToChannelID string ) // Find the user to post as. authTest, err := api.AuthTest() if err != nil { fmt.Printf("Error getting channels: %s\n", err) return } // Post as the authenticated user. postAsUserName = authTest.User postAsUserID = authTest.UserID // Posting to DM with self causes a conversation with slackbot. postToUserName = authTest.User postToUserID = authTest.UserID // Find the channel. channel, _, _, err := api.OpenConversation(&slack.OpenConversationParameters{ChannelID: postToUserID}) if err != nil { fmt.Printf("Error opening IM: %s\n", err) return } postToChannelID = channel.ID fmt.Printf("Posting as %s (%s) in DM with %s (%s), channel %s\n", postAsUserName, postAsUserID, postToUserName, postToUserID, postToChannelID) // Post a message. channelID, timestamp, err := api.PostMessage(postToChannelID, slack.MsgOptionText("Is this any good?", false)) if err != nil { fmt.Printf("Error posting message: %s\n", err) return } // Grab a reference to the message. msgRef := slack.NewRefToMessage(channelID, timestamp) // React with :+1: if err = api.AddReaction("+1", msgRef); err != nil { fmt.Printf("Error adding reaction: %s\n", err) return } // React with :-1: if err = api.AddReaction("cry", msgRef); err != nil { fmt.Printf("Error adding reaction: %s\n", err) return } // Get all reactions on the message. msgReactions, err := api.GetReactions(msgRef, slack.NewGetReactionsParameters()) if err != nil { fmt.Printf("Error getting reactions: %s\n", err) return } fmt.Printf("\n") fmt.Printf("%d reactions to message...\n", len(msgReactions)) for _, r := range msgReactions { fmt.Printf(" %d users say %s\n", r.Count, r.Name) } // List all of the users reactions. listReactions, _, err := api.ListReactions(slack.NewListReactionsParameters()) if err != nil { fmt.Printf("Error listing reactions: %s\n", err) return } fmt.Printf("\n") fmt.Printf("All reactions by %s...\n", authTest.User) for _, item := range listReactions { fmt.Printf("%d on a %s...\n", len(item.Reactions), item.Type) for _, r := range item.Reactions { fmt.Printf(" %s (along with %d others)\n", r.Name, r.Count-1) } } // Remove the :cry: reaction. err = api.RemoveReaction("cry", msgRef) if err != nil { fmt.Printf("Error remove reaction: %s\n", err) return } // Get all reactions on the message. msgReactions, err = api.GetReactions(msgRef, slack.NewGetReactionsParameters()) if err != nil { fmt.Printf("Error getting reactions: %s\n", err) return } fmt.Printf("\n") fmt.Printf("%d reactions to message after removing cry...\n", len(msgReactions)) for _, r := range msgReactions { fmt.Printf(" %d users say %s\n", r.Count, r.Name) } } slack-0.11.3/examples/remotefiles/000077500000000000000000000000001430741033100170215ustar00rootroot00000000000000slack-0.11.3/examples/remotefiles/remotefiles.go000066400000000000000000000037241430741033100216740ustar00rootroot00000000000000package main import ( "fmt" "os" "github.com/slack-go/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") r, err := os.Open("slack-go.png") if err != nil { fmt.Printf("%s\n", err) return } defer r.Close() remotefile, err := api.AddRemoteFile(slack.RemoteFileParameters{ ExternalID: "slack-go", ExternalURL: "https://github.com/slack-go/slack", Title: "slack-go", Filetype: "go", IndexableFileContents: "golang, slack", // PreviewImage: "slack-go.png", PreviewImageReader: r, }) if err != nil { fmt.Printf("add remote file failed: %s\n", err) return } fmt.Printf("remote file: %v\n", remotefile) _, err = api.ShareRemoteFile([]string{"CPB8DC1CM"}, remotefile.ExternalID, "") if err != nil { fmt.Printf("share remote file failed: %s\n", err) return } fmt.Printf("share remote file %s successfully.\n", remotefile.Name) remotefiles, err := api.ListRemoteFiles(slack.ListRemoteFilesParameters{ Channel: "YOUR_CHANNEL_HERE", }) if err != nil { fmt.Printf("list remote files failed: %s\n", err) return } fmt.Printf("remote files: %v\n", remotefiles) remotefile, err = api.UpdateRemoteFile(remotefile.ID, slack.RemoteFileParameters{ ExternalID: "slack-go", ExternalURL: "https://github.com/slack-go/slack", Title: "slack-go", Filetype: "go", IndexableFileContents: "golang, slack, github", }) if err != nil { fmt.Printf("update remote file failed: %s\n", err) return } fmt.Printf("remote file: %v\n", remotefile) info, err := api.GetRemoteFileInfo(remotefile.ExternalID, "") if err != nil { fmt.Printf("get remote file info failed: %s\n", err) return } fmt.Printf("remote file info: %v\n", info) err = api.RemoveRemoteFile(remotefile.ExternalID, "") if err != nil { fmt.Printf("remove remote file failed: %s\n", err) return } fmt.Printf("remote file %s deleted successfully.\n", remotefile.Name) } slack-0.11.3/examples/remotefiles/slack-go.png000066400000000000000000004513411430741033100212370ustar00rootroot00000000000000‰PNG  IHDR<λû±¾ biCCPICC ProfileH‰•WXSW>wd’°öe@F+‚€LATBH1&7µTÁºEGE«"­V@ê@Ä:‹‚»Žâ@¥R‹U\¨ü'jí?žÿ{žïž7ßùö='÷ôºù2Yª@¡´HžÆšœžÁ"=:€ ô)ðà 2NBB,€4<þ^]ˆj¼ì¦òõÏùÿJ†B‘B’ q¶P!(„¸¼L “@ ‡rÛYE2Cl$‡ B_ž €n;”³Š¹ÐîCˆÝ¥B‰=#ˆƒb¾âdˆÇÎPá…;A}Ä»!fgâ3÷oþ³Güóù¹#XS—šÈá…¬€?ûÿlÍÿ¦Âåp È4±<:QU?ìáü1*Lƒ¸Oš¯ê5Äo$BMß@©betŠF5(¸°ð­Ô]ÈØâHiA\¬Vž#‰äA W Z")â%km—ˆIZŸ›ä3ã‡qŽœËÑÚ6ðåê¸*ýve~ Gëÿ†XÄöÿ²Tœœ1ŒZ,IƒXb#E~RŒF³)sã†uäÊDUþv³EÒ¨0,3G™¨Õ—*†ëÅÊÅ^œW‰“£5ýÁöøêüM nI9)Ã~DŠÉ±ÃµEášÚ±‘4E[/vWV–¨µí—$hõq²¨ J%·ØLQœ¤µÅÇÁÅ©ñÇÊŠ’5yâYyü š|ðb ¸ °€r6˜ò€¤£¯©þÒÌD>ƒ\ nZɰEšzF ŸI ü‘(FìÂÔ³"P åF¤š§ÈQÏ«-òÁ#ˆ A (€¿•j+éH´TðJ$ÿˆ.€¹@VÍýSÆ’X­D9ì—¥7¬IŒ †£‰‘DgÜ ÆñXø …쉳qÿálÿÒ'<"t 7§KÊäŸå2tCÿ‘ÚŠ³?­w€>}ð0<z‡žq&nÜpo‡ƒ‡ÀÈ>PÊÕæ­ªõoê©à“žkõ(î”2ŠJqúÜR×E×gÄ‹ª£ŸöG“köHW¹#3ŸÇç~Òg!c>×Ä–`±ÓØ ì,vk,ì8ÖŒ]ÀŽªðÈz¨^CÃÑÕùäC?’Äãkcª:©p¯wïu¯E¢’"ÕãÎÍ–KrÅE,ü ˆX<©`ì–§»§;ªoŠæoêSý­@˜çþ’•m (xhhèÈ_²˜öÃm~ç/™Ü»º]œY+PÊ‹52\õ À=¸£L%°N°"Oà A(ˆ@VUaµXÖßôe¬ëÃÞâDœ³p7¸†£ñ\€ÏÄçãËðøn¼oÇ/ã÷ð~ü#N0'¸<ÂdB.a¡œPEØI8D8wSá‘Hd‰~p7¦óˆsˆËˆ›‰ûˆ­ÄNââ‰D2%¹’‚Hñ$>©ˆTNÚ@ÚK:Nê"õÞuÈVdOr$9ƒ,%—‘«È{ÈÇÈ]äÇäAŠ>Åž@‰§)³)+(;(-”‹”Ê Õ€êH ¢&S󨋨ÕÔê)êmê I:…:Õ:ûuÎèÜÓyK3¤¹Ð¸´Lš’¶œ¶‹ÖJ»I{A§Óè¡ô z}9½Ž~’~—þF—¡;V—§+Ô] [£Û¨Û¥ûL¢g¯ÇÑ›¦WªW¥wPï¢^Ÿ>EßAŸ«Ïן¯_£Xÿºþ€ÃÀà ޠÐ`™Áƒ³O I†††BÃÅ†Û O>`` [—!`|ÁØÁ8Åè1"9ñŒòŒ*¾3ê0ê764ö6N5.1®1>jÜÍĘL³€¹‚y€yùn”Å(Î(Ѩ¥£Fuzm2Ú$ÔDdRa²ÏäªÉ;S–i„i¾é*Ó&Ó;f¸™‹Ù$³Yf[ÌN™õ68Z0ºbôÑ¿˜£æ.æ‰æsÌ·›_0°°´ˆ²Yl°8iÑgÉ´ µÌ³\ky̲׊al%±ZkuÜê7–1‹Ã*`U³ÚYýÖæÖÑÖJëmÖÖƒ6Ž6)6e6ûlîØRmÙ¶9¶kmÛlûí¬ì&Ú͵«·ûÅžb϶Û¯·?mÿÚÁÑ!Íá+‡&‡'Ž&Ž<ÇRÇzÇÛNt§§™NµNWœ‰Îlç|çÍΗ\P±KËEWÔÕ×UâºÙµs aŒÿé˜Ú1×Ýhn·b·z·{c™ccÇ–mûlœÝ¸Œq«Æ÷ÑÝǽÀ}‡û-C e-zºx 2?{~ÛÛ‹ô,ŒZ¸{uQþ¢ŸËÜËV—½ü"í‹–Å‹.~ðeÔ—õåºåòòë_~µu ¾D²¤c©×Ò K?V+ÎUºWVU¾_&Xvîk¯«¿Zž³¼c…ïŠ-+‰+¥+¯­ Yµ{µÁêÒÕÖL\Ó¸–µ¶bíËuÓ×­ò®Úºžº^¹¾»:¶ºyƒÝ†•Þoo¼ZV³o“ù¦¥›^onîÚº¥a«ÅÖʭム|sc[Ô¶ÆZ‡ÚªíÄíÅÛíHÝqú[ö·u;ÍvVîü°Kº«{wâîö:¿ºº=æ{VÔ£õÊúÞ½™{/}þ]sƒ[ö}Ì}•ûÁ~åþß¾ÏúþÚ˜mÙ~°ÿaÓ!Æ¡ŠF¤qvc“¸©»9½¹óð„Ãm--‡~ûã®#ÖGjŽ]qŒzlñ±¡ã¥ÇZe­}'rO~&èÌ‘³gŸcŸk:ï{¾ñ‚Ï…C?ûü|¨Ã·£ñ¢ßÅæKþ—Z:Çwë é:q9üòOWxWÎ_»Úy-åÚë™×»oo<¹Ypóù/Å¿ ÞZx›p»âŽþª»æwkuþu_·o÷Ñ{á÷.ÜOºëàÁӇЇï{?¢?ªzlõ¸î‰ç“#½‘½—~›ò[ÏSÙÓÁ¾òß ~ßôÌéÙ„þq¡rÏsùó¡?—½0}±ë¥÷˶„»¯ _ ¾®xcúf÷[öÛÓïÒÞ=œõžô¾úƒó‡–1o Éør¾ú(€AFsrøsôt—àùaŠæÎ§&DsOU#ðŸ°æ^¨&_à :®s[ØÙ2²ê¨ž P/¯Ö’"ÇËSã‹o<„7CC/, µðA>44¸yhè¼£b7h©¹kªˆïßx«P³d!øŒ4÷ÐOjü|ª Ôæÿô½Š®Û¬¢(ŠeXIfMM*>F(‡iN’†x < ÎASCIIScreenshot=L¢ pHYs%%IR$ð×iTXtXML:com.adobe.xmp 1596 Screenshot 974 áî¢iDOTç(ççX·û<5k@IDATxìÝ ¼ åÀñǾ”ìi-ä_Q–ʾ/•]H‘-RÈÙR²SHö]¶,!*¡(’¤MѪ„R*B¡ÿWZÙ2g’Ù· èWï A@@@@@ :Ixô]²F¶~ùUtZ—ÄR+¾AF4­›ÄR¸@@@@@â] ÉZ£§ÄÝè ]§·ZÖµ­µË+ € € € € €\¤Ix”}z|"šíO´ìN¼•l»9@@@@@’_€€Gò¿´@@@@@’(@À#‰€\Ž € € € € €É/@À#ùßZ€ € € € € €I à‘D@.G@@@@@äˆû€‡ïbæNdÖbé¾ç[ùN×Å*ÿèÑŸeé²e!U—.]:É™3§äÍ›W.¿<¯)RDÒyÁ¤ÿýW¦Ï˜)ÿýw0§uNÙ2e¤xñÛƒ:×û¤•+Wɇygy¶›?Ð̼GOF¡zªež½{ù]@Ó2Y~¢géòHgk7¨×hyjåô¨Y£†c;‚©{ÉâErÇ¥Ëp;°mÛviÙªµÛ)BÀÕ‡ƒ € € € € â>àá;bÃé=²'÷=ßÊwº.VùNäº.GÇíýš¡‹ÿý÷²ï‹/eïÞ½~ǽ36nX/…n¼Ñ;KÜ÷kVdp˜ +Q!>;µkß+5ªW÷ÉuÞ;o¾ <Äù„ GtDĦ76<Ïû'O=G×9ñNÿë™=rT¾ùö[9vì˜÷!Çí­›ß’üù¯±=îV·uAÆ dìèQÖnH¯]í&ëÖ­w½&Ô€G¬Þs×Fs@@@@ˆ¢(âzíÔI^ú®»dÑÂùÞ§úm뵟$ûð;¦íÚ>$žìŸè˜[ÀcÿŸ;®ý‘¨$îÔox_À`UÅšÕ+E6…ëyüøï²æÕWežŒqòÔ6”*YR-Z`;]˜Sݾmß½k§äÌ™Ó7Ûuÿˆ˜)]¶œë9z0Ô€G¬Þó€ ç@@@@@ J<¢ë[¬S'y0-KG&TªRMNž<é[´\rÉ%òÉÞ=‰ò“;àñ­1š¢J5ÿi¡t”ÈÚµëµUwt”Kß>½ýò2’ê©ëdŒ?A&MžâT…¹ˆùCmü§–rªÛ· ž”¶µñÍvÝŸòâT9j´ë9z€G@"N@@@@@T&÷Pß”6¥U°uX¹r•tïù„-ɞݻ${öìžcÉðxaÒd3vœ§=Ö†.PÞªõCòýÁƒV–ùªS{½÷î6Û‰N¼°ãtÅS‹Zºl¹ôîÓ×® qšjË©nßBB]¼<˜ÅÊ­:xX¼"€ € € € € ~ÜX»C~y:w‘ ƒ[¼€‡ € € € € ÊxÄèÀ©“<”€‡Û‚Ö[6o’kóç÷ÜMr<žyv„L›>ÃÓkãÍ×7Êõ×_':¤lùŠrøðaëùªk‘ìÚùždΜ9Q¾ÝN$<½ËuåQèÆeã†õÞ§ŠSÝ+V­[ßNt®î|ðþÉ•+—_¾wÆá#G¤LÙòÞYæ¶S™<ü¨È@@@@@T.@À#F?Nä¡<6¾þºtìôˆm‹¿Ü÷™dÈÁs,¹gQ(¥Ë”3Y÷4ÆØð <7r”¼8õ%ïSÌí§L’Z5kúåûfDÂÓ»Ì5¯¾*ÝëîåÙþæ«ýžmÝpª{ÊäIÒù‘.‰ÎÕOö—vmݧªš<åE5zŒßµ“žŸ(ŒñM<|EØG@@@@Ô.÷Pß ‹uJ«¿þ:%ÕjÔô¡>v‹k»<ž8@Ò§OmÁ¤Bÿ¾…lÛ¾]Z>ØÚ7[zöè.]»ü¬ùðÃ=Ò¨q¿óî¹»–Lžô‚_¾o†SÐ!”’w™?ýô“9êÄ;ÏÚþdïÑÑ'Vrªû•åËdúÌ™²ví:ëTóUGÞ¼e,Öž6mÚDùÖŽ‰*k³øŽx¹¯Q#iÚ¤±Üÿ@sëTÏk¨h¾çžF± € € € €$£á;u’ÓAÿõ×ßÈÀ§ÉöwßµmíÐ!ƒåÁ–-s x$:1ÈòåËɼ9³žÝ·_yyÉR¿ó6¼¶N *äÉ?þ¼ÜYº¬ßH=aÏî]’={vϹvIñ´+ïÌ™3Räæ[íÉf#XQÀøXÉ©n xüuê/iѲ•uªçuá‚yR¦tiϾ÷Ææ-[ä¡¶{g™Ûº.˹³ç"ðð+<ˆŒ`ßó Šâ@@@@@ ê<¢NœPS'yîܹå côƒoÒ§þøá9ðÕWòæ››|{öuäÁÎ÷¶KÖ¬Y=yº‘S§NÉÍ·KÔݱ¢ùOFfÌœ¥›‰Ò¨‘ÏIãûü=÷>ÉÉ3˜’w9ÞÛ·»]Nž<éenû.ïT·<Š+*UªV—ïLTNÝ:udâ„q‰ò¬;Ëëo¼a횯Ö`;w¾OÀ#‘ ; € € € € €€½{—ˆç:u’'µ¢g†“š5ó+&9ë_Û téê×–î?&ÝõÏÿý]Ò´Ù~ç—-SFÌŸë—ïáä€Ç¢…óE˵’SÝð¸ýöÛdæ¬Ù2ìéáÖéž×÷w¼+yòäñìë†NcU¦\…Dyºóô°¡Ò¢ùBÀÆ @@@@@ÀV€€‡-Kä3:É“RSŸÞ½¤SǶE$GÀ£Sç.²aãF¿ö¬_»FŠ)â—¯£Xn/^ÒvTÅ»ÛÞ–+®¸Âï+ÃÉ3Ü€‡N±uC¡›¬â½úNÇåT·ðøíøq)QòŽDeèNÿ~}¥ýÃíå¿0i²Œë?òcïžÝ’-[6‰´ØA@@@@œx8ÛDôˆS'y8•è4Xƒž :M’Sr xhÇ{¦Œ.µÍ¿¶ÀµR¹R%ÛcšyüøïR¼d)¿ãº`÷–ÍÎSr 2TæÎçwÝ€'ûK»¶ùå[Nžá<œ‚Zßïï\¹rYU‹SÝVÀCOìÕ»,[¾Âsnø.^®ŸÒeÊù­c¢#;t„‡¦HðˆÆ{n6@@@@@8HrÀ£Ö¨)ò§±às<¦+³_&ËmMsê$µqìØööÉ”)“ë¥ný_|.éÓ§w½>ÔƒK–.“>}ûÙ^V¼øí¶ùšùá‡{léˆ├<à xh;5nb[ÝWû¿´iÓzŽ9Õíðø`÷niÜä~Ï5ÖÆüys¤\Ù²æî¦·6K»‡Û[‡<¯kV¯”[o¹ÅÜTÀ#ï¹§Ál € € € € €q ä€Gß%kdë—_ÅÁ­ø7¡bádDÓºþ’!Ç©“\§mzzè¿Í_°P6oÙâ—¯+–-· ‚žë€ÇÍ[Ê{;vhÕK¾SIyìänÀcì¸ñòü “¼«0·5À´kç{‰òêöxè5kÝ#ûHtmíÚ÷Ê '˜yv‹•+VLV½²Üs  € € € € €€«@’?ÿCÚL[w£<²# f·o!Wæ¸Ì V:É:èüA*V®bÛ¼;î(%K/²=feÆ2àqèÐ!)WÁyº+«M¡¾>ÖíQyü±n¶—…êi[ˆWfÕê5å›o¾ñÊIØlР¾Œ3:Q¾SݾE‹Kÿ'&ºVwv¾·Ý H•-_ÑïØ˜Ñ#¥QÆž| 6@@@@@W$<´t zLظEöùY~úý× £}P§±*”/¯ñ[/Å©n߀‡8ð©A¢Ó“y'&KÓ±cǼ³ÍEÚu±vïDÀÃ[ƒm@@@@@ÀY€€‡³MD8u’»<´£F‘ÉS^´m‹ïôGÞ'Å*àñå—_J­{j{WíÙþôã½’5ë£<l6jû°íš%­[µ’Áƒü§… ×Ó»êÇKûä“O?õÎölßß´‰ŒxöϾµáT·]ÀãÓÏ>“:uë[—º¾nܰ^4åxxk° € € € €8 ðp¶‰è§Nò@ߎ— +ÛŽ@Бooy+ÑHV£cðpZìÛ{qn«Mn¯Ë–¯^½ûø¢Ó<íÙ½KÒ§OŸèX¸žZÈ?ÿü#o¼¹É}á;ʪDë}ãõ rE¾|V–çÕ©n»€‡^T¿á}²wï^ÏõvN?<ì´ÈC@@@@üxø›D%Ç©“Ü©£Û»“&O‘ÑcÆzgy¶û÷ë+ínçÙ·6ÜkV¯”Œ2X§ýšÏèüÏž=»çüóçÏ›‹•ëH ß4éù‰rï½÷øf;îëÔR%JÝi{|ÖÌéR¹RâEÑ&M›Ù÷ÉÞ=¢m %µ~¨­lÝú¶ß% 6±£G%ÊwòLtR˜;õëÕ•ñãìLZ¤SÝNS§NÉw•±}ÿ´—ññC+ˆGÍ[¶È ACäûƒ=Í{ Y3éÛ§—\vÙež<6ìþý÷_9þûïräða9|äˆ9rÔüÎÍ“'·äÉ“GòßÇúœ!CûÈE@@@@ bðÐŽ4íœÜ¸q£lzksP—\r‰Üsw-©Q£º”/WN²fÍÔu¾'ýùçŸ2ð©AröÜ9ßCžýOö—+òåóì³á.ðÊ++åÍ·Þr<éŽR%¥u«VŽÇSâÏ”~ü1`Ó‡jëžä gΜ‘~O¿ÿþ;È+œO»ÔøL]uÕUrÅWH¾|—KáB…ŒQW:_Ä‘õ¯mµëÖ9ž™1cFyvøÓ’)S&ÇsÂ9°æÕW¥ÛcÝm/½õ–[dÅò¥tÔûèèwàfãûwÅ+¯È¾/¾”o¾ùÆç ûÝ"EŠHÚ÷šßÅú3CB@@@@ žbðøðÃ=ò̳#d×$ùþïkÔHz=ÑÓì¨ ¥°~øQ*TªìzÉÊËå¶ÛйžÃÁÿúöë///Yú_†ÏVÕ*•eÆôi>¹)w÷СCR®B¥ n`ÔÈç¤ñ}‚:7˜“~7žÀ¿½D©`N 뜊+ÈýMšHÕªU$sæÌ!—ñìˆçä¥iÓ]¯Û³{WDƒ@§OŸ–Rw––“'O:Ö;dð iõ`KÇã©é€þü.[¾BæÎ›oެKʽ_›?¿4kv¿´iÝJ²dÉ’”¢¸@@@@ˆD=àñóÏ?Ëè1ceÉÒei°w!ƒ ”-ZHú çê'àá­™íÔð˜6}†¸ F¯l™2²`þÜ`N êœh<¬F航Áƒž 9X“?þXê5p*Õ6F$¼0q‚u{©òU§ ÔÑm: 'ÒIG ==tˆT«V5ÒES € € € €! D5à±cÇNi×¾ƒëÓ×!µÖæäbÅŠÉÜÙ3ƒzjœ€‡ `³R[À£f­{dÿA«½»ímsÚ¨ /p91V« 60;²ƒB.9:-^»‡Û[M¶}-U²¤,]²ØöXjÈüäÓO¥}‡NrØXŸ#šIGs zXÄ~Þ£ÙVÊF@@@¸8¢ðÐ)¬5n5íМ3{–±¶‡û´*<"ÿv¤¦€Ç_|!wß['$D]¦]Û‡BºÆéäX<´×]w,Z0?¨éã’#àñõ×ßHµ5ÈÌ|vI×IiåªÕÒ½GϘݺŽöX¾ôes}˜˜UJE € € € €\ˆJÀãÓÏ>“û›5êÈßw°r¥JòÒÔ)®‹ððUKú~j xŒ;^^˜4)$4]äyýÚ5!]ãtrr<´-P\´p¾¤OŸÞ©if~r<΋oש×@öíÛçØ6]CFG¤¦ôï¿ÿо:[¬“=–#j®¾úêXWM} € € € €©\ âóçÏKíºõ]; ÝÌsçÎöbºº¦GëV­‹'àáHöÔðПk]¬<œi6nX/…n¼1lcëÂä xhý:v>½{YM±}MŽ€‡6DGÞÜ×ä~Û«~è÷BjK/¿¼Dúö2Ùn[ƒ+W,jdP²52¯Y³FvîÜ)?ýô“œ={VÒ¤I#x²^­ê¬}ßWŽ'x»¤¹²á'†£ðšŒß³Òœ–Bù³Êë[‰ˆ¼òùôÿ|YßOUªTñ|Oyþ¬ã¼þ÷=ž|ôAþùGräÈ!MšÄf¤}D>¤‚ € €@"ðذq£têÜ%¨&jp£e‹æR¸P!¹á†ë¥`Á‚’)S&óÚߎ]dãë¯Ë+¯¬ º¼mooñ”á{_‘¤ï§–€Ç®>&M›…öX·GåñǺ…u­÷EÁLhfß1MÀ#^èÇSKÀcС2wî¼ÐŒ+´3yû;[Í'(Ã*àÂEÁ<:´XúõíT5Ùù@‹–òþû»‚ºõ[o¹E:vl/wß}·¤7nIŸ¿`¡Œ;.èÀÇü¹s¤\¹²nŦøc”ï õä¯uCì'ŒpIЇ5ÂÊuX#=Ø7T½F|\¬éþýK¦=ÛÜú ë•Ïg›Û÷‘<¬ã¼&™w1xhУC‡a}~¸@@”*Ñ€‡®ÝQǘÎ*Pš0~œÔ«ÚâϧN’Nt‘­[ßv-ÞmÍ®taL ¿ÿþ[J”º3èN^;ÈeÆBÎ%K”°;t^¤Z±)W¯^#Ý{>T;ÞÛ¾Íqš"AFõ¤™³f˰§‡U‡Ž®<è)I Ðá[ØÏ?ÿ,mÛµ—O>ýÔ÷ß¾.z¿aýZ×µ•ü.JA«W¯–åË—§ §¬¦Z#<Ò½û:Î#!Ó˰ŸjŸÞÎÛŒðp¶¹XŽdÉ’Eš6mz±Ü÷ € €@@ˆ<Ö­[/]uŸºçÞ{ï‘IÏO Ø0»þøã)[¾bÀŽçý_î³}Z™€‡jÒòRCÀcÓ[›¥ÝÃí¡têž¼—_î:$kID#àaÝÔ’¥Ë¤Oß~֮㫮ã¡ëyØ%v*±Ëûå—_䎻ÊUaR§Y;qâ„´5>ÁŒ$ñì3rÓ‹sñ˜SY1Ç}tæ¸Ok¬áaÄ6×ððÙ`±^9n|¤p¿ôit”G‹ ¾×|OâóÜç’5<‚sJÉßó:¥iÇŽ}?"ì#€ € pÑ D4à1yÊ‹2jôW¬¥//’R¥J¹žãv0˜'˜·lÞ$׿ÏïWL,Ú9}èÐOòÃ?Èï¿ÿ!9²g]³$W®œ’7o^Éš5«_û¢‘¡Oðÿúë¯røÈ9üÓa9n´+§±aÞ¼yär£“^Û”!C†°«N Ç»÷UÆ(§4qÂ8Ó±EËVN§È%—\"{vï’ôéÓ;žè@4ú‡¼>µ¿yË×f”*YR–.Yl{Nj xh‘£G/|®Ž›ï±.ªë_äÊ•ËÖ(š™‹/–þÆÔR0 Ð*CëÚ•«T xjÕ*•eÆôiÏ æ]Cäðaã»ìÈaùùèÏ’1cFÉwE>Éwy>ãû,¯¹L9‘:硇FŒtÆþ#;þ[ÓÃ~¤ÇÍ(‡!wQøü{^f?w_X?`|>ƒcc„GpN)ù,ý[ mÛ¶)ùh; € €! D4à¡Oˆë“ânI¿êª«ÜNq=¦ .W¯YËõœ¹³gI… åýΉfÀããO>1ŸðÿrÿcQ诎B)[¦ŒÔªUS*W®dœñk|ýõ—¬5FÛè|ûÁ,N]¹R%iذT¯V5ä@L¤sçÍ—­o»O[¦ÏOk°&ÚIŸd/z[q×jöîÙ-:]Àí%J¹¾ï³fNµ7E3à¡múúëo¤Z ÷E¾Õ|×Î÷lo!¹>eôledêZAúy³KÚæ¯¾þÚî¤M“V†?=Ô PZ'ègk•1ѬYsdÿV¶ß«N­W£z5s!p pÆ"=м¥¼·c‡kUúþm5‘ ¸2LæÌëZ§ü쓽æg$à‰>'h nçÎ÷eñ’%òúëo¸~¾ôR]/§aƒúÒ´Ic)X° Oi‘ß}ðÁ™û>Môæ¼O¼†‡Uñ>š1 öž8¿ø=fhÖ‡—Ïgp#áœSÂçÍúÞIy¯mÚ´ ësÄE € €¤Dˆ<ºv{LÖ®]çêÔµ ô)¥þOp}ª¶s§Nrýõ×ùµ#}ûöÉø ψýê 6£Y³û¥·±ˆ{Μ9ƒ½Äö<½¿3gÊì9; m 02uÑëÇëtçd$Ë–¯^½/¸ÝÀèÐ=ò¹×pºg·ü•«VK÷=Oñ^È[Ÿ®×§ì’•ÆŽåt8`~´úÇüõ7ØŽ/÷}f;*(¹·»Ýµ#|ÈàAÒêÁ–¶÷UµzMùæ›oliæúµkDƒš47rÔh׺Ì}þéÙ£»tîÔ1ª?¯:‚«LYÿ ¯OSÌéuZÁH%]ÏãÎÒe7{Ö ©T±bÀó¬tÝý,Í™;ßõý±Î·{Õ —»ªÜh%íP%EO€5<ÔÖi¤†å~ñŸæ>ŸÖψû«ïk¡n§«8žìHi>:≄ € €@jˆhÀã©AƒeÞü®vI;Þµð#ðøí·ßd q¿<šä9¬S=ûÌÓR·Nh‹¹[lÛ¶]:v~$äÎXëzïW]hx¸1R´hQïlÛíH<­“aU®—/Nž”¤©¡¬²‚y}¨íîÓ<3Z4£éí·ß‘VmÜÿ üôãÂ~º>Ú½‡F›È‡îÑMÇä4Jëb xÜX¨ þL’‰d3j¤dΜÙÑ5)43hð×"Š+&+W,3G$¸žâÁqã'ÈÄç_p½ªk—.Ò³Çã®çX9*]µ v}𕕤W]?çÉþ}mƒtI*ظ¸eË–Œðˆâ„5<Ò\XÄÿ0²Ã~QŽ_¬>sž o„ŸÏàF.0Â#8§”>ƒ€GRãs= € €@JˆhÀC;½´ó+PZ¼pÜu×N‹øñH<þúë”´4:Ñu ‡sº {(O`ë`Ó¦ÏílŽtš0~œÔ«ë€‰DÀCµ£=PºãŽR2gÖÌ GŸ*/Ðñ`ž^×u9²ë³hÒ§ÒK”ºÓ5è¤ë}„ÔŠEÀã‰^}dùŠ®4«W®° †]¬Y³çœªÏìÂÁH­aWW0ªþýúJû‡ÛÙ]ž¤<ý>ü|ßç®e¸öZÉ“'ë9zp×®]Òé‘®rìØ±€ç†r‚®=ó‚ñÝš/ßå¡\ð\íP%EOÀZÃÃy$ôåóš*æŽ o >ŸÁ}6}Gxwg¥4ÖðHiïíE@HŠ@Dºà±>(éh†Áƒž2çZO—.] Ó#v<³ÆµŒ‘o¾¹)bíò-hÁü¹Žkøžû̳#Ì€‡o~¤ö-2ŸÔ€‡®ÉR¿a#× ÞË­·Ü"êrÙe—EêÖ–££•tÔ’S²[ŒY§äÒ©¹œ’Ý5NçúæÇ"àÑîáö¢£mÜÒ7È 7\ïwÊÅðÐ鍯Œçw¯áflÞô†(P ÜËm¯Óï¤B…¦Ý²=áBækë^•›nºÉí”d=¶mûviù`먵A×/ÑŸÝ9”‘¨¨E‹ŒðˆúãJ4²#>ösæÈ*§Oÿ#§Îü—í3~0vpžhQÊÝŸ;²qXW>ŸÁ\°Ö ›ý¿‹Åƒ€‡õNòŠ € "ðÐ}o)z[Ðn:7~·®]¤lÙ2ž§äƒ¾8Œ#ðÐ9ü§¼85ŒÚƒ¿D§”z}Ãú€sþG»ƒP[¬Á©uÆ:׿Ïo{I xèº 5–ÇÛ–meª‡^b±H¹U§¾ÖoxŸë¢ï£ŒuDß—xªM›Þ’ví;xã·­‹~‡s/±x”)W!àûñÉÞ=æÏ…ï]Œß{Lê~­š5åÅ)“’ZL¢ëu ¨ÒeË%ÊóÝÑŸ·÷w¼ñé¬|ë wÿøñߥzÍZÙáÛ=§£è"•´C5¥§[o½UúõëgÞF=äˆñ½/)ÞFx\Ÿ?§<ÖªŒäÌžÕŒ'¨ÓÉ¿þ–΃V§Ê‘žXŠá`Ƥ¢ô:oTø}BMsæÌ1§Í\·n,Xà>Mk¨eûžŸ7o^©^½º™½ÅxhèСCæö5×\#Ï=—0rwÈ!òå—_ú^j»ÿðËNQuæÌ ¶ƒ»ZµjFlLßÁ„ä»FGríëwÓ]wÝe6jÆŒVóeùýÁƒ²cÇNÙþiŸ6õEãàjŽçjçwµÁwjÀ¢h±¢rE¾|ò“`øüó}A/\¿^]?n¬m[ xhû›4m&û°-×ÊÔŽZBÉt´2cðúí·ßJ•j5\kÚ½k§ßBó§N’›o-æzÝÓÆJ‹æ¸žcw0Ú“'OŠ.î–4¦»”Új¡f¡¦… æ™ß ¡^çt¾~–ï­S×é°™Ó&2âÙg\ÏI΃]Œ5;Ö­[T4Z±B¹æš«ÍiätÑùõ¯mú½;z”4lØ ¨ºÔ¼ysOç›Õé–Ò^o¿ývéÝ»·y«O<ñ„üôÓOfçg<ÜGâ5<¬ Éóšý²,òüÀÚ~Aÿÿ9'íú£ú<#*’§}sýá<Âý|Î;×|àdãÆ¢Ák¤C4^58Ѿ}{óó7oÞ}Ú\Üûý÷wyʱÛÐåU¯¬°>ÉîüHæ½0i²ëTF+V0×±«óñî=dÕê5v‡Ì¼âÅo—Ë–:w:í€G0 _ëÔbkV¯´mbjx´jõ 4kÚÔ˜Bª¤7¦ãÓQmßÁ±ÕÆûýÒ´é¶.¾™ºŽ†®§©ôöÛ'·òëö¨<þX7·S’íØšW_•nuXÿW\!ƒžh|—Uóýö矊þüŽc˜õ-ü½íÛ"²ž‡v¨¦ô¤OQ÷ïßß¼ áhÄ],ï7žFxtoSNJÜz•Ñé,²lÍ;²|Ý69uêŒÜ\¤üp,}TG8!”T]þüÑMÂú± ÷ó©nUGxÌŸ??¬ºƒ½Hƒ¤Ð4{ölÑ ‹¦üÆC*Þ#<¾øâ 3?Ð?:ÂC§¨²Î×ãÖ+Èi]cíû¾Æêøÿþ÷?)W.aôâôéÓ=Á›XÕï{ßÖ~J­_6H € €¤ˆ<nåÊUÒ½ç5ÔÎïJ•*ŠN £^Y²d ¹ü¤<îÐÑuí .3õ’>©wOm÷'²Ýž¤ÿíøq)Q2á©77íX×E¾³eËæxšÎÿ߯_×u'ôb‘ #|S¨­¯k×n²áÂõ¾åyï¯^õŠ5:ã’#Uª\Õ5(¥OËëSóvIï­Sç.v‡ú¨y‰ŽðÐ)­|S8uéïŽFï+gΜæÚúݺ{÷nßâ÷½GxX'YkEØí_‘7›üïÆ¼†Á?òÞž„Q”™3gÛ‹\a<±ŸVö~qXþ8qFtÄ„¦\9²H™â×ÊMóÈ¥—d’~ú]ÞÿôGùxßëJeÊ”^Ê–¸VZÕ/.2¤“¯¾ýIÞØ¶_ÒeLxèâÍm_egL(Ðø·à59厢×Èr™yßþp\¶¾ÿüxäsß»ý¥oÏo<¼‘A>?ð³þùOóx«se䟔ÏõŒìÈ>”/U@nº.¯\‘÷R9úË Ù÷í/òæö¯<÷£hùWz9¼k8\}y6)W² qmùËXhý³Gäµ­ûÍú¼Ûc]Ÿ.m)yëÕò¿ëóʕƵ¿?%“·ßÿVþ9{Þ¯¾ë{.åpÏvå[þV}öŒij¶5ÔÂý|ê¨á¡¥K—šŸO êÃ#úÀÊo¼a~æ¼Û£Çî¼óN³Sþ­·Þ2_½ëçA×£ÐNúÍ›7ËùóçÍú_ªT)s{Ïž=rÎx(D“oÀÃn m“–©ßüñ‡1UéçÆT×U2¿34àLòáaE¬k­}}ÐHëÒ}kŠ-=Ç:®Á ì{½*TÈüNWçŸþY>üðC9{ö¬'¨¡.úÿ÷VÀcÚ´ižz¬ú4ž#G3ÿ 1jZÿÿü²Ë.3§@Õ)hõ»Mk]ùŒé\õ;ïÇ”OQÍZ¾•¬òôUÿÓï,-[ßÓãÆFzÍW_Ÿ3ã˜^§¯2d0ïIËÐßcú½ë}\ëÔQÕz¿ú^[×é½ßhüþÑ}mßßÿ-áúصߪG_­dµËš:ÍÊç@@‹Y jE{÷½÷¤GÏ^Q"£ví{Í'ÎsÔJI x´{¸½ürìWÛj.»,›Ìš9ÜæÆöŸÌ`¦¡Ñ9í˜óM-Z¶ ¸ˆ.Œ¬£a‚M»>øÀ\SÃíüïm]`Ó;…ð˜øü 2nüïËm·gL{ÉxB±Ší±Xdê¸/mt0,Z8ßµ)Ö$ÐÑ@›ÞH˜ºÂµ ¯ƒÑxl|ýu4xhPŸQ·é¬´™Üôœ=»wIöìÙu3b)P ÒmtCÕê5®eÓ±C{éÛ'aƒ@ÖΪ{î­0Pðå¾ÏÌ“@åsüéáÏÈŒ™³\O;{–ÑqTÞõ߃ë׿fŽXñÍwÿ›¯:X½¯Ÿ<åE5zŒw–ß¶NÅ¥Sr›ô=¨S¯Ù!çvÍúµkÌ5·skÖ¬Y¢N.ïÎ&«“ÉíUÖ‹õ§Ô«W/ÑÎ<«í88p Ùñè{vò3FöîÝë9ߺNX¯c½ÜêêÞ½»ù]`݇®¥õÔSO9Ö5zôhùä“O­€‡~î´ã[;·½“~ÇŒ9R>úè#óýÑöj ±k×®æiÚ±|âÄ ó¾­û-[¶¬të–0­`§NDŸëuúÿÏ>û¬y.Nn4°`ðÐÏ ÷çE;Íûõë'7ß|³w³mkçþƒ>èiŸÕ»Wk„G¢lv´£^×ûФ£`4(î4¨¡ÁMºØ»>ü¡Iƒ õêÕ3·5p ÷ì;Ū;V®\)¿ýö›yžþãðð064 P¹re3KïuÉ’%f}:–º¨­®Å¦uù&ñå—_6Ïñ>¦±Ô®]Û @xçë¶^³fÍÏCO ÑŸ/M¬Ùµk—¹­ÿxßïöíÛÍ‹uPƒ)+V4w/^l–ë}~(>V™Á¾ðVŠó@@.¨<HÿH:ìé€S&%SGŒõ\ÀÑI x$¥¾×êS´eË'üÑã{ÌÚ·ë;r䨔.[Î:ÅöU;¦õI|ýÃ6ؤO ÝYº¬ët„v4êg@×Ñ'ßxãÍ€3ï6Œ|n„4i|ŸwV¢í‹5àêzÁŒ¸pZ'h;ßyV¦Ï˜ézv(£Ï¬‚bð&àä4ÒÍj§ÝëëÆØ:v¶;äÉëÚ¥‹ôìáþõœì°aux9vÍÖ`GÍ Aií@µžP¾êª«ÌÖźø?ü`íÊäÉ“%W®„ÑÇŽ3Ÿ<ÖŽ:íØ³:35Hâ}Íý÷ßo,ÔÞÐ,C;õ÷vÈj[G_XIGx:tÈÚ «.5iР§.m‡v|êkÄá”)SdË–-žzœ6t„‡•Œ.pqû¦ÇjX\jT(l^bܦáa]ðºæÍÏdñÚ¥ÉÝE¥AÍ„Îâ'ÏÈGŸ}/ú[n-r1¢!! ûÁ'?ÊØ™ï˜#úwª,9³g1}Ïž=gޱJîkVNþõ· ïQÓÝ¡ù‡Žü.ŸïÿA2eÌ %Š”¬™3š§OY¸CÞÙõ­¹­ÿÌ5Ö¦Hg<n×V+àqõ¹dèã5ÌkÎA>;(G~þ]®/p¹ÜtCÂoôè±^o¸áO@Cv×ÿ§Ðïë¸<4€«IƒF:êAk‡~ýú‰ÿ¿NÛ«Fjj”4Ø¡#j¬òt k„ÇK/½äÉ÷n‡ŽÐ…å5`¢ uÖu‰¼“ž£ßA:úà ´èh³÷Œ‡²¬ögÊ”É ÒkuÔ‰¶çòË/7G‰hžšÎ6ÖYѤÿϬßáÚv’Lƒ5V»kÔ¨á ´èwôŠ+<õ¨~Wk{gÎLø½®U_°¯:t0ÛÎ? € €¤¨<,ÄO?û̘¦Ã˜`Ùr×Îdëüp^'OzAî¹»–ã¥ñð¦óÚ.à±Å˜²¤ÍCíïQôéÝK:u ý JlÛþ®cÙ]éì÷Dt0fÍîØù¨•º=‰ïبØf<×òÁÖ®¥Ó®È»½„k9úé{l æg&زB=OGµÌ›7ÇuÓÅð(U²¤,]²8$®uëÖ‹ŽðqK+W,—Ûn+ævJÐÇt±tµwKá£ðøë¯SrKQwƒp×; fZ9~PAIIH¶³Éû< P<ÿüóæµÚ©¦§úju¾é׃6›¦ARE¯×Ž´ví¾ÿW­Z%:ÂÂ*·X±bò kKèTU½{÷6ËÓ ƒvj®9tw«<­OëÒŽVM:ÂCƒ!š¯Á«.íÌ;v¬§}Ú¡8iÒ$³n­K,ÚíPœ:uª¹­Sìh‡ vZ÷UºtiyòÉ'eÖ¬Y‰:­ã¾¯¡®áѲî-rO•ÿ:;w|x@Ö¼¶MvïýJ®º"—ü›!·¤Ët©LÒÀIñѧßÈc^”!FóvŠLÞIn»õzsû‘A+åØñ?äÈwŸÈúECÎäÌòîûŸKß§gyοòºÛåÎb×Jc†¦—殓Ë7{Žëȵó‡×f’F`¤Ã€W<#1ÌÆqM'þ:#KVn•7ÞÞmþøãrß} ÿõë×›íús«ízL“~ntz%ïŸgzJ?Ëš:vìèá¡#9|zS~‰%¤OŸ>f9ÑϵCßjßСCÍQÚ9ß²eKO¾uÜîU¿¬|³`›ô¸.¤~÷Ýw›Gµã_ëµ’×é´tt—&5³F€h€¶qãÆf¾¶KG6ì¸0…¦^§k”XS„êw••¼:¥•º,XÐ(Öòõ»Kƒ'Ѳt=k:@ *ipU¿ï4éôVz¾~êïïéud‡5m¢¶AGÉYI¿w­€æk DëÔïH­× „hû4é÷¹NS¥Ióf̘a¾êvóæÍÍÀ¯Ngµaó-áú˜\øGïݪß;ßÚÖãŒð°4xE@H 1 xX˜úGÂîÝÊcŽòuÆ”)úôS¤’>)¶yÓž?œ|ËeÀCÿð8uê´ñGï óß?Ž&ýøÄŸ'ÌN§£ÆÓc:)íË–¯^½þØõ½?kßmëœH½ x[O÷Ç“nv öô¨è~Bé×'ÌõIs§¤#“v¼»Íüƒ×éïüä xh;_[÷ªãçÊjãÅðhÚ¤±<7"aºë>½j‡Hý†Î#aôúH<‚Y´üÉþýDGÿ„’¢ðøÎè(¯\¥šk“’ 4õ_$ÖñÐÕp’έ¯lšô©[í¸Ôùïõécí<»í¶ÛÌ`†×NTk] ’èÇúä´v¶éœúú„±>m¬¿_4H¢—šZ·nmv8j5jdæõìÙÓœzŪK;t#Y—S´sP“Nï£OQkµ³MïKŸפ›V‡ ™áðO¨kxa¬÷‘OžÐÆlÑĹÛeÛ®¯å÷cßËŠi=Í Å{»¾Áã–›ÇõŸy ʘ~uäê|—ÉÁ–Œ’K²å–L™/5: ÏË_'~“ÞëJÊÅÍkš÷xÙ 0èμÑMïÿ´òéßK—>“:E|1¤ÏIÊ–«(ú'x>Úo²|ºÿQv^I—>ƒœýû”œ>yL^_:ÜôÝùÑA?{›y¿­.8hà¢J£¾fy3_b–yúÔŸ2vp[)^T;jÏI«ÞÆõF}:u× G>+×½+㦾"Y³å’´é3u–3§þ0Cçå²ÜWK¶ìùdTß{=÷ÜÒ¸ç¬îù¼qϧ|î¹…qÏVg¬v k}V fѸ„©ƒ¬k‚} ÷ó¹páBó÷òòåËeüøñæ¶þ¼êÿ¿j'ÿëÆTúÒQÖ4VÌÓ€¡& xhpÑ;éúúùÓ¤k„‡Žä5j”™¯#9¾øÂ2É)_§¹*htúk[4¸ Ÿ+ Ÿ1m›Ž³ò5àL²¦¡Òsõ}Ò²¬ä½¯mò xx×€ˆoÀCëÿCX½WaªmÖÑ2ú]¦vÖq †h°T“wÀCƒÞåëuÜQYbܰú¨Sˆiýú·¾ú©¢Â… {FXXí×`€Þ·"4¸«mÖk´}¯t>ŽS¿ÃtD‰&îÌšªK§øÒs­à…¶K“WÞyçùÌxèK“U®Ó¤£é’â£åYí×mMnûŒðH0â_@@Ô!ó€‡/ëÁƒ?ˆŽþÐŽ‘={> iŠß²t¿†1\ü¥©Sì™ÓùT¨TÙö˜•jG¤þq¡O¸î5ÚÿÑG{e·1—ï÷ߌH Ç.à1åÅ©2rÔh«¹¶¯¾Ügþ‘e{0™ÁTW¿^]?nl0§Fõýcõæ[ÝŸ8¥6˜¬.˜'eŒ?öƒIÉðX±l©ñÄäí›x1<™öH¿Ó*V®âêê÷Œ[aÁ¬Ô¹SGéÝë ·büŽE;àñ±xuã&îƒ`öüodèÚ ºFˆSÒN0.+)©iÓ¦f'™ÕÉì«®Á¡£$ô)c}rXŸ<Ö;ëú[Œi µóS“<´cL;ã´£M;Zu¾xíìÓmÍ×ë´ƒO;$µ³NÓ°aÃÌß«ýû÷—Ûo¿Ý ~hE;äôë:]øXÛ£I§Ü9|ø°Yž>¡mÕ¥OEkç¤Õ> ”èèV]Ú]X_uŽz½/íÔûÒ?ë:íœÕÎLÿ^;­|§Wï5<¬öº½Zíè¯jtô_–ë*Éœ5» ÈdÔuÎèh7ž&ïVÓXØür££ô¼|÷Ã/ÆÚÌ{·þ9îsº(Ý×)°½º×lçŒgIÖ,™äý¾‘±³Þóøi{f<{Ÿd1GÿëÔ9ò˟ƱtVqæë¥YÓKž\ÙÌíÞ#×ËÁCÇÍ뭀NJµÛäÅy›ä’Ë.7%—˜£4þ=NêU/*-ŒÅÒ5øö°1eO&sÛúGÏ)?·1¢"|÷ã¯ÒwÔ³\o‡:­GJ¶œW×&xQ i]ÿrwÕæ}YÁˆ5n–ûkßf]¿õÓ’)Û5¦›å­~üzH2‹µgÍ–GfŽhüß= ®§IërÏÏ­3×qzŸå/ 3àîçÓ x¼òÊ+ægNV-U ßú¹ÑÿghÓ¦é¨#<¬€‡Žl²áa<´ãYŸë}ëçÈ xèçPƒŸnù:Z@??úyÓò´ÞÛOƒ)ÀÐÏš®£cµÛíÕ ’Z?WN¯ÚV+à¡#_¼Gxè5¸÷Þ{ÍËõ»Ê{„G“&MÌ| Új0@¡VÒà‡:jZ½zµ'À«k½ ]?ÃZƒHÝ5¸«÷}Ýu‰×ÑÀ’ŽðÐïCýõ^ƒEëÕâ­Åå­Ñ$úݦëhÒµ@4À¬ß}VÒ»:e™¾ÇZ§u~—éè6MºV‡þ£kœh@E×âÐ6èZ#¿üò‹hðLÛbÑ÷\GÄiÒïäp|Ì‹CøG6H € €¤dxøBë$ú‡ÂÛïl“M›Þ +2eò$¹»VMߢ#ðÐ?¸,\$ã'L4ÿ°õ«,vÁC†É£#Ê)éSi»v¾çt8âù‘xT­RY¦ •ëäÉ™^Û°Q:?Òŵ ­'³¯½6¿ë9ÖAó{Òdûà›uÎÆ|÷Ï fíº¾Æ:àq­Ñy1qÂø §^º={t—®]q}_|Ær$™ÖýùçûäÞ:u}›‘h¿®±XõÄ ãåÚ‰dÀÃ.¸Ìç-)kèB»[úüÓul¹kwL;TÃI:”®Ÿ¡Á….ÆZ"ðN¾Ak„‡.´«ß“ëÖ­RÆ7éhŽG“v8®5Öš8q¢ÙѦSKéÚ:ϽwŠd]/¾ø¢Ùy§íÕ'ß}ïË»Þ`¶½×ðæüVõ‹š#<4˜ñÀcóŒû„ie¼¯}þ©ºFðáRï,Çí7·}!Ó—î6{{Œ€ÇìÄ¿cŽ{Àx_‹It`ؤMòÙþ#fÞü1÷FiåÕ×?”k÷%:Owº´¸KÊßq½_¾]ÆOG‹¯7y;´ìiŒ(ñIÍk‘º5Š›·Í»/6vkUFÊ”((ç݆í'KÖKsù\•x7Ü{N\Jp{‹Æ?܉>g…ûù\´h‘ù³«OákàÏ7i`P;ÀõÿWx ¡m¾ß:¥•wÀÃ{„‡NM§IÞ#<ìò­ï€7ŒÑ£ÖôqÞíó xxsÚv[ÃúF;ú ,è xÌœ9Ó ªx×€«ðÐï'kD—w‡¾®Ñ¡ÿodiôzýý`FÑ “5 ž.>n<4x`âÐQ7övXåéöKÏ×i°¬d×௾ºo.ô¾jÕªežªï·þÿ½u¾u½N]e­O¢Á½7 jéwª¶_×*Òï[Y§Á-GËÐ}}Õ¶èHzPË×߸Ү÷ÿ3[íõ}µÚ¯ùÚV € €©E î¾ðúdÕcígžt`A;¥JxbÕ»¼HtDê4úäþˆçFFd‡wû|·íté*ë_Ûà{ªg?Ó´x b#­fÈ §¤U«„'삨6*§têÜE6lÜ•² Õ?”5@åý4¡Ó¹± x訛aC‡xþ wj“w>H|Ïx»Ú>zôg¹«LY×Óôçlχ¸®Áâ[€ŽTyÅX»!˜´Ü˜jïûƒOµû^š7<5h°ã5z`÷®‰Õv=Ùçà £ã¬ç½}rï¾³u³gÞöÄG‚ÛÓ§rÝžœöí|²öÇgÖ«S:ê+V¾õª£?¬Ú¹jðÐé^´ÓoÓ¦M¢Áë|ëU;Î4_ÓôéÓÍ9âõ‰j}²X;5¸âÛ^퀳Fxè0Ö}ÒYÏÕºt|ßëtZ*Íפy:ÍÖ¥ ðj]ð°Úî«g„‡1Q“ŽÎð”ã°oNåTµ¨9zC;úíΟ0 ¶äË›Ý\ãùÙú}¯‘ kb¥„×3'×Û’C¿kkœNoýWf£8ÌFÀcÌìwµGG ¨Ï>c¡ò4@’¸¼ÏŸ•¿O4Ëüúh:1f’2¯·k7},óWâ×ÞöMJHÕr7™×=óÂj¿rµžŒrÏ#=~?%rø·tf;ZÕש½¼Œó¼ýZÔ¹YêT¿í?'ãxǦ%¥JÙ›Ì64{|1jä¿‘9zÿÞ×ëþ"3ÈcÝóFÛ;j‡ó?§¦uúúhZ9{î¿Lvåù–ï½nÀ#ÜϧoÀÃósdtëû¬kh”4ÖwÒÚQ¯ÇË–-›h„‡<¼¯ÓÎn+à¡Óyðð lèu:šÂ.?ÐçR;µ­º^„ïçÖnßn ë<ó‡Oß]ã¾5 aMY¥ k‡ž£Ç}GxhǾ&}ÇÁ ýÞó._G‚Y Ì/[¶Ì¡Ç½Gx˜]øGG¯èw¡¬d•§£7¬€‡ÐÐs¬ãú¾iPD>­ãºvGݺ  hÀåÈ‘#žó­ò50¬A.M:ÊFG¹iÀK2ú}­z­À´l¤¯ 0ƒg[õ÷ô~4Э ÓoÞ¼Ù¬CË ×GN¬dÝŸÛ>#<,^@@Rƒ@Ü<¬7A&>/úÔn tÇ¥dÉâE~§%µ#RÿÈz¼GOãéÚ„§(ý*ˆp†]À#кEн.V)Rmï«kVÉ-Æ}É‘Žÿ]Š—,•UËÔ'KÍ óÞ»5 Úµï•ûNÜ £ukï1 "Iýžñu ´¯AØB…‹:M–-}YJO¾F#õr ,2: œRÅŠdά™‰Ïš=G†{:QžïΞݻÌ)[|óƒÙ_¹jµt7¾¯Ý’®ù¤‹á&«/Ôëûöíkv˜j§™vJzw\iYÚ¹f<4aðÐ'‹uò}ûöy‚Þu{wºêÉ ”.¬rÚù¨k‡ø¦HÖÕ¯_?³³QWê}%5yFxX}éVû­u„Ç…€G‹^?^ç÷~¸Œ”(zÑY}Vª5î›P¢×q3ãÂ~μŒ‘¹ÞRñ x|m<ÞKˆi\hÏ´á %Û¥Yä˯~”vÝ‘Tåééù®¹EÒgÌôöîNªª}àøÃ.ÝÝ%(6‚¯ ¯ H "©€øêß ì.BÁBìBQ,y_Q[¤Eº¤–ŽÿyÎr.wf§vwfØø>pçÖ¹÷~ïÌ,{žûœc÷ód'›á¡7ÆÏ:X›™Ü¿é)ÕäÿºÿÛ.ïd,ÿkõúômBÔ_¼DÑóÕrU[ÍtIxX‡ í»™1>.=¿¡ x¸õÿ>¥º\Û­™Ý¿ÿà ²zíVûÚþ´¿žß‹}×|³¹æCm¯Þù» ªÔ2×ìïŽ+D}‘öûtÖÞKYý|jcºf'i†‡¯Á]‹Nµ+7mÜ×ààM7ÝdWi†‡”\»¡se»ý4«@Ç×Т†ÇOÉûÃÔÉáVÇ}y<Ú…Ò'&X£÷/ÙåÝ÷Þ—;î¼+Ù‡µÇkÑ¢¹Œ|fxÔcÇ;à¡ï•šæÉÆ5ªÛ'[š¾·Ë–-õ<Âm@À#]&»ß3á|#-×AÒu°ôHå¦o›û÷‹´I–×uîÒM~üé§°û_Ññryô‘Àî¥&˜ÆÄ>}Óù ·ãwßL2 ‹5Ã­Ž¸|ôoÊ>q›™Ó§e*‹)¸2hW›\£X¬SK£G¶:m\Õ§š]=ÚЪ¨gŸtÔ€‡Ëðxä‘Glã™G»Jѱ2Ü~:Õu_]¯•k]:€²k¤Ôñ<´ÑОúÔ²{@ë2<}ôQïX(q—îxC‡õŽÕ®];Û×¾}{ïImÍúøüóô1%üÇÓÆ=í&ø xW¯šR½­i[÷ù†È4ð¯×®œZø¡2 Ú4­#].K·}ûãßäýO§òðÕ¯Ý^mßµß<ÉÞèùê3<Ë㯘 ½ï·¸OSiP¿†©GäöGÇË¢eiëÝõW3›§mÜmï»îï<&Δ74ÃÃw|]_¶Ä^ynpzjÁ’4¹c¨iD áQ°`Š”/[R6nÙm×gp0'æ?ßn­Ž÷2<4à¡çW¹Lyf@z`aÖ¼Õ2èÙIÎ'%µ€T¯dg_½Yômæ»ædñò ¶×ë©Q¥¬¬Ý°3}Œš ë ¾ÞpóY xdõóé:n„ëšÈú'Í`1b„õÔq4[KýŽ8âo,Ͳš8qâ¡÷•Y¯]=éØZz÷îí 4ˆàš  æZ_¸åÚE]õêÕm† –º÷—N5‹Dƒ/tÔlwÞ‘¦þ1<Âm§ç­ã…¸@¦f(h¦‚Û^×k€¢iÓ¦úR4Ä#4 áºÓ€†ŽiáöÓ©?h ß… Ðåþ€‡ HhàDƒZ´~íÂO¯Õ ,¹ ×å—º¸¢Ù.àáÖëw›f¤iwúpÕ›o¾p~º¯Þ3]¯ÿÓcº1—6mÚ$èÕóuE0wc¸hpÊ}×»õú=^®\9ïý‘Uÿ}wuû]ƒ×k6!@@ü"ëú Ω§ŸÊêfMü­ý¥Ð¿.; ‘ß|û­ôìÕÛ_]Ä×Ú˜|¢yª¶jÕ*v`Bœ°T©’¶qMûíÝ»g¯ôþϵëðþÌyjØÓ÷[²hAÄõñ\Ï€‡žW‡ö—ÉcC‡Äócª+ZƒmL•dc£Xžd%à¡ Ëѧ.`ºÄÑ÷`AÓÏBÀ#]3;ß3Y½±dKhƒÜ—ŸjÁ³zœPû铲 ñ;¹oŸ›¤_ß>»ÿðãÒ¥känì>ÿ¡ý Ø1ÆÍÔ1–"•ì~WjƒjVŠ*>2Ý…é`ÈZô©n}*X»VÑ,=Ä×íÒÊ=-® Úà©E³>´k­K‹v­ãž~ÖîS´!VÚশÚ¦??õ)ò¥K—JåÊ•íS×Íš5óë²{,mxÔK}ŠYŸ„Ö~êõ\´èyh·5:è±6Oš4É.ôOjv¡3´mQÛ0Ýô`%Wµmè<ºô;ÃzÝ~Ç–u2vÔõ6x¡í Ÿ~3OF4Í6>êúº5ÊI‡KN”SN¬%<ÿµÌ˜»ÚÖî?O7¯þpü²E·ÊsC®µŽ»LæÈ‹ïþ,“^bŸj¾oO8ºªtkÝXjU+#ú¾å—Žƒ¡cxüÏ®~ÔÜuieÌúÝ;·ÉS÷µ•ºµªØ­¿œ²ÀŒ_ò³=޾w.hr¤@ýëdÈ‹ßHÙ"A×ü޹æ_Í5›óJ5˜]³Öt}Þ¼=bøõï ïê¶ÈÔ4«ŸO'C?;|þÃìà€þ²ÓÙ±Sgùå—_ýÕ…|­ {WvïfSôCnppa,סÚmŒv©dç©èHõ†ZkÀC³6oÚÓ ôO=ñ¸ù³M¨Ã%d™>=Ý䜦 ©;ÖJ5È£ÁžH%–÷Ì®é-wÝyG¤j¶Ž€G:mv¾g²zs´Aüìs›EÝýÑÁƒL×!Yh;\åßší=¯·Ú.xå^3ÎSzpL+ÛcêMI)`ƒ%®òo~\$£Æ¤gžh—V-ÎKï²Ê:ø®_ïviÕêàný}&•e×_2öù»¼zÕE8E rùmÖJðغq•\Ýñ̘®ùоæ‰yŸW¸LŽàëwóY xdõóéÎV3’ôsªq®Eëuöj"–cé9¸F_­C¥Ofë ½ÚU‹¿hÃÜôéÓmæ‡{Â]ׇóÊìÚ•“f.èýïÒïíC‡V&ÓxŸ^Èú5 ¤|™"2äÞ^R¿nàÿt›={÷É´ åÙ±¿ÈöfÙÿ•G:HÉEå§i‹ì å‡êÙ·¬[1WNk|”ÜK7)SúÐÓÛéÇ4OvoÛ)_Ož.c>YhëÓýßÖÙfÚMøjFÈ1Ñæß}¦›w™y‘Õϧ~&4€¨ öîs䎫Ÿ§Q£F‰Ža¡YRúYsï_ øi#»6ˆû‹þOÁÝxšàÆðÐï€aÆÙÍ5P8þ|[ŸfK¸åÚf›hÑFxýx饗2d1/Z´Èv§ŸsÝN»´ÒâÎÏ΄˜5ÃCßkš¦c‘¸ W§14ˆéÌ_yåoPsmÐwcthVˆ Ý_»´r]e¹€ˆ.×€G³fÍô¥5·/Ì?z,}PE»Ó¬-z^Ÿ|ò‰ Ðj0Y´Ëªà¢ë4ˆën½Þ ,k×V.âÖéuÿh25SN`ö—¢E‹zÝ•iˆ~¯ú‹nß³gO»H»ÃÒÿËùKV}üuÄòÚu ˶lƒ € €@nˆ[ÀãÙçž—¡=ÑcÊwßx¿œDÜ0ÊJíZJ»˜ŠTΟ祻í²Úù½İk·+]5!§O=ù„´mÓ:äºP µ¿{íF)R ðÐ_†5h©\|ÑEòܳ##mrÝÕ½¯‘¯'}r.þôSÒÊ Àè/ÑÚ=ËÓ-{ú8–ëÖú5@öÁûïz]¾øï×Í[¶ò W·,ž¢Áàn‚ëûáû)³ x‹¥ÏðHw=ú yà¡ôFòÐRéK{\u¥Í¬O<ÿd~¦è:} ] º^u?Ý^3A40qî¹çÚF\ ¬hpÃ_´W/Áûù¥¡kãŸK¿×uL-îüôºô‰k-Ú¨O¼kã¨vÕ£½^màÓŒwœpSo ÓZn3¢L·oÛ(›×/“”Ô‚RµÖ‰fëÐûíÙ³CÖ¯š/ûì“Ô)R¯nu9ö¨ºÖiÎüe²nÃf{¼J5Ž5žEm=V/4Oko•2åjJ‰2•2œÏÎ6ËÆ´%vyA“)rÂ1õ¤N*²rõ™·p…ñÚeŒR¤Z“¼óZ³b–ì7™eupôå½åþó6H²~õ|Ù³{‡]_­Jixl}sÓÚâef`ú4Ùw`¿”«PGŠ•*oÍaû?Æ)-£Ó¾ýæÞý5ÏŒv[—£ª%Gׯ#KW®–¹.5Ù{®ßÕ£ç[ؼï´ë­:5ª¼æ•²íŸ’R UªÖ1óÇ]™™ÏjÀ#«ŸOý\¸Ï¥6Æk¹ 4X§u.×÷·{ßû?gÚ(®ŸKmøÖ§ùõóç/úypŸO­K‹Z´k(ýLéç!Ür=Žþ_BúP7ôÿS§NµŸqw÷ÙôŸ_¸Ï™?Ã#Úöz®ê£:@·…4kAƒ=þúO0]ºj]úWÏÕM¢]Gé¹ùã_¯ÿgÔï0]¯nÈÐz6lP¿V]æ›~ŸhwºZè÷¡~¿èùùm½fgè÷¤žŸvA¦×¨ßYúÝ¥ûê÷ªXôµ¿^€Ñåúª÷׿^ÏSÏW×ëyêùú×û¯?3>Áçmž€‡ût0E@Èq xhC¹6˜G*Ymˆ÷×9ßô­{ñ%-ü‹2¼>âˆ#ä믾Ȱ<«'üWúö‹<¸îßO5¿äTÎpÌp ,/¿òj¸Õvy¨€‡®ˆöTºnn_]ªèÓ¶'Ÿ’ÞuI¨õº,TúÑçý»™¼üÒ‹UÆ2‰îÐûê^rÏÝwìï™XHíÚµ•',[‡~tÈPyþ…@‡à ï»÷éÕ³Gðbož€‡Gð‚€G:‡6ªœÙ$ð þ(ßÌwÜ.×þ'ò÷µoó//^"­Û¶‹°ÐCugå*Ôj"•¬tqwý 7ʧŸ}©ZýÚ«¦±ðìˆÛD[©A md W\£V¨õõMÆœæÌ™cZ6´iƒŸŽ¿¡~úÄöm·Ý–awí¦J¿‚­O£ëøµqZßÃÚh®Ñþ¢ï[ v¸.ÚüŸA}­ï{ýÌø‹>á¯]fƒk×÷¿Çðpc=<Ür­W£ŸIýðý\év(Ðï  øÏÏ¿­¾vŸ¿X3b†‡q¶çgZÙCfdbý>“ѰßXjƒ}Á‚¦1ߌ ›‰©~“q±ßtsµ×tû”šRHRŒ™f:œg¤ýÍ–žoÐõ0÷P+fwsš…̽(X¯Û>‹õ{×kö߻ǸìÛc3f ¦šŒsLÿúhÑÌÍX)’j‚N&øb·ÎþýÉjÀ#;ŸO÷yÓ©~Îô³£ïa}ÿ»âÿ|è27¯Ûê>Z\¶‚¾vëõµ–ìÎëgKÿê{Å—ÿóký™ÉðpïK=®6䫉tꎗۧzÿô»Nƒ]v÷0·_ôÏÿ"€ €ä¸<ôͼX`Ò¿£íJDŸ0Öî5b-P¹þÆ>2sæÌ¨»¼ôÂórþùçeØ.–@ÃGã>0]p4 ØwêÔï¥Û•W, žyø¡¥{·®Á‹3ÌÿñÇ<é~Uï©´ ø„ x,^¼Dοð"ß–¡_j†ÄÝwÝi½EúÒX²s´;>¸d%à¡u¬1]"\páÅQŸ׆ɉ_~‘©ì™às 7¯¿Ôê`åúd|¤2wö̘[ÃÕ£Ç:팳¢Þ÷/?ÿÌ[êÖ­r]ý…Sûý÷áGòâK/ÇTïG!_}ñ™—Jïß)«X ÚO}ãÆü‡ xýíwßÙ •€…fÂÅ­]}‚íí±ïÈ>¼*ÃücC‡ˆ \²ðÐzt%ZѽG¿þª}‚0Ú¶™YÿÛ´iÒáò+"îÒ¦u+f\GüÈ£QßÇýúö‘¾}n y8!Yx±Ì6]$µ2AÌízîj =îØãL–A™ »ê÷ð„ÿþOƾónÔ ÛY¿ã¿þêKÓ§yÆï·NŸþŒ {z¸QÈך¹7hàö›–P̘1Óþüq]œ„ÚÆ-Óà­q³[\ƒªþ¬ÒFÄp%ÔzݾQ£F¶¯ÍêÐ'‰µ+–?þøC>ýôSût±ëb*ÔþþcåÕõ3ñøP¹ìàS·n™›f5à¡OÛêøÚ¥V´rEÇË¥s§NR»Nm)múR×~ŽçÌ+}4^ÆnÚ¸I–˜ Çÿ8¦.iªV­j»” õtuvzm¡Ûi×;7\ÿú2n%–ñtðw{&%–‹Z?å»§ú|0ê*wÖ™šfçó™Ÿ>wdxdêm•k7¾é¦ÐÓäÚ âÄ@@ˆ ×€‡GŸ²m{Yû‡LܪSþõ/yk̶¿ÝPGÉjÀCëzjØÓ¢ˆÉ,‘z/¾ø’ ޱk™ìœw¤ RvšeÒÁôáKWeï¿÷Žüë䓳s)Þ¾zÜÆÿ:5jkά™f€ÝbÞ~Ùy¡}^Ÿ~f“¨A¼p™BÈm·Þó‘fÍše@ϘAs1n¨€ÿ›0>Û]Ô¹Ãé Èú¹vÅ5’Æ2¯Ûê}Òýµk/×A~5Ø¡µd¦¾¼¸}@†‡{‚Úer0ŸþþÈó"Œê¡oïL—ì|>õ`ùåóG†‡oÌ÷½’Ǧú^îÛ·o¦?Cì€ € €@nˆ{ÀC!¦~oƼ0ƒR'³h÷$oŽ~MJ™¬Šp%;µk×Ég…îo=Üñ"-×>ó5K#R‰ðІ²Î]»É/¿ü©šl­ëÔé ³ÿðùŒ­›%2<2ûÎÊ}Ûkp]Ç¢ € € _ðP¼d=N8þxyó×£6„g'à¡×ôÉ'ŸÊ 7õÑ—Ù*ÚeÌçŸþÏf6D0;ZÀCOB»Ìº©O?ùþ‡²uN¡vÖ>óG!SSC­¶ËâðЊ>ýìs¹þ†è*6¿äb52û™6ýúßµ‹±Q#ž‘æÍ/ {íYY¡Á)T9RÑ÷Çôi¿fh0&àZ€Gh·t܇Ê-·Þîf:½óŽÛ¥· $5ÂwF¸ÐÍSO “QÏ>n“,/×ÏÔ𧇉câYÞ}÷]3fL<«¤.Ÿ€Íð÷¤upfCðv¬OÓ#ØÅÍçŸS”{úwô½+bÉç36+2<ò~†Gùòå¥{÷î±½!Ø @@< °€‡ÚüñÇ÷¼ }ìñ¨ÇŠ´ëšéÌ&çHvzm(|ÌœÓ ¦‹«x‡ä¾{ïŽÚõK¼zÞ÷?ð ¼ñfôÆÃA&ã¤KçNY¾Tí;_É£•9³f˜î¬ŠGÛ,Sëõ^C·V¯½ú²4=÷Ü€º xpx3<<а/23 wØJ"¬Ðì+ 6nýs¡»ê‹/¿”k¯»>Úf1¯?êÈ#åÅž“:uêļOf6ÔnB–š1‘\÷7nêêpóÁSÖ§ »¸y]«›ØaéÉèÈþû]$u·ŒÍbvGú»Kl7>|>3fz8ý¼‘áá4òætïÞ½dwäÍ[ËU!€ €DHhÀÃ÷«¯&Ê€ƒ²<ø­«Ç?mÖ´©i¿GêÕ;¿8âëx<ô“'O‘þ·Üu,†à“Ñ Bß>7I•*•íªx<Üq4eÈÐDzå\»V-:äQ9ýôÓ\µ§ñ xìܹSÚµ¿øÐy}ô舻·k×Vž|ü±€mxpx3<<Šˆ/víÚ%LРb¼ºÁ«P¡‚è÷Zï«{FͰ‹xrA+/^"  ß}79hMæf¯ºòJ¹ý¶[ã6O¨£ëÀîC† ‰yÐõPu°,´@Æ 1Ý+šÐ‡i¤ ݽëóŠOÑ‚{äžë/”«úÍãR>ŸÑ¡Èð÷}’û—k×·mÚ´‘#MàŸ‚ € €@~HJÀCAµÿØŸþE¾ýî;ùüó/²Ô(ê©§ÈÅ]dŸ|?òÈú™¾OëÖ¥™A£ÏЏŸv5Õ AƒˆÛèÊ-[¶ÈûŒ“?üHfÏ™qûóÏ?On½¹¿è¸þrÑÅÍeÁÂ…þE¯'MüRêÖ­°,ÚŒ~ûó/¿È;ï¾'}4>ÚæÞúV—^*ÚÈ߬é¹R´hQoy´ÈzåÕ×Ân–Ùq0´¡óü / [Ÿ[Ѧu+öÔ“n6SÓAƒ‘—^~%â>Ï &—¶lq›¬®ÔŒ§Î]ºEÜ]Oß~óuÀ6±d¦ôëÛÇÕvLÒÌÓß‘aOGÍž9]´{¡x–¦Í΋ø}òØÐ!Ò¡}è±[µnñó«AÕ^={dêtãù=“©gbãù È»æ;bì;ïF;'TµWt¼Ü4¢´–ÓN=5KÝW…ª3Ô²e˗˸qʘ·ÞŽ9À¬]vëÚÅ~ŸEÓ)Ôñ²³L»Ï™:uª,7ç¬LáåÝ1Xº1Ñï“ZC?·3ì —égûÔ.]A®<éßR±X)÷fŠ € € € € ÃâðxðÁeÉ’%^F‡ËäÈÓ2]L†‡¹é¾ÐFˆÌÖçtŸZ¥+Ê-g´Îá_N@@@@@À d;àñÉ'ŸÈ;ï¼ãêË÷Ó²]›ú¢.“!xj˜ú·b}z?_Î!gø´9êT9ﺷÊ÷j@@@@@ Wd;àqÿý÷˲eËìXù1£Ãu×妡Æð³ƒyJã$§zÔ)]In9“,\ñMÆI"€ € € € €@¾ÈvÀ£W¯^v€ò|/y ÙN)\&ëÓr†Ojyꢞî¦0E@@@@ÈÁÙxtïÞ±;LŠ‚—áaÇð0óæÏ¡±<Ü|ðÔ—é`·g}º›s8ü>Ã/éƒ?¾œ € € € € €€ˆKÀÃUÆT$p #r(êáZŽÜâòLs|®@@@@@Ü í€G·nÝÈð—áá–›wBz¿Ë\88e}zfLöy† Üð=Æ9"€ € € € €—€އ2<ÜâǤðæÍ Íüp…õÆÃâ2@r€Ïˆæ×¸³`Š € € € € ƒ²ðèÚµ+.SÃLËènÞD5ǤÎì`}N÷Ñ‚€GþþâÔ@@@@@O .¯¶8¿Ð7o.{öì‘=zĹöÄTg3<\Õ2Ì _"ÃÁ~®Ü֮߫\3Níã¤bñR²iÇ6ùfÙœ\wþ±ølùŸC÷ƒW € € € € €9V Û.]ºÊhp™ qšöìÙS.¼ðBÙ·oŸtïÞ=aÇÑŒŒ2eÊÈ Aƒì˜?þ¸,]º4Kdz.³Ã9äÑùÇ.¼JJ.*;÷î–›?ÕóêÞ°©[©–,ݼN^øí sõ3[î<û2)U¤˜|½d¦L\<+=&zlAÀ#Ç~{qb € € € € €€O ._}q}©AÍðp¸VTYõêÕEZ† "3fÌÚ"¶ÙÐcx˜}óPf‡ËŒr~w)U´¸lß½Knýò5讳ÛK­2e㎭rï×o…ÌüÞ¢·LI•WΗÑÓ'åXŸQ-¯õ®‹ € € € € €9W ÛÎ;{Oö{cW¸'õ³9½òÊ+½€‡voïúýõU­ZUž|òI{§yä™9sf–Ž0†G6¯ß~ÌÀæ9m>mþRÙ¾y‹+]R*SÏ;¿;Ïj'uÊW‘õÛþ–û¾yÛ[î?ÿ§/¹Z ¥”©‹çȘ?¦ØÌÿúœr½Ò‹€1<"mxp]j9³ÖÑrT…êf<ŒÒ²fë&ùcý ùuÕ¢ {)XHN«~”]þÓ_ómÀàŒš äØŠ5ÍëTÛ…Ô' ¦É®½{2ìËu;«æÑr|åÚR®XIÙ²k»ü¸âOÙº{‡T+Y^VoÛ( 7®ñªªøO©\¬”¤íÜ*iÅuÀ‘3Íþ­œ"eÍØÿìÚ!ãÿüÅÛ~ûž]2oÃ_rrÕzrÅ gKjJŠ,^¿Z~\µÀÛ&øºâ¤ªu帊µ¤Fé òÏî²ôïuòíÒ9¢õùK°Ïî}{%Å\“ú”6™(ËL[«Œofʳ—^—™ÍÙ@@@@@à0 d;àÑ©S§OðGzB¿E‹ÒµkWI5ôÁe×®]v,ùóçËUW]e3<öïß/¿üò‹œ~úéÁ›KZZšôë×Ïv{å24rË-·dØÖ-غu«Üzë­²eË›Yпÿu»íÇ/o¿:S!ø:Cfxaq0ó£nÙÊÒ÷ŒK¥X¡"îPÞtÃö­òÄ÷™n¡¶yc`Y¾ªÜÚ¤Ýæ×¿ÊÉÕë™ýo}¡üC¦| mÙx辄9¾óÒ16j”.oÎ¥•W# BßÌÊ-ëeà·ïyõÞßì ©^ª¼¤ýc29L×UZ_¤ÁÞýûäõß¿–«ÿu¡¯ÖÀ—+þ^/ƒ'¿oïKyté{f+©R¢làFfn¯yO¼1c’üdºÄrc„ø}žþq‚4?êdHªaÖ§—•[6˜ó×;ÿõ0ýtyóïO´ëÉpR,@@@@@8lq xdæì5@Ò¶m[»‹ V®\)Û·o—5jˆf}hyöÙgåÛo¿ÍðÐàˆ¿hf‡**V¬èL>ùä=z´·™ xhý𙡙úZ39J•*e·Óå}úô±¯Û´ic£—kÙ¶m›ìÞ½Û¾ÖÞzë-™2eŠ7éE¬%Í`ß]ÔÃ6²k}+6­“´¿7IÍ U¤r©ôþ&S£ÿg/Ûs×mê•­"·Ÿs™¾ôŠ6ý³UJ+!šá eÕÖòð7ïxÛD{¡û=qq/3¦FzðäodY½y£/RDj”­d31´Ž›Ód F¸r_ÓŽ6ëbݶÍrÿ¤·íâÿî"åMv‡fo¨ùæÿ¸Íe‹yýÎì)rÍ)Û ]±Çh¶™¬ Wf¯Y*cfO¶ã{×êÿbÚŽ@@@@@¯@¶W\qEÈ'ãµ1:ø‰yíZê¹çž³Ë5hqõÕWËš5k¼íÎ<óL¹çž{äµ×^“ï¿ÿÞfx¸€ÇÚµkåÎ;ï”… Úí5àñÎ;ïH¡B…dóæÍríµ×zõ/^ÜP4H¡žûÏãÑG•³Î:˪ë èØÐ€‹;Þ|óM»ü¶Ûn“üÑÛ¯B… RÄ4þûë u}º>d†‡/cÀí§™ÇVªe÷ð[ÏÉÌð2:žs±ô¼(=(ôå¢éòÁÜlãþ¥*Êÿîh÷Ñn«ž™ð¶LœöƒÝO[÷_éÿ°T-WÑ®¿öãQ1¯žOs.Ú•–—>'ã¦|e‚ æþ™?:sû£R¾TYj|?Ϋ÷Þ&fpr Y³yƒ<ðÝ;vù²Ÿgʈëï–úÕjÉ:4¹ê‰{¼z +*åëÕ”µsÉø†K¡‚å«ß”'ÇöŽWªJ)W§ºôj|¾œfºëÒó»ù…ÇdÞÊ%^=•ËT×ohÏwnÚ þã3øèÊ×ÉGS'Ê÷s~—¢…‹H…jUdk‰ïüc¹Ÿ<,3ÿ € € € € €9^ .X¯²wïÞrá…éÝÝxãvlŒbÅŠÙè={öˆvg¥Eƒ… ö´hÖ¬™]§Ëµ+, Tè ã'Ÿ|²}­Á W´.íê*Ådh ¢ iX׿åË—·Û»Ìއ~XæÌ™c«ë^yå[Å]wÝ%¿ýö›«Îfžh`%–k†‡6¤kƒûÜå‹ä–—R•+Hj‘²gûNùgÃ&yÓ*˜ ƒŽSÑÿÓ—í¡ëš€Ç]ýž*š @¡¢E¤p‰b²gÇ.iqÒ™rm‹Ëí¶÷M#ëLWS±”Qfœ ÍÈX²æ/¹~ä@)^¶´*^TöïÝ';þÞ*/ßø€T.[^–¦­–G~øÐ«28à¡+6,^)C:]/õ«Ö”uo”ÿ{y¨·}Á"…¤d•вyùjÛg  x|=ëùåÞ6EË–’’ˉ;§ÿýòŒüï;RÚìW°XÙ¿{lY»A^íûT*SÎdl—Û¾xÍîï÷ýõy{Ò'^½úÂSjWXmæùÖ×GÛ„õ € € € € €9@ ÛŽ;ÆüÄü€ì åšQѪU+€ÐF÷¤½6þþûo)]º´ PôèÑÃv5¥Ë›7on—kC·×q=zõêe—k€?ÓD]5¢ë5jd÷Ó}‚ËСCå×_µõU­ZUž~úi»ÉÀeæÌ™Þy¹ó‹eK†G9ÓýÔ‹zØcœ0V~Û¾J šÀ…«ç–¤ëqgËÅÿ:Ëf.\7áÙô †Ò•¼€Çm¯?-ë ïµ ·_­ýÅå¾v=m½OùPl\m÷sëCM‹,,ÃZô¶ûhvÇwëH±2¥ödº©ªTºœ x öexÜwv/ÃãþoÇzç÷Y—IÝJÕ$mËf¹gÒo¹ÿø#ZþǺ>yÁ yó©Ç+Y¤˜hwVZ´›¯öš:tNÿ19'¦¿ªÊf€wÍÚÐ.½nøïóvÿ#|>ýž"«öl•båËHÚ¿o¿V )SCžË¼ žð°lüƒ € € € €äx¸Ø¥•<\¹Ë<ްM&àñ–[0yéµ^Àã¹ã£œTµ®ÜpzË€íÃÍè}Õî»´ø}úd´üeÙ-/´¹!»U°? € € € € €@²ð¸üòËc~b~ĈR¥J;ø 7Üu¿ž={zÐðghC·vcÕºukÄð¯×Ì5ÃC·™`}°ŸÎ×/[YîhšÞ Ö¨ÉËï–gp|^W›á¡ÁS?ðÖßwÎå^Àã¾oÞö–ßmÆöp»¿ááßÍ× )çé÷™ðÛd3`ù_Þnl‘Ý;wÉÞ]»eÏ}²ªø>»4ŸPׯï‘HË xØ·ÿ € € € € €9^ .X¯RÇÆÐ17tŒ.]ºDÝM3!ï‚^³At¬ -/¾ø¢|ñÅOökPB»¼Ò€È–-[D3<\ÀÃ?F‡{"ÿÊ+¯ô2<ÜúsÎ9GÜ äO<ñ„|õÕW¶ +ÝGÿÖ®][t¹ x¸ íæêõ×_·Ë_zé%ùì³Ïb¾.w>:ucx˜ 3CNÈp}ê¡™ /]ÞW ¦”Û·Ê_¼°ŽSâ2 6˜q0î˜ø†=/Í`ðô¿2Ó¥•ß·N© ¾€ÇXðŸááÎoÉ”x¾ýMR¤Pa›Er³ }í¯[¶’Ü~v{;Àø’´U2hJz†‡®¿ïÜŽ^†Ç½¦ë*çpËé­äØêuåŸ];¤ï'/yËÝzoq3cpÌYµD†ýü¿€ëPŸL@¨pÁBöœnø`¤ìÙ§ãxòtçWÍŒ-²aï»4ŸPûûýB­'àaßzüƒ € € € €äx¸NpÀC×WªTIž}öY»iZZš§cùòåR¶lY;Pú¥—^ê2hÐ /ÃCÇÑsÓ`ƒî§cŒè9êêZ4KqéÃkÚÃ?¯ì­«ž mÎ:ÏnðÓŠ?å¥_MðÇÌ¥šãßqN{©W¾ª]÷ø‡£ežüm‡ë¶ ¤e2Ð<–š€†+Z-“áq¨K«±²hË¡ ÿñu7¯çrA¹#¥ó¿[تvìÝ-3V/‘}Æ£^ù*R­Ty»\ÿYjº´8å};¯û»€ÇêÍä~ðÐó×Òåè3å¼ãN±¯_øå3ùyå{¼¥+ÈÊ-éY):.H•²ì ã÷}ù¦¬ß¾Å\{ªT(VRÖlÛ,甬-=.jkëРÐÈÿ'ËþN¿žb&8szÒöø3e—9ß;>=&w~Z©»~{€ó/µ»ÉmÂ@@@@@  d;àѾ}û€'ïÃ=1¯ËwíÚ%Mš4íÚÊ *ìÝ»W .lëÑå:Ö‡}º xlÞ¼Y\@C"®+,ÿöݺu“víÚÙÀ„<´hàBÿj–‡ŽËá/[·nµ˜÷ë×Ï.ÖÇŒ3¼Mô8šuâ–èʱcÇz{‡yQ®kS/Ó!Ô&.³`ûÆ¿eëò52¨G9©ÞÑ›êÀÜ_NûAÿ5©rl})\¢˜]¯]V¹€ÆC_¼i-÷糖–?xqw»h  ˆ,ñe€¸íÜñݼNwlÞ"ë.—¶•sNü—”/]FvìÞ%+7¬“‘¾)C®¾EÊ—*-¿,þCžŸþ¥w}÷›.­êT¬*«6­—ûL†‡­ÛúMrQ¥çEí¼à®Ûmº¥ºÞŒG¢A”KVÊ-»ÊÇœ¤«¼²xƒ1ùö]Ùù÷6I[°T:5½Dº_ÐÚŽ÷ámtðź¿7ʸ_¾‘_¶¬´K¢ùèF¡®ÿ`uv¼þåËÒÇ7ñoÃk@@@@@œ'í€Çe—]Ш힌w—<¯6¤wkT´hQiذ¡T­ZUæÍ›' .´ÁŠŠ+Ún¯vìØ!øÐìŒÊ•+Û*ýõ…[¯ÝOmß¾ÝV7n,;wî”™3gÊ‚ ÜiÙ©v¥uÛ'ÿÍ ˆlÛ¶Í|®ç¥“¹sçÚ =W-þ㇚7†Gpf…Χ-X&;LÃ~‘B…ä”cNz5jËœÅóeÆ¢?eÿ¾ýRªJ)S«júù™í÷îÙ+«füiæEªØ@ )|è|Ìú}fý_ný GIÁ¢EÖëŽÞù›íýó›–þe› 5WgŽ“b|þûà3ÞHª|øë·2aÙtï|6/_-[Öm”’ËIùº&¸t°þý{÷ɪéóLvLªU½ŽÔ¨\UÖlL“kVJ¥ãêÙýwnÞ*i —™q<ŠÊñuêK©R¥dɪ²aÿN)[Ûdš˜Ù¼blY»A«•’E‹K£ÇJÉb%dÉ_+dñêv\âåËH…zµìöÑ|Üù…ÊôÐã…ZOÀCßå@@@@@ ç Ä%à¡—éºr—i^»ŠÒà‚"üEÇÏÐÁÃõ¯"‚ÚXïŠÖïÖë~UªT±ùº^·Ó ‡®÷í6«dÉ’²qãF»X+Úe•­O¡ö+W®œhÀ#øø¡æcÃÃÏü£ûÿ½z½lYµ. ó %5EÊš@G HpE3öîÞ#«fη‹j4l )… ¹Õv×௠«›õ©Aëé™ë5ÛøçO¯Ù@¦þ1]¶nÜ,{Í@æ©&S¨X¹½Í•rj­ö8w¿ÿ‚¬)nªûo2­6àQÖçGÀÃ8R@@@@@\ —€‡^g~Ïìp×(ÃÃtƒeþ„Ï0ÈYëÿúmŽíÒêÐù:¿’Õ*I3¶†éwÌw=‡Ö‡ÎøÈë_»¼.øsŠ € € € € €ÙxtèÐÁŽweºÀ¡ ˆ0fFÆŒ„ûý¾]»e·GdÿîÝfÌ 3ž† n¤-,…Íx"©!ÆÞÈi矈ó)˜ZP^j߇·6 € € € € €@.ÈvÀã–[n‘… z.Ó!¿NËum–«2;et˜ÞªÌ¶€ù“›2SyþuËU‘/ìš >Æœ" € € € € €@¶~ø¡¼öÚkH8”áá<\æóé /¹Ç£S£s¥ùѧðÞF@@@@ÈÙxè5Þ|óͲhÑ"ïrÝ@ÝÁS·Aðr7ŸÖkÀÃeJ¤çLÊœ`^er‡G½rUåÁ‹ÈîpŸI¦ € € € € €@NˆKÀcÍš52tèPÛµUN¿àDŸ_Ù.MÝœÉ`Ž|°ÿ¥Ð™¬Ï)>õÊW‘ëϺT*—,›è· õ#€ € € € €ÄI .w.ãÆ“)S¦È’%Kdß¾}féùnlÃÃå0¤…qh” æÍûÁE5Ì4'yL)(5ËV3j#-Ž9Õ½¥™"€ € € € €ä¸l”÷t¼Šýû÷Û÷EÞ¿ÚèWHÀ#º[ p¸xî;Àñ@ò“íiùénçkÍl[IÞ¸êÃs92àñÇó¤ûU=¬È)ÿú—<÷ìÈ,éì3hW^ÕS¾ÿá)aRÌǼ1ZN:©a–êÊêNg69Gö˜F *$_õ…+VÌ«jØÓÃåéáÏØùÁƒHçN¼uñ2ð*ä ¯¿ž$}úõ—L÷;í/»LlHÈ£þý÷ß2rÔ³òÕįeÉ’%Þ6Ì™E·§‘ý·Ý~‡Lúæ[[Ñs£FÈ)§œâUšÎ,^¼Dž6L~ûmš¬Y³ÆžëÙg7‘7^Í;ïx¾ˆäÏãÄ£.ñP¤+W6lyþ)6l”íæç{ýúõå˜cŽ–R¥J%4×>vì;òø“OÙ+¸öšÞrùë/‘~GôoÇëÜ/À{!s÷0šWæjcëìäôÿ'GjOËÎu³/‰ˆµ­$QÇÏoõÆ%àÑ´Ùyòy¢7;åÜsÏ‘'ÌVñûïÓå²—Û× 6”ñ~¥ª5Ðѵە޾Ú_& ÝøæmçGÔ?Ê«qöÌé6𢠶mÛ&'žÔØ[W¡Bùõç½ùxxòH‚Àå;ɯ¿ýæ雯¿’:uêxóúbëÖ­òïó/4 '–ëÌÜÙ3Ã6`A¦®êÙK¾ûn²ÝçµW_–¦çžëíŸÓ~Î̘1SÚ^ÖÞ;?÷B|xïݱn6®ÓH>q=P*#àÄ,V±sçNчh‚KÁ‚¥H‘"Á‹™ÏDZÿ‹òè¡öì®ÿ¿ëä¶[o 8Óp¿#lÄLžཹÛÍ+sµ%~ëD|Ÿ'þ¬c;B<ÿŸüþã¼ïÄØŽz«×_{EŽ?íi¡÷ÎýKý?ëK/.Ÿ}ú¿?£]©þ>~Ñ%-ìÃÙºíé§Ÿ&#Ÿm7ÖgS –¶’l‚Ý}q xøÿÃæ«;S/5àñú«¯Ø}âÕ³hÑb¹à¢‹½ó¸ñ†ä–›ûyóÉxá·ñ<´ —3Îlâ5ø5nÜHƽÿžwJñ2ð*ä9X`åÊ¿äÍ1cìÖ5ã:]‘ƒÏ6ožšˆ]ø~Ÿuæ™YºÐ~ýo–ñOðöýý·_3tÙ7dècòÜó/xÛœpüñÒ¨ÑI’šZPî½÷n)˜šê­Ëm/rÚ{9Ò/*9íçL«ÖmE×\iÖ´© –ÕªUS®îÕÓ-ŽiªO.5ÝYjéÖµ«Ô¬Y#ä~‘|Bîp敀ǪU«¥s—®V²{·®Ò»÷Õ‡Q5¶C÷éÛO&Mú&ÃÆš=üãS3,ω þsíuòÃ?Úo¾™4‘LºݤXšyBÃF™> Íz¿ø¢‹Bîצ]{™9sfÈu‘öï×WúÜtc†M>žð_ék26c)ÇsŒ¼øüs¾ký¿ÅRO¨mü¿#†ZŸS—Ek´õÛøGÌ©×Ãye]€÷Bº]¬¿kDóÊúHÌž‰ø>OÌ™f¾Öxþ?ùõÑ£åÁ‡ûÖ#â@IDATdþ$‚öÐ63m;‹Öž´[ž™ þY¯tëƒÝ™-o½=Vî¹÷>o·SO=EÞû¶7Ï‹ÄÄÒV’˜#çÏZãðÐÿ°ë}vÊ…\ /<ÿ¬­"ž0£ßxSÞ6 '5ÔÝVVÿóîÞl;uöžH~û­7íÓÑnÉÇ¿]NxW+W®”æ-.µ¤;w’»ïº3'ðF<‡pÝi–ÉÞÍ ¥×Õ½½ÏÄßO•’%Kä†ÓÎu瘀‡Ðü%ÜïVᾃAüuEz}ûm·Êÿ]wmÀ&þnBÜ j4<ñ©T©’,\¸Èfsú35õüǾ=Fô!Wâý;¢«77L£5ÚFú17\ç»ï…t«X׈æ»|r¶ ðÈî÷yrÎ<úQâùÿä1o½-÷ÞwôƒFÙâãÆyíf‘ÚÓ¢T“kWÿ¬×Ÿ¹>þ(S×sàÀ¹Ødw,X¸ÐÛ6"¡/bi+Iè ä³ÊãðˆdvÃM}ä“O>µ›Üs÷]Òûê^‘6·ërR#LÔ“²AVÿ3›— ¢±ÉiÄùñ–ÄúKHvmtêúGíUóæè×¥I“³¼ùÜþ"§½—#ý¢’“~ÎÌš5KZ·=ôtÒ¬¿›Ù’Y~;ðÈ2]ÂwÌuii¢]V¸róÍ·Êüùóí,§ÂT²ðøáû)RµJ•D}zuµÉˆš4i’}À õ³ÓßòÌða!88@˜Kéü e‘h·»Ï?7JþuòÉ{èù9J48âŠ>uûÁ{ïÆôCV~GtÇÉ Óh¶Yý17\;ç(À{!Ý#Öß5¢yêþ¹à€G<¾ÏÿU¥ŸA¤ß#âyŽú œ>çÊ’E ÜK¦AþŸõn•?ä–Ešj÷ÓÚµ’¿ððkð:¯ðHðÌêfsRCT‚‰¨9à=ë/!Ù=Õ={öHƒcŽóªÉkcvððnm¦^øÝ‚»xÌTE7&à‘µäì£íÇù€ ~6hÐ@Î8ãôä8ŽGÑñá\×A<⛪ŠgÀÃϱtéR;ö•[æÏŒwËü Ù x<3b¤<ùÔ0W­„j¼óVš/¾ô² ~äQoÑç¦?qýlG+x"§žzŠ>½ö-s|ýÌÉ <G¢þœ§“‰|gÇ:‚ç1]sª'<Âøi(³l×îÝÄ[E¼¿Ï4?WB¹üDôûà[¯±ó»ït‡ëç>:z—J_G>Ösÿë×­‰Ô£í—¸ò*T›£<€'ê ãê?ÿüKëãSô7ÄC‹K„,P/ÉðáÜ8ÒG¾¦~þ}:¥ þæÏ?ÿBüäóuýžL뽨„ñg-’ùü‹Ïuçœ}¶*xAAË“Åæ®iœalà'xƒx%ðÄ‹2öX»ƒ€7xÛàQD¡¨=¯ß°Aáàé¢ßïóbâë^#êõâ‰Rà_>W”tr¢Ÿ\#ðxšŽLCÉ-— ÿwÏ‚îRÕj×Y¦ÇœÙ/«B—ìÝYlüBÏG÷=Í«ç‰û[µl¡Z4oæ¿Ç³ÏKØdöÑÞ}ÔÈFéP=º?¤5lhcðcDÍœù²êÿØ@…Ô¥êÕ¯S]élLº\š>c†êÐñm»ù¯½¶)!loïЇC‰âÅÕÔ)“lP Ï??Ü£XØ:î1àz²N>@Іtt'jîõÅÕ$ƒQS_šæ©?Çß¼e‹Ý†NfŲ¥¡Ì Q£Ç¨^dI¯Ï!&ì¢óâÒ³@ÇÖ¶}{µëŠ;´k«.½ôR“;‘q™ÏôÂë}ÝšUê˜cޱ^0Àl¶mÅ@úêÙëQ}ÊÁzÚcÀbªS§ÕÕ•*ºNêbªW¬TYÛ›jþ‚êÑGûØÁ‚àn]»$ìÀ-Z¬u9påÁ<´èÃ)YòJnmͼ|au_¦LiN6Çà§O"»¾YÓ‰ã_T]ºvSS¦¾ääJiå`À ½/h!Õ§_{}÷|& >ÆŽ¸ƒ_,N¬P~„8ÐÇ%Ræž,.¼ŸñKר…é@0~ÌehÑò.ýÊw º ©þD°rù›Ü*Ô|ÿêâð„sK—.¥Ž<òHE$sv <Þ|s™jxgc›ë×y˜öœ ’ŽÀƒ "jÕºI ø˜M3È€ À„McîéާõÚc¿x‚ìPç R¯¾öº¯ô]½í¥J]ÏøA0&ÐóÞ}Îwí?X7<1dˆï·ÂǤ»ïj¥û~„1Äç AJ˓י¸3ù\òÆ ;ó £Å8}êÉÐù6ØÝ×®} ŽõëÕS÷Ý{O¤Í &ýÕ«×(œæ…Í9c9ì®{¾.¿§MkÕžÖƒœ°>zlÐ`Ïú™»c³Ðƒ´.>†b.e¢-¸qºï9µžø\ÓÍ3çk /ÔGGšóÏû ¢Í˜›è×WDo^5Z!^³ç~ÖY-‰çusïò ŠCØDý9üd"ßÙ¹Ž@3E© <øwëòÓ\XƒÛê«æ-[ùöe]ˆßÒ¤Ic=ŸÅuO=ÜÃ÷[?噡O…ŽqéôQðäc=ú>ÃìÙ£»jÈ6,Å5|øÍ·€;oøI~áÀ«@|È… ù9«²eËh^$ÖJA”*ÎÄ—î8‚2<ýô3jöœ9&JÏýÀ£ôR7Õ¼ÑcÏ_ðM¢yfس¾¦ñƒÍXçaƒ„ÑO …òнf(ˆWw—5yâÍ+…Nj?>x‡öíTËÍíºÖ¤ÁŸóæÏWýú?¦× Üf¬ÿ»‘Ú‰Ë.+joà€€fͪ·\¯¹ö=Wî›âaðmΘö’ÂI*—°@iܤ™kíû޳/µw—yª¸ð~Æ7Á=–|ænAƒxv <ÀéMWjø-dxž_{eíHŒ?¡æYã¦Í#õ{X0þ¼gaÎûzž7'+ðÈD_ì2ô+”ßÛßòúGŸŠÀ#ÜršÀ“ѧˆ©cvƒñºãæY/ÏPE æVÚ Æ+˜‘QÆNLPÛ´¾;.Ž0‹dØåo¼±4pÜ;Nm>ñøàÀëg \‚’“H·@:*ß´Yó¸,â ²'žxÜ—Éc<¹ô2cô<±0»Ÿ&g7áôjsæÌ Lªæ7І𹦠œ¬Àc]ù3iòR ¢>¬jÕ*AÎÖ~Ó»ïªÎ»j†¿µd†J•*ªnݺj¦3³N(ðÀÆ¢çhc‚ q4G;óÌ3x3§Zd;ì;Þÿ€¯PsœÞT‡•+_—WÓ/¾øbµ…6Ûpþüsj º—-[f­q2vÔ #’zd§Àãí·W©z·5°yt™äœ ’ŽÀ×Ya>¦siÃXÐîy›™ Ù)ðHfÎï7ŸEÿ á÷ëóo¬ÃmlqOôºL[0A8ñyƒ[—©Îëxü™0sÆX¢ø^G:ÙJ—Žó6bä ªwŸ¾qö®Ó^šyÇ*N´_xq!Ͳ¥KâêŽü»Àûê·Wú¦Áy“&Œ÷lìÂZcH"B^76n¾ŸN[H”&Üsr=ñ¹fXYøZƒãfNd°Aq1{ñ-µ#½[Q¾eŒO?ùD_#(Mcïò ¢<õç™Èwv¯#Lù3ñÌn„Ϋé†Jƒy‹/§ÛÂõæþðçžõm'éönZ~ï¼O{ˆæ{<Ú[{ÃX´pþë¡›‡Á°/[¾¢’ððA(†¿½a#¿¬ÄÙ=J•+W6Î>™1ÙåÃ!²tÇ‘•o½¥nkpG\¾ü,ÐÐ︄yø±~‚Wî|Ä™ tåÊ–õ\1ÊÓ1fðZq²Öøfq?wc7‚æÁÍZ´Ô¯"ð0¨D|¦2™ Á\ćˆvÈa0ä’.,Là‡Ÿ¼¸'<†ÐN^ìX1tsíÚªúu×êÝøXŒ-^¼Di‹¨Ûê×3¯‘ža“Ù¨ž`Õ*Uô.±Ãàm¤ºð‡¯CïNà$°%¯¼’&}Çé]êØ¹ º»uÏn( þ`üâØÔîw«—gÍVK—¾i£Âƒ'Îô6öX4áh|ºæƒ8víbbxG£;i‘¹\{óÛeÂc=:hCA[ãΟØÝP¬øÖ 8bçŽbïÞµ[€¸SÜ0KáþÖÊåGw"“)01m§8*PûÅÕ‹HA%¾NnûåÌyîqBuÅWБñíjÞ¼ùʶtÉ"uÃæ„‰$¾WN:OÄ ýƒKß|ÓÖ“ñã.`ŸLÝÏ|yíú¥#£tµ2vÜß±½6ã§² ƒ:»¿Ù LÑ7”¡Ýž`8âê'ì*5Úý ªLmè2ºRð:1áBÏŽ&‡fL›jëáP–ZµnÖ»ÃM<øŽJó§À0§À7fú ø Z˜ðî3YÆÙ+K–òDƒñ×@à[ä„ øn\º¦J5_&=»‚“ ãÇyN3ò¸ ³™ÛÁ\ŸæT]ètjvw›4õŒ©èÏp$v†Àà³- %+ðàþMü"¹ì.„`4ˆ°pL'f9¹u7\Aänl:á“'8…;fLÖµE¨Ë‘#†ëãîÏÄšyò¤ ž9B:mÁ¤ôÌéõ”ÊZƒãeÊ~èêJ•T‰ÅÕ‘4~¯ß°‘ÕYã–ñóöÊqWq~ üá[oãlº ×dÏ›¿À3ÿoÓºµêо­‰2ÒÓåDx$êÏ3‘ïì^GD'¢§ìx˜l`ÝŽü¸b§~! 0¼ø1s*|çèOðïúa—¾•…oˆòëo2ÑW˜|†=ùXYÍ[´²¼.8ô‹ã¥KÕ³æGà>ßÇ2Nžg¿þ¬?’øzÜð±Ez4QŒQgçÔIÚ¸PŸtU¯?`„¸ðOZ_?A–ƒBEŠÆvíÚçÕÅœ3ÖêΤg"â~i!îñîbƒüÐ`âñƒ—ÅK–xÒ¤ËãçãO>ñ¸#M´QÚñäñ‡êœ=~i!åñCw)Æ“ï <Ñ}‡Öü–¯P)ö×ß{ârËV÷& IÝm¼aß;;¾Y?L鸤ɦ~“W·?ƒ•y’€Êãí¸s—n¶\è‹\¢ ‡uG<¨s—h0‹ñ~ þh@öxË.O<ù”Í Ì©Möl<4‹‹íÓ`†§_{ `A»|=q‘Ð"†¶Ì õ0pÐ?ÒíĽÄhaåq§]|wóâöµÓ§Ï0Nöµ-Û†LõÅtâÀ–„NžÔxŸì’¥œˆ›)úÓÎP'AÄñô³¿üòkœwô7&> ñÅ/ýLØ¥[>fÓ-[·ê¼ÒuJ±Á?nËò¸å0eÄsÒñ«uó-6 âûa×®ØÊ·Þ¶õÑ¢Õ]Icôýºöî¼’t¸^<ïÛøXï7¾òvùüð1Ìiýc¬q ã&æÙ3c¦Ý±Y³gǰK—RY#FI“¯ÝoÚÐ s«J•«Ø2;NèŸM™ëÕoà;Wqçé¤ïG#æˆÃo^aâÇÓ]#òºÎŽù®'£/tBÐæëb ÅùDûáëÚAíñÃçi$t[3À3øaXx"t^&LœhÃÒ)wÇ5£ÖݤA×<Çù[°`¡õÇ¿-Ìx]`>Ž~Â%bšÚðHgÆÌ—=^Òi žˆ|^rC=™lG]kp¼€'ê€6zšhìsõêÕÜióœuƒaã¦Mwô_tͧÇÖunŸAà<~½dº?ÏD¾÷Å:".ɸÓÉKO]E ÆOãü´#ð™Ü± ssÞ‡Á¾sˆ{²>_#]N™ê+xœAf>ÖÓ5M1Ú°b±£AÁ´=ú7”ÿX«€?jÞýx0Ü?ü¹ëDúÍ›LxÒÆFOx=ëdùpéŽ#tÓŒ'~üôXšr¸ã.Ú‰qÃ}]ì)'^Àcåþ`Fþ9…ñJüxQà•ºäŽ½~ãÚµÕkxò‚zr åâcœ)›ë/7¿ç ‡+ à€“^[‘˜œºÄ;&.ðÀ`ÇãúõÜ ú¤uzbÉýÂE‹}ý„Yò4ÜÉlXÍQˆ“r7¼I—ñíNÚy'ƒ¸è4… ÷D\tjDÿ“Ä/nR`^6º·×8é'ÿp°Pp™âÜ3<.¿ŽþùÂÔÑÈãtÍœ1‡ w¹ºÆ?ŸØ»™ ðÆž—'‘Àƒ$ã&XÜ“O 'Ÿ€¹"0ßIL,?þ|ñê –Ü ŒAè.\¿ÉÔ½Ék&ñ¾øf]Li‡ŸÉ¢çé~ŸAþІx[@[7æw£ë#ŒSÜ>ã‰LáÂÛ?.3 ,ÂqÍ.ðñ[„"Môk?Ú‰kKœ±xP…ÉnjÚzó›FiË6áC¦úbÎÐÏ´À#'âf åÌ]·2~ðäø _¤vÜÙš1†»ùf1Fp⌳°ïß²‰Ï ¹Û˜“e8ƒ ¾mûŽP†-g8Ò ¯8¿\à˜ë<`*›òà#ÝwîטÁ¤6aö…ÀãIê[Mzt­TÂü™|†=ùb3ÌŸqÛúþû¡éŽ»w±Š&&œy÷Ú·Ôµås߸¹OÚÑû’˜®=ÿ& ð€€ ÌƒMÕk«Çh÷b\87žTßÓ-ÃÛ«VÙ¼B@¡›®Ly`æîÆ8ûç£õbX{Ó/cõi¾×DOw^$ð€ðsÞæP0ú\rý˜2‡=ñÝúòÃÿ~q`íA×_ù2"ýâtíxÿuSœ‡ûî2%°A+ˆÐÿòrAØhó³ š[À/ŸÐÎN\?9Ó6G¦æužÌ¤ð‚u³ÁÀÙl¢4‚V`I÷škýäBJáü‚Ò³b×_AäüÂòy»àA ›:6ke´y`Ì©Oß~Ö„(†^yõ5kp~ .ã—oŠqך©¶wØ37Ô“É﵂ õÇy8&.ó£ÏÔ±+ôêÞ£—uÃÍ ù=æÜ|ÔVMšî3Óýy&ò½/Ö.é¼ï Òð#·½ùã›tÁ—┩¾‚Çdæc=._ã Ñµö{À7ƒv‹oË|?îºÂ!ã†'ß ìÆÏùè+9o0]>\ºã6›±ÌÊór€×kÊënè¦Ö ~Ü®<.€‚ßtakJÒKdó„6ÁÉݤLz£¹³ÇìÎ…ÀËËK”+Ÿ}öy æØ©k&žîná "äŒÙûÚ¶‹¹;sMÂç §d~’H—+MäÒk·JfG)OÃ5£33åÄ“ÇD"âR¿EË8vwq'Šò”©6¯ÈsP%(w"äÏØ\ð x`rF˜sœ9cOò‘N"!õ`Àsì0yN´3ùå‹`w‡C²uø¢2‰yÜÙñͺ˜º'WÐÄ(2xṘŠ~F÷Ç¿>p¢Ž}—?ñ¤ ý§LàuÂÓõ3ï/‡»¸tóÆwoa—µ!wBð>1&Ã;§y~÷ÝwïQÛ²'Pš/¼àm ÑÆòœIGNÇ3w£ <0…ßaÄ”;v|àiXP„_¨„MdÝ8¢2\“ñ‡oÁ´g˜Ý°\à1köœ8wøÇ.x]ýæë‡Ç»¯C‰gòG×%ÌÏk™×aŸdìßÛ¼wgã}íÚÇå‘»ƒÙ¿“údâ‡_þMà´g6׸±¦=¹“l¼Qý§[>F&zwóf[×tª#Ó ä1y¦«­˜=)ÇÕöhÿÆ.êÓýnƒÞÝy%˜±˜[ñà5y7Ï ¾Š·Kã7ÑãGAЋy'gŇ18hÎ?oƒ™xðÿ([ƒÓä Â$¬±ðïÎן°gÛölÂz‹g¢ù­m8–î1ó:ž—ì4ClÊâ2v¸P w×t&òÅ7_pÁ!æàG0Ì8Ó ·.¸âík S6wײ›w~šaÀp6”N[0q¤ûÜßõ„üó~.¹ãb¯ÄåEmØà¿)郟iòäÎå ´5n(KP›6倰˜û7öyá™…ð\ì•á—Tîgå:e(ŠN†>üð#EWox‚à¾ÝÛH¯î+<þøã=nî íÌW…™ÒÕLéðR°ÆÓÇÝ߸+„öÙ­kmvõM¼:w¶Ö7 }~háª.¸èëòܳÏX} tÕ†½¿´c‡öªõÝwY~Ô…¹7ú ?ÇP²upo½ý¶ªÛí: ¿û#MÜÙýÍrLÃúÚ}¦.)¼Wñûöm[û ŽÿN À‘Ž^ë¢ùé•0e6OÚý¦õë˜÷¶¿oïÎ.Ðg„;£AÉ*×6yÂ3H‡‡ñã¶Ãm[7«ƒÊÒáaüD}r‰”@wïÙK%¥´ ®]:+èuÑ5ªy˽:ˆåÇík§¿4Õ£_"j[Ö‰'ù“J_ÌuTŒ5ReJiyNÇ­n½úV1aØ}¶Ÿ~}û(èá "þðûØqÿ©Qô†~cÆô—‚¢Ðöƒ ±Ê3‘ÒBÉêð0qÒ$W­X±BAïtFÐÎxºƒþ_íL'2¬òç®4¶Ô»µ® ¦Ÿ\‡Çr—<òH;^¦L™j&Bù/t¤„” C:h_èðXAºZ¶Ú;¦A!îÂ…ÎŽ<0,«n\'Ç&GB` rؼy³¢+´´~è˜N#úEb–i3îȇsN¼ ©*z§Åšý&Š+¦ÖÑݾ ´Ùóç©#èNþì¤tËðè£}Ôä)Y:VÆŒ~A¡ ~ÄÛì†õëì8itxð6G›…Ô]weéÄ6l¨*[¦ŒŽrÝÑL×Ãjl Ï-JU‡GÔ4î }{Ý~È·íò{½q7÷ÙgŸ0Ú#þw„ì´77–Š´PAé(î|SúÖߺñ¦²FtãpßùÚ ÷]#?™ `€µÖ?zMDO˜éDƒÅcÐÀªv­Z69b°+:a¢ßù˜a<„­31¯3édòù믿Ùõ ÁcÁ¢Ev~åÞUýˆFq«ÉGÝ:·èqâ2Òâê4~’yò:Çk]× €õÖ•´ÞÃzôà÷«V-³ôÔÐ&&uY±ÚúÛÞ =‡†øúº¸~;ãÇ<¤;ÜëÜZß¼’¾ÄÅVyy:mÁF˜„!'Ö²ÏçQak Ž×M7ÕTC ,=1V­2bè®zyÆ4íºC./žU¯°xqìuâI'ƃy- =´;x+—¿è×upù®{л_žùÎô:‚vÍ«iÓfÏÚpÀšw]AÄ¿WøIÄk1ñðïßå§qX˜~ ®#"lMÎù®žƒLõ¦\aO>ÖÏ{ýU­Ã—„1êÆšYãòÆuÑ".Ì3/'}¶h« èþÀ<ø=š—’ÀWÛ¹<:ù¦h3‚v¿§K—p{àåÝXøfñí‚ÒåÃeÇ82ú~àaÆtäÓèIsë÷êkªZœ  èósï}íÝ ¢]Üq1ŒWÂÛ»z±xRß~»S•,µ—ÿË¿Î÷€NIèæ £‰“&©.]Ò^ܲ‡…Ë n9^àÁ® @Ã&ŒaÚ™«n ŽÁ(6uã7Š{+_}µV:êºGyË[XÍaŒWž0äM'öüsÃTR¢â½ûÁñðÜ ¦íö$&ÉJ_å;Ü/Ìœ‘‹÷ &/Üüˆv·k…ä¦.fÍœNÊÍ‹X¯µo©cŽbÂR¦ÌÞÛzJ` Ý"qІLsÎ9‡EW …Œ5HQmQcmŸîD†w(Ö3ðz_·f•G‘+ÇÆOÁ‹Fy;Aþ lävˆ\Ážöàóÿ(І"mPPÛñ‰B[ñNñ¢‹.R¶âåsÛ…ñã>£2‰³û›å˜†uö®Àc <‚gAx´%¡QòÅY.6æ ™Q†Ö­]­Ž9úhýš \¢.BLúAϰAa²Kàá2Üü逈OT ,“®ÕÑö`a☈ø÷2ü¹gµÜ„‰Ú–ÿDÏtûbÎÐϤÀ#§ã–ŠÀÃÅÇ­¾çÌ+Ž…&Ñ{”I¨‰#èÑIŠ+ç6ñ¹ÏnݺÀ'Xà±qÃ;ž&<˜íÚwЯ–·jÕÒ8ù>÷µÀŸ;7±sž),îÀä†ʺ£R²,¢úõï¯V®|+a~‰ÔOõ!!9¨ lë×ó*!L)yà×ß>½U RH”nZFÊA³gÍ dæ7lt§­ë…¤8óÄNÐaŒÀ£ñTûöí´÷À4ê…‘Z.Ìþ¿ðÂ(U2-øÏNÇ믽¢.(XÉøg‚¤£´Ü7rfI;×õúà•×^S3fÌd.Jaî…¥ÿ#¥Â‰(;œÉ€ ïS%º’CM™ú’Z²d‰z{Õj»Æ ŠÏ‡&<_+¸ö21¯3é¤ó„ Bý9sçªeËWXfVPœî:ýoÓf-¬@× %æPD]óÆ­pÀõ“èÂâÆM²¾ã† ïP=»?¬ƒ`S6’P÷œsÎVW”,¥ËÀ{<¼;çó=Ä“ Íœ>M+FF˜tÚB”4sC=¡Q׉ðâ˜pž çÑé[U¥ÚµÜkRæwlóóøEâò üü¸vAýy¦óë:I ìÍ÷}Õ[+<Ь]OÙ-ðpû&ž~&™ê+x¾‚Ì|¬7øåö#‡?¯ œÚЫ¯¾¦èT¥~åý_˜Àƒó*L&qv~³Q1Í„À‹3³£×¸xãÝÔÞ|c 1èN³^ÓÅ%ê"Ä&`Ä$§ <†¡úôë¯K•ùÔ×"’dÚ²N4ä'}qv ሰÒ\;ůµÂ548–â‚m‘âÒB§¿ŠêÜoÇ>tL˜ÍõîD†w(~Yàõ&ðH´£qóÉy­Z7Ù«x‡›h÷mØQQø©PñjEJIaTú÷SunɪC.¤À œ$ £Q£Ç¨^<ª½.THï¶4þy\® ÌøqŸ©0‰³ã›å˜†uö™xðë °ÈÃU0aDwlªn¼ÉzÙHWuøí L—¨‹›CØ Ž 9MàÁÉòEq@ñâúZwqœJ[öK+S}qv ¹ðq@˜P¶Li½Q 'ŒÑáÑâÅ »wA.cG[îùÁZuÆjÕêÕz‡­ÙôÃý@Œ[ ’¥;èdײeY×Ïa'9®ÊÂŽk?µÁ¯ã4'tùwóÞ¦ žk¶øšÂ3\ÕJ•*¥Ž>ú(í=Ÿ¯]~A¢ôrS=E]k$‹cÂçË\àAºXé²^±´@ö=Ìp`þõMùhCBrùéôç™Ê7ÇÅ”!žNØ:b]ãö ]Ï….¼ðÂÐ3œáŠøñZLšaü´¨<°LœðÈT_aÊöä}x`¬À)6´GáÃð[" P~‡n‰0×I‡ < <1§817Ř•Pßçž{ޝ÷dùp<’TÆ~Úqƒë®­FóÈóôã9Ʋ]?ìÒóøqù@¥Ê”³s±¾½Uõœ´ÆõP8MrÇÅ0^IT^â xtêÜ…® ot‚òõøð“X¤§Q‘íß-»¶ÌÍ?$ñÊVJE!È¥)PÊ“ˆŒ_<¡4ŠS˜ÒrîÏ5Cq-P= £¿«Ý 羇å-¥å4ø¹Q{Þ?ýô3‹Òä n¸ÒrWiŽ'z»É3”í|õÕW®ýî*Ó†‚NPŽcâqݸ?×L;1l8£Xœ+ÈN¤PØ/Ê;uœ1("†B;“gûÜãã¦M°¶”‰¶ŒÈxÙÒ鋳KiyNÅÍTWÐŒ: ¢0|Ü0\‰&W@Ë•¦Û¦Ý4ù{T¥Éð÷Õ×ߨvZõÚꊗ¹’ÎghÌsÓàJË?¢yˆëŽwŒcæš0q’¯.U¥å³çÎÕŠ¶1¯4ÿAŠÔyzaf(¹¦6ÿÃG¾0ÿˆË?‘¢Æ°4 Üàeá~~é|ës×octêçîú÷{çßæ)ðóÝ÷ßÇÐ>LþhsI¸™Áß<¡Ó/Mn—nº>ô°Íç‚… ÓƒRwS^7Æ®×£½mØÅKÞ°~‰ùjíéÝÇÚó2D1óo6ÌìÎ+¹²eŠvMÞñœ5{6wö˜¹"Sº;Úãõ…бիWë¾–Hžî½¶ù¼­Á‰¼kw´S6>.G à‰÷iPúž ñuòG×_FÓ¼E+[w4f˜ÈLÙñtׯÆß3ê¼Î/lT;¬;yþž§~žvØú=fŒõ›hÉ# ^lúô1¾VGštZˆ{‹dùÂ(›ºÆ#Æû:ñeã »Ú­?´Yþ~{ÃFÖŸ1ðõ_÷¨Ïìj ¹­ž¢®5áÅqâ¡/ãm8•vÅÓ 3g²?ÏT¾÷Å:" “dÝè§§¾¢†ã§ñ¾<¬o"a©M;lü ãdª¯ˆRn>Ö¥å&ç/˜µ Çó5NaJËy\X/eEáÃ…¥›hÁ¸…ë¦/À¼|?âX¸| Î4 ßýâ0v¼=¸m/ŒWÖÆLÜæ¹{÷n[.”êÙ”™¿ÜÉ×ÌëÚ-»o€\d©²;¯©Lfƒ® ¼šÊÄÓ0òIq²„IßíTÅÇúyã&‹œ8ˆÂ0š;÷O>13µ³‡0ÖLžÁ` "Ú5`ýÁ¿ËØbòÅgìé.t/ãÑuVöGvgbÜso[›Ú £‡{ô´þS %Â…/BÜÉPX¾\·°A~sšÀB]^o¿½Ê-’çÝíkÝ~=Sm9S}qCŸ3Q6p ²·TÚ7ÏÌœ¹KЉ]gû†õ´Ç´çãÚÆÇì (ÌVãgÇØvÝ¢Õ]–‘kÜñÜMÌ€ •*[‰¤×Ë7žú n·qЕ‰¾~xº© <èúQ›ŽùfŸxêé„éñ´ý̯¾öš—3Ãýü;ºÂˆ b¿eËWÔ~éÊ•À¼v|àAŸŸÀƒ×'Êž(M“6òo1ã¶~ÃF›6âÆÍï¹|åJ„¡/¡aOºe «3lº]hÎâ—·•lžƒ¶Íý˜6Ãë87<0çãÞä wÚÅéÛÅp&Hªw®°cǾi¹–¼/¥]‚®³ï{*kD߈˜åk¯¿n±f¤ ƒ¹Æ1£«'ô?6èdµ|‚ÈÏì+‡É[WÉ®QM<~O´Þæüü;¾aÌeì?aÏ]»¼L› '†y÷u£Ýá6¿X_Ó)tûî2· Ø@Ùð-q ip\Ü|}”h͘YÍŒƒ5žî¼Òøñ{æ¶zŠºÖH„Ç"l.ËçÒ‰ÖÉ<ÎdÍîz=H€µ?ÏD¾yéðtxߎÙE¹]à‘©¾" ¾|¬wû}.ż|(Âàäòxß·°¹„«&<Ýq1ŒW•…ü‡ <8?y kŽýЬíè4«G(ä–=0`.qøÏ <ÐÚuè¨ÿôÓ¯Î~صËÓ`1¡J†xcw'0É<ÐQÑ'ߤ¿¦…+ïÈèÚ¿d|Q68òNeÌ”Àƒ¸˜ˆò]CcÆŽó”+™0‹M}›“#~áù 1N\²;fÌXîdÍÑ”ïö6a¸Mì!ñæ‹AµGC¼C„þƒ„zÀ•O|\áßÙ€xÀØ "Ò³âù&0Aá„NÒäÇmÜ7ƒÉl Þýh_|³Ó°Î>º2Á–e§«b¨s?rë‹sC™Â…3Ãø&Ý gØ Ž0.ƒ`Ÿð@žø·ï»«ü}-oßn0QÚ²_Ü®]¦úâ°…JØ"ÑÍß{&qã3wÂí—v"»zõØï+l, ÃÇM#lÎH;ŽücÆŒÓøÇ÷•8ó6‘ ӧâ”vò»až1ÒúßDÒ]ÏÚuëlØYÿ=-¬ÜtÜ÷}-ðxoó–]嘯燰eÀ‚ÆÍ¯ß»çÄ1Éüü;L]@¸`ìÍ“®w±îðç'ð€ß–wÝmýÑ5Z`eâàϹ¯¼ª… Üæ Üè^c7æ“Ц7¼yOUàðé”õhpÄóƒ?ôäñGSyüé´ªÉ3ž&lnx àLY”#èt0ï‡Rx`Îj°ÂÓ]¨ûõWî)s|GQˆÏq3uÂßaú]?âó”õƒ>ÔÞHѵŠl>ä2ƒ2%ðÈԼίÌQíøæ­ y9ââóf·½`ÎdÖ^hGAs+ÄÃ7R{q<¬’"ÌŸÍü q™5»&F¤¼¿Ã)uÓÞýÖøî·‡Ó­A&âCšhÛ|N6@|&xºü‚ ô`ŸÛê)êZ#^“°¹ìcYlÑ>èúHÔcÆzÜÌÍÏd(ªÀqºmʯ?ÏD¾÷Å:"ŒùÍí·^Sí+áw>Ö»¸óµ ŸûGåR˜ÀÃ=m„vD;wîÔýžù†øí4éðáÒG\^nP€ø7ãòÜö4·À†@3™~ÝGñ¹‡['|LuóàbÏù¦H‹„Sœ?Œ¸€¥KðÇ×̈'Qºn9ýý?+ðøˆs¦âé7¹#®ïçþÜ]"‰*˜‡u'0É<>B÷#ýæ›o=Ì:øÃ1^NÉŸÐÃ/ñ¨bÈ;_lÀjž'Ô=N˜˜´ðDéR*uÏw# Þ×çÍó,ƾøf9¦a½» æ8%ƒ¿åF›swn¢ñv 3oS™Â…ôغE|¢â–)ì=lG¸œ(ðpOm`ò '¿¾Öïš‹(m™ÇdÎT½º|™@IDAT_Ì'½îά°EbP¾¸}&qË´À“KÓW¡ŸÂ¤ÐÂðqý‡-Àݾ;œ]aú >!~“O7]óΙ·Q̵o©k1À) RØûìó/bëÞy'öp÷ÖÍà”Hà`Þ¯  #~ôq WLñ¾ ‹v¿|mz÷ÝØª5kìÿ´3lÚ`Ps7˜±¹Ã/Î(1yŽr£g¯Gtzmî½/açß|ûmì7ߌq¦â|gýzß´Ýü¼³aƒ-ÂAˆ5yêÔúQüA§SMžNŒxá-€à‡_ÃbÊ$ðض}‡'MÔï|:Q áNOÀlȸ¦Ó¤ož|ÑËOxÀW?5&&ÉÚ &,¦#ðH· ô˜þ3%ðÈÔ¼ã*„OøŸ4yŠ)^¤§ËHÁF(÷J+\yÆ1œ±ƒuÖ%Ÿˆýþûïq黂£°¨q™Eç.ÝlZ&M¿ \Û¾~1çç -Æq¬‡M|xbîä6Óq,H¯€ÇKØüyü.¿À‘ó’Ûê)êZ#^†°¹¬;'G]‚1ëÒDºŽ“×Áüù \/¡ïÉ<Q¢þ<ùÞëˆPP’ttÊQƒsþŒ{cJTßXˆº ¢0>A¦úŠ ´¹=ëý\ÊÛ5Æk—Âðëö«Ø€ìö•X3s>6Öòñ‚¯}’åÃebáx!/î¦4ð¹`æÇâ倴ôø^1â$²¶sÜù¸LÃx%am a9¹ý?wƒÙ§ ÿÃÕŒ¨slðÀøè ;‚ÊîÆ›Þó!³Ù©ƒ$…t\ÉW>”Ï0E_U«]§ ¨4gö˪Ð%—Øh¸&X^U²¤*W®¬:þøãÝÓ®^ýuîmï»WÝwï=0F¦°¼…)Yâ@÷×L!TÁóÏW…‹VÛ·mWÔ`=yñSöUa"Z±r¥‚2NNÀ¥páBŠŽÀ©¥o.³Š¸£ÉØqÅM®›ñô„»ÆMšyœýÊåñà…ã ¯PVTïÖº¤Lé\EžÚ¼y³‚BMCpóÅê˜cŽ1VŠ+´2–P úg¨ÛwX¥…ÆÍ<Ô–?x"Í Ê«ßIá] ´ۢóÕ‰'ž`í¹R#k¹ÇåËÅKWÔ+”Óå—_¦^š29Né%1¥Ô5kq¯:ÝòôMüûoŒ”»/«{¿ºM¥îIà¢.¸hï·‰L  ¥I9bµªUe²û›å˜†)l‚R°K _ªó„ŸÛ¶ª<оsC4AVU¨âß7¢¼PHŽï‘+Í„›Ÿ"ÒLàB;µíØG–J/®Î/x¾jÕ²…:ëÌ3­}˜!L¹u½mëf«8-,^?7®œÔUêú'ˆ"†¬¶îÚ¥³jÖ´‰ÇK«»Z+´yì0þ ¿…B7(Óãt{ƒÛÔ#½zr+mvËK¿¶бÈT_ÌÛÆèQ#U…òåmJ¼_Œ2ÖÚ€Ì)Ü2¥ìÝdËŠ{æU?1ŽœtÒ‰ªG÷‡í÷†'0½ ý •‚òhN·0~qª\ùjýí|øáGjõš5ž>4Ù9E²Jˉ¡®ê֭dzg®[·Žš²G©]"¥åÇ{¬"ak\°(V¬˜zvØPu衇ƹeÑq]èû¬ï£ pôè1ZÉ6Ö®][Õ¤ñÜ*Î<˜s¢°‰¨Q£†ªc‡ö‰¼Y÷ƒ+:íiß¹aÚKSÔ\ ­ <²æMµÕ7ß|ýxÌ ¨??~‚¶CÿÿÔSOxÜÍ -b]·ãiGÆ?Ç¿8Vá›æD»îÔêÕk´ÕÊËÕGΔÞDù4uŒöÙÔé'€ô© ¼JÓË”.­ž}6K1»'RŸ—tÊ€±²Yó–ê“O>ñ‰y¯U¿¾}Ôõ×WßkA&Ó›4i¬Úµ½O»­Y³–æ›Mµýcqó@t’Uù‚6oÚèCiËŸ#h.…Ð.ŒReø_飴œÇ³‰p§¹µjÔ°!õeÙw¸"S¼…í0‡Ñ½÷´Q ï¸Ýz¡QªöÍuâæeË–QR»>šVùåW yr×!PìܹӃ6®0C*kİøŒæh×^w½B98•/_NÏ­h³B\ÙøøH›Z1¯yP=(Uº”ž£#¼7<»óDcFØ‘Mˆ;•5*Ÿ#á{xúIÿ~ñûQ?èÔ‚uB{ªT±‚:ŒÚøºµëâê]嬸c>tÓM5µòwb2é>‰›P?yâ„PåÆˆÇHw‡º»u“ß:¸Z¼C-Ñú‘ßü ÜK–¼RxÀŠ4~^C.]¢ ØÝÇÁoþÖLAÏÜTO~Xú­5áűH4—¥SœŠNeò i‚·ñý÷?Ä­µÐ§LšèñŸè%;úótó½/Ö‰pIÆë,:Ilƒä6¥åȸ_ûN¶¯°„øXÏ•–› ÿóºªTÏ8‡¾wñ¢qüŸ0¥åˆNy¨ÊUªyâÂX€¹êa‡ª¶lÙê'sÐÒ¥JÁ¨‰£°H–Çû„Ov!¡¿ž×!¬¡êÕ¯S'Ÿt­ïß÷åáùñ0fÝ|K]ßñßÄkž˜£›µFpÇÅ0^IT^ÒAÝ\V¬„IR¹ß ¦hL¼'Ž¿a0x=X;ƒüÊμæ:ãZàÉjÛvíãÂ~µØ…&ïXø@›d(lUàqCêŽÛ¨ºõê‡&Éðð知EìÉ< ÿzfس ù0êÖµ‹þxÌâÙP†1yÃâ…í‚Õ/&††=3T][­ªyMé †íÂL¶p¡Bêib6œuÖYq~é*!Õ½G<““{ìÕ³‡GþŒ -Là1røóêÞ¶íB™Æ«Šõ2/ܱûÃÝÔ½÷µãY‰3£lXÔ¡#ó£7–.U­ÛÜš„CžÆ­ nñ ·Âvë>˜3oÜ8Ì{ófMU{bÖrÈ!Æ*á3Y"\KÌ*R®Ç >ýôÓU×.(ýC-Ã7Làq- ž{v˜jsϽŠNšzòZ»v-Š«³:øàƒ=öæÅ0šÍ{¢gÀãïc ô÷˳ǽ¬X±RÑ)^µlÙ2×I¿ Ô'‚ÉÎñæÌ™«fÎ|Y3þøœ… <6ŒôéÛOÑî:O0–‚ߨáªÄ%µ[˜À¾Ý¹SõéÓW-\¸È^_ýúõ´BsN-Z¶R+Wfi~ø#·y‹–6ØÌÓÕyçkßaxï½Íª^ýÛLþ@Ac³fÏÑ»ÁÑ»¡òw6¤SGáÂרmÙßï=ݾuaNŒ½8vŒ*C§¦ ñq&ÊXk¹ÏLආN?Ô¹5K¨]AØ”.á{¦ëûÔø =;“¸À# 7ý´ã›îv×Öš]âÜê~I~ù7?è?‹Ò®ûöíÛÆ1CxAæTˆ‹Ž•Ó©ÕÏ´ '0Î=ï<ÏÎÓ ô` íêÂÉ<ÌÏ¢úøãOô\ §üæaqî/7ôÛŸ;:éê}åä“OR§žzª=ñ“Ýyûꫯ)ŸÐu²Jë§vZÊIšzý˜„æùèïÔSOQgÒI¼LÌ•RÎT’Ó)„çü±Ú¹ó;uÈ¡‡¨óèÄ.Ÿk%™•Œyz ðBEöndYõÖ u {Oñúe$*^}uª[çÕ¿_VŸKw§·õ˜À€“M­ï¾+Îês²©Ô†1:0~àD_â"ó±HeèM „ciŽ€¾Û[)“Ð8¹t‚ ¸Gíc×:&!¬»ÐÿìÂ6›ÄÜyH¢1#lˆ´Ò™×!lÁ ö þRY‹!`¨ô¤ÍbóæÍ÷¬=áÖ²EsÕ’ÖÐM füZg‚ÁóôÐaÊon…õuëÔѧCo:Ä¿°ÓFîmï¬]C'—ö®Õüò¦]IhçÜ„+˜´i}·ïIÇtÛOËÏœ›ê)ÊZ#^ƒ¨sYÜÔ€xý6¥`}…Sž7ÓŽ|ùòñè#™³£?7 §›ïì\G˜HcbYZ_vzðþI¥Ë‡KgÁ|æé¡Ï¨éÓgÄÍg°®Ç<èì³ÏR†GÆBù֯ߠèúK½¡ ë³si ¸äâ‹x؆›cpÊä <Âx%QÛâE_Z¤èå0j ûfïlÚ¤•5oÞ©×>à]Ö¨q½Â ~>¿D}mú5iå¦g¶ ʯÄè;FFmp_¬mÝv”ì{ºùÎ-ëˆdqÉÉþsz_‘ vw~û­ ò”_÷ᩬÓáÃ¥;Žà4 6÷‡B‡vX*P„†Á©2\¥ º;4‚}ìHºÿÔíw4Ò©bCÈKâO‘ïã,e,9xd J‰(S`|EÉR6ºY/ÏPE ïÝMor©!Hà‘lqøb&¯Ib“ÅBü ‚€ ×G^¯a)_^@ ¯ <òBÝü—ÊÀwBg®A@äÒ\Í!=® Ú, LmÒõaN4âä NDîk>b¤>ÑŽtkе½~×õ›}B×2t.ÂõÜåIsÑ¢EõU˸B}þüV(¸ÎjîœY uþš8sÃS¹¡–þyäBS܇ºuÕŠšÍ{^yò².Y¼0PYb¢òÊ•V‰wA@òûCà1xðõÝ×ôÑGk…~yM)‰ =ˆÀ#{p•X“Cà†oRï¾÷ž*[¶Œ7ftrÅ· ‚€ xøñÇé&šŽ WFF¡&ïT;wÚo:‚ O›ÊÍUU‰ò ¥õO?ý¤¯rûDas²»‚:vhŸ’"d„E§;pÐ`µ2ùöíÚj³ü‚€ ä=ö‡À#ï¡(%²xd/¾{4Úw¼_ý¸{·ªY³¦ºñ†Ñ‰/A@A@DB„iÓg¨={Ùë­\Ï]t‘ÖñqË͵]§ýòþÎ;ëÕƒ:«í;vø¦SÕ«_§:´o§òçÏïë'7[ŠÀ#7×^Êûo¿ý¦ÐvØay¨TRA@A 3ˆÀ#38J,‚@v" ìDWâA@A`ÿ#ðý÷ß+\_õùç_(•/Ÿ*Xð|uþyç© ìÿÌùäàçŸVò‰Îóßý­Î8ó uAÁ‚êÈ#ôñw¬Dà‘wêRJ"‚€ y_icÀ¿ÿþ›GK'År?p€:ìÐCsA¤‚€ ‚€ ‚@.G@¹¼%û‚€ ‚€ ÷øó¯¿ÔŸþ™÷ *%r)|°:ø ƒriî%Û‚€ ‚€ ‚@ÞA@y§.¥$‚€ ‚€ ‡Sy¸r¥h¹9Ý‘««O2/‚€ ‚€ ÇG«P)Ž ‚€ äMp¥Õïü!W[åÍê•RåR ì8„îlÆSHA@A@ö?"ðØÿu 9A@A 2¸Þêï¿ÿÁGdÄÄ£ y àÈŸ?¿\c•yh%FA@A@A -Dà‘|XA@A@A@A@A@r"ðÈ µ yA@A@A@A@A@´GZðI`A@A@A@A@A@A@È ˆÀ#'Ô‚äAA@A@A@A@A@ÒB@iÁ'A@A@A@A@A@A ' œP ’A@A@A@A@A@A@H x¤ŸA@A@A@A@A@œ€€ë,U¯Þ­™OdOŒ8áЩKWÿØÑ£T¹reí{^4,Z¼D5mÖÜíä“OVeIÈqlirg#uažI kW"ðÈ$ÒW²ˆÀ#YÄÄ¿ ‚€ ‚€ ‚@NA@¬&ž|êi5äñ'´M»¶÷©{ïiÃ\ó¾Qy¿ŽÃJ(`tÂÓÁ¡2ïòÖÛo«ú·Ý®#¾âŠjʤ‰™OdOŒ¿þú›ÓWZmݺU}ôÑǺ<ùóHå8Y¤2wÞ¹¾ùxoófk|°}÷3|øáGê·ßÓNgœ~º:òÈ#ã¼ýõ×_jݺwÔgŸ®¾ÿþ{uì±ÇªSO=E/V,ôª¿ö l·P×¾þñ§ªQãzuÊ)§Ä¥É-Ðn?úø#muéÒð#Õ÷ç_|¡ý}ÔQê´ÓNãÑXó?ü ¾úúký~Âñ'¨OŒ¯ÓTË$ðø‡Ê½}ûvµfí:õï¿ÿªÓ ëË/¿LsôÑ6_a„ù”àâ»D=œHíðÔSOUEŠQ‡æÿM¹ñýßÿýŸZ¿~ƒnK»víRÇÖ¶èSÑn¢’_£?ØJíqíšµêÐÃS^PP]R¨PÜxüí·;Õæ-›õ·†¾ùÒ"…©ï?Oc˜(}´õM7©/¾ü‚¾‘¯ÕÑÔ×;ä=슱¯¿ùF÷óˆŸÙè3ðí|ðá‡48MgçÓ7ï7‡pó–j^ÜxжmÛ¦ÛíQTŒƒ—\|±í¿ðÝ Ÿ¡M…åm÷îÕú 4W oó†O¦¶ö± ýò„>+Ë1‡ñ#àjæzÑœîêÿüè÷ß×õ·Ã©½}öÙqÞ€ÒüòË/ÕÎß)Ì0Å|ÓGô?ÿò‹Ž'J¿‰¶¼ó»¬>âØcŽI8îÇeP,A@A@A@ö)x”¸ò*Ëhضu³ú€„={=ª°#Ù%0-:uzP]]©¢ëäYdãŽúF WmÛw°×Œ˜›ßÝh°3aàÀÁjþ‚Æ‹çy& NîjÕ2NÉ/g*y8/'¼¨®*YÒc›jšžHè‹õ'ž|J|a”ë¤ß;î8u÷]­T“ÆwúºÃL¨Q£Ç¨gŸ{ÞÖƒëù†5Ô÷w$†µ?3=SE‹k](NùQ‰âÅUÇíUÉ’WZç?ÿüS+q¥úeâ™§ŸR×]w­uw $*r©µ9üyuõÕ•ì;ðxaÔh&>.NŒn´‡–-[ø2Dx{Þ´á5hð5õ¥i6ˆ/ÊpìÇ®qO+—¿©ÍîÏSO%} këÂÄ<œ=k¦ëE¿?Ú»m'Що][ë/Ý2»íÛ©~ý¨ 'yÊmlÙ¢¹¾ò§@Æ*î9–„ˆÝ{ôŒ³‡ê áߢy³@aAƒI¯Îرãã¸ãöZßN@Ò|céRÂø µqãFcåy–.UJ èß×WØƘ6‘¬]·NÝRg¯qôcF¿à)úŒ ŒëÏLeË–Q];wòáÆ¿Mã×}fZï˜Äø& }ôÁvcÔOþ ÏŸûÊ« íÓï{CßÓýán }Yª´ä7Ná¡Ïö#W†>õ¤ºøâ½w?®Ú8Ú~"Z¶t‰nè£ _z™õîâböxÝ­[³JC SCaíŠM—^z©zyÆ4løˆ‘ªOß~ÚŒ>eŲ¥¡LnŒ ½yTûF‹Ì3É'|ºu׬zË7.׊‹ ±Öo¾±ÄwÎñöÛ«t™ÇõëÖø -çÍŸ¯Z¶º[{ƒ }úKY›R`±k÷nõ<}»˜÷Q³¦M4ÆVjÚ¬¹Z´x‰~Åæ–~}û'ßg‹–wÙ¹eÆw¨žÝöõ'–‚€ ‚€ ‚€ 9lx`AZû–: KدOouë­u=þø®B0$pBÁOhÂX¼7nÒÌOÐË-7×V})]œq¦RPØ»tÒäéà$F“¦Í|ÜÌuëÜ¢éÕÓÃ@…=˜ÿmÛµ· rؘÌ`Â℃Kœ1÷îÆõš!íúIôÎ÷‰ü>úH/Õà¶,aüv{èa5ž˜] (I~úÉ,òÚÂùy}Þ<Õê®ÖÚe#ÑœùNö´kßQÁO"ã 鸻†9£ÌÚÙsæÄEEàAÎ…²a ÓÔZì1Ô¬u³‡¿ú핚áäú«Zí:ÛV&Mo…F™(3x@Aœ©7æB‚çŸ×VŸ.ÝR3g¾l¼>«W¿N=ñø8Ává6nÚœN e Œ€ÐWŒ ¡WÐi((¢†BêDô}„1¦§Û—áÄØå'X–/_¡noØ(Q´ûØÑ£T¹re­_þmZKǰ?£^‘°sÌöD‚)§Xúu8Õ_ŸõÏ/Ž£Ê”.í¯]nxàTPé²åmYfÍœ®OKY Ç€±Ø‰|à~-dt¼¾ò~ðÙaCmŸëLñy¯½¢ t0Ìd?°C{©X¡‚ǧ`î¹·m¤~ŒýqcFÅ '¹À˜Þ$òÀñ„_{eNÜ)Läi@àݘÆwW@ÄÓ‡úbV¾õV¨Às…†w6ŽÔBXݦu–ÀM §X;uîâZǽ8@uèø€¶OFà˜aŽ2xº©æÚÌìŠ {f¨º¶ZUîE›¹~úAˆÝv{ÃHx@4xÐ@;Îc<‡ „>ÿµ«õ)6máüüôÓOªèåÅ­íKS'ûΟ¬1‚€ ‚€ ‚€ ìw²Eà¤YàãGbl`ѸhñbËx1%Ÿ3ûe}m“yçc‡'˜e`bsÎÙú ‡tBWàˆ ½»E± °RÅŠÄ0>N-_±R-Yò†Íââ‹f\ ±hq–’Ú¹s_±ŒäLwC`ΘëbÒMÓĉ¤µˆÑÍOB œÕédÊðûvÔâêCõêݪúöÎÚ­kìº÷ìåÙü¯­VMá„ ˜$K–,ñ¤÷¥KÙò˜x8S5Ç+´Ã»õ=÷šèôS×ùòêbü/}óMµlÙr;gÚ»;äßÛ´Ƈyü›x^ž5[¿âäËCݺ'½ËœŸ–3‚ \ù«]æÍ_àÁ´MëÖªCû½'%gô™ˆÙEèj0ôQ89”ˆøîP0öo¼¡†'ˆ»“ŽO>1DAÐÂÉõ÷þ–÷¬‡3…&•2sO'²ªT¹FDצà:¯¤ÔÙ|ßðç·ãuÊÀn|C7×®­Û5v·oz÷]µ˜v׆üô~ôu[ýzÆ»¾èÆ›j{ê X#¸ª§4æ/XèÙŽº7vtÜR8õ…:à„ï¹}ã¸jgÆ jô˜±ÜY¹;¬Ã~ÂŽßó0öqÕJ™r^o-ÚÕ}E‰,Fv;›öŒŒ ­-^8ß^C7óåYt]©¶e«1òWô÷wloó«c]¯d=G0¸íÍ=Éà÷`wxÅ åUÁ‚çSÝ|£pM úKCaŒXãÇ}¢ÌÕkÜ`­QîªU«¨Ë‹U‡q¸¾v m—·É­›ßUa'lddÀU8æjÇè” 9Y€áü´Hõë®Ó S¤³?Ox ïw4ºÓö¥8ý‡]ý~ô]“W¶|Eë$pµƒ_ãÛ¹úêJº}¿úê«vÌDP0Æ€¡ZÕªªtéRú[³fGxŠ:tO»¹§ÁЇU ¶T¸paõ/•8!Á°8 5nÌhO޹ÀÃ8 ß@~‹R{AÝáÄÀÒ¥ogݧÑûýg"/8ož·KjªQÛÅsõêÕêÕ×^÷¸›L¹ýß\ckišá*7ô©˜¿ì+4…ÚzjÂh ßl•k*«tÝ6—ø «’xŒ;VõèùˆN£~½zªOï,³M” ×]ƒ§Çæl‚p‰ûÃfÌ@w·n£ñ3þ1Æãô(®'ÜýãnÝŸòzÆ7‚o„ñû’Â{OˆŽ5RU yŠÍš=GÝ×6K8‚¶„º‰zm˜_|b'‚€ ‚€ ‚€ ° ûÈ3Bů(;ûÜóí¡"EcÄä‹‹{ñ’%ÖüÓ0?ò‰Ç~èz þrgã¦ÿˆß%b8ĪT½Öãv»Þbt­”õse*MbÊÛôP·{ôŒK’®)Šu¼ÿ?Ò`ý­[÷ŽÇ­Î­õb´;ÕºôéÓ=þx°“q²O^t/¸µb Ó1Ô¹‰#¨þßß¶Íúßò*ÅþúûoÝ)®ßM¯¼úšoÒteŠ'b~[7mò¸µëÐ1ö÷žø'àÓ´y ?´gýtÛ3]©#&‰ÇOÔ— 'Ú´ºv{(.1ª¬»);í¬ó·`ÁBë¯y‹VÖ=Se¦kAlü&cÆŒµéƒß÷´eË㬟?ºÚÆã†´k^h·œˆíÉ ]Å­íØäÏéÓgX7Wž´Kº³Þã/ÄðöÄÓÀc?´³Úº“ Æº‘^O»¿­Á1´O—P_<Ÿ¼Í¿o½õ¶ÇO§.]“}Ò.pëÇÅÌzÊôÅØ´w—8®p§Z1|Ü€j±ñ L²„x v¨?bâÇEA§<õ°zÍš8?Q,€©I XûúFãÏDÄýSNAí ~xßNÂ?,FÂ1›‡«J—ÃÝx=fŒõ‡¶™,¹uŒïÒ%ºŠÏ¦aÊ:îÅñ®·ØŒ™/{ü¹X`,0áëÕo£Óqqq¸óº>ÉãÇ+Æ&Nh£ ±i!M·OÈD^z÷éëIãé¡Ïðlh3Æ>þ}˜òcĉ×#üLœ8‰;k3 ch &”ý,'Ì·Œ;žóç/àÎÚŒyËá/Ùï–®î³é .!¯<& ·ÿÀ\ÇøCžÌ\ß»±Çó±ƒÜ$ô;Þ²þ€3§Î]ºY·;uæN3ï»é«ÇM^A@A@A@r&PÀšr#´c90^.XÀb•3›]˜)ADJ‡í‚ñ„-F!àà‹x?Ï—Ÿ;ò‘É4+U®bóF„ƒiÒNQÓ–tSÀZg¢¾ýö[ã÷„@…3 À æÄÝ’xО¸ƒ˜…HÏeðs¿` ™|Ðiž=k¦«ª¬`È©{^Ö à á~`#–ãOWy¼ðö æ[P<ž@/¼Mû1Àl1e¾¶z mF[u5to¿õ!Š¡L•Ùx@XD¤ÌØæyç 'äÛ”Oüh÷îݱY³gëÿ…‹{¼p†#˜ÂAøCE;–mz®Àmkn:íÍOØñÍ7ßÚxíTçÉzÌ\0‰<üñÇ÷œ*ðÀ7â¶W“qÚ}î)¿ÛïAO:Ã7´» ¢+Ël:aBò ð°Ï-`È¿1?ÊSëæ[¬?W÷DÄûÁ»ÛÜãëdž— ®@›vß{âû©g.Ô÷x¢þ½¯Y»ÖãÌø~‚âÁw w“ïLçßsWø`2¾k×n yr|¬ š› >ÌsL™ðä}/æYÜôR™,Ä=y?ƒ0hÉêš·(8͘1ÓæB)“/lˆà´pá"ëF×Y'ÔŸi+ðÔ÷`Œ1qãÉûÓU«V[7¿¾‰aîÅÃó¹ªÍŒA@A@A@rÙ"ðð;=ÀKî2èúëÌ™ÃXhº `ë‘ t½…]ŒbÁÄP0aâIë'>\Š"ðÈTš|$Ê ¦^чݭø7L?”—3U^?!, ÍŒå‹w0 8q·dN˜ðA‚ žßiúÈ£½­Ó§Ÿ~fãA|¿ü¿[ž®—°~Ha© ë2Y°£8ŒpjÀäÙe”sf ê<]â |Í ñ<ãä?ÙF1'Îè3;’yx”%2»º†Ž'g¦k«,~®à‰ã‡ú⃸ˆ ÷N_„Ñò+l>€Áwß}g½ƒIfê˜ŸŠ±˜BœÞÀ7†'²¸0˜ù·$ì@ô“&M¶yÀì0Ânt“_<¹0á8#Òm³añ¦â–Ì R¤˜Ú(/S¢ú Œ(äÉSl:`|§B¹Eà²qA©ß (œ„ḻ'¢àÿã©/M Âû&¿Ó& ß-á^*Ô¶}[.7.ð ÝX¡Ñóf‰ÆÎ ˆ‚òAÇ‚ˆ0‚à—ûç;>ð¸}þùçaQŸ G0Ï2i ïâý›_„8ehü£$K8ÍhÂcC'Ž?ÙÆ7sÀ?b†µ?·kæý4Çó'~"†ŸÜ3qð“TÙÝßš4å)‚€ ‚€ ‚€ >Ù"ð3v\œáz ³æ o.ðÀB̲ "½6Ž(×uàŠ$“&ž®€$ŠÀ#SiÒ=âž¼ð‡Aåuíùu(Ý îz‰{ç;EÝëI86É <ÌÉÄáwu‡›~*Œ~NØ-nòBwœs§8¡ `§¬ ‡'é ‰A°ô]àÆ¿ËˆæŒ>Ò¡âÉC*/hã&-œ:0„« Œ=¾þ>ìÙ猷ß©ÊO‰d²Ì\àÁÓ°™p î÷ÄwÙrfʇo§µpýV"F®Y1˜àtòÉd§š¸\dˆ_evÌøzrú ÎDƒ›ßÉ¿Úm=¨={Þö°šSNx`·tñ~'cÒ%Bq-Îöí;ôÕOØ¡ÏO°ýœQŒþË+ù5H~WäE©ÞÃU\€Ö¸@ÂV¸ùAy°Ã‚ôÁ¸¦ ÂH.”rãàñ'úÞy{ Ø!_ÉæÅ=YÆûF·œxÇ·Ïû0ΘçW¢ß1ýDЧIL\üª&>aŒMD¼ý¤"ðÀ† “‡în“CÿoúÏ»în­Ç󎾔¤¹§D¸?˜/æD8… $ø>€‰yá¸" 陲yäXÁ Ät© \²b’_A@A@A@}‰@>$– U!\¹éä‰Ô•W^-W² åÌPÒ ¢ÅªªX©²6'R”ÉW7mÒXuëÚE‡ úÙ¾c‡ªZí:ë¼níjuÌÑGÛw(Y†²eP»¶÷©{ïicÝŒ!SiNœ4IuéúŽJ˜_›¥€Û¤åI;?U•j×Z¯ëÖ¬RP F÷Þ×NÑnKíJy»vél½§£´J|éš×óÏ #%¨×Øxý aåŸ>c†"ÆÛ,¸êòåË©1£²8ÃÍÅvÉЇ;¶Ye¤¼=» d“‰Óø¥Ý£ªq“fú•+ù¦k”1ĵýÂùóH™íÙꊒ¥´òY(Ÿ2i¢vãáy[Ïd™I¦èÎsÞõ×WWO?™õ-h ŸŸ¸ï‰µ?:e¡n¨YË*€vƒCá.Ò¨|õÕZé.w§ªs×nÚ Óç½þ*wö5óö7ü¹gÕ5¤„dïIˆ%WZîzúÔ“ªzõ½ýŠëÎû ×-Ñ{—ÎTófM­7(®Ûíú·ë!ƒ†d”–'úFn¸ñ&ELH»—gLSP”œ a˜¢«kÔœ¹sÕ²å+<Ê™ýâ¹åæÚê±ýýœBíêÖ«Oʤ×h?'¼¨®*Y2Î?ú¹ý­´™"½VHn”¬Ïš9])RÄæ·ö-uÔ;ï¬×ï/Ž£Ê”)mÝ¢¢öƒuêÖSt½”Žvê䉪D‰¾Ip¥âƒPµkÕòø#Á¿š2õ%µdÉõöªÕvLñxb/n‰âgAu_‡>„qã!§tò•žCÉ6”m‡”’¼à"ë…O¼?´"ÐÏŽ‘UF>öó1$(*(‰op{Cíœh.æ «ÔeŲÚ”´/Z0O{£Óqªf­›µ¹¿¾ªn[TûŽ÷+îj»÷6mT‡v¨âáƒÆ;w*:£V¬XiÛŸ_^ŒÇvŸ|ò‰ªxuÖ\åðÃW˜CLÊÛAt"J]zY1mþöÎÚŠâZÃ[…°5 j^Tp "`TÁˆ€CTpQæIP”Y&p–Á—dFÄ—(cㄊâ@Q|‘!qxBb‚Yåi oÿEªÙ]·Ïpïíù<þZëÞ®áëê:çì]µ7þ½ýû·¤FÑ5OH€H€H€H€H€H€*.‚(<^|á9ùÁ~µ×º¢RÔ'ËsN«V2kætw^…È#„‚Aü³? ‘ôÊË/I½zu£\ù(<Òª3Sÿ£Æäq²nÝÛ¢6Ú£œV`E'j"BÔ_…‹½úª+åž»ïŠr”Gáaï]¼ð)iÔ(»@sùòEMЏºëÔ©#«W¾µCw—È5Ž®½êWB/^âÒ $ƒBć‡Ï÷ø‡÷ß“*Uª¸ìù úò-[wˆ®4uÙ­ð%–Ž>ê(yù¥å.]ý²ˆšÑrç^È¢&kDw|¸8¼+xgÒì³Ux@ÈŽw*[Èõ>©I,ÑUÖ²@^–TÞ„ñãäšNåÀtÉêø¼KÇ)‚عˆ ÿ.¸èbÑUà.uʤ‰Ò¾ý¥îÜŽK!|,KȦð€ léâEòÃþGbÑvÎHÌ%2T¼î Œ£^}úFŠˆ,¸¢¤ýAáÎên#QS®ßý¯ï'ÆqçêxZšŸ½{Ž€°úµÕ+¥r¥J.­4ÿòÓPxlÜø¾ôèÕ;£’4©Ý…Rx”·-3fÎ,ê@hÑ¢¹Ì›3Ûgûg•³V0oçÃl÷'¥Y¥¨‡FÝ2Bz÷Þ­€Oºqj:SÎkÛÎ%—Eáí¸X³z•Ôªu„L}ð!™8i²+wÕ«+äÈ#t‹0 A˜=k†´jÙÒ}·C›úõí##†sçþßâ%KeÐàÝãÝÇå:Z®>¯m£¯iXâÛté%ËSv·ÙßÇ# @Å%P…‡ýј©ëV¨Û¡C{™<ñ~—µ4 ¡ÃFˆš´p÷u¾¦“Ü1a|¦ê\ ´ê´»Ê*L°«Ñ (  <Èúô½Þí’@žPˆ`…ÃÖ¯ró V0*"’ʘ5{ŽŒŸp‡KjxòɲléîÕ>¯]ñùðCS¥]Û¶¢ªå¤†{)ïmX/ÕªUó·ˆúµ5]Cð]µjÕè:ÛI¥Ê•Ü®”8Àe³ýI’d++SÚu]»‰šÙrɯ¿¶JW±VVŠÛ·j†Cz÷íçòùÝ X‹•±ï¾ó¶»çiöÙ*W¦OÛ­`A=I!|Ÿ¼r&Ì‹ç¶æõ×ÀZM2E«Îm¾±cFKîÝ\Ô“O-¬ÔF°Š ‘ðOÍÍȱÇ7ˆR¬BÈ>GÌ1˜kÊB…比ByPbaç‚O|=v,CQ…Íù† Ä)û£Â£§ Á—¿øR„ ¬[èn(Õ±C«®ñ·üÅEͲ¹|û‹ÂcË–­ÒºM[×gÌý c›;wžÜ6n÷ç¡U„Dó<±ïO¶yР˲ÃJ­sÏkÛ¹a}“Æ¥nݺnX‡¸÷kò䜒](„Â#¶@)å<>Gñyš-XòYÖjJIÔ/š»ß&Œ»Ýçóïˆ#jF»mì<”Ï÷%¼sx÷ÊúJz(ëœú ¹àüv‘ÄîÚPA¢f³\>¿óôþ‰“DÍNº¸ùÍ•³š5sçø‡KØÁd¾#œxâ R»V-7`.ƼÐþ²Ý»I×rõ÷bG‘š³r—vH¿ëˆšårñù|§õåñH$@$@$@$@$@$PÀ¤UÁÚúVÁiÎ"ugBd;Ž)}€eo÷9—Ýh5¿åEy¹ìáû²q C>><Òª2m[࣡´A3±2à³"W°N:uj,»mOi}xX[Û“&O‰•›taîÂylÐß8½F°¾n5:¼ÅÙ{÷÷à¨&VJäÉ7ÂŽçÐîw¾e„ùÀÛ·¾I^Xþbt >XÇÕ°}n¯áÛÝe•QÞ>[¹Þ=´!|ŸB¶öΰáçÅÚVGÛ½¯Øé÷œpLr\oËûè£cùá'ÄëÀWw”ùèR­]¡ìîýðÀÞ»š*Q¶3Bõ%2爨¨>ß—àCÊçÏçó ©NÌžŒÛÇù¢R…Fì?޽ÿ(;wïܹ3–×>gäÏäß¾SìçŒåê ´þ±þÔìw+ô=—ß)_$@$@$@$@$@$@ƒ@Aœ–‡N¨Ã®B˜îã¸dé²( ~ú´\?²—.[åÅ=j6'*'éÄ:)…ãÓ0X…‡úò“ÝuZuªíéXÛá\3[€Ií‚»?8oõ?ö=¯\NZáä×çÅQM)ùbÜѦ•Vá1zÌØ¨ì\ä!„°Î¤Á= 0XaœBëŠÙ¨ŽLŽq-GËlÝIB’²” Ç»ž1Èê—è:tz ÅòBAeˆ¿O}~”¨:­>[…êûðÃJÔe# Œñí‚À³´Áö åÀ /„W¾\׬y=kÑ¡ƒ`;v-cݳ9Æ3˜÷Ø|÷ŽáÁ—™ÓjN(ÖÖ¤±Îß~û­/¶ÔG«ð( óÒTá¼}á½¥yGʪðP³2Qð.d øÜñí-¤ÂcÈ׃ãÖ­[36Ë:œFÞB(<ÔÇCÔž{ﻗÁ½OäÑÝ öö¼ÏCµ'“ ±ïÆAYúæøÌÿÝ3ÏDý ?ÃÁÈ3xï½ÑyÒ‚û™‡~e `åËÄÑrµ÷`Q…Ï…»³¡°f      Ø·DáŽ/XH«Àûßpcôãyÿ¶cG”·4 » å ¸qà®L«ÌíŽä…ò P ™„.iÖ Å€¯($’ÂÇ _M0DÙ&Oy *iïlØ¥Ù(l}B„«}[pÌ% ±eã<䫎DÃ,Ñu($Þ¸qc”fO&NšõmÑâ%Ñ9Ÿ™Vycը]¦€1Ú£g/÷*‹òôe*;)cÓ— áÚ  a˜9kvÔ+ ƒÒ$ iõ9Tx`…­úÛzÃçmWHC(åþÔœ‰½-vŽ÷Þ?+m߬œ°“%)àñLQæ T³u$)$|~(¡|^”ißL $a%v"Ùî W6Û¼P„¢~\b‡ƒ Pþø6æRØûÊr^Va­¿ös| ©ðK;>çÌ™›ˆãÇîF@» ¡ð°+ÔÁÈît™3w^bÛò´ïV&a1ʲ<ʲÃãÎ»îŽÆu¦Ï^ÔcÑàY…GZm¹¾ÿ€¨Oh+æÌ¤€‘H·!k»Û¡c§ÎçeŒ9(yýü]h>üõ¯Õy&Ó÷¥ žˆåÅ8(k° ìgY¨øÅwÏÀæƒò9 ç_øÓ(o6…‡]ˆ²C®¾\»ãã¯o¿þQù™¾Ÿà½ƒb†H€H€H€H€H€H â(˜Â?.aŠA°‚ZýdD?$‘?†m°‚«|~dÛUæ(‚kõ`‹Ü¥¾b ûCÁnP[ÙQÛ'6úBÓª34U]'0bÃöíÞú†?´ËšPQŸ'QÒ!ðRg£¶·óÅ s‘Ï! ¾K«ð€ðÂîÚ@X)iŸ?„1ØÍcëAß2¬ø÷yÑožMhšÀQŸ» DC(Ð …Ôù úÂrs]5&ê‡ïO’rèƒ6•ȇ6% ¨Òês¨ð@û ü Í­…ïòYeV¼û¾á˜$°ÂJm˜8±ùì.—p×ÇV1 Îx7¬ e…¦p0þ¬°y <³Â6ŒËßþöéX[Ôv|ìQfRx g¶/x~6„Ï‚èðY¢öÂêüP±>kµ1_¢_/vèà]ÁÞ÷Қ© +Ì_¼+!˜È²óòZáaçS¼—x'lÀøR¿ ±1v…Ÿ;ÙÆU¾&­P/”ý~üYŸ}ö™mV©Ïóí{X…GhÆ.iÃýòWQ}_ ¡ðH«-öó íÅsÁn•«Víúä“Ova÷Ò¡ÃKô yCÁ|ø¹‰ áûŒ¹Î*PŽ—ñðíN¤c^°ó.Êû|e aÛQÆkð^Ûñ†|øÃçI°“ɧcî Ç9æt˜½ôyü1äêËEþ¤º¡XI Vñ†y:|I÷0ŽH€H€H€H€H€H`ï(ˆÓrëš;[µj);Õq±u<‹ ÎBóë¥R¥JåŽåqZŽt…¦\ri‡X™xÆ-Ïn!º¹AÔ®¹¨©Xz’Q›Á:ëöñ/-^Ž9æYâ8íÑé¢öÙcñxŽ ž¬cáo%Æœãþò‰ñü§Ÿ›\mŒÝ˜ãB}wHÿ7Äre*¿Yó³cãºcÇ«åî;w;z iôÙ:-‡ch4EÕœyÆR»v-u>þF¬MÈpëØ1Ò½[×(/NB'߸ÿl5kÖõ¹!Ï<óLllß|Ó@¹ià±2¬ÓXŸpÊ)§Æï»ï½'º#ÄG»ãµ¯‘ ãÇÅâp¡‚Giwþ…±xŒËÍ›»8ï˜ÖgHš—l’ØnÚ´Yç¡ó}ÎÙú²¥‹Ó\DªRDK·‹ÆâàóTõêÕdãÆ÷E}Î : ¡£^$¨ ]êŸpR”'x×ÏR'ÞíÚ¶qåùÄ…‹‰ TÝ%ú´~ÝZ9ðÀ}rÎãŽ;¤Éi§Gù¶nþ :ÇI¾­‘÷âKÚ œÜ#À¹;žc¾Á:ÄÆ=àö“s”›öií[k£rmy…tZŽzV®\%jvÎV)çþä©§ÏbÓ›J̹>ãÚ7_—Ã?Ü_ÆÞ“p\YÇÌàn™‚®P—î=zÅ’ÛœwžL{ä¡X\i/ò}Æö•Åi¹ ÝE±æá=ovV3÷ÝA™òáGÅÒqQ§åiµíÓz¢Ê1œf —_v™àê. —/é3ÏylhÝú\Á÷’-ê¼^M]Æ>_“æÔmÛ·+Ó¶wޱ{PµjòòË+¢2êÔ©ÍõeuZŽÂuÁœ~æY±:Ãçæo9JT±å/mX½ò•èÚŸà{]çk»øKwÄg >ãU™$+^y5ê‡Í”ÄÕ§«ETéï/Ý1é³ çž×6öùøðCSuþm»—$@$@$@$@$@$@Å#P…ÇŒG§ÉÀ›%þàô]…nþ¼¹Ò¨Q\ðUZ…ÊûÓŸþ$Ý{öŽýõõ„Ç»îœ :v ££k+ØŒ"õdÁã ~PûVj2HFŽ#j–ÁñxˈáÒ§w/9à€byT?&ê`TÔTN,>邳i?ä„×azy(ïå+dÀ ³>{äÃóŸ7g¶4n|*.3†ù/µ©¥Cqñ«_>]'èÊyQ³Y¢»}’’cq8ß}÷R§víX|¾‚¾ØMy\|þùߥqÓÓ¢œP,,þÙèÚžŒ?AfÏ™E=8õrÁùí¢k{’FŸ­Âï‰î$5ïb«)qŽñˆqŽI'o4X ´ÍFÝ2BzöìQBÁ£®Ô(‰r(ƒÆß~[ %ž¿ï7ÞÔ9¢WÎq !ë£>℈þ^í¼ ¦}¾çž^úô½Þ_:ø´G–J•*¹8Ýy%jj&Qpݤ'x7n»u¬\yÅå6::‡P¹0 ºù&xãešàuèÐ^&O,yOX†½®( |&@Økp¶8‡ÒgèÐÁ2ð¦A.©Ð T¢få¶ÛK*Ø\þýoü¸ÛežæóŠëB)ÿÜ À±#'l˜´Ðw $Ì}™¸BA %§U@Àyã {„…vEø”É“¤ý¥—d*21»R DGÀ{»a}\yUšwÄ ½l‰œ|R|—JbL$Ú2nÂòì³Ï•àÖ·OoéÛ·lÖÝpW^ÝÉÝ•m7”)¶Ä)Vcõ8B¶ þF5×&Kõó"ÜÅÙèÑ#¥UË–1aﺵoÊ¡‡îy‡³+ŒwpCÈGÑ‹~ÎÅózë5RµjUßÔ2ó}ÆWuì •>ù«Œï4”ûêdݵ%iLBè<Ꮋ¢gàw =ÌIxw¼ ;,#Wù¾<±@BsÕ!\·¡¼m±eá{…š°’÷ÿûnÂa‡&ÇûCi}îO¤~ýú.k>Ê%¼ûO©e†ÎÕᜊgÞH4 Ö M›4±Õ—8Ç‚ Ìv7…ÏÔM?1Ô¨QCŽop¢‹.Íw1_Ž=â» WRá³ãéß,³Éѹs9cú£NqeNÔ¡ë‡ß=æ“QéØUhßé\ŸåPðx®Øõ‚ú“>goR…>vä\tÑ…òŸS&GŠí¤üŒ#     Ø» ¦ððæŸÔ¶±3a£Î¾åàC–z*8®¦¦  ýË_>s¦ H€€¸¶®Þ/KhówZ^%5ƒÿ™BšubÀ¶mŸÊ×_ï”ïÿûy)Âvaeü§Ÿn´¿r•Ên÷L …«ðÃûÒ¾†ùµ!.j·^MZýËõ¥–>‹Êÿ^ñžv}™ÊƒbêÓO?UAéWò½ï.uëÕÛëmÈÔ¶BŧÝg<ËÍ›7Ëÿþï7:.t¦©J3ž0¨ýtùú«¯ÝýG±ÇŒ]¾  ,Ø®¦ëþ¡ãû`îAg…Çù–ƒÕðx?0.ñ~wì±å ç[·ÏÁå_þügQ?$® Gé˜,K_Ôî½3ë‡÷ ÷[“UV€šiEµoϾt„â ÜÐ_(ÍCÓ|ÅèËÎ;UùôGùç?¿Õ6S1Yˆ6ZóˆP²AÙ¶¯¼Pæ}¥s6L^â3ÜŽí½Ù¯½Ñõ;&'5<%êÖêU¯–Øu%ê ¾Û@Ø®>¿ä_ßýËÍ…09XÚð•ÎÇŸüÏ'òå_ª©µÃœI¶½ýù\Ú6'å÷ïß·ß~£ïßQ1³qIù“â0—žÑ¬y´ËfêÏ&^¸{‡KR~Œ ˜j=øàƒ“’G$@$@$@$@$@$PDWx±o¬šH€öKPv6jÜÔõ=ÛŠêýÎÿÃN«ÓfùñÍ¢ž-]²H~Ô°atÍ“½O»6·üÛR“&¥Ù™gflü›a·›þð>w x{éøÂ Ë¥WŸ¾®6ì–yóõ×2î†ÝKMb5$@$@$@$@$@$@e$@…GÁñ6 ¨¨Ö­{[:\~…k^ÿëûɰ¡C*jSÙ®rÀ޾ÁC†9_( &â`VŠ¡¸`Îï®{îu€I.8O2óR—_yu´³ góÅíù¾W;v€^vÅU‘“v˜Z¢¦ÁH€H€H€H€H€H€öMTxì›Ï­& Œ.Z$C†wéó›+g5Û³ú?ãMLاÜwÿDCwø1°!Éß”MçùÞ!ú£À®–êSâÔSO•ãÔÇËçŸïµk/ó_k#§¨/†Â€‰ÄIêô|óæ-òÚš5QexN¯®x¹(fé¢Fð„H€H€H€H€H€H \¨ð(>ÞL$@À½÷Ý/?2Í5lã»ïÐ4KÅ{Dån‘õÑâ ;f´ôèÞÍ_òXdPF ¿e¤úúGΖÀ'ÑLu’}â‰'äÌË å'°eËViݦm¬ (;ð N?ýDZx^ ì[RSxLžò€ìرÃõ~èÁerÀ»o¡ckI€H b˜>c¦¬^½Zêׯ/#†«˜d«ÊEÀ+< ¤…«ë®í,mÛ´)W™¼9}Û¶o—1coøˆH x~­ZµtNækÖ¬™”…q `P65Ò]5C† ’ãu÷  À¾M 5…Ǿ­'  }‡À×_-»ví’êÕ«ï;Þ[úÍ7ßȇ~$[ÿ¸UÍYý]êԮ풵k×Ú©·ë_~ù¥T©REªV­Z܆°v      T Pá‘*NF$@$@$@$@$@$@$@$@$@$@$P Txƒ:ë$          H•©âda$@$@$@$@$@$@$@$@$@$@$@Å @…G1¨³N           T Pá‘*NF$@$@$@$@$@$@$@$@$@$@$P Txƒ:ë$          H•©âda$@$@$@$@$@$@$@$@$@$@$@Å @…G1¨³N           T Pá‘*NF$@$@$@$@$@$@$@$@$@$@$P Txƒ:ë$          H•©âda$@$@$@$@$@$@$@$@$@$@$@Å @…G1¨³N           T Pá‘*NF$@$@$@$@$@$@$@$@$@$@$P Txƒ:ë$          H•©âda$@$@$@$@$@$@$@$@$@$@$@Å @…G1¨³N           T Pá‘*NF$@$@$@$@$@$@$@$@$@$@$P Txƒ:ë$          H•ÀÿÿÿÇìr÷@IDATìݘÅրჀ€ ” (I1‹¨€ T‚’TrÎ ‚ŠdDD¢‘$GÁ|ÍYÁ„¢ä,’Dî9=Û³=³=³³°»¸ðÕÿüÎLwuuÕÛ=³—:]UéŽk € € € € € †ÒðHÃWª#€ € € € € €€#@Àƒ@@@@@Ò¼4 i € € € € €ðà@@@@@@4/@À#Í_B€ € € € € €<¸@@@@@@ Í ðHó— € € € € € €î@@@@@Hó<Òü%¤ € € € € € @Àƒ{@@@@@Ò¼4 i € € € € €ðà@@@@@@4/@À#Í_B€ € € € € €<¸@@@@@@ Í ðHó— € € € € € €î@@@@@Hó<Òü%¤ € € € € € @Àƒ{@@@@@Ò¼4 i € € € € €ðà@@@@@@4/@À#Í_B€ € € € € €<¸@@@@@@ Í ðHó— € € € € € €î@@@@@Hó<Òü%¤ € € € € € @Àƒ{@@@@@Ò¼4 i € € € € €ðà@@@@@@4/@À#Í_B€ € € € € €<¸@@@@@@ Í ðHó— € € € € € €î@@@@@Hó<Òü%¤ € € € € € @Àƒ{@@@@@Ò¼4 i € € € € €ðà@@@@@@4/@À#Í_B€ € € € € €<¸@@@@@@ Í ðHó— € € € € € €î@@@@@Hó<Òü%¤ € € € € € @Àƒ{@@@@@Ò¼4 i € € € € €ðà@@@@@@4/@À#Í_B€ € € € € €<¸@@@@@@ Í ðHó— € € € € € €@ª<Ö¬ùM>þøcÙ¼e‹lÚ¼Y6¬ß k×­“óÏ?_òåÍ+¹óä–‹óä‘[o¹EÊ”)-™2eJòUú²aã†Ç/^\Š.œ`;ήE«Öò¶ž/<5kÚDzöè¾™Ïg @¡Ë‹ø¶zà€þR«æC¾û؈ € € € € ðßHñ€Ç_|)mÛ="»ví:)…6­[IûG‘,Y²$ZD‰Îø <Îø[@@@@8ÍR4àñê̙ҭ{Ïd#+?¿Œeœ.|yÔ2 xDåa§ ðà6@@@@@N/ x;vLúô}V¦L™šìZ9rä…óçI¾|y#–MÀ#" ;âxp+ € € € € €§—@Š<¦L&½Ÿî³”-$~ôßeíÚµ1c#=æÍ-9sæôÍOÀ×… Þ"€ € € € €§@²<Ö¯_/w–»;*-FÞ¼YS©S»–äÉ“GÒ¥KçäÿWƒ_}õµÌœ=[,XµŒ‡|@蛇€‡/ =<<¼E@@@@Nd xXÀ¢nýb •GJåË—“¡CË\)‹³}ÎÜyÒù©.Qó,c©-R$A HØ&@À# „ € € € € €@HÖ€Çҥˤݣí#’T®\I†}^2fÌ1wÇ7ß|+-[·‘]»vy7ßW¸÷^yįàg÷ W‚×H<"ɰ@@@@H›ÉðhÓ¶,_±ÂW¢P¡B²BGddÈÁw¤?üø£Ü_¥Z¤Ýòê•rÎ9YBöÇðøå×_åµ×^—ßuí7Éþýû#W\Q\l]‘R¥nЏNHÈ }>Øh—>üP~ýåWÙ°q£¬Ó©¾6¬ß ÿ9"¹rå’ÜúÿEŠ–råî’k¯½V2¤OïSJè&³øeõ/¡õ“Ùe…‚Û¿ûn¥,{ã Ù°aƒÓ®Ã‡KÑbE¥x±bRXÛUúÖ[a,,ìͶíÛeÅò²vÝ:§ü?vý)—( Å´übE‹JéÒ¥C®Ç;ï¾'{÷ì +E¤lÙ2rÑE%Øî·aÇŽ²äµ×ä·ß~—›6óçϧ‹×ç“‚—^*•*Ut®™ßñ~Û’#àaëμÿÞûòÙçŸËæ-[œzmݶM.Ìž]¬nyóæ•âÅ‹Ë}èËš5«_5bÞv*Üc®@@@@@ÿ€@²<,XpÍu7DlÒ”Iå¶ÛÊFÜmGž½dúŒW}³ÌžùªÜtSÉ}‰<6oÞ,]ºu—?ü(ä¸ð¶ÖÈ ýÅF¦ÄšŽ;&o¾õ–<ÿü0ùuÍš˜Ë‘#‡ôèÖUªU«\ÏÄïÀ®Ý{ÈÌ™³ìz¼SGy¤ÝòfÍoÒ¹K±‘1Ñ’oÄð¡RFƒ±& ÚŒ=FfÍžõ[“¥oŸ§åî»Ë;ùJ–ºÅw„Ψ‘/$êºzõj;î•D×sq+tËÍ7K›6­äŽÛow7E|=™€Ç_ý%&N’)S§ù¶Í虜æL›Ö­åòË/óÛqÛ©pXv € € € € €ÿad x,^òštx¬£oSKßz«LŸ6Åw_,mÄBõôÍÚå©ÎÒºUË}ÑÕ5¨Ð´y˘;ª­àjU«8 ¤'6×îÝ»¥Qã¦òý?„Ô'Ö6êaì˜1!#$¼ÇF x”ºé&iÖ¢¥8pÀ{HÔ÷-[4—n]£¯“b¬\¹Rê5h”¤²ï»¯²Œ6Tn¾µŒ¯ubĦG‹Ö°Ç:´—Gi'guVÄl'ð°Q&6ÍÚZt"iô‹#Ñ(±{*Üc©y@@@@@ÿ¢@²<ißA^}©oû÷{VêÔ©í»/–6=T‹–­åÀÁ„ùT¯ž ìH5ªËÿþ÷o|bõèÕ³‡4mÒ8b¶þùG4juÁöˆ{vÜyÇòòØ1¾ëœD xÔ¯WW.Zœ¤€„{ʉ^;g¤ôå—_JÍÚu#펺ÝFžLš<Å×;ZÀc”Ž$òüШe'¶Ó.#G 8bæDò‰Ô×ÀÏɦŽuö>µ˜SáµBìD@@@@þãÉð¨VãAg$€_{?xÿ=]k!¯ß®Ù)àq2'³é­Þ÷m±é üÒ“Ÿ’¹óæûíJò¶gtJ¨† ê'8.RÀ#AÆ$l°µU–/{Ý7Àò÷ßK…Š•5H’PdLY#<>úèc'pS!‰dz®__©[§Žo®¤½{…l³I x|ðÁ‡Ò¨IÓe¸¬ü:"'|Í/¿úJš4mñ~²µgl o:îÞóó@@@@H«Éðøã?䦛oõ5°Î~ëôOÍ”XÀcʤ‰rÛme}«ôÃ?ÊýU(ÜÌ¿¯ù%ÁºžxR,HiÖ´‰ôìÑÝ=4Áë'Ÿ~*õê7L°Ý6Œ}i´Ü{Ï=!û xŒeœ”»ëÎcÜËÞX.·ó_7ÂoQyÍÐ\A”Ì›+×]w­ïn[¼½vzQƒá Ì”.{»ø€lÚ­9³^8ØÑ£G¥ÍÃí|GTبo¿ùJ2x‚2Vé¤<žxò)™7ߺ²n]ž’–-[ø:ØF[xܦ{óK6ÊãÓO> ©[j»ûÕ‹m € € € € €@ZH–€‡=Ío…J¾íoÒ¸‘ôîytïA'¹1ZÀ#ÒúÞSF[ã“?”<¹s{³Ëì9sÅ%á©©¶½`Á‚ᛃŸ9"E‹‡ŽâpwúÕ3ZÀÃF ؈HÉ U«Õïø!AëxÿòóOC¶÷èÙK¦Ïx5d›ûá±í¥CûGݾ¯_}ýµHb©n½òégŸùf xØZ!¶v…_úåç}UÏ©Îwê(´{8${¬hÓY%drOm”G›Ö­ä©ÎOºY%R,CJ¸OÌ@@@@@ $KÀã/]ˆûêk¯÷¥¨T±‚Œåß‘í{@2lŒð°é¾_ùm¢g°…¾m4„_²©•J–,é·+â6óÙ³g¯®åð—üuà€ØâÜt^ûö{Îw‡¤<~ýåç©‘ü*3sæ,± ‰_úñû•’%K–à®H[Pý“>æ‹öfà ÁòÒØ—}³„<¢M5hàß2Â7Úùü<÷[Ç#RûÂpŸ3wž³&Iø¹ìóøq/K¹rwùí ÙíûQ­j>lh0¤z¥”{ðļA@@@@Ò¸@²<Ì RGmÉo”9³g¦*S¤€Gùòåä•—ýG8x+¸uëVg= ï6÷}b[Dûí·ß›¢éçŸW‹Oü:áÝò"½&%à«ñò+¤MÛv¾§ô<¢-œ]áÞ{å¥1þ ‹‡¼pá"éøøá›Ïᆛȇ~ä›÷d7Þs÷ÝòòØ1!ÅDº_Ã/Ž-Ïr¬ûá½wÞ’K/½Ôýõµd©[|n*)³g¦;îQ+ÍN@@@@@ $[À#ÒT<‘NI£H5ªËÐ!ƒ=õ‰<þÕ@Çòå+dˆvޝ]»6Ñs$–!)²eËÈÔÉ“+Rb xDk¿­ÝakxÄ’¢-ð(w÷½ÉâæW/›‚̦"ó¦X½û<#S¦Lõ|˨7³-o ’‡§ùóËûï½ãl>îáõá3 € € € € V’-àQ«N]ùâ‹/}’{Ý ß“x6¦vÀ­Û>¬#;מªœðÛSðˆ¶&K÷n]¥Eóf1µkÃÆÎš~™Ã‘F@ø›ÔmW•(!K/ 9,Ö€G¤©¶bÍ=i,åœ w·~¼"€ € € € €i] ÙÏêZã'Lôõ°62àD“ $‡NPÄwÜ‘`…Ôx y~¨Œ:eR‚Š&qé xìÝ»W®»Á’ºuêÈsýúÆÔš÷ÿ÷?iÒ´¹oÞð€G¥ûªè`?ûæ=Ù¥o½U¦O›RL¬»ïƾ<.äX÷êムóÎ;Ïýõõ‡jÊ7ß$\?¦Há²bù2çØSáµÒìD@@@@Ò@²<¾üòK©Y»®oÓsäÈ!¼ÿnȢؾ#l|C§Šjû°ÿÚO<ÞIÚ=Ü6äÈÔ xX'¶ufGJ… ’ÚµjJ‘"Eäb]ðÛ:ÈÏ;ÿ<9__3dÈ ‘¦r:•ãÇËe…‹ú6éšk®‘E æùî ßøò¸W¤ÿ€á›Ïá¦ÍZÈ{￟ oãFäéÞ=l?Ù ±<,ˆgÁ<¿dæ‘XŠæi»‰^qŠˆ–/¥Ü«;û@@@@@´"l…qó­e|f6ŒÇ;u”GÚ=œdë®Z­†|ÿþÇÎ3Kn¼á†}©ð§úÏEèÔïøX'“>}úúy?DšÊéT<¬~‘êeûb]»Â,·…ËýRxÀ£K×n2kvè:vœßtT~å%u[¬%¯½&í;tô-ÞÖƒ±uaK[¶l‘2·Ýá›Í‚aúÇTRÛÝ·RlD@@@@Ò @²<¬íѦµ²ýo­X.—_~™½9}ðÁ‡Ò¨IÓˆù]ý“3R›!5ím/K—¦$òÖÁÊž7g¶¤K—λ9äý¾}ûäÚëo Ùæ~8ÕV­ÛÊ›o½åV'äuʤ‰rÛmeC¶…°é™¬“ÿÀỜÏá›6ʦòK?¬úNÎ9ç¿]'¼-Ö€G´…×5j(}z÷J´+Þ|SZ·ñöuëÚEZ¶ˆŸö+µÝ­<@@@@@4"¬Ÿ~úY*ß_%bÓm¡çy:"£X±bóxw,[ö†<üHäµ?zð<(á”I©ðˆ4%U¤ºyÛ7î•ñò\ÿÞMÁ÷§:à1wÞ|y²óSÁúxߨº‹-Ì™3{7‡¼ºO_™<%tÝ o†ð€Ç￯•ò÷ÜëÍ|ïgÜéyc#1lDFxêÕ³‡4mÒ8ds¬atç]åÅ`÷Kþï=É›7¯ß.g›|º¿jõˆë“¼ÿÞ;R þàñ©í<1o@@@@@4.¬³<äy=楈,ôxZŸŠ¯VµŠd̘Ñ7Ÿ-NÞÀ ¨æv`¤ÎæÔ xÔ¬UG¾üê«í°µ;–½¾D2eÊ”`ŸmX¸h±tìô¸ï>Ûè×Éßµ{™9sV‚cÊ–-#S'OJ°=|Ãò+¤M[ÿµP~ü~eÈ+;wî”R·”/"øÙÎùÒèQb×Ó›Ž;&/Œ|QF¼0Ò»9Áûð€‡eˆ¶p¹_~o¡ï¼ó®4oÙÊ»)øÞoDJ¬+ÄÚ3løˆ`yÞ77ÝTR&MqÊ ÁCdÌKc½‡ßÛ( ùsC§ñ:îÁ ñ@@@@HÃÉð°`…u\¯]»6*K]À»aƒúÎW¶˜÷î={ÄFˆ¬úþ{ùì³Ï#®âúpÛ6òäþƒÔ xtzâIY°`¡[­×ûî«,}û<-Ù³gnß½{·L6=bº›ñT<¬´ï ¯¿¾Ô­R‚W v´mÓZJ”¸RŽý{LvìØ!&N’_׬I7|ƒ_cÚôÒ³Wïð¬ÁÏÏ$Uî¿?$PfSfÍxufÄ‘2vðO?¬J0%)õ68£<‚ {c Š÷ìÑMJÞ?=Ù¦M›5èó‚ØˆHÉÚó@ v§¶{‚ °@@@@HƒÉð0ƒU«VIÕ꤇u0Ϙ6%Áè÷„©ðˆ¶¨µ[Ÿòåˉ‚6oÞ’h È=æ¿ðض}»ÜZ:úZn}“úêð8ªÓ?Õ­[ßwÄŒ[~Ž9¤”ŽªÈš-›ìÐú}öù× ±cü¦³²íI xXþñ&:kÔØûHÉ‚xçŸwž³;± ÏwÜ!Æó]ã%µÝ#µ‡í € € € € €@ZH‘€‡|úÙgÒ¢eë¨Ñ'uU‰2mêdÉš5kÄÃS3àqäȹµÌm‰ŽH‰XÙ;þ «šžèÞ£g„ZFß\©bYöÆrßL~˸~ýz¹³ÜݾÇ$u£M5{ÖLÉ>}‚C“ð°µ85n*òI‚²’ºÁFƼýæ É;WÄCSÛ=bEØ € € € €¤ xXûm¡ç-ZÅ4ÅQ,^·ß~› ú|ÈQ~Ç¥fÀÃÎoôÖiak¿:Ú¶.Ou–/¾üRÞ~ûYþ+«XbSM%¨¼n°5Ll}Šëo,é·["<,óW_-­Û<|RA$›^jü+/Ë\à{þ¤<¬={ö:뮼÷þû¾eƲÑF¼2n¬”¸òÊD³§¶{¢" € € € € ðHÑ€‡µÛÖX°¬í‰u{"ɦ1²µ0*UªÓá©ð°JýñǺhvkY¹reLu|ªó“ÒºUKiÙºÍ>àa ²EÁû<Ó7¦ Î#íÚéÚ­œµ6Š÷ïØð°ómß¾CÚj9ß|ó­}LR2×í Y„=¼€ xX6Òcä‹£]”=ü|öÙ¦±¦»lÙ"N ?.µÝÃÏÏg@@@@@ ­¤xÀÃ…8xð,]¶L&Ož"ßÿðƒ»9êkåÊ•¤b…{¥|¹rrÎ9çDÍëÝÙ¾CG±µ5ÂSýzuåپτoNðÙ‚7Ý|k‚í¶añ¢rõUWùî;xð Ó>g£êÖ©#uh/¹r]䔩®ô—Z5 9Oßgû9‹‚‡lÔ÷Ü}·¼w£uö¿ûî{²xÉkÎZ$k×­s‚Xòç—+®¸BŠ+ª‹pW—K/½Ô9Ä¢¿áÆ›ÜÃC^ÇŒå\ÛaŽ;¦kt|.³fÍ–E‹—„í ýhA±jU«H£† ‚çÍúéªk®ó À½0b˜³0zhlQòù È”©Ó"^k÷¨ÚµjJíZµÄ¦Ø:‘”Úî'RGŽA@@@@Nµ@ª<¼ ݽ{·lÛ¶]¶nÛ&[·n•þùÇé|ΩÖ9sæë¼.^¼XÔ'ô½åý×ÞÛâÛ¿­Y#;vì”cÇ9Õ+|ùårñÅËYgõ_«îIÕÇÚê·F†ú¥N×U³v]ßòé®’Ø¿¿XaûŽí²më6ùKG eÓ…Ë/¼ðBÉ—/¯)\Øwpß“'ãFkÿ&ºÍ·zÙg PØúyrç– $)XkÕRË=Öú@@@@8Õ§$àqªÍù°àÂçŸ÷Íøx§N’?>ß}ÞCž*£Fû<ùüÓ墋£\¼Çœéïq?ÓïÚ € € € €'*@ÀãDåNóã¬ãý¶;îômeùòåä¥1£#Žì°ƒl-“j5ô=ÞFð|ñÙ'§dD†o…þCqÿ] ª‚ € € € €iJ€€Gšº\©[Ù{+T’_uj.¿T¶léÓ»·\vY¡Ý6ÕÒœ9s¤ßs|×ȰÌ}z÷’F†LJxÜã-x‡ € € € €Ä*@À#V©30ߊ7ß”ÖmŽÚò’7Þ(W–¸R2é‚ç6l”ïtdÇ6]›%R:÷ÜsŦ³JÊ"ô‘Ê:]·ã~º^YÚ… € € € €))@À#%uOƒ²ëØI-^’l-Y´`ž\sÍ5ÉVÞéZî§ë•¥] € € € € R&v–Íþ#Ûw‘s3Ÿ%¹²f”—d–œúJJ=•ü#›wý#»ö‘¬çdœçgbù²Hv}=]’ÝOÖÎ-Öv•³$\’#£\œ=£ä»(³œu’}XÁûu·Þ¯‘ÌgŸ%9.È(se”¹2Ÿ.ŒQÛQ{à/òç¾£¢=‚2åñËÕöì¨ù½;?úè#éÙ»¸÷§t*¬žØŸâmÓ¶¬Ô17Íž9CòåËç~ä5/¿úJÚwèÌy_¥ŠÒ½{·àçéÓgȨ1/9Ÿ5l mZ· îK®7¥ËÞ,ê­oœTÇp|YÇå­ËOª¬`¥RàÍ4u}uæÌ¨%ÛÓû—\r±)\D*W®(Å‹‹˜?¾Ý¢í>9È'aGÌã'Lû7uïÖUî«\ÉýxR¯{*TÔ2²gÏ&¯/Y|ReF:8%Ûéœ?þø“´he¿3¡ø®¹újyįH‡±=6mÞ,µj×Õ’ŽKöìÙSì¾J†ªR € € ðŸH‘€Ç’Ïÿ”¡ ¶9ÂÇw¦ŽÐ—ˆŸ›”Ï%=æ^÷¿ß+}fl;þ¼,gÉ’^Ž»£¾ß¦#Jêú%xþ×z“s³$ÞþÞª}zÎMÁúž§Ç,îYÜùõ„ºÓ[_ë/ ÁŽ ].gÅ0ôàO±ðàs«cª¯ežž¾Q>þy0¿/šwͲJ›J¹#Ö¥Þ _e«Žl°©>¢]¯ðý•Î!íªÄO_4tþVYòÅnÇë ²ôn?1¶¨û§¾½S&¼µÓ)/ZûÜögΘ^5- WŠíéÝ9ì’ÑK·Ç\þU—ž#ýæ— Î=}FÎx/@¥^?Ëßð0ÏI]–¤‘-}žé+o¾õ–õÍ8}c¯/Y’âÓ»xë~:¼_¿~ƒÔoÐÐñ³ïa“Æ¥e‹fÁ¦MŸñªŒ±€‡^Ÿ† êKk§#2¸;YÞ”½íŽàùß\¾ì¤‚eoײâî‡oœ\YÉÒ¸…Œ}yœL™:͹ïÝúº¿ƒ‘>ׯWOÚ¶ií[¢µÛ=þ¿ÜnßÊŸ†_{í5é?ppðú}~ˆ”*uS²´ô¨ŽÊªQãAùswàï^‰+¯”±/ )ÛFf,X¸ÐÙ–/o^©ZµJÈþX?X;h;ìûo÷×0Ÿv,^¼D¬£ÜRêÕåâ‹ãÿ>Çzo¾fÍ[Ê/¿þ¼Ÿo¹ùÉ«m¸DË­]»–7+ï“Y`ó–-j¬½ÞÙ²e—×î¡d>,YòšlÔi-%Ç=“Üõ£<@@@ )Éð˜°b‡ÌÐ'ñ½)½vöÛÔJyuÚ¡M:µÕo[Ë?GO»ùJ>Oz×Ï—`Æÿ~Ø'ÏhðÁ’=Á¿HGiÄš,àÑ`Hüˆ‰E=b x<1~|ûûÁÓ iVP®»<ñto}Ýê눀¦÷$>Bd·Y$ÉÓgiÿét x8SZ¥“WÚÛLÿéúži•{uæ, x¼ì|›ëׯ«£?’?àqÇ]僿âo,}M²d9ñÀÞZ–ûkºôõ%'}ß±ãc!§³µUþøãùùçÕŽÿÖ­[ƒíjݺ¥Ô­S'$ZiwH¥ùbß~÷<:9÷×Õ×\-#Spí‹G;<&«V®rîÏÇÊu×^{ÂíZ½zµ®}òpð÷`Ùø;|Âühzõ:þÙ²e“… æ¥HmížY©÷Œý¯ÄaÞ?© YŠTB@@@$$[Àãè¿Ç¥¾öˆ¾oýürS±ÈësìÑ5+ºMÞ¨#¬Y“öŠ i~©\U0þÉÚ4àÑoV`j†s2%ó5hk²€Gã¡k‚Ùçu-šè”Vó>Ú%ã–ïpŽÉyAùÃoÖt–Fæw/ê,bílˆðo}ƒ½aš7‡%&hç{xGº· xÔÕQnò«ïW¿ü%ÝãÖ4±òójgþ°V—Šîˆ”l!îžS7È÷ëbš±k­K䎫BG?4z~ŽhŒðèPíb©tc¶HEFÝþÂ"5±Ç¹ž·]yt¯›7j~¿èÈŠFzíŽÙ0M6êbp3Q¡#…¢¥Ùü!ÞÜÈ¢‡Ö‘/úwöüõ¯ÔSkoùÏ7/  Š”¬*ßÜ!³?Ü,¿Â Y¥cK"’&·W}fu`„‡¶wœ<ò_DÀã¿t!gΚ-/ëôKöͨ¯‹–·h?ÝUrÕ³\ù{‚?_¯¿¶ø¤wiYP«ÈÒ“,+¹ÚçWŽ-8=C§ 3W x<Ö¡½_6g›-ÂþX§Çå']ÛÀòÛâòóçΑL™â¿+i¥ÝÉŽd°€G§NO8÷Ë5W_%6ò"¥R‡Ç:êZJß;ß»¡C‡œTÀÃ[ï+¯¼BF|!¥ªM¹>ðhذ±sßd×€Çüys|rü&»gVé=c¿gÃNòž9ùÚP € € prÉðXñÍ®Oö[§°2h\þ"©­ H'–¶êWÍGüæü#Ë:ÅlaínµãGRX¡ÿl]ÃC÷Ù4Bs5hk²€Gí4·úØñv¬•-µÐºlÑ:Yþǵ#ÛÊö~¾ûºÐ AxYÞú:|qV^…²ÉcHˆ”,àaðÑêë­ŸH˜öDaÉÃZ»÷ëî:‚ý>WÈ"ƒš ©ŠY™™¿}Õ‹¥â ß,ÿ Ε,98çlõi9ò7Ù¬#JìúZ l‘® c×")iíÖ¿uŒã:¥Šƒ8Çt1öÕ›É7:R*ËÙg‰M?v…^§ð²-ôÓ¦ƒ²~û?κ6W8G.»8SĵYÂëeçþEÿÛŸGœã/ËYŠëy윖ª÷]-‡uÊ9kÑØvðHÂC‡‰Mke÷ûçŸ/¹sÇOufe{“-ô»víZYóÛïú=.¹rå’üùóëÿ'ý~ñ–ë}¿vÝ:ÙªF;vhL¯Õe… I¡Bå|­[xúóÏ?e—þ¿µû¼óΗñ¶ÕÞ;vÌ5f¿ùö÷Çù;¨¿«—_nk¿åüiب±æL§÷HV™7gvxÁÏöýMÊß›½{÷:ßSûž 0Ði‡§ý£H ½gl»÷:Oäyó«þ}Û´q£3ú-SæÌZÿÜR¼Xјïg+ê?véwöwY·n½œwî9NæïÊÑsJÞ"€ € €‰ $[À£Ç”ÚQ{À9aÖsÒËÔÇ ë?ríŸØ‰§As6Ëû?ìw2f9;ÌíV,xЇ?îw¶áœÌgÉœ.I x4þ[°¬Ù]ŠD xlÐu ÚŽ^Ì?¯[Qyí‹=ÎSý¶ÑÖuÖª`p¿ßo}³Ÿ›^®¿ì\yÇ™f)»Nï©cÝ t”…›ÂëkÁo{šéº –‰ŸÛß=.Ò«N˜ûÑŸÁÝ‹u=ï5j:ì·àöU¹>L@IDATó8š`æ$¼yqñ6Y¦KðèZ;i#<,¨P³ÿjîÌþ¹­ó눟þº^F¬ißAuò›ü{,püݺŽGÇ¡&[+åàßud®ÓÅÍûéº&±&›Ž­ûÔÁìµnÞQIÁQÞÔxÖFQêgÓ´ÍÿøO™þÞÁ:»‡æÊšQzÕÉ+…tm›–l¢Ž^qFϸ<¯O>x‰Üyõž-¡o×éHªaHZ³ípèýtv†tÒìÞ‹äþ›.”úÅ×ml»B’/ #<>þäéýt-1”¾õVéót¯ç²ÎìɪïˆÛgñ¿%KÞ M5J´#=AÁž Ÿñ…3=ÑO?ý¬[CË·Ïùò哞ݻ…tþÍ™;/.Ø?q| ¨à)6ä­°9û­¼Ë ]¦‹îß§¤³50aÁ ¿ó[{zðgt†uƆ§-[ÇuXŠŒý¢ÖÎ/7Y¹ãt4‚¥ºð°õÂÓ‰´ß[Æ=*?Úœñö”ñСÃÅ:·ÂÛS¬hQi£S:]uÕUÁc¼oî©`óý®¯•å7=–uZÀáÓO?KP¾ï†nÖºVI,µÞs'åý„‰“Ħ ³óU¹ÿ~§Ó/±ãkÖ®#{öØo] “°Êý÷‰7<®óî/ðm·u¦Ož2Ué"Óñ)þ~ͧŸÍš4‘Ûn+¿;ìÙÙ½øî{ïÇí‰?Þ6˜·oÊ—»+ìÈø¡ßÉÐãÍ£dÉ’úlõ; ?Õ‚ã¿Ïîýrà ןÐ5´)Älº7Íœ1Í ¶¹ŸÃ_í)õNO<Ü\ x°fmÝhߛ˗ó¸o,¸2楱²tÙî¦`~Û`×£QÃr—v¸‡'›fnÞüºù¸´{¸­T¯VÍÉâ½ÿÇ$ôm¬¿w ê× /Ò÷s|;»ÝvÌž£¿ ¯ØïBÂò½×æ¤òPýNb÷åÕŠÛ¹¼Ê•+JÇBŠøì³Ïôžž÷[a»B¿Jtoª¿Y×è´^~)ðÚëìZ¤Ó7Mœ4Y–¯xÓ ò—AúËõ×_ïwxÈ6 W­^C·îG×*$“çCà;¨¯{߸»ãëu\–½þšlРïè1c廕ßËä=®K¥eËærs©Rîᾯö;m£õÖk'>Îo¿«mÛ¶–ìÙ³Kã&ÍœÝð˜3kf|Ö¸w'ú÷&pÏŒ×R~_ãOhoø=óÙgŸ‹ýfZ ÂïøJ+8ëOÙƒ‘’}WŸ0À xÄ牿_î«\I6¨õûï@@@xd xØÂØ5ûëô@qÌÖ ßäî\ñgIäÝ~]Ú¦µ²reÔ©‹ì©v7}ü£Žð˜cë,XÀ#½Ìzªˆ»+Ñ× °Ñ#nšù”ð8Ëý˜àuܲ²øó@@ T‘suí‰üò§ŽŒh<4¾ŒI Kê*RòÖ×F^Ø”@-_ø]lú.K™õ)z[ú| …§Ýº~ˆM+å¦ðúÎþ`—L}GŸTK³5ø“E§ùŠ5ÙuÚ¢ |ëc‹:2A¬ÍÐ\ƒCî”VT9ñ€Ç¨%Û一G xtÑÅÓ’~ÚpH:O\<äi];åÆF_Ð7Cço‘wãM¶Ö†îq”þuóß΢ïnþ)ß‚cÎí®Ñ™KržíL¹å–Ë냞 ÂcÕ/vFGE:Îê?îÑBÒWײY£#3¢¥§j^¢£¤=¾_wPzhÆ E*à_øéî¸5ÌÕÀ…­Çàü ø”ëÝ> ÿsÁ¹ÊíÉø:uˆ»ßÑžoÕº¬_¿ÁÉߦuk©Q=ÐÁiOÚwîÒÕéìë‹P²eÊHç'Ÿ™ ÉÚØ²UÙ°QËÖãFð˜3g®ØôKVÏ:µj9‡^—m¿·Œ +lJœîÝ{Ê¡¿Å÷]ú´§W¯R¦tio1Îû •´¬¸ü çÏKÐñÿÍ7ßJ—®Ý‚çsýý^Ÿ{öY¹ñÆœ#96XÇêL x¨ëý÷Ý'>Ò.Ñb;=þ„óÔ¹µ¯Q£:ÅX½à1Þv/˜77ÁÈÍѵ{÷˜îë¯W·N°l÷M7ô”Þk®o´W›ú¬f͇ÜCƒ¯Îw²OßD¯¯¹ }~ˆ>ù}eðX÷Í7ß|#]ºu©ÏõKÚ5´§ô›6m.[·ëzNêlOŸßwŸÞS’,Xè\Ç[o¹Eƒz99-PP³–j;¬œåo, )aÛ¶íòô3Ï8OìGs´ã½ßw·±Úq=ߺÿá¶mtZ´ªÎ.ï}©Ü† $)àá×ïïB¤óØv›¢.¼óÚmƒûj‡jipHó»^~¯•*V ™úmºÞ¦LšèqVn¤ëX»n=Ù££áì|wÞ~‡¼÷¿÷”7à9ýݾþ:·º_-àQýƒÇ/_zÍÃôþîÍ™=Ó ”¹y¼õ>t¨Ni×)Xn$§Ž:-^E5òKo,_¡ke êkå>ùäã2xðóN¾lY³É¬™3BŠ;™¿7³õoÉø ú·$‘ël£ü2fÌ<ï,Vq¤I‰wqî<:r¤¿ïhI Z°Çï¾òÖçâ‹óè[Ï;Ÿ`xƒ € €$",µ¬xlÜz=Uà_Mƒô‰÷bùドÔ!êîu„ÇÀy¶†G:9W;÷gtNZÀ£åH{ú,P¯OŽ8ÂÃ:¯ëê‚á6Í¥îÚI_*ný‘NÚ¶ßœuFŽK½;rFª+Pß@€&›5&ëH›¦¨óDí¼Œ«Ç—Ÿ+½t¤Gx²€Geá¦ðú>=ÝFÑhG±¦"úÄÿ—ºY“åÕ¦ËÚ·fI»ûs˽×g;¡rGëBãË¿±'4K™âHgí„OJZ¤ît4ŠoSFÍÖ`EÆG ¹çù|õ_Òoö&ý¨½&ZÎøö…%gÖ@ j±–?þ$ËwÏs¢¯5ûÿX'Ãó¯ýËrg’Ò ʬ÷ù»ßíÓ{N;˜ãöÛ&°²ÏÅt‘’;?ËYòÞŸ¬¶ÑQvZ@m–ö¼ÉÖp±õPÜãmŸ}—nÒ2òæÌ$?o´i´Ä­gbå¸é¸Œj“´Ÿ|ú©<Ó÷Y§€[n¾Ezk'¸›ì)ÔÚuêº%«vÞØÓÒ6õÎ~Òçû~·Þz[öìÕÎ.M¶ú´)ΔÁƒyó»NYÓN§pq’~•mª˜[Kß"Å‹“,ºÎºµëdμy§…í«®Í]´`¾NIu¶sH·=Å:o-Y'¤_'³í³éMZ·ië²ËŒéSƒ2ý´3îÃ>v¿îÎS¾ÖI|é¥dÿ_É»ï¾'_}ýup“Æ¥v­šVl0µnó°ó±ÕïÅF8Ó›¸;m$Ê„‰ãk×®%M7rw9SöœLûÝ‚*Ýw°~q?[Îç²eËèô8WÊA X}ª×zN3ãÝ?iâÆ,—[ŒóZ©²–¸=ùç½#<,ÈÔ¨qÓà~ËW®\9¹ªD S}ÕªUêõ~p¿]Ãñã^Z‡œè$?Lš<9nDŽÈ}•+Ë#íN´ÄºõFxh½nÓFªèTXnŠÖî“ÇtÎ|¯ŸM‡uÝu×:Ó©ÙHšÿýïÙkß…¸û´_ß¾:Ò%þ©vдY‹àù-_¹òåœétråºHvh@eáâŲY§rý‡ °p¾“µõ;©õ·ãmZ¥†aßÉ7õ;éÖÃöO›:Ù™"Êm§s ›4 ÖÓ¶'¸†6ú$®vý“z íIô‰';õ¼æê«e v¢ú%›¨^ƒFÁúöìÙÝify-àaÖn²§ô½éñ':Ë?ýÜÔ°~}¹RïC›Vè‡~”>øPÖ®[ÜÿÒèQú¾4øÙ¦™[°p¡ó¹m›ÖRµJçý;ú}·i´lЧùˆÑ”3GNýÞ6Ôö(¼tlú¢‚ :ûûO°qžn;lš9›ÎÈÊ›4yª3Å]×jÔp¦g²í6UÓ]wÝ©¯º#J²ûó=­·o¿©óh GÏg¿§Þzç͛ϙvÍŠ2{Z?˜4©R7IÉoÔúýÍûJ[¿ î¶ò  Wëz&ÞTG¿SîýæÞ—ö½¿öšk¥h‘Âò÷áÃD¨ çÉã=Ì÷½<xP[­¹z¾eKC¯yøAÎï^ÜÆY¯Î xë¥ûí>tPÿ>k¹ö½-YòFùë¯b#6~þùç@ z>Ûþûm;Ó{á‘GÚëeùŠ)*·Ür³œ­…•ú»÷ùç_„ì·úÛ÷ïUáäM þÞÜ\JJ\aoôïê_û£þ½±Q*¿þú«ž'ޤ™"»výáíÜ3… :ÛÓiå¼÷Œâìûl?ÇÓêm÷réÒ·JáÂ…éß¾Ö¿mÞ¿oV—Aƒ8ß#·Þßêƒ]»éÿ6Ðã­]ů(®#¢ªê”yœð*ýßoØ(«¸ý·ß~›tíò”{8¯ € € €@¢ÉðøzÍyfæ&÷ß&2_§‚:+‰kDª©ëÓúöoGwájû7›âþMé~tò¹ûwì9*Ïj§wÜ¿™dú6¥•ÿˆˆoÖü¥m¬=`k&ØH’ qìË¿Þ+c–nsÊÉn‹?vyð|áo¼õͦSZÙˆKÓßÝéL'åÖ÷‘ûóHù°õ@öXÀCGYDªïccu®èµ#ªÞœ]šê”VÉ™ZipÈFxØùÒQ:wØôHn…#¼ž­ssçÊž1¤¶>†<¬œ[‹Ÿä€Ç$ F,ül·s|=1B;Ý“šö鈚ÆM;ÄÉž7o^yyìï¡Á÷³fÏ–):m‹³v0õîìXÙîô#–¹–2ëT@áÉFØH ;þ}Rùپτdi«õÛ°q£³ä ÃåòË. îŸ;o¾vþNr>ÛûÞ€Çɶß=É}÷WuÞº÷­_0|ذ×ëwÞÕk:Ì=LÊh@¤[XÇ”[–eš«Ó yÏôí'6Ž›†ë¨['Á›lÚgˆn´úXggûF_xˈå½M-eSÙýYY§R±i‰¢%[Ÿ •½,¿¥Þ½z:¼O4QC×/¼ÝK–,‘—Æž·ãm4‰µË›lMˆŽ×NÈ]Îæ<ÚÁ;îå—‚‡ÿûànpðü}úôv:—½eÕõºêÈ [‡ÁR#½½Á5ûNÚTe–ìºLœðJ‚õm¼ßIkÏýN^ãùNZçg`*²Àõåº*Þ«×ðç¼±üǦܲàŽ{üÔ)“u~ÿì ýA;J;?ÕÕÙní±@¤Ì´@uZ»×ËF:¸ÉÖ±iäÜò­ƒ6|$ËþýI»G ^›‚ª®gÔ*[°p‘S¾MñV%.àážÃ:±»tíîì/¡ëì?‘dí°@›%«ïRO;œúŸÎOuq‚4¶@ÿ~bA¢MVï®ZoKìSo €Zàò þ¦š¯ý¾3Ê 8ÆýÇÖEy¸]\@Z·ÙïÊË/ j×kÐÐYñúÛÔˆO>Ñ)Á(8ÛŸX²€ÇCÎ4fœÞkîwleýκ÷‡l*47Y½öè:î~»¿lºÁðµD¾üê+éÝ»s˜ÕßÖÂùôÁ]g‡Ý§V^çóÊÍÅÛ×`ȯò÷?Çœrò鈎a:2È»v‹{Þ]:º§Ý˜µÁzÙyG¶.¤£@£Ü|Ñ^­óºo¿þN=n¾ùféÑ=ÐùhÇØÓÍÖQoå–ºéFéÕ³§oQö4õ®?¼¶økx‡’ïAqíÉYëµ'˜óë:´ðKpp¦ŸÑÖ \ó!öDÓßv¦r1GK/¾ø‚³ÐnàSü­Ón£$,uÑþ2ú„«¥Ãúôñ×:BÄΟþ¬ôÎôKö„xxúKGzx;-Ì›2eˆ·üádžeoý‹t­[¬9<ÙÂÒö³tÉÅ;Oµ»yìÜ‹ôÜV~›V-ä~]Æ›V}ÿ½tÓ§Úm¿<êtz'’Üv¸ÇzÛán{J§ˆûQG-YÐøêëë¸ù£½ºõ¶<ðð«÷~¨ß@±µ¯”s~øáG2pÐàà)ÃëgÓ ÚbÚ–lôGßgú„Œ( à xÔÔLîýégå-ƾ³îýa÷§7àÑ@·…ÉÝýH*Q¢„÷ðà{›Ϧ÷²dù_7VG/äv>oÛ¶Í™²Ðù ÿ±Qz‘Öo1{ áÖ?«®á1Mƒ}n²¿7Έēü{cå¹÷L´ë÷ÑÇŸH]ÜÜêcA­—uÚG[cÄ/½0r¤¼ùæÛNûÃÿÞ?­Sè}¥!KÍš6‘5âÿÞ¸eÙh­Ou£G8›nÑÿ=á/Ý<¼"€ € €‘’%à±DŸ–ŸlkKè?º.È’>ØÉé¤IÙn„!ºÐ²MÇ`åŸÌë”Nþë ¶Na·ü>ºvG ](Û›ž™±IV®?蜿üµY¥í}¡OÈ»y½õµ5<^Ñõ:Ü´íϤýËë‚k?ÌY7»Ô9­å±€G‹~ Ö#¼¾µüªk0è´Fêжr.)m6·è×_t ­óõª“O.Õi”ÜÔvÔZ xü<¿ëíÕòØZ"Þ4vév xìqʹU×Þx<‰¾jýݺÎñe´ã¾c¤LܺDòе|[ïâáѿ˿ÿ:-ìùng‡÷ÕÖ½¨pƒÿµpëþZo<4 a÷s³{sIå’þ­_üMví· —†äªv‹«ë¤õòëÖÃN¾æZ^¥¸òÖèz%]¦lp¶Ûu|®Q~)ªSbEJ¯±[&ê÷{6¢å¥Î´W‘ò‡oÿìóϵƒm€:שœn–îݺ³ØtU/¼ø¢v–¤söÕÎ[›ãT¤o¾¥kcŒvêQ^§P²EÝôâ¨Qbû­žõëוZ5C§›²'ûÖ§Óm¿u¾O<é„:blzë³r^ÒŽ#ï-6ÝÉÆM›œú 6TE/äVÏ ÙôKvœ-|nS%5Ek¿•e ýZùvzðÁ Jø¹,HT«N`þ.OuN#dy«UÀ)Çʳ`ˆ;ÂãM5©Ö¶ýÂJ´Eâí r Y},ÿ³Ï>sR¸ám°ÏS§M{2ÙÊ·Ñ6-Qx²ÊùåW¿~²lÕ`„[{Ò¿Žv\zS¤v{ï;Þ¦wŠÔ{üÉÎδ3V/¿óxÏé÷~³VÚ¶kç´ë‚¬8÷«›Ï¦«zñÅQÁv<¯S^%å;¹B“¶QEÖ›ÚfÂøqnÑ ^íÖ©Wß©‡åï÷lß$]Ã÷tZ¬¡úd¸9\¥Í\ñ&ë mÜ´™ìÛkÁìã2dР`pÈòY À:­íxÛo É»Éd´í¶¿•,*Wª¤?™ö«[?a¢.>oÁ–t΂նŒ7Yçuw2Ïö_¡Oýx®ŸwwÌÃ-ÄÖSùéG Æ%ÝÙ-Ã}¥ÞƒtTÜG:Ÿ¯lÙ²Òù‰ÇÝÃ}_»té&?éÔO–ßÖ:iÞ¬i0Ÿ±i ÍÉ‚Ž+ÜÜ—Ô7öûZ§n}ç'x;}šopÍ-{Ĉ‘òö»ï8ùÿ¿nžX^£ý½±ãm$Ø?ÚÈ¿¯6šÏ¦Ú³ëVáÞ{£Ž‚³ÀÿÓ}žqʳü¶Øz¦Lÿ½9lø±ï³m·i¬ë(½BB@@’K Yï¬Ü+/i'·¥LºÖÀÔ'’¯ó³Ÿ÷ËóðˆK—i ÖdŒ-»µ?.ÙT?¶ðyxz[G¨Ø”@–²dE Õ½=§Ô(èĶ€G+eá¦ðúÚûiç·&ëü®RÊ¿“üb¬¿ŽXI,õÑuD®(Øi7ZûOÓYÝsyFŸD*˦ëV;t†MÅô–šZº¥èùÒéøÑ2‘Êñn¡×û#½î–®½ô鮋–'5Y|¬¶®Éâ¦'5hâŽðˆµü#¨?Xa‰¤ôúÕÑ똔ÔpÈ9ì¬É¡SŽ4. —éš,~©ÃØu²5îî^3¯\«ë¿ø¥³7Ë׺‡¥¦÷\$•n Üÿû~Ÿ¼¨£¯Ü4M¿›vÿFJ,ë1mcp÷°“4Âãs xè ­’›unó®]OM[ÖñÚîÑöú.>\tݵ×ʽ÷Ü£ëB\áÌOnù’;Y‡¼u¶ÃÙ_Y¤måyŠÜ›Ï}oAJ÷ÜV[°ý Îÿ>T¬=¤ë0Øyl=›²ÈMíÛ?&6ÙuH§ E xØT“u:-;Î ê×wó}Mjû­êØâÖëÔY§#)]º´oÙîÆ‡5@³e‹ýæ¤s‚#xžØu ÖíVÞ«Ú¹ç<,h³pÑgû7ܘšËÛÁì¹ãNÒ£g/}ò{Ÿ“¿c‡rÇ·ÇíIž—iÓ¦Ë\[t:®ÝY58àM§Îí°vºùªU­âÔ=¼sýU}Òù3'ÿ=w—×NÆÈk‹ѵlô­ïq@ïw›bȾw¶ ¶Õ×:'ë:+n |'-Øߞ뮽F;3ï‘+®Hü;¸†‹ão¼þziÒ´±[´žNëÙ£W︧÷Ó‰-蜔khßÛºõjùûr¢W¼O—[º³.€î÷ûÞZ  Q“fÁãΟ¬«}G[µn£ÓUýÜÉ%KU¥aëªäÖ§óïoðà¸76Rfñ[#⸴lÑÜYÆ›ÇÖ(ê÷»b¿wÏ=û¬wwÌïmAñ†¶îMœƒ·n!Ýzôˆ›ÆL;¯ûöqD¾ÆRïÇ:>.ëÖ¯Ó¢ÓI±ôP܈¹H粩Š^_úº“ßîÅM´r¿ç#4Øë]'ÅÍëkàž±€pàþ^8^ÔC½¿{S&M xxëÕJ§uª¬£À¢¥^½ŸÖµ8¾×,Çõ\l$ƒ%ï}rU‰«œëãìˆðŸ×õï”yY9áß_¿CNäg~vÎcS,ZP1IÛ¡°ï”V]'muÛÿÖé¤pžLÒøî‹‚}5î¿áþÑ5ú:k|ž}\§¼r;нÍôÖ÷‚s2ÈØG/óîvÞÛh‘Ÿ6tÎg^ƒ›\*t¤…‘Kò^¢ëY\/·i'Jø:ñ$þî¸~i¿üò+ùèãä»ïVÉÞ}|óœGŸt»<ñ¬ aOŠÛ¢áδZºĈaÎôXîAt±i}a©¿~R¬XQwWðu÷îݲ\Ÿ~_¹r•ü¼Z;‘¢œßê3úÅ‘!à9ô8{ê¾P¡BÁ².Z$S¦ZÀ#° qøt('Û~§Ü‡âFµèùmJ­:W{´dÁ›?úÈi§-ÜÝ´Iã`ö¬¬¸öOŸ:%ð>b„>-üáÿÙ»8©‰öã½£HPÀŽŠJ³°½vÁ‚Š‚¨ˆ‚ÒTDAE "½ÛÅŽ Ø,”Þ{Ÿ'w“ËævïöØ;vÅßü?’M&“É7Ù½×y23Á6/?Ÿpúqîx·¼¾ys}3üb?[n¬<ÿ òÚkð=Þçp=ÒOhSçP Ö#Öu[Ï–Áú†·—ÂåºÏ® ÷9}i¡®]îu{½å¼yóµGÒG:gÃ/:Ìš>—YÔŸÒûxót °y'žµçÉŽsçKϰÿþ•¥žN”~Ê)§Dô–pÇÛ›Ú6œ‘\èøLÛûwçÐÞ('MòNßú–VrŽK]²aêÞ¶€ƒ^G³ë®“Æ.u»¼¥ ¼ùuÒ}^õ•ˆýÖ°ß­[÷¨~æVWNÔù$Ž=¦VÔ¡¸‚ço©=.¸à‚˜å[0©ghÞžˆÌY|ð®Cƒ^RÏ×u8¼pêªÁAnÍüz¨{ÔÆëð1±>]jêäÒ={ôȔՆ Û´y“w¾{î¹Û›È;S¦À{^‡ æå¯V½º<¡A]—n¸©¥?¤Õ@ý] ö|syâ]ZÀãÚfÍýç3|ÏÃåøœë –*UÊÏâÕK‡´²çëafëÈ#ô÷E[ >§ê÷玎¼lëÐ|_~¥ßMë°k7´há­ÇúÇzØxs‚èy£}í¸Lo\aîûZ†ÿÞXv (ÿú«>3zž‡ŒþÌX¯ ¦ú)T®ÿ}wûûôî¥×ñöØßØG4è;uêiß·@>;ÿa‡¦óÕÑÿ-pš$qE²D@@âÈ•€ÇÜ%[ä>oØ=¥þÇJÏæJõÀPIñT$Vž):¯Æ“o-ò^-V8ÄQ±ŽqÛ-àÑaØÁûeùmÜ{7ïJz^¼ðâ‹Þpa®>ÅŠ¦H-ˆhAO—,0g“È7ÐçÆÍ§áö—¾¡Ö÷¹g3®{üø·å>ËO9¸~›ÓÆæ4°d­±cŸ‘wÞÕžJÕËÒÓLGŽÐ†æP²ylnï;£>vîðwÒ»‡zl¼ç÷N›^þUMr~­ñ½{÷‡¼óÙ¼6Q¼%k@½¹Õ­~ps¨W¶ldÚÖhíîï«:çJ8Ù÷ûÅ—^’o&OÎØòØo¿ý¼aá7àÇx÷â]¯ü›nºAÎ×Þ_ÁdÃ=Ð]ï–g¿oî>óijî_Gzæh×q¿ö¤q¿>Ð-æ\ñœÏièÐD–‚Ï_ðXÿY×6TWvâýðÃTéÕ§W„y2Ø/­Òî£nyz@ä)Îkœ·9AÜóÍ*X”wé÷Û†Ø <üzéþ:§Ñþ:‡KVÉzûؼ@v¿kk ß ïèÿîéÁÍ50wI6[ë…Õ¾cGïTѾ¿¹ñ÷Æ ÷Ÿ­ïƒ> “­gè}¼ Ù?é^Ù}~°{d™ö0a‚¼®ו+­gUz •g=@mž*÷wÃec‰ € €d%+ÍÛvJË'ÿHûJý•Ƨ”•ËÓ‡iÊêäñì›2k<õöbï?ªŠépTãô˜ˆUŽõVè¨Á ï?vµ^â<^ýr…¼©oÁûÿÑæþc+ŽåP-φt ¦`}K—( ƒÚdxXþæ¬×ž+:DVúyÎÖÞ6´• +«¾ã´ñz¼ÕÕ·ô¸Ñ–BY O¬—­¯Ù “Rʨ“¦ÛÄê.ÙðI+lH+-ßæh¸›‘.•‰ÓÓÞ„´€G»K³npçwËßæo’žÚCÅ]ç½MªÈQ`ÊI²^&ŸépNæTX‡)³¹T\Ìdæ‚MòðKåß§CrèéÏyÚhOox1-ßæs9åÈŒ7Aã9þíEá¼ôøG³*Ü9|ž,Ñ œ]‡<ŽŽÑãï«ÿ¤Í{¢ùšk¥sk§Í)òþ÷«åùI˼ããùþ¬Ó [k½6÷\>jÁ˜½O¢]§<úö}ÌÛU¿^=éÔéÎhÙ¼mÖ¸h€ö&òô3¼±÷ƒ™m¼rÒ¤D‰ÁÍY®÷ÖF´©S§ùy¬Qê˜cŽözO”*YJË*î•÷ÃÔ©:ÁpZ}ƒgH›ÐÕ‹-’öîðÊ©ZµŠ+•6!ï›Ú8ó‹/yÛm>…ð[ä6¿ƒ½)LÇ__‡e9Pö+³Ÿ/^Üû»¦.]ïó³=¥ hÁ€ÇwÞ%6׃¥>}zIêÕ½uûç-m0ÞRK‡žÒ·Ø›j=\Ê­ëo¢ýºd“ó¤ üY%ÃßzÕXºôÒKäZÖÆ¥`Ycu˜7¤ÕÓyãÁ[¾ãŽ;ÎëiãŽÉnyàè°E•³Ë–£ýÖÐýÆoyÇœ{ÎÙÒR‡%J$źî‰:vý`èÝ’=ã-o¼1îÓì«“¡Kï¼û®<ã m–v¸ÍpâñÇK•*U¼rí³ç͆–±·Ê-ÙùF šv@”3¾“¿zÃñØ|ÁþNÐ{ø…Žéo©víã¤aƒÞz<ÿxàzsö·ÁæF° ß]½† ì …g“Žßo½34£=0îëÚÅ[þck´viœN$+Ùðe\™ùÛL™®oØ/Òߪpz¤çÃÚ°°¿yì3ÏÊ»|Òtƒí-àñ NÖl鈚‡ËCÝ»{ë9ý'žëè¦eÏüm–Wôܵñ:ÞóÚotvõnÙêÿž´oßVNÉf¼÷Þ{_,@dÉæ'êÝû¿:Á²Â¿‹~¦8W¬‡‡×«'=V÷ܲ¿³ <‚õê¢C5ÚoVViøˆ‘:i÷Ç^–Ót¨¦¶À·üÝ;Gglø³¬’õ‚p½&ÃßßøÿÞ׿7iÁA;W4×nŒ³çÝR¬g&hp‰öäË.°å–þÏÑ ,Y2óßr |üþû^€î—_~•iÓ2þ~»ãëÕ«+wwÊüò„ÛÏ@@ äJÀà }DgêÉ–¬ÇÂÚcÁ50{³øg†¾™>aª6k²a«Ú\TÑÏýÝìõ2ÀÞ¾ü2ôö¬Þüue¹6Þß¡Ã@¹d½-Â=Yª}PñM\nÃ2ݦ÷Õâá¥"ž©pù”¹=u´^AwŒÈx®îÓÉß«xn²½B (é|,®~½[hÃ_Œ B§‘óe‰ieéîË*Ç x<¦“Ôÿ4o£—ïº3ËùÉ¿­“AéóëØNûþ+’ßËíŸ_ço”ÞïV³]þ£ÔÛdÓë$ï.…¯×†™³†|K-Z4×ɶÿç²zË_õ÷í¡=½õ#Ž8\"£™³øÏuØdÑ3g¦<ºéüÙ ¿”Åét˜£ìëm^Ï;Ï+æ²ËI“+ӇċQ°Í)3qâ$ooºµu’óŒÆìV·¶öƒ'áßÅÅÅÜl=ÛÊxÔ;$gÁWÎPm¤ÿFço±‡Õ׿¯8L‡ñŠ'ýØz˜¤5LØñw4ª,ÇjÐ$˜†i¯u"zW~7=ÖÄáÁãl}ð»Kä[íyd©PÁ|Þ3Y°€»kÞælÿq;¿MZ¾Œ€ÇÝðXj=<4uÒ€ÇQÕ#¯ÃÛ¡ÿ<þú?2]Vž<Î9.­‡Çâ•[åž1i ô–·£Î=sìA™ß²´}–¬ÇÓ;:¤˜K6V¬`ŒË\ZÀãñÇŸÔzìÒ1¸ëÊwt îŽkÝÞN¶†"Fè¤O”¶mo‹ë8†Æ&BµãÊìWFêp(±Rmpþé§é^=Ï8ýt¹Uç'{Øðá^yMš\!'èÛówÜÕÉûlÃ}tízoøé ½B–.³ßÁ]r[ëÖQ^í {£Õ&µuÃ%=ñx¿ˆ€G§»;{“MÛþ^:DLõjÕüsYc™ ¿d×iÁ «ux'K¹yý×\{W¾¿¡ö€±Izc¥U«VK›Ûo÷óÛÁá~šêDÓî:í­i×Ãã›o¾‘§tN»ÛoÃ](P Öiò|ûË/“7Ç÷êsöÙgÊ7ÜÐ9ƒ×=J'ØvC²¬X±Bno×Þ¿î>ÑS ž“®Ô9bn¿½ïú¤ö„ªP¡|ÔCßÕž Ïi€ÌœK•.%6ypN“=«öÌÚ}:Yç±h›>çÍ×zŸ~z_gŸ“ç÷Ðz_¹ï¡5äÛÛöÖPmÑV¿1£FJ´À ¬1Ý=o/>ÿ\NdܸW5(ö–wòÌî«K6éý»ï¿ï•ýõÍtâ÷sÝ.où«ödëñpOo¿ …–HÀ#»ëxð¡^°Ö<ÂßLjJÅñ!Xo›¯È #ö˜ÿ|X`0ØÃ#X/ Ðv¾»SÌZX ÅÚæoçµ ÜÉ'Ÿäåÿò˯dààÁþylè,÷Û­À>ö“i?Móò‡¿¿¹õ÷ÆÎû`^Ìêkß)ë‘NÁû|ܱÇE²2|Ìî~^¿~ƒÜ|Ë-¾ÓÍ7Ýè7w·<ŽC@@ÿ–@®<¬ñ¾ãðybo×Û›¡6ç‚ ÁsH6o¾¿öÕJm`µ9ÒŒ¯<µ¬œ_/­±ÖnÅ÷ðÐ‚í·†ýA: S¼ÉwšïÕÇŽ·ÞÁ€Çzék ^X}+î[Xƒf[ôì…›¤×+ûõ}¤ùR)$ Ö·T±Òÿ–êY–ù‡]~Yƒ.é×o™­>Ñêkû¾ÒFú.ñóV¹¨ÜyÙþYmµq³N©CDý³2­áÜÊowÉþRûàȆﻴ×ÂJíáaû-àqÆÑñ÷±º¹dÏI Ô;¤„ö®ˆÞÀàòG[.Öºvy&­¡Þêc=º]]%Â:ÚqÖ³Äγ⾅¼Z¸·‘ÍïÒytFù%u‚ù´§F¹}b¿i­ÕÑZöÚËǕ汥£Nožÿ¹h³D‰ ¹E'Tߦy,õlv@Ì€Ç=ZÇ%«·zç»Sƒ±O¾±H¦kï KM”•³Óö¹£]̾C}ô9·g3œþÐúöÔybÜógû{êó]0XŽ eXÛ·¹Nº:Qk{· ù²qSZÏÓÆÀ5¢—­áÐz`X:þ„ã¥6¦Ç“l‚ÇB›˜ÄëU1 ÿ“Q[²t©Ü©CF¹tÚi§É-­27èÛ|·h©•WU‡ :õÔSåå—_ö>·¹õVÈùdW„¿¼W‡©² vý­cä±Ì£G‘O>ýÔ;ÎÊ·!³*j—Òÿö>>üpˆ€‡ ed½,Ù¤ÙMš¤½M›×]³æ^ùöÕÏ&ܵ ³Ãi‡6^öîÕG‡#ùÍÛUT‡ ÑcÁ& 6K#†ó÷6nÜ(­n¹Õ+ßö_rÉ%úfø^¾ð?6ÄÐXë–­ºk—7)u°GŒå·ûeÁ*iwÒ¸q¯Èx‡Ä®÷ì³ÎÒÉ„¯ßbücœ¡•72pÝ–á~ ÌýóO/oÍ#jJçÎ"̼úù¾¨ÁŠÅK–x›6là݇uë×KkÚÉ’•oC¿•+ÙÛÈömÙ²EßÜî¡“™§ýÖ•.]Z |Úvy)îï¤ÎóÁéßI üµk›ö´{x³ÞCw㻇[ôÜùô^äK¯R\ {ý÷ßÓÞžo×®­Ñ3À;ÎæU‰5™bøP¿ {sÿý>ðÊ¿¾y3±¹‚Éz\<ܳ§·ßz}Åú} mÝ]‡Û¼·­ç#½Ò&-× Yý¹üY--àñˆ–géð×ûïëš)ûÏ?ÿ¢Ã.¥ÍÉaÏã7´ðæòÉ”Q7¼þÆÞNnŸ üζ¾ívY—Þ“â±Ðï¢;&'ˇzXfÏ™íb÷%8ѽ+ǾgýûÐáðŸÁƒF<Úh½Ò‚ji/aÜxã : _CW„¿´¿Ÿ”)ßNñ¶™Ç 2–*YÒûl×f×èÒñú}²¢ûßn»-­̈Q£üïWøû›[oì\î™±úÆúû6cÆÏÒûÑGýútÒa¦Ž=æ;Áë‰wÝ»îôÌáëþæ­ï­oz:Y{2Ù3¬³½ ?lÄùꫯ}Ÿ§ž|RÊ”I{!àîÉŸsÓ‚&|³¹@‚Ç[ãìcÚÛÊË“~J—*-ŸÎèù4dØ0¿|›¥Ó]wzsb¸zÙò¯¿x=6oÑ烙s…¾‘²ýÞ=Ô†Ys³ý͵QÙ&ƒ÷Ð4={öÒ·ú5ˆ§ù¬ÇÒCvÈceÅ›&Nœ(£ÆŒõ²ÛÛñî{ÙåÞ{¼‰µ£•cÏQë6ÚМ^OëâÒç:Éð‘#}çîݺÉÁäv{K 4ô×ÀʬÙÚx®×i½¼î¹çn? 3÷¾…´| æ„K4huW'¶-ýüöÛXG'³:ù…e±qšïÙ±c2å6|„|ñå—Þv›h¼½…b5*g:8´Á&?ä‘Þ^½mH¡hó£ØïZ×ûºù÷×|l8CëíázmYPÁ‚Þs¯ûÍÁêf÷,˜nkÛ.mH+Ýþ] æ‹w=x_ìù¿»Ó¿§Vw @öÅþý·úô´Î9‘¤°sëeû­þwêdâ6gŒûc¿‰´ßEï>k¶SN9En ÔmŸ¯õ7Ë¥Ë/»Lç|ùŸß3ÉêdsÈô{ìñ´,éç sü÷&½œh®Ãõ™± ÐÝ}‰öÌlß¾],Ȳx‘öb¶çXÓ÷ߟ©‡Úܹså‘Þ}ü!íÚš^s—ßžƒŽwÜ)«´—š¥Óôïï ú[[¨paï³ûÇþîÚß_/i½í¹³F.=ÿ ú·g‚÷Ñæ\j¦À“@@@'«+ôÙO–éÛýiÝî$Åô­ò#(&å÷)$«Öm—ùK·ø¸.O‘Bù½!{ªé<Á4U'÷ìZÎS9 xÜ;f¾_\ÿ[2zxXOŒ¡:¬‘K}uâè}uî‘xÒKŸ-÷Ö­ç +åȃõ-¥Çu_vÉæéöÜ®—?X_·Í–,zø¥¿õ­ýŒ †m¯¤½TjT*"eJ”õ˜»d‹,X¾ÅvùÉœm˜"›_$œ:úK{x¤õ±}ñNˆ~öÈ¹èøŒ`Qøˆ·œKN(#çÕÍèÝc.i°‰B:@IDATfv (fõ >OÛ´~Ѫ­2S{Þ„ÓÕgD6ü‡÷[ùýßZ,¿.Hëuàö—ÔûfC~Yoí;D{Æl•ßBy,ï+{ϵ;Î-‡¿¿T¦ÌIëábÛNÐ9DZþ/ãí}ÛÖF'ß¶=-höb<ºŽÍxtÔÉߌ1yûSo-’é=<Â×m×iÃZ­JŸŸÅÎo©léBúÌ’?o–MÚ3Ë%ZÍŽ±”UÝ\þàrªö²°FAKÖ˜g&.ýñÇòéâRÑ"E¥ÎŸamÛ¶Ë_óçË_i@$=ÙþÇú=ÑèäöÅZZ£êìô7µ-5« RE´QÔÞŸ§ç§Ó´A*Ö[á6™ºõ ¦¬òÛÛÐ6\V0Õ<ü©V½š¬X¾\fh#– wN}ûôÖ!‰2ž›ÐÜ<¬a¸Z ×‚Íðò+¯xE\|á…^´+/·®ÿúntEJ…òå½aºlƒyÚ„Ë›6n’:™s0Ù¾¾öÎ –eÃ)‡o± Í=÷v•µë2þfX9ÖˆhÃ-ÐÆöàý´óÙP2áñå;ßÓE{Ah#\z²Ñz¤¸ýÑ–¯¾úZZ°Jwž¥on[Ã}")«ë¶rí¹²ç+˜jëDÈö¶ò¢EKd¶6¬{A†ô u‚úF—^êg·¡•Þxó-ÿ³­_¿¾Y°`¡Ø³Nf;ੌžO™¾“ú=ipº}'÷×ïäV¯·Ò¤LOvïÓà\°!8·î¡;GYjT¯.… Ôû±X&}öyÄ~›DÙ‚.Ù|&NHk€m¦o­Ÿ}öÙn—·´Æâ›nn±Í¾[6,Wݺuä˜Zµ"öÅúÕu¸c,(:NxÁd¿ÇeöÝW®Ó`Œk îµ>S$¶t˜<º†î8›+ÆzÞ“=/µŽ>Jÿžˆüb¿}4 ¤ðïžíjÛ®ƒÿ{màð¸Vm"ìGûõ‹È{ܱÇh¨òbs³DûžXf1øœë,Ì®ÑîÝV vL›>=¸Ëû­ëÓ«—#ûÖØßAýÃÉêU¨p™¡¿ îï„=×.8þþæÖß«G¬gf_}f,€çž™9úwÔÐÁT©b%±yiì;ؼWîo—å‰ö›üfyì{p’•W®\YY³f­þöÍÑ߯ K{îl~ ÷ݶ^"î¸ÃõSÿ'žÈäìïd@@þs¹ð0Áw¿[%ã'ëÛ[ùô…9íáýGŠu°¨€¿ÔŒéû®TLn»°¢”ŒÒo„¡:ô”W´pöCDïà íÕpïØùþùŸl•ÑcÂ&yž­CøXý¬ÇC׫«Ír}á²­ÞÜ®þ6G„t,eÔWçðÐaƒúi•xÒ’UÛ¤Ûó "|žÔ`I´VÞFíaòÄÿÈ|}3?;_·¿b™"ÒîâŠÚ_(j•îÕᓼ€‡wŸ4Kœ÷ï¬c÷Õ^.eý2Ÿût™|aCZÅy¼«ßÿ4ØqÙÉåXÖînå}õ«6ˆÆx~Üñn¿ý‡ù­çWÈr® WY+ÿÅIËåóŸµgHœå[Шµ>¯5(X>?Qƒ~?g\¿õ”¹V{¦Óí6©ºö±çÏz¡‡E æ»ï™¿d™õÎÑ|.©,5côð0~‘üü—}4ßUè9ó˜P‹{K’eqoО=ïkÅ6QºÞ¿îM«Æ ÆëéÖ-à1@çe°7`k×>ÎúÆíŸðÑÇòâK/yû­|«oš»®{ŸÓ–ÖÀߦõ­AWFVK®êÉ'ûËâÅÌŒQ~ym\¹üòÆ2dè0ïü6ÁñMúv|´doÚÞ¡Ã_­]›v?íºì ášQ&tµãí~Úäɯ½þFÌó[½l¢ñ©ÚƵíúûôz$âZ»Þw¿ü£s˜O÷îD<Þ×É“½ÆL-çB”ÝzÁ¸”[×Ã-½ú[С÷#=u“¾bo¨»7›3–i÷ÏÆº¿çråý]Uüå 7iYšÍ®{°§d `ÁdÉOhÏ…erù2–χ×´éÕÞÁÁãmý¡‹5¸ºãìÍ룵¡5'é5}+û½wVφ Hóf‰½1œvÝiõvÝ›6m›tØ ¥û¸úg,ÓŽ·!à7n$…oA[#½Í1cß¹ŒüzÅZÿŒÏ»än =ú¨6øêv»OO=ùDËG,/¼ ßɈã\9þÖ˨õ­­¢Ceój<ñäS²L築,'ãxÛ^TŸ§¦M›ê=<%¢»óÁì¦L™¢çÓ‚õ4]’eo0 X£µ«ßèQ#"Nû›ˆëo‚õäú¹òÝqöÞÖ¦u¦çˆ™ûpÂG^ù6Aµ ‹Nïj°òÕ×^ËT¾²‚½fÂÇ?G^Ç.=J{¦„’¡¬7ÉæÍú7!ð{o×e“q{…Íôqæ¬YÒ§q•ùÐCÍrbzk¨4xhZc½÷šÕsK[Úïo7BÌzÏØöð÷7óß›ÈãÝß׌¿7s¼rÂo¬‚iÏÌÝ” Ö'ÍÇz¨Ÿ>Ñæ_råǪyf¯•ö\;D{“]³?ÿüs­^˜ž&xÝŸwÉI:÷Isí½ü»a϶íÛGœÿéýw»S°n¬#€ € €ÀÞ!'£™«Á„÷´áôç¿Ò&R¶ÿ† §Bòëýûèœe¼ÿæ ï·ÏÓÿÜà ieÇ—ÐÞñìXë q¯¾!ïþ[ꉛkhÐ$Ÿ¾µ¿KÚêäß.ÙT9 ûí ±f“6Dk:¹f)ivfZƒ¶Õ×õH±¡ŽlÈ£xÓ¤ékåå/´Ñ/=¹úºÏáåNm üÛzùP‡ÃZºV¨c¤}K†Çì«“ÈgZ,xÈÏ.ˆZŽó æ ®ÛðIWèÜ+.½¨=`>×y.\Êîx·ÿ<í)ré û¹Ã"–³n–~Xµ'‡;Þ†M;ýèRÞ„Ýû•.q|vfkùïgS¾=¯'YR.Ò¡ÏJF™ÃÃæyB‡˜²e´×Mn½›‚©ÃÐy²ÅõðЀGyíi-uמ?‹µ<{þ;hë¦ÖÂym"õéó6x›­‡G´áÈ6lÞ)6Œ×gzoìÙqÉÖ,èw­w ö°êñ‚Î÷¢½f,Ypݽ1þ™>}†ßã^½ºÚ@zK¦œöö§ d“»·X]¦jÕ”Zú¶¬ T¨Pt—7ÖÒn^ÔÆÇœ–©üóthóÏ?Oiz[#ž¦ÓO?Mll÷XÉ(ßÿCo·Mûx¿¾þÛ®±Žùþûä=Àxþü¿"²T= ª4¹â 9ê¨#¥_¿Çuî‹™ÞþGûô’²e3¾CրᛪV­ê—cª6”‰¥‹/¾Hßú¿Äßg+¹qý7µL{Ý®÷ÉÇÓ·n×xã±Û0+ËtÜô`:çì³ä¼ó΋ùv­+ËŽ±ù#Š)<Ü[_¯÷ìÓO'ʇ:DIø™°:¥&JÉÞvŽ–~ýõ7oø§u˜ª_¿ž×ÈæÞHŽ–?Ú¶7ßï]£í;K‡djzÍÕѲŽ-žë¶á]¾þêk™ðÑGú¶¹¸ÉøjÔ¨® ù—Ê!Ø“±jÃ^½£“ØÛäááûR§Nm º‘HkuKkï óë×·OFékY~'5øXK{Üd÷´{8Qï¡Í¿³y³6šR<÷0=®ÕàoðHÏQƒ1®0û^´kßÑûh¶Á¡½‚yl8"›«À½Uïö•/_Îëá`¦öÖ{8½¢=„Ü<'6/AÆ ÂY¼9Y¾Ñ{õž-—XP6=Ù=¾ø¢ ÝÇ,—ñ\‡`óÙXïœIŸ}¦sbh'= 2(¢ñÚmµœ£s1¸ßÊš5ð±òÚv zYàà£>É”ÍÜ6ÖnØ!¥´·LYmlß_«”+’ø R¤„Õ¶Ë?ú¯«ôEtµ²ú¼–Õá­Ñ â½Þ$qž`ý¦YFâ,&׳íЀŸeÖmÚ©6"•Ë‘":I2’5¤¯^½Úç½R¥J9jˆ‹§¾+W®Ôò×xFÖ`鯓çØÜȳuëVí±TvìØî5~ßÎò³+#/®ßvïÍ'§ºx=í~lÒÉåí|Ò{º»ÏE¼çsù¬wÜÊ•«Äz¤ØµÚ3½Š ¨g2“ûMÞ¹s‡þ¾”“%KÄU;îì¤ßQ{‘#ŸÎGÓÆɴ¡Ê,Øh÷Òæy²ß¬¬ù£Ìz<¬Ð¿U›µ ûQVŸ·Ýqr×–èßï™ÑºX»–¬®ÇÎiÃ-g¿64cVù£]ÿN{Vô7íÚ5RR{¡™a<ß;·¥œž/Z؆ € €{—@ž<ö..®@@à¿$`=O¼áÎô¢îñXP‘„ € € šm.”»?à söŸG@@HQ)zc¨ € €@òîíÒU‡´ZoSxÈÝî'à‘ü[B @@@˜ÄúvˆðýàûÁ÷ƒïG,~ø}à÷!Ö·ƒ¿Ÿü>ðûÀ￱ø}Hí߇X÷-úö\xD/˜­ € € € € € €Àž à±§¤9 € € € € €ä™<£¥`@@@@@ØS<ö”4çA@@@@@< à‘g´Œ € € € € €{J€€Çž’æ< € € € € € g<òŒ–‚@@@@@@`O ðØSÒœ@@@@@òL€€GžÑR0 € € € € €ì){Jšó € € € € € €@ž ðÈ3Z F@@@@@=%@ÀcOIs@@@@@È3yFKÁ € € € € € °§xì)i΃ € € € € €y&@À#Ïh)@@@@@ö”@.Äúvð÷“ß~ø}à÷!–¿©ýûë¾Eßž è³@@@@@ØS<ö”4çA@@@@@< à‘g´Œ € € € € €{J€€Çž’æ< € € € € € g<òŒ–‚@@@@@@`O ðØSÒœ@@@@@òL€€GžÑR0 € € € € €ì){Jšó € € € € € €@ž ðÈ3Z F@@@@@=%@ÀcOIÿÇγsçNÙ¶m›Ø²@R¨P!É—/ßLËE@@@@ØS¹ðغu«ìÚµ+SÝ ,è5zgÚÁ†¤ ĺW˜ÈŸ?D½ìžZþh©H‘"™6ïØ±CÖ®]+;4ØáRA z”.]:SÙn?K@@@@@ȵ€‡5НZ½Z4â!ú*IJX±bbÿOJ•+WfºOvßJ/.á Æv `¬Y5ÿ>İÁ´nÝ:¯wGø9(Z´¨×òI € € € € €ä¶@®<ÖX£x”d Ýöÿ¤ÔXmÁ©(É… ŽØc=6,ˆ-E뵫l Œ”*U*Z1lC@@@@@ !\ xÄj·á^ Õ:…¶ž.6oE´î -ÏžÚfCNEK˜ŠðذaC´ìR²dɈaªìúc=ùóÐü%¢–ÃF@@@@@ÈÅ€‡È† ë£ÖÅÐÃèQ3î­7ĦM›¢^I‰%Rfâîõë£ß+ LÙ<Ádœ77ùëÖ#$<ç‡]¿9„“•û_ |…¯Ï € € € € €y+ ›¤<Ÿ7Y¹5t»)<\µí v„ÑÝ~ìÃ;>ãsxíßµßú·lÙâ_„]>Îcbë‘)9×çÁúYÍì^Ù$óÉz¬ì’M›7ë]ΘšÅí·!ဇ•¹YóÛÒ•oy,Ø‘*×ïê¿·=×åÖ’ó|¹³ã‹¿ý}ˆðÁ‡çƒïG,~ø}à÷!Ö·ƒÿ}Åï¿ü>ðûK€ß~ø}ˆõíà?ðûðïþ}ˆýdGÛ“ Œb­‘ÛRø*P  <‚èÇìmkÖbËÖ­M|Úî¯ ý£Á?9î^ÙÙ]ýlÝSá¡·ìš¶ê5¹¼¿Ñƒi9ƒC{…ƒ"®,– € € € € €ä†@®<ìMþ­Û¶e´ž»Vt]Zp#znT>˰FþmÛ·Gu°Þ™{8$ç*¼^(… Ü'‹ÊÔ‰ÅÃ÷Ê»·ðˆ’¿°HRåš’#ÉY@@@@@RA ×v1Û,à%Yúå  l·€G”dŸT ää^ý[®) 9›@@@@@þ#¹ðˆ6Qµ9Z°#ZC¿5¤GKy5åryméö‡—ÑÊÊn[pØ%Ë»;建¹ãƒŸƒçôq×ÌmÝ•\zǦ/eëñ–eåǺWVF¸žvΰ“«c´{ëêèò—™ê˜~ƒyÜz8¯•ëʶ¥Û^ºãs² –>.ìaû]þp^W—ðv>#€ € € € €ä­@®ñ9‘ @@@@@RX€€G ߪ† € € € € €ñ ðˆÏ‰\ € € € € € ÂÉÛ~Ë™õ¤ùiÇʸɿHÿ'»Ã#–6üÖmç/%‹ŽØüðõì2bâ2cáÒàf½ZÙ}¤×ÕgË¡•ÊúÛÜJ¸>WŸt´Ü7îùjΗÅ_/RHº7n ªáo ¯X@hà„)òÊ”_û¼Ï-ÔÑk>Nú¾ó¥¼­–¼¬\Pû0o@@@@@ØR*àa½®øšÌ×^–Žª\^FÞr©ÈŸ?ÂþÕo‘>ï|åo{£ÃÕRµliïs0ÀЪa]™·|µL˜ñ‡Ÿ7ÚÊû—ó&Nw½A‚yžûrzÌ`I0Ÿ­jq¡Ô?¸JÄæ`}n8½¶ÌÔ Í7¿/ŒÈþÐõ’Ó¤QýšáͲjÃfi7ö=™¹hy¦}Á gQ]Jhð佟æx› xuXG@@@@ØR*àaÀ6ÄÓõCÞð­;]x²49ñhÿó’5ëå¢~/øŸ»^zº4ªw„ÿ9`°·lóöU-SZY]*_F~œ¿H>ùu®¿Ï2œyd ésÍ9~9¶2gñ iª—*”*á•qtÕ ^@aöâ•òìW?E”óÕ7Iá‚Ü!¬Ûhõj A‰:Õ÷—BòË´¿–Èßÿæv‹íûΦR:4TVç?’OµÞ.Y¾3kÖ£ª–—…+×Ég¿Í“…éÃy¹<¶$àÔ`@@@@@`oÈ“€ÇÙGä Xå}KIþü:!F ˜8U†~ú½¿eü×ÈþéstÜõüùlæn×Þ.= C[]T'cªïÿüGZ~Çí–ZtrÓÅÁÛ9è£ïdôç?úùlew[´ÇÍ®ˆ’"?Ø<+6— @@@@@d äIÀ#'mžŽíŒh9,cŽ l<Ùü|™ôë<éôâ¯x ¼ÖþªL“އ vìã×—)¨b…Ø$éÖƒcUúäç—Ô9\îo|†_ýY:tÔâÕë½ÏÕËï+Õtbñhi€Î=òLúÜ#·s¼´8ý8?[¸>Cn¸HêTÙß\ sÂAšo|&ã§Îò²Ûµ¿Ú®‰”/]"x¸·nó›t|öƒˆa³v'àñÕ¬¿¤Ãsd*?¸¡ãù'IÓ“k7±Ž € € € € €@RR2àaóuî+úóQ:_tª Ó‰Ã]p¢÷UgËYGäïw+áÛ¯–*û¥Íïáò—¯éäß½ßþÒÛd„‰]ZD Ž ¯ÿa¦ôxósoóÅ:9x7$Ü¥p}&?Ø2Óœ$.ï _Ï'ÞÿÆûxŽ^Û#z–vîÜ% ã÷"é|Ñ)rÅ Gyû¢ý³pÅZiüäKþ®Ý xÌ[¶Úë¹âeåÑ«ÏÉr‚õ(‡° @@@@@<È“€G™Åä˜*ÆUá»/>E*Dé©`‡''wžwÌ!ÒãÊ3ÝLje0À`ŒI]oÈrØ¥Ÿæ/––#ÆûeL¸§¹D›¼ÜeØ´u»lزUÖoNû[ÿ|æ|§©[Ê*àQ¿FetãE®¨LËOùS:¿ô±·ýÔÃÔáºÎóÖm²òs{?ãç¦CYÕÖù?b¥»vÉ Ý†û»w'àaßý™¨ó‚DK6'Ê«šÄ ÞD;†m € € € € €ä•@ž<®¨¤t¾äÔ„ël ÷žy?bx& ¦¼Ú¾I¦ ½ÝÉ‚ºÕ+ë<± vÌê›åœ^Á„qm¯”ù5´ òå¬ù2aÆòíû=LÜùÂˬá}ácƒÃHs—®’&^ñ³g”±Œ7ïÏO²»é:™úMÃßòÏ\érÉiÒ¸~Íà&Ö@@@@@Hš@Já}~Áé+± V¬‘Ëž|ÙÏþn§kcæ25ôºÌÔI×-%ðÖÉ•ÝæìúrõÝG– € € € € €$] ¥#'M•!Ÿ|Ÿ ÉÏßv¹‰Òà `°³š$Üö‡{|Úåz)U¬ˆ„'þnÿ¿¥é)µ$¾|vXD7ùgéûî×Þ¶pP#XŸð¾ˆBôC0¸Òjݦ-ræ#cýìO_œpHUÿsxÅE'uáoN$àaÊ5ÑÉãçkÐÅ¥OÔ¨´‘@@@@@H” xÌúg¹\7øußÉzuÜõüYºnƒ·­ù©ÇJÛÿàïw+Áƒm Qåò—#&N•¡Ÿ¦U*”*!ïÞ}­·û#³£Ë¸O¼õàöà±nÝæùjÎïc8¨¬OxŸ;Þ-cï_û-gÖ“– ë¸Ã2-ÃC`%ð°Âߟ6Gº½6Ñ;ϵ'#Î?1Ó9Ù€ € € € € €@2R2à±eÛvi>ø ùsÙ*ÏÆ5ð={´ö}ßkDËKäØj•ü϶ 0ØçªeJ˳m.󆟲ÏÁ4Sƒ*ÍA•`å‹™óåŽç?ô²gðX¸r­4~â%¿ØpP#XŸð>ÿ ô•¬O¾?Yžÿzºȳ­/“#*—ó?»•õ:ïH‹!oDôÈH4à±uû¹¨ß Þ„íoßÙT*í[ÒŽ% € € € € €)!’~+Ï|ù“dÃW½pûR¸`ïs——?–~þÓ[·`Æ ·_.Å ò1ƒ·ñŒ#ªK×F§I™ÅÜ&™¿|µ´õ®ßkÂv¼Ò®‰T/¿¯—'<ŒÔ½Ÿ*ê׌Òê׿—IëÑïÈÆ-ÛürÃA`}ÂûüƒÒW² xü±d¥\ýô«þ!„tã…R­\Z}mǪ ›¥×[ŸËÄßæùùl%Ñ€‡•ñßÿˆÍwrþq‡ÚG € € € € €)%'»Â`p!«+®PeéÙä,?ËOóKËãýÏÃnºXjWßßÿ¼|ÝF¹¼ÿË~¡É GI§‹Nñ÷ þÆô•ZU+x’Oÿk‰,\µ6bw‹Ó“ÛÎ9>bÛÍÃÇË´¿ûÛìšN=ì/Àbe¸IÁý ºjëÞ<ÎÖ³ xØþþL–ç¾ÊèåaÛ,èsÌÅ&6Ÿ±p©mò’ÕuÕ†MÞzn<Ò‹e € € € € €@J äYÀ#Þ«­­CR Ó¡©,YO‰+u‚l7OÇ¥uŽûŸž©¨7¿ûMzŽÿÂß>°Å…rüÁU¼ÏáCá‚åµï~õóF[±@D×F§Küù#vÛpU6?GpÂîˆ úÁmή/]^I›ï#Ô×§Ûe ÂEøŸ³ xlÓÉÈ~ã3yï§9þ1ÑVlž’E ûs“ðˆ¦Ä6@@@@@½I ×C?ù^FLššc›ú5*ëÐLyÇ œ0EÆ|1Í[/^¤Ø|¥‹ÉTæÎ»¤åð·ü 6¼ÓÛw5•üùóEÌáaÁ‡û!ÿü‡öŽøÖ¤¸m¸¬›Ô‘ÿ{HÄPUn¿-mh«~ï~-“fÎó{•¸ý6çGóÓŽ•yËVû½R×=BºhðÄ¥~ú]îõSïcxŸËã–ßèÄçí4ÀbɆáêwí¹n—¿´kO'óù™1•/#ít"÷S?PFö£ úø;ï¸Wœ)çé5’@@@@@Ø[r-à‘@ÛµÃö;½¢òçËçÏÛ­ìšÏz<¸T´PAo5«6ÄÓâÕëeç®]RqŸ’R¶dqÑÓÄìXR«T±ÂRY{v*Ù#$î‚r!£]ÃÊõ›dÉšõ^Ï”ÊeJE å©(@@@@@”H©€GnheðÈò)@@@@@RO€€GêÝj„ € € € € €9 à‘C0²#€ € € € € €@ê ðH½{B@@@@@@ ‡{]Àã—…K婳=†c¬(çwhIÈŽ € € € € €ÿ6½.àño»Ô@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% € € € € € €@’x$ùpz@@@@@H\€€G↔€ € € € € €I à‘äÀé@@@@@@ q‰R € € € € €$Y€€G’o§G@@@@@Äx$nH  € € € € € dI¾œ@@@@@ à‘¸!% €»%°xÉ™8q¢¬]»NŽ>ê(9唓w«B@@@@‘¤<ý¾NÆõþEZö­#%Êεû1çûòÁðßåÖ§êI¡"r­ÜdôÞ{ïËÜyóâ>uÉ’%¤Bù R¡b)_¾¼TªXQ Î=Û¸+BFÈR`úôéÒôºæ²aÃ?ßÕW_%½z>ìf@@@@âHZÀãï™kå©['ËÆuÛ¤Â%¤Ãˆ“¤TÙ"ñ×E¶lÊv¸ºîW©˜twº)‘óàĬo—ËÓD‰–ªVZÚ=QŠ—.mwJoËÍ€‡»Ð6­o•Ž;HÁÿîá¾Üõ°Ü»¶nÝ*öí'Û¶Gþfvè¡rmÓk’~ñ#G–¿,ˆ¨GáB…äÞ{:gÙ£êÜÿ/s~ÿ=â8ûðÜ3c™Ë#“ @@@@È^`âˆ#Ć´Ê금ø€ € € € €¾À xçÖðÏXiÜ®¦œyýA-9_ݶe‡ ¾ý;™3uEÔƒss®¨'ȃ±µk'p@Ä׬Y+ÿýwÔ·Æ#2¦¸ñ†rÿ}]£íb)#°·< ø£?–7ßïYW¯VMZÝÜRöÙgŸ”±§" € € € €ü›öHÀÂCÚO‰˜[#ˆte§£äô««7íöúö­;eÐmSöš G¬€ÇK/²D åöæ€GÊaS!@@@@þÅyðˆ6‘xÐ+7ƒ®\ z mÿÌœ²ÜmŠXæÆéæá‡Ý x«3{öl¹¹Õ­™æpyÊ–-+ŸOš(Å‹3´•3a™Z® *Ù½»¾y³,WY]÷òåËå’÷Þ{ßui–M®¼"SbÂGÉ-·¶‰zLV§Mý>f¿ÝÓ'žì/Ÿ|òiVExûÌ÷Î;:hÝ®ÌøX¹r¥Ô­B¶e„3Œ1\ÎÔ„KíÚw”·ßyÇ}ô—Ÿ|4A:¨†ÿ9¼²C¿'¯½þ†Œ36f3xÌ—_&7·¼É 4·‡×óò~†ÏÅg@@@@òB OSÞ^(Ï=ø“¾YŸ¹Êìhñpm©s^åÌ;ó`‹\FÞ;èQªLé0â$©P=ãÍë<¨Æn™[«@ŸGûʡâÖ%«†âì†ÅŠZ n´†óaCK­£Ž•ÅßnÈö¦ü×ß|ão‹gŦŸêÿ„Ø[ùá”ì€Ç£Úûàšk›É† ÂUËôù¾®]¼ Ã’%K½Þ8ñ|,XòòK/x½22ذJ{Ÿ´¹­­X/“œ¤úõëÉàO‹GK±Èà]÷âÅÑá²,0dð ˆàJn<¬GF·ºËó/¼>u¶ŸëÕ­+£4°TªT)?o26¼×wÞ%ŸNœä×'Þ•Çûõ•ÆÅÌž—÷3æIÙ € € € €@. äzÀãë×þ’—z͈ìÈŸ?Ÿ\ßã¸=ìpNôÛešLýè·)bYbŸÂrǨ“S2è‘›™3gÊùFïåa=<¬§G8M™ò\uMÓðæ}=jDD†ðÁ6¤ÐeW4‘xÈÃÇÛç×_}Åê)¸/™Nh«öªÈÉ5M™üµÜß­»|8aBð2²\· Çk¯¼,‡~xÔ|‹—,‘«®º&Û^QÖ´zgü›QƒÑÈkÔ¨!EЉ«çAðœÝ¸_{”4÷7åfÀã‘^½eøˆ‘~Ù9]9ñ„dì˜QþðTÉ x,[¶L®¸òªÝ¾—vÝíÛµ•íÛE%ÈËûõ„lD@@@@ —r5àaÁŽ™µŠù ä“›þßÞÇÛTîÿÊ”"ÊÔpåèV¢’LåÐIItÍ‘R2fȘ©ËTf"™B„ԥ̩(—SÊTB¹¸ù]7—ÈôÛߥµì½†}ÖÙgÛgùõY¯Wíµžõ+:–Mš6‹if‡ý¡¦N™$·W­j/v]šÍQÉg}<}6£ € € € €™ˆ[ÀcåŒeÖÀM®É‘3›4TAÊT»Üõ|¢ Ó zä͹i°Ã-g‡¶íòB'IN®fäüøW(çÈÂ…Èos]‚LÇnÅò3¹?4‡Æ²Ð’RfÒòŽ:»¶Ñ¥Ö“–WªX)"ßLFrx¼;}†¼øÒÙg5ŸO?uɱöíÚJµjwIáwîÜ%S¦M“¹sç…W³öµþÒËÅGþ;ç'€ëxZ7g@@@@s(—€ÇÒI?ÈÜaß¹vSƒ-Þ¬(×U.ìz>Ñ…ô˜ÙçkùtÞ.×[çΗSÚŒª$WÝXÀõ|¢ ãðxsØp㥮ý94±ñÀ×_³ŠwïþI’kÜi‡ïÜ_·® 2HræÈ^,št¼[·î2;uND¹y°äïIRR)ó0”Ðþ”T¬\ÕsÉG‹:–jÒ_º5Zj]'|çÛoÒBÁ‘÷ð#¤ÛVaØŽýWûZ¿êÉ®3FtÆÆ°PÒv·ñëС“k"m]Öiú»SÃî螬ݬ ‚9©³"fÜè¹+WÊ3›Õ">ÝÂkÍ[qsù uõ@sŒ¼7Ã;/‡æ.)_¡¢£;æÍIuˆ´¢æ9Ñ¿9·Í+Ï.§ËÅÙ·­›¿ÈKb?ï7àm,K'%É´©“]gOÍ|o–t ý=ºmýûö‘zõ8-àÏñŒ¸) € € € €ÄQ ÓGo•Eã·¹v)çÙ¥ù·&ØÞÉÙ6ÉŠY?†Yû¹òä”A«kYÇY¹ï€Ç”©ÓŒÎögºãŽÛeʤw¬bMn®IÎ훾,^¾ôc#OƒýœŸ8qBî¹÷>qKº­¹4‡€¹}µ~½‘“À<ÿпŸè p·M%7‘•+W9N‡çòÈꀇ΢ÿÖXÉa i§'Ož"=z½êè¿xå<Ñ€Ò=µj»ÚC…Š•]—{wÚ©R¹²õ˜‰Oë†ì € € € € g¸<´OÑ’–gÏ‘Mš FÒòhù;þ I˽~½ÞøÙFòÒ‹gÖû?pà€ñ’Ôí»ž#Ãí¼–éÒK¥=&Ÿ®Z!Å‹7šzÍ"±ç£ðºOzåYðˆfuôèQ¹¾LYÇ#¤÷«ý÷fÍvMàmxxÙ:n˜9£3hÌÍë¹}é+³~øg•Û«¹æ9—ý^oLûZ6†’§§…>÷îÛ'CÉÑ:5ŸŠÙï¬ xx¥ß¿“§ž~FV¯þÔ| ëóÍ7†Êßî¯k'j<­²ƒ € € € €@œâðÐ~­IÝ%3úZBÆÙK z4P^Ê¥užL@‰öiê+eí¢Ÿ\ï–ï’ ¥í¸ÊR,)¿ëù¬*Œ÷’Vžmâ:3¢k—ÎÒ¼Ù™%¢¶lÙ"÷ÞwöE¨ùìö%©Ìr·O¯e~ÞŸ›*åÊ•3šÜXîf×ÍŸ­Y-E‹q»l†ÊþÌ/Û Ú*Û—ózA-Ðc^²zÙµ{·yh}ž‹€Çþýÿ’‘£G»ÎÚ°nìc'«^cé÷ïÄ+Èùb÷nÒ¤ñ³Ö“'j<­²ƒ € € € €@œâðо­]ð“LíµÑ=è‘=›<Ýûf)ï™_øÇùY×óYYÏ€‡þ’½ÜÍÎú|áù&¼’$»%:ö²ñZ†gÚÔÉÆ²M§C¨k’®um¾uó·žIµ]xþYÑl=¨|''W“IÏæIÔ òÌ,i¥É×zäQ_Ï—^¥¬xDËh¹^ŸÅk;¿K”Å;€Þ7ö@@@@ˆ§@ÜÚ¹hAlÙDžésK‚ì˜Ði½¤­Øëê–¿`.i7¾J ƒÚáx<>üð#iÙº«Cx¾ ¯@A¡B…ä˵Ÿ»¶·z-YžÄ+·@x_ì×Íȱ×søÉáñͦMrÿßœ Ç3’Ã#Ú‹âs½¤•׬ÃX·šw¥È€þg“™=à¡KVi]ÊÊï¦>^õ³"à¡ýÎìßI÷_–é3f8ì9@5žŽŽP€ € € € €@œÎIÀCû¶þ£=2éå ¢û¦A'{Ü$ï¿Â~*®Ç§N†‚/x;.)œ[ÚO¨"…®È×ûÆóbñ x?~\êÔý›lÛ¾ÝÑ=MB®sûõ×_å¦[n5#>×µN (Qf?ðJÈ­õÖ~¾F.»ì2£‰4°`߯-w׬i/Îð±WÀC¯­÷ˆ¶-^²Dš·hé¨r¾<¼–‹×ì…IÔ òXgx<ß¶½,X¸Ð1†Z A¯ºuêHR©RR¨Ð¥rÑEY³ŠÞxs˜¼9l¸£]V<¼þNÞ;FjÖ¼ËÑO{ÎpÑ™.ömÔÈRûÞZVq¢ÆÓº!; € € € €ÄYàœ<´ŸiK÷Ê„®ëEöMƒõº••ª_e?—ã“'NË[í¿”Mkö»^ïÒ¢y¤í[UäÒây\Ï¥0^#GÉà!C]«[×.Ò¬i“ˆs%K•Ž86¼^úšçõÓkv„žÛ¶u³äÌ‘Cw¥I³æòÉ'KýðÿuêØAZµ|.¼È±¯A•ÉS¦:ÊkTO àèæð(”$‹ÿþ¡£mxÁ ÁCdä(gPä| x4nÒT–.[þHÆþ‚ùóäÆ2eå±$êy¬¯™ÃÞ*÷×uæ¨1 Úµï ïÏ?4˽¾û±—¼2öNÍš?'K>þØì†õÙù…Nò\‹æÖ±ÛΩS§¤T鿺ûó$j<];C! € € € €q8§íߦUûå­N_Š ܶúÝãô8ñû)Ûvl^û‹Û-¥Pñ¼ÆÌŽK.Ïíz>H…ñxh` GÏ^žµbùRÑ„äáÛSO?#«W^dì·}¾èÚÿѶÑcÆÊë9ªØs€Œ=F쨧‰÷çÍ‘|„ÌNãÙ÷Í›I—Î/8ÎÏ™;W:vêì(ׂOW­âÅÝÏïßÿ/I©y·>|ØÑÖþ²}çÎR#Å}é* ZhðÂmûý÷ßåÑÇëKZZšãôüP ¤lÙ²F¹¾¸/}í™ÙöŠ;´—Ö­œKVi½ oO”>}Ïæªo{¾<¶lÙ"÷ÞçœÅ 9*RgÍ”«¯¾:ü±"ögÍN•Î]ºF”éÁ€~}åñdzÊõ‚Ü+à¡ùzã?Œå¨¬Ný±³yófÑ@„}+W®œ¼?7Õ^lÏ;O:trþ=h…Œ<¦Nž$·ß^Õº¶}ÇoÀcç®]RãN÷¥«ÆŒ)µî¹Ç~iãX¿ÿõë7/¿úÊqÞ-M¢ÆÓÑ @@@@ˆ“@BÚ×ô‚>½¤<}M¦ëø±“2ºõ:Ï`G±’ùCËXU–|/ÌÔ}ÙØ+àÑûÕ^Rñ¶ ]9ôßÿÊþЬMÖ¼tÙ2×á té§ysfK®\ÎàO´—Ì:dÚÔ)rÅ%Â/'{÷î•'žl(;vìˆ(7Ö}ñ™.\Ø<4>½^²êÉðÙf#Í/ң׫2oÞûf‘õ™/_>ùjÝÏ“RóÏþôìñ²<Ù äøc‰­C‡Éü 套_±®iß9_Úo¯g/Z´¨Ì {Ñ"Eì'«V­–†Ï4r”kÁÊåËäÊ+ÏæÝñ»ôf6赪×H ÈÙ·ßo³I´ÀÕ«½z3V²gÏÑîßJù[o‹(3Þ™8Aª''›‡Ögz3¡¼O7zÖuF̃> ýúô–ܹÝg’ù xh½òpè9·ÀÊÑ£G¥Sç.òÁ‹´ŠcsË““¨ñtt†@@@@â$°€‡öwÛ—dt›urü÷“®ÝÏLÐãØ‘2²åZÙñõ¿]¯­ÁŽvoW‘¼_àz>¨…^xôW•›ù.Ü®çµä”Y÷ÉOË:}Z¶lÝ*“'O1O9>[·j%;´s”þÅ¢Ïèµé/òï%fΟÿ"Ùµk·Ì|o–ëìmß½[WiÚ¤qÄ¥†)C†¾Q~ 3Ê”¹Aü댑ðºº><–.]&›6³?‚q¬Á¡ê¡|'•+U’Ò¥“ä—_~‘ùóºæŠÐ5ªW—‰o¸V"_{o´C:†w¥Üi$ïÙãÉ™3§ÑO¯z²açäÎÕEƒ\?þ¸SÖ¬ùLô»mó xh.Úw_gSäË›WZ>×"bfMFº¼š.³æµÝuWŠapêÔi9pà€‘ßF?Ý6] lúôiV.³N"ÇÓ¼'Ÿ € € € €ÄS ¡íøþ-#[}!¿u= É#ÝÞK–<ya™‘ÝòÅ/2¢¥û Ë+ÿz‰´Sé¼ vèóŸ‹€‡þÂlh) &DÛ¢-‰­ýœæM˜“:Ëx!m?§Çýú·ÆOp;å»Lg|¸ha(/GÞˆ6»wÿ$É5îŒ(ËÌÁùðÐçìÚ­»$ÊÌ3k[·a=K<‰»%H¿êúÒ&ìÈCÅê\îÄ;à‘œ\MÞ2X ,èë©öìÙ#Ï4j,Û¶o÷Uß^©dÉ’ÆÌ€«¯ºÊ~Ê:ÖåwÚµïèë…¶Õ(lGá?7uvÄrKa§eê´wååWz„¥»¯ùFÜ–¶:ßÿ -s¦ÉÝÝ’§‹ª ¤#†IÙotTOä òücƒ±¬“£¶‚ð€Ço¿ý&mÛuðœµbkjêwIg.iÒwûæðÐeÖtŠ×Œ ó:™ xhîÍ/âµL•yŸhŸ:KGgë¸m‰O·ûS† € € € €@f²$à¡þyó¯2¬ÅçräÐq‰g"qÍ2¦íZ9yâ´”,[PZª(¹òf|ÆHfaãÕ>^]òæ±G‘””ÇR6éõõȑߤû‹/Êûó¤W5â¼&S4ð5פÒC§N2~½>hðû©¨ÇU«T‘~ýúH´€ÊéÐ’[#G–ÁC†F½–yò™§Ëc]{Ý f‘õy¾<´ã:SgpÈuÌØqÖsøÙyø¡‡¤WÏWD—¿rÛý‚|à Á¢Ë¬EÛÂZOŸ½gÏ^2íÝéÑšçÌàΖ-[å…Pþ ûæðÐzŸ|²Tš4knoqœÙ€‡^L¿ËÞž(}ûõ¸vzx?n¬\sMIϪ‰OÏŽp@@@@²,à¡ýýçöC2£ß7Òlð­qM$®A¥“¦Co• r刑&Í=ÛD–¯Xá»3úrºx±bR¢D‰ÐÅ¥T©Rr_íÚR¤È徯áUñ«õëeÖ¬Ùé.‘¤/Ê50P¡Â­’-[6¯Ë¹–ÿðÙšjä 8|ø°k-Ô_©?üðƒRç¾û|ßcíÚu2nüxãå´Û…K'%‰Ò+T¨à™([s?ô å‰ß4·‚æX°o›¿ý&"zøyýµþ_¯/^dìׯWÏèƒãÄóÞŸ/í;ttœøúkòÈÃ9Êà ¶†ò¬Ìš*ÓgÌỗ¢õ5¯Gƒõ¥n:áÍû±<·y‘{jÕvÌÒïî7iÌ*®Ÿk>û,ôý˜cÌXq›Qá•0}ÊÔi2ñI® ìuVÇómZKýzË®-X¸Pt¹)û¶`þ<ÑåÙ¼¶;wßÛeËW¸Þgú»S [³½UôYì›×’SáõvîÚ%sæÌ59nf] 6x¢¾hÀ3W®\f±ëgVŒ§kG(D@@@@ F, xÄØgše±€Îøøé§Ý²oß~9ð¿ÿ+Ù³g—‹/Î/E‹ -+u¥çŒ€Œtûøñã¢ù7öîÛ+ûöǎʥ—^*… –R×”’.ÉÈå"êê âC/§÷‡úâä cvˆþ>þüõþ¿;v,4~?˾ýû ÛßBKŠég…C/þ“J'IÁ΋G×Ù¿9bÌÒëøéw1Ú¦A ýÞ?q\r]x¡ô»¼[´ëÚÏéŒ%]N,GŽò{軬}ËÚ÷¦?íÞú;Ù'{ÿ¹Wr^ÓlèßbñâÅ„îñ¾'×C@@@@ ¨<‚:2ô @@@@@| ððMEE@@@@@ª Ž ýB@@@@@ß<|SQ@@@@@‚*@À#¨#C¿@@@@@@À·ßTTD@@@@@  ðêÈÐ/@@@@@ð-@ÀÃ7@@@@@@ ¨<‚:2ô @@@@@| ððMEE@@@@@ª Ž ýB@@@@@ß<|SQ@@@@@‚*@À#¨#C¿@@@@@@À·ßTTD@@@@@  ðêÈÐ/@@@@@ð-@ÀÃ7@@@@@@ ¨<‚:2ô @@@@@| ððMEE@@@@@ª Ž ýB@@@@@ß<|SQ@@@@@‚*@À#¨#C¿@@@@@@À·ßTTD@@@@@  ðêÈÐ/@@@@@ð-@ÀÃ7@@@@@@ ¨<‚:2ô @@@@@| ððMEE@@@@@ª Ž ýB@@@@@ß<|SQ@@@@@‚*@À#¨#C¿@@@@@@À·ßTTD@@@@@  ðêÈÐ/@@@@@ð-@ÀÃ7@@@@@@ ¨<‚:2ô @@@@@| ððMEE@@@@@ª Ž ýB@@@@@ß<|SQ@@@@@‚*@À#¨#C¿@@@@@@À·ßTTD@@@@@  ðêÈÐ/@@@@@ð-@ÀÃ7@@@@@@ ¨<‚:2ô @@@@@| d:àñý޾oFE@@@@@@À@©’Wû©fÕ!àaQ°ƒ € € € € €AHxÀ#(N?@@@@@@àÏ+é^:ž@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"@À#(#A?@@@@@@ f1ÓÑ@@@@@‚"ðô e‚æ„IEND®B`‚slack-0.11.3/examples/slash/000077500000000000000000000000001430741033100156155ustar00rootroot00000000000000slack-0.11.3/examples/slash/slash.go000066400000000000000000000022741430741033100172630ustar00rootroot00000000000000package main import ( "encoding/json" "flag" "fmt" "io" "io/ioutil" "net/http" "github.com/slack-go/slack" ) func main() { var ( signingSecret string ) flag.StringVar(&signingSecret, "secret", "YOUR_SIGNING_SECRET_HERE", "Your Slack app's signing secret") flag.Parse() http.HandleFunc("/slash", func(w http.ResponseWriter, r *http.Request) { verifier, err := slack.NewSecretsVerifier(r.Header, signingSecret) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } r.Body = ioutil.NopCloser(io.TeeReader(r.Body, &verifier)) s, err := slack.SlashCommandParse(r) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } if err = verifier.Ensure(); err != nil { w.WriteHeader(http.StatusUnauthorized) return } switch s.Command { case "/echo": params := &slack.Msg{Text: s.Text} b, err := json.Marshal(params) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/json") w.Write(b) default: w.WriteHeader(http.StatusInternalServerError) return } }) fmt.Println("[INFO] Server listening") http.ListenAndServe(":3000", nil) } slack-0.11.3/examples/socketmode/000077500000000000000000000000001430741033100166405ustar00rootroot00000000000000slack-0.11.3/examples/socketmode/socketmode.go000066400000000000000000000073051430741033100213310ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "strings" "github.com/slack-go/slack/socketmode" "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" ) func main() { appToken := os.Getenv("SLACK_APP_TOKEN") if appToken == "" { fmt.Fprintf(os.Stderr, "SLACK_APP_TOKEN must be set.\n") os.Exit(1) } if !strings.HasPrefix(appToken, "xapp-") { fmt.Fprintf(os.Stderr, "SLACK_APP_TOKEN must have the prefix \"xapp-\".") } botToken := os.Getenv("SLACK_BOT_TOKEN") if botToken == "" { fmt.Fprintf(os.Stderr, "SLACK_BOT_TOKEN must be set.\n") os.Exit(1) } if !strings.HasPrefix(botToken, "xoxb-") { fmt.Fprintf(os.Stderr, "SLACK_BOT_TOKEN must have the prefix \"xoxb-\".") } api := slack.New( botToken, slack.OptionDebug(true), slack.OptionLog(log.New(os.Stdout, "api: ", log.Lshortfile|log.LstdFlags)), slack.OptionAppLevelToken(appToken), ) client := socketmode.New( api, socketmode.OptionDebug(true), socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)), ) go func() { for evt := range client.Events { switch evt.Type { case socketmode.EventTypeConnecting: fmt.Println("Connecting to Slack with Socket Mode...") case socketmode.EventTypeConnectionError: fmt.Println("Connection failed. Retrying later...") case socketmode.EventTypeConnected: fmt.Println("Connected to Slack with Socket Mode.") case socketmode.EventTypeEventsAPI: eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) if !ok { fmt.Printf("Ignored %+v\n", evt) continue } fmt.Printf("Event received: %+v\n", eventsAPIEvent) client.Ack(*evt.Request) switch eventsAPIEvent.Type { case slackevents.CallbackEvent: innerEvent := eventsAPIEvent.InnerEvent switch ev := innerEvent.Data.(type) { case *slackevents.AppMentionEvent: _, _, err := api.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false)) if err != nil { fmt.Printf("failed posting message: %v", err) } case *slackevents.MemberJoinedChannelEvent: fmt.Printf("user %q joined to channel %q", ev.User, ev.Channel) } default: client.Debugf("unsupported Events API event received") } case socketmode.EventTypeInteractive: callback, ok := evt.Data.(slack.InteractionCallback) if !ok { fmt.Printf("Ignored %+v\n", evt) continue } fmt.Printf("Interaction received: %+v\n", callback) var payload interface{} switch callback.Type { case slack.InteractionTypeBlockActions: // See https://api.slack.com/apis/connections/socket-implement#button client.Debugf("button clicked!") case slack.InteractionTypeShortcut: case slack.InteractionTypeViewSubmission: // See https://api.slack.com/apis/connections/socket-implement#modal case slack.InteractionTypeDialogSubmission: default: } client.Ack(*evt.Request, payload) case socketmode.EventTypeSlashCommand: cmd, ok := evt.Data.(slack.SlashCommand) if !ok { fmt.Printf("Ignored %+v\n", evt) continue } client.Debugf("Slash command received: %+v", cmd) payload := map[string]interface{}{ "blocks": []slack.Block{ slack.NewSectionBlock( &slack.TextBlockObject{ Type: slack.MarkdownType, Text: "foo", }, nil, slack.NewAccessory( slack.NewButtonBlockElement( "", "somevalue", &slack.TextBlockObject{ Type: slack.PlainTextType, Text: "bar", }, ), ), ), }} client.Ack(*evt.Request, payload) default: fmt.Fprintf(os.Stderr, "Unexpected event type received: %s\n", evt.Type) } } }() client.Run() } slack-0.11.3/examples/socketmode_handler/000077500000000000000000000000001430741033100203355ustar00rootroot00000000000000slack-0.11.3/examples/socketmode_handler/socketmode_handler.go000066400000000000000000000130161430741033100245170ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "strings" "github.com/slack-go/slack/slackevents" "github.com/slack-go/slack/socketmode" "github.com/slack-go/slack" ) func main() { appToken := os.Getenv("SLACK_APP_TOKEN") if appToken == "" { panic("SLACK_APP_TOKEN must be set.\n") } if !strings.HasPrefix(appToken, "xapp-") { panic("SLACK_APP_TOKEN must have the prefix \"xapp-\".") } botToken := os.Getenv("SLACK_BOT_TOKEN") if botToken == "" { panic("SLACK_BOT_TOKEN must be set.\n") } if !strings.HasPrefix(botToken, "xoxb-") { panic("SLACK_BOT_TOKEN must have the prefix \"xoxb-\".") } api := slack.New( botToken, slack.OptionDebug(true), slack.OptionLog(log.New(os.Stdout, "api: ", log.Lshortfile|log.LstdFlags)), slack.OptionAppLevelToken(appToken), ) client := socketmode.New( api, socketmode.OptionDebug(true), socketmode.OptionLog(log.New(os.Stdout, "socketmode: ", log.Lshortfile|log.LstdFlags)), ) socketmodeHandler := socketmode.NewSocketmodeHandler(client) socketmodeHandler.Handle(socketmode.EventTypeConnecting, middlewareConnecting) socketmodeHandler.Handle(socketmode.EventTypeConnectionError, middlewareConnectionError) socketmodeHandler.Handle(socketmode.EventTypeConnected, middlewareConnected) //\\ EventTypeEventsAPI //\\ // Handle all EventsAPI socketmodeHandler.Handle(socketmode.EventTypeEventsAPI, middlewareEventsAPI) // Handle a specific event from EventsAPI socketmodeHandler.HandleEvents(slackevents.AppMention, middlewareAppMentionEvent) //\\ EventTypeInteractive //\\ // Handle all Interactive Events socketmodeHandler.Handle(socketmode.EventTypeInteractive, middlewareInteractive) // Handle a specific Interaction socketmodeHandler.HandleInteraction(slack.InteractionTypeBlockActions, middlewareInteractionTypeBlockActions) // Handle all SlashCommand socketmodeHandler.Handle(socketmode.EventTypeSlashCommand, middlewareSlashCommand) socketmodeHandler.HandleSlashCommand("/rocket", middlewareSlashCommand) // socketmodeHandler.HandleDefault(middlewareDefault) socketmodeHandler.RunEventLoop() } func middlewareConnecting(evt *socketmode.Event, client *socketmode.Client) { fmt.Println("Connecting to Slack with Socket Mode...") } func middlewareConnectionError(evt *socketmode.Event, client *socketmode.Client) { fmt.Println("Connection failed. Retrying later...") } func middlewareConnected(evt *socketmode.Event, client *socketmode.Client) { fmt.Println("Connected to Slack with Socket Mode.") } func middlewareEventsAPI(evt *socketmode.Event, client *socketmode.Client) { fmt.Println("middlewareEventsAPI") eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) if !ok { fmt.Printf("Ignored %+v\n", evt) return } fmt.Printf("Event received: %+v\n", eventsAPIEvent) client.Ack(*evt.Request) switch eventsAPIEvent.Type { case slackevents.CallbackEvent: innerEvent := eventsAPIEvent.InnerEvent switch ev := innerEvent.Data.(type) { case *slackevents.AppMentionEvent: fmt.Printf("We have been mentionned in %v", ev.Channel) _, _, err := client.Client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false)) if err != nil { fmt.Printf("failed posting message: %v", err) } case *slackevents.MemberJoinedChannelEvent: fmt.Printf("user %q joined to channel %q", ev.User, ev.Channel) } default: client.Debugf("unsupported Events API event received") } } func middlewareAppMentionEvent(evt *socketmode.Event, client *socketmode.Client) { eventsAPIEvent, ok := evt.Data.(slackevents.EventsAPIEvent) if !ok { fmt.Printf("Ignored %+v\n", evt) return } client.Ack(*evt.Request) ev, ok := eventsAPIEvent.InnerEvent.Data.(*slackevents.AppMentionEvent) if !ok { fmt.Printf("Ignored %+v\n", ev) return } fmt.Printf("We have been mentionned in %v\n", ev.Channel) _, _, err := client.Client.PostMessage(ev.Channel, slack.MsgOptionText("Yes, hello.", false)) if err != nil { fmt.Printf("failed posting message: %v", err) } } func middlewareInteractive(evt *socketmode.Event, client *socketmode.Client) { callback, ok := evt.Data.(slack.InteractionCallback) if !ok { fmt.Printf("Ignored %+v\n", evt) return } fmt.Printf("Interaction received: %+v\n", callback) var payload interface{} switch callback.Type { case slack.InteractionTypeBlockActions: // See https://api.slack.com/apis/connections/socket-implement#button client.Debugf("button clicked!") case slack.InteractionTypeShortcut: case slack.InteractionTypeViewSubmission: // See https://api.slack.com/apis/connections/socket-implement#modal case slack.InteractionTypeDialogSubmission: default: } client.Ack(*evt.Request, payload) } func middlewareInteractionTypeBlockActions(evt *socketmode.Event, client *socketmode.Client) { client.Debugf("button clicked!") } func middlewareSlashCommand(evt *socketmode.Event, client *socketmode.Client) { cmd, ok := evt.Data.(slack.SlashCommand) if !ok { fmt.Printf("Ignored %+v\n", evt) return } client.Debugf("Slash command received: %+v", cmd) payload := map[string]interface{}{ "blocks": []slack.Block{ slack.NewSectionBlock( &slack.TextBlockObject{ Type: slack.MarkdownType, Text: "foo", }, nil, slack.NewAccessory( slack.NewButtonBlockElement( "", "somevalue", &slack.TextBlockObject{ Type: slack.PlainTextType, Text: "bar", }, ), ), ), }} client.Ack(*evt.Request, payload) } func middlewareDefault(evt *socketmode.Event, client *socketmode.Client) { // fmt.Fprintf(os.Stderr, "Unexpected event type received: %s\n", evt.Type) } slack-0.11.3/examples/stars/000077500000000000000000000000001430741033100156375ustar00rootroot00000000000000slack-0.11.3/examples/stars/stars.go000066400000000000000000000016241430741033100173250ustar00rootroot00000000000000package main import ( "flag" "fmt" "github.com/slack-go/slack" ) func main() { var ( apiToken string debug bool ) flag.StringVar(&apiToken, "token", "YOUR_TOKEN_HERE", "Your Slack API Token") flag.BoolVar(&debug, "debug", false, "Show JSON output") flag.Parse() api := slack.New(apiToken, slack.OptionDebug(debug)) // Get all stars for the usr. params := slack.NewStarsParameters() starredItems, _, err := api.GetStarred(params) if err != nil { fmt.Printf("Error getting stars: %s\n", err) return } for _, s := range starredItems { var desc string switch s.Type { case slack.TYPE_MESSAGE: desc = s.Message.Text case slack.TYPE_FILE: desc = s.File.Name case slack.TYPE_FILE_COMMENT: desc = s.File.Name + " - " + s.Comment.Comment case slack.TYPE_CHANNEL, slack.TYPE_IM, slack.TYPE_GROUP: desc = s.Channel } fmt.Printf("Starred %s: %s\n", s.Type, desc) } } slack-0.11.3/examples/team/000077500000000000000000000000001430741033100154315ustar00rootroot00000000000000slack-0.11.3/examples/team/team.go000066400000000000000000000010101430741033100166760ustar00rootroot00000000000000package main import ( "fmt" "github.com/slack-go/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") //Example for single user billingActive, err := api.GetBillableInfo("U023BECGF") if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("ID: U023BECGF, BillingActive: %v\n\n\n", billingActive["U023BECGF"]) //Example for team billingActiveForTeam, _ := api.GetBillableInfoForTeam() for id, value := range billingActiveForTeam { fmt.Printf("ID: %v, BillingActive: %v\n", id, value) } } slack-0.11.3/examples/users/000077500000000000000000000000001430741033100156445ustar00rootroot00000000000000slack-0.11.3/examples/users/users.go000066400000000000000000000004731430741033100173400ustar00rootroot00000000000000package main import ( "fmt" "github.com/slack-go/slack" ) func main() { api := slack.New("YOUR_TOKEN_HERE") user, err := api.GetUserInfo("U023BECGF") if err != nil { fmt.Printf("%s\n", err) return } fmt.Printf("ID: %s, Fullname: %s, Email: %s\n", user.ID, user.Profile.RealName, user.Profile.Email) } slack-0.11.3/examples/webhooks/000077500000000000000000000000001430741033100163245ustar00rootroot00000000000000slack-0.11.3/examples/webhooks/webhooks.go000066400000000000000000000017771430741033100205100ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "strconv" "time" "github.com/slack-go/slack" ) func main() { attachment := slack.Attachment{ Color: "good", Fallback: "You successfully posted by Incoming Webhook URL!", AuthorName: "slack-go/slack", AuthorSubname: "github.com", AuthorLink: "https://github.com/slack-go/slack", AuthorIcon: "https://avatars2.githubusercontent.com/u/652790", Text: " All text in Slack uses the same system of escaping: chat messages, direct messages, file comments, etc. :smile:\nSee ", Footer: "slack api", FooterIcon: "https://platform.slack-edge.com/img/default_application_icon.png", Ts: json.Number(strconv.FormatInt(time.Now().Unix(), 10)), } msg := slack.WebhookMessage{ Attachments: []slack.Attachment{attachment}, } err := slack.PostWebhook("YOUR_WEBHOOK_URL_HERE", &msg) if err != nil { fmt.Println(err) } } slack-0.11.3/examples/websocket/000077500000000000000000000000001430741033100164715ustar00rootroot00000000000000slack-0.11.3/examples/websocket/websocket.go000066400000000000000000000025041430741033100210070ustar00rootroot00000000000000package main import ( "fmt" "log" "os" "github.com/slack-go/slack" ) func main() { token, ok := os.LookupEnv("SLACK_TOKEN") if !ok { fmt.Println("Missing SLACK_TOKEN in environment") os.Exit(1) } api := slack.New( token, slack.OptionDebug(true), slack.OptionLog(log.New(os.Stdout, "slack-bot: ", log.Lshortfile|log.LstdFlags)), ) rtm := api.NewRTM() go rtm.ManageConnection() for msg := range rtm.IncomingEvents { fmt.Print("Event Received: ") switch ev := msg.Data.(type) { case *slack.HelloEvent: // Ignore hello case *slack.ConnectedEvent: fmt.Println("Infos:", ev.Info) fmt.Println("Connection counter:", ev.ConnectionCount) // Replace C2147483705 with your Channel ID rtm.SendMessage(rtm.NewOutgoingMessage("Hello world", "C2147483705")) case *slack.MessageEvent: fmt.Printf("Message: %v\n", ev) case *slack.PresenceChangeEvent: fmt.Printf("Presence Change: %v\n", ev) case *slack.LatencyReport: fmt.Printf("Current latency: %v\n", ev.Value) case *slack.DesktopNotificationEvent: fmt.Printf("Desktop Notification: %v\n", ev) case *slack.RTMError: fmt.Printf("Error: %s\n", ev.Error()) case *slack.InvalidAuthEvent: fmt.Printf("Invalid credentials") return default: // Ignore other events.. // fmt.Printf("Unexpected: %v\n", msg.Data) } } } slack-0.11.3/examples/websocket_respond/000077500000000000000000000000001430741033100202235ustar00rootroot00000000000000slack-0.11.3/examples/websocket_respond/respond.go000066400000000000000000000017001430741033100222220ustar00rootroot00000000000000package main import ( "fmt" "strings" "github.com/slack-go/slack" ) func main() { api := slack.New( "YOUR-TOKEN-HERE", ) rtm := api.NewRTM() go rtm.ManageConnection() for msg := range rtm.IncomingEvents { switch ev := msg.Data.(type) { case *slack.MessageEvent: msg := ev.Msg if msg.SubType != "" { break // We're only handling normal messages. } // Create a response object. resp := rtm.NewOutgoingMessage(fmt.Sprintf("echo %s", msg.Text), msg.Channel) // Respond in thread if not a direct message. if !strings.HasPrefix(msg.Channel, "D") { resp.ThreadTimestamp = msg.Timestamp } // Respond in same thread if message came from a thread. if msg.ThreadTimestamp != "" { resp.ThreadTimestamp = msg.ThreadTimestamp } rtm.SendMessage(resp) case *slack.ConnectedEvent: fmt.Println("Connected to Slack") case *slack.InvalidAuthEvent: fmt.Println("Invalid token") return } } } slack-0.11.3/examples/workflow_step/000077500000000000000000000000001430741033100174105ustar00rootroot00000000000000slack-0.11.3/examples/workflow_step/README.md000066400000000000000000000031461430741033100206730ustar00rootroot00000000000000#WorkflowStep Have you ever wanted to run an app from a Slack workflow? This sample app shows you how it works. Slack describes some of the basics here: https://api.slack.com/workflows/steps https://api.slack.com/tutorials/workflow-builder-steps 1. Start the example app localy on port 8080 2. Use ngrok to expose your app to the internet ```shell ./ngrok http 8080 ``` Copy the https forwarding URL and paste it into the app manifest down below (event_subscription request_url and interactivity request_url) 3. Create a new Slack App at api.slack.com/apps from an app manifest The manifest of a sample Slack App looks like this: ```yaml display_information: name: Workflowstep-Example features: bot_user: display_name: Workflowstep-Example always_online: false workflow_steps: - name: Example Step callback_id: example-step oauth_config: scopes: bot: - workflow.steps:execute settings: event_subscriptions: request_url: https://*****.ngrok.io/api/v1/example-step bot_events: - workflow_step_execute interactivity: is_enabled: true request_url: https://*****.ngrok.io/api/v1/interaction org_deploy_enabled: false socket_mode_enabled: false token_rotation_enabled: false ``` ("Interactivity" and "Enable Events" should be turned on) 4. Slack Workflow (**paid plan required!**) 1. Create a new Workflow at app.slack.com/workflow-builder 2. give it a name 3. select "Planned date & time" 4. add another step and select "Example Step" from App Workflowstep-Example 5. configure your app and hit save 6. don't forget to publish your workflowslack-0.11.3/examples/workflow_step/go.mod000066400000000000000000000002741430741033100205210ustar00rootroot00000000000000module workflowstep-example go 1.17 require ( github.com/gorilla/websocket v1.4.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/slack-go/slack v0.10.1 // indirect ) slack-0.11.3/examples/workflow_step/go.sum000066400000000000000000000016641430741033100205520ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/slack-go/slack v0.10.1 h1:BGbxa0kMsGEvLOEoZmYs8T1wWfoZXwmQFBb6FgYCXUA= github.com/slack-go/slack v0.10.1/go.mod h1:wWL//kk0ho+FcQXcBTmEafUI5dz4qz5f4mMk8oIkioQ= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= slack-0.11.3/examples/workflow_step/handler.go000066400000000000000000000141261430741033100213600ustar00rootroot00000000000000package main import ( "encoding/json" "fmt" "io/ioutil" "log" "net/http" "net/url" "time" "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" ) const ( IDSelectOptionBlock = "select-option-block" IDExampleSelectInput = "example-select-input" ) func handleMyWorkflowStep(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } // see: https://github.com/slack-go/slack/blob/master/examples/eventsapi/events.go body, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) return } eventsAPIEvent, err := slackevents.ParseEvent(json.RawMessage(body), slackevents.OptionNoVerifyToken()) if err != nil { log.Printf("[ERROR] Failed on parsing event: %s", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } // see: https://api.slack.com/apis/connections/events-api#subscriptions if eventsAPIEvent.Type == slackevents.URLVerification { var r *slackevents.ChallengeResponse err := json.Unmarshal([]byte(body), &r) if err != nil { log.Printf("[ERROR] Failed to decode json message on event url_verification: %s", err.Error()) w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "text") w.Write([]byte(r.Challenge)) return } // see: https://api.slack.com/apis/connections/events-api#receiving_events if eventsAPIEvent.Type == slackevents.CallbackEvent { innerEvent := eventsAPIEvent.InnerEvent switch ev := innerEvent.Data.(type) { // see: https://api.slack.com/events/workflow_step_execute case *slackevents.WorkflowStepExecuteEvent: if ev.CallbackID == MyExampleWorkflowStepCallbackID { go doHeavyLoad(ev.WorkflowStep) w.WriteHeader(http.StatusOK) return } w.WriteHeader(http.StatusBadRequest) log.Printf("[WARN] unknown callbackID: %s", ev.CallbackID) return default: w.WriteHeader(http.StatusBadRequest) log.Printf("[WARN] unknown inner event type: %s", eventsAPIEvent.InnerEvent.Type) return } } w.WriteHeader(http.StatusBadRequest) log.Printf("[WARN] unknown event type: %s", eventsAPIEvent.Type) } func handleInteraction(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { w.WriteHeader(http.StatusMethodNotAllowed) return } body, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) return } jsonStr, err := url.QueryUnescape(string(body)[8:]) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } var message slack.InteractionCallback if err := json.Unmarshal([]byte(jsonStr), &message); err != nil { log.Printf("[ERROR] Failed to decode json message from slack: %s", jsonStr) w.WriteHeader(http.StatusInternalServerError) return } switch message.Type { case slack.InteractionTypeWorkflowStepEdit: // https://api.slack.com/workflows/steps#handle_config_view err := replyWithConfigurationView(message, "", "") if err != nil { log.Printf("[ERROR] Failed to open configuration modal in slack: %s", err.Error()) } case slack.InteractionTypeViewSubmission: // https://api.slack.com/workflows/steps#handle_view_submission // process user inputs // this is just for demonstration, so we print it to console only blockAction := message.View.State.Values selectedOption := blockAction[IDSelectOptionBlock][IDExampleSelectInput].SelectedOption.Value log.Println(fmt.Sprintf("user selected: %s", selectedOption)) in := &slack.WorkflowStepInputs{ IDExampleSelectInput: slack.WorkflowStepInputElement{ Value: selectedOption, SkipVariableReplacement: false, }, } err := saveUserSettingsForWorkflowStep(message.WorkflowStep.WorkflowStepEditID, in, nil) if err != nil { log.Printf("[ERROR] Failed on doing a POST request to workflows.updateStep: %s", err.Error()) w.WriteHeader(http.StatusInternalServerError) } default: log.Printf("[WARN] unknown message type: %s", message.Type) w.WriteHeader(http.StatusInternalServerError) } } func replyWithConfigurationView(message slack.InteractionCallback, privateMetaData string, externalID string) error { headerText := slack.NewTextBlockObject("mrkdwn", "Hello World!\nThis is your workflow step app configuration view", false, false) headerSection := slack.NewSectionBlock(headerText, nil, nil) options := []*slack.OptionBlockObject{} options = append( options, slack.NewOptionBlockObject("one", slack.NewTextBlockObject("plain_text", "One", false, false), nil), ) options = append( options, slack.NewOptionBlockObject("two", slack.NewTextBlockObject("plain_text", "Two", false, false), nil), ) options = append( options, slack.NewOptionBlockObject("three", slack.NewTextBlockObject("plain_text", "Three", false, false), nil), ) selection := slack.NewOptionsSelectBlockElement( "static_select", slack.NewTextBlockObject("plain_text", "your choice", false, false), IDExampleSelectInput, options..., ) // preselect option, if workflow step input is defined initialOption, ok := slack.GetInitialOptionFromWorkflowStepInput(selection, message.WorkflowStep.Inputs, options) if ok { selection.InitialOption = initialOption } inputBlock := slack.NewInputBlock( IDSelectOptionBlock, slack.NewTextBlockObject("plain_text", "Select an option", false, false), selection, ) blocks := slack.Blocks{ BlockSet: []slack.Block{ headerSection, inputBlock, }, } cmr := slack.NewConfigurationModalRequest(blocks, privateMetaData, externalID) _, err := appCtx.slack.OpenView(message.TriggerID, cmr.ModalViewRequest) return err } func saveUserSettingsForWorkflowStep(workflowStepEditID string, inputs *slack.WorkflowStepInputs, outputs *[]slack.WorkflowStepOutput) error { return appCtx.slack.SaveWorkflowStepConfiguration(workflowStepEditID, inputs, outputs) } func doHeavyLoad(workflowStep slackevents.EventWorkflowStep) { // process user configuration e.g. inputs log.Printf("Inputs:") for name, input := range *workflowStep.Inputs { log.Printf(fmt.Sprintf("%s: %s", name, input.Value)) } // do heavy load time.Sleep(10 * time.Second) log.Println("Done") } slack-0.11.3/examples/workflow_step/main.go000066400000000000000000000021271430741033100206650ustar00rootroot00000000000000package main import ( "fmt" "log" "net/http" "os" "github.com/slack-go/slack" ) type ( appContext struct { slack *slack.Client config configuration } configuration struct { botToken string signingSecret string } SecretsVerifierMiddleware struct { handler http.Handler } ) const ( APIBaseURL = "/api/v1" // MyExampleWorkflowStepCallbackID is configured in slack (api.slack.com/apps). // Select your app or create a new one. Then choose menu "Workflow Steps"... MyExampleWorkflowStepCallbackID = "example-step" ) var appCtx appContext func main() { appCtx.config.botToken = os.Getenv("SLACK_BOT_TOKEN") appCtx.config.signingSecret = os.Getenv("SLACK_SIGNING_SECRET") appCtx.slack = slack.New(appCtx.config.botToken) mux := http.NewServeMux() mux.HandleFunc(fmt.Sprintf("%s/interaction", APIBaseURL), handleInteraction) mux.HandleFunc(fmt.Sprintf("%s/%s", APIBaseURL, MyExampleWorkflowStepCallbackID), handleMyWorkflowStep) middleware := NewSecretsVerifierMiddleware(mux) log.Printf("starting server on :8080") log.Fatal(http.ListenAndServe(":8080", middleware)) } slack-0.11.3/examples/workflow_step/middleware.go000066400000000000000000000015071430741033100220570ustar00rootroot00000000000000package main import ( "bytes" "io/ioutil" "net/http" "github.com/slack-go/slack" ) func (v *SecretsVerifierMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { body, err := ioutil.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusBadRequest) return } r.Body.Close() r.Body = ioutil.NopCloser(bytes.NewBuffer(body)) sv, err := slack.NewSecretsVerifier(r.Header, appCtx.config.signingSecret) if err != nil { w.WriteHeader(http.StatusBadRequest) return } if _, err := sv.Write(body); err != nil { w.WriteHeader(http.StatusInternalServerError) return } if err := sv.Ensure(); err != nil { w.WriteHeader(http.StatusUnauthorized) return } v.handler.ServeHTTP(w, r) } func NewSecretsVerifierMiddleware(h http.Handler) *SecretsVerifierMiddleware { return &SecretsVerifierMiddleware{h} } slack-0.11.3/files.go000066400000000000000000000330261430741033100143220ustar00rootroot00000000000000package slack import ( "context" "fmt" "io" "net/url" "strconv" "strings" ) const ( // Add here the defaults in the siten DEFAULT_FILES_USER = "" DEFAULT_FILES_CHANNEL = "" DEFAULT_FILES_TS_FROM = 0 DEFAULT_FILES_TS_TO = -1 DEFAULT_FILES_TYPES = "all" DEFAULT_FILES_COUNT = 100 DEFAULT_FILES_PAGE = 1 DEFAULT_FILES_SHOW_HIDDEN = false ) // File contains all the information for a file type File struct { ID string `json:"id"` Created JSONTime `json:"created"` Timestamp JSONTime `json:"timestamp"` Name string `json:"name"` Title string `json:"title"` Mimetype string `json:"mimetype"` ImageExifRotation int `json:"image_exif_rotation"` Filetype string `json:"filetype"` PrettyType string `json:"pretty_type"` User string `json:"user"` Mode string `json:"mode"` Editable bool `json:"editable"` IsExternal bool `json:"is_external"` ExternalType string `json:"external_type"` Size int `json:"size"` URL string `json:"url"` // Deprecated - never set URLDownload string `json:"url_download"` // Deprecated - never set URLPrivate string `json:"url_private"` URLPrivateDownload string `json:"url_private_download"` OriginalH int `json:"original_h"` OriginalW int `json:"original_w"` Thumb64 string `json:"thumb_64"` Thumb80 string `json:"thumb_80"` Thumb160 string `json:"thumb_160"` Thumb360 string `json:"thumb_360"` Thumb360Gif string `json:"thumb_360_gif"` Thumb360W int `json:"thumb_360_w"` Thumb360H int `json:"thumb_360_h"` Thumb480 string `json:"thumb_480"` Thumb480W int `json:"thumb_480_w"` Thumb480H int `json:"thumb_480_h"` Thumb720 string `json:"thumb_720"` Thumb720W int `json:"thumb_720_w"` Thumb720H int `json:"thumb_720_h"` Thumb960 string `json:"thumb_960"` Thumb960W int `json:"thumb_960_w"` Thumb960H int `json:"thumb_960_h"` Thumb1024 string `json:"thumb_1024"` Thumb1024W int `json:"thumb_1024_w"` Thumb1024H int `json:"thumb_1024_h"` Permalink string `json:"permalink"` PermalinkPublic string `json:"permalink_public"` EditLink string `json:"edit_link"` Preview string `json:"preview"` PreviewHighlight string `json:"preview_highlight"` Lines int `json:"lines"` LinesMore int `json:"lines_more"` IsPublic bool `json:"is_public"` PublicURLShared bool `json:"public_url_shared"` Channels []string `json:"channels"` Groups []string `json:"groups"` IMs []string `json:"ims"` InitialComment Comment `json:"initial_comment"` CommentsCount int `json:"comments_count"` NumStars int `json:"num_stars"` IsStarred bool `json:"is_starred"` Shares Share `json:"shares"` } type Share struct { Public map[string][]ShareFileInfo `json:"public"` Private map[string][]ShareFileInfo `json:"private"` } type ShareFileInfo struct { ReplyUsers []string `json:"reply_users"` ReplyUsersCount int `json:"reply_users_count"` ReplyCount int `json:"reply_count"` Ts string `json:"ts"` ThreadTs string `json:"thread_ts"` LatestReply string `json:"latest_reply"` ChannelName string `json:"channel_name"` TeamID string `json:"team_id"` } // FileUploadParameters contains all the parameters necessary (including the optional ones) for an UploadFile() request. // // There are three ways to upload a file. You can either set Content if file is small, set Reader if file is large, // or provide a local file path in File to upload it from your filesystem. // // Note that when using the Reader option, you *must* specify the Filename, otherwise the Slack API isn't happy. type FileUploadParameters struct { File string Content string Reader io.Reader Filetype string Filename string Title string InitialComment string Channels []string ThreadTimestamp string } // GetFilesParameters contains all the parameters necessary (including the optional ones) for a GetFiles() request type GetFilesParameters struct { User string Channel string TimestampFrom JSONTime TimestampTo JSONTime Types string Count int Page int ShowHidden bool } // ListFilesParameters contains all the parameters necessary (including the optional ones) for a ListFiles() request type ListFilesParameters struct { Limit int User string Channel string Types string Cursor string } type fileResponseFull struct { File `json:"file"` Paging `json:"paging"` Comments []Comment `json:"comments"` Files []File `json:"files"` Metadata ResponseMetadata `json:"response_metadata"` SlackResponse } // NewGetFilesParameters provides an instance of GetFilesParameters with all the sane default values set func NewGetFilesParameters() GetFilesParameters { return GetFilesParameters{ User: DEFAULT_FILES_USER, Channel: DEFAULT_FILES_CHANNEL, TimestampFrom: DEFAULT_FILES_TS_FROM, TimestampTo: DEFAULT_FILES_TS_TO, Types: DEFAULT_FILES_TYPES, Count: DEFAULT_FILES_COUNT, Page: DEFAULT_FILES_PAGE, ShowHidden: DEFAULT_FILES_SHOW_HIDDEN, } } func (api *Client) fileRequest(ctx context.Context, path string, values url.Values) (*fileResponseFull, error) { response := &fileResponseFull{} err := api.postMethod(ctx, path, values, response) if err != nil { return nil, err } return response, response.Err() } // GetFileInfo retrieves a file and related comments func (api *Client) GetFileInfo(fileID string, count, page int) (*File, []Comment, *Paging, error) { return api.GetFileInfoContext(context.Background(), fileID, count, page) } // GetFileInfoContext retrieves a file and related comments with a custom context func (api *Client) GetFileInfoContext(ctx context.Context, fileID string, count, page int) (*File, []Comment, *Paging, error) { values := url.Values{ "token": {api.token}, "file": {fileID}, "count": {strconv.Itoa(count)}, "page": {strconv.Itoa(page)}, } response, err := api.fileRequest(ctx, "files.info", values) if err != nil { return nil, nil, nil, err } return &response.File, response.Comments, &response.Paging, nil } // GetFile retreives a given file from its private download URL func (api *Client) GetFile(downloadURL string, writer io.Writer) error { return api.GetFileContext(context.Background(), downloadURL, writer) } // GetFileContext retreives a given file from its private download URL with a custom context // // For more details, see GetFile documentation. func (api *Client) GetFileContext(ctx context.Context, downloadURL string, writer io.Writer) error { return downloadFile(ctx, api.httpclient, api.token, downloadURL, writer, api) } // GetFiles retrieves all files according to the parameters given func (api *Client) GetFiles(params GetFilesParameters) ([]File, *Paging, error) { return api.GetFilesContext(context.Background(), params) } // GetFilesContext retrieves all files according to the parameters given with a custom context func (api *Client) GetFilesContext(ctx context.Context, params GetFilesParameters) ([]File, *Paging, error) { values := url.Values{ "token": {api.token}, } if params.User != DEFAULT_FILES_USER { values.Add("user", params.User) } if params.Channel != DEFAULT_FILES_CHANNEL { values.Add("channel", params.Channel) } if params.TimestampFrom != DEFAULT_FILES_TS_FROM { values.Add("ts_from", strconv.FormatInt(int64(params.TimestampFrom), 10)) } if params.TimestampTo != DEFAULT_FILES_TS_TO { values.Add("ts_to", strconv.FormatInt(int64(params.TimestampTo), 10)) } if params.Types != DEFAULT_FILES_TYPES { values.Add("types", params.Types) } if params.Count != DEFAULT_FILES_COUNT { values.Add("count", strconv.Itoa(params.Count)) } if params.Page != DEFAULT_FILES_PAGE { values.Add("page", strconv.Itoa(params.Page)) } if params.ShowHidden != DEFAULT_FILES_SHOW_HIDDEN { values.Add("show_files_hidden_by_limit", strconv.FormatBool(params.ShowHidden)) } response, err := api.fileRequest(ctx, "files.list", values) if err != nil { return nil, nil, err } return response.Files, &response.Paging, nil } // ListFiles retrieves all files according to the parameters given. Uses cursor based pagination. func (api *Client) ListFiles(params ListFilesParameters) ([]File, *ListFilesParameters, error) { return api.ListFilesContext(context.Background(), params) } // ListFilesContext retrieves all files according to the parameters given with a custom context. // // For more details, see ListFiles documentation. func (api *Client) ListFilesContext(ctx context.Context, params ListFilesParameters) ([]File, *ListFilesParameters, error) { values := url.Values{ "token": {api.token}, } if params.User != DEFAULT_FILES_USER { values.Add("user", params.User) } if params.Channel != DEFAULT_FILES_CHANNEL { values.Add("channel", params.Channel) } if params.Limit != DEFAULT_FILES_COUNT { values.Add("limit", strconv.Itoa(params.Limit)) } if params.Cursor != "" { values.Add("cursor", params.Cursor) } response, err := api.fileRequest(ctx, "files.list", values) if err != nil { return nil, nil, err } params.Cursor = response.Metadata.Cursor return response.Files, ¶ms, nil } // UploadFile uploads a file func (api *Client) UploadFile(params FileUploadParameters) (file *File, err error) { return api.UploadFileContext(context.Background(), params) } // UploadFileContext uploads a file and setting a custom context func (api *Client) UploadFileContext(ctx context.Context, params FileUploadParameters) (file *File, err error) { // Test if user token is valid. This helps because client.Do doesn't like this for some reason. XXX: More // investigation needed, but for now this will do. _, err = api.AuthTestContext(ctx) if err != nil { return nil, err } response := &fileResponseFull{} values := url.Values{} if params.Filetype != "" { values.Add("filetype", params.Filetype) } if params.Filename != "" { values.Add("filename", params.Filename) } if params.Title != "" { values.Add("title", params.Title) } if params.InitialComment != "" { values.Add("initial_comment", params.InitialComment) } if params.ThreadTimestamp != "" { values.Add("thread_ts", params.ThreadTimestamp) } if len(params.Channels) != 0 { values.Add("channels", strings.Join(params.Channels, ",")) } if params.Content != "" { values.Add("content", params.Content) values.Add("token", api.token) err = api.postMethod(ctx, "files.upload", values, response) } else if params.File != "" { err = postLocalWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.File, "file", api.token, values, response, api) } else if params.Reader != nil { if params.Filename == "" { return nil, fmt.Errorf("files.upload: FileUploadParameters.Filename is mandatory when using FileUploadParameters.Reader") } err = postWithMultipartResponse(ctx, api.httpclient, api.endpoint+"files.upload", params.Filename, "file", api.token, values, params.Reader, response, api) } if err != nil { return nil, err } return &response.File, response.Err() } // DeleteFileComment deletes a file's comment func (api *Client) DeleteFileComment(commentID, fileID string) error { return api.DeleteFileCommentContext(context.Background(), fileID, commentID) } // DeleteFileCommentContext deletes a file's comment with a custom context func (api *Client) DeleteFileCommentContext(ctx context.Context, fileID, commentID string) (err error) { if fileID == "" || commentID == "" { return ErrParametersMissing } values := url.Values{ "token": {api.token}, "file": {fileID}, "id": {commentID}, } _, err = api.fileRequest(ctx, "files.comments.delete", values) return err } // DeleteFile deletes a file func (api *Client) DeleteFile(fileID string) error { return api.DeleteFileContext(context.Background(), fileID) } // DeleteFileContext deletes a file with a custom context func (api *Client) DeleteFileContext(ctx context.Context, fileID string) (err error) { values := url.Values{ "token": {api.token}, "file": {fileID}, } _, err = api.fileRequest(ctx, "files.delete", values) return err } // RevokeFilePublicURL disables public/external sharing for a file func (api *Client) RevokeFilePublicURL(fileID string) (*File, error) { return api.RevokeFilePublicURLContext(context.Background(), fileID) } // RevokeFilePublicURLContext disables public/external sharing for a file with a custom context func (api *Client) RevokeFilePublicURLContext(ctx context.Context, fileID string) (*File, error) { values := url.Values{ "token": {api.token}, "file": {fileID}, } response, err := api.fileRequest(ctx, "files.revokePublicURL", values) if err != nil { return nil, err } return &response.File, nil } // ShareFilePublicURL enabled public/external sharing for a file func (api *Client) ShareFilePublicURL(fileID string) (*File, []Comment, *Paging, error) { return api.ShareFilePublicURLContext(context.Background(), fileID) } // ShareFilePublicURLContext enabled public/external sharing for a file with a custom context func (api *Client) ShareFilePublicURLContext(ctx context.Context, fileID string) (*File, []Comment, *Paging, error) { values := url.Values{ "token": {api.token}, "file": {fileID}, } response, err := api.fileRequest(ctx, "files.sharedPublicURL", values) if err != nil { return nil, nil, nil, err } return &response.File, response.Comments, &response.Paging, nil } slack-0.11.3/files_test.go000066400000000000000000000125741430741033100153660ustar00rootroot00000000000000package slack import ( "bytes" "encoding/json" "io/ioutil" "log" "net/http" "net/url" "reflect" "strings" "testing" ) type fileCommentHandler struct { gotParams map[string]string } func newFileCommentHandler() *fileCommentHandler { return &fileCommentHandler{ gotParams: make(map[string]string), } } func (h *fileCommentHandler) accumulateFormValue(k string, r *http.Request) { if v := r.FormValue(k); v != "" { h.gotParams[k] = v } } func (h *fileCommentHandler) handler(w http.ResponseWriter, r *http.Request) { h.accumulateFormValue("token", r) h.accumulateFormValue("file", r) h.accumulateFormValue("id", r) w.Header().Set("Content-Type", "application/json") if h.gotParams["id"] == "trigger-error" { w.Write([]byte(`{ "ok": false, "error": "errored" }`)) } else { w.Write([]byte(`{ "ok": true }`)) } } type mockHTTPClient struct{} func (m *mockHTTPClient) Do(*http.Request) (*http.Response, error) { return &http.Response{StatusCode: 200, Body: ioutil.NopCloser(bytes.NewBufferString(`OK`))}, nil } func TestSlack_GetFile(t *testing.T) { api := &Client{ endpoint: "http://" + serverAddr + "/", token: "testing-token", httpclient: &mockHTTPClient{}, } tests := []struct { title string downloadURL string expectError bool }{ { title: "Testing with valid file", downloadURL: "https://files.slack.com/files-pri/T99999999-FGGGGGGGG/download/test.csv", expectError: false, }, { title: "Testing with invalid file (empty URL)", downloadURL: "", expectError: true, }, } for _, test := range tests { err := api.GetFile(test.downloadURL, &bytes.Buffer{}) if !test.expectError && err != nil { log.Fatalf("%s: Unexpected error: %s in test", test.title, err) } else if test.expectError == true && err == nil { log.Fatalf("Expected error but got none") } } } func TestSlack_DeleteFileComment(t *testing.T) { once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) tests := []struct { title string body url.Values wantParams map[string]string expectError bool }{ { title: "Testing with proper body", body: url.Values{ "file": {"file12345"}, "id": {"id12345"}, }, wantParams: map[string]string{ "token": "testing-token", "file": "file12345", "id": "id12345", }, expectError: false, }, { title: "Testing with false body", body: url.Values{ "file": {""}, "id": {""}, }, wantParams: map[string]string{}, expectError: true, }, { title: "Testing with error", body: url.Values{ "file": {"file12345"}, "id": {"trigger-error"}, }, wantParams: map[string]string{ "token": "testing-token", "file": "file12345", "id": "trigger-error", }, expectError: true, }, } var fch *fileCommentHandler http.HandleFunc("/files.comments.delete", func(w http.ResponseWriter, r *http.Request) { fch.handler(w, r) }) for _, test := range tests { fch = newFileCommentHandler() err := api.DeleteFileComment(test.body["id"][0], test.body["file"][0]) if !test.expectError && err != nil { log.Fatalf("%s: Unexpected error: %s in test", test.title, err) } else if test.expectError == true && err == nil { log.Fatalf("Expected error but got none") } if !reflect.DeepEqual(fch.gotParams, test.wantParams) { log.Fatalf("%s: Got params [%#v]\nBut received [%#v]\n", test.title, fch.gotParams, test.wantParams) } } } func authTestHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(authTestResponseFull{ SlackResponse: SlackResponse{Ok: true}}) rw.Write(response) } func uploadFileHandler(rw http.ResponseWriter, r *http.Request) { rw.Header().Set("Content-Type", "application/json") response, _ := json.Marshal(fileResponseFull{ SlackResponse: SlackResponse{Ok: true}}) rw.Write(response) } func TestUploadFile(t *testing.T) { http.HandleFunc("/auth.test", authTestHandler) http.HandleFunc("/files.upload", uploadFileHandler) once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) params := FileUploadParameters{ Filename: "test.txt", Content: "test content", Channels: []string{"CXXXXXXXX"}} if _, err := api.UploadFile(params); err != nil { t.Errorf("Unexpected error: %s", err) } reader := bytes.NewBufferString("test reader") params = FileUploadParameters{ Filename: "test.txt", Reader: reader, Channels: []string{"CXXXXXXXX"}} if _, err := api.UploadFile(params); err != nil { t.Errorf("Unexpected error: %s", err) } largeByt := make([]byte, 107374200) reader = bytes.NewBuffer(largeByt) params = FileUploadParameters{ Filename: "test.txt", Reader: reader, Channels: []string{"CXXXXXXXX"}} if _, err := api.UploadFile(params); err != nil { t.Errorf("Unexpected error: %s", err) } } func TestUploadFileWithoutFilename(t *testing.T) { once.Do(startServer) api := New("testing-token", OptionAPIURL("http://"+serverAddr+"/")) reader := bytes.NewBufferString("test reader") params := FileUploadParameters{ Reader: reader, Channels: []string{"CXXXXXXXX"}} _, err := api.UploadFile(params) if err == nil { t.Fatal("Expected error when omitting filename, instead got nil") } if !strings.Contains(err.Error(), ".Filename is mandatory") { t.Errorf("Error message should mention empty FileUploadParameters.Filename") } } slack-0.11.3/go.mod000066400000000000000000000004421430741033100137730ustar00rootroot00000000000000module github.com/slack-go/slack go 1.16 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-test/deep v1.0.4 github.com/google/go-cmp v0.5.7 github.com/gorilla/websocket v1.4.2 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 ) slack-0.11.3/go.sum000066400000000000000000000023331430741033100140210ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho= github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= slack-0.11.3/groups.go000066400000000000000000000002121430741033100145260ustar00rootroot00000000000000package slack // Group contains all the information for a group type Group struct { GroupConversation IsGroup bool `json:"is_group"` } slack-0.11.3/history.go000066400000000000000000000021001430741033100147060ustar00rootroot00000000000000package slack const ( DEFAULT_HISTORY_LATEST = "" DEFAULT_HISTORY_OLDEST = "0" DEFAULT_HISTORY_COUNT = 100 DEFAULT_HISTORY_INCLUSIVE = false DEFAULT_HISTORY_UNREADS = false ) // HistoryParameters contains all the necessary information to help in the retrieval of history for Channels/Groups/DMs type HistoryParameters struct { Latest string Oldest string Count int Inclusive bool Unreads bool } // History contains message history information needed to navigate a Channel / Group / DM history type History struct { Latest string `json:"latest"` Messages []Message `json:"messages"` HasMore bool `json:"has_more"` Unread int `json:"unread_count_display"` } // NewHistoryParameters provides an instance of HistoryParameters with all the sane default values set func NewHistoryParameters() HistoryParameters { return HistoryParameters{ Latest: DEFAULT_HISTORY_LATEST, Oldest: DEFAULT_HISTORY_OLDEST, Count: DEFAULT_HISTORY_COUNT, Inclusive: DEFAULT_HISTORY_INCLUSIVE, Unreads: DEFAULT_HISTORY_UNREADS, } } slack-0.11.3/im.go000066400000000000000000000007401430741033100136220ustar00rootroot00000000000000package slack type imChannel struct { ID string `json:"id"` } type imResponseFull struct { NoOp bool `json:"no_op"` AlreadyClosed bool `json:"already_closed"` AlreadyOpen bool `json:"already_open"` Channel imChannel `json:"channel"` IMs []IM `json:"ims"` History SlackResponse } // IM contains information related to the Direct Message channel type IM struct { Conversation IsUserDeleted bool `json:"is_user_deleted"` } slack-0.11.3/info.go000066400000000000000000000734311430741033100141570ustar00rootroot00000000000000package slack import ( "bytes" "context" "fmt" "net/url" "strconv" "strings" "time" ) type UserPrefsCarrier struct { SlackResponse UserPrefs *UserPrefs `json:"prefs"` } // UserPrefs carries a bunch of user settings including some unknown types type UserPrefs struct { UserColors string `json:"user_colors,omitempty"` ColorNamesInList bool `json:"color_names_in_list,omitempty"` // Keyboard UnknownType `json:"keyboard"` EmailAlerts string `json:"email_alerts,omitempty"` EmailAlertsSleepUntil int `json:"email_alerts_sleep_until,omitempty"` EmailTips bool `json:"email_tips,omitempty"` EmailWeekly bool `json:"email_weekly,omitempty"` EmailOffers bool `json:"email_offers,omitempty"` EmailResearch bool `json:"email_research,omitempty"` EmailDeveloper bool `json:"email_developer,omitempty"` WelcomeMessageHidden bool `json:"welcome_message_hidden,omitempty"` SearchSort string `json:"search_sort,omitempty"` SearchFileSort string `json:"search_file_sort,omitempty"` SearchChannelSort string `json:"search_channel_sort,omitempty"` SearchPeopleSort string `json:"search_people_sort,omitempty"` ExpandInlineImages bool `json:"expand_inline_images,omitempty"` ExpandInternalInlineImages bool `json:"expand_internal_inline_images,omitempty"` ExpandSnippets bool `json:"expand_snippets,omitempty"` PostsFormattingGuide bool `json:"posts_formatting_guide,omitempty"` SeenWelcome2 bool `json:"seen_welcome_2,omitempty"` SeenSSBPrompt bool `json:"seen_ssb_prompt,omitempty"` SpacesNewXpBannerDismissed bool `json:"spaces_new_xp_banner_dismissed,omitempty"` SearchOnlyMyChannels bool `json:"search_only_my_channels,omitempty"` SearchOnlyCurrentTeam bool `json:"search_only_current_team,omitempty"` SearchHideMyChannels bool `json:"search_hide_my_channels,omitempty"` SearchOnlyShowOnline bool `json:"search_only_show_online,omitempty"` SearchHideDeactivatedUsers bool `json:"search_hide_deactivated_users,omitempty"` EmojiMode string `json:"emoji_mode,omitempty"` EmojiUse string `json:"emoji_use,omitempty"` HasInvited bool `json:"has_invited,omitempty"` HasUploaded bool `json:"has_uploaded,omitempty"` HasCreatedChannel bool `json:"has_created_channel,omitempty"` HasSearched bool `json:"has_searched,omitempty"` SearchExcludeChannels string `json:"search_exclude_channels,omitempty"` MessagesTheme string `json:"messages_theme,omitempty"` WebappSpellcheck bool `json:"webapp_spellcheck,omitempty"` NoJoinedOverlays bool `json:"no_joined_overlays,omitempty"` NoCreatedOverlays bool `json:"no_created_overlays,omitempty"` DropboxEnabled bool `json:"dropbox_enabled,omitempty"` SeenDomainInviteReminder bool `json:"seen_domain_invite_reminder,omitempty"` SeenMemberInviteReminder bool `json:"seen_member_invite_reminder,omitempty"` MuteSounds bool `json:"mute_sounds,omitempty"` ArrowHistory bool `json:"arrow_history,omitempty"` TabUIReturnSelects bool `json:"tab_ui_return_selects,omitempty"` ObeyInlineImgLimit bool `json:"obey_inline_img_limit,omitempty"` RequireAt bool `json:"require_at,omitempty"` SsbSpaceWindow string `json:"ssb_space_window,omitempty"` MacSsbBounce string `json:"mac_ssb_bounce,omitempty"` MacSsbBullet bool `json:"mac_ssb_bullet,omitempty"` ExpandNonMediaAttachments bool `json:"expand_non_media_attachments,omitempty"` ShowTyping bool `json:"show_typing,omitempty"` PagekeysHandled bool `json:"pagekeys_handled,omitempty"` LastSnippetType string `json:"last_snippet_type,omitempty"` DisplayRealNamesOverride int `json:"display_real_names_override,omitempty"` DisplayDisplayNames bool `json:"display_display_names,omitempty"` Time24 bool `json:"time24,omitempty"` EnterIsSpecialInTbt bool `json:"enter_is_special_in_tbt,omitempty"` MsgInputSendBtn bool `json:"msg_input_send_btn,omitempty"` MsgInputSendBtnAutoSet bool `json:"msg_input_send_btn_auto_set,omitempty"` MsgInputStickyComposer bool `json:"msg_input_sticky_composer,omitempty"` GraphicEmoticons bool `json:"graphic_emoticons,omitempty"` ConvertEmoticons bool `json:"convert_emoticons,omitempty"` SsEmojis bool `json:"ss_emojis,omitempty"` SeenOnboardingStart bool `json:"seen_onboarding_start,omitempty"` OnboardingCancelled bool `json:"onboarding_cancelled,omitempty"` SeenOnboardingSlackbotConversation bool `json:"seen_onboarding_slackbot_conversation,omitempty"` SeenOnboardingChannels bool `json:"seen_onboarding_channels,omitempty"` SeenOnboardingDirectMessages bool `json:"seen_onboarding_direct_messages,omitempty"` SeenOnboardingInvites bool `json:"seen_onboarding_invites,omitempty"` SeenOnboardingSearch bool `json:"seen_onboarding_search,omitempty"` SeenOnboardingRecentMentions bool `json:"seen_onboarding_recent_mentions,omitempty"` SeenOnboardingStarredItems bool `json:"seen_onboarding_starred_items,omitempty"` SeenOnboardingPrivateGroups bool `json:"seen_onboarding_private_groups,omitempty"` SeenOnboardingBanner bool `json:"seen_onboarding_banner,omitempty"` OnboardingSlackbotConversationStep int `json:"onboarding_slackbot_conversation_step,omitempty"` SetTzAutomatically bool `json:"set_tz_automatically,omitempty"` SuppressLinkWarning bool `json:"suppress_link_warning,omitempty"` DndEnabled bool `json:"dnd_enabled,omitempty"` DndStartHour string `json:"dnd_start_hour,omitempty"` DndEndHour string `json:"dnd_end_hour,omitempty"` DndBeforeMonday string `json:"dnd_before_monday,omitempty"` DndAfterMonday string `json:"dnd_after_monday,omitempty"` DndEnabledMonday string `json:"dnd_enabled_monday,omitempty"` DndBeforeTuesday string `json:"dnd_before_tuesday,omitempty"` DndAfterTuesday string `json:"dnd_after_tuesday,omitempty"` DndEnabledTuesday string `json:"dnd_enabled_tuesday,omitempty"` DndBeforeWednesday string `json:"dnd_before_wednesday,omitempty"` DndAfterWednesday string `json:"dnd_after_wednesday,omitempty"` DndEnabledWednesday string `json:"dnd_enabled_wednesday,omitempty"` DndBeforeThursday string `json:"dnd_before_thursday,omitempty"` DndAfterThursday string `json:"dnd_after_thursday,omitempty"` DndEnabledThursday string `json:"dnd_enabled_thursday,omitempty"` DndBeforeFriday string `json:"dnd_before_friday,omitempty"` DndAfterFriday string `json:"dnd_after_friday,omitempty"` DndEnabledFriday string `json:"dnd_enabled_friday,omitempty"` DndBeforeSaturday string `json:"dnd_before_saturday,omitempty"` DndAfterSaturday string `json:"dnd_after_saturday,omitempty"` DndEnabledSaturday string `json:"dnd_enabled_saturday,omitempty"` DndBeforeSunday string `json:"dnd_before_sunday,omitempty"` DndAfterSunday string `json:"dnd_after_sunday,omitempty"` DndEnabledSunday string `json:"dnd_enabled_sunday,omitempty"` DndDays string `json:"dnd_days,omitempty"` DndCustomNewBadgeSeen bool `json:"dnd_custom_new_badge_seen,omitempty"` DndNotificationScheduleNewBadgeSeen bool `json:"dnd_notification_schedule_new_badge_seen,omitempty"` // UnreadCollapsedChannels unknownType `json:"unread_collapsed_channels,omitempty"` SidebarBehavior string `json:"sidebar_behavior,omitempty"` ChannelSort string `json:"channel_sort,omitempty"` SeparatePrivateChannels bool `json:"separate_private_channels,omitempty"` SeparateSharedChannels bool `json:"separate_shared_channels,omitempty"` SidebarTheme string `json:"sidebar_theme,omitempty"` SidebarThemeCustomValues string `json:"sidebar_theme_custom_values,omitempty"` NoInvitesWidgetInSidebar bool `json:"no_invites_widget_in_sidebar,omitempty"` NoOmniboxInChannels bool `json:"no_omnibox_in_channels,omitempty"` KKeyOmniboxAutoHideCount int `json:"k_key_omnibox_auto_hide_count,omitempty"` ShowSidebarQuickswitcherButton bool `json:"show_sidebar_quickswitcher_button,omitempty"` EntOrgWideChannelsSidebar bool `json:"ent_org_wide_channels_sidebar,omitempty"` MarkMsgsReadImmediately bool `json:"mark_msgs_read_immediately,omitempty"` StartScrollAtOldest bool `json:"start_scroll_at_oldest,omitempty"` SnippetEditorWrapLongLines bool `json:"snippet_editor_wrap_long_lines,omitempty"` LsDisabled bool `json:"ls_disabled,omitempty"` FKeySearch bool `json:"f_key_search,omitempty"` KKeyOmnibox bool `json:"k_key_omnibox,omitempty"` PromptedForEmailDisabling bool `json:"prompted_for_email_disabling,omitempty"` NoMacelectronBanner bool `json:"no_macelectron_banner,omitempty"` NoMacssb1Banner bool `json:"no_macssb1_banner,omitempty"` NoMacssb2Banner bool `json:"no_macssb2_banner,omitempty"` NoWinssb1Banner bool `json:"no_winssb1_banner,omitempty"` HideUserGroupInfoPane bool `json:"hide_user_group_info_pane,omitempty"` MentionsExcludeAtUserGroups bool `json:"mentions_exclude_at_user_groups,omitempty"` MentionsExcludeReactions bool `json:"mentions_exclude_reactions,omitempty"` PrivacyPolicySeen bool `json:"privacy_policy_seen,omitempty"` EnterpriseMigrationSeen bool `json:"enterprise_migration_seen,omitempty"` LastTosAcknowledged string `json:"last_tos_acknowledged,omitempty"` SearchExcludeBots bool `json:"search_exclude_bots,omitempty"` LoadLato2 bool `json:"load_lato_2,omitempty"` FullerTimestamps bool `json:"fuller_timestamps,omitempty"` LastSeenAtChannelWarning int `json:"last_seen_at_channel_warning,omitempty"` EmojiAutocompleteBig bool `json:"emoji_autocomplete_big,omitempty"` TwoFactorAuthEnabled bool `json:"two_factor_auth_enabled,omitempty"` // TwoFactorType unknownType `json:"two_factor_type,omitempty"` // TwoFactorBackupType unknownType `json:"two_factor_backup_type,omitempty"` HideHexSwatch bool `json:"hide_hex_swatch,omitempty"` ShowJumperScores bool `json:"show_jumper_scores,omitempty"` EnterpriseMdmCustomMsg string `json:"enterprise_mdm_custom_msg,omitempty"` // EnterpriseExcludedAppTeams unknownType `json:"enterprise_excluded_app_teams,omitempty"` ClientLogsPri string `json:"client_logs_pri,omitempty"` FlannelServerPool string `json:"flannel_server_pool,omitempty"` MentionsExcludeAtChannels bool `json:"mentions_exclude_at_channels,omitempty"` ConfirmClearAllUnreads bool `json:"confirm_clear_all_unreads,omitempty"` ConfirmUserMarkedAway bool `json:"confirm_user_marked_away,omitempty"` BoxEnabled bool `json:"box_enabled,omitempty"` SeenSingleEmojiMsg bool `json:"seen_single_emoji_msg,omitempty"` ConfirmShCallStart bool `json:"confirm_sh_call_start,omitempty"` PreferredSkinTone string `json:"preferred_skin_tone,omitempty"` ShowAllSkinTones bool `json:"show_all_skin_tones,omitempty"` WhatsNewRead int `json:"whats_new_read,omitempty"` // FrecencyJumper unknownType `json:"frecency_jumper,omitempty"` FrecencyEntJumper string `json:"frecency_ent_jumper,omitempty"` FrecencyEntJumperBackup string `json:"frecency_ent_jumper_backup,omitempty"` Jumbomoji bool `json:"jumbomoji,omitempty"` NewxpSeenLastMessage int `json:"newxp_seen_last_message,omitempty"` ShowMemoryInstrument bool `json:"show_memory_instrument,omitempty"` EnableUnreadView bool `json:"enable_unread_view,omitempty"` SeenUnreadViewCoachmark bool `json:"seen_unread_view_coachmark,omitempty"` EnableReactEmojiPicker bool `json:"enable_react_emoji_picker,omitempty"` SeenCustomStatusBadge bool `json:"seen_custom_status_badge,omitempty"` SeenCustomStatusCallout bool `json:"seen_custom_status_callout,omitempty"` SeenCustomStatusExpirationBadge bool `json:"seen_custom_status_expiration_badge,omitempty"` UsedCustomStatusKbShortcut bool `json:"used_custom_status_kb_shortcut,omitempty"` SeenGuestAdminSlackbotAnnouncement bool `json:"seen_guest_admin_slackbot_announcement,omitempty"` SeenThreadsNotificationBanner bool `json:"seen_threads_notification_banner,omitempty"` SeenNameTaggingCoachmark bool `json:"seen_name_tagging_coachmark,omitempty"` AllUnreadsSortOrder string `json:"all_unreads_sort_order,omitempty"` Locale string `json:"locale,omitempty"` SeenIntlChannelNamesCoachmark bool `json:"seen_intl_channel_names_coachmark,omitempty"` SeenP2LocaleChangeMessage int `json:"seen_p2_locale_change_message,omitempty"` SeenLocaleChangeMessage int `json:"seen_locale_change_message,omitempty"` SeenJapaneseLocaleChangeMessage bool `json:"seen_japanese_locale_change_message,omitempty"` SeenSharedChannelsCoachmark bool `json:"seen_shared_channels_coachmark,omitempty"` SeenSharedChannelsOptInChangeMessage bool `json:"seen_shared_channels_opt_in_change_message,omitempty"` HasRecentlySharedaChannel bool `json:"has_recently_shared_a_channel,omitempty"` SeenChannelBrowserAdminCoachmark bool `json:"seen_channel_browser_admin_coachmark,omitempty"` SeenAdministrationMenu bool `json:"seen_administration_menu,omitempty"` SeenDraftsSectionCoachmark bool `json:"seen_drafts_section_coachmark,omitempty"` SeenEmojiUpdateOverlayCoachmark bool `json:"seen_emoji_update_overlay_coachmark,omitempty"` SeenSonicDeluxeToast int `json:"seen_sonic_deluxe_toast,omitempty"` SeenWysiwygDeluxeToast bool `json:"seen_wysiwyg_deluxe_toast,omitempty"` SeenMarkdownPasteToast int `json:"seen_markdown_paste_toast,omitempty"` SeenMarkdownPasteShortcut int `json:"seen_markdown_paste_shortcut,omitempty"` SeenIaEducation bool `json:"seen_ia_education,omitempty"` PlainTextMode bool `json:"plain_text_mode,omitempty"` ShowSharedChannelsEducationBanner bool `json:"show_shared_channels_education_banner,omitempty"` AllowCallsToSetCurrentStatus bool `json:"allow_calls_to_set_current_status,omitempty"` InInteractiveMasMigrationFlow bool `json:"in_interactive_mas_migration_flow,omitempty"` SunsetInteractiveMessageViews int `json:"sunset_interactive_message_views,omitempty"` ShdepPromoCodeSubmitted bool `json:"shdep_promo_code_submitted,omitempty"` SeenShdepSlackbotMessage bool `json:"seen_shdep_slackbot_message,omitempty"` SeenCallsInteractiveCoachmark bool `json:"seen_calls_interactive_coachmark,omitempty"` AllowCmdTabIss bool `json:"allow_cmd_tab_iss,omitempty"` SeenWorkflowBuilderDeluxeToast bool `json:"seen_workflow_builder_deluxe_toast,omitempty"` WorkflowBuilderIntroModalClickedThrough bool `json:"workflow_builder_intro_modal_clicked_through,omitempty"` // WorkflowBuilderCoachmarks unknownType `json:"workflow_builder_coachmarks,omitempty"` SeenGdriveCoachmark bool `json:"seen_gdrive_coachmark,omitempty"` OverloadedMessageEnabled bool `json:"overloaded_message_enabled,omitempty"` SeenHighlightsCoachmark bool `json:"seen_highlights_coachmark,omitempty"` SeenHighlightsArrowsCoachmark bool `json:"seen_highlights_arrows_coachmark,omitempty"` SeenHighlightsWarmWelcome bool `json:"seen_highlights_warm_welcome,omitempty"` SeenNewSearchUi bool `json:"seen_new_search_ui,omitempty"` SeenChannelSearch bool `json:"seen_channel_search,omitempty"` SeenPeopleSearch bool `json:"seen_people_search,omitempty"` SeenPeopleSearchCount int `json:"seen_people_search_count,omitempty"` DismissedScrollSearchTooltipCount int `json:"dismissed_scroll_search_tooltip_count,omitempty"` LastDismissedScrollSearchTooltipTimestamp int `json:"last_dismissed_scroll_search_tooltip_timestamp,omitempty"` HasUsedQuickswitcherShortcut bool `json:"has_used_quickswitcher_shortcut,omitempty"` SeenQuickswitcherShortcutTipCount int `json:"seen_quickswitcher_shortcut_tip_count,omitempty"` BrowsersDismissedChannelsLowResultsEducation bool `json:"browsers_dismissed_channels_low_results_education,omitempty"` BrowsersSeenInitialChannelsEducation bool `json:"browsers_seen_initial_channels_education,omitempty"` BrowsersDismissedPeopleLowResultsEducation bool `json:"browsers_dismissed_people_low_results_education,omitempty"` BrowsersSeenInitialPeopleEducation bool `json:"browsers_seen_initial_people_education,omitempty"` BrowsersDismissedUserGroupsLowResultsEducation bool `json:"browsers_dismissed_user_groups_low_results_education,omitempty"` BrowsersSeenInitialUserGroupsEducation bool `json:"browsers_seen_initial_user_groups_education,omitempty"` BrowsersDismissedFilesLowResultsEducation bool `json:"browsers_dismissed_files_low_results_education,omitempty"` BrowsersSeenInitialFilesEducation bool `json:"browsers_seen_initial_files_education,omitempty"` A11yAnimations bool `json:"a11y_animations,omitempty"` SeenKeyboardShortcutsCoachmark bool `json:"seen_keyboard_shortcuts_coachmark,omitempty"` NeedsInitialPasswordSet bool `json:"needs_initial_password_set,omitempty"` LessonsEnabled bool `json:"lessons_enabled,omitempty"` TractorEnabled bool `json:"tractor_enabled,omitempty"` TractorExperimentGroup string `json:"tractor_experiment_group,omitempty"` OpenedSlackbotDm bool `json:"opened_slackbot_dm,omitempty"` NewxpSuggestedChannels string `json:"newxp_suggested_channels,omitempty"` OnboardingComplete bool `json:"onboarding_complete,omitempty"` WelcomePlaceState string `json:"welcome_place_state,omitempty"` // OnboardingRoleApps unknownType `json:"onboarding_role_apps,omitempty"` HasReceivedThreadedMessage bool `json:"has_received_threaded_message,omitempty"` SendYourFirstMessageBannerEnabled bool `json:"send_your_first_message_banner_enabled,omitempty"` WhocanseethisDmMpdmBadge bool `json:"whocanseethis_dm_mpdm_badge,omitempty"` HighlightWords string `json:"highlight_words,omitempty"` ThreadsEverything bool `json:"threads_everything,omitempty"` NoTextInNotifications bool `json:"no_text_in_notifications,omitempty"` PushShowPreview bool `json:"push_show_preview,omitempty"` GrowlsEnabled bool `json:"growls_enabled,omitempty"` AllChannelsLoud bool `json:"all_channels_loud,omitempty"` PushDmAlert bool `json:"push_dm_alert,omitempty"` PushMentionAlert bool `json:"push_mention_alert,omitempty"` PushEverything bool `json:"push_everything,omitempty"` PushIdleWait int `json:"push_idle_wait,omitempty"` PushSound string `json:"push_sound,omitempty"` NewMsgSnd string `json:"new_msg_snd,omitempty"` PushLoudChannels string `json:"push_loud_channels,omitempty"` PushMentionChannels string `json:"push_mention_channels,omitempty"` PushLoudChannelsSet string `json:"push_loud_channels_set,omitempty"` LoudChannels string `json:"loud_channels,omitempty"` NeverChannels string `json:"never_channels,omitempty"` LoudChannelsSet string `json:"loud_channels_set,omitempty"` AtChannelSuppressedChannels string `json:"at_channel_suppressed_channels,omitempty"` PushAtChannelSuppressedChannels string `json:"push_at_channel_suppressed_channels,omitempty"` MutedChannels string `json:"muted_channels,omitempty"` // AllNotificationsPrefs unknownType `json:"all_notifications_prefs,omitempty"` GrowthMsgLimitApproachingCtaCount int `json:"growth_msg_limit_approaching_cta_count,omitempty"` GrowthMsgLimitApproachingCtaTs int `json:"growth_msg_limit_approaching_cta_ts,omitempty"` GrowthMsgLimitReachedCtaCount int `json:"growth_msg_limit_reached_cta_count,omitempty"` GrowthMsgLimitReachedCtaLastTs int `json:"growth_msg_limit_reached_cta_last_ts,omitempty"` GrowthMsgLimitLongReachedCtaCount int `json:"growth_msg_limit_long_reached_cta_count,omitempty"` GrowthMsgLimitLongReachedCtaLastTs int `json:"growth_msg_limit_long_reached_cta_last_ts,omitempty"` GrowthMsgLimitSixtyDayBannerCtaCount int `json:"growth_msg_limit_sixty_day_banner_cta_count,omitempty"` GrowthMsgLimitSixtyDayBannerCtaLastTs int `json:"growth_msg_limit_sixty_day_banner_cta_last_ts,omitempty"` // GrowthAllBannersPrefs unknownType `json:"growth_all_banners_prefs,omitempty"` AnalyticsUpsellCoachmarkSeen bool `json:"analytics_upsell_coachmark_seen,omitempty"` SeenAppSpaceCoachmark bool `json:"seen_app_space_coachmark,omitempty"` SeenAppSpaceTutorial bool `json:"seen_app_space_tutorial,omitempty"` DismissedAppLauncherWelcome bool `json:"dismissed_app_launcher_welcome,omitempty"` DismissedAppLauncherLimit bool `json:"dismissed_app_launcher_limit,omitempty"` Purchaser bool `json:"purchaser,omitempty"` ShowEntOnboarding bool `json:"show_ent_onboarding,omitempty"` FoldersEnabled bool `json:"folders_enabled,omitempty"` // FolderData unknownType `json:"folder_data,omitempty"` SeenCorporateExportAlert bool `json:"seen_corporate_export_alert,omitempty"` ShowAutocompleteHelp int `json:"show_autocomplete_help,omitempty"` DeprecationToastLastSeen int `json:"deprecation_toast_last_seen,omitempty"` DeprecationModalLastSeen int `json:"deprecation_modal_last_seen,omitempty"` Iap1Lab int `json:"iap1_lab,omitempty"` IaTopNavTheme string `json:"ia_top_nav_theme,omitempty"` IaPlatformActionsLab int `json:"ia_platform_actions_lab,omitempty"` ActivityView string `json:"activity_view,omitempty"` FailoverProxyCheckCompleted int `json:"failover_proxy_check_completed,omitempty"` EdgeUploadProxyCheckCompleted int `json:"edge_upload_proxy_check_completed,omitempty"` AppSubdomainCheckCompleted int `json:"app_subdomain_check_completed,omitempty"` AddAppsPromptDismissed bool `json:"add_apps_prompt_dismissed,omitempty"` AddChannelPromptDismissed bool `json:"add_channel_prompt_dismissed,omitempty"` ChannelSidebarHideInvite bool `json:"channel_sidebar_hide_invite,omitempty"` InProdSurveysEnabled bool `json:"in_prod_surveys_enabled,omitempty"` DismissedInstalledAppDmSuggestions string `json:"dismissed_installed_app_dm_suggestions,omitempty"` SeenContextualMessageShortcutsModal bool `json:"seen_contextual_message_shortcuts_modal,omitempty"` SeenMessageNavigationEducationalToast bool `json:"seen_message_navigation_educational_toast,omitempty"` ContextualMessageShortcutsModalWasSeen bool `json:"contextual_message_shortcuts_modal_was_seen,omitempty"` MessageNavigationToastWasSeen bool `json:"message_navigation_toast_was_seen,omitempty"` UpToBrowseKbShortcut bool `json:"up_to_browse_kb_shortcut,omitempty"` ChannelSections string `json:"channel_sections,omitempty"` TZ string `json:"tz,omitempty"` } func (api *Client) GetUserPrefs() (*UserPrefsCarrier, error) { return api.GetUserPrefsContext(context.Background()) } func (api *Client) GetUserPrefsContext(ctx context.Context) (*UserPrefsCarrier, error) { response := UserPrefsCarrier{} err := api.getMethod(ctx, "users.prefs.get", api.token, url.Values{}, &response) if err != nil { return nil, err } return &response, response.Err() } func (api *Client) MuteChat(channelID string) (*UserPrefsCarrier, error) { prefs, err := api.GetUserPrefs() if err != nil { return nil, err } chnls := strings.Split(prefs.UserPrefs.MutedChannels, ",") for _, chn := range chnls { if chn == channelID { return nil, nil // noop } } newChnls := prefs.UserPrefs.MutedChannels + "," + channelID values := url.Values{"token": {api.token}, "muted_channels": {newChnls}, "reason": {"update-muted-channels"}} response := UserPrefsCarrier{} err = api.postMethod(context.Background(), "users.prefs.set", values, &response) if err != nil { return nil, err } return &response, response.Err() } func (api *Client) UnMuteChat(channelID string) (*UserPrefsCarrier, error) { prefs, err := api.GetUserPrefs() if err != nil { return nil, err } chnls := strings.Split(prefs.UserPrefs.MutedChannels, ",") newChnls := make([]string, len(chnls)-1) for i, chn := range chnls { if chn == channelID { return nil, nil // noop } newChnls[i] = chn } values := url.Values{"token": {api.token}, "muted_channels": {strings.Join(newChnls, ",")}, "reason": {"update-muted-channels"}} response := UserPrefsCarrier{} err = api.postMethod(context.Background(), "users.prefs.set", values, &response) if err != nil { return nil, err } return &response, response.Err() } // UserDetails contains user details coming in the initial response from StartRTM type UserDetails struct { ID string `json:"id"` Name string `json:"name"` Created JSONTime `json:"created"` ManualPresence string `json:"manual_presence"` Prefs UserPrefs `json:"prefs"` } // JSONTime exists so that we can have a String method converting the date type JSONTime int64 // String converts the unix timestamp into a string func (t JSONTime) String() string { tm := t.Time() return fmt.Sprintf("\"%s\"", tm.Format("Mon Jan _2")) } // Time returns a `time.Time` representation of this value. func (t JSONTime) Time() time.Time { return time.Unix(int64(t), 0) } // UnmarshalJSON will unmarshal both string and int JSON values func (t *JSONTime) UnmarshalJSON(buf []byte) error { s := bytes.Trim(buf, `"`) if bytes.EqualFold(s, []byte("null")) { *t = JSONTime(0) return nil } v, err := strconv.Atoi(string(s)) if err != nil { return err } *t = JSONTime(int64(v)) return nil } // Team contains details about a team type Team struct { ID string `json:"id"` Name string `json:"name"` Domain string `json:"domain"` } // Icons XXX: needs further investigation type Icons struct { Image36 string `json:"image_36,omitempty"` Image48 string `json:"image_48,omitempty"` Image72 string `json:"image_72,omitempty"` } // Info contains various details about the authenticated user and team. // It is returned by StartRTM or included in the "ConnectedEvent" RTM event. type Info struct { URL string `json:"url,omitempty"` User *UserDetails `json:"self,omitempty"` Team *Team `json:"team,omitempty"` } type infoResponseFull struct { Info SlackResponse } // GetBotByID is deprecated and returns nil func (info Info) GetBotByID(botID string) *Bot { return nil } // GetUserByID is deprecated and returns nil func (info Info) GetUserByID(userID string) *User { return nil } // GetChannelByID is deprecated and returns nil func (info Info) GetChannelByID(channelID string) *Channel { return nil } // GetGroupByID is deprecated and returns nil func (info Info) GetGroupByID(groupID string) *Group { return nil } // GetIMByID is deprecated and returns nil func (info Info) GetIMByID(imID string) *IM { return nil } slack-0.11.3/info_test.go000066400000000000000000000013701430741033100152070ustar00rootroot00000000000000package slack import ( "testing" ) func TestJSONTime_UnmarshalJSON(t *testing.T) { type args struct { buf []byte } tests := []struct { name string args args wantTr JSONTime wantErr bool }{ { "acceptable int64 timestamp", args{[]byte(`1643435556`)}, JSONTime(1643435556), false, }, { "acceptable string timestamp", args{[]byte(`"1643435556"`)}, JSONTime(1643435556), false, }, { "null", args{[]byte(`null`)}, JSONTime(0), false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var tr JSONTime if err := tr.UnmarshalJSON(tt.args.buf); (err != nil) != tt.wantErr { t.Errorf("JSONTime.UnmarshalJSON() error = %v, wantErr %v", err, tt.wantErr) } }) } } slack-0.11.3/interactions.go000066400000000000000000000166251430741033100157300ustar00rootroot00000000000000package slack import ( "bytes" "encoding/json" ) // InteractionType type of interactions type InteractionType string // ActionType type represents the type of action (attachment, block, etc.) type ActionType string // action is an interface that should be implemented by all callback action types type action interface { actionType() ActionType } // Types of interactions that can be received. const ( InteractionTypeDialogCancellation = InteractionType("dialog_cancellation") InteractionTypeDialogSubmission = InteractionType("dialog_submission") InteractionTypeDialogSuggestion = InteractionType("dialog_suggestion") InteractionTypeInteractionMessage = InteractionType("interactive_message") InteractionTypeMessageAction = InteractionType("message_action") InteractionTypeBlockActions = InteractionType("block_actions") InteractionTypeBlockSuggestion = InteractionType("block_suggestion") InteractionTypeViewSubmission = InteractionType("view_submission") InteractionTypeViewClosed = InteractionType("view_closed") InteractionTypeShortcut = InteractionType("shortcut") InteractionTypeWorkflowStepEdit = InteractionType("workflow_step_edit") ) // InteractionCallback is sent from slack when a user interactions with a button or dialog. type InteractionCallback struct { Type InteractionType `json:"type"` Token string `json:"token"` CallbackID string `json:"callback_id"` ResponseURL string `json:"response_url"` TriggerID string `json:"trigger_id"` ActionTs string `json:"action_ts"` Team Team `json:"team"` Channel Channel `json:"channel"` User User `json:"user"` OriginalMessage Message `json:"original_message"` Message Message `json:"message"` Name string `json:"name"` Value string `json:"value"` MessageTs string `json:"message_ts"` AttachmentID string `json:"attachment_id"` ActionCallback ActionCallbacks `json:"actions"` View View `json:"view"` ActionID string `json:"action_id"` APIAppID string `json:"api_app_id"` BlockID string `json:"block_id"` Container Container `json:"container"` Enterprise Enterprise `json:"enterprise"` WorkflowStep InteractionWorkflowStep `json:"workflow_step"` DialogSubmissionCallback ViewSubmissionCallback ViewClosedCallback // FIXME(kanata2): just workaround for backward-compatibility. // See also https://github.com/slack-go/slack/issues/816 RawState json.RawMessage `json:"state,omitempty"` // BlockActionState stands for the `state` field in block_actions type. // NOTE: InteractionCallback.State has a role for the state of dialog_submission type, // so we cannot use this field for backward-compatibility for now. BlockActionState *BlockActionStates `json:"-"` } type BlockActionStates struct { Values map[string]map[string]BlockAction `json:"values"` } func (ic *InteractionCallback) MarshalJSON() ([]byte, error) { type alias InteractionCallback tmp := alias(*ic) if tmp.Type == InteractionTypeBlockActions { if tmp.BlockActionState == nil { tmp.RawState = []byte(`{}`) } else { state, err := json.Marshal(tmp.BlockActionState.Values) if err != nil { return nil, err } tmp.RawState = []byte(`{"values":` + string(state) + `}`) } } else if ic.Type == InteractionTypeDialogSubmission { tmp.RawState = []byte(tmp.State) } // Use pointer for go1.7 return json.Marshal(&tmp) } func (ic *InteractionCallback) UnmarshalJSON(b []byte) error { type alias InteractionCallback tmp := struct { Type InteractionType `json:"type"` *alias }{ alias: (*alias)(ic), } if err := json.Unmarshal(b, &tmp); err != nil { return err } *ic = InteractionCallback(*tmp.alias) ic.Type = tmp.Type if ic.Type == InteractionTypeBlockActions { if len(ic.RawState) > 0 { err := json.Unmarshal(ic.RawState, &ic.BlockActionState) if err != nil { return err } } } else if ic.Type == InteractionTypeDialogSubmission { ic.State = string(ic.RawState) } return nil } type Container struct { Type string `json:"type"` ViewID string `json:"view_id"` MessageTs string `json:"message_ts"` ThreadTs string `json:"thread_ts,omitempty"` AttachmentID json.Number `json:"attachment_id"` ChannelID string `json:"channel_id"` IsEphemeral bool `json:"is_ephemeral"` IsAppUnfurl bool `json:"is_app_unfurl"` } type Enterprise struct { ID string `json:"id"` Name string `json:"name"` } type InteractionWorkflowStep struct { WorkflowStepEditID string `json:"workflow_step_edit_id,omitempty"` WorkflowID string `json:"workflow_id"` StepID string `json:"step_id"` Inputs *WorkflowStepInputs `json:"inputs,omitempty"` Outputs *[]WorkflowStepOutput `json:"outputs,omitempty"` } // ActionCallback is a convenience struct defined to allow dynamic unmarshalling of // the "actions" value in Slack's JSON response, which varies depending on block type type ActionCallbacks struct { AttachmentActions []*AttachmentAction BlockActions []*BlockAction } // MarshalJSON implements the Marshaller interface in order to combine both // action callback types back into a single array, like how the api responds. // This makes Marshaling and Unmarshaling an InteractionCallback symmetrical func (a ActionCallbacks) MarshalJSON() ([]byte, error) { count := 0 length := len(a.AttachmentActions) + len(a.BlockActions) buffer := bytes.NewBufferString("[") f := func(obj interface{}) error { js, err := json.Marshal(obj) if err != nil { return err } _, err = buffer.Write(js) if err != nil { return err } count++ if count < length { _, err = buffer.WriteString(",") return err } return nil } for _, act := range a.AttachmentActions { err := f(act) if err != nil { return nil, err } } for _, blk := range a.BlockActions { err := f(blk) if err != nil { return nil, err } } buffer.WriteString("]") return buffer.Bytes(), nil } // UnmarshalJSON implements the Marshaller interface in order to delegate // marshalling and allow for proper type assertion when decoding the response func (a *ActionCallbacks) UnmarshalJSON(data []byte) error { var raw []json.RawMessage err := json.Unmarshal(data, &raw) if err != nil { return err } for _, r := range raw { var obj map[string]interface{} err := json.Unmarshal(r, &obj) if err != nil { return err } if _, ok := obj["block_id"].(string); ok { action, err := unmarshalAction(r, &BlockAction{}) if err != nil { return err } a.BlockActions = append(a.BlockActions, action.(*BlockAction)) continue } action, err := unmarshalAction(r, &AttachmentAction{}) if err != nil { return err } a.AttachmentActions = append(a.AttachmentActions, action.(*AttachmentAction)) } return nil } func unmarshalAction(r json.RawMessage, callbackAction action) (action, error) { err := json.Unmarshal(r, callbackAction) if err != nil { return nil, err } return callbackAction, nil } slack-0.11.3/interactions_test.go000066400000000000000000000341401430741033100167570ustar00rootroot00000000000000package slack import ( "encoding/json" "testing" "github.com/stretchr/testify/assert" ) const ( dialogSubmissionCallback = `{ "type": "dialog_submission", "submission": { "name": "Sigourney Dreamweaver", "email": "sigdre@example.com", "phone": "+1 800-555-1212", "meal": "burrito", "comment": "No sour cream please", "team_channel": "C0LFFBKPB", "who_should_sing": "U0MJRG1AL" }, "callback_id": "employee_offsite_1138b", "team": { "id": "T1ABCD2E12", "domain": "coverbands" }, "user": { "id": "W12A3BCDEF", "name": "dreamweaver" }, "channel": { "id": "C1AB2C3DE", "name": "coverthon-1999" }, "action_ts": "936893340.702759", "token": "M1AqUUw3FqayAbqNtsGMch72", "response_url": "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ" }` actionCallback = `{}` viewClosedCallback = `{ "type": "view_closed", "team": { "id": "T1ABCD2E12", "domain": "coverbands" }, "user": { "id": "W12A3BCDEF", "name": "dreamweaver" }, "view": { "type": "modal", "title": { "type": "plain_text", "text": "launch project" }, "blocks": [{ "type": "section", "text": { "text": "*Sally* has requested you set the deadline for the Nano launch project", "type": "mrkdwn" }, "accessory": { "type": "datepicker", "action_id": "datepicker123", "initial_date": "1990-04-28", "placeholder": { "type": "plain_text", "text": "Select a date" } } }], "app_installed_team_id": "T1ABCD2E12" }, "api_app_id": "A123ABC", "is_cleared": false }` viewSubmissionCallback = `{ "type": "view_submission", "team": { "id": "T1ABCD2E12", "domain": "coverbands" }, "user": { "id": "W12A3BCDEF", "name": "dreamweaver" }, "channel": { "id": "C1AB2C3DE", "name": "coverthon-1999" }, "view": { "type": "modal", "title": { "type": "plain_text", "text": "meal choice" }, "blocks": [ { "type": "input", "block_id": "multi-line", "label": { "type": "plain_text", "text": "dietary restrictions" }, "element": { "type": "plain_text_input", "multiline": true, "action_id": "ml-value" } }, { "type": "input", "block_id": "target_channel", "label": { "type": "plain_text", "text": "Select a channel to post the result on" }, "element": { "type": "conversations_select", "action_id": "target_select", "default_to_current_conversation": true, "response_url_enabled": true } } ], "state": { "values": { "multi-line": { "ml-value": { "type": "plain_text_input", "value": "No onions" } }, "target_channel": { "target_select": { "type": "conversations_select", "value": "C1AB2C3DE" } } } }, "app_installed_team_id": "T1ABCD2E12" }, "hash": "156663117.cd33ad1f", "response_urls": [ { "block_id": "target_channel", "action_id": "target_select", "channel_id": "C1AB2C3DE", "response_url": "https:\/\/hooks.slack.com\/app\/ABC12312\/1234567890\/A100B100C100d100" } ] }` ) func assertInteractionCallback(t *testing.T, callback InteractionCallback, encoded string) { var decoded InteractionCallback assert.Nil(t, json.Unmarshal([]byte(encoded), &decoded)) assert.Equal(t, decoded, callback) } func TestDialogCallback(t *testing.T) { expected := InteractionCallback{ Type: InteractionTypeDialogSubmission, Token: "M1AqUUw3FqayAbqNtsGMch72", CallbackID: "employee_offsite_1138b", ResponseURL: "https://hooks.slack.com/app/T012AB0A1/123456789/JpmK0yzoZDeRiqfeduTBYXWQ", ActionTs: "936893340.702759", Team: Team{ID: "T1ABCD2E12", Name: "", Domain: "coverbands"}, Channel: Channel{ GroupConversation: GroupConversation{ Conversation: Conversation{ ID: "C1AB2C3DE", }, Name: "coverthon-1999", }, }, User: User{ ID: "W12A3BCDEF", Name: "dreamweaver", }, DialogSubmissionCallback: DialogSubmissionCallback{ Submission: map[string]string{ "team_channel": "C0LFFBKPB", "who_should_sing": "U0MJRG1AL", "name": "Sigourney Dreamweaver", "email": "sigdre@example.com", "phone": "+1 800-555-1212", "meal": "burrito", "comment": "No sour cream please", }, }, } assertInteractionCallback(t, expected, dialogSubmissionCallback) } func TestActionCallback(t *testing.T) { assertInteractionCallback(t, InteractionCallback{}, actionCallback) } func TestViewClosedck(t *testing.T) { expected := InteractionCallback{ Type: InteractionTypeViewClosed, Team: Team{ID: "T1ABCD2E12", Name: "", Domain: "coverbands"}, User: User{ ID: "W12A3BCDEF", Name: "dreamweaver", }, View: View{ Type: VTModal, Title: NewTextBlockObject("plain_text", "launch project", false, false), Blocks: Blocks{ BlockSet: []Block{ NewSectionBlock( NewTextBlockObject("mrkdwn", "*Sally* has requested you set the deadline for the Nano launch project", false, false), nil, NewAccessory(&DatePickerBlockElement{ Type: METDatepicker, ActionID: "datepicker123", InitialDate: "1990-04-28", Placeholder: NewTextBlockObject("plain_text", "Select a date", false, false), }), ), }, }, AppInstalledTeamID: "T1ABCD2E12", }, APIAppID: "A123ABC", } assertInteractionCallback(t, expected, viewClosedCallback) } func TestViewSubmissionCallback(t *testing.T) { expected := InteractionCallback{ Type: InteractionTypeViewSubmission, Team: Team{ID: "T1ABCD2E12", Name: "", Domain: "coverbands"}, Channel: Channel{ GroupConversation: GroupConversation{ Conversation: Conversation{ ID: "C1AB2C3DE", }, Name: "coverthon-1999", }, }, User: User{ ID: "W12A3BCDEF", Name: "dreamweaver", }, View: View{ Type: VTModal, Title: NewTextBlockObject("plain_text", "meal choice", false, false), Blocks: Blocks{ BlockSet: []Block{ NewInputBlock( "multi-line", NewTextBlockObject( "plain_text", "dietary restrictions", false, false, ), nil, &PlainTextInputBlockElement{ Type: "plain_text_input", ActionID: "ml-value", Multiline: true, }, ), NewInputBlock( "target_channel", NewTextBlockObject( "plain_text", "Select a channel to post the result on", false, false, ), nil, &SelectBlockElement{ Type: "conversations_select", ActionID: "target_select", DefaultToCurrentConversation: true, ResponseURLEnabled: true, }, ), }, }, State: &ViewState{ Values: map[string]map[string]BlockAction{ "multi-line": map[string]BlockAction{ "ml-value": BlockAction{ Type: "plain_text_input", Value: "No onions", }, }, "target_channel": map[string]BlockAction{ "target_select": BlockAction{ Type: "conversations_select", Value: "C1AB2C3DE", }, }, }, }, AppInstalledTeamID: "T1ABCD2E12", }, ViewSubmissionCallback: ViewSubmissionCallback{ Hash: "156663117.cd33ad1f", ResponseURLs: []ViewSubmissionCallbackResponseURL{ { BlockID: "target_channel", ActionID: "target_select", ChannelID: "C1AB2C3DE", ResponseURL: "https://hooks.slack.com/app/ABC12312/1234567890/A100B100C100d100", }, }, }, } assertInteractionCallback(t, expected, viewSubmissionCallback) } func TestInteractionCallbackJSONMarshalAndUnmarshal(t *testing.T) { cb := &InteractionCallback{ Type: InteractionTypeBlockActions, Token: "token", CallbackID: "", ResponseURL: "responseURL", TriggerID: "triggerID", ActionTs: "actionTS", Team: Team{ ID: "teamid", Name: "teamname", }, Channel: Channel{ GroupConversation: GroupConversation{ Name: "channelname", Conversation: Conversation{ID: "channelid"}, }, }, User: User{ ID: "userid", Name: "username", Profile: UserProfile{RealName: "userrealname"}, }, OriginalMessage: Message{ Msg: Msg{ Text: "ogmsg text", Timestamp: "ogmsg ts", }, }, Message: Message{ Msg: Msg{ Text: "text", Timestamp: "ts", }, }, Name: "name", Value: "value", MessageTs: "messageTs", AttachmentID: "attachmentID", ActionCallback: ActionCallbacks{ AttachmentActions: []*AttachmentAction{ {Value: "value"}, {Value: "value2"}, }, BlockActions: []*BlockAction{ {ActionID: "id123"}, {ActionID: "id456"}, }, }, View: View{ Type: VTModal, Title: NewTextBlockObject("plain_text", "title", false, false), Blocks: Blocks{ BlockSet: []Block{NewDividerBlock()}, }, }, DialogSubmissionCallback: DialogSubmissionCallback{State: ""}, RawState: json.RawMessage(`{}`), } cbJSONBytes, err := json.Marshal(cb) assert.NoError(t, err) jsonCB := new(InteractionCallback) err = json.Unmarshal(cbJSONBytes, jsonCB) assert.NoError(t, err) assert.Equal(t, cb.Type, jsonCB.Type) assert.Equal(t, cb.Token, jsonCB.Token) assert.Equal(t, cb.CallbackID, jsonCB.CallbackID) assert.Equal(t, cb.ResponseURL, jsonCB.ResponseURL) assert.Equal(t, cb.TriggerID, jsonCB.TriggerID) assert.Equal(t, cb.ActionTs, jsonCB.ActionTs) assert.Equal(t, cb.Team.ID, jsonCB.Team.ID) assert.Equal(t, cb.Team.Name, jsonCB.Team.Name) assert.Equal(t, cb.Channel.ID, jsonCB.Channel.ID) assert.Equal(t, cb.Channel.Name, jsonCB.Channel.Name) assert.Equal(t, cb.Channel.Created, jsonCB.Channel.Created) assert.Equal(t, cb.User.ID, jsonCB.User.ID) assert.Equal(t, cb.User.Name, jsonCB.User.Name) assert.Equal(t, cb.User.Profile.RealName, jsonCB.User.Profile.RealName) assert.Equal(t, cb.OriginalMessage.Text, jsonCB.OriginalMessage.Text) assert.Equal(t, cb.OriginalMessage.Timestamp, jsonCB.OriginalMessage.Timestamp) assert.Equal(t, cb.Message.Text, jsonCB.Message.Text) assert.Equal(t, cb.Message.Timestamp, jsonCB.Message.Timestamp) assert.Equal(t, cb.Name, jsonCB.Name) assert.Equal(t, cb.Value, jsonCB.Value) assert.Equal(t, cb.MessageTs, jsonCB.MessageTs) assert.Equal(t, cb.AttachmentID, jsonCB.AttachmentID) assert.Equal(t, len(cb.ActionCallback.AttachmentActions), len(jsonCB.ActionCallback.AttachmentActions)) assert.Equal(t, len(cb.ActionCallback.BlockActions), len(jsonCB.ActionCallback.BlockActions)) assert.Equal(t, cb.View.Type, jsonCB.View.Type) assert.Equal(t, cb.View.Title, jsonCB.View.Title) assert.Equal(t, cb.View.Blocks, jsonCB.View.Blocks) assert.Equal(t, cb.DialogSubmissionCallback.State, jsonCB.DialogSubmissionCallback.State) } func TestInteractionCallback_InteractionTypeBlockActions_Unmarshal(t *testing.T) { raw := []byte(`{ "type": "block_actions", "actions": [ { "type": "multi_conversations_select", "action_id": "multi_convos", "block_id": "test123", "selected_conversations": ["G12345"] } ], "container": { "type": "view", "view_id": "V12345" }, "state": { "values": { "section_block_id": { "multi_convos": { "type": "multi_conversations_select", "selected_conversations": ["G12345"] } }, "other_block_id": { "other_action_id": { "type": "plain_text_input", "value": "test123" } } } } }`) var cb InteractionCallback assert.NoError(t, json.Unmarshal(raw, &cb)) assert.Equal(t, cb.State, "") assert.Equal(t, cb.BlockActionState.Values["section_block_id"]["multi_convos"].actionType(), ActionType(MultiOptTypeConversations)) assert.Equal(t, cb.BlockActionState.Values["section_block_id"]["multi_convos"].SelectedConversations, []string{"G12345"}) } func TestInteractionCallback_Container_Marshal_And_Unmarshal(t *testing.T) { // Contrived - you generally won't see all of the fields set in a single message raw := []byte( ` { "container": { "type": "message", "view_id": "viewID", "message_ts": "messageTS", "attachment_id": "123", "channel_id": "channelID", "is_ephemeral": false, "is_app_unfurl": false } } `) expected := &InteractionCallback{ Container: Container{ Type: "message", ViewID: "viewID", MessageTs: "messageTS", AttachmentID: "123", ChannelID: "channelID", IsEphemeral: false, IsAppUnfurl: false, }, RawState: json.RawMessage(`{}`), } actual := new(InteractionCallback) err := json.Unmarshal(raw, actual) assert.NoError(t, err) assert.Equal(t, expected.Container, actual.Container) expectedJSON := []byte(`{"type":"message","view_id":"viewID","message_ts":"messageTS","attachment_id":123,"channel_id":"channelID","is_ephemeral":false,"is_app_unfurl":false}`) actualJSON, err := json.Marshal(actual.Container) assert.NoError(t, err) assert.Equal(t, expectedJSON, actualJSON) } func TestInteractionCallback_In_Thread_Container_Marshal_And_Unmarshal(t *testing.T) { // Contrived - you generally won't see all of the fields set in a single message raw := []byte( ` { "container": { "type": "message", "view_id": "viewID", "message_ts": "messageTS", "thread_ts": "threadTS", "attachment_id": "123", "channel_id": "channelID", "is_ephemeral": false, "is_app_unfurl": false } } `) expected := &InteractionCallback{ Container: Container{ Type: "message", ViewID: "viewID", MessageTs: "messageTS", ThreadTs: "threadTS", AttachmentID: "123", ChannelID: "channelID", IsEphemeral: false, IsAppUnfurl: false, }, RawState: json.RawMessage(`{}`), } actual := new(InteractionCallback) err := json.Unmarshal(raw, actual) assert.NoError(t, err) assert.Equal(t, expected.Container, actual.Container) expectedJSON := []byte(`{"type":"message","view_id":"viewID","message_ts":"messageTS","thread_ts":"threadTS","attachment_id":123,"channel_id":"channelID","is_ephemeral":false,"is_app_unfurl":false}`) actualJSON, err := json.Marshal(actual.Container) assert.NoError(t, err) assert.Equal(t, expectedJSON, actualJSON) } slack-0.11.3/internal/000077500000000000000000000000001430741033100145015ustar00rootroot00000000000000slack-0.11.3/internal/backoff/000077500000000000000000000000001430741033100160745ustar00rootroot00000000000000slack-0.11.3/internal/backoff/backoff.go000066400000000000000000000026111430741033100200160ustar00rootroot00000000000000package backoff import ( "math/rand" "time" ) // This one was ripped from https://github.com/jpillora/backoff/blob/master/backoff.go // Backoff is a time.Duration counter. It starts at Min. After every // call to Duration() it is multiplied by Factor. It is capped at // Max. It returns to Min on every call to Reset(). Used in // conjunction with the time package. type Backoff struct { attempts int // Initial value to scale out Initial time.Duration // Jitter value randomizes an additional delay between 0 and Jitter Jitter time.Duration // Max maximum values of the backoff Max time.Duration } // Returns the current value of the counter and then multiplies it // Factor func (b *Backoff) Duration() (dur time.Duration) { // Zero-values are nonsensical, so we use // them to apply defaults if b.Max == 0 { b.Max = 10 * time.Second } if b.Initial == 0 { b.Initial = 100 * time.Millisecond } // calculate this duration if dur = time.Duration(1 << uint(b.attempts)); dur > 0 { dur = dur * b.Initial } else { dur = b.Max } if b.Jitter > 0 { dur = dur + time.Duration(rand.Intn(int(b.Jitter))) } // bump attempts count b.attempts++ return dur } // Resets the current value of the counter back to Min func (b *Backoff) Reset() { b.attempts = 0 } // Attempts returns the number of attempts that we had done so far func (b *Backoff) Attempts() int { return b.attempts } slack-0.11.3/internal/errorsx/000077500000000000000000000000001430741033100162055ustar00rootroot00000000000000slack-0.11.3/internal/errorsx/errorsx.go000066400000000000000000000005351430741033100202430ustar00rootroot00000000000000package errorsx // String representing an error, useful for declaring string constants as errors. type String string func (t String) Error() string { return string(t) } // Is reports whether String matches with the target error func (t String) Is(target error) bool { if target == nil { return false } return t.Error() == target.Error() } slack-0.11.3/internal/errorsx/errrorsx_test.go000066400000000000000000000007231430741033100214630ustar00rootroot00000000000000package errorsx import ( "errors" "testing" ) func TestIs(t *testing.T) { test := func(receiver String, target error, expected bool) { if result := receiver.Is(target); result != expected { t.Errorf(`expected "%s Is %s" should be %t , got: %t`, receiver, target, expected, result) } } test(String("test error"), errors.New("test error"), true) test(String("test error"), errors.New("different error"), false) test(String("test error"), nil, false) } slack-0.11.3/internal/timex/000077500000000000000000000000001430741033100156275ustar00rootroot00000000000000slack-0.11.3/internal/timex/timex.go000066400000000000000000000003371430741033100173070ustar00rootroot00000000000000package timex import "time" // Max returns the maximum duration func Max(values ...time.Duration) time.Duration { var ( max time.Duration ) for _, v := range values { if v > max { max = v } } return max } slack-0.11.3/item.go000066400000000000000000000043321430741033100141540ustar00rootroot00000000000000package slack const ( TYPE_MESSAGE = "message" TYPE_FILE = "file" TYPE_FILE_COMMENT = "file_comment" TYPE_CHANNEL = "channel" TYPE_IM = "im" TYPE_GROUP = "group" ) // Item is any type of slack message - message, file, or file comment. type Item struct { Type string `json:"type"` Channel string `json:"channel,omitempty"` Message *Message `json:"message,omitempty"` File *File `json:"file,omitempty"` Comment *Comment `json:"comment,omitempty"` Timestamp string `json:"ts,omitempty"` } // NewMessageItem turns a message on a channel into a typed message struct. func NewMessageItem(ch string, m *Message) Item { return Item{Type: TYPE_MESSAGE, Channel: ch, Message: m} } // NewFileItem turns a file into a typed file struct. func NewFileItem(f *File) Item { return Item{Type: TYPE_FILE, File: f} } // NewFileCommentItem turns a file and comment into a typed file_comment struct. func NewFileCommentItem(f *File, c *Comment) Item { return Item{Type: TYPE_FILE_COMMENT, File: f, Comment: c} } // NewChannelItem turns a channel id into a typed channel struct. func NewChannelItem(ch string) Item { return Item{Type: TYPE_CHANNEL, Channel: ch} } // NewIMItem turns a channel id into a typed im struct. func NewIMItem(ch string) Item { return Item{Type: TYPE_IM, Channel: ch} } // NewGroupItem turns a channel id into a typed group struct. func NewGroupItem(ch string) Item { return Item{Type: TYPE_GROUP, Channel: ch} } // ItemRef is a reference to a message of any type. One of FileID, // CommentId, or the combination of ChannelId and Timestamp must be // specified. type ItemRef struct { Channel string `json:"channel"` Timestamp string `json:"timestamp"` File string `json:"file"` Comment string `json:"file_comment"` } // NewRefToMessage initializes a reference to to a message. func NewRefToMessage(channel, timestamp string) ItemRef { return ItemRef{Channel: channel, Timestamp: timestamp} } // NewRefToFile initializes a reference to a file. func NewRefToFile(file string) ItemRef { return ItemRef{File: file} } // NewRefToComment initializes a reference to a file comment. func NewRefToComment(comment string) ItemRef { return ItemRef{Comment: comment} } slack-0.11.3/item_test.go000066400000000000000000000061271430741033100152170ustar00rootroot00000000000000package slack import "testing" func TestNewMessageItem(t *testing.T) { c := "C1" m := &Message{} mi := NewMessageItem(c, m) if mi.Type != TYPE_MESSAGE { t.Errorf("want Type %s, got %s", mi.Type, TYPE_MESSAGE) } if mi.Channel != c { t.Errorf("got Channel %s, want %s", mi.Channel, c) } if mi.Message != m { t.Errorf("got Message %v, want %v", mi.Message, m) } } func TestNewFileItem(t *testing.T) { f := &File{} fi := NewFileItem(f) if fi.Type != TYPE_FILE { t.Errorf("got Type %s, want %s", fi.Type, TYPE_FILE) } if fi.File != f { t.Errorf("got File %v, want %v", fi.File, f) } } func TestNewFileCommentItem(t *testing.T) { f := &File{} c := &Comment{} fci := NewFileCommentItem(f, c) if fci.Type != TYPE_FILE_COMMENT { t.Errorf("got Type %s, want %s", fci.Type, TYPE_FILE_COMMENT) } if fci.File != f { t.Errorf("got File %v, want %v", fci.File, f) } if fci.Comment != c { t.Errorf("got Comment %v, want %v", fci.Comment, c) } } func TestNewChannelItem(t *testing.T) { c := "C1" ci := NewChannelItem(c) if ci.Type != TYPE_CHANNEL { t.Errorf("got Type %s, want %s", ci.Type, TYPE_CHANNEL) } if ci.Channel != "C1" { t.Errorf("got Channel %v, want %v", ci.Channel, "C1") } } func TestNewIMItem(t *testing.T) { c := "D1" ci := NewIMItem(c) if ci.Type != TYPE_IM { t.Errorf("got Type %s, want %s", ci.Type, TYPE_IM) } if ci.Channel != "D1" { t.Errorf("got Channel %v, want %v", ci.Channel, "D1") } } func TestNewGroupItem(t *testing.T) { c := "G1" ci := NewGroupItem(c) if ci.Type != TYPE_GROUP { t.Errorf("got Type %s, want %s", ci.Type, TYPE_GROUP) } if ci.Channel != "G1" { t.Errorf("got Channel %v, want %v", ci.Channel, "G1") } } func TestNewRefToMessage(t *testing.T) { ref := NewRefToMessage("chan", "ts") if got, want := ref.Channel, "chan"; got != want { t.Errorf("Channel got %s, want %s", got, want) } if got, want := ref.Timestamp, "ts"; got != want { t.Errorf("Timestamp got %s, want %s", got, want) } if got, want := ref.File, ""; got != want { t.Errorf("File got %s, want %s", got, want) } if got, want := ref.Comment, ""; got != want { t.Errorf("Comment got %s, want %s", got, want) } } func TestNewRefToFile(t *testing.T) { ref := NewRefToFile("file") if got, want := ref.Channel, ""; got != want { t.Errorf("Channel got %s, want %s", got, want) } if got, want := ref.Timestamp, ""; got != want { t.Errorf("Timestamp got %s, want %s", got, want) } if got, want := ref.File, "file"; got != want { t.Errorf("File got %s, want %s", got, want) } if got, want := ref.Comment, ""; got != want { t.Errorf("Comment got %s, want %s", got, want) } } func TestNewRefToComment(t *testing.T) { ref := NewRefToComment("file_comment") if got, want := ref.Channel, ""; got != want { t.Errorf("Channel got %s, want %s", got, want) } if got, want := ref.Timestamp, ""; got != want { t.Errorf("Timestamp got %s, want %s", got, want) } if got, want := ref.File, ""; got != want { t.Errorf("File got %s, want %s", got, want) } if got, want := ref.Comment, "file_comment"; got != want { t.Errorf("Comment got %s, want %s", got, want) } } slack-0.11.3/logger.go000066400000000000000000000025031430741033100144730ustar00rootroot00000000000000package slack import ( "fmt" ) // logger is a logger interface compatible with both stdlib and some // 3rd party loggers. type logger interface { Output(int, string) error } // ilogger represents the internal logging api we use. type ilogger interface { logger Print(...interface{}) Printf(string, ...interface{}) Println(...interface{}) } type Debug interface { Debug() bool // Debugf print a formatted debug line. Debugf(format string, v ...interface{}) // Debugln print a debug line. Debugln(v ...interface{}) } // internalLog implements the additional methods used by our internal logging. type internalLog struct { logger } // Println replicates the behaviour of the standard logger. func (t internalLog) Println(v ...interface{}) { t.Output(2, fmt.Sprintln(v...)) } // Printf replicates the behaviour of the standard logger. func (t internalLog) Printf(format string, v ...interface{}) { t.Output(2, fmt.Sprintf(format, v...)) } // Print replicates the behaviour of the standard logger. func (t internalLog) Print(v ...interface{}) { t.Output(2, fmt.Sprint(v...)) } type discard struct{} func (t discard) Debug() bool { return false } // Debugf print a formatted debug line. func (t discard) Debugf(format string, v ...interface{}) {} // Debugln print a debug line. func (t discard) Debugln(v ...interface{}) {} slack-0.11.3/logger_test.go000066400000000000000000000013041430741033100155300ustar00rootroot00000000000000package slack import ( "bytes" "log" "testing" "github.com/stretchr/testify/assert" ) func TestLogging(t *testing.T) { buf := bytes.NewBufferString("") logger := internalLog{logger: log.New(buf, "", 0|log.Lshortfile)} logger.Println("test line 123") assert.Equal(t, buf.String(), "logger_test.go:14: test line 123\n") buf.Truncate(0) logger.Print("test line 123") assert.Equal(t, buf.String(), "logger_test.go:17: test line 123\n") buf.Truncate(0) logger.Printf("test line 123\n") assert.Equal(t, buf.String(), "logger_test.go:20: test line 123\n") buf.Truncate(0) logger.Output(1, "test line 123\n") assert.Equal(t, buf.String(), "logger_test.go:23: test line 123\n") buf.Truncate(0) } slack-0.11.3/logo.png000066400000000000000000001463301430741033100143420ustar00rootroot00000000000000‰PNG  IHDRAw˜Þ;szTXtRaw profile type exifxÚi–9Îlÿs½ÎÃr8žóíà-ÿ]£G¤2RY}º»T•’bpw’€Á Xfÿ¿ÿ;æ_ÿú—‹!DS©¹ålù'¶Ø|çÕ>ÿìûÓÙx¾ÿq¯Ÿ¯[^ôüø=—2ï+¾_xÿ=Öp%=¦÷;èõÓ?à Ï]¯ó“ 0ú ¿ôR¼?5næ£õê¿OföϬµõãõþ¼n¼g¬ácž',Ÿ¯·õõ'{'á5Éáy†šý>ûåug¬óãõš_¯»æ¾_è®]¸sÔ¼íßîüzTëÛÇúûáóóý=4îüñÆx¡4ÿ›]ÙöùúûmcûçÇëeòº†žAÌ÷dÛöü©¼,ýe>¦¹Ï7X°ûFó?^}Áêñ?'[& 9êö7‹¶‚o–;å¾÷›Ÿ¯sãqŸÈ•î[~nîÿ¾Aüíó,ÿïoØÈëßæn¿†ŒþÃ…ìØØ_“ø|ï=¿¸…×üdþ‚7ü–ÂðÁ÷ç|wúöc÷_74ï;ºöù_ÿ¼Öªåg²ßˆ+ž_Ë/†]JßMν,Øõ”îŸãçà¯ßÏ~-ƒòÎo|€¯¯¤à¾…÷¶ªtåÏçÓùºPúëçõüóBÏhó9ÂÄù^¨|~ðë åùóׅήçõºÿ|ÝÅ÷…ÂÇüö¯×ãÇëaÕç‰î3·ï76ÏÙþ|ãõºû‡×Ã?](þú…rŸÆBð3—`Y/ þ¹Î×1„pò…¿èy=Æ”vûöúx](.¾à¿½ñ8IH5?íýzH1öדæï¯cýɼÞH?ï`CÖùÉvk'„öš#÷cuÂû és5Ã$süqƒýžìøãñý…7¸Ðë?o°Þúa©ñµÌÉþÃëîçë¯ %ÿ_ˆ_s&ç<åäÖ ¯§¯O“çÔùçR ¼nÏËÞÏ ®)|½žÜ¨¯'º‘íë±ó~œv—¿ u^O¿XvݼQÿú‚Ÿž×ó_¯Xa´0Y‹öÝüûÑx=þþzúnñQ”Ǫgµ¿,5îøÀ…»Vþ~½?ï–\Ü÷Ï»w8ªÌÅ}礶Ʌη/¼†Ö¸‹¿\¨;Þøð2ÊzžÈ•¿y0ëû½½àSéÛÜa…Ç~:óîöëÊü*IÁª˜a†ò,b—~fþsò¨b"äÍ–h!¯{Å·û{¼ŸZBQR}Þ\îB„&N¡7‰Õ•e¬¼‚‹þá•.|Ã&tÏÛ8Ó]§Kýx±ø£§àç]`Å9çy’Âë››$ž{ø‡.¹Ä_cã")ˆŸÆ’¹wŠö Îþ>›žˆ¦Áò÷zCx®Ý§,šŠ;]ÞÞŸÉjø5‰)ï\³ãgÀWÜ—¬/^™~q˜ÿé½²tÕDdoî ïûq^¾‚Û§SBHy€è3ã.€n ¯H• yæÍÅùÖüòÈwÀùÅøÿÍßÍwòÅDÆ7_¬Ö½ö-TÄý&Zþe³á‹°¿ òó ©¼]ôóóµ?Ÿ7_ôÎý‹> {y‚ïêï ¹÷]ß¼ùõ…KmŠ}sY;__¸žåÿ }=Ñ{ ò§ï‡ðù÷1ç[È>i¾oüùÁÇâw¿¾€ ×¼¼)ŸKŸŸ÷_¬Ö}iÔÏ –ˆ_ãœW'^Ça­%èß°ˆGL~µûðµ_Øì—$ý¡ùÏôþ+`~ÓüXj}™À§òÎ_¿ÿõºyûóoÁøû £õË8¿½Þ³?Ÿ.òMaŸ³ê9û],Z̯AÙï–Çç†fÌܯe+±ž1´Êïú…,ãÏÝN`~!¿&*Ô3‘ÇE·\wÇíûûtŸñæ)!뉈á¾XºæçUbQ¿ÜñÜÑ ú0:`6ø÷³ðdÜÖðcÞ»Uî¼õŽ‹]0ýo~™ÿö úuÎ|â\}ÍS³Ì‘ó×Ûœ¦1è'‹bá¯IMoÙò5ÑòðŠu”¹Ó\`·ã¹ÄHîm…»ÐᆰøÂ&WÖ[ícðÜŠgqD ä29À´x¯p|e:®6X‹_<¤'NeÖ¦zÝš¯w?ê“ñÏëÍwVB \X›†>…OãM9¢\‹DôI™ÈZSKô& ¤Ë Ý” ï„a”RK+½†+»–Zk«ÙðÔÔr+­¶Özçž+÷Üùxç…áG0#Ž4ò(£Ž6úÄ|pé4ó,³Î6ûˆ>‰àRV]mõí6¦´ã††í²ën»göW1èÔÓNÿZ5÷rÛŸ¿þ‹Us¯Uów¥ôÁòµjB¼ò¾„œ$ÌA&Z¬˜–ƒöZ3[]Œ^+§5³MD$¦.im–ÓŠ±‚q;ŸŽÓÚ™»xVî^7ÃÔÿ\7ÿ¿¬œÑÒý+÷׺ýºj«ßôLx³{|І#÷D wOxµüð}rݺfîÁÌ’;9ÚI„³+ž!&5 $’!13kCκ ZÓO-Jâc«4Ÿ˜¿3ͬµwõÈiÁ¦EìûÔÛêB>Zû^w/EK}Z4§Ûy¸'3½ïÍÌV¹öåXìÜ5 ~Á±s݇u+vÒ홥—âܸl=ô.«³>|ožK3aÔv…¬q!l% F'.ìÊz——›˜hZ½œéÇUÐ-ß8§ì©¿UžÐ¬yª;©V}6Ä ƒ+Ö!üÏ“¦cà5ãš…•Ÿ]–µ-îPv:¼r`s˜0Ú–“ý-Þ.3œ>fp#6ŸÛ X–¹g¾ÏTcÖ ±tÌSÕø˜ìÑZh°ƒ^¹ ËÇ&=gßÀ“áå0’ë›'ÂXªÇ©±Ç@„Ù#o'¬Ö´ÐøÐ(£ì¼°IwÚÂø¬ŸÛ¤T Înã9? ­c¥‘ª÷qsÕî Ê)®¬ár{w¾Cè;-Gi?Þ˜cæp$tOMxÓìbö>sCVp„Ÿmgû`f7¦æ×Ü8âfÖJÝ匑p³’®áÂ}œ†xê«û² _Çö¶³9¨¡N…îðP ?Ö6WÀ±’Òã0ü Jižµ<”Ç:È·pÐQ-çüfÃÒž±xRÙc ¬ —ªrAêÆBKo+áÍ~\¬+€õôî;ÎÎd[b À›KŠ®eˆ s‚Gˆ5 -V\òŸûXgÍÄxVSxægP¥VWáÁä"±`>-áž@.xÛ˜ÿ1P¶~ø8ódÔ,ö=4ïÀÚrž,‚N?HQ®oû³³QôËÀßZclÔ´ã2D+Æ,&7Qd*ç»-Óµ‰ÕõlŠ Ô%ÞŠ1+O`Í àñI?p '\²áäû LÑÅaÝ…Mbdíe|µÁÐˉ/á«@M—d8ÝnqÂÂ"¨+pTûûïæŸÞøí÷q@àœ¨’6Žs<7f| ““› h'l<@›Ð‘h`εq(殇ª}­ƒÝ‡FØê|n+Èì7ê:>¤K¬¯™¡ò7æöf§åÉu*¡r¤MŒö{rÝé¨,àRðècú¨³Í<ë ¡KuBÎ8<†XÁâÊ(¿g±ÝÃCÔxÖj=o¼}X.T† à`\Qû(€‡G/âæ%à8vd‚“ÏÚ8(†¸Õüˆ8g7Ð A\°ÀfW§ _XºÓÁžŒqâhXO˜08=°…;æñDL72Š?0Œã­±áçu®a®³à•¸Ë˜ÊZ¢lØV„µŠï¦†^öÛ´s ÛåãÂîick•ê4¸ó¸Tl7¢f¬B°‚Hð·ˆÙpœƒ‹XØ@®¬Ùd¼31˜f0[>Ii-&‹öÞB ƒÅ̱ –/3QUˆènFëJ¢Œ)OlE…Aß¡„0ÅáÌÁ`›†f6Á"™?\$Á§ñ© /!´-Ì7éÞ Ñ+"„x;N°×]µ•íNæŽ{vpû`GqÖh_Eá¹LcÆ¥°ý%îØ°Pœp~_8Q”SŒ:àôDy⫉ΧÓèq³ƒ}'lªŠÄeâd’ˆ42…M€„1‚^€5Í 01ÅËÔ¼Ïëb¦ÌÃà«.¹³ ËgÀù¥÷€ËaÅeŒ9×EÑ`b Å탗ŒŠ&¶á²§Ã}wžmŠæsùÌè4pÆÇÂ…@¤.Mvü@)¼‰â˜ÐÔ°wdÓ³[Ük©0DG€a‚²¦ÿ¦Dîï€󊄈«W†8”ê`Öá^Œý.ÁQf—g>•p‡cŒ uh¼§K&¸†„Ð-[ˆ„ bJà‹Š)@†ò*Rx ˆÇ3‹z@Ö[di‚¦$¸B–Ùe¥Ïi|qB´HäÃbà‘<"1âá8ì.b*   ƒˆ¤h¥y€ÙÜl† ó¶}À #.-“fqô%°å‘—MH-,…Ô1¦2¥#€ÚŒaà @—{b{Ô>ÆÄ½ø‹î±*°µÉ”1Ax,üžq±Ñ2EE˜a@÷æí**,æa;'kB#³{ŒÎ»m¡6@ÈN25 ÂÞLüÃTûÝ¢„^_÷DÔºÃ@œÒ ËÅ5µ2!– Á@`¤èv‹C¦Á]W¾Ì©ÄÆ¥—ñGÈO4W^1D5«Eœ]˜a&ðÖf ÎBàIàŸ²gÕ`‘ôÕJ˜Jû+ÔgãlÔ!Þû`´7‚‚„µVjå §a†ø„ aÀ˜£/ÐhçQŠ~³814` ¶ƒºˆd>–”ýÙCK§KÀ¶‰Ñ„ ø$c'|—¡h$ÁM©‡LŒ!¢Š|Aô²ø¢±X Œéê³éBã‚’<µá“ÅÿØŽ„sôEíkÓ`‰›°‹–§gÒ°~3ZíQ²h€J^Þ«ÝX€†¢·¹"ãûÀ’CÓ”¿$ðÕ à7à(Òð–=Êek⪎#-vÃ_aL8ìÅóÂØ@Œsš•;m%ßÇp å¸_¼/¿ÞÞUðáíͳðP¬~ÈŸ‰·DÌ‚w6*Ålæv:"ˆ¨_Lyi/ˆèY)YL "‚ñ5øXd*°Ê…XŒrŠ%•—±,;sÑ`ÒAjL4@ÀàE$àÂ›Ò öç¡£Ø'. ˜8–„že‰¿›ì)@µ˜2#/É~¢k8ú“üÊð¤ÀrϞಯ}ˆo[èF=y“ÜÑ›Uûtøq€8—€CÖ‘ÁÍ`8"•H ¾µ°´¶"ïa…"d[™FSŽš¹<+ã¢,ÂÈñ/4rQ (”sо2n€'! 3ŽtÙ ­¶$¸HQ f¸81@d÷LBŒ¨É€Ù`» üѾÖÁ¢wƒí ^ªí Ê×5<ÚFé E XÓ"ô­z®ÖIL/ÂQ¼iª‡×0ŸLÚÅ%‰Ïó² ed‰ò‰–€3lº‡¨bÌVÖž1ŒÉ·Žö'x àÖ$hE–”(š rý“‰Â³5‘³±‡ëÖˉlÕÀ¥`Z‡h¯l–9˜xŒ²bô0ø/üóC\À øwy"7Ôeúß‘±+QX• IF55IƸa•aÙ’0àî$°Ä¾°}Ô¨1ñY@$%¡”¥‰s#¤ú°,ºõ«2ñ°…Rç£'”­P°ª3Ìùø†ñ #ÊRY×*íAü€“À ÿYæZˆ„&¤ýÑ{ÆŠ2”°“»ÌíÊ?ÌLbe:!rí³ä®ÃÉû º `ñwÓfgˆPxo…b²\ÀV!4ð‰(u#Ò&¢ÆÐ†‚ÞF©}? ~¬9FËô‚éø!.¡œ3‹(Ì  ÔÊa6±Ò¥.2G9© R‘È<ŒR’En…vÇZº ƒp¢ `¼/Œ¹Sý(r7"b[§ü'>³Ñýp+Tv‹0Å¢Üv(-çç†` V¶¶x%s„r‘ÂïùÐY¹ –Ý÷ „F©@x‰áA¥}eoœ¢Œ{Ea„—<• ¾»N ‡Ånuf"8P‹Î#ÜÁ¨@]Ìà )àíhP8 +a6vò^ÍÚ EL j%¥@ãZ±¬¦ ˜ Î*Ð#Ù€?*¡1«xs¥€ÕéÐgÒE°E‰nê>¡Óƒ2ˆãq ¼èFš,tˆ 6qiy$ŽÁX¸•Ÿà+x=”ó“Wž¿… ówÌø )?Î7kØ¸Ž’ŠI쩸ÇòŽÛ@!óUUÎÂNX^‚=Ò %2ÞD0¾c Dj‚",0¨W‚†lgö9^H ˜d’èâ.'¡,XZe¨qü©È³ ·)] ŸÄ@a, eAh2ŽW”KG“öÅ o’ ~œÀ©ª)Þ®Frèõµ(AuÊ÷hÀWP<Õ§öJ”5©ðHÅMt$àBo5¨8&žÍRÅËe cKÉ i4¥‰ÕãÂÀF=)ÝÑ¥¨ìrTظ˜ TYø#Dš" ª¸»í >çì4²7‘kEKq4V "¬w»€Kù‡†Áefñ¬ˆ–¨²jÏi5¢¥à¥Ÿ-²!&üÕa}Á†óUbV[ås5ˆ @Ÿ@´ÃÂrð6o5£ !¤îb8 8[#$hÎàÔ|Îá•©sŠiv|Í2âòT8j{â uÃ¥áì xòHÌ50 ÛÒ^ÏPÒ›„«bóðœTzRе‡[_JCÐZ¡‰QyÔ"ÓÛ<ƒªSU)V…Ü®¤$|N¹šŒeq‡ƒÛâ®ÛVÏ€@ÏòžYÄ ˆN J?ø‰=M%S§‚kÏ™L –‡ %4µ›ö¡êHJ¥Tåó°èXª‚¢=Ÿ«q{¸£š#b”˜³ü?åü“>Ê¡^e"_Cs£ÔµiËÒÉA”ŽÕ6íŒr"ü1Y  æÉ~ëáC—äÄ#XW• éšÊksÖ‰®Ä6³j©eÅ3È«XB°Ž€¨hR¶”á«Í²œ`\!>0à6â2¨öX 1D.²’v²\uR~ðfI¡‰"‡£âxè=HOoO1šCúEtÂ^Í:Ž*1†”*A…I6\ÙiåY¬³4úŽíGŽØ<,O5Lø™€»[bô•E"Å Œwˆa"^ǹv(qq‰¼‚¦2‘%×*„‡wöî=U\!N¤ØÃ…U´ó(̹¸¹²8Ûµƒæ~IuvX ©-§’=R4Kñ«Vªe^aØœVÒÇRx•à˜1K {ÖÜy|)Ǻդ'oæï÷ôT@è$,ud¶Ò²;¬L×Åвò¯|f[yƒ…‹@9,e+‡ŒHDLï},/xˆ¼š*ˆqö TÁó;×qÌó‘5HÞ¯¨Ï­±$–! C·§Õ$< .™QÎ2” qͼߡÓxŠÒšRÆX¸ ¼i¡ ¬ù$È3”‚Tð3\À°àGŒ™Ðwù!¢Èn‚Î2ŠI*A,®º½Õ†N‡·¡a`Tx»RÈ][Ï©î¸(NsÚÄ´ú *‰K*áQˆýH;Ö$'Î_³¯7õ„ß`E¡Ä.¼¢—± Ê—4ÒFÀóŒ22• .¸¥tcæƒõÌ"œº&ɺ À ô« A;4JnxQúƒáÒ¸`¹3g9i/“a"í±êÝ< FTR‘`ËJáæØyòÇÉ3ÊMª¶ ½Ä†ñI¸"´ˆ­1+s]wÄ¥˜)Ë0B^^¸ž#äãkàãnD¦àñãͪٺ<ÖÑ÷…Š âF¼²^M2Ø.ä£ìv ŽPì6ÆÂƒúGpÇy^Z¯ì´,³ÄÈÈ‹öònÀð h¾¦†#5hy~27 ¯«ýác΄¨½—•T,Wó…Ð̘Wü´´¥7•bFqu ŸÇB !’0—ÁyÝT¬c~EœªEí6a ê`ѵC¦Œ¡gÙ=ß‚S+µ7‹G›bc¨7°Sîö&Ú ðø €’—·»œvÜ%¬Àn¡­\$r]c¥6Fáï\ ¸Õ hRg凨Ь]Åø·Ê)¦d}#œcͳyžŸ™Ï"àU| !f2ÌJ­Î-ÿlE œ–ŒRráÜMâ£ò¬h÷Ž)iýaùªu2xLL»¨Àû›ÒL:ŽO¾7Ôíyv«í¼>U-ç¥XIw¹íD­€Ÿ¡ôˆJ ¡ÅdÇYŠ$Z1 }0Eê"ô‹(™ËªvÞ˜ûC섉ˆ LÚ@‘C:c!Ôt“; ʺT¶±ÿy£@a¼h³ M×ä>uk³=–1£ŠÝTxœ²k¼™ÓƤ°¸™…÷ lÔ²—€PTÿ s”(‚õ Ž0’f”]Ë¥ff¾Æ²Á|PãèùûD@a|*`3FÒ*JGd:µ&|Ë?IË_Ñ굓qïÉOäxKø’zìg˜Ùÿƒ ã/æ_û$@±#".” Â …'„¶€¯Ä$Ó‚¨,Dë{·Z˜2%©"þ£2!(m5‹ÉÁc”K}Âi6`¿ h×@¬LD2ìÁO¤'4aÜeMåÅ™…ÆT9•,7ã@)" ›€5€"ŠÍúLÖÂc¢ÜÆV†gú Éòðµ!F… „DßÞÂ]MŸ#Ð@ûDPþÒ™Ð^¥Y1D”hŽÀ2„¤I1!„]Ã%§â"j„dS¾!O„—°çQDúUsÀÒ}¹>!7¨t &"[¦J9§p¶xH¢ÌQ &QÐ2Ê#-Û˜h¤§M d#ýÑ,”æ’Ç­Åh r¼’ÒÕ7U€‰]Šª±%öBÜ¥õRž•e3-.D”æá!£MM"¶¦ ¼‰£DWXµöd“™f@WU|²=©¦* MJÕ”ví<ÏÏÆÎÐ{bÄG¸0Ä7¯ˆŠN>ÂW›ZWµËŽáKæ)eg J¥²ˆ°Œ“ÇÍ‚ZkC¨©_/–2ªÛjüuô®‚Ñ•ÌC¬þ\+æÙ¼ÅÀ´¯Ý]:ïl>4Ð|Cùûr«Vˆá?‘Û¬*,QJ—ªÜ´ìTP–”òDZq[|žybp¬ q×£ ç5«–_¤õB ’³Ü’Z˜PVPt›¹Îrs~îŽ[{Á¥*´9>¸eþ@âtÃű·£&ÕûÀ  ¡ÄoâÈEâÂÌÒ¸jCf. ¢Œ0s¬ZëE0^Uê2´3-¹‰y â€,Õ ±´¬š°‘ÅÀÎ=Ñé` DO“Ä.R-Kºí¿ÌŸ±`›Ý¼3Çî#=Ö}!¤*IÀòLùyKM,oúT–® Èã¶Êäðµ?˜Ø¥Œâ(ëVSÁ¾ŠmkFÌ©.†:™ŠÐ%ø²»Áï-ó°#¡.ˆÏ?Òo]i"Ø0*áG$ò]ÿ&•ð·àƒYªWá>¾…èUÄ:X~>ÈôŠâ ®ƒ•©bØ ?UKÀT=R&*»®*î~“![Õ@x¾54V+ =b56I´Ï ²Mø2Ìî™à+).±÷P&¡ûbpk÷Ül‡PôJfu{Û=1UqS·,‚ê!—ÌUÏ|”cUnR妈È+B3SS‡`¨bÜ //à®ZÎhë¸ÆÁ¤–'SáÁ²ð‹# ±rÈçÚªlÓ gJb3? ¦‡ž;Ì?œ­«’Øëù ÜRU']¹Àeµ]RoN$T.'Y¥M(Ú”pÕÌ À«arÇ1Œ|S$y"YJæa&¡"c©°ó€‚FåbºN‰¼›ÉYVÕŠG£•Õ<†¡!™ý<.QL:(s77.ê3oÌØ^¼¨¼ŠìXó#©Ô%Žàt° ݵhnÆ”%IöM]©ìG‹<‚iªEdjZ!4ÎÒa [\£Aqû¿—) 8!€e‚+_‘sxâS~6¢mžÔ®Šn  Ë+n1ÕeÎà0ê$è©®=@±Ì)S”ã‚áÃ¥€À•¤:°n•>%h¢XÑpÚ3mJìû£ <8£JT­dO HÈ `•Ì™wújÖFì-=g²Õ+‡‹¨–Å BÓ¾ p1ëÄÚˆw°Ú2Ö1€D¥ê¡!’i_ea@Ô³T}„ CP§VÔ‰ÇnVÙ™ë {šAr%%ݼZÍ*þeu¥"@â ’ –ªþkbWgI"*“gn“'/îî©z‰Í5¥­É.5Áày¸‘ìèµbS˜*"Ö嶦¬nhcEW8o`HéŠê÷Î]Á´¿N¿’€SNí™ô™àÃ9XiëK„¶…"aÙa^ÒÉ <7(iš¦S¿#ø¡Í]â( ©ö^I&èš mF<ÈŠÕ^„¨J¥m»«5Ä#&áÒˆ"íÃ܉àÂcª8²mºm™§ÁDoÈ:*Ñó¢pn-ϬÊ­ŠŠ E‰w{Ð~¾T´Ï°ïoøQ¹1(ñPz¢°ZSuã¹|Ä‹ ()áÓ°#m]Â3_u6ÐVlŠzõÏa€Êz¢JVX£twå„ÚªÅ0Ô·€u¹¨Êu–YÉÇ#ïQ^ž¹ˆw3†…R¹”Q+Ë1׸"_eÅ,ºAµõ?jª"á=`ûQÑDÇŠˆ7©ÈJ˜ŸC‰(ô§ ÃàuT›Cìýôà[ÖNÃ6d¨¢äPR¡yˆV[·òL¿ƒ#âdËjZ‹rm•ÁJÔ«|¶$içÝ‘I‡áâéÄ2$–ö¦cá àö J±îƒ³ÜÁOpCBz©H›‚O Ÿ Dkâ„!8Mºû¹ª9ÀEî”%Õ ¸¿i1ñŠÈG5VÄf·´‹pkó‚aK¬€6ë(¸«ù± "(Z©.%©î4ªŒÔ+¯³UéV.¡Q=¢ú„¡Í¨z%ƒ• –wƒ!{û–R/Km'Å•él4_Wª¢õ¦L´N`>ÎÝNø[€! ¨E¡^Làéƒ$UÖ•óÚ,ó÷0à®}TÐÜ#ßT©ºU¥ÖYŽÚÆ¢ r5E|l³¨‚Øåö""i«7Ö™#ûÊ0Î/\w#<,Ì`²HUOx¼Ñ ªÝÇxP]2%± ~ΪÂv¢ F§¢¤5> vœF”Ài}r=¢¼Š²#a±ÎNÛJ>Á¿&ÅR´šÍÚnP}ÃgŠ’ªX½+è¯P ,(ûBG¶ò†%FÕ /¹FUAVRÊ(©j‚ ¼W€çÚªnxL!Ö#ç M¬%x#´-0e÷PÕæpx°öŽuÀ*ŒT7 Ób@¹·‡ÈT8‹ý­ÓÅ‘šèeæ{PCÅÛ[A’÷±±€qR"Q-BÌT ‚e£¿bØu» €iX°›Šñ¡$ …ÐEõ¥Œæïq(asü‚Ü4¨ö&qcc—êL™¬ýSô  üg¼]$±I.9•¸—›¼‡+3ú˜:ナòrT“ƒ9#(eá³[½FS ƒy¨\çÙ×vfêÂb’Ti›»J¿¸,s‹ÒÁ%°D•,Ú½ßûöÆížÅÞÚ£ÃQ4X˜ú T¨Qp9ãTò¬þ”l²¸øH¤Á(Ë!o%T(zºª¹˜NL¼¨a°•*5N)´ª¬ÔŠ<)Òú!Äö!3˜ŽS=ïê!ŠOâ¥9lÊ“C•f1FÕùÆ*Æ•=¤b¨ }v声¯î)â­v—!îq‰®9¢ÌÙx¬ö#›YÛùGZ6¡žy[ý >*lfP¨Eœinï“JŠ¢Š`§òÜ ø²MÉOÕ±eí¼ªî l€qª5ƒ§‰¤¥xÐ!`߸1 B+C1!†LôQÙŽR<Ö?=•Tðm+KPÏ&¬J njúø»³L°­S™Qæ³8X8qBN„=ÐEÔWžö8g¯Mís¯½Új…Ú©Ê©zÇ@œwÔö³áqŠ®½¨\ž<Ì¿ÞÏq˜?) •{Š˜Jv`ŒÚ'÷U­rù§œ©"V—rÐÚ‰]@(d@sd`¥Æµ™Aã`v¨åH¼†ßi7±—'ïÅò °¯Ô…kHD.ôä€~ŒÂ>ã&ÈOâ=–±öj¨Mqâ‹txÝxQócï·¯Z T’ðˆÅÐh¨ûØu°Ñ (óx¶?ÈûhÄJ!Q¢leàd8wüàÕŪ쪔ÿyTŒÖ-Äå0Ÿë1¶º9žá&µœ?†èDT“ÄÕ]סʾƒ^'Ü73ÁèG@\ÀØêFWßd–Ì®¨~¢ì5Š7*Y¿om³Ç¡C2æm32ڄдö *R¾†:ø(fºÅ2 ßœ>*h ª¾TÐp¸$qž cT„ ó ÅÊûV°b¾WÅϻ֣*9àá˜BT=ÅQ™UL¤oü@e/|!ªWTŠN\±Be@’¦© Â?jUA.MívÙ âµ çª6D½ª‹Q5cD>Æ›‚Ľ–ª·W`['JtÞ‚?ß A^••ó¨E÷V¥1k=åN¶À²CG°Ä ·ÜOæ§•Å«‹ NMlð4¹£¸;VCO’ `°ob2³SÇ%oo•3:õ¦Ô÷¤åùwÌÂÝ2~JÌ«úÔ¼º] kÝ8•æ ‘z5ð¨Y.Ýý‡­6!à£.ì¨?ªvª¹è×1ÛZ潩}7õB¨,G{í=c.€aE¡ÂC뺼/‹E«ê¦œ»N 5=‘=ƒoç±nÕØ}!¹[eÄVûÃÅPp»ÛŠ"qÚ* N5ÚXxõ^{¶[;µøS1Ñ̈·-¥ð‰ÿ„v;袗B˜§‰5ÝdÞƒû{5ýÙ+K»ª¤W OR©#ðaï®>XNV–Wpj4Ž $÷Yn-ðÑÁ)ngMWÿáÂjäÔÂe ûE•• SÏáQ_·È>\ŸYiêƒyRnêש˜pó“°)b‘ï¨;4þÛe¹Y™WµdàÆM-ÉÑ/™˜‡j…Í0A_|¡4©@‚ÅãÙÐìAhÒgEó">L,íÖ–2lYUjèNîÑ6ÚCe¹Ã(—:òe;ð tŠ*ÐÔÍ©JצºFé`¶¡ÃÁDµ4TðÒæžRDølZén*ƒbÞ—%~45dPð"k”8¡®}í£1¦@‡üø»Œ²³ºÍí¼dÐ ‹ûàØÌp=jÞXª´^ŒË¹);æâ馓 ]ÏyljIŸCéïY.î]µ§¢kÉlp‹¢[¼¹õFhP sR‘´Ê¥c8³aíPiL¦zõl8¤  ‘,~<ÔD"æ©}· {%â*ÆY¥ nj:†gXÁÌçíT+„кCÓN,qf«JjŽ Ê]«WF™± ž  ar{‚ÓÑhqµ)ÓQ Œ69æRîSè¿,õrÆPŒ\Ê•yP¸u‡ŠÖÖ++^t„’)[;lY›Ãw_—Xû§Öq•”ÊMªWVÍ 'OÍ‘„£v†qEÄãHÔÛt(Ä®œÝ’ê·•H¥~{Ñ ^ïª$ÛE§ô¡vô´ù’õDð™jéðÎÉ¢ÕÖ|Ó(Õ%•õ5qÀßYÇ#á(ëÚê_Fõ:gŒ[Š„M»r i È”º!ÔËÑrªÌmQ*µ)“TÁãt0¶¥ZUü Þ¢W[¸GÂÞ­º_Eðî*³¨—F´$`到1¥ÞMïuRB­áö|BÓ}ú¡Ó†zž°ªue«’n)Ïås:œá@é4£:¸xnÅ4„½Zm8ˆHÏä‚rŒ»¨Úƒ±)Ž{KJ@ÌÍ`[•üÔ§‘êüA—š[^Yu—ª«Á'¢Õ´1ðd·k®L¯Aš½*ùÓ\@«z[ µ„±[†¶ŠÎd‚¡f5kUÔÁAµÁøußµ ¨SgðÈ¥m­7¥Ø?è‚¡ƒ0´ÃL\kרš*“òT2¹écfÈ&•éÈŨã!T ÐMLÕ§ùÂ]#0‰ñ ¸Ö­ !…êÆÒþð„,ª`Ý=¾¡¤*äµwüçÝ×[Q©W¤+ÿéÌG¨ËØG÷ê óÐ bŠ îªf¾Êo‹°Þ‘ q©]=ž­«nÛ`¼bº¨œ!Þ¢iÕ–aÐêNÄ‚ÉÁ©³ßô Úñ¹° öª*¯™_%1Q5ŽP²¤ó´­v"€¼ª4_Ô¦?SØ1|<ƒ JšNÏ ’‚Þú6ÚþS¹ ¥QÄ{÷[À…¢¾|.H¦DQB;‡XuU牨äÞi£“/™WÒ¹~“ Æë c[Ôߢò;Õ興6ÀˆŽŸ&Xó®ÈxO¸!07«Íh‚uÖ‡»îÈ+Ú]:Qu”IÏÔ‰1§BófFÜ«]_%À©¼Q–¾ù¡RU4…¥˜Ò(ª™•¨F <ãFŤ®mlmVjÿAyJg£¯ ø"ý¢æ&ø§š¹Éj>ètj#^êµ"¤R•UÖZTÞ}t@[5ß34¥%†ºA@“UÀd1wüªåfÕeýtˆAƒ ‰6%l!{9«ƒeµ1ŠÁMûR+'„#«JW'™ÜSrÄŽg­ãJä¤]Ý`©êó r¦¢r\/.N¨›fê_ÒALXçhèð¿{ØÌRE…’„àÂU‰PT}-EœMÝ_¥¡<æ®Ff FâíâùyÊ0}éõ²"«¿=öQ±DÙêžÇUpSµ¬} ¤cC â+v/‚À>š‡šH´zDQùŒ”€S£×zhw‹œÍ*ÑQ ÑñVMbÚßß*¡/­Â-& S/TŠ ¬ ÀS}TÃ.§J¹¥#àZ0þ1à«ÎÊêPV2Ý›ÏÖÖñ.©ªfE,ÑqæšOˆyÞõèÒ•æä‰=rÝ¢*Ôª(WÆmµ#Ã'ÐjO**Ü÷ï ºNôø“AÿŽeƒ.Ú4òíØMûã3,ø@ÑñI:#{§þÇ"]À˜o1ж«“~!_®u—Âk&p 5C•cÚˆ@²¨KõƒY½t.鼓²˜Ä?Šò÷UíõÞ4‰[U,;•rO¥+Pö’¼6œâ–½ZñȨl%2Ei8…¼‡ 3ÑŸ‰7M 0*y"Â{Üwß­ÉçŒßÖ”…ö4T± ˜2›;vx’‹Yg8¨ûnš@ê¬÷èŠÇ*ž©`ZÑom’uµ«á2S«xnèD²t( ²oªu³¡ «µ†XeõðÝs¢”¹{5ʪZž:Ø*¨éÚ’ÂjZ‚é.[iVFgšÚ>ÓÕN¬·:œ ©ÅŠoÿ]G‚ Þ%‚x„ŽJ“à‰)3ÙDÔ¢fû Cå‹­©Ã×*ˆAÏtÎi¬8:Ú@(IšãbÝÕÃxÛX×9”Xî „\†£ª ívö{~ê"k+õž¦µ[ÎtÖÎ=l'¥®½Õ—Eh ;­U… ýÖ!b&7Q¢Y0¬öŸ‰À¢`˜÷¹¤¨†¸Ž[-‚Ȱi/iÇ ÝRÓ³[g”˜ì<Ñ®ªÒ‘!X‚£© “¯Nù$l=EÞ;ØE媌䈗C÷ªëh_L2ØÝ´ù¦lÁÝ zZ‘ÆÕÁÛn0=\}û;‚°˜TÌ£pBõbÒª-Ñ1NÛv#Ìfmò ¾Z$®Á2¶îÆ0w¡Fcu$'5<‰Ì>!…*oPéRÐáwp('¤ÈUaµ˜6ú…€uK½ 8»Ê];*mÊÇÄ9Ô`kש©"ÈÈ3qGÇñ…cnW̃GQÇ^Ôvƒ ëvw>;ÐÉë(õ¢yuWww¤§p7¥ÓP+V#=0‹Î›ëªT-:Æ1/ÊÅÖ˜«9ÖNäú†$qIuØé´@ÎK¼Ãìª+@[ª=‡NR2Ár_š¶²z=ê%Ðê*€‰ ı±9õº &pšftR–v öùêLOר±OÕßÒÉ,*gt S$J‡×Ñw8wžMB¨7À²¶?áþŒ¡äÈ?‚*yUžÿŽÑ€VŽ‹¡›ºÅDêXD‚.‹"µ¡¢ù{!0š`©ìî¸P|:êù¯ªë šè@¼Û÷~Œ¶Äe}„ý:x1ÿÉ_Aü…–:5Dm³°:>­öh5T´Û@¸6ŽÅ@Œ|W’b­ÎÑ‹˜ŽÎ‹Òá’ cxll·ëÁ&8Tý7‰»Ï–ž:™3å~+žJ+2~kú³NsêѲFuiR½‰ó©"Ñ…ìlеµšNšVçwR!ŽÎ£ZÚ&®J­uˆ£ŽäÅeE ¸ì‡eŽ0ztê6ÚÜŒ”Wë®m+Î!¨na0] µjÙ3ˆæ‰¢*l>=Ž{”£¼Š:™^ÎÜfhâ¶Fpñ¦6Ô“·Óák±‰[~ªÏMíý=©jAÇx™“f¢9Ò §$ôˆH %BÐ&ªè *ãr:v¼ «¡’R"HЭT3fWýmÅ›F‚\G–mWTdà·6èÕà‘Dó: ¥“ïDGU•§v,6pÓúa¼Úѹ×u©D/«ì  P½MÇ+©½i³c0X%lQ'ƒ`ʪõQÏ¢û„Ñ7ŠV‚:J¦é„ ‹3µ×;YqÀ_gŠ…L$ÒI5iê¤Ãª¬¶¦í£:>5Ø«4µÆ¥ê.ÆÐÔx•É£Ú¦IêVÀó‘ÙŒ•¸”ujUP%3³T–>îIþj^Fv¶LšGga9­D8F0ðµ/()?c ^yH±-Í«*! •:ªUâã=1QÅÈÌfÝ`ê6à‰š›ÑîˆþRÝ0’_E *P¾ÛÑñÙîi®[-ê0jŸu p~‚DRõRÚ¬4 Y&ÿ)«C•|l·ÿkÖAa{©Y¡Rm(ê¥WÁ ÂnÞôú€#©{ ™ÛÌ”'øÛæ%7TÎÄSÅ}µG¹9´£jŸ+â©k¨:š»aµUU£‡JœŽ•P‰ØÑéXÇYÚƒ€1áÁêãW‹µcÔÑ_ðɤ{‹óŠj«*ÍÛ“õSoÖ8å9í ×gkëÀ\DMÕêéè,mZ¼méÌ1þªþŸzƒƒ\„-e9n>gž3¼»^Æsâ˜:>øða*^1Su]û§n9ÜÎ.Dk_©æj2‡»ýœd±T7 ôª-äÞØü6±"2:LÙë6IH§þÿ¼“W4ÙöœÖ1ÕÆmˆyÌOäA7¼µ·Å‹©nˆ¨s‰Y²i'©Sunõ±N«ÙZs<®V %šCà¨vsQî{Ê€v‹!õÈy…3uÎBšœçe¨Ñ)z!Ô>˜+õÝÐaW:7KëKªf톄¯Ãatr¢z:t5‘šXÓ›¯úߥh!öðËÑFõ²Â‹:^€Þ9"'K<<*º" Tï¾ð0¨‚´Zà ç/·s¦nÓ” U…þª‚è¡3)Úó;CXNe²u, +¨\Óy€Ðj“²þ'Q2ÃÙÊúaœLôR ‰ÞÞò ÝžbæbKGDùC» pHYs.#.#x¥?vtIMEä  CÌÌt IDATxÚìwxTeöÇ?·LK›TZ轨ˆŠ"**Q± ¢®Š׈èoEwíº¨+ö.kEt­X¥è*jl«¨¨X¥w„„ôdÚ-¿?î”;“™d’̤àœçÉ“)wÞûÞ{ßó}O?¢¥(E)JQŠR”¢¥(E)JQŠR”¢¥(E)JQŠR”¢¥h/#!u Ú†sKœÀH`lVë\ÅE¾ÔJQ öNÆï ü ˜¤E9d°xøÜU\¤§îZŠR°w0ÿ_€gý;~<ô5ð𶫸HKÝÁ¥ ÄLY€ÛU\äí$ó-æ„ÝcAÐt›C×EI|_ÌKù˜â*.ÚžZ¢)úS€cnÉHàà¿îlt  x¸ÏU\´ªƒÎý?ˈ’îëÒSP²óA”B7_ñ!ÕV"U”"ºj#‡Ùœê*.ZžZ¦)úS€cn‰Ü ÖÄü|ÀLWqѽìd¿( €.[ðöŠfs4ú;±¾KÙÄÚ*óÇ{€‘)I EÉ"¹1ÎàA`lœ?±÷8æ–dºŠ‹þÙ Àq½'`rýcו@=†Å¾ÒU\¤Ä1ì)æðõèß$óhi™xzAª­ÂºuŠ®K@ð(ð—ÔRMÑ^)øuûûb@4§Û¨Y¹hi™h‚®"Ö×!—nÓÅgžûÙ®â¢×â8× àp?ƒî  âœj=°Xüü|å*.ªˆ8ÇG@€–™§×àæ£rùn,l ¼Uý]ÅE¿¦–kŠö*pÌ-ÙXô Û Ó³Pò»£¦;£OZñaÛü‚Çø¨ …e?ß Lú'ø4?¼,ÖÛýÞ^ƒQ3³›?ª®c_ÿ‹.xÝçó«¸èêÔrMÑ^£8æ–ígþÌືÚñu냚álœ?d Þžƒ°møt ø'pµìÀùÀE~ñ>òÕB=†±QB×s0|÷b,Õåÿ»Ñ¯2XŒo¤&¯#6, (9]Ë®-OŽK-Õí5€_ÿŽ \@ÉïŽ/¿ˆbÜãXJ·#—íc5þ]þ*àä˜à&I5š-­BKË5GF¦fKuÙ"  #VdóNŒ®#h*‚σèq#xê±¾ÑãMm¤|Ýú fæ€ÐüÛ,Ö×bÛtr¸ggq¦(%4Æü"ðBùoáÔ¬Üf¥ävEÞ³4 ¿$ñYTf´§¹•ì»–îD³Ù3ÍRGS;1‚€.Šè²Í‘aTCt»êªªËÜõ Ôë¶uè6ž>CÐek3ŸŒùœÒ€¤¨Ó«'‡Þx{ôoó肈nµ7`>]’]jnWIqæYu«ÝžxÙIDs¤£9Òñå÷@ðº‘j*‘+KͶ  ©º%·k3­ aÒ…Ô¥–kŠö˜\ãNTg^‹‘ꪱì܈àõDŠÞ•¾n}²ÔÌGKDï–’nµ£äuCÉë†èªE.ß…T]Ž.ÉhéYÍO¬ Ú™JJÑÞû^¨™-Øùuë®-Hå»Â?¶XQ QœùÙmÉøQ7oGÞ èÞ×0³42óÛÿ¦–jŠöšÆõf2†èucÙ¶Ñ]¦§+yÍ7 ¶ ™Â~›53hIÕˆ®0‰ÿµÔRMÑÞ;ð»æDŸµ"¿uÛZPC¿Ðíix»÷Cs¤wλ¯iض¯Cp»Pº¢8óŸ9àÀ—©¥š¢½¾Æbî|yÝ›t“IU{°îØ`ì”õ!·+Þ.½:Þ®ß1ßU‹XS €eûw=b}-‚T÷ÝÀå©)JšÚç|#ÐÁãB®ØÝ8B•ïº}}ˆùoÏx»õéÔÌ Ùè²%t­{þˆÌ ¼6œ¢dR{½œ`hO¿áhö†År,e;wo ‰ü²o¯AáþøÖR]5ìÙ •ePSÀ&C*ÉéÎÈíö´ä<ŸëÖuáv ƒæºŠ‹.I-Ñím*Àßc€\t ëÖ5xúC·ØÂvþ0æ·ÚðôvL‹Hסt¬þþ;þ·¤éßdåÃÑ“àˆaøŸ°¡[lxúömbm¥ù+_Äs˜­Š›12SêAŠ:—à—&ïµ.Éx{Fsd UW`ݶ.¸¶u«Oߡͦ‹dü-ëà—à•‡[>NV>L½Ž9 ò»%î†è¶-këª@UPwïÀ÷á[ŸéŸ/éâgþH°Ö0ê ®>ÁÈ«X—ZÒ)êà3€×ƒóð»ôäò?á½è+ž>ÃЭ­Øùk*á•GáÅ¢~=pà@ïĉ«‡ ¦fddh€¾cÇá·ß~³¼öÚkyuuuÑïÓßîªæÔ‹³p$F=j«ÿû2Úû¯£ÿ¶¢Ù&? >|žZÚ)êðà gdÑ™I’ðôWA˜ôó×ð¯i;7†Y 9æ˜-_|qMQQÑЂ‚)¶à ³sçNV®\É3Ï<ã›?~ø<ÇŸ®sÅ]{¶|ŽšßÜW†}ew¤éûŒ^Ý»wïòž={îÖ4²²2û÷ߟ·víÚ®Qî›|LǨ_¢u\ðƒÀ‘Á.a2µ·÷–§Ô”¼µ‡™†Åßxãµ—^zizŸ>}„æó©Æ/¿üœ9s´'žx"(=iü{±Ø"¨*‡y³u^z08[¦“É\ÂÔÓ&rØèHOOGŒâñp¹\»Ö®]ûËüùóõG}tlMM9 ¢ø«_5HQŠ:.øA`ðK`NZ†Oï!-pÉK?p÷å£o'L˜Àm·ÝƘ1cZ=WMÓxçw8çœs¨«ó[ïû÷rç‹Vú ióÿk|½4øÑ“O>Éé§ŸNAAA³æäv»}K—.Ý9sæÌ®¿üòK@_R€Ó€%©¥ž¢hÔ‘éÅ![€¨{»÷kùHŸ,þZºïŠýƒªúßþÆë¯¿žæE‘I“&±|ùrFòc̦UV®8¶oŒoú¸{FùO?ýtmåÊ•LŸ>½ÙÌ`·Û-§œrJïÏ>ûÌvýõ×>–7+RK=EÑHê“pÌ-é¼Ðg•‚‚š™Ó²Á¶oªJ»ú”^G0ÿÝwßMVVVÂç]PPÀ‰'žÈwß}Ç–-[ÀU ›ÖÀa 1»…Ç^ ½Ài§VûüóÏÛzöìÙú{éppÔQGѽ{wÞ}÷ÝÀ3žü¬I-ùuD à:Œê¼è’Œ’×B÷šÏKá#×Êõ~«ý©§žÊwÞIFFFÒ&Þ«W/æÍ›Gy¿-7žjüGÿ}þûÇ7¡úùçŸÏÈÌÌLØœdYæÒK/åÅ_4?çW1܉)JQÇ‘sKºbT²¨=bm’¾|ŸšÿÜcÈÌÌdÁ‚tïÞ=éו•Ř1cxî¹çŒ~ø1ò¢Y¿ ®9€Áƒ«K/väææ&|N‚ °Ï>ûðÙgŸá¿¿ü@¢u àrÀ°^KJN×–âªG~ìŸÁ¨¸§Ÿ~š´ÙEŒ;–[o½5ôÁ¢ç¤©0ovðíœ9s¤®]»&mN’$qõÕW‡ìF5¦q­¸df%3s(™™OÉ̬u^jï@ #¤µ§¡ûâ+(lÙ`Ÿ¿[Î Sr;ì0>üðCG›^϶mÛèÕ«W胅¿C—¡÷ëV†!òòË/WfÏž-KRò…°>úˆc=6ðö`j3˜]b\=£r7¿D!bÔ)ÜÑ é3àuŠfU¤X+%ÄCG˜@É.hÙ(ºoÎ îþÿøÇ?ÚœùzôèÁôéÓC]}WGDó}ýaðeqqq›0?ÀAÄàÁÁ%gì-M0~%3ÿŠ‘¾ý;ðF…¾”kÇêÿl ð$°’™·R2Ó‘b¯4Eç¥ãô,tK cýËvé|÷q0àç°ÃkŸ›)ŠœqÆ!©ê÷ŸB_Ö×ÂëO0dÔÁ¾áÇ·Ù¼²³³)..¼Möo„ñ%JfNÃÈ+x£U[s$Ålà6àWJfŽI±X b‰ÿ6L=ïÔÖdØíÜ\ 7ÜpÉÔ«#Éëõâõ†ªu<8Ä,K^„@q=»`Ïκà¯56›-)óÑuÝ»w³fͶmÛ†¢í MvbÚJfö–O‘ÖÓX}uŒ<Õÿ?@ýÏ)™yFŠÍRƨÏ/¨Ù-©¾ÎÌ€IŸ¸®ëüúë¯Ü|óÍ~øá~øá\ýõüøãЯ¿¿ÙžP¶Ó?Çšàï{õê™ÃÙÖ­[¹þúë8p C† ¡W¯^L™2…ï¾ûŽÂÂ0ÛÊ1†¸8Àô^51¾ÜˆIÄð(E®' ð*%3¤dfnŠÝ:µgwàc‚â¿#]jÅTBÝ’êó0ÿ믿δiÓ¨© 1õ·ß~Ë}÷ÝÇsÏ=Ç©“O塇4¾Øµºõ†Úêà±éé Wþ7lØÀÉ'ŸÌªU«Â>_°` ,`É’°hà>1†)7½®Zs3u?8HÀ?€.f•¯¹”3{š £éê@!¡f­U̘ó{{3RÎìi½1¼,}0Úº¯>­˜1Ç•€èl®µf÷Ûî2~ùåÎ>ûì˜ßÿõ¯寛n2#†ñßpŠ’ØÿŠ¢pÏ=÷4`~3]}u\½E1½n-’ @éÑ æº £÷ch@“3{ÚBào3æìhÆ·÷`„[GffîÉ™=íAàþŠs”Ží¢8æ–XÑA93­•kÍd<4ëãÉØý_{­é Ýòi”‹Õ P|‰] 7nä™gžiô˜5kâŠN´db¶ ØZÀ\bÎìis€'b0hN¾É™=­O3¿ #ÑêDKg‡<à.`‰(Rà§¡Á!è¶VÔp†ò|¾ä5ÐQ…—^z©Éã–/_Þ°L—=”©«ª‰€Ý»w7ûRb|žèúêR+Áå*à3««éV|¹|Ùv4kØ=ÿæÌžÖ–5âïÆÈ³06IT§5Í‚`^Çw¤ DÁœY]¶ ·Ön µþ ¦ç&‰ªªªš–4Uh07“”²võª„Öñ³Z›í>ý­½=‘d #¨(Þݵ0+d'²èÕƒò©î›Cm÷,j T ÌÇÝ=,‡b8p}íþbʲT2mzÕà|©¦§“ê~¹T Δ¬0¡ç*ÿ5¥†ËéÖôíL-‚wß}7iR€,ËL:5]!ôº«ßúžß Žà¹'kkk6¯¾}ûÒÌLÂXõžæ` ¬fJç`Ä, K¢^Ó;[P-Ráß•›†7/L‚œž3{ZZ¬áñÛÐt‹¤×öt ºÂ7M©-t¢KA³b´­ïPÔ^FÀÞA^±X[?Za_£Xguï½÷ëÖ­cذa Ÿ´ œsÎ9üûßÿŽïÇœJ²Úàœ+àöbÊþØÉÚµk#}ó-¦‚‚fÍšÅE]›Ýår ~½üíaúiæ†3>¨ÖeÛMŽýSW~:ÖrWx €£ˆÑOѯ‹ï‹§ÐÝ¿ 0Œž ü¯J`«ÿo(O ž¿kFó×¶( dÙ°T‡bÄXüé 't“0…ô,øëõðȵ|ÿý÷I€ƒ>˜Ù³g3cÆŒ¨ßwéÒŽ{÷nC¬}Tø—ƒƒ}Qyûí·ÕQ£F%ÌèvÖYg±mÛ6fΜٓÆçã?¬ÐÿúrSä‹aØj+ F+*iOC“EÔt RmÐ|œÉ08#‡á?ã'.F@ó¥Yb¢“â°˜``JðkÄ&¹:1#š˜ë©§žÂív'熉"—_~9Ÿ~ú)çw^ðó3Ï<“^x¡*ÈüFÜõEFðã¬Y³¤/¿ü2az€ÃáàÆodùòåÜ}÷݆;òÆY°`A¤àÙF†q%qs°ÄËx9³§Ùý»0ªµiœTÒ®ñäœÙÓ>Ê0 ¤Þ…‘1€D'ÀéˆYëË‘|a].38™¨{G€ö’BJ²¦%fÄa@×> »6Ë_~ù%o¾ùfƒ&’$IâÈ#äðÃç±ÇC×uÒÒÒ¸à‚ vè~|¹УoÄeC (y3°k+«V­ªÍÊÊÊHÔ¼FÍèц‡UUUîºë.ÞÿýÀ!Ÿ‹¢6\ÃN¼¯L‡‘pd,–&zGÊÉæØèïÿ‹9 ]–Ð,"šEB—D4‹~1Þè[' è:h:’WEP4D‚ jº êaT É­`²QÈf5À¬­¥ E6;ß´;/í pùå—3nÜ8z÷î´‹$ §Óà÷ lýõ×Cw¦ü_l º|<1“íÛ·g_xá…»_y啌´´ÄÚ­t]çÕW_å–[n |TáRÓÚq½Y1ž6E^ÿ.*1V‹äSIß^TçmÌpƒêQ‡Õ&£Z%¢éìq®\ATTd·‚¥Î‹\ëEµËø2â²eu¸f–í5!o=è?þ̾>± ¦¦†o¼1.·]kiÕªUê™Sιx®¸íû“/‚QG°páÂ.—_~y}uuuB™ÿÍ7ßäüóÏ7ît`m;¯·¼xª˜1§Æ,ˆJtÌrìªÊüšMÆSN]Ÿl*‡PÝ/—ºn™xœv»Üræ÷KšE›i£®[&Uó¨íéŒ)¥D€W] Ú|¥ª Ü’e¸ò®l,†ô8oÞ<®»î:*++›+Ä=© 6xŽŒ®)>cq²ÁàQ†þ57/¼ðBÚI'äÞ¶m[«oAuu5÷ß?gžyfðcôè¥Àš“´+ч9H³É¸»fP=0ªyÔwÉÀ›ak³'€T«BPêú<kg‹ÞçJöƒÞ¾3g—]vÛ·ooŽž—u~ÅŠ[9ê½t×NC•y8üý^pÄŒ–ß îzŽ= €/¾øÂ~ÐA)/¿ü²Çœd/)ŠÂ‡~È)§œ‚©,¸Ü <ßAÖ[s¼ÁÄÉ]J¬/È ®O65ýs©˜‡+?Õ&·ÿUúTØU Ë×¢.úF­|óóuÕó>Y[qû+ùÀ¿K1BáÛ=<¸½îÖ¦ÐÍòj躈@¤}4<¼®;|.^{í5~úé'ýµ×^öÝw_„VžËív{þ÷ãëoºîšPU!À¬ÿ4¯a¨3®yÀøÿæ“ìÚµK>ÿüóå#F(Ó¦MÓ;î8Kß¾}±Û£KÕ×׳uëVV¯^ͼyó˜?¾ùë*¿Øÿ‡š³à¿Å_/B®÷Å„jo†-:–×(lØåaK™L@‚D‘Â\ ]œŸ9AÃ_B¨¢þ· ÞúZgke``I‡Áþ ˆQÐzà_~é¬]’…ÚE>rÌ-ék–܃öOL@P$ýö#L?LRFqqqé 7Üàèׯ_F´v[ŠsªªôÑG¿Ÿûÿè½gýªÕnàH˜õôia-¯>¿vmûjÀ€êgœáëÖ­›­gÏž‚¦ëTUV²aÞ}öÙXyùw™æõ,™y&F³ÖdÑÑÍú4žý¡¶ßViÕ4IŒßþZgKeÓkû¤‘pÊÁп+­Ú€¼ ,Z^ÃÜO2P´– ´£8î§€@WoÏºš•›œ¹lX 7ž[Ã³áÆŒ³çª«®úãˆ#Žèß½{wG,© ²²’M›6é_ýõ®‡±ö·Õá.»ÉÅP|ä&ÀÃSS %oÃÂç5Ö¬h‰zöFÒÉÒ¿dæq-þm|TDѬã€IúŠ•C b»½ ¼¿žûjZ~êpÆXèÚ‚´ôí{à޷ݬÜÙ@L›1cF}QQ‘­gÏž’ ìÞ½»þóÏ?ÿý£>Ê\¾|ùÀ(¶§;™{=øAà-Œ4NÔìü oþ9I;YùnXôöÓ)šõVœðp%€’a¥¦ONl|â}øº¡°sà 70vìX òòr6oÞ¬,[¶¬þÙgŸ ¯Êd“à_gÂèfëUÔÂô§Ê\AUúœsÎñþýï·Ž9‹%¶Æ³yóæò{î¹§â©§žŠ¬]+F”¾·Àßü¥:×ГŸÆ¹m¼õ,¼6»åcäõ€Kn†£N†¬&0ë£pËÔðÏ.¸.¾!6h4°îù@U r˶î'{à™éu5§zo»ä}âó«Ç G&Y FѬgâ€UÀ0w· \yéÑðêças¨ ùÅ_ÌôéÓiŠ·nÝʼyó”n¸!Üvÿ¹p@ÿ¦'øG%Üòª›õev€!C†è<ð€p 'МjÏ+W®,=óÌ3åU«V™ÓSÀe{; ÂÔ«ÎÓš½ R¹uªË᧯á«`ÑsMÿæ¨É†aqÀp0",û0&íÞgž(ѾO¼û7¿r±mãJDWЕ|­«¸è„Þ›äÀ•ÍšówÃÈWj䢨- ðÁE°b ÇçÞ{凉 øûï¿sÍ5×øÞyçÐ n9 Žl¤€Q ®~^a}™ FÊ÷Þ{þýû·è¦ÔÔÔøÎ;ï¼ÊÅ‹„Ý+˜lvh7Ÿ‰«¸h­cnÉø“>¤Ê=•Z·ôìäCžÎ<w’ñwéL£^_m5TW„—×2s Ë~¼F"· ªöÀ÷ŸGg~0¤W= Ýrâ·hŽL3Dç£xíckS›¥!Þõ&¬4 ®<˜%K–´ˆ‡ ¼yó,7Ýt“þØcùö·`vŒèýGÏ~´“õeÝ Ä¢E‹ZÌü™™™–×^{­à’K.©å•WÆåûýö˜ß÷JðÓ++w;”.…èb·+ÌÎ7þZC7üü5üï¿ðæ“MÿÑ|ãàâ›á¤sŒÂ¡M /á‡è„¯«'(©éÖ†rêÂo˜? È}÷Ý'¨ªÊ“OúŸßó5ž¾L$+¢¿ÉÎ …%?“zæÏŸÏСC[/;<òÈ#iëÖ­S¾ùæÙ¯®ÇâjwDN&:¬¦Ù¤êNØQjýJ¸n \yr|Ìß`7¹NŸ½SVH$š`[Œ½sK­3%[%Œ݃ E¤ffçCå Þxã„1àí·ßÎøñþ2»ëD¾þÝÓàÀ¥?m.×]w²ß~û%ìæäççóàƒš7åó1* ïà*.Ú‰)PE.ÛîkŠ :mZÅÇ-ÁM´ßîL;ûHf]}'ß/ì󷟾’7Ÿ¼‚³ÏŽ(sãÙé¼ÿZ£‰:Õ“lé­ µÄx(j²BÎìi"Fî¾!Dèþ§¿_ßtÓM$šo¿Ýä)zj©—)× ¼^ú2èºè¢‹dAH,f|ðÁ™{1ïÍ*~CÇ€(x=¹ª¬å=Û’Tž¹3¨ç1Œ[.ÇAûö'?7“@Ñ%SƳyGŠO¥W<2Ò &>úÐá\}Îhϵw¾\þÉ×k ‘rÖ4‘!#¡ô¶aº$£Ë„PYñAÀÏ ¼*--Ä“ °¦â¤Š]c@}î§¡íñüóIžtÒI¼ûî»PåYµôãì¶P Ë…^¨‡uJY,¦OŸ.–”7•qÉ\Æížžè*.úx;ˆH¥Û5t­ãÀž]ð‰áÒ¶X­,zt*'=Š.ùN̆V«Ì ¾Ý6¨0Èü`”ëß»‹íÕÏèö—©†Æÿtv€°´álmÄ“ó¬¤[¤ð‚&œ:ujRº@Y,–ðÒjëv†^難ǼÐÜHÒxiŸ}ö1<8™ªYGÉO¾ £‚Ï+ZJwt|ð„Üï ë®t-pFgX]§¼²–Šª:\«V«,Ì:ß´ã¿ÿT•7aj@ßNf1‰g!ó¨ÕHýßÄ€&L Y 8b„ɸþÐëÕ¡JjýúõKÚM*(“€{ú¥¢½\ÅEk0K”Ëvꢻ®c/åôÌ þÍO›åÊêú‡¸=^žzù#y Æ]ÍQgþKýò»ßu-¢ R×|'à¯aè©5܈±dt‹­­$€dP<^€‘QÅÿöbÀ’ÕPé_‹å!·nfffÒΟ››KïÞ½wšÍ{»€ü†']°n]§ ©w)çä[8ìÄ ×¿õÞ7¨ªÁØ^ŸÂ¿n¤øº9ÊÍ,þdíæ=ÒÉ?,\{ç+Þß7ì  ÅYM€~ ¤æ@ƒÔÞ6d@S1ðø±>-tï“ÙJµ¦¦¦¾-HG0â˜[b^6‹ˆ‚Ï#[ÿØ‚·G]ã‚çΰñ¥Q}úš»^çí÷¿Õ‡ *>ýj•¾nËÁ|GŽ©ÿôÓOÀÞüÒúŸ7¿äØÃ‡qÜû ²âGS¼G#!ÆzxÕž m‘3{ZFQ?DàEÛ1`ô&³y!Ð ´]CW±) IDAT—t]Û`©ëúºŠŠŠô? `<Œl°]T–b±Úðå÷è˜Ëyä¡»Ká±›d€/Wl¾\±!L×wß}7……Ñc|–,YRjž¦œ™½Qx”@9Qª÷ö/D×±mZà5,í‚×m4ÿèˆ$JPt*Œ)2€ž¹k;Ÿ¾]pØa‡EZtéÕ«W_}5‹-âÿóG´Ýð8 ÚîÓd– .ÉfH¤2œŒžjæ´)@“þ¯5°QÅÉ€µµµ<üðÃæŠÈA:öØcyøá‡Ã-ý´~ýújAKýó Ó¿[ 1ì§Ÿ~Ê?þñ¨}+++™>}:ï½÷^Ôñ_zé%ª««yñÅÉÊj°®}?þ¸¹Ëñ—Iz.ÆòmçÝÿ8Œ..Æ•wïc »xúEuæ£ævEqæÓá)= ú …})0“bQXðÑGåÂãLÖ…°ÇVØÁïŒyoª$XP×Ó,bC °‰—.]ªF³¨ªÊ½÷Þ•ù>üðCN;í46oÞsï¼óκà›cF†Ê†åfBš¬øÏÏڵы,/X° &óhÑ¢E,^¼8šôñýW_}uŠé£dhi?ðWº;°@t{šKqæKf]×[Øo·> Št’CJºÇã‰zˆªªáåÊ-¶æ#<Ý5£óÜœ& _HÖ9ÈÍ„^ÎÀ,­Y³¦H|÷ÝwÜqGã¸×¬Y³¿£×ëuß{。þÃLvÖtœ8*ðÜsÏ5ðÞø|¾ÈÚŒ1iáÂ… l?üðzB!Ó>àŽ€I˜2Ú¼Ýú8Ú䬺NRÝ‹i!‰|×®]Q©ªªâ믿6¡aóš‚D¸m‰¼;I¾ûM©]ƒ i‘нé68ñÀàۻ2’ƒjUôàƒR^Þ0àê7ÞXQQQa<Ä.é:ƒ# Ч¬úúÐC™».m+W®Œk¿üò .W¨^åºuëVÞtÓM§›ySyô½ ® Š{éYš––™ô Šûú_p¬ù©º<9'ér[~öÙgQ 8¹ùöð”é<:9(™UÏñ'u_M£ë½ãBºû«¯¾š·pá°¨©~ø!îÉDTÝ´iSéÅ_J³¾l‚@d!’n92”N¦L™Âo¿…*±Y,âíò”‘‘ìwëVVVª“'OîFÈUêîIº «Äÿ±÷J~6™‡X_c5ëöõˆõµ‰?‰©2ðO<Á²e˾v¹\<û¬©?gÑiÍߦÃã ì4 ˜a€è”nÙ0½(øöÌ3ÏÌ]½zuP¤kÊÂo&³¯²²’ÓN;Mòz½̇ƒbZžzˆÑ}ëÁè@5nÜ8Ö­[´íœxâ‰q„ 8jkk™:ujýÊ•+óL’X1ñuqî”À߃‹Ùž¦·•‹OKÏ •×u¬ÛÖš3ëCÙyð·»‚o;ì0î¾ûnÞ}÷].\ÈÔ©Sµgžñ—ÅËÈh¥]DVPIv=™Øž§0 Ód1¶j3ñ ?,`O>ø`1À€cÇŽk"ýû÷ºájkk¹ä’Kê~øát,"\ZXàQøf…k'§1À0N—––râ‰'òÉ'Ÿàóù8÷Üs›<ff&gœq?þø#'NÔ-Z5¿t<¯M6ÅvØýø«(¹]Û¬.¡.Éx{ úÙŇeW@ö” áèÐÎ~ÓM71qâDN=õTÞxãÐ=¿åi…¼®ü‰Hhdz†Ù"‹…pXaúc—6X7nœúÉ'Ÿ¨‡v={6 yçwb³ÙøñÇ™4i’öæ›o‘wé¸ÿ<£W@c”— wžk¸ ýjÝøñã™2e .—‹ûџŸqƼù曌5ŠÏ>ûÌ̇×´Õi `´PQT•¬ÜÖ¦iX·¯7 fzšn3¦ÙÓðu •ß’ªÊ¯ ¤gÁuÔÑ¿ïÖY ‡Ÿ ·˜’c¸ÓÛ`½ÅZsé! ”¦Z‚“— wœ }su€;wJãÇ—þò—¿ÔœtÒI«$IŠy-W^y%#FŒàÖ[oeÔ¨Q|òÉ'¡9ÝuìÛ'¾«)È2@àÝçí·ßfܸqÜqÇZc÷ó¹çžã®»î2änjKfl@ àî¯f戴² T[‰äÏž“K·ãíÙt‚œ’Ó©²Ñmä[Xʶãé=$±WéÌ…+fÁéÅFs’ÒFóÒ¾ˆ…}Ñó»·œÛô6ÛÛjÓ1Kð!ÄÑF¬ î>Oàž·|ü´Í°bÅŠÌ+V ÇhŠªG¬qmèС5¯¿þºóÑG «—fL„}šÙN¾‹n9>_eT{ö7&©®®Ž{súç¯Ö7”|ßÖ£MÀ1·Ä™ õ‹×´SÄ#~£tí…u³‘€#ÖV!xÝ‘%· ðŠF³ÒÂÐa)Û¼{T•á°O¤KïÏ@Y@i4ë‰éùX›Å€wkáóUðï÷ ^iÌ6"þöÛo ­„玅ɇ@n C*ìV8n3–ýü?5ÑŒÖ&éòøý|ŽYå.ÙÝ€53æ°WF­·¬QÎÙêÍL+x\ kã5AjºÝj†ËÕøò»'ýH~ד¦"zܨQ@¬¯Aª«Fqæ%”Ú×FBÐúÆ@›©ššðËÕ:K€_v4~-˜rŒ…M8RT ¬rãyY8~7vVÅDj]!‰M œÐ-¡{®¹¦Tö— !Ð q/€ÃƒÌïÈ@—ZoÀÖ,6ƒáýeÄDw=š#>$WyÈ¥R‹® ¹ (>_(|U‹ÂÜ‚ª`Û²4©¦wÿMñeg¨¢jŽë•æj1I-¢,œp€À„ý£3 ºJ~–‡¹iæ5ÝX~Ù¯};ÊáСpú¡ßDÌŠ(Ba®ñ×ÈкEoЃ9ðÏÁrÏ üE4»#Ø0Cª¯‰4{(`Cðz’¿½šÏ!JQ;"‹îúP¤¢Ï½4gxÍÄD–Nj 0I¾”›%â«KhЦ]ð÷—Bï·~¿owqÍ)e8{b‘Zu-š$"€.íÌm Á(+Ýž–°AµtgÄšJÈ‹s'7 Í;sÒÖ¥/ºÅUœ ¤Aƒ¿H”c„pHd®x²ÒU“`kDJèX2ÍÂïOÛœÿ¸Ñ2è~»9yt:£ú§7ˆŒK}•Àåûs€cn‰h¾HÍbMÜêÊÌF.Ûág ZŸ7êîÚpGå›h¶6HE0Å­kQu{=,D9–$¬)T&"6yîöî QV ?n¤Ë?5Ž®ßlìÂ7¡žÆµ“ÅyM@¸ ²×KÝÍèQÙ¦u|eOG·Úü"¶Ž\±_—&‚At©"dŒÖÒÚ ©Îœ„eg—êj CfÐFC‡TÃ6jwg˜,Î3O8–U³<$á´¨|¶X•ÝQÔ•ÉÇîÏ#·]HeUk6îä?o|ê~ﳕv.{ž¿ zÅŸº®‡k޽ì&ôב¤Äé‚‚€’Ó%Õ'—ïBÉíŠ.ÇË,{vb®<¬¶6 )^Ý40åÈ’Vš†åPŽº–áŒ!%€ž·‹ÎE±ntuèÞ躎.´aˆƒ<ÿÉNæ-ëpÍ%Ç3áÈýp¹¼<ùò‡lز›ãÉ¥çCV†ƒ¬ ½ ó9êÐö;]°iö %}yoL;®Y6€ûÄ^ B$Ó&¼³»`)Ýa첚ŠuçF<½5´9é–ÝÛ÷üÆüñ[µÆL.?Ñ]g¨¢ˆàON2ïþ¾‚èu>U4îN0$Eö1½ŽU¬Ì$ IéKÔmøæ-ëž™fÑ–œYá¶+Y™xÌòìJà·mÍÜ„ØüÑFÔ~éÀ ^jº$…‰ýbM%Ö­ë }Y×T¹²ûú_Ø_·ÚñuëÓ&—¬9ÒCÀ§©Ø¶®Áº}¶µ?ÆË€¼œ×=6 iŠYGÖ=Lˆµæ¶†]w‡Údàèõ—Or›™À§¨¼ôÖ w5÷?¹X©® 8ûîçõÆdGõ§³Q[J^BN-M¬lÓzE3·+b}-RµÁRMRMEd ½0Û·× FU…Ä‚”Œ’Ó¹Ü ±®ºáœ2³µ_¾°ìEŸ«¸èN°ÎÌÜlmÄNà èÂ’OC³´¡Tì¯û·a[iš¦ëê‘êºÎs¯Â?4ÚÀÝûô{òëï|¥üóŠÓÄ¡{ˆ›·—íùçCo^1C:´¥°Ë~=/9u½=ú¡eFH™ ˜_@É톧ﰸ¼ ©.…hiYQíJ—žxzjT=Ô°ûöG'YgJS›NÅŒ9°)x;|mÜfhOè’Îs¯}Î’¿ÇçS¨©uñÔËyÿùà[ôèуÏ?ÿ\™:uª{ÓöJ¹ø†çÄÃÿrç^ùTž¦êWN€AÍ $T½-Ô¯Ž!¸Š‹<޹%¡`ÄíkŽ$ô>E<½!W”!ïÙ|£K2ZV.Jn×¶qûE“D OŸ!Hµ•þ,DÝ–†š™mTümjÑ„»7$xzµm!î`€äQ´ÖnPoB\ÆÄ,Üü¸ò.ºv.Cúh¥{ª…òj5##ƒ 0fÌyìØ±ò´iÓÜÓ§O÷üüóÏNFöT8ëp™ÑÍïÒ¡æ¨{5øé§ ¸ëH^Ë3%§%;ß¿U|FÄ Õžpãc˦' fæ fæ4ÿ§á‹ÌÔ˜»ep €\ïóÒD'!AÓ‘Ý Š]Žô©#y27W:µ½³Qâ ÔÙ§7Ë/¿Ž%sÈ –--lÓwµÇRlk#`°b£X]‘|©GÐ-V4Gº±ã -s6Ês×G2Lg -Nø!tJã27W±±œÌ- –’­ÆƒàS|i;jâŸé¾}àéé"³/ü*0ïì솎‹îÝ»àämy𤠄ùs·ü`q@Ô/R] )jæó„þ­í„— 7!!ÄcKcvQÓê {ˆTçCTÂõ¦‡4 ÉåCv5Ãæd³ÀÂCI3¦êó5üm]]Ød¢QíÁVånt=ŠYŸ+ÛëÀU\´ø&ø`*KSݬê©3wÒãYs3æ¬ÁäÖ´4´š$†‰ý’7‡5ÍbbÈfLJ"ÐO€†Õƒ~úé'Ã…“ÝÐŽe«t‘¶µ’´mUØ+b7ùÿ„ÀSÁ{\U)Ò¦¨ÑÝ?LM¬K‚ Y¯ $ÕžF3´4GˆÁ-õ õ9C&¹¦™É^‚c‡|ðºÉ“T^^îy衇 N߆!ü¶òÐs£Ä3¤í®%kc¹Ž¦›£Š~ù³À BèXvmIqv¼k2~sy;!4åÜÇR„¡,œÁ3BXb©n˜Îí3I¢W¥±±¢ÒAÁ*që­·òØcñí·ßòÉ'Ÿ¨OžX§ªªÀÉ£tz„r%ŸŠd’\¼™áÉÖZ¶Ò:¤zŸYw¨¶ý)ÀU\än N ®¹bwŠ»ãyXáÅK—w¢©›9¯©2PK4ÃÔu©1ÝÝ—a­V°Ô…ã¡j5™t=ênÜ(åeÂ]gk3fÌààƒfüøñÒW˾Êå >0õh!Ò`©5|‘x¬UQU‘U3æè ðÓ| Ø6ÇòÇ–°ÄœÅxX®0£éIx1½nÔ÷[1cN%ðUཽÂSyWì2ªÉØ—öGMØ.él‘ÓiT?‘g/…KÇÃáƒ`Ò(…[Nƒ[ÏŠªÿË&UDÉßýU*©´'˜·K{pWq‘î˜[r†Û']úe ž¾ÃÑ­¶§Gã Ÿ'2è«N4}sbC<•`æãä*·Uìš3,ØÕ%ƒŒåA) sK%õÝ2Pì–v½¥ ¨}»˜uýFyÆ,þ«ŽðCmU.ˆ®†|Þn›J{ØU\´ 8+ î ŠÛ¦ÕñWöÝ H®ØuÇF,{v"ÕV5Z•H ÿ·c ›í”ÓÌ5·€P–£˜VVsQøÒ,xòC;±Tç%s}99«v‘±±"$ŽÛåÈôÛ¤(:‚/¤f(69ì;ÛžúXö‘’?øAàcŒ6aº^lW!ÕVuÞZU”¦msR]5–›*K‘wmźåwìkÄñÛ÷Ø7üŠuÛz,lÁR¶KÙN䲿Õ󅫸HïD·ÅlùoRÄ«˜1§x8¨"–»lR#1õ]2ðå8÷]]“Ÿî-hZX1³ b¯¨GôF½†Ï+fÌi·/·÷Êp=î˜[bîD4ëÖ5(=ñåuKxôžàõ‚¢ˆ.[ŒL@Q2^j ‘jk¨×ý†%ŸÁëAt»ëkë ÷°êÌÇÛ½oìùk1 RšŠà®Gjè ŠÎª3ÿ–ÕŒŽ¢gÛµßÒ0,Ë››©u 0ˆ7ÅïqÿæÐÓvT×ÖôÉɈea¨í‘…5Ó†cw-¢;ätÐ-õÝ3ñf$_µÔE#6!ä#*ª d‚}WÌ(ëWÚU MÈ(Ëjº×ûE½t >BÊpBÌN/º ¹²ôWËÎÍû k"ºŽ¼{+bM¾ýb&îˆ^w0¥VÉ.h,UÑl›V‰f" ‚¢’¶‘LâCÐÔFk×I•¥ˆÙù1«kŽUØTÚ¼1ÒÒ³ðöèלöQŸ³¬f c3wvˆK꬘1§&gö´ÛÇäZo†­Ú­{²ì1²7Ó†7Ó†èSU]ÕÖ†{œšÝ‚ä÷F¤í¨Fɰb©rÇrAÖownXV3 ø(lÍ0Jvš=ÝèØëOx]µØ6®D³¥…zªŠÁ€ª&ç‰îzcÇmlåÕV‰M¦!ëºqðy[W¦¥‘%ºdA—dã\€§Ït« ÑãFðy|PUM1YFsd DÍ“ŠÆŸ°¬æÆfv„ÐK %33)šOøSÀ9ø[ɧm¯ö)‹Um¢N€f‘Ð,ísqž\i~½*ÖòFmZÏU̘SÑáѸæ |ÚZæjÒðôÛÕ™&*‹®Z¿xmÍ4˜&Q¥ª²H P‹¦£™Ÿ•nµ¹ŒÜV°¹  [íFv_NxTX£ A0Ê~›L—­¨éY(Ùùø ñuë·G¼…ýñuímÔ-ŒŸùÍ7h°„e5¶8~“lÛ‚Lìæ ‘R€ L J”šnÍØ\énv@O[@–O—Œ†•èDÍ*Eîþ÷¶÷|åV0&FrO—ˆ§ZXåT—$¼…² wm‰?TXÓo Ú°kìsÚu÷Àý†o¡ Šöšé³€¨/†­@ð«Ø¢ˆn±¢‹bp§ëkB­¿D©Éü~Ýj€$4&0<,½ðàfŒ´ c¨km¾ñT̘³2gö´¿ÏˆÅž¹¹‚š>9 ýü„ê Òñ8mت܈> Õ*¡ÚdÒ·Vš+>Q1cζΠ0Û¿³˜™?`€juµ 5= µßDWmäÎmìž’ ¢ˆmÝϱ kB´]×ôÚúL@«ÔªíO41q<GÌBD¥ŸÄ‰ÛF¦€þz–Õ¼ÄØÌµqJÉbþf­»Šsþ“3{Ú¾~£ R½ÌMÔör¶mÙ°æ¬_«L}AFИµ~†¾mÀÆ e÷?¸ ‚ÙÇ‚€––‰šá ÿKÏB³§|S7«ž¼®%<àÀÌÄñÔ4KIìLd%d”µWw€u×®½øOp—¬õ媵ÆCG&AÓÉØZ© Šà58¿=]‰°\fúm»=]”@³¯ºÄû…LHb‡iOj $Ü?$6}kä=©®z§I´ÏökÀ§o†Õ*·< g©÷âØU–à§*f̹¶£I)-©0½™|ñJfc¢.ˆëI$¿^¡ù‚»·ƒ`>kúN âmºÕãͶÛì»kÃbðE‚mw-6¿CFÉ´¡¦YPl2ªMF³ˆ‰i;¦ùÿ²Û‡\çÃRíFP´hÔÿU̘ólG´Q´:RX)º ÖhùРmv' vógW5òlË€I8g€ã~¾nÅ8c‚†,›ÍíÀ“eÇVíÆ¶§>Räö«äOX‚nÑe MÑey1!Îkv1 ª†¨êÆ£‰èS‘\J¬ì>¿Š'~'(Úù3æüÖQW`K ¨2M«¸ý$Ùâ vËUšÐÿÌ‚˜  ™»Šèó´¥`¦Ÿ¢~Z4K£dæß—IN,€\CѬy`rfO³ý‚€¿$˜. ¸³¸³Ènk¹Ö,õQù4Ÿ–Ôl8AÑíÈÌ)–ÅGc3ÝÀ—¦OÚ5¼T³§÷ Ýð&\i& !íÀtÉÝÞkxB’°fi³:4V®hÖÀIDÚDÿ¦hVkR_³0ÕP¢Äù+v™ú‚tªûå¢f„Ù§÷IɇÐ-J– W,”ðsŽëè2hK-áÿ!ä ÈoÏ ÐmŽà›ˆ¦ óµ‹I ‘Í~}OÚu{Z[ݲE~§ø’™ã€…@¯ž§ŽpcÝW¾üÖP˜ñ°©H@%Í‚*Ñõ­ز€þ@W F[ÿ:¶ù%s\ËÿºEÄ—i3TI@µ[Plº¹Î€®#‡Î9jo€EeŒ{û¥ˆV·qj1˜wò&˜NÐTÓâIütµ°ößõMJ#R}µI’iP€ã:²hÖ Jf<‹Éðy1 Åfæÿ8…¢Y­vÒ¼k"2‡ûûV+š¡vü ø7€f“©ëžÕèñª=윃rfOýçÝKTC ð7´zœD0-TwIô¸õ§›%= "·y¼îÆ+üÔU‡Ü†‚½ahâéqÆfþ÷ÑE³vS4ëd` í»!+á‘~o‡Q4+ªbX+ô¦‚"²³rfOkɆ·;´¾šŽ1ˆ8§“Žå1K ð*ðn$?¶½ `AÔâWDé¬ðIJhMüÅe®×¬vtÙÔ¬Üà±rÙÎ0µ#¸pwmEt…llJn×ääR„h 0‘±™-Ï(šUKѬû1š»<„L´ #Ãë—Þ.úQ4ëIŠf% Øü¢ô{÷öҺȻ*Ÿ®D¨?- \탛k›R3õN­ ‡›¹‡e5Çú <ÝÛë"ÔÌlÄ:ƒñ¥ÊR”‚¡ò^f< šÍžœ¹äv5úè:‚σuË|]{£[,ˆîzä=«¨Yy1«µ@?DûVaµ;±™‰I=-š¥b€ù€’™’ßp¦>Šf%[ |¸° ŠFæ–Jê ¨Ö†F][…k=¡ŒÕÍ3æ(-OÎìiK€óÁ(ìév:bµ®&Ôa¸Ì÷R0@`3ËjÆaøŽ{EW÷/DO„È]Ax’J4"P[Àœn¬7€«e×ÖãÐ5YP,»¶†ªé:¢×Ö|D·Ø¶!ˆÏ™ÎéCÌW1h´ƒWØgšÕŽfOÿ«èª FÛÆ•1ä/i‹’ßm†i‘Ô%§c´®iÔšŸ9ÜWê›™ÜîA ´Y&NÅŒ9¿åÌžv+p©ÁYëÊP2lø2m¨v£Œ›µÖ³ËRéîa‹h)=œ¢[!}w u]£ƒ¶-¼ñÇÙHÂÅÎe5c3Û%¼Í1·ä1àÿ‚Œ˜–‰.[뫉RìWqѽIœ‹x 8­‘Ã>Îví$EͦœÙÓË6êk IDATî&ÜÝ”=bxÅŒ9{š:°j±(b¹Iu-Òá¬-=ú&M·oº¨–ågJ«6tI@T4ì•.ÒwÕõŒ™é•¯_\ðæ1Ü‹ÒSPz›lóùœèÿ^þÀˆ»)~pNÒ|Ú‘sK2üªÈ>MZìã*.Ú•äùH@10é¿×•À2àIWqÑ;ýžV- ¿¹ÃO.úªÝ=¢é³ ÓwikK&T±8`S±ø ð[KÄo¬„¢¥Ã?¾d.¯ßë¡Ê>ƒ¾V¥˜zö~¢¦=Öåǽí+Tÿµ˜×½è¯I[K+ä–ã™ï‹Ï{t–ÅÍ]=! Õ¾NqNÒÊSéºa4="â+Pî7R»Š‹~jãyI€Ô‰šyRµX¼Œð¯{G\/ªžÆ×(~qð½;ƒo+*£e/㕌wþL†´9!çrkݘ[:Ž\»l'Y¼ÜÞuÙòºd\òWÀ1ÎIZ} b3ÛxŒ0L ÃKñ_”Ò:YCöbþ\Œ¤^kæ"ªnlè’PO2¼Ò:~¯Í5½øÀ“ίZÈñu¤¤0%£”£ßcÿHæ…¾LqNj}uT!µÜS^ÎL݉ÆIѳ¨Wóðé6lb=±ÌZi·êuáðÏ &ü£3q0&3[¼£+-ìW![ \"–ˆ 0‹&ÐÅ%r@¹…!õa›`wàñÊü“1²,©—|WÅÉGÄÁìRŠ¿:<­ýSÀ‚ ÿ¨k'ú/@^'³¹ÅšákÚ¤!=ê$»ÂÖö) &ÔÁ˜ÿ@`ÉñÔäKª8¹§é³h9ËY†ÓuLrÓŒú†Ñ¨£¹%`_à¿N¿¡4Jý@Ñ™µ¨z}—nrý­ÙôÊþU9Ƥµ;lµF518ø®ƒ0&0Ÿ´Yo„œ•çú߯À¨G—{ü€‘¢¸:'iuDŒª³—bt}Éuà÷Zß{ªŒäÔ:8\ùúqyäѼ"ŸÐÝ#²Ö¬eqXz°·aj€‘D îîÎIZ]Õbq‰Í4PèuÒÙ‰`¾ö¤c0üÕ‹0|ø¹Íùñ{”s6ßÓŸxšµTÓ¼Z ’Ãð²û‹À¤6:ÝÎ(À­g@orSFÁDï;'i_uVpO`¤é0Ñ¥K¦OŸÎüùó™?>§Ÿ~zƒwÍÎÁ鯣¿ñþÇÊ&Œ¢šºŸïåp÷`FBöÚ6:WX›nç$m ðÏǦûmuµRÔúç¬a¸Z}„Š–þ%Þ¢Q-¦êW­¡ö:#B-(R¦¥¥qíµ×rúé§Ó¯_?222ذa§žz*?ÿüsÔAòóó¹ùæ›Y¶lóçÏûîYFr½#.¯Ì¡ñ[º‚Øt«¡jÜ>Œ=“K—&¤ÆÝ‚ æJ8:à\º´I1¥Š“?§aQ“dÐ)N–,6P¹H§€i)í°t¡s’öbg€†¶`á‹.ºH¹ùæ›åB‚€Ïçã²Ë.ãÙgï¨|ÄG°dÉ~þùg®ºòJVüðCð»ÿ°?§EÔ³ø!×GeÓå÷ÿð«%¿`ƾVÇøþ¨Ä}Ó#ý¯sL÷¹# çàåÉ¥K·Ç€eÀ¡mð<¦8YòzƒóuñfJïh4Û9I»2QƒµåÃŒQJ:¸ó¿õÖ[Êĉe‹%¼Qçºuë4hP\ƒ~öÙgŒ7Ž;wrë­·òÌ3Ï¿»—¡Lc`PØ™®ñ{šÒ’¦ÜÕ…?ÞŸ\ºôׯ—0ŒgWbøÙã!FüÁm“K—ÖDÀ—ÀØ‹›¥Of:ÛÉ’×bJ"‹Å‰~CS¿ßuš\朤i Ò1j™íø`ùòåŒ=:êÁ%%%sÌ1q üÌ3ÏP\\ €Ûíæ¡‡âæ›o~¿€ÑC·à{¯¤ã’À-ë¸$·¤S+êÔKzsj[oÃðÑ?ë·aÜQ;¯%´8aréÒ­&øÔ/A$6Eaäs,™×¨:²XLÇðÐ\IÊÐ^T Ü뜤ݑèÛÊ ø˜™ù?ýôӘ̠ëñïѪb[»ÝÎu×]‡èñpãí·ð~àަ›?˜Îª XUpz…â‘uj-:u²N¤S-é¸Ä¨sé‰Ñ&ëj¢T“M× ¼"NŸ€]tÃøè’u*­:;¬ªæ‚ØÀû &Œž\º4PO*ÑÍ$¿#’š4ûýÌU-ÿá8£f@–ß¾è ÍÀyó\Ä×C@ñß•ð^.ÿŸñ¹j:oàswç §ú%<-r9õQÆ ´‹3Ÿ?p®Èyež54̱pûÏ¥G¹.`Mkýýí ‡Sož~úiÆk¼mzaaa܃žÃ"Ë2WÝx#_½ó‹W|Ëfçwngd£âŽØƒaÍAñ^Qg£ÅÍwT²†ZÖ¨U(ºŽŽŽ€ É‚@W)nr£ÅòdàôFç+‡"ë†>¢$nÈTÜÛ¬Z Äw8†õý&Ó‚K49Z& ð ü;¨ƒš€â”)jj àC ?_|1O<ñVkãíÒ<çŸ~ë~$í·ß~|üñÇäåå5øîço–3rÌ!! GÓ?NOŸ¬§†þà#þà*ã¾Ø‰ärnœÁI«³•]X´®þ·•@É¥K]Uœ¼ˆÄÇü&]È Ë,y2Å^JvÀŒòÜ\qÅM2?€Ífã¶Ûn£gÏž÷ðÃGe~€ý9øÿÛ;ïø8ªkïl‘V«ºj–{ï6Æ6ÆTcSL‰±x¤@Bˆ ƒ€ !€!SBƒ@`lÀظãÞä¶²ú®´mîûc¶ÌJ+i%ï®$³çóñÇ;«Ù™;wîùÝÓ÷\õ~ìØ±,^¼˜óÎ;¯ÍæÌ™í^ãÂB±ï±_;’µ•·ÙÇIü‡ù-À";+ ƒÒþT]|ñÅ䷣ނ“Œ*¢ }PÍ„àpbO†î€òR›±¼Ô–Q^j3%Y¯gP¼mAü–[n‰j÷×ÓqÇÇk¯½ÆºuëØ²e >ŸÁƒ3iÒ$rr:ÎO9>d˜ÿ„츂Æ@=¦™»YË‹„º…eddpóÍ7óƒü€;3÷Þ{¯Ý{UVVòÍš5¬[·ŽùóçÕ—•88™OÚ N2ILMíïÒ±¢´ßYãÈì88Ã/NE«‚«jy©mŸ_=|¨¤¬zk’M `bààAƒºt«ÕÊôéÓ¹üò˹âŠ+˜9sfTÌ`±X¸å×7÷ÒÚêÀËí¬ cþ«¯¾šÕ«Ws÷Ýw“’’Ò!ó¬X±‚;wrÞyçñÊ+¯ðÉ'Ÿ0yòäà߯ä[ÒºC·*˜>€ñè(kL” ¨¼ÔvZÜÄ*´Ôí¹hõ Íþq˜¡hîÅõ奶Ò$+{‡.𤠠 [pÒ´»±º…d]ƒ›[ù†7°¿{ë­·xüñÇ D&îÙ³'ê{mß¾]ã*!8í´ÓX¼x1W]*Ý~kø;[Qýªˆ* I‘zØG ÞP^jSÊKmûwöh#ÍÀ奶ےìxl©ýËÏÏï–LOYþ«t‰nTfSØÎ)8IQ¢ÇÈ–çñØc1xðà`pR)›Ngч“Š/ô¼À ÿçD¹Õb}Ÿi‘¤’mU)Êñ’mõ‘aña0H|>Á‘£oÝî·Û+îÉ¿”—ÚÞ-)«^X,h%Ñú£…‘§êlV4Ÿ½ -Ôz'°¯¤¬úÐQY:p šQv(ZžÀK%eÕ“ЋHfܤ‹ YÈ>#´»·œÔÕ¥eL„‚“ ·ß®µ³'ÕYŒ.Þ¿²Ø^Q“` f%¿ÊKmТÈIW9npùY‘½ YV¯¡(ÇmY²6Cmv Å¿o-/µÝå·L&ùÕÈÂ.Œ§ X¬>––”U×Gñ»Ÿøízú1poy©íàá’²jϱÀ KnjjêvpûùêM”êÞ^pÒ°aøä’K:¼Ï¬Y³˜0aBd”5¹ñƹ袋ؗÇÄwì7«úù×=4'hzbit¼2(ÖT©ž1¾¡MæÚhRTŽܬŸƒŸ;üsq+ZÁ×Â.ŽÇœ€Öìä-àpy©íß奶Ÿ”—ÚR"0¾(/µÝ¼ù”<|T^j³% }ªAW@bß¾}Ýò€UUUÁÏ}üÁpoèvþ+/¿œyóæ!Ddu8“0räÈ6ïQPP@YYmž“ššÊŸþô§àñrû=Ž:æÇQ4O„ ˜'0¼[1¢{„¢7%l<¢­šŒRZS¥ÌÍPéŸëc@ž—y^úçúÈÍP±˜¥þZ‘Àî|àe`Gy©íw奶,ÝßnÞOàëŸë=4q°Ë>¤Ð{ب„N>>@ ž*€-e"Àúõë£Îð‹%}ñE¨hJÒ¨ÃÃsì ~÷ë›oîÐ=9jÔ(Þyçî¼óN^}õÕ°¿Íž=›x€I“&u8– &pß}÷ñ»ßi¿_5d®iÀƒÅöŠî¨¶cˆ˜tÂl‚É()Êñ±ïHh(f£¤ KÅ–á%+͇5ÕGŠIb2¨mát|ªÀã46¨s9\g”ûk RUÃ6»¾hMVn./µý_Ç!méªçÄQSzª/(}L¬°z{šoO•10ÐñÀ¿ÊKmç—”U«IˆLo`ùòåÌ;·SFµ£¥C‡ñøã‡ôy¬¬âßùÁ|攩Q'5Š ð‡?ü]»v!¥dÀ€Œ5Š´´´¨ÇtÁàK÷æ0t‚xøƒF·ªzË¢ÿᤡRMÒR$}mnÒ-^Deƒ"1˜%©f•¼LÊ>U{‰=v3{ªŒèrÍr2ýï³­ª:c|ƒ©¥c6ªLÙh°¤Xù®2¨VÎ~<ŸT"Ó‡?ü0••• }¸Ï>û,øù úRH*›teín¹ãŽN'¥¦¦2aÂæÌ™Ã…^ÈñÇß)椠 ÒÁgŽÊ¿DSl$N”Ãk- |8PmÄå‰~iYÌ*Çs0ª¿“Œ´®3{ Ð'ÇÍ´‘\0µž1ýÝ# OQ'v´©¾;(Êñéß×I@Ûô)Z÷^þGyÂÌétòÈ#‹Ñò ¾$dpî9- ÕäÎ?ßCCCC\jÙ²eÌ›7/x|+cÈÀˆDâÐØ{BpR(ƒ`fŒ¯÷ ZÚ1>V|gŧöì’‚E9nΞÔ@n†êWÀ`è´F$ 5ógå!”e†\ho¼ù&¥¥¥8q)pºuë˜;wnðø¿À]v%LJZÔ@Œ”ãKÐPbºE—”U׿ ×:ŽÔGŸøçõAu}ø?_d¡T³Êéã˜>ª‰™IOmúš øÔ ]{+ÄÓ p þ ŽB£E¾¡L«_ðä“Oâr¹¸÷Þ{)**ŠÙ ?þøc.»ì² „QL.¿c¦6p®©© ‹ÅÒÝIéì­¡À—Ò-m3“*aïaɶJX»]òÉZXÚBC”çO‡“Æ †ô1ƒ¦8¬Z£A2 ÏÕ¹÷X¤n×ß[RV½= 6„À‡þ.ƒ¦ò Sø «xöÙgY·nO<ñ“'OÆ`èúfÔØØÈ믿ÎW\üî²y”©d`ÒqšÓÉàɽ}{÷2¢Ÿx‘>8 Øi“é‹©¼Ôv5ZItÆ p‘–Ò|*|½Y² BòâÚ¿æî*xr1<¹Xêâé’+/œ2A´ôÓ%ryàpM A^&˜MÛ¬ßmeO•Q/òßO/¦¸ÀÂüÙÇAWQ“ö¦Î¥/ocà§|…ÉW_}Å´iÓ¸öÚk¹îºë3fL§âjkkùâ‹/¸çž{X±bEðûsÉáa&“Ý¢Þ…ÁñØ‚°nÕ7Ýúà$"wåM” bÈü}ÑùÔ‹r|ŒèÛ:üû@5üõU•§Þ|«ŠüÙÞµPÕBS\¸®Ì;[rË »þª„û%›öÀVKVmÕ;Â$«.?&X,š}aÒÛ§„š«öJq€ëÐ*“¦ õÄ#¦0®^O-7²Š¯[T¾ºòÊ+™3g£F¢_¿~¤§§‡…說ʮ;غi3¿ù†§,`Óΰ7ƵôçÆ“EdÝs)‡¸M¹éóxèÙgœÔ§OXi¾©à‹üTÇœë¿%`8˲X43F¬ch4À¹Ç×cI Wà¿Ø ¹ño’ïZTg»åb˜2R0´/äg òüºMn8R»jjÂå‡. ·o6û¸ÍsÇÂÏ ÚÔù*q2–‚Ç{öìaÀ€ ›ð7ß|S½øâ‹Üî—”d hœéã,æ¿x3pÕõ°ø ɯŸ¿Îw ΚÝRnvÃóH~ûÖæ O„ãGˆ~Úµ*«`Í6ÉKËÂÏ3(0¸@<»ý ¼–ø”oëõ°­v  ËÇŒñá&ŒïöJ¦]b¸³&ÁÝó†vmùíØ/¹åIf0|ûÁ“Ú¿Þ‘:¸í)•×—‡¾ûÕðÂ!E‚ÜLZE!J N—ÿ|Oòê§a^‰\×[ ^²ïРá­mvé\Ï(¶so3»ι´_òëV†ð SXÁé¼Ê©Ì¦(jæ8Ÿ¾!ñó–[ضm[Ü'ZJÉüùóí:æwÏ&˜ãôþ¯C×qhÒp½ÿH=\óPˆù~&Ì¿]é2ó í+˜_ªPrn軟Þ+Ù¾_¶»óÿiAˆùÇ €Å÷ î¿Já„ÑšÚ)Y°¦Â)ãOܤð¯;y!Çí‰hù.I#`h÷WеÚNñE÷¢óHá 9ƒBT$nTì¸ðé‚âÒ1‘…©]?šJ.WÒ—gØO£×Ãí×]Ïü×_k7÷hiéÒ¥žë¯¿^yô7`o[xÑ$ÀòR[6ìÃ6¢ÈC–5¼éÍ?ßWƒ†µ©ÃáÎË2c¼\¡Þ¡òê§àpÃÿ-”üÏ5‚–%U“o«,ðg+\6 îž§D­zÈl‚ N ë7<&Y© ­3€—€9 |o=Zè¯_XÙ•A R10€4“ü—GJDæÿŽzîd-óø‚ùl§O¨'¸™PàTpÛM7Å-8ié«oÈóÎ>[/¢lÚë󖨂 G‹x¿Aë~ŒAÑýÃwÿ-{%÷¼:~ðW‚‚œØ >;~ÿ󆕿ßlm½à¾Ú,¹ÛŸ¯wêX¸çŠÎ3¿žFâ~vD<÷lá|V0š%ÜÎj¶Óñ<-8iV¿¬òõšÕL›6__w6l@íd+öÚÚZÞ{ï=Î:õ4~ù«k‚ßOV²¸.k9†ÔA=hJ»¤”—ÚŒÀÕ!ÝßMŠ)|žöÙ%ûý]Õ¬fÍ¿/ÜGpñ)¡ã]C{З›Cß_|zlÇ`P®)Ð:''%€x ´ a§³œûÙA*}M&¦eµ–ç&êËø”Ë.<«ÉÀ“ìã4–ñ‘+:ŸA!p*Suõ1ž|ê)ÆÏÕ%%¼ýöÛlÞ¼™†††V­ËUUeÇŽ|ðÁ<úðÜ2u*çŸ>+¿]<çJÑŸ[r¦g´ŒX˜?»§ˆŠ]åˆbüYŒB ‡õim²¨ÔE;;Ü0ç•ç?ì< Qc˜à#%ì?9:(s6‡þöw´÷5¶?XRRÆî¾»J>úFê%›i½ â \4x†mü/;”iáw\Âä‰C)ÌËÂh4°èÃUÜx÷s8š½Ì>m,¾åÇ Ò‡y?šÁoº˜·ßû’Û|ÿâ+¾àtÆÒ4Æ“ÍëœÖ*8é™ùóyf~¨^ç˜Â"Ξx<4»¨±Wñüæ¶ë‚“N¬T¼zÉ`[w«QGA7m)¹ÞVîvص»áúǵÇ:wŠä“cCÿü¶]p¥@©ùó÷’lÙ~-yã³ðsêüÖ•Õ°Ï¿7¿P)ž§OŒE¹Ÿ#ÈèD>XUEXñ•äÿÅF¢:¦ Ø^qdaþì`¶T,ÒÁðrŸ¿iÎkÏ•2|Hx—ëÿ:o³gLÄÑ䯖eÅh m°ù¹™”\viÖT®¿û°ƒ2ŽxŸÌ\É0fQØfpÒ¦CØôaû†Â–ÁIR³wh.Fw¾žº`ÊKm#éãáE®ˆoØÜNXÆû«àýU2ˆuÁ9'hjBaXRB€ ª$é?øšV¡Ä­ì/~³)‚œÈ%ÐcíÔápò8P y)ŒíŸÜ^íÞ‡k%Ÿ¯'hOhƒúù×½üÞ€Ÿ\þ·Abñ xQƒ…<òr#×°°¦¥bMk» ÕŒ“ÆðwöqHkçÑÁI—0˜õÔò Õ¬äï‡ 7at+C˜ŠAXNF˜»RUàùD‡uˆy‰¡®Ôíº4îÖéÌÍôDôêë}ýwüΛ.Xòµ¤üßmzé`íî® >V3Œ_û!Õã¶ÒucÞ¶µÑÓçëmßvþþ¿¿>]‹úÉb[c±WÀaüuâ=1°2daâ,²XBßíØÏ‰“Zwàñx} J›õýUò'H|pRš*¨‹Ôžøþý|ˆ>nS[3™ª«rPUÇ 7Lpí\زOòí6X¾N²ô›ÖQßl˜1I‹Î?†÷<ó®äëmÚü|ü³7L ËUØ}P²u¬Ý!ùb|±¹ó`óã3`ö ‚É#5õåѝ«ž^ /Ø—rôó¡ (a8KXÅCwþ‹§Ÿ¿…¬,-³Ùåæ±¿¿Ë¢Š¯)>g ¿þï9˜ÍÆVÌ¿äí•ÜÈ ,]p×ꃓºBæp]¨OwRc`<]šw_›§MA?'=$ ÿë#¸órÈ´BZ*L.˜4~q®ÀÙ Uu’õÐàÔô÷êzM×MÏÏÖÂq³ÓÁ–yY½°çõ;ó‰IDATÁ»:±~€®ÔãyÓàõåðéZm8ã‡h qÑiŸêPçÔ6jîCG³fDllÒÆ™¦Ý3Ó ¹™—-¤›J;ìª ¢ÿš¤ Q0ÄÕ#{÷L ù…¼¸{?WýôA.)9cš™7_ùŒwVi0¾a~vâ’+Ïf`ÿ|E°ÿ` ïük¿ó9V?…­'”ZT™êHpöÐ÷vHÄW›3Ò¼mª…9pÎdøàmþj³äÌ{i©00U0ð(ª¶mØ ËÅj†!E¡û\0]ðúrÍ%ùÍVÉŒãB34ïANF×UÔ›¤JÈ›öŸ$h tq*±ÙÌR1ð&!ø–ödÉÝ¡^ sçÎåÆoäOø#Ï~üÏ~܈ûbàyNdD7j[äDtˆÚÜC×Ëùó¼íÚ®þàdÞ?KN/°¤Äv@/<úzhÝõ AŠN.9i\H)_,9qŒ Õ›{WÕÁ­OJ4UÁ½ ⬑æ0ÄNšÍÁÌ_™Â´²ÚV‹óÒK/1sæLÞZ¼ˆ g Éh§’ÎãŒg³˜Jn·Mt  paþì”ðþEs¢Gy©-ͯPÕq/“É#ƒò´Ïï¯ÒrùcMïrZÍ0ç¤ðݼM+8ðÎJXôylÆàòž^,}GA5êU"Wwú^@Ð֤Ș*´fFø«YO·qæìsƒ…=srrøÙÏ~ÆÐZŸÊ9•ÅÌäç ¡îmTÑ"PÎ íæ/'²×\´330fEAm™õ‰r3áþ«B Yò°äãoc·Öí\ö@èz^/è›§鸽ZíA! ä%˜Â[ò°déꣃӥPöŠ^‘·­›1IÐhk`K É[äoô÷džiÃТ2¤Ç㡦Éé—³M=¦`»¹uZt{))‰ŒˆÖJŒs/ÌòEÝý÷ì©‚Ëf…Ž/ý³¤âKyÔykwȰ:%眓‡ká^R9ï6•«TY±QªE¹ðô­¡ù¿è.ÉËK%]I÷¨mT¸ã…‡^uëÁú§Àþ$ø©Ø^a*jˆiŠ-xý¤)B-?ÇÃC‡üÛ¬©ÇL´­LîýzÈТmŒžÊÏòF}q³ î»JáçgúUB7üø^Éÿ-”8]¬ÓÏUHN»I²Áoj>}üþ2EÀϪÜ÷²æ×ã3˜}»T6ï‘rÖdÁ+½k‘ÜóœÊÞÃÑ‘ªÂÆ]’ß<îåŸï‡ª€y¼áQ´¶h½’ŒqD–M>8 Á(É‹áµÓü›VmŠÒ*†Ôn·Óèr1c§ª%m%èöDïDú’£Ý&E—´ÎmYV-¿Á¡²Ð_¼ùwó%ï®”Üð_‚F líÀO…šøv›äñ…á¥À&‚¿Ý¨ý~ã®Öõû®{D6ÜøCÒO#”wþ¬I!7<ò<ò–äÊÙ’ O Óý¡ÁŽh.Êõ;aÙjÉËŸ„®Y WüÀʈ)Ÿ~´ºš$èhaþì™>BÉö͆خç/¥Û7ãt:Ã:ô~û©Vó©˜~˜{P²£R¤À)‚sÑžñ­)ÁCk—ü•‚± uΉD¶Lxò…qoªüå%í»åaùF‰Õ,¹èT8ý8Aÿ|Ívš}ë>X±IòÖç{üö' Uu’û_”ümaä{¯ÚNæåe‘¼°4tn•#rdb ?À¥g ¶ÊD zÿÖDZ{п“Ìo–§˜R½‘,WìAru\Èrª"ðÊ â.&`èa=¿¶yh¡ÀåÅöŠç#WÇœÐÅ2ɧY,ÚÖp9° Ã"9wòÑÁ­shÁAóß“,þ²ãó­f˜1æ+8i¼·a§ääµùüë/匓Æð—ðö_3lpn.¹€ÓO‚O¿Üì˜{Õ#Ö<+ÞÍ/(F“AS/݇${kŒ½~GЦŽÒ”†öÕîépxoUX,Éåþÿ³Õhy0‡€ê’²êÆï›pc€ù†; qa~€qdñ)³øš#¼Ï~VRM®c$séß㘿“h›ÈÁGã Šÿ™–Ø'YV8kŠ`æ$Á‘z÷íû%ª_à Œì¯éæE¹¡†!úl½öÿ7ÌÙÿ‹ÎŠ|7_uÇ BQ'?<˜2|P¡Å¿ËÝ-[Р@¿|è—/8¹©WUAÓÀöƒ&·îqñ\è./µUŸïïôD@ˆ,ÌŸ=]¡ËB?G|ë^ôÅÂ…ôçBúãFEA`콚»ËÍK vNI‰±GÇ`€‚(Èƾ:‡6–¢Û~Ðt¾ƒöZ~Wö2 ?Ô¢A/ù†k.;‹~}l¼÷Ÿ5.ÀrÑÉ4ZSÛUÁpyÔ˜9Xmä@­oçÍf´ºCШ奶7€²’²êÇ,ÌŸ-ÐÊ\[@Ë}YŸØ¢7æ^`‹‘Ñëù¢‡@Тˆž“ì6v°æûû_Ûèlö:ÍÆŸú7Žf/—ÌA³×ÀÂ/eÑÒµŸX®¿HÔD²ÁøTÁáZ»›©¬6Få4%™‰¢Hª íµ3·úÕ…Ÿ•—Ú^JKʪ÷ÌÎ ‰þFLê1±Ç“ÚS¤] G4ÈTRÍ=f/¸âl)çØœvë}¯¿¿þ—5üå¯ÿÌ0™LÜw÷ÞWfT„à eõßn©SG‰°^pŸ`çÁT6Wšqy"¯[E›U%?ËK–ÕG¦ÅGºÅ‡!B®‹×'p¸ Ô; ¨6±¿Ú€'äÿ5—sÊKm¿))«žß«`aþlp_àØæ:•${G uQÚKøId{ðhRc2ô;^O!K fŒ¯gDQ˜Ô?ø°¼ÔfëM6€r¼‚tOÛ»¿PkTƒÛ›¯uä2Hvdø0«‚! ”c'\á"¤·=&Ïb‘§Ž9=i ÀãíÙÆÝ_ž'˜6¶ì³‰§M™Ùé\äò(|¼>:}*ǪrÂ'Ѥ6Çš„€IC˜ilؤ‰~˜YRVÃ.ÃùÂüÙ}ЕˆØÔþîïRdXŽkjs¾;ÝÇA“Êžv‹Ú#•O=>Åx<áHv˜ž[õ'ÝU_ϨZû¯‹gæœ,Æf§ÓßíUødC8óÊ÷2sbC·0¿žÆt2iH˜Óg2ð¯òR›Ò£øq@4KÈé âÏ¥ùhÓM¨¿Œ;Ûÿa‹ÊwY^í¸ìL÷²=Õdžt/5]ŒƒoÇǪb{…Ú‹ Øç§¡©÷Ùn¾Új¥Ö÷ønNÙÑ…×4¢oã†Ù„/îèép^àC¾G¡£ø½êhõµ-FútŠw,A“Q²1ÝË~³Ê挶w¯nì5)][4-l[£øI¢¶¦h&yOÈ»öZ ‘ÞTÁáºúŽàfÌgófŒé蘭ͧŸÙ;ËKm'ôHðûþƒé¾¹QÄûëwóÙöô7éþdŽóÙh •+kkÒuzoCÝA-j#néAk/šš€ß%4 ÉÝ{º`Éø.ÒR$ãº;ÐÙ#Ç)Lá)&P ÍÀË奶¸V±íªð8 '°}dEaÖýÚ“¼ô}â-è s)íÜ˪€FC×P©1ü¡×Gñ“žÔd·ßnQ ©ÒR|½FômbDߦ?N³Qå¤QNã²õÖ@k½¡@ðßíý®¼Ôf&ø×Ì7%eÕQK]U‚塬ªÀ£¥Úl/ ï5¦×DÚSíSuàïwè6HšÃàëh¦£§,Ì’²j êë½Gèm”Ÿå1.ð®Õ}uUy©-b3˧Km¢¼Ôv_EûX ¬)/µ Ž7}ÿ龨íÒúÚ&±fLÉ£³7ÛQKLjx3±ÎV8j 70Ö¡+›ÞK$Ðu½‰58¸¢èçh6°v—•ƒ5æï%LægP‚½ìÀßümÙZoîþècƯ——Ú ñ€`2Ej”Lª7¶µƒÖë%Ãk³'JµÄ ëùtV¨ O ùªØ^áë…ëry ¨œ„Syu:ƒvQ+·Xù®ÒÄòMi8š¿Ò†A‘Æé#õÖ0N~ÔBì¿ ¸½KL~OÈ‹Ft£u@áhƒÛªuQX9 8m´/}_'SbkÂu¤e=l½¥u| õâ«i윰õ€¶›»½Zqö({/%ÔÆ1·¤Þiäóͼµ"›…+²ørK:u=$—¥o®«°O¶O¯þ¥¼Ôfô3ÿLt x9é*?8¡ž¹aªq<Àß„‘¾Ex³"i)88Rß=—Üä èÛuy:ÁÔJ'œHnƒlé9ø¤+øÏiˆÒpįg°¿:zñܧ êuA8iì¶t•xÛךYòm:•G x}ZJðn»‘%kÓÙW•Ò#@à¸!M©º¢×ÃËÊKm™À|ü18ÖÉic±˜U䇅O&˜¨«\ÖiýÔâÁ›I :5ü{¬!©8]X=ñ_ÿzÕ¼=¼‘€ª3OvF¨5‡6kôLÔ%ÊÛníĹ¯>ì¶£ŽpêÜÄŠ€4sûP¦®ìx<Í>Ûœ±x‡ªÂŠ-*t?d¦y˜¶«ßŽVukh®Ã“F;‚ù 9éÞ–jzJ¼ :´sF©×H-`(@ÛÓ|¸üVÿýVtNÿM‰ÑûôR‰£#€W‘a¹ü±º%Åö o€ÎôL{ €R“[PUoŠBïÓl”ÖÐivÇ~#Xµ--¨f pÊè&NÓ,w&%|¹ÅB½³ûÕÑýÜA£€ëã¸Ã˜>B"ÓàxÀ¡à ê7 r„4é&E²Âæá³<7ßé?Ã'(LP¸©^ÊhVd›ŠNchWw v$þW…ëÿ¯tbxu ZcQs˜¿„U°Bÿ{t»¤W§jY¢¨(”ª[ÈÎgÚëLÒEž4ÊIß\}m.fNhÄì_^¾Þ–ÖíQ™i^úåúZñ¬5E¶Šm~ «3Ò]Wg7ÉÖ™î¿V`ˆÎª«¶ R¤`\½‘D•Kõ ¬:kv[ÉGU©¡eyEÔS•ªW¨£s-¤ ÜÙÙ~J¯¸½Qéæ8-µãÛ)Š^ˆíÃ~WxŠr|ôÉ Ý Ãâcú¨SiPØu¸{›ÊŒ(jŽ`hަH‰ˆ¬ i ­ zíJ Æ8Œ´Üò¼ SkŒaby"¨P·ÃìJõQßb`N£dŸ.")Ïý”í ÿ_)¶Wt&5Qqðwü¶ |*l?Ð1ƒ4éÄxc 8" bgâ2p &´К± ³Ý )aïºÝ)Q\<)7Ó”Lò2UúÚbS6²«Oö9þq¨íd†\§ÂIGÌœPkbJ‰“«ML¨1Æ=ð'õs‚`¤«³ö¤ø‚Æ?Œn4DmÜÞ@ó£b{ÅúN-QE"»n÷þÂ¥¯èP HÕxÕ(–оì˜1†}%÷ØCX78ß‘‰Æ´qB¼³u¿™æn¬„´yŸ%ø9?S%/ËÓ؆Š—ðFð.&·¡÷–îÉv)WßZ- A‡1ê:†GRUêÃçãnŽ!òÇ<­`ÒÍECsجS³2Rc³®Ü^V ONûø:´OsÐ`éUµHÆî †&•Õ!©stÿæ6•¢aÖ5ñ€×ð‹TÊ4µW/êl—‰ÕfF; ôñ(äzúxF: œTmŠºÔ¹*`»5l÷ÿ˜è}ÿ-%ºDPW}®ãoŒéñ ¶îo›AÌ:OˆÃÕq=:š•£ªP%t­‚,™iÞΗŒî’¶0…y3§¶¤ç+ÛªR˜Ó¶T¯¯n,.À7(¶W4êw½©¾–uïzU(rSkdb‘1µFú9 JK®´úô1ø]±½¢+“¨|.­ê’²ê*à±Àñ¦}fÚˆßÐöx¼‚†¦öýëûŽ„þnËðÆ$3Æ7rú8'§ŒilSü×Ó üæ` oWt*ú1V¤w‰Žàj÷eéë4i&-øh›¿Ä °%ÓÇ÷™œFÉKؼPl¯ø¼‡ûhBÞîötÏUÛ¬ˆ§˜Trtá½zc\+™µÑý—Ÿ;SHŠI¥°5þMFÉ‚ÐýwN< *hæäÑMœ>ÎI¿Üö-ÿú )!°Gsý£€b{E5ðûÀq•Qe¿õû ª€Í™^½ïî0ð›^0tËQØœÀMc{½Â¶ý‘/7XÇH›÷™iŒåçSkv„r“ò2TÒS»w= È ‰Ü‡ê aö‰„ˆgúåº(Ìî8 ¢Q'¹½bmÜÀOGçØ’æ£.EýÞÀŽ oX2pC±½âÈQ\2QAŽ*¬úMàåÀñºÝ)²1‚*0(¿™TSȨöñz-é¦É­ÐìV8Xcæ“õT5„–ä¸AÝ_Å–á ¥„ª:S]ƒ5Ž0vÞžðë·¿ðïxH`C†¦ïQãƒi*{ÃAïÅb{Å+GyÙ£V~£|é1˜‚ëªD¬ÜbUÕ¾“Q2yXˆ¡.ÁßYXüU&‹¾ÊäÓiaÌ?¢ÈCA–»Ûß­ZÄ ^Eé‰$%Ô‡‡ÐoH”@±½bZÁ7€KH¾ÉöÐü=*‹ÊæðÚòk€«ât»ÚN-ÞèN;jÅÖï 6‰©nT”õ»[—è—ëâ„áÍ-ãÕ[ÑȾ&qô˜w¬O¸©ë¡o›=JËÄ©5 ?|‚Ö(T­vÞA˱­ Ø-*ëÓ½úö0·Ø^‹ ”‘ZDÅ£[L¬äì7ÐòÔø®ÒÄÞyõƒ ›9{R#ƒ ¼Áì;вäz™5ÁÁqC‚D")]·Ž{ x}‚z§‘݇Sùzk:KÖ„®.)«ÞÍub*ÏÛ+^[˜?{:p3t¾v^o¢J«­i>=ó7Û+öÄQ7ˆ!KEtg,.RRV-ËKm×¢•£š°r‹E¦˜¤h)Êg¦y9aD#ªŸß·n0ÈÅôa/B—¿àNP®ŠTUàö œÍ.ÍVRç4PÓ¨ÐØ¬D¬gà§¢½O<µÕ‡cˆ|¶gz© oZPü Ø^ñE oµ]õe½:ãGÊŽÕ…Jʪ]奶¡åŠäJ‰øtCš/Ã’ª„AÑ‚ÍF‰`Zæ_ŠIbô€"$-zÐl”(Š–È4*Ú¹FƒŒÊ—ßSôs·WÁã¨RàSµc·WàS¯ É­àõÃ¥àö\³óMXš…è:vw:©ƒöª7K6gx¥C ³pfÛ+ÖÆøvC€ãüXëcy±’²ê-奶b` ¢J ábsìEhƒî’f£Ä hb0èÖ D‰ ]£­ø}6£O…O7fë“€ZÃ[U“ÜPýß©ñß¿>C‹8]â¯Ù@w@˜ÓYr${­>LR߬æé~i2Jv¦ûj™Ôì¸ö%pI±½bg¬ï™Å" ÜTÇœ#h¹]ᵃß} <ë±—”U/÷ƒÀs@~Ü¥25³ÆgÝH k^©¸-àj?š{o=ZŸ†Õ%eÕGeoŠù,-ÌŸý0þ˜BÂØÚÎaÌ›‡gú}\ 9îăAƒY²'ÍçÂ(iþßgê$"Ùb4µÑbMþùt¿—hžš‹Ö¥cr¯ÿs5`×{¦x1tO€ÃzƒY§õ9Ùöòv É!“ä Hó!€TU¦ RTíÍý]||B«ßïR´¾|Ö\ÕfÉøšŽû Em‰d¿OÛ+\‰~QY,: ü¨Ž9gNêÂeª€ßJx.[S1z<ùZ¼þ3Òó$©‡@•ža;mÒ®ÓÿäGùó‘‘l MФ©}Þk£lîi d[ ÌŒ‘ÿhà#à£:æœüÊ/tÏ¿­–ßÓY,ªO.û$%š•΀*Âó×ÅöŠÅÀoæÏÌDk“4­D²èÂõ£"«Wèóärzó·‚Ï€Ïê˜s­>& 5Ì_“Ÿñ—«²zÉŽŸ¤Þ›¼B+Ý™Z2|7 îVÅöŠíhÐò…ù³…_¯å_øE@¿´`öƒ-8g_o»¢O²oQœ´ÏÂüÙ)Ý!úGh.·%É圤n€b{Å–…ù³]@ŠDË‘<šÄ 7ÄìmãÍjúÿ_»´0vt½Ô\ÙaÂð82ý€ãJ.™$K¯Àæ SÖš£—<›Ãá¨Añ(éˆÐe×¹£p$¥´ùÌI.—$% :ú0ðáY:§³1¼µQe±½"&)aÅöŠ&E†2éš ÑÕ¦áCßärIR¢£…øý¬NEš¢”jMaç-ñƒn |vDŸ¦¬´À„$%) QÐt ö¤ulzó({x„ÎÒXHÕUæ­7Fç ”áL$¹\’”€èDn ”Ž›TŽtÐ<¤Òªê‡jüRDÌÈ'ø (i;ngæií´'—K’’=½Œ–lÀÆto›µëÌ*»Ã‹?>]l¯ˆuÀÊJá·â«@M€äWGê‰O1Ž$%騀b{…˜‡?„Ó+`u¦—-Y^jRTšŒ‡I²/݈¹P•^zˆáxÑÅ¥WvPm¶>6÷Ä€$%©'KÛ+Ö?Ä_+P•f•5™^Växø2ÛÃV‹OÏü^à—þrã1'©õ´ Ú([už'`¸´òIr©$) ]w3]œZ\\l¯XÇá|„®µùÆ oÄž†»Ò}úüPž\*I:)a®­…ù³ÓÑʇÿ˜Œò«¢eÙ½