├── LICENSE ├── README.md ├── constant.go ├── error.go ├── example └── poe_test.go ├── go.mod ├── go.sum ├── models.go ├── poe.go ├── poe_graphql ├── AddHumanMessageMutation.graphql ├── AddMessageBreakMutation.graphql ├── AutoSubscriptionMutation.graphql ├── BioFragment.graphql ├── ChatAddedSubscription.graphql ├── ChatFragment.graphql ├── ChatListPaginationQuery.graphql ├── ChatPaginationQuery.graphql ├── ChatViewQuery.graphql ├── DeleteHumanMessagesMutation.graphql ├── DeleteMessageMutation.graphql ├── ExploreBotsListPaginationQuery.graphql ├── HandleFragment.graphql ├── LoginWithVerificationCodeMutation.graphql ├── MessageAddedSubscription.graphql ├── MessageDeletedSubscription.graphql ├── MessageFragment.graphql ├── MessageRemoveVoteMutation.graphql ├── MessageSetVoteMutation.graphql ├── PoeBotCreateMutation.graphql ├── PoeBotEditMutation.graphql ├── SendMessageMutation.graphql ├── SendVerificationCodeForLoginMutation.graphql ├── ShareMessagesMutation.graphql ├── SignupWithVerificationCodeMutation.graphql ├── StaleChatUpdateMutation.graphql ├── SubscriptionsMutation.graphql ├── SummarizePlainPostQuery.graphql ├── SummarizeQuotePostQuery.graphql ├── SummarizeSharePostQuery.graphql ├── UserSnippetFragment.graphql ├── ViewerInfoQuery.graphql ├── ViewerStateFragment.graphql └── ViewerStateUpdatedSubscription.graphql ├── poe_test.go └── utils.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 li wei 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Golang Poe API 2 | The golang version of https://github.com/ading2210/poe-api, used for the golang project to call poe api 3 | 4 | 5 | The latest commit that is currently compatible is https://github.com/ading2210/poe-api/tree/7470d07b989af596293f24d0bcfc6315161505e0 6 | 7 | # Notice 8 | As a single person, it can be challenging to keep up with the frequent API definition changes made by Poe in the ading2210/poe-api project. Therefore, 9 | welcome those with the necessary skills to contribute by submitting modifications directly via MR, rather than forking the code. 10 | This will help streamline the process and ensure that updates are efficiently implemented. 11 | 12 | # Instructions 13 | 14 | ## install 15 | 16 | ```bash 17 | go get github.com/lwydyby/poe-api 18 | ``` 19 | 20 | ## use 21 | 22 | ```golang 23 | 24 | import ( 25 | "fmt" 26 | "time" 27 | 28 | "github.com/lwydyby/poe-api" 29 | ) 30 | 31 | 32 | func ExampleSendMessage() { 33 | c := poe_api.NewClient("", nil) 34 | res, err := c.SendMessage("ChatGPT", "一句话描述golang的channel", true, 30*time.Second) 35 | if err != nil { 36 | panic(err) 37 | } 38 | // 等待全部返回后 直接返回全文 39 | fmt.Println(poe_api.GetFinalResponse(res)) 40 | res, err = c.SendMessage("ChatGPT", "channel是并发安全的吗", false, 30*time.Second) 41 | if err != nil { 42 | panic(err) 43 | } 44 | // 流式返回 每次返回新增的数据 45 | for m := range poe_api.GetTextStream(res) { 46 | fmt.Println(m) 47 | } 48 | // output: 49 | } 50 | ``` 51 | -------------------------------------------------------------------------------- /constant.go: -------------------------------------------------------------------------------- 1 | package poe_api 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "log" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | fhttp "github.com/bogdanfinn/fhttp" 12 | ) 13 | 14 | const ( 15 | gqlURL = "https://poe.com/api/gql_POST" 16 | gqlRecvURL = "https://poe.com/api/receive_POST" 17 | homeURL = "https://poe.com" 18 | settingsURL = "https://poe.com/api/settings" 19 | ) 20 | 21 | //go:embed poe_graphql/*.graphql 22 | var graphql embed.FS 23 | var queries = make(map[string]string) 24 | 25 | var logger = log.New(os.Stdout, "", log.Ldate|log.Ltime|log.Lshortfile) 26 | 27 | var userAgent = "This will be ignored! See the README for info on how to set custom headers." 28 | var headers = fhttp.Header{ 29 | "User-Agent": []string{"Mozilla/5.0 (X11; Linux x86_64; rv:102.0) Gecko/20100101 Firefox/102.0"}, 30 | "Accept": []string{"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"}, 31 | "Accept-Encoding": []string{"gzip, deflate, br"}, 32 | "Accept-Language": []string{"en-US,en;q=0.5"}, 33 | "Te": []string{"trailers"}, 34 | "Upgrade-Insecure-Requests": []string{"1"}, 35 | } 36 | var clientIdentifier = "firefox_102" 37 | 38 | func init() { 39 | loadQueries() 40 | } 41 | 42 | func loadQueries() { 43 | queryFS, err := fs.Sub(graphql, "poe_graphql") 44 | if err != nil { 45 | panic(err) 46 | } 47 | // 遍历嵌入的查询文件 48 | err = fs.WalkDir(queryFS, ".", func(path string, d fs.DirEntry, err error) error { 49 | if err != nil { 50 | return err 51 | } 52 | if d.IsDir() || filepath.Ext(path) != ".graphql" { 53 | return nil 54 | } 55 | 56 | queryBytes, err := fs.ReadFile(queryFS, path) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | // 将查询文件内容存储到 queries 映射中 62 | queries[strings.TrimSuffix(d.Name(), filepath.Ext(d.Name()))] = string(queryBytes) 63 | return nil 64 | }) 65 | if err != nil { 66 | panic(err) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package poe_api 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type InvalidToken struct { 8 | token string 9 | } 10 | 11 | func (e *InvalidToken) Error() string { 12 | return fmt.Sprintf("Invalid token: %s", e.token) 13 | } 14 | -------------------------------------------------------------------------------- /example/poe_test.go: -------------------------------------------------------------------------------- 1 | package example 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/lwydyby/poe-api" 8 | ) 9 | 10 | func ExampleSendMessage() { 11 | c := poe_api.NewClient("", nil) 12 | res, err := c.SendMessage("ChatGPT", "一句话描述golang的channel", true, 30*time.Second) 13 | if err != nil { 14 | panic(err) 15 | } 16 | fmt.Println(poe_api.GetFinalResponse(res)) 17 | res, err = c.SendMessage("ChatGPT", "channel是并发安全的吗", false, 30*time.Second) 18 | if err != nil { 19 | panic(err) 20 | } 21 | for m := range poe_api.GetTextStream(res) { 22 | fmt.Println(m) 23 | } 24 | // output: 25 | } 26 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/lwydyby/poe-api 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/bogdanfinn/fhttp v0.5.23 7 | github.com/bogdanfinn/tls-client v1.3.12 8 | github.com/google/uuid v1.3.0 9 | github.com/gorilla/websocket v1.5.0 10 | ) 11 | 12 | require ( 13 | github.com/andybalholm/brotli v1.0.4 // indirect 14 | github.com/bogdanfinn/utls v1.5.16 // indirect 15 | github.com/klauspost/compress v1.15.12 // indirect 16 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 // indirect 17 | github.com/zhangyunhao116/fastrand v0.3.0 // indirect 18 | github.com/zhangyunhao116/skipmap v0.10.1 // indirect 19 | golang.org/x/crypto v0.1.0 // indirect 20 | golang.org/x/net v0.5.0 // indirect 21 | golang.org/x/sys v0.4.0 // indirect 22 | golang.org/x/text v0.6.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= 2 | github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= 3 | github.com/bogdanfinn/fhttp v0.5.23 h1:4Xb5OjYArB8GpnUw4A4r5jmt8UW0/Cvey3R9nS2dC9U= 4 | github.com/bogdanfinn/fhttp v0.5.23/go.mod h1:brqi5woc5eSCVHdKYBV8aZLbO7HGqpwyDLeXW+fT18I= 5 | github.com/bogdanfinn/tls-client v1.3.12 h1:jpNj7owMY/oULUQyAhAv6tRFkliFGLyr8Qx1ZZY/gp8= 6 | github.com/bogdanfinn/tls-client v1.3.12/go.mod h1:Q46nwIm0wPCweDM3XZcupxEIsTOWo3HVYSSsDj02/Qo= 7 | github.com/bogdanfinn/utls v1.5.16 h1:NhhWkegEcYETBMj9nvgO4lwvc6NcLH+znrXzO3gnw4M= 8 | github.com/bogdanfinn/utls v1.5.16/go.mod h1:mHeRCi69cUiEyVBkKONB1cAbLjRcZnlJbGzttmiuK4o= 9 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 10 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= 12 | github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 13 | github.com/klauspost/compress v1.15.12 h1:YClS/PImqYbn+UILDnqxQCZ3RehC9N318SU3kElDUEM= 14 | github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= 15 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5 h1:YqAladjX7xpA6BM04leXMWAEjS0mTZ5kUU9KRBriQJc= 16 | github.com/tam7t/hpkp v0.0.0-20160821193359-2b70b4024ed5/go.mod h1:2JjD2zLQYH5HO74y5+aE3remJQvl6q4Sn6aWA2wD1Ng= 17 | github.com/zhangyunhao116/fastrand v0.3.0 h1:7bwe124xcckPulX6fxtr2lFdO2KQqaefdtbk+mqO/Ig= 18 | github.com/zhangyunhao116/fastrand v0.3.0/go.mod h1:0v5KgHho0VE6HU192HnY15de/oDS8UrbBChIFjIhBtc= 19 | github.com/zhangyunhao116/skipmap v0.10.1 h1:CMH4yGZQESBM1kUNozQqQ+Ra2pKqwF3HxaTADOaIfPs= 20 | github.com/zhangyunhao116/skipmap v0.10.1/go.mod h1:CClnLPHl3DI+hHgrcy0OZ/QJ45AWgA3ObVcQyJop12c= 21 | golang.org/x/crypto v0.1.0 h1:MDRAIl0xIo9Io2xV565hzXHw3zVseKrJKodhohM5CjU= 22 | golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= 23 | golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw= 24 | golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= 25 | golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= 26 | golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k= 28 | golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 29 | -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | package poe_api 2 | 3 | type CreateBot struct { 4 | Handle string 5 | Prompt string 6 | DisplayName string 7 | BaseModel string 8 | Description string 9 | IntroMessage string 10 | ApiKey *string 11 | ApiBot bool 12 | ApiUrl *string 13 | PromptPublic *bool 14 | PfpUrl *string 15 | Linkification bool 16 | MarkdownRendering *bool 17 | SuggestedReplies bool 18 | Private bool 19 | Temperature string 20 | } 21 | 22 | type Point interface { 23 | string | bool 24 | } 25 | 26 | func GetPoint[T Point](s T) *T { 27 | return &s 28 | } 29 | -------------------------------------------------------------------------------- /poe.go: -------------------------------------------------------------------------------- 1 | package poe_api 2 | 3 | import ( 4 | "bytes" 5 | "crypto/md5" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "log" 11 | "math" 12 | "math/rand" 13 | "net/http" 14 | "net/url" 15 | "regexp" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | "sync/atomic" 20 | "time" 21 | 22 | fhttp "github.com/bogdanfinn/fhttp" 23 | tls_client "github.com/bogdanfinn/tls-client" 24 | "github.com/gorilla/websocket" 25 | "github.com/zhangyunhao116/skipmap" 26 | ) 27 | 28 | type Client struct { 29 | token string 30 | deviceID string 31 | proxy *url.URL 32 | session tls_client.HttpClient 33 | activeMessages *skipmap.StringMap[float64] 34 | messageQueues *skipmap.StringMap[chan map[string]interface{}] 35 | headers fhttp.Header 36 | formKey string 37 | viewer map[string]interface{} 38 | userID string 39 | nextData map[string]interface{} 40 | channel map[string]interface{} 41 | bots map[string]interface{} 42 | botNames map[string]string 43 | gqlHeaders http.Header 44 | wsDomain string 45 | wsConn *websocket.Conn 46 | wsConnected bool 47 | requestCount atomic.Int64 48 | } 49 | 50 | func NewClient(token string, proxy *url.URL) *Client { 51 | // Initialize the client 52 | client := &Client{ 53 | token: token, 54 | deviceID: "", 55 | proxy: proxy, 56 | headers: headers, 57 | activeMessages: skipmap.NewString[float64](), 58 | messageQueues: skipmap.NewString[chan map[string]interface{}](), 59 | } 60 | // Set up the session 61 | client.setupSession(token) 62 | 63 | // Set up the connection 64 | client.setupConnection() 65 | client.connectWs() 66 | 67 | return client 68 | } 69 | 70 | func (c *Client) GetBots() map[string]string { 71 | return c.botNames 72 | } 73 | 74 | func (c *Client) SendMessage(chatbot, message string, withChatBreak bool, timeout time.Duration) (<-chan map[string]interface{}, error) { 75 | // 支持通过name获取chatbot 而不需要拿到poe后端需要的name 76 | if name, ok := c.botNames[chatbot]; ok { 77 | chatbot = name 78 | } 79 | result := make(chan map[string]interface{}) 80 | timer := 0 * time.Second 81 | // 防止并发 这里要先检查下是否有仍然未完成的消息 82 | for c.activeMessages.Len() != 0 { 83 | time.Sleep(10 * time.Millisecond) 84 | timer += 10 * time.Millisecond 85 | if timer > timeout { 86 | return nil, errors.New("timed out waiting for other messages to send") 87 | } 88 | } 89 | log.Printf("Sending message to %s: %s", chatbot, message) 90 | 91 | if !c.wsConnected { 92 | c.disconnectWs() 93 | c.setupConnection() 94 | c.connectWs() 95 | } 96 | 97 | chatID := c.getBotByCodename(chatbot)["chatId"].(float64) 98 | messageData := c.sendQuery("SendMessageMutation", map[string]interface{}{ 99 | "bot": chatbot, 100 | "query": message, 101 | "chatId": chatID, 102 | "source": nil, 103 | "clientNonce": generateNonce(16), 104 | "sdid": c.deviceID, 105 | "withChatBreak": withChatBreak, 106 | }, 0) 107 | 108 | if messageData["data"].(map[string]interface{})["messageEdgeCreate"].(map[string]interface{})["message"] == nil { 109 | return nil, fmt.Errorf("daily limit reached for %s", chatbot) 110 | } 111 | 112 | humanMessage := messageData["data"].(map[string]interface{})["messageEdgeCreate"].(map[string]interface{})["message"].(map[string]interface{}) 113 | humanMessageIDFloat64 := humanMessage["node"].(map[string]interface{})["messageId"].(float64) 114 | humanMessageID := fmt.Sprintf("%v", humanMessageIDFloat64) 115 | c.activeMessages.Store(humanMessageID, 0) 116 | c.messageQueues.Store(humanMessageID, make(chan map[string]interface{}, 1)) 117 | var lastChan = make(chan string, 1) 118 | go c.dealMessage(humanMessageID, lastChan, result, timeout) 119 | go c.sendRecv(humanMessageID, chatbot, chatbot, lastChan) 120 | return result, nil 121 | } 122 | 123 | func (c *Client) SendChatBreak(chatbot string) (map[string]interface{}, error) { 124 | log.Printf("Sending chat break to %s", chatbot) 125 | result := c.sendQuery("AddMessageBreakMutation", map[string]interface{}{ 126 | "chatId": c.getBotByCodename(chatbot)["chatId"], 127 | }, 0) 128 | return result["data"].(map[string]interface{})["messageBreakCreate"].(map[string]interface{})["message"].(map[string]interface{}), nil 129 | } 130 | 131 | func (c *Client) GetMessageHistory(chatbot string, count int, cursor interface{}) ([]map[string]interface{}, error) { 132 | log.Printf("Downloading %d messages from %s", count, chatbot) 133 | 134 | messages := []map[string]interface{}{} 135 | 136 | if cursor == nil { 137 | chatData := c.getBot(chatbot) 138 | if len(chatData["messagesConnection"].(map[string]interface{})["edges"].([]interface{})) == 0 { 139 | return []map[string]interface{}{}, nil 140 | } 141 | 142 | edges := chatData["messagesConnection"].(map[string]interface{})["edges"].([]map[string]interface{}) 143 | messages = edges[int(math.Max(float64(len(edges)-count), 0)):] 144 | cursor = chatData["messagesConnection"].(map[string]interface{})["pageInfo"].(map[string]interface{})["startCursor"] 145 | count -= len(messages) 146 | } 147 | 148 | if count <= 0 { 149 | return messages, nil 150 | } 151 | 152 | if count > 50 { 153 | var err error 154 | messages, err = c.GetMessageHistory(chatbot, 50, cursor) 155 | if err != nil { 156 | return nil, err 157 | } 158 | for count > 0 { 159 | count -= 50 160 | newCursor := messages[0]["cursor"].(string) 161 | newMessages, err := c.GetMessageHistory(chatbot, min(50, count), newCursor) 162 | if err != nil { 163 | return nil, err 164 | } 165 | messages = append(newMessages, messages...) 166 | } 167 | return messages, nil 168 | } 169 | 170 | result := c.sendQuery("ChatListPaginationQuery", map[string]interface{}{ 171 | "count": count, 172 | "cursor": cursor, 173 | "id": c.getBotByCodename(chatbot)["id"].(string), 174 | }, 0) 175 | queryMessages := result["data"].(map[string]interface{})["node"].(map[string]interface{})["messagesConnection"].(map[string]interface{})["edges"].([]map[string]interface{}) 176 | messages = append(queryMessages, messages...) 177 | return messages, nil 178 | } 179 | 180 | func (c *Client) DeleteMessage(messageIDs []int) error { 181 | log.Printf("Deleting messages: %v", messageIDs) 182 | c.sendQuery("DeleteMessageMutation", map[string]interface{}{ 183 | "messageIds": messageIDs, 184 | }, 0) 185 | return nil 186 | } 187 | 188 | func (c *Client) PurgeConversation(chatbot string, count int) error { 189 | log.Printf("Purging messages from %s", chatbot) 190 | lastMessages, err := c.GetMessageHistory(chatbot, 50, nil) 191 | if err != nil { 192 | return err 193 | } 194 | reverseSlice(lastMessages) 195 | 196 | for len(lastMessages) > 0 { 197 | var messageIDs []int 198 | 199 | for _, message := range lastMessages { 200 | if count == 0 { 201 | break 202 | } 203 | count-- 204 | messageID := int(message["node"].(map[string]interface{})["messageId"].(float64)) 205 | messageIDs = append(messageIDs, messageID) 206 | } 207 | 208 | err := c.DeleteMessage(messageIDs) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | if count == 0 { 214 | return nil 215 | } 216 | lastMessages, err = c.GetMessageHistory(chatbot, 50, nil) 217 | if err != nil { 218 | return err 219 | } 220 | reverseSlice(lastMessages) 221 | } 222 | 223 | log.Printf("No more messages left to delete.") 224 | return nil 225 | } 226 | 227 | func (c *Client) CreateBot(req CreateBot) map[string]interface{} { 228 | if req.PromptPublic == nil { 229 | req.PromptPublic = GetPoint[bool](true) 230 | } 231 | if req.MarkdownRendering == nil { 232 | req.MarkdownRendering = GetPoint[bool](true) 233 | } 234 | result := c.sendQuery("PoeBotCreateMutation", map[string]interface{}{ 235 | "baseBot": req.BaseModel, 236 | "displayName": req.DisplayName, 237 | "handle": req.Handle, 238 | "prompt": req.Prompt, 239 | "isPromptPublic": req.PromptPublic, 240 | "introduction": req.IntroMessage, 241 | "description": req.Description, 242 | "profilePictureUrl": req.PfpUrl, 243 | "apiUrl": req.ApiUrl, 244 | "apiKey": req.ApiKey, 245 | "isApiBot": req.ApiBot, 246 | "hasLinkification": req.Linkification, 247 | "hasMarkdownRendering": req.MarkdownRendering, 248 | "hasSuggestedReplies": req.SuggestedReplies, 249 | "isPrivateBot": req.Private, 250 | "temperature": req.Temperature, 251 | }, 0) 252 | data := getMap(getMap(result, "data"), "poeBotCreate") 253 | if data["status"] != "success" { 254 | panic(errors.New("Poe returned an error while trying to create a bot ")) 255 | } 256 | c.getBots(false) 257 | return data 258 | } 259 | 260 | func (c *Client) EditBot(botID string, req CreateBot) map[string]interface{} { 261 | if req.PromptPublic == nil { 262 | req.PromptPublic = GetPoint[bool](true) 263 | } 264 | if req.MarkdownRendering == nil { 265 | req.MarkdownRendering = GetPoint[bool](true) 266 | } 267 | result := c.sendQuery("PoeBotEditMutation", map[string]interface{}{ 268 | "botId": botID, 269 | "baseBot": req.BaseModel, 270 | "displayName": req.DisplayName, 271 | "handle": req.Handle, 272 | "prompt": req.Prompt, 273 | "isPromptPublic": req.PromptPublic, 274 | "introduction": req.IntroMessage, 275 | "description": req.Description, 276 | "profilePictureUrl": req.PfpUrl, 277 | "apiUrl": req.ApiUrl, 278 | "apiKey": req.ApiKey, 279 | "isApiBot": req.ApiBot, 280 | "hasLinkification": req.Linkification, 281 | "hasMarkdownRendering": req.MarkdownRendering, 282 | "hasSuggestedReplies": req.SuggestedReplies, 283 | "isPrivateBot": req.Private, 284 | "temperature": req.Temperature, 285 | }, 0) 286 | data := getMap(getMap(result, "data"), "poeBotEdit") 287 | if data["status"] != "success" { 288 | panic(errors.New("Poe returned an error while trying to create a bot ")) 289 | } 290 | c.getBots(false) 291 | return data 292 | } 293 | 294 | func (c *Client) requestWithRetries(method string, url string, attempts int, data []byte, headers map[string][]string) (*fhttp.Response, error) { 295 | if attempts == 0 { 296 | attempts = 10 297 | } 298 | client := c.session 299 | var payload io.Reader 300 | if data != nil { 301 | payload = bytes.NewBuffer(data) 302 | } 303 | req, err := fhttp.NewRequest(method, url, payload) 304 | if err != nil { 305 | return nil, err 306 | } 307 | req.Header = c.headers.Clone() 308 | if headers != nil { 309 | for key, value := range headers { 310 | req.Header[key] = value 311 | } 312 | } 313 | 314 | for i := 0; i < attempts; i++ { 315 | resp, err := client.Do(req) 316 | if err != nil { 317 | return nil, err 318 | } 319 | if resp.StatusCode == http.StatusOK { 320 | return resp, nil 321 | } 322 | if resp.StatusCode == http.StatusTemporaryRedirect { 323 | body, _ := io.ReadAll(resp.Body) 324 | if strings.HasPrefix(resp.Header.Get("Location"), "/login") { 325 | return nil, &InvalidToken{token: c.token} 326 | } 327 | fmt.Println(body) 328 | } 329 | logger.Printf("Server returned a status code of %d while downloading %s. Retrying (%d/%d)...", resp.StatusCode, url, i+1, attempts) 330 | time.Sleep(time.Second) 331 | } 332 | 333 | return nil, fmt.Errorf("failed to download %s too many times", url) 334 | } 335 | 336 | func (c *Client) setupSession(token string) { 337 | // Set up the session with the provided token and proxy 338 | jar := tls_client.NewCookieJar() 339 | options := []tls_client.HttpClientOption{ 340 | tls_client.WithTimeoutSeconds(30), 341 | tls_client.WithClientProfile(tls_client.Firefox_102), 342 | tls_client.WithNotFollowRedirects(), 343 | tls_client.WithCookieJar(jar), // create cookieJar instance and pass it as argument 344 | } 345 | 346 | client, err := tls_client.NewHttpClient(tls_client.NewNoopLogger(), options...) 347 | if err != nil { 348 | log.Println(err) 349 | return 350 | } 351 | c.session = client 352 | 353 | if c.proxy != nil { 354 | c.session.SetProxy(c.proxy.String()) 355 | log.Printf("Proxy enabled: %s\n", c.proxy.String()) 356 | } 357 | 358 | // Update session headers 359 | c.headers.Set("Referrer", "https://poe.com/") 360 | c.headers.Set("Origin", "https://poe.com") 361 | c.headers.Set("Host", "poe.com") 362 | c.headers.Set("Sec-Fetch-Dest", "empty") 363 | c.headers.Set("Sec-Fetch-Mode", "cors") 364 | c.headers.Set("Sec-Fetch-Site", "same-origin") 365 | c.headers.Set("Client-Identifier", clientIdentifier) 366 | for key, value := range headers { 367 | c.headers[key] = value 368 | } 369 | // Set cookie 370 | cookie := &fhttp.Cookie{ 371 | Name: "p-b", 372 | Value: token, 373 | Domain: "poe.com", 374 | } 375 | url, err := url.Parse(homeURL) 376 | if err != nil { 377 | panic(err) 378 | } 379 | c.session.SetCookies(url, []*fhttp.Cookie{cookie}) 380 | } 381 | 382 | func (c *Client) setupConnection() { 383 | c.wsDomain = fmt.Sprintf("tch%d", rand.Intn(1000000)) 384 | c.nextData = c.getNextData(true) 385 | c.channel = c.getChannelData() 386 | c.bots = c.getBots(false) 387 | c.botNames = c.getBotNames() 388 | 389 | if c.deviceID == "" { 390 | c.deviceID = c.getDeviceID() 391 | } 392 | 393 | c.gqlHeaders = make(http.Header) 394 | c.gqlHeaders.Set("poe-formkey", c.formKey) 395 | c.gqlHeaders.Set("poe-tchannel", c.channel["channel"].(string)) 396 | 397 | for k, v := range c.headers { 398 | c.gqlHeaders[k] = v 399 | } 400 | 401 | c.subscribe() 402 | } 403 | 404 | func (c *Client) getDeviceID() string { 405 | userID := c.viewer["poeUser"].(map[string]interface{})["id"].(string) 406 | deviceID := getSavedDeviceID(userID) 407 | return deviceID 408 | } 409 | 410 | func (c *Client) extractFormKey(html string) string { 411 | scriptRegex := regexp.MustCompile(``) 412 | scriptText := scriptRegex.FindStringSubmatch(html)[1] 413 | keyRegex := regexp.MustCompile(`var .="([0-9a-f]+)",`) 414 | keyText := keyRegex.FindStringSubmatch(scriptText)[1] 415 | cipherRegex := regexp.MustCompile(`.\[(\d+)\]=.\[(\d+)\]`) 416 | cipherPairs := cipherRegex.FindAllStringSubmatch(scriptText, -1) 417 | 418 | formKeyList := make([]string, len(cipherPairs)) 419 | for _, pair := range cipherPairs { 420 | formKeyIndex, _ := strconv.Atoi(pair[1]) 421 | keyIndex, _ := strconv.Atoi(pair[2]) 422 | formKeyList[formKeyIndex] = string(keyText[keyIndex]) 423 | } 424 | formKey := strings.Join(formKeyList, "") 425 | 426 | return formKey 427 | } 428 | 429 | func (c *Client) getNextData(overwriteVars bool) map[string]interface{} { 430 | resp, err := c.requestWithRetries(http.MethodGet, homeURL, 0, nil, nil) 431 | if err != nil { 432 | panic(err) 433 | } 434 | 435 | defer resp.Body.Close() 436 | body, err := io.ReadAll(resp.Body) 437 | 438 | jsonRegex := regexp.MustCompile(``) 439 | jsonText := jsonRegex.FindStringSubmatch(string(body))[1] 440 | 441 | var nextData map[string]interface{} 442 | err = json.Unmarshal([]byte(jsonText), &nextData) 443 | 444 | if overwriteVars { 445 | c.formKey = c.extractFormKey(string(body)) 446 | if containKey("payload", nextData["props"].(map[string]interface{})["pageProps"].(map[string]interface{})) { 447 | c.viewer = nextData["props"].(map[string]interface{})["pageProps"].(map[string]interface{})["payload"].(map[string]interface{})["viewer"].(map[string]interface{}) 448 | } else { 449 | c.viewer = nextData["props"].(map[string]interface{})["pageProps"].(map[string]interface{})["data"].(map[string]interface{})["viewer"].(map[string]interface{}) 450 | } 451 | c.userID = c.viewer["poeUser"].(map[string]interface{})["id"].(string) 452 | c.nextData = nextData 453 | } 454 | 455 | return nextData 456 | } 457 | 458 | func (c *Client) getBot(displayName string) map[string]interface{} { 459 | url := fmt.Sprintf("https://poe.com/_next/data/%s/%s.json", c.nextData["buildId"].(string), displayName) 460 | 461 | resp, err := c.requestWithRetries(http.MethodGet, url, 0, nil, nil) 462 | if err != nil { 463 | // handle error 464 | } 465 | 466 | defer resp.Body.Close() 467 | body, err := io.ReadAll(resp.Body) 468 | 469 | var jsonData map[string]interface{} 470 | err = json.Unmarshal(body, &jsonData) 471 | 472 | var chatData map[string]interface{} 473 | if containKey("payload", jsonData["pageProps"].(map[string]interface{})) { 474 | chatData = jsonData["pageProps"].(map[string]interface{})["payload"].(map[string]interface{})["chatOfBotHandle"].(map[string]interface{}) 475 | } else { 476 | chatData = jsonData["pageProps"].(map[string]interface{})["data"].(map[string]interface{})["chatOfBotHandle"].(map[string]interface{}) 477 | } 478 | return chatData 479 | } 480 | 481 | func (c *Client) getBots(downloadNextData bool) map[string]interface{} { 482 | if _, ok := c.viewer["availableBotsConnection"]; !ok { 483 | panic("Invalid token or no bots are available.") 484 | } 485 | botList := c.viewer["availableBotsConnection"].(map[string]interface{})["edges"].([]interface{}) 486 | 487 | var wg sync.WaitGroup 488 | bots := make(map[string]interface{}) 489 | lock := &sync.Mutex{} 490 | 491 | getBotThread := func(bot map[string]interface{}) { 492 | defer wg.Done() 493 | lock.Lock() 494 | defer lock.Unlock() 495 | chatData := c.getBot(bot["node"].(map[string]interface{})["handle"].(string)) 496 | bots[chatData["defaultBotObject"].(map[string]interface{})["nickname"].(string)] = chatData 497 | } 498 | 499 | wg.Add(len(botList)) 500 | for _, bot := range botList { 501 | go getBotThread(bot.(map[string]interface{})) 502 | } 503 | wg.Wait() 504 | 505 | c.bots = bots 506 | c.botNames = c.getBotNames() 507 | return bots 508 | } 509 | 510 | func (c *Client) getBotByCodename(botCodename string) map[string]interface{} { 511 | if bot, ok := c.bots[botCodename]; ok { 512 | return bot.(map[string]interface{}) 513 | } 514 | // TODO: Cache this so it isn't re-downloaded every time 515 | return c.getBot(botCodename) 516 | } 517 | 518 | func (c *Client) getBotNames() map[string]string { 519 | result := map[string]string{} 520 | for k, v := range c.bots { 521 | object := v.(map[string]interface{})["defaultBotObject"].(map[string]interface{}) 522 | if name, ok := object["displayName"]; ok { 523 | result[name.(string)] = k 524 | } 525 | } 526 | return result 527 | } 528 | 529 | func (c *Client) exploreBots(endCursor *string, count int) map[string]interface{} { 530 | var url string 531 | var resp *fhttp.Response 532 | var err error 533 | 534 | if endCursor == nil { 535 | url = fmt.Sprintf("https://poe.com/_next/data/%s/explore_bots.json", c.nextData["buildId"].(string)) 536 | resp, err = c.requestWithRetries(http.MethodGet, url, 0, nil, nil) 537 | } else { 538 | // Use GraphQL to get the next page 539 | queryData := map[string]interface{}{ 540 | "count": count, 541 | "cursor": *endCursor, 542 | } 543 | result := c.sendQuery("ExploreBotsListPaginationQuery", queryData, 0) 544 | resultData := result["data"].(map[string]interface{})["exploreBotsConnection"].(map[string]interface{}) 545 | 546 | bots := make([]map[string]interface{}, len(resultData["edges"].([]interface{}))) 547 | for i, node := range resultData["edges"].([]interface{}) { 548 | bots[i] = node.(map[string]interface{})["node"].(map[string]interface{}) 549 | } 550 | 551 | return map[string]interface{}{ 552 | "bots": bots, 553 | "end_cursor": resultData["pageInfo"].(map[string]interface{})["endCursor"], 554 | } 555 | } 556 | 557 | // Handle error in HTTP response 558 | if err != nil { 559 | panic(err) 560 | } 561 | 562 | defer resp.Body.Close() 563 | body, err := io.ReadAll(resp.Body) 564 | 565 | var jsonData map[string]interface{} 566 | err = json.Unmarshal(body, &jsonData) 567 | 568 | nodes := jsonData["pageProps"].(map[string]interface{})["payload"].(map[string]interface{})["exploreBotsConnection"].(map[string]interface{})["edges"].([]interface{}) 569 | bots := make([]map[string]interface{}, len(nodes)) 570 | for i, node := range nodes { 571 | bots[i] = node.(map[string]interface{})["node"].(map[string]interface{}) 572 | } 573 | 574 | return map[string]interface{}{ 575 | "bots": bots[:count], 576 | "end_cursor": jsonData["pageProps"].(map[string]interface{})["payload"].(map[string]interface{})["exploreBotsConnection"].(map[string]interface{})["pageInfo"].(map[string]interface{})["endCursor"], 577 | } 578 | } 579 | 580 | func (c *Client) getRemainingMessages(chatbot string) int { 581 | chatData := c.getBotByCodename(chatbot) 582 | return int(chatData["defaultBotObject"].(map[string]interface{})["messageLimit"].(map[string]interface{})["numMessagesRemaining"].(float64)) 583 | } 584 | 585 | func (c *Client) getChannelData() map[string]interface{} { 586 | log.Println("Downloading channel data...") 587 | resp, err := c.requestWithRetries(http.MethodGet, settingsURL, 0, nil, nil) 588 | 589 | if err != nil { 590 | panic(err) 591 | } 592 | 593 | defer resp.Body.Close() 594 | body, err := io.ReadAll(resp.Body) 595 | 596 | var jsonData map[string]interface{} 597 | err = json.Unmarshal(body, &jsonData) 598 | 599 | return jsonData["tchannelData"].(map[string]interface{}) 600 | } 601 | 602 | func (c *Client) getWebsocketURL(channel map[string]interface{}) string { 603 | if channel == nil { 604 | channel = c.channel 605 | } 606 | minSeq := channel["minSeq"].(string) 607 | channelName := channel["channel"].(string) 608 | hash := channel["channelHash"].(string) 609 | baseHost := channel["baseHost"].(string) 610 | boxName := channel["boxName"].(string) 611 | 612 | return fmt.Sprintf("wss://%s.tch.%s/up/%s/updates?min_seq=%s&channel=%s&hash=%s", c.wsDomain, baseHost, boxName, minSeq, channelName, hash) 613 | } 614 | 615 | func (c *Client) sendQuery(queryName string, variables map[string]interface{}, attempts int) map[string]interface{} { 616 | if attempts == 0 { 617 | attempts = 10 618 | } 619 | for i := 0; i < attempts; i++ { 620 | jsonData := generatePayload(queryName, variables) 621 | payload, _ := json.Marshal(jsonData) 622 | 623 | baseString := string(payload) + c.gqlHeaders["Poe-Formkey"][0] + "WpuLMiXEKKE98j56k" 624 | 625 | headers := map[string][]string{ 626 | "content-type": {"application/json"}, 627 | "poe-tag-id": {fmt.Sprintf("%x", md5.Sum([]byte(baseString)))}, 628 | } 629 | 630 | for k, v := range c.gqlHeaders { 631 | headers[k] = v 632 | } 633 | if queryName == "recv" { 634 | _, err := c.requestWithRetries(http.MethodPost, gqlRecvURL, attempts, payload, headers) 635 | if err != nil { 636 | panic(err) 637 | } 638 | return nil 639 | } 640 | resp, err := c.requestWithRetries(http.MethodPost, gqlURL, attempts, payload, headers) 641 | 642 | // Handle error in HTTP response 643 | if err != nil { 644 | panic(err) 645 | } 646 | 647 | defer resp.Body.Close() 648 | body, err := io.ReadAll(resp.Body) 649 | 650 | var data map[string]interface{} 651 | err = json.Unmarshal(body, &data) 652 | 653 | if data["data"] == nil { 654 | log.Printf("%s returned an error: %s | Retrying (%d/%d)\n", queryName, data["errors"].([]interface{})[0].(map[string]interface{})["message"].(string), i+1, attempts) 655 | time.Sleep(2 * time.Second) 656 | continue 657 | } 658 | 659 | return data 660 | } 661 | 662 | panic(fmt.Sprintf("%s failed too many times.", queryName)) 663 | } 664 | 665 | func (c *Client) subscribe() map[string]interface{} { 666 | log.Println("Subscribing to mutations") 667 | result := c.sendQuery("SubscriptionsMutation", map[string]interface{}{ 668 | "subscriptions": []map[string]interface{}{ 669 | { 670 | "subscriptionName": "messageAdded", 671 | "query": queries["MessageAddedSubscription"], 672 | }, 673 | { 674 | "subscriptionName": "viewerStateUpdated", 675 | "query": queries["ViewerStateUpdatedSubscription"], 676 | }, 677 | }, 678 | }, 0) 679 | return result 680 | } 681 | 682 | func (c *Client) wsRunThread() { 683 | dialer := websocket.DefaultDialer 684 | 685 | if c.proxy != nil { 686 | dialer.Proxy = http.ProxyURL(c.proxy) 687 | } 688 | 689 | conn, _, err := dialer.Dial(c.getWebsocketURL(nil), http.Header{"User-Agent": {userAgent}}) 690 | if err != nil { 691 | log.Fatalf("Error connecting to websocket: %v", err) 692 | } 693 | c.wsConn = conn 694 | c.wsConnected = true 695 | 696 | for { 697 | _, message, err := c.wsConn.ReadMessage() 698 | if err != nil { 699 | c.onWsError(err) 700 | return 701 | } 702 | c.onMessage(message) 703 | } 704 | } 705 | 706 | func (c *Client) connectWs() { 707 | c.wsConnected = false 708 | go c.wsRunThread() 709 | 710 | for !c.wsConnected { 711 | time.Sleep(10 * time.Millisecond) 712 | } 713 | } 714 | 715 | func (c *Client) disconnectWs() { 716 | if c.wsConn != nil { 717 | c.wsConn.Close() 718 | } 719 | c.wsConnected = false 720 | } 721 | 722 | func (c *Client) onWsConnect() { 723 | c.wsConnected = true 724 | } 725 | 726 | func (c *Client) onWsClose(code int, text string) { 727 | c.wsConnected = false 728 | log.Printf("Websocket closed with status %d: %s", code, text) 729 | } 730 | 731 | func (c *Client) onWsError(err error) { 732 | c.disconnectWs() 733 | c.connectWs() 734 | } 735 | 736 | func (c *Client) onMessage(msg []byte) { 737 | var data map[string]interface{} 738 | err := json.Unmarshal(msg, &data) 739 | if err != nil { 740 | log.Printf("Error unmarshaling message: %v", err) 741 | c.disconnectWs() 742 | c.connectWs() 743 | return 744 | } 745 | 746 | messages, ok := data["messages"].([]interface{}) 747 | if !ok { 748 | return 749 | } 750 | 751 | for _, messageStr := range messages { 752 | var messageData map[string]interface{} 753 | err := json.Unmarshal([]byte(messageStr.(string)), &messageData) 754 | if err != nil { 755 | log.Printf("Error unmarshaling message data: %v", err) 756 | c.disconnectWs() 757 | c.connectWs() 758 | return 759 | } 760 | 761 | if messageData["message_type"].(string) != "subscriptionUpdate" { 762 | continue 763 | } 764 | 765 | message := messageData["payload"].(map[string]interface{})["data"].(map[string]interface{})["messageAdded"].(map[string]interface{}) 766 | 767 | copiedDict := make(map[string]float64) 768 | c.activeMessages.Range(func(key string, value float64) bool { 769 | copiedDict[key] = value 770 | return true 771 | }) 772 | 773 | for key, value := range copiedDict { 774 | queue, ok := c.messageQueues.Load(key) 775 | if !ok { 776 | continue 777 | } 778 | if value == message["messageId"].(float64) { 779 | queue <- message 780 | return 781 | } else if key != "pending" && value == 0 && message["state"].(string) != "complete" { 782 | c.activeMessages.Store(key, message["messageId"].(float64)) 783 | queue <- message 784 | return 785 | } 786 | } 787 | } 788 | } 789 | 790 | func (c *Client) dealMessage(humanMessageID string, textCh chan string, result chan map[string]interface{}, timeout time.Duration) { 791 | defer c.activeMessages.Delete(humanMessageID) 792 | defer c.messageQueues.Delete(humanMessageID) 793 | defer close(result) 794 | defer close(textCh) 795 | lastText := "" 796 | messageID := "" 797 | ch, ok := c.messageQueues.Load(humanMessageID) 798 | if !ok { 799 | return 800 | } 801 | for { 802 | select { 803 | case <-time.After(timeout): 804 | return 805 | case message := <-ch: 806 | if message["state"].(string) == "complete" { 807 | if lastText != "" && fmt.Sprintf("%v", message["messageId"].(float64)) == messageID { 808 | return 809 | } else { 810 | continue 811 | } 812 | } 813 | 814 | textNew := message["text"].(string)[len(lastText):] 815 | lastText = message["text"].(string) 816 | messageID = fmt.Sprintf("%v", message["messageId"].(float64)) 817 | textCh <- lastText 818 | message["text_new"] = textNew 819 | result <- message 820 | } 821 | } 822 | } 823 | 824 | func (c *Client) sendRecv(humanMessageID, chatbot, chatID string, textCh chan string) { 825 | for text := range textCh { 826 | m := map[string]interface{}{ 827 | "bot": chatbot, 828 | "time_to_first_typing_indicator": 300, 829 | "time_to_first_subscription_response": 600, 830 | "time_to_full_bot_response": 1100, 831 | "full_response_length": len(text) + 1, 832 | "full_response_word_count": len(strings.Split(text, " ")) + 1, 833 | "human_message_id": humanMessageID, 834 | "chat_id": chatID, 835 | "bot_response_status": "success", 836 | } 837 | id, ok := c.activeMessages.Load(humanMessageID) 838 | if !ok || id == 0 { 839 | m["bot_message_id"] = nil 840 | } else { 841 | m["bot_message_id"] = id 842 | } 843 | c.sendQuery("recv", m, 0) 844 | } 845 | } 846 | -------------------------------------------------------------------------------- /poe_graphql/AddHumanMessageMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation AddHumanMessageMutation( 2 | $chatId: BigInt! 3 | $bot: String! 4 | $query: String! 5 | $source: MessageSource 6 | $withChatBreak: Boolean! = false 7 | ) { 8 | messageCreateWithStatus( 9 | chatId: $chatId 10 | bot: $bot 11 | query: $query 12 | source: $source 13 | withChatBreak: $withChatBreak 14 | ) { 15 | message { 16 | id 17 | __typename 18 | messageId 19 | text 20 | linkifiedText 21 | authorNickname 22 | state 23 | vote 24 | voteReason 25 | creationTime 26 | suggestedReplies 27 | chat { 28 | id 29 | shouldShowDisclaimer 30 | } 31 | } 32 | messageLimit{ 33 | canSend 34 | numMessagesRemaining 35 | resetTime 36 | shouldShowReminder 37 | } 38 | chatBreak { 39 | id 40 | __typename 41 | messageId 42 | text 43 | linkifiedText 44 | authorNickname 45 | state 46 | vote 47 | voteReason 48 | creationTime 49 | suggestedReplies 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /poe_graphql/AddMessageBreakMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation AddMessageBreakMutation($chatId: BigInt!) { 2 | messageBreakCreate(chatId: $chatId) { 3 | message { 4 | id 5 | __typename 6 | messageId 7 | text 8 | linkifiedText 9 | authorNickname 10 | state 11 | vote 12 | voteReason 13 | creationTime 14 | suggestedReplies 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /poe_graphql/AutoSubscriptionMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation AutoSubscriptionMutation($subscriptions: [AutoSubscriptionQuery!]!) { 2 | autoSubscribe(subscriptions: $subscriptions) { 3 | viewer { 4 | id 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /poe_graphql/BioFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment BioFragment on Viewer { 2 | id 3 | poeUser { 4 | id 5 | uid 6 | bio 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /poe_graphql/ChatAddedSubscription.graphql: -------------------------------------------------------------------------------- 1 | subscription ChatAddedSubscription { 2 | chatAdded { 3 | ...ChatFragment 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /poe_graphql/ChatFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment ChatFragment on Chat { 2 | id 3 | chatId 4 | defaultBotNickname 5 | shouldShowDisclaimer 6 | } 7 | -------------------------------------------------------------------------------- /poe_graphql/ChatListPaginationQuery.graphql: -------------------------------------------------------------------------------- 1 | query ChatListPaginationQuery( 2 | $count: Int = 5 3 | $cursor: String 4 | $id: ID! 5 | ) { 6 | node(id: $id) { 7 | __typename 8 | ...ChatPageMain_chat_1G22uz 9 | id 10 | } 11 | } 12 | 13 | fragment BotImage_bot on Bot { 14 | displayName 15 | ...botHelpers_useDeletion_bot 16 | ...BotImage_useProfileImage_bot 17 | } 18 | 19 | fragment BotImage_useProfileImage_bot on Bot { 20 | image { 21 | __typename 22 | ... on LocalBotImage { 23 | localName 24 | } 25 | ... on UrlBotImage { 26 | url 27 | } 28 | } 29 | ...botHelpers_useDeletion_bot 30 | } 31 | 32 | fragment ChatMessageDownvotedButton_message on Message { 33 | ...MessageFeedbackReasonModal_message 34 | ...MessageFeedbackOtherModal_message 35 | } 36 | 37 | fragment ChatMessageDropdownMenu_message on Message { 38 | id 39 | messageId 40 | vote 41 | text 42 | author 43 | ...chatHelpers_isBotMessage 44 | } 45 | 46 | fragment ChatMessageFeedbackButtons_message on Message { 47 | id 48 | messageId 49 | vote 50 | voteReason 51 | ...ChatMessageDownvotedButton_message 52 | } 53 | 54 | fragment ChatMessageInputView_chat on Chat { 55 | id 56 | chatId 57 | defaultBotObject { 58 | nickname 59 | messageLimit { 60 | dailyBalance 61 | shouldShowRemainingMessageCount 62 | } 63 | hasClearContext 64 | isDown 65 | ...botHelpers_useDeletion_bot 66 | id 67 | } 68 | shouldShowDisclaimer 69 | ...chatHelpers_useSendMessage_chat 70 | ...chatHelpers_useSendChatBreak_chat 71 | } 72 | 73 | fragment ChatMessageInputView_edges on MessageEdge { 74 | node { 75 | ...chatHelpers_isChatBreak 76 | ...chatHelpers_isHumanMessage 77 | state 78 | text 79 | id 80 | } 81 | } 82 | 83 | fragment ChatMessageOverflowButton_message on Message { 84 | text 85 | ...ChatMessageDropdownMenu_message 86 | ...chatHelpers_isBotMessage 87 | } 88 | 89 | fragment ChatMessageSuggestedReplies_SuggestedReplyButton_chat on Chat { 90 | ...chatHelpers_useSendMessage_chat 91 | } 92 | 93 | fragment ChatMessageSuggestedReplies_SuggestedReplyButton_message on Message { 94 | messageId 95 | } 96 | 97 | fragment ChatMessageSuggestedReplies_chat on Chat { 98 | ...ChatWelcomeView_chat 99 | ...ChatMessageSuggestedReplies_SuggestedReplyButton_chat 100 | defaultBotObject { 101 | hasWelcomeTopics 102 | id 103 | } 104 | } 105 | 106 | fragment ChatMessageSuggestedReplies_message on Message { 107 | suggestedReplies 108 | ...ChatMessageSuggestedReplies_SuggestedReplyButton_message 109 | } 110 | 111 | fragment ChatMessage_chat on Chat { 112 | defaultBotObject { 113 | hasWelcomeTopics 114 | hasSuggestedReplies 115 | disclaimerText 116 | messageLimit { 117 | ...ChatPageRateLimitedBanner_messageLimit 118 | } 119 | ...ChatPageDisclaimer_bot 120 | id 121 | } 122 | ...ChatMessageSuggestedReplies_chat 123 | ...ChatWelcomeView_chat 124 | } 125 | 126 | fragment ChatMessage_message on Message { 127 | id 128 | messageId 129 | text 130 | author 131 | linkifiedText 132 | state 133 | contentType 134 | ...ChatMessageSuggestedReplies_message 135 | ...ChatMessageFeedbackButtons_message 136 | ...ChatMessageOverflowButton_message 137 | ...chatHelpers_isHumanMessage 138 | ...chatHelpers_isBotMessage 139 | ...chatHelpers_isChatBreak 140 | ...chatHelpers_useTimeoutLevel 141 | ...MarkdownLinkInner_message 142 | ...IdAnnotation_node 143 | } 144 | 145 | fragment ChatMessagesView_chat on Chat { 146 | ...ChatMessage_chat 147 | ...ChatWelcomeView_chat 148 | ...IdAnnotation_node 149 | defaultBotObject { 150 | hasWelcomeTopics 151 | messageLimit { 152 | ...ChatPageRateLimitedBanner_messageLimit 153 | } 154 | id 155 | } 156 | } 157 | 158 | fragment ChatMessagesView_edges on MessageEdge { 159 | node { 160 | id 161 | messageId 162 | creationTime 163 | ...ChatMessage_message 164 | ...chatHelpers_isBotMessage 165 | ...chatHelpers_isHumanMessage 166 | ...chatHelpers_isChatBreak 167 | } 168 | } 169 | 170 | fragment ChatPageDeleteFooter_chat on Chat { 171 | ...MessageDeleteConfirmationModal_chat 172 | } 173 | 174 | fragment ChatPageDisclaimer_bot on Bot { 175 | disclaimerText 176 | } 177 | 178 | fragment ChatPageMainFooter_chat on Chat { 179 | defaultBotObject { 180 | ...ChatPageMainFooter_useAccessMessage_bot 181 | id 182 | } 183 | ...ChatMessageInputView_chat 184 | ...ChatPageShareFooter_chat 185 | ...ChatPageDeleteFooter_chat 186 | } 187 | 188 | fragment ChatPageMainFooter_edges on MessageEdge { 189 | ...ChatMessageInputView_edges 190 | } 191 | 192 | fragment ChatPageMainFooter_useAccessMessage_bot on Bot { 193 | ...botHelpers_useDeletion_bot 194 | ...botHelpers_useViewerCanAccessPrivateBot 195 | } 196 | 197 | fragment ChatPageMain_chat_1G22uz on Chat { 198 | id 199 | chatId 200 | ...ChatPageShareFooter_chat 201 | ...ChatPageDeleteFooter_chat 202 | ...ChatMessagesView_chat 203 | ...MarkdownLinkInner_chat 204 | ...chatHelpers_useUpdateStaleChat_chat 205 | ...ChatSubscriptionPaywallContextWrapper_chat 206 | ...ChatPageMainFooter_chat 207 | messagesConnection(last: $count, before: $cursor) { 208 | edges { 209 | ...ChatMessagesView_edges 210 | ...ChatPageMainFooter_edges 211 | ...MarkdownLinkInner_edges 212 | node { 213 | ...chatHelpers_useUpdateStaleChat_message 214 | id 215 | __typename 216 | } 217 | cursor 218 | id 219 | } 220 | pageInfo { 221 | hasPreviousPage 222 | startCursor 223 | } 224 | id 225 | } 226 | } 227 | 228 | fragment ChatPageRateLimitedBanner_messageLimit on MessageLimit { 229 | numMessagesRemaining 230 | } 231 | 232 | fragment ChatPageShareFooter_chat on Chat { 233 | chatId 234 | } 235 | 236 | fragment ChatSubscriptionPaywallContextWrapper_chat on Chat { 237 | defaultBotObject { 238 | messageLimit { 239 | numMessagesRemaining 240 | shouldShowRemainingMessageCount 241 | } 242 | ...SubscriptionPaywallModal_bot 243 | id 244 | } 245 | } 246 | 247 | fragment ChatWelcomeView_ChatWelcomeButton_chat on Chat { 248 | ...chatHelpers_useSendMessage_chat 249 | } 250 | 251 | fragment ChatWelcomeView_chat on Chat { 252 | ...ChatWelcomeView_ChatWelcomeButton_chat 253 | defaultBotObject { 254 | displayName 255 | id 256 | } 257 | } 258 | 259 | fragment IdAnnotation_node on Node { 260 | __isNode: __typename 261 | id 262 | } 263 | 264 | fragment MarkdownLinkInner_chat on Chat { 265 | id 266 | chatId 267 | defaultBotObject { 268 | nickname 269 | id 270 | } 271 | ...chatHelpers_useSendMessage_chat 272 | } 273 | 274 | fragment MarkdownLinkInner_edges on MessageEdge { 275 | node { 276 | state 277 | id 278 | } 279 | } 280 | 281 | fragment MarkdownLinkInner_message on Message { 282 | messageId 283 | } 284 | 285 | fragment MessageDeleteConfirmationModal_chat on Chat { 286 | id 287 | } 288 | 289 | fragment MessageFeedbackOtherModal_message on Message { 290 | id 291 | messageId 292 | } 293 | 294 | fragment MessageFeedbackReasonModal_message on Message { 295 | id 296 | messageId 297 | } 298 | 299 | fragment SubscriptionPaywallModal_bot on Bot { 300 | displayName 301 | messageLimit { 302 | dailyLimit 303 | numMessagesRemaining 304 | shouldShowRemainingMessageCount 305 | resetTime 306 | } 307 | ...BotImage_bot 308 | } 309 | 310 | fragment botHelpers_useDeletion_bot on Bot { 311 | deletionState 312 | } 313 | 314 | fragment botHelpers_useViewerCanAccessPrivateBot on Bot { 315 | isPrivateBot 316 | viewerIsCreator 317 | } 318 | 319 | fragment chatHelpers_isBotMessage on Message { 320 | ...chatHelpers_isHumanMessage 321 | ...chatHelpers_isChatBreak 322 | } 323 | 324 | fragment chatHelpers_isChatBreak on Message { 325 | author 326 | } 327 | 328 | fragment chatHelpers_isHumanMessage on Message { 329 | author 330 | } 331 | 332 | fragment chatHelpers_useSendChatBreak_chat on Chat { 333 | id 334 | chatId 335 | defaultBotObject { 336 | nickname 337 | introduction 338 | model 339 | id 340 | } 341 | shouldShowDisclaimer 342 | } 343 | 344 | fragment chatHelpers_useSendMessage_chat on Chat { 345 | id 346 | chatId 347 | defaultBotObject { 348 | id 349 | nickname 350 | } 351 | shouldShowDisclaimer 352 | } 353 | 354 | fragment chatHelpers_useTimeoutLevel on Message { 355 | id 356 | state 357 | text 358 | messageId 359 | chat { 360 | chatId 361 | defaultBotNickname 362 | id 363 | } 364 | } 365 | 366 | fragment chatHelpers_useUpdateStaleChat_chat on Chat { 367 | chatId 368 | defaultBotObject { 369 | contextClearWindowSecs 370 | id 371 | } 372 | ...chatHelpers_useSendChatBreak_chat 373 | } 374 | 375 | fragment chatHelpers_useUpdateStaleChat_message on Message { 376 | creationTime 377 | ...chatHelpers_isChatBreak 378 | } 379 | -------------------------------------------------------------------------------- /poe_graphql/ChatPaginationQuery.graphql: -------------------------------------------------------------------------------- 1 | query ChatPaginationQuery($bot: String!, $before: String, $last: Int! = 10) { 2 | chatOfBot(bot: $bot) { 3 | id 4 | __typename 5 | messagesConnection(before: $before, last: $last) { 6 | pageInfo { 7 | hasPreviousPage 8 | } 9 | edges { 10 | node { 11 | id 12 | __typename 13 | messageId 14 | text 15 | linkifiedText 16 | authorNickname 17 | state 18 | vote 19 | voteReason 20 | creationTime 21 | suggestedReplies 22 | } 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /poe_graphql/ChatViewQuery.graphql: -------------------------------------------------------------------------------- 1 | query ChatViewQuery($bot: String!) { 2 | chatOfBot(bot: $bot) { 3 | id 4 | chatId 5 | defaultBotNickname 6 | shouldShowDisclaimer 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /poe_graphql/DeleteHumanMessagesMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation DeleteHumanMessagesMutation($messageIds: [BigInt!]!) { 2 | messagesDelete(messageIds: $messageIds) { 3 | viewer { 4 | id 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /poe_graphql/DeleteMessageMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation deleteMessageMutation( 2 | $messageIds: [BigInt!]! 3 | ) { 4 | messagesDelete(messageIds: $messageIds) { 5 | edgeIds 6 | } 7 | } -------------------------------------------------------------------------------- /poe_graphql/ExploreBotsListPaginationQuery.graphql: -------------------------------------------------------------------------------- 1 | query ExploreBotsListPaginationQuery($count: Int = 20, $cursor: String) { 2 | ...ExploreBotsMain_queryRoot_1G22uz 3 | } 4 | 5 | fragment BotImage_bot on Bot { 6 | displayName 7 | ...botHelpers_useDeletion_bot 8 | ...BotImage_useProfileImage_bot 9 | } 10 | 11 | fragment BotImage_useProfileImage_bot on Bot { 12 | image { 13 | __typename 14 | ... on LocalBotImage { 15 | localName 16 | } 17 | ... on UrlBotImage { 18 | url 19 | } 20 | } 21 | ...botHelpers_useDeletion_bot 22 | } 23 | 24 | fragment BotLink_bot on Bot { 25 | displayName 26 | } 27 | 28 | fragment ExploreBotListItem_bot on Bot { 29 | ...BotLink_bot 30 | ...BotImage_bot 31 | botId 32 | displayName 33 | followerCount 34 | description 35 | } 36 | 37 | fragment ExploreBotsMain_queryRoot_1G22uz on QueryRoot { 38 | exploreBotsConnection(first: $count, after: $cursor) { 39 | edges { 40 | ...ExploreBotsPagedList_edges 41 | cursor 42 | node { 43 | __typename 44 | id 45 | } 46 | id 47 | } 48 | pageInfo { 49 | endCursor 50 | hasNextPage 51 | } 52 | id 53 | } 54 | } 55 | 56 | fragment ExploreBotsPagedList_edges on BotEdge { 57 | node { 58 | id 59 | ...ExploreBotListItem_bot 60 | } 61 | } 62 | 63 | fragment botHelpers_useDeletion_bot on Bot { 64 | deletionState 65 | } 66 | -------------------------------------------------------------------------------- /poe_graphql/HandleFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment HandleFragment on Viewer { 2 | id 3 | poeUser { 4 | id 5 | uid 6 | handle 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /poe_graphql/LoginWithVerificationCodeMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation LoginWithVerificationCodeMutation( 2 | $verificationCode: String! 3 | $emailAddress: String 4 | $phoneNumber: String 5 | ) { 6 | loginWithVerificationCode( 7 | verificationCode: $verificationCode 8 | emailAddress: $emailAddress 9 | phoneNumber: $phoneNumber 10 | ) { 11 | status 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /poe_graphql/MessageAddedSubscription.graphql: -------------------------------------------------------------------------------- 1 | subscription messageAdded ( 2 | $chatId: BigInt! 3 | ) { 4 | messageAdded(chatId: $chatId) { 5 | id 6 | messageId 7 | creationTime 8 | state 9 | ...ChatMessage_message 10 | ...chatHelpers_isBotMessage 11 | } 12 | } 13 | 14 | fragment ChatMessageDownvotedButton_message on Message { 15 | ...MessageFeedbackReasonModal_message 16 | ...MessageFeedbackOtherModal_message 17 | } 18 | 19 | fragment ChatMessageDropdownMenu_message on Message { 20 | id 21 | messageId 22 | vote 23 | text 24 | linkifiedText 25 | ...chatHelpers_isBotMessage 26 | } 27 | 28 | fragment ChatMessageFeedbackButtons_message on Message { 29 | id 30 | messageId 31 | vote 32 | voteReason 33 | ...ChatMessageDownvotedButton_message 34 | } 35 | 36 | fragment ChatMessageOverflowButton_message on Message { 37 | text 38 | ...ChatMessageDropdownMenu_message 39 | ...chatHelpers_isBotMessage 40 | } 41 | 42 | fragment ChatMessageSuggestedReplies_SuggestedReplyButton_message on Message { 43 | messageId 44 | } 45 | 46 | fragment ChatMessageSuggestedReplies_message on Message { 47 | suggestedReplies 48 | ...ChatMessageSuggestedReplies_SuggestedReplyButton_message 49 | } 50 | 51 | fragment ChatMessage_message on Message { 52 | id 53 | messageId 54 | text 55 | author 56 | linkifiedText 57 | state 58 | ...ChatMessageSuggestedReplies_message 59 | ...ChatMessageFeedbackButtons_message 60 | ...ChatMessageOverflowButton_message 61 | ...chatHelpers_isHumanMessage 62 | ...chatHelpers_isBotMessage 63 | ...chatHelpers_isChatBreak 64 | ...chatHelpers_useTimeoutLevel 65 | ...MarkdownLinkInner_message 66 | } 67 | 68 | fragment MarkdownLinkInner_message on Message { 69 | messageId 70 | } 71 | 72 | fragment MessageFeedbackOtherModal_message on Message { 73 | id 74 | messageId 75 | } 76 | 77 | fragment MessageFeedbackReasonModal_message on Message { 78 | id 79 | messageId 80 | } 81 | 82 | fragment chatHelpers_isBotMessage on Message { 83 | ...chatHelpers_isHumanMessage 84 | ...chatHelpers_isChatBreak 85 | } 86 | 87 | fragment chatHelpers_isChatBreak on Message { 88 | author 89 | } 90 | 91 | fragment chatHelpers_isHumanMessage on Message { 92 | author 93 | } 94 | 95 | fragment chatHelpers_useTimeoutLevel on Message { 96 | id 97 | state 98 | text 99 | messageId 100 | } 101 | -------------------------------------------------------------------------------- /poe_graphql/MessageDeletedSubscription.graphql: -------------------------------------------------------------------------------- 1 | subscription MessageDeletedSubscription($chatId: BigInt!) { 2 | messageDeleted(chatId: $chatId) { 3 | id 4 | messageId 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /poe_graphql/MessageFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment MessageFragment on Message { 2 | id 3 | __typename 4 | messageId 5 | text 6 | linkifiedText 7 | authorNickname 8 | state 9 | vote 10 | voteReason 11 | creationTime 12 | suggestedReplies 13 | } 14 | -------------------------------------------------------------------------------- /poe_graphql/MessageRemoveVoteMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation MessageRemoveVoteMutation($messageId: BigInt!) { 2 | messageRemoveVote(messageId: $messageId) { 3 | message { 4 | ...MessageFragment 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /poe_graphql/MessageSetVoteMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation MessageSetVoteMutation($messageId: BigInt!, $voteType: VoteType!, $reason: String) { 2 | messageSetVote(messageId: $messageId, voteType: $voteType, reason: $reason) { 3 | message { 4 | ...MessageFragment 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /poe_graphql/PoeBotCreateMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation CreateBotMain_poeBotCreate_Mutation( 2 | $model: String! 3 | $displayName: String 4 | $handle: String! 5 | $prompt: String! 6 | $isPromptPublic: Boolean! 7 | $introduction: String! 8 | $description: String! 9 | $profilePictureUrl: String 10 | $apiUrl: String 11 | $apiKey: String 12 | $isApiBot: Boolean 13 | $hasLinkification: Boolean 14 | $hasMarkdownRendering: Boolean 15 | $hasSuggestedReplies: Boolean 16 | $isPrivateBot: Boolean 17 | $temperature: Float 18 | ) { 19 | poeBotCreate(model: $model, handle: $handle, displayName: $displayName, promptPlaintext: $prompt, isPromptPublic: $isPromptPublic, introduction: $introduction, description: $description, profilePicture: $profilePictureUrl, apiUrl: $apiUrl, apiKey: $apiKey, isApiBot: $isApiBot, hasLinkification: $hasLinkification, hasMarkdownRendering: $hasMarkdownRendering, hasSuggestedReplies: $hasSuggestedReplies, isPrivateBot: $isPrivateBot, temperature: $temperature) { 20 | status 21 | bot { 22 | id 23 | ...BotHeader_bot 24 | } 25 | } 26 | } 27 | 28 | fragment BotHeader_bot on Bot { 29 | displayName 30 | isLimitedAccess 31 | ...BotImage_bot 32 | ...BotLink_bot 33 | ...IdAnnotation_node 34 | ...botHelpers_useViewerCanAccessPrivateBot 35 | ...botHelpers_useDeletion_bot 36 | } 37 | 38 | fragment BotImage_bot on Bot { 39 | displayName 40 | ...botHelpers_useDeletion_bot 41 | ...BotImage_useProfileImage_bot 42 | } 43 | 44 | fragment BotImage_useProfileImage_bot on Bot { 45 | image { 46 | __typename 47 | ... on LocalBotImage { 48 | localName 49 | } 50 | ... on UrlBotImage { 51 | url 52 | } 53 | } 54 | ...botHelpers_useDeletion_bot 55 | } 56 | 57 | fragment BotLink_bot on Bot { 58 | handle 59 | } 60 | 61 | fragment IdAnnotation_node on Node { 62 | __isNode: __typename 63 | id 64 | } 65 | 66 | fragment botHelpers_useDeletion_bot on Bot { 67 | deletionState 68 | } 69 | 70 | fragment botHelpers_useViewerCanAccessPrivateBot on Bot { 71 | isPrivateBot 72 | viewerIsCreator 73 | isSystemBot 74 | } -------------------------------------------------------------------------------- /poe_graphql/PoeBotEditMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation EditBotMain_poeBotEdit_Mutation( 2 | $botId: BigInt! 3 | $handle: String! 4 | $displayName: String 5 | $description: String! 6 | $introduction: String! 7 | $isPromptPublic: Boolean! 8 | $baseBot: String! 9 | $profilePictureUrl: String 10 | $prompt: String! 11 | $apiUrl: String 12 | $apiKey: String 13 | $hasLinkification: Boolean 14 | $hasMarkdownRendering: Boolean 15 | $hasSuggestedReplies: Boolean 16 | $isPrivateBot: Boolean 17 | $temperature: Float 18 | ) { 19 | poeBotEdit(botId: $botId, handle: $handle, displayName: $displayName, description: $description, introduction: $introduction, isPromptPublic: $isPromptPublic, model: $baseBot, promptPlaintext: $prompt, profilePicture: $profilePictureUrl, apiUrl: $apiUrl, apiKey: $apiKey, hasLinkification: $hasLinkification, hasMarkdownRendering: $hasMarkdownRendering, hasSuggestedReplies: $hasSuggestedReplies, isPrivateBot: $isPrivateBot, temperature: $temperature) { 20 | status 21 | bot { 22 | handle 23 | id 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /poe_graphql/SendMessageMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation chatHelpers_sendMessageMutation_Mutation( 2 | $chatId: BigInt! 3 | $bot: String! 4 | $query: String! 5 | $source: MessageSource 6 | $withChatBreak: Boolean! 7 | ) { 8 | messageEdgeCreate(chatId: $chatId, bot: $bot, query: $query, source: $source, withChatBreak: $withChatBreak) { 9 | chatBreak { 10 | cursor 11 | node { 12 | id 13 | messageId 14 | text 15 | author 16 | suggestedReplies 17 | creationTime 18 | state 19 | } 20 | id 21 | } 22 | message { 23 | cursor 24 | node { 25 | id 26 | messageId 27 | text 28 | author 29 | suggestedReplies 30 | creationTime 31 | state 32 | chat { 33 | shouldShowDisclaimer 34 | id 35 | } 36 | } 37 | id 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /poe_graphql/SendVerificationCodeForLoginMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation SendVerificationCodeForLoginMutation( 2 | $emailAddress: String 3 | $phoneNumber: String 4 | ) { 5 | sendVerificationCode( 6 | verificationReason: login 7 | emailAddress: $emailAddress 8 | phoneNumber: $phoneNumber 9 | ) { 10 | status 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /poe_graphql/ShareMessagesMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation ShareMessagesMutation( 2 | $chatId: BigInt! 3 | $messageIds: [BigInt!]! 4 | $comment: String 5 | ) { 6 | messagesShare(chatId: $chatId, messageIds: $messageIds, comment: $comment) { 7 | shareCode 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /poe_graphql/SignupWithVerificationCodeMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation SignupWithVerificationCodeMutation( 2 | $verificationCode: String! 3 | $emailAddress: String 4 | $phoneNumber: String 5 | ) { 6 | signupWithVerificationCode( 7 | verificationCode: $verificationCode 8 | emailAddress: $emailAddress 9 | phoneNumber: $phoneNumber 10 | ) { 11 | status 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /poe_graphql/StaleChatUpdateMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation StaleChatUpdateMutation($chatId: BigInt!) { 2 | staleChatUpdate(chatId: $chatId) { 3 | message { 4 | ...MessageFragment 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /poe_graphql/SubscriptionsMutation.graphql: -------------------------------------------------------------------------------- 1 | mutation subscriptionsMutation( 2 | $subscriptions: [AutoSubscriptionQuery!]! 3 | ) { 4 | autoSubscribe(subscriptions: $subscriptions) { 5 | viewer { 6 | id 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /poe_graphql/SummarizePlainPostQuery.graphql: -------------------------------------------------------------------------------- 1 | query SummarizePlainPostQuery($comment: String!) { 2 | summarizePlainPost(comment: $comment) 3 | } 4 | -------------------------------------------------------------------------------- /poe_graphql/SummarizeQuotePostQuery.graphql: -------------------------------------------------------------------------------- 1 | query SummarizeQuotePostQuery($comment: String, $quotedPostId: BigInt!) { 2 | summarizeQuotePost(comment: $comment, quotedPostId: $quotedPostId) 3 | } 4 | -------------------------------------------------------------------------------- /poe_graphql/SummarizeSharePostQuery.graphql: -------------------------------------------------------------------------------- 1 | query SummarizeSharePostQuery($comment: String!, $chatId: BigInt!, $messageIds: [BigInt!]!) { 2 | summarizeSharePost(comment: $comment, chatId: $chatId, messageIds: $messageIds) 3 | } 4 | -------------------------------------------------------------------------------- /poe_graphql/UserSnippetFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment UserSnippetFragment on PoeUser { 2 | id 3 | uid 4 | bio 5 | handle 6 | fullName 7 | viewerIsFollowing 8 | isPoeOnlyUser 9 | profilePhotoURLTiny: profilePhotoUrl(size: tiny) 10 | profilePhotoURLSmall: profilePhotoUrl(size: small) 11 | profilePhotoURLMedium: profilePhotoUrl(size: medium) 12 | profilePhotoURLLarge: profilePhotoUrl(size: large) 13 | isFollowable 14 | } 15 | -------------------------------------------------------------------------------- /poe_graphql/ViewerInfoQuery.graphql: -------------------------------------------------------------------------------- 1 | query ViewerInfoQuery { 2 | viewer { 3 | id 4 | uid 5 | ...ViewerStateFragment 6 | ...BioFragment 7 | ...HandleFragment 8 | hasCompletedMultiplayerNux 9 | poeUser { 10 | id 11 | ...UserSnippetFragment 12 | } 13 | messageLimit{ 14 | canSend 15 | numMessagesRemaining 16 | resetTime 17 | shouldShowReminder 18 | } 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /poe_graphql/ViewerStateFragment.graphql: -------------------------------------------------------------------------------- 1 | fragment ViewerStateFragment on Viewer { 2 | id 3 | __typename 4 | iosMinSupportedVersion: integerGate(gateName: "poe_ios_min_supported_version") 5 | iosMinEncouragedVersion: integerGate( 6 | gateName: "poe_ios_min_encouraged_version" 7 | ) 8 | macosMinSupportedVersion: integerGate( 9 | gateName: "poe_macos_min_supported_version" 10 | ) 11 | macosMinEncouragedVersion: integerGate( 12 | gateName: "poe_macos_min_encouraged_version" 13 | ) 14 | showPoeDebugPanel: booleanGate(gateName: "poe_show_debug_panel") 15 | enableCommunityFeed: booleanGate(gateName: "enable_poe_shares_feed") 16 | linkifyText: booleanGate(gateName: "poe_linkify_response") 17 | enableSuggestedReplies: booleanGate(gateName: "poe_suggested_replies") 18 | removeInviteLimit: booleanGate(gateName: "poe_remove_invite_limit") 19 | enableInAppPurchases: booleanGate(gateName: "poe_enable_in_app_purchases") 20 | availableBots { 21 | nickname 22 | displayName 23 | profilePicture 24 | isDown 25 | disclaimer 26 | subtitle 27 | poweredBy 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /poe_graphql/ViewerStateUpdatedSubscription.graphql: -------------------------------------------------------------------------------- 1 | subscription viewerStateUpdated { 2 | viewerStateUpdated { 3 | id 4 | ...ChatPageBotSwitcher_viewer 5 | } 6 | } 7 | 8 | fragment BotHeader_bot on Bot { 9 | displayName 10 | messageLimit { 11 | dailyLimit 12 | } 13 | ...BotImage_bot 14 | } 15 | 16 | fragment BotImage_bot on Bot { 17 | image { 18 | __typename 19 | ... on LocalBotImage { 20 | localName 21 | } 22 | ... on UrlBotImage { 23 | url 24 | } 25 | } 26 | displayName 27 | } 28 | 29 | fragment BotLink_bot on Bot { 30 | displayName 31 | } 32 | 33 | fragment ChatPageBotSwitcher_viewer on Viewer { 34 | availableBots { 35 | id 36 | messageLimit { 37 | dailyLimit 38 | } 39 | ...BotLink_bot 40 | ...BotHeader_bot 41 | } 42 | allowUserCreatedBots: booleanGate(gateName: "enable_user_created_bots") 43 | } 44 | -------------------------------------------------------------------------------- /poe_test.go: -------------------------------------------------------------------------------- 1 | package poe_api 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestSendMessage(t *testing.T) { 10 | c := NewClient("", nil) 11 | res, err := c.SendMessage("ChatGPT", "", true, 30*time.Second) 12 | if err != nil { 13 | panic(err) 14 | } 15 | for r := range res { 16 | fmt.Println(r) 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package poe_api 2 | 3 | import ( 4 | "encoding/json" 5 | "math/rand" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/google/uuid" 10 | ) 11 | 12 | func GetFinalResponse(ch <-chan map[string]interface{}) string { 13 | var m map[string]interface{} 14 | for message := range ch { 15 | m = message 16 | if message["state"] != "complete" { 17 | continue 18 | } 19 | return message["text"].(string) 20 | } 21 | return m["text"].(string) 22 | } 23 | 24 | func GetTextStream(ch <-chan map[string]interface{}) <-chan string { 25 | stream := make(chan string, 1) 26 | go func() { 27 | for message := range ch { 28 | stream <- message["text_new"].(string) 29 | } 30 | close(stream) 31 | }() 32 | return stream 33 | } 34 | 35 | func generatePayload(queryName string, variables map[string]interface{}) interface{} { 36 | if queryName == "recv" { 37 | if rand.Float64() > 0.9 { 38 | return []map[string]interface{}{ 39 | { 40 | "category": "poe/bot_response_speed", 41 | "data": variables, 42 | }, 43 | { 44 | "category": "poe/statsd_event", 45 | "data": map[string]interface{}{ 46 | "key": "poe.speed.web_vitals.INP", 47 | "value": rand.Intn(26) + 100, 48 | "category": "time", 49 | "path": "/[handle]", 50 | "extra_data": map[string]interface{}{}, 51 | }, 52 | }, 53 | } 54 | } else { 55 | return []map[string]interface{}{ 56 | { 57 | "category": "poe/bot_response_speed", 58 | "data": variables, 59 | }, 60 | } 61 | } 62 | } 63 | return map[string]interface{}{ 64 | "query": queries[queryName], 65 | "queryName": queryName, 66 | "variables": variables, 67 | } 68 | } 69 | 70 | func generateNonce(length int) string { 71 | if length == 0 { 72 | length = 16 73 | } 74 | const lettersAndDigits = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 75 | var nonce = make([]rune, length) 76 | for i := range nonce { 77 | nonce[i] = rune(lettersAndDigits[rand.Intn(len(lettersAndDigits))]) 78 | } 79 | return string(nonce) 80 | } 81 | 82 | func getConfigPath() string { 83 | var configPath string 84 | if os.PathSeparator == '\\' { 85 | configPath = filepath.Join(os.Getenv("APPDATA"), "poe-api") 86 | } else { 87 | configPath = filepath.Join(os.Getenv("HOME"), ".config", "poe-api") 88 | } 89 | return configPath 90 | } 91 | 92 | func setSavedDeviceID(userID, deviceID string) { 93 | deviceIDPath := filepath.Join(getConfigPath(), "device_id.json") 94 | deviceIDs := make(map[string]string) 95 | 96 | if _, err := os.Stat(deviceIDPath); !os.IsNotExist(err) { 97 | deviceIDBytes, err := os.ReadFile(deviceIDPath) 98 | if err != nil { 99 | panic(err) 100 | } 101 | err = json.Unmarshal(deviceIDBytes, &deviceIDs) 102 | if err != nil { 103 | panic(err) 104 | } 105 | } 106 | 107 | deviceIDs[userID] = deviceID 108 | err := os.MkdirAll(filepath.Dir(deviceIDPath), os.ModePerm) 109 | if err != nil { 110 | panic(err) 111 | } 112 | deviceIDBytes, err := json.MarshalIndent(deviceIDs, "", " ") 113 | if err != nil { 114 | panic(err) 115 | } 116 | err = os.WriteFile(deviceIDPath, deviceIDBytes, 0644) 117 | if err != nil { 118 | panic(err) 119 | } 120 | } 121 | 122 | func getSavedDeviceID(userID string) string { 123 | deviceIDPath := filepath.Join(getConfigPath(), "device_id.json") 124 | deviceIDs := make(map[string]string) 125 | 126 | if _, err := os.Stat(deviceIDPath); !os.IsNotExist(err) { 127 | deviceIDBytes, err := os.ReadFile(deviceIDPath) 128 | if err != nil { 129 | panic(err) 130 | } 131 | err = json.Unmarshal(deviceIDBytes, &deviceIDs) 132 | if err != nil { 133 | panic(err) 134 | } 135 | } 136 | 137 | if deviceID, ok := deviceIDs[userID]; ok { 138 | return deviceID 139 | } 140 | 141 | deviceID := uuid.New().String() 142 | deviceIDs[userID] = deviceID 143 | err := os.MkdirAll(filepath.Dir(deviceIDPath), os.ModePerm) 144 | if err != nil { 145 | panic(err) 146 | } 147 | deviceIDBytes, err := json.MarshalIndent(deviceIDs, "", " ") 148 | if err != nil { 149 | panic(err) 150 | } 151 | err = os.WriteFile(deviceIDPath, deviceIDBytes, 0644) 152 | if err != nil { 153 | panic(err) 154 | } 155 | 156 | return deviceID 157 | } 158 | 159 | func min(a, b int) int { 160 | if a < b { 161 | return a 162 | } 163 | return b 164 | } 165 | 166 | func reverseSlice(s []map[string]interface{}) { 167 | for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 { 168 | s[i], s[j] = s[j], s[i] 169 | } 170 | } 171 | 172 | func containKey(key string, m map[string]interface{}) bool { 173 | _, ok := m[key] 174 | return ok 175 | } 176 | 177 | func getMap(m map[string]interface{}, key string) map[string]interface{} { 178 | return m[key].(map[string]interface{}) 179 | } 180 | --------------------------------------------------------------------------------