diff --git a/.travis.yml b/.travis.yml index da3107b..5ccf2b6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,3 +6,4 @@ go: install: - go get + - go get golang.org/x/net/html diff --git a/README.md b/README.md index a13e08d..515d35c 100644 --- a/README.md +++ b/README.md @@ -8,25 +8,7 @@ [![Patreon](https://img.shields.io/badge/support-patreon-E66500.svg?maxAge=2592000)](https://www.patreon.com/toby3d) [![discord](https://discordapp.com/api/guilds/208605007744860163/widget.png)](https://discord.gg/fM4QqmA) -## Available features -### [Methods](http://telegra.ph/api#Available-methods) -- [x] createAccount -- [ ] createPage -- [x] editAccountInfo -- [ ] editPage -- [x] getAccountInfo -- [x] getPage -- [x] getPageList -- [x] getViews -- [x] revokeAccessToken - -### [Types](http://telegra.ph/api#Available-types) -- [x] Account -- [ ] Node -- [ ] NodeElement -- [x] Page -- [x] PageList -- [x] PageViews +All [methods](http://telegra.ph/api#Available-methods) and [types](http://telegra.ph/api#Available-types) available! Yaay! ## Start using telegraph Download and install it: @@ -35,6 +17,53 @@ Download and install it: Import it in your code: `import "github.com/toby3d/telegraph"` +## Example +This is an example of "quick start", which shows **how to create a new account** for future pages, as well as **creating a first simple page** with the name, picture and signature: +```go +package main + +import ( + "github.com/toby3d/telegraph" + "log" +) + +// Example content. Not abuse tags, okay? Be easy, bro. +const data = `
+
Cat turns the wheel? Pretty weird... But cute.
+

Hello, my name is Page, look at me!

` + +func main() { + // Create new Telegraph account. Author name/link can be epmty. + // So secure. Much anonymously. Wow. + acc, err := telegraph.CreateAccount( + "toby3d", // required for assign all new pages (invisible for others) + "Maxim Lebedev", + "https://telegram.me/toby3d", + ) + if err != nil { + log.Fatal(err.Error()) + } + + // Boom!.. And your text will be understandable for Telegraph. MAGIC. + content, _ := telegraph.ContentFormat(data) + + newPage := &telegraph.Page{ + Title: "My awesome page", + Content: content, + + // Not necessarily, but, hey, it's just an example. + AuthorName: acc.AuthorName, + AuthorURL: acc.AuthorURL, + } + + if page, err := acc.CreatePage(newPage, false); err != nil { + log.Print(err.Error()) + } else { + log.Println("Kaboom! Page created, look what happened:", page.URL) + } +} +``` + ## Requirements - [fasthttp](https://github.com/valyala/fasthttp) - [net/html](https://golang.org/x/net/html) diff --git a/account.go b/account.go index 9753507..6694943 100644 --- a/account.go +++ b/account.go @@ -7,29 +7,27 @@ import ( "strings" ) -// CreateAccount create a new Telegraph account. Most users only need one -// account, but this can be useful for channel administrators who would like to -// keep individual author names and profile links for each of their channels. -// On success, returns an Account object with the regular fields and an -// additional access_token field. +// CreateAccount create a new Telegraph account. Most users only need one account, but this can be +// useful for channel administrators who would like to keep individual author names and profile +// links for each of their channels. On success, returns an Account object with the regular fields +// and an additional access_token field. func CreateAccount(shortName string, authorName string, authorURL string) (*Account, error) { var args fasthttp.Args + // Required. Account name, helps users with several accounts remember which they are currently + // using. Displayed to the user above the "Edit/Publish" button on Telegra.ph, other users + // don't see this name. args.Add("short_name", shortName) - // Required. Account name, helps users with several accounts remember which - // they are currently using. Displayed to the user above the "Edit/Publish" - //button on Telegra.ph, other users don't see this name. - args.Add("author_name", authorName) // Default author name used when creating new articles. + args.Add("author_name", authorName) + // Default profile link, opened when users click on the author's name below the title. Can be + // any link, not necessarily to a Telegram profile or channel. args.Add("author_url", authorURL) - // Default profile link, opened when users click on the author's name below - // the title. Can be any link, not necessarily to a Telegram profile or - // channel. url := fmt.Sprintf(APIEndpoint, "createAccount") - body, err := request(nil, url, &args) + body, err := request(url, &args) if err != nil { return nil, err } @@ -42,28 +40,26 @@ func CreateAccount(shortName string, authorName string, authorURL string) (*Acco return &resp, nil } -// EditAccountInfo update information about a Telegraph account. Pass only the -// parameters that you want to edit. On success, returns an Account object with -// the default fields. +// EditAccountInfo update information about a Telegraph account. Pass only the parameters that you +// want to edit. On success, returns an Account object with the default fields. func (account *Account) EditAccountInfo(update *Account) (*Account, error) { var args fasthttp.Args - args.Add("access_token", account.AccessToken) // Required. Access token of the Telegraph account. + args.Add("access_token", account.AccessToken) - args.Add("short_name", update.ShortName) // New account name. + args.Add("short_name", update.ShortName) - args.Add("author_name", update.AuthorName) // New default author name used when creating new articles. + args.Add("author_name", update.AuthorName) + // New default profile link, opened when users click on the author's name below the title. Can + // be any link, not necessarily to a Telegram profile or channel. args.Add("author_url", update.AuthorURL) - // New default profile link, opened when users click on the author's name - // below the title. Can be any link, not necessarily to a Telegram profile - // or channel. url := fmt.Sprintf(APIEndpoint, "editAccountInfo") - body, err := request(nil, url, &args) + body, err := request(url, &args) if err != nil { return nil, err } @@ -76,21 +72,19 @@ func (account *Account) EditAccountInfo(update *Account) (*Account, error) { return &resp, nil } -// GetAccountInfo get information about a Telegraph account. -// Returns an Account object on success. +// GetAccountInfo get information about a Telegraph account. Returns an Account object on success. func (account *Account) GetAccountInfo(fields []string) (*Account, error) { var args fasthttp.Args - args.Add("access_token", account.AccessToken) // Required. Access token of the Telegraph account. + args.Add("access_token", account.AccessToken) - args.Add("fields", fmt.Sprintf("[\"%s\"]", strings.Join(fields, "\",\""))) - // List of account fields to return. - // Available fields: short_name, author_name, author_url, auth_url, - // page_count. + // List of account fields to return. Available fields: short_name, author_name, author_url, + // auth_url, page_count. + args.Add("fields", fmt.Sprintf(`["%s"]`, strings.Join(fields, `","`))) url := fmt.Sprintf(APIEndpoint, "getAccountInfo") - body, err := request(nil, url, &args) + body, err := request(url, &args) if err != nil { return nil, err } @@ -103,18 +97,17 @@ func (account *Account) GetAccountInfo(fields []string) (*Account, error) { return &resp, nil } -// RevokeAccessToken revoke access_token and generate a new one, for example, -// if the user would like to reset all connected sessions, or you have reasons -// to believe the token was compromised. On success, returns an Account object -// with new access_token and auth_url fields. +// RevokeAccessToken revoke access_token and generate a new one, for example, if the user would +// like to reset all connected sessions, or you have reasons to believe the token was compromised. +// On success, returns an Account object with new access_token and auth_url fields. func (account *Account) RevokeAccessToken() (*Account, error) { var args fasthttp.Args - args.Add("access_token", account.AccessToken) // Required. Access token of the Telegraph account. + args.Add("access_token", account.AccessToken) url := fmt.Sprintf(APIEndpoint, "revokeAccessToken") - body, err := request(nil, url, &args) + body, err := request(url, &args) if err != nil { return nil, err } diff --git a/content.go b/content.go new file mode 100644 index 0000000..5630b10 --- /dev/null +++ b/content.go @@ -0,0 +1,95 @@ +package telegraph + +import ( + "bytes" + "errors" + "golang.org/x/net/html" + "strings" +) + +var availableTags = map[string]bool{ + "a": true, + "aside": true, + "b": true, + "blockquote": true, + "br": true, + "code": true, + "em": true, + "figcaption": true, + "figure": true, + "h3": true, + "h4": true, + "hr": true, + "i": true, + "iframe": true, + "img": true, + "li": true, + "ol": true, + "p": true, + "pre": true, + "s": true, + "strong": true, + "u": true, + "ul": true, + "video": true, +} + +var availableAttributes = map[string]bool{ + "href": true, + "src": true, +} + +// ContentFormat transforms data to a DOM-based format to represent the content of the page. +func ContentFormat(data interface{}) ([]Node, error) { + var doc html.Node + switch dst := data.(type) { + case string: + dom, err := html.Parse(strings.NewReader(dst)) + if err != nil { + return nil, err + } + doc = *dom + case []byte: + dom, err := html.Parse(bytes.NewReader(dst)) + if err != nil { + return nil, err + } + doc = *dom + default: + return nil, errors.New("invalid data type, use []byte or string") + } + + var content []Node + content = append(content, domToNode(doc.FirstChild)) + + return content, nil +} + +func domToNode(domNode *html.Node) interface{} { + if domNode.Type == html.TextNode { + return domNode.Data + } + + if domNode.Type != html.ElementNode { + return nil + } + + var nodeElement NodeElement + if _, ok := availableTags[strings.ToLower(domNode.Data)]; ok { + nodeElement.Tag = strings.ToLower(domNode.Data) + for i := range domNode.Attr { + attr := domNode.Attr[i] + if _, ok := availableAttributes[strings.ToLower(attr.Key)]; ok { + nodeElement.Attrs = map[string]string{ + strings.ToLower(attr.Key): strings.ToLower(attr.Val), + } + } + } + } + + for child := domNode.FirstChild; child != nil; child = child.NextSibling { + nodeElement.Children = append(nodeElement.Children, domToNode(child)) + } + + return nodeElement +} diff --git a/page.go b/page.go index 32d507b..d8466a3 100644 --- a/page.go +++ b/page.go @@ -7,32 +7,40 @@ import ( "strconv" ) -/* // CreatePage create a new Telegraph page. On success, returns a Page object. func (account *Account) CreatePage(page *Page, returnContent bool) (*Page, error) { var args fasthttp.Args - args.Add("access_token", account.AccessToken) // Required. Access token of the Telegraph account. + args.Add("access_token", account.AccessToken) - args.Add("title", page.Title) // Required. Page title. + args.Add("title", page.Title) - args.Add("author_name", page.AuthorName) - // Author name, displayed below the article's title. + if page.AuthorName != "" { + // Author name, displayed below the article's title. + args.Add("author_name", page.AuthorName) + } - args.Add("author_url", page.AuthorURL) - // Profile link, opened when users click on the author's name below the - // title. Can be any link, not necessarily to a Telegram profile or channel. + if page.AuthorURL != "" { + // Profile link, opened when users click on the author's name below the title. Can be any + // link, not necessarily to a Telegram profile or channel. + args.Add("author_url", page.AuthorURL) + } - args.Add("content", page.Content) - // Required. Content of the page. - - args.Add("return_content", strconv.FormatBool(returnContent)) // If true, a content field will be returned in the Page object. + args.Add("return_content", strconv.FormatBool(returnContent)) + + content, err := json.Marshal(page.Content) + if err != nil { + return nil, err + } + + // Required. Content of the page. + args.Add("content", string(content)) url := fmt.Sprintf(APIEndpoint, "createPage") - body, err := request(nil, url, &args) + body, err := request(url, &args) if err != nil { return nil, err } @@ -44,34 +52,41 @@ func (account *Account) CreatePage(page *Page, returnContent bool) (*Page, error return &resp, nil } -*/ -/* // EditPage edit an existing Telegraph page. On success, returns a Page object. -func (account *Account) EditPage(update *Page, returnContent bool) (*Page, error) { +func (account *Account) EditPage(page *Page, returnContent bool) (*Page, error) { var args fasthttp.Args - args.Add("access_token", account.AccessToken) // Required. Access token of the Telegraph account. + args.Add("access_token", account.AccessToken) - args.Add("title", update.Title) // Required. Page title. + args.Add("title", page.Title) - args.Add("content", update.Content.Data) - // Required. Content of the page. + if page.AuthorName != "" { + // Author name, displayed below the article's title. + args.Add("author_name", page.AuthorName) + } - args.Add("author_name", update.AuthorName) - // Author name, displayed below the article's title. + if page.AuthorURL != "" { + // Profile link, opened when users click on the author's name below the title. Can be any + // link, not necessarily to a Telegram profile or channel. + args.Add("author_url", page.AuthorURL) + } - args.Add("author_url", update.AuthorURL) - // Profile link, opened when users click on the author's name below the - // title. Can be any link, not necessarily to a Telegram profile or channel. - - args.Add("return_content", strconv.FormatBool(returnContent)) // If true, a content field will be returned in the Page object. + args.Add("return_content", strconv.FormatBool(returnContent)) - url := fmt.Sprintf(PathEndpoint, "editPage", update.Path) - body, err := request(nil, url, &args) + content, err := json.Marshal(page.Content) + if err != nil { + return nil, err + } + + // Required. Content of the page. + args.Add("content", string(content)) + + url := fmt.Sprintf(PathEndpoint, "editPage", page.Path) + body, err := request(url, &args) if err != nil { return nil, err } @@ -83,17 +98,16 @@ func (account *Account) EditPage(update *Page, returnContent bool) (*Page, error return &resp, nil } -*/ // GetPage get a Telegraph page. Returns a Page object on success. func GetPage(path string, returnContent bool) (*Page, error) { var args fasthttp.Args - args.Add("return_content", strconv.FormatBool(returnContent)) // If true, content field will be returned in Page object. + args.Add("return_content", strconv.FormatBool(returnContent)) url := fmt.Sprintf(PathEndpoint, "getPage", path) - body, err := request(nil, url, &args) + body, err := request(url, &args) if err != nil { return nil, err } @@ -106,22 +120,22 @@ func GetPage(path string, returnContent bool) (*Page, error) { return &resp, nil } -// GetPageList get a list of pages belonging to a Telegraph account. Returns a -// PageList object, sorted by most recently created pages first. +// GetPageList get a list of pages belonging to a Telegraph account. Returns a PageList object, +// sorted by most recently created pages first. func (account *Account) GetPageList(offset int, limit int) (*PageList, error) { var args fasthttp.Args - args.Add("access_token", account.AccessToken) // Required. Access token of the Telegraph account. + args.Add("access_token", account.AccessToken) - args.Add("offset", strconv.Itoa(offset)) // Sequential number of the first page to be returned. + args.Add("offset", strconv.Itoa(offset)) - args.Add("limit", strconv.Itoa(limit)) // Limits the number of pages to be retrieved. + args.Add("limit", strconv.Itoa(limit)) url := fmt.Sprintf(APIEndpoint, "getPageList") - body, err := request(nil, url, &args) + body, err := request(url, &args) if err != nil { return nil, err } @@ -134,38 +148,36 @@ func (account *Account) GetPageList(offset int, limit int) (*PageList, error) { return &resp, nil } -// GetViews get the number of views for a Telegraph article. By default, the -// total number of page views will be returned. Returns a PageViews object on -// success. +// GetViews get the number of views for a Telegraph article. By default, the total number of page +// views will be returned. Returns a PageViews object on success. func GetViews(path string, year int, month int, day int, hour int) (*PageViews, error) { var args fasthttp.Args - if year >= 2000 && year <= 2100 { - args.Add("year", strconv.Itoa(year)) - // Required if month is passed. If passed, the number of page views for - // the requested year will be returned. - } - - if month > 0 { - args.Add("month", strconv.Itoa(month)) - // Required if day is passed. If passed, the number of page views for - // the requested month will be returned. - } - - if day > 0 { - args.Add("day", strconv.Itoa(day)) - // Required if hour is passed. If passed, the number of page views for - // the requested day will be returned. - } - if hour > -1 { + // If passed, the number of page views for the requested hour will be returned. args.Add("hour", strconv.Itoa(hour)) - // If passed, the number of page views for the requested hour will be - // returned. + + if day > 0 { + // Required if hour is passed. If passed, the number of page views for the requested day + // will be returned. + args.Add("day", strconv.Itoa(day)) + + if month > 0 { + // Required if day is passed. If passed, the number of page views for the requested month + // will be returned. + args.Add("month", strconv.Itoa(month)) + + if year >= 2000 && year <= 2100 { + // Required if month is passed. If passed, the number of page views for the requested year + // will be returned. + args.Add("year", strconv.Itoa(year)) + } + } + } } url := fmt.Sprintf(PathEndpoint, "getViews", path) - body, err := request(nil, url, &args) + body, err := request(url, &args) if err != nil { return nil, err } diff --git a/telegraph.go b/telegraph.go index c507bca..33e3de0 100644 --- a/telegraph.go +++ b/telegraph.go @@ -4,7 +4,6 @@ import ( "encoding/json" "errors" "github.com/valyala/fasthttp" - "golang.org/x/net/html" ) // Telegraph constants @@ -82,7 +81,7 @@ type ( ImageURL string `json:"image_url"` // Optional. Content of the page. - Content []NodeElement `json:"content"` + Content []Node `json:"content"` // Number of page views for the page. Views int `json:"views"` @@ -112,10 +111,10 @@ type ( // Optional. Attributes of the DOM element. Key of object represents // name of attribute, value represents value of attribute. Available // attributes: href, src. - Attrs []html.Attribute `json:"attrs"` + Attrs map[string]string `json:"attrs"` // Optional. List of child nodes for the DOM element. - Children []NodeElement `json:"children"` + Children []Node `json:"children"` } // Response represents a response from the Telegram API with the result @@ -130,20 +129,20 @@ type ( } ) -func request(dst []byte, url string, args *fasthttp.Args) (*Response, error) { - _, body, err := fasthttp.Post(dst, url, args) +func request(url string, args *fasthttp.Args) (*Response, error) { + _, res, err := fasthttp.Post(nil, url, args) if err != nil { return nil, err } - var resp Response - if err := json.Unmarshal(body, &resp); err != nil { + var tResp Response + if err := json.Unmarshal(res, &tResp); err != nil { return nil, err } - if !resp.Ok { - return nil, errors.New(resp.Error) + if !tResp.Ok { + return nil, errors.New(tResp.Error) } - return &resp, nil + return &tResp, nil } diff --git a/telegraph_test.go b/telegraph_test.go index 25f2119..9894224 100644 --- a/telegraph_test.go +++ b/telegraph_test.go @@ -1,41 +1,45 @@ package telegraph -import "testing" +import ( + "testing" +) var ( - demoAccount = &Account{ - AccessToken: "b968da509bb76866c35425099bc0989a5ec3b32997d55286c657e6994bbb", - } - demoPage = &Page{ - Path: "Sample-Page-12-15", - } - demoContent = `

Hello, world!

` + demoAccount Account + demoPage Page + demoContent = `

Hello, World!

` ) func TestCreateAccount(t *testing.T) { - newAccount, err := CreateAccount("Sandbox", "Anonymous", "") + acc, err := CreateAccount("Sandbox", "Anonymous", "") if err != nil { - t.Error(err) + t.Error(err.Error()) } - t.Logf("New account created!\nAccess Token: %s\nAuth URL: %s\nShort Name: %s\nAuthor Name: %s\nPage Count: %d", newAccount.AccessToken, newAccount.AuthURL, newAccount.ShortName, newAccount.AuthorName, newAccount.PageCount) + demoAccount = *acc + t.Logf("New account created!\n%#v", *acc) } -/* func TestCreatePage(t *testing.T) { + content, err := ContentFormat(demoContent) + if err != nil { + t.Error(err.Error()) + } + newPage := &Page{ Title: "Sample Page", AuthorName: "Anonymous", - Content: demoContent, + Content: content, } - demoPage, err = demoAccount.CreatePage(newPage, true) + page, err := demoAccount.CreatePage(newPage, true) if err != nil { - t.Error(err) + t.Error(err.Error()) } - t.Logf("%#v", demoPage) + + demoPage = *page + t.Logf("%#v", *page) } -*/ func TestEditAccountInfo(t *testing.T) { update := &Account{ @@ -44,78 +48,74 @@ func TestEditAccountInfo(t *testing.T) { } info, err := demoAccount.EditAccountInfo(update) + if err != nil { + t.Error(err.Error()) + } + + t.Logf("Account updated!\n%#v", info) +} + +func TestEditPage(t *testing.T) { + content, err := ContentFormat(demoContent) if err != nil { t.Error(err) } - t.Logf("Account updated!\nNew Short Name: %s\nNew Author Name: %s", info.ShortName, info.AuthorName) -} - -/* -func TestEditPage(t *testing.T) { update := &Page{ Path: demoPage.Path, - Title: "", + Title: "Sample Page", AuthorName: "Anonymous", - Content: demoContent, + Content: content, } page, err := demoAccount.EditPage(update, true) if err != nil { - t.Error(err) + t.Error(err.Error()) } - t.Logf("%#v", page) + t.Logf("%#v", *page) } -*/ func TestGetAccountInfo(t *testing.T) { account, err := demoAccount.GetAccountInfo([]string{"short_name", "page_count"}) if err != nil { - t.Error(err) + t.Error(err.Error()) } t.Logf("Account info:\nShort Name: %s\nPage Count: %d", account.ShortName, account.PageCount) } -/* +func TestGetPageList(t *testing.T) { + pages, err := demoAccount.GetPageList(0, 3) + if err != nil { + t.Error(err.Error()) + } + + t.Logf("Total %d pages\n%#v", pages.TotalCount, pages.Pages) +} + func TestGetPage(t *testing.T) { page, err := GetPage(demoPage.Path, true) if err != nil { - t.Error(err) + t.Error(err.Error()) } t.Logf("%#v", page) } -*/ - -func TestGetPageList(t *testing.T) { - list, err := demoAccount.GetPageList(0, 3) - if err != nil { - t.Error(err) - } - - t.Logf("Total %d pages\nPages Raw: %#v", list.TotalCount, list.Pages) -} func TestGetViews(t *testing.T) { - views, err := GetViews(demoPage.Path, 2016, 12, 0, -1) + views, err := GetViews("Sample-Page-12-15", 2016, 12, 0, -1) if err != nil { - t.Error(err) + t.Error(err.Error()) } t.Logf("This page have %d views", views.Views) } func TestRevokeAccessToken(t *testing.T) { - account, err := CreateAccount("Sandbox", "Anonymous", "") - if err != nil { - t.Error(err) - } + t.Logf("Old Access Token: %s", demoAccount.AccessToken) - t.Logf("Old Access Token: %s", account.AccessToken) - - token, err := account.RevokeAccessToken() + token, err := demoAccount.RevokeAccessToken() if err != nil { t.Error(token) }