all repos — telegram-bot-api @ f9ab9c0a4df305dcfc111e96a8b92c6fda442a39

Golang bindings for the Telegram Bot API

bot.go (view raw)

  1// Package tgbotapi has functions and types used for interacting with
  2// the Telegram Bot API.
  3package tgbotapi
  4
  5import (
  6	"context"
  7	"encoding/json"
  8	"errors"
  9	"fmt"
 10	"io"
 11	"mime/multipart"
 12	"net/http"
 13	"net/url"
 14	"strings"
 15	"sync"
 16	"time"
 17)
 18
 19// HTTPClient is the type needed for the bot to perform HTTP requests.
 20type HTTPClient interface {
 21	Do(req *http.Request) (*http.Response, error)
 22}
 23
 24// BotAPI allows you to interact with the Telegram Bot API.
 25type BotAPI struct {
 26	Token  string `json:"token"`
 27	Debug  bool   `json:"debug"`
 28	Buffer int    `json:"buffer"`
 29
 30	Self   User       `json:"-"`
 31	Client HTTPClient `json:"-"`
 32
 33	apiEndpoint string
 34
 35	stoppers []context.CancelFunc
 36	mu       sync.RWMutex
 37}
 38
 39// NewBotAPI creates a new BotAPI instance.
 40//
 41// It requires a token, provided by @BotFather on Telegram.
 42func NewBotAPI(token string) (*BotAPI, error) {
 43	return NewBotAPIWithClient(token, APIEndpoint, &http.Client{})
 44}
 45
 46// NewBotAPIWithAPIEndpoint creates a new BotAPI instance
 47// and allows you to pass API endpoint.
 48//
 49// It requires a token, provided by @BotFather on Telegram and API endpoint.
 50func NewBotAPIWithAPIEndpoint(token, apiEndpoint string) (*BotAPI, error) {
 51	return NewBotAPIWithClient(token, apiEndpoint, &http.Client{})
 52}
 53
 54// NewBotAPIWithClient creates a new BotAPI instance
 55// and allows you to pass a http.Client.
 56//
 57// It requires a token, provided by @BotFather on Telegram and API endpoint.
 58func NewBotAPIWithClient(token, apiEndpoint string, client HTTPClient) (*BotAPI, error) {
 59	bot := &BotAPI{
 60		Token:  token,
 61		Client: client,
 62		Buffer: 100,
 63
 64		apiEndpoint: apiEndpoint,
 65	}
 66
 67	self, err := bot.GetMe()
 68	if err != nil {
 69		return nil, err
 70	}
 71
 72	bot.Self = self
 73
 74	return bot, nil
 75}
 76
 77// SetAPIEndpoint changes the Telegram Bot API endpoint used by the instance.
 78func (bot *BotAPI) SetAPIEndpoint(apiEndpoint string) {
 79	bot.apiEndpoint = apiEndpoint
 80}
 81
 82func buildParams(in Params) url.Values {
 83	if in == nil {
 84		return url.Values{}
 85	}
 86
 87	out := url.Values{}
 88
 89	for key, value := range in {
 90		out.Set(key, value)
 91	}
 92
 93	return out
 94}
 95
 96// MakeRequest makes a request to a specific endpoint with our token.
 97func (bot *BotAPI) MakeRequest(endpoint string, params Params) (*APIResponse, error) {
 98	return bot.MakeRequestWithContext(context.Background(), endpoint, params)
 99}
100
101func (bot *BotAPI) MakeRequestWithContext(ctx context.Context, endpoint string, params Params) (*APIResponse, error) {
102	if bot.Debug {
103		log.Printf("Endpoint: %s, params: %v\n", endpoint, params)
104	}
105
106	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
107
108	values := buildParams(params)
109
110	req, err := http.NewRequestWithContext(ctx, "POST", method, strings.NewReader(values.Encode()))
111	if err != nil {
112		return &APIResponse{}, err
113	}
114	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
115
116	resp, err := bot.Client.Do(req)
117	if err != nil {
118		return nil, err
119	}
120	defer resp.Body.Close()
121
122	var apiResp APIResponse
123	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
124	if err != nil {
125		return &apiResp, err
126	}
127
128	if bot.Debug {
129		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
130	}
131
132	if !apiResp.Ok {
133		var parameters ResponseParameters
134
135		if apiResp.Parameters != nil {
136			parameters = *apiResp.Parameters
137		}
138
139		return &apiResp, &Error{
140			Code:               apiResp.ErrorCode,
141			Message:            apiResp.Description,
142			ResponseParameters: parameters,
143		}
144	}
145
146	return &apiResp, nil
147}
148
149// decodeAPIResponse decode response and return slice of bytes if debug enabled.
150// If debug disabled, just decode http.Response.Body stream to APIResponse struct
151// for efficient memory usage
152func (bot *BotAPI) decodeAPIResponse(responseBody io.Reader, resp *APIResponse) ([]byte, error) {
153	if !bot.Debug {
154		dec := json.NewDecoder(responseBody)
155		err := dec.Decode(resp)
156		return nil, err
157	}
158
159	// if debug, read response body
160	data, err := io.ReadAll(responseBody)
161	if err != nil {
162		return nil, err
163	}
164
165	err = json.Unmarshal(data, resp)
166	if err != nil {
167		return nil, err
168	}
169
170	return data, nil
171}
172
173// UploadFiles makes a request to the API with files.
174func (bot *BotAPI) UploadFiles(endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
175	return bot.UploadFilesWithContext(context.Background(), endpoint, params, files)
176}
177
178func (bot *BotAPI) UploadFilesWithContext(ctx context.Context, endpoint string, params Params, files []RequestFile) (*APIResponse, error) {
179	r, w := io.Pipe()
180	m := multipart.NewWriter(w)
181
182	// This code modified from the very helpful @HirbodBehnam
183	// https://github.com/go-telegram-bot-api/telegram-bot-api/issues/354#issuecomment-663856473
184	go func() {
185		defer w.Close()
186		defer m.Close()
187
188		for field, value := range params {
189			if err := m.WriteField(field, value); err != nil {
190				w.CloseWithError(err)
191				return
192			}
193		}
194
195		for _, file := range files {
196			if file.Data.NeedsUpload() {
197				name, reader, err := file.Data.UploadData()
198				if err != nil {
199					w.CloseWithError(err)
200					return
201				}
202
203				part, err := m.CreateFormFile(file.Name, name)
204				if err != nil {
205					w.CloseWithError(err)
206					return
207				}
208
209				if _, err := io.Copy(part, reader); err != nil {
210					w.CloseWithError(err)
211					return
212				}
213
214				if closer, ok := reader.(io.ReadCloser); ok {
215					if err = closer.Close(); err != nil {
216						w.CloseWithError(err)
217						return
218					}
219				}
220			} else {
221				value := file.Data.SendData()
222
223				if err := m.WriteField(file.Name, value); err != nil {
224					w.CloseWithError(err)
225					return
226				}
227			}
228		}
229	}()
230
231	if bot.Debug {
232		log.Printf("Endpoint: %s, params: %v, with %d files\n", endpoint, params, len(files))
233	}
234
235	method := fmt.Sprintf(bot.apiEndpoint, bot.Token, endpoint)
236
237	req, err := http.NewRequestWithContext(ctx, "POST", method, r)
238	if err != nil {
239		return nil, err
240	}
241
242	req.Header.Set("Content-Type", m.FormDataContentType())
243
244	resp, err := bot.Client.Do(req)
245	if err != nil {
246		return nil, err
247	}
248	defer resp.Body.Close()
249
250	var apiResp APIResponse
251	bytes, err := bot.decodeAPIResponse(resp.Body, &apiResp)
252	if err != nil {
253		return &apiResp, err
254	}
255
256	if bot.Debug {
257		log.Printf("Endpoint: %s, response: %s\n", endpoint, string(bytes))
258	}
259
260	if !apiResp.Ok {
261		var parameters ResponseParameters
262
263		if apiResp.Parameters != nil {
264			parameters = *apiResp.Parameters
265		}
266
267		return &apiResp, &Error{
268			Message:            apiResp.Description,
269			ResponseParameters: parameters,
270		}
271	}
272
273	return &apiResp, nil
274}
275
276// GetFileDirectURL returns direct URL to file
277//
278// It requires the FileID.
279func (bot *BotAPI) GetFileDirectURL(fileID string) (string, error) {
280	file, err := bot.GetFile(FileConfig{fileID})
281
282	if err != nil {
283		return "", err
284	}
285
286	return file.Link(bot.Token), nil
287}
288
289// GetMe fetches the currently authenticated bot.
290//
291// This method is called upon creation to validate the token,
292// and so you may get this data from BotAPI.Self without the need for
293// another request.
294func (bot *BotAPI) GetMe() (User, error) {
295	return bot.GetMeWithContext(context.Background())
296}
297
298func (bot *BotAPI) GetMeWithContext(ctx context.Context) (User, error) {
299	resp, err := bot.MakeRequestWithContext(ctx, "getMe", nil)
300	if err != nil {
301		return User{}, err
302	}
303
304	var user User
305	err = json.Unmarshal(resp.Result, &user)
306
307	return user, err
308}
309
310// IsMessageToMe returns true if message directed to this bot.
311//
312// It requires the Message.
313func (bot *BotAPI) IsMessageToMe(message Message) bool {
314	return strings.Contains(message.Text, "@"+bot.Self.UserName)
315}
316
317func hasFilesNeedingUpload(files []RequestFile) bool {
318	for _, file := range files {
319		if file.Data.NeedsUpload() {
320			return true
321		}
322	}
323
324	return false
325}
326
327// Request sends a Chattable to Telegram, and returns the APIResponse.
328func (bot *BotAPI) Request(c Chattable) (*APIResponse, error) {
329	return bot.RequestWithContext(context.Background(), c)
330}
331
332func (bot *BotAPI) RequestWithContext(ctx context.Context, c Chattable) (*APIResponse, error) {
333	params, err := c.params()
334	if err != nil {
335		return nil, err
336	}
337
338	if t, ok := c.(Fileable); ok {
339		files := t.files()
340
341		// If we have files that need to be uploaded, we should delegate the
342		// request to UploadFile.
343		if hasFilesNeedingUpload(files) {
344			return bot.UploadFiles(t.method(), params, files)
345		}
346
347		// However, if there are no files to be uploaded, there's likely things
348		// that need to be turned into params instead.
349		for _, file := range files {
350			params[file.Name] = file.Data.SendData()
351		}
352	}
353
354	return bot.MakeRequestWithContext(ctx, c.method(), params)
355}
356
357// Send will send a Chattable item to Telegram and provides the
358// returned Message.
359func (bot *BotAPI) Send(c Chattable) (Message, error) {
360	resp, err := bot.Request(c)
361	if err != nil {
362		return Message{}, err
363	}
364
365	var message Message
366	err = json.Unmarshal(resp.Result, &message)
367
368	return message, err
369}
370
371// SendMediaGroup sends a media group and returns the resulting messages.
372func (bot *BotAPI) SendMediaGroup(config MediaGroupConfig) ([]Message, error) {
373	resp, err := bot.Request(config)
374	if err != nil {
375		return nil, err
376	}
377
378	var messages []Message
379	err = json.Unmarshal(resp.Result, &messages)
380
381	return messages, err
382}
383
384// GetUserProfilePhotos gets a user's profile photos.
385//
386// It requires UserID.
387// Offset and Limit are optional.
388func (bot *BotAPI) GetUserProfilePhotos(config UserProfilePhotosConfig) (UserProfilePhotos, error) {
389	resp, err := bot.Request(config)
390	if err != nil {
391		return UserProfilePhotos{}, err
392	}
393
394	var profilePhotos UserProfilePhotos
395	err = json.Unmarshal(resp.Result, &profilePhotos)
396
397	return profilePhotos, err
398}
399
400// GetFile returns a File which can download a file from Telegram.
401//
402// Requires FileID.
403func (bot *BotAPI) GetFile(config FileConfig) (File, error) {
404	resp, err := bot.Request(config)
405	if err != nil {
406		return File{}, err
407	}
408
409	var file File
410	err = json.Unmarshal(resp.Result, &file)
411
412	return file, err
413}
414
415// GetUpdates fetches updates.
416// If a WebHook is set, this will not return any data!
417//
418// Offset, Limit, Timeout, and AllowedUpdates are optional.
419// To avoid stale items, set Offset to one higher than the previous item.
420// Set Timeout to a large number to reduce requests, so you can get updates
421// instantly instead of having to wait between requests.
422func (bot *BotAPI) GetUpdates(config UpdateConfig) ([]Update, error) {
423	return bot.GetUpdatesWithContext(context.Background(), config)
424}
425
426func (bot *BotAPI) GetUpdatesWithContext(ctx context.Context, config UpdateConfig) ([]Update, error) {
427	resp, err := bot.RequestWithContext(ctx, config)
428	if err != nil {
429		return []Update{}, err
430	}
431
432	var updates []Update
433	err = json.Unmarshal(resp.Result, &updates)
434
435	return updates, err
436}
437
438// GetWebhookInfo allows you to fetch information about a webhook and if
439// one currently is set, along with pending update count and error messages.
440func (bot *BotAPI) GetWebhookInfo() (WebhookInfo, error) {
441	return bot.GetWebhookInfoWithContext(context.Background())
442}
443
444func (bot *BotAPI) GetWebhookInfoWithContext(ctx context.Context) (WebhookInfo, error) {
445	resp, err := bot.MakeRequestWithContext(ctx, "getWebhookInfo", nil)
446	if err != nil {
447		return WebhookInfo{}, err
448	}
449
450	var info WebhookInfo
451	err = json.Unmarshal(resp.Result, &info)
452
453	return info, err
454}
455
456// GetUpdatesChan starts and returns a channel for getting updates.
457func (bot *BotAPI) GetUpdatesChan(config UpdateConfig) UpdatesChannel {
458	ch := make(chan Update, bot.Buffer)
459
460	ctx, cancel := context.WithCancel(context.Background())
461	bot.mu.Lock()
462	bot.stoppers = append(bot.stoppers, cancel)
463	bot.mu.Unlock()
464
465	go func() {
466		for {
467			select {
468			case <-ctx.Done():
469				close(ch)
470				return
471			default:
472			}
473
474			updates, err := bot.GetUpdatesWithContext(ctx, config)
475			if err != nil {
476				if ctx.Err() == nil {
477					log.Println(err)
478					log.Println("Failed to get updates, retrying in 3 seconds...")
479					time.Sleep(time.Second * 3)
480				}
481				continue
482			}
483
484			for _, update := range updates {
485				if update.UpdateID >= config.Offset {
486					config.Offset = update.UpdateID + 1
487					ch <- update
488				}
489			}
490		}
491	}()
492
493	return ch
494}
495
496// StopReceivingUpdates stops the go routine which receives updates
497func (bot *BotAPI) StopReceivingUpdates() {
498	bot.mu.Lock()
499	defer bot.mu.Unlock()
500
501	if bot.Debug {
502		log.Println("Stopping the update receiver routine...")
503	}
504	for _, stopper := range bot.stoppers {
505		stopper()
506	}
507}
508
509// ListenForWebhook registers a http handler for a webhook.
510func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
511	ch := make(chan Update, bot.Buffer)
512
513	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
514		update, err := bot.HandleUpdate(r)
515		if err != nil {
516			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
517			w.WriteHeader(http.StatusBadRequest)
518			w.Header().Set("Content-Type", "application/json")
519			_, _ = w.Write(errMsg)
520			return
521		}
522
523		ch <- *update
524	})
525
526	return ch
527}
528
529// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
530func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
531	ch := make(chan Update, bot.Buffer)
532
533	func(w http.ResponseWriter, r *http.Request) {
534		defer close(ch)
535
536		update, err := bot.HandleUpdate(r)
537		if err != nil {
538			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
539			w.WriteHeader(http.StatusBadRequest)
540			w.Header().Set("Content-Type", "application/json")
541			_, _ = w.Write(errMsg)
542			return
543		}
544
545		ch <- *update
546	}(w, r)
547
548	return ch
549}
550
551// HandleUpdate parses and returns update received via webhook
552func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
553	if r.Method != http.MethodPost {
554		err := errors.New("wrong HTTP method required POST")
555		return nil, err
556	}
557
558	var update Update
559	err := json.NewDecoder(r.Body).Decode(&update)
560	if err != nil {
561		return nil, err
562	}
563
564	return &update, nil
565}
566
567// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
568//
569// It doesn't support uploading files.
570//
571// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
572// for details.
573func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
574	params, err := c.params()
575	if err != nil {
576		return err
577	}
578
579	if t, ok := c.(Fileable); ok {
580		if hasFilesNeedingUpload(t.files()) {
581			return errors.New("unable to use http response to upload files")
582		}
583	}
584
585	values := buildParams(params)
586	values.Set("method", c.method())
587
588	w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
589	_, err = w.Write([]byte(values.Encode()))
590	return err
591}
592
593// GetChat gets information about a chat.
594func (bot *BotAPI) GetChat(config ChatInfoConfig) (ChatFullInfo, error) {
595	resp, err := bot.Request(config)
596	if err != nil {
597		return ChatFullInfo{}, err
598	}
599
600	var chat ChatFullInfo
601	err = json.Unmarshal(resp.Result, &chat)
602
603	return chat, err
604}
605
606// GetChatAdministrators gets a list of administrators in the chat.
607//
608// If none have been appointed, only the creator will be returned.
609// Bots are not shown, even if they are an administrator.
610func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
611	resp, err := bot.Request(config)
612	if err != nil {
613		return []ChatMember{}, err
614	}
615
616	var members []ChatMember
617	err = json.Unmarshal(resp.Result, &members)
618
619	return members, err
620}
621
622// GetChatMembersCount gets the number of users in a chat.
623func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
624	resp, err := bot.Request(config)
625	if err != nil {
626		return -1, err
627	}
628
629	var count int
630	err = json.Unmarshal(resp.Result, &count)
631
632	return count, err
633}
634
635// GetChatMember gets a specific chat member.
636func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
637	resp, err := bot.Request(config)
638	if err != nil {
639		return ChatMember{}, err
640	}
641
642	var member ChatMember
643	err = json.Unmarshal(resp.Result, &member)
644
645	return member, err
646}
647
648// GetGameHighScores allows you to get the high scores for a game.
649func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
650	resp, err := bot.Request(config)
651	if err != nil {
652		return []GameHighScore{}, err
653	}
654
655	var highScores []GameHighScore
656	err = json.Unmarshal(resp.Result, &highScores)
657
658	return highScores, err
659}
660
661// GetInviteLink get InviteLink for a chat
662func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
663	resp, err := bot.Request(config)
664	if err != nil {
665		return "", err
666	}
667
668	var inviteLink string
669	err = json.Unmarshal(resp.Result, &inviteLink)
670
671	return inviteLink, err
672}
673
674// GetStickerSet returns a StickerSet.
675func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
676	resp, err := bot.Request(config)
677	if err != nil {
678		return StickerSet{}, err
679	}
680
681	var stickerSet StickerSet
682	err = json.Unmarshal(resp.Result, &stickerSet)
683
684	return stickerSet, err
685}
686
687// GetCustomEmojiStickers returns a slice of Sticker objects.
688func (bot *BotAPI) GetCustomEmojiStickers(config GetCustomEmojiStickersConfig) ([]Sticker, error) {
689	resp, err := bot.Request(config)
690	if err != nil {
691		return []Sticker{}, err
692	}
693
694	var stickers []Sticker
695	err = json.Unmarshal(resp.Result, &stickers)
696
697	return stickers, err
698}
699
700// StopPoll stops a poll and returns the result.
701func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
702	resp, err := bot.Request(config)
703	if err != nil {
704		return Poll{}, err
705	}
706
707	var poll Poll
708	err = json.Unmarshal(resp.Result, &poll)
709
710	return poll, err
711}
712
713// GetMyCommands gets the currently registered commands.
714func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
715	return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
716}
717
718// GetMyCommandsWithConfig gets the currently registered commands with a config.
719func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
720	resp, err := bot.Request(config)
721	if err != nil {
722		return nil, err
723	}
724
725	var commands []BotCommand
726	err = json.Unmarshal(resp.Result, &commands)
727
728	return commands, err
729}
730
731// CopyMessage copy messages of any kind. The method is analogous to the method
732// forwardMessage, but the copied message doesn't have a link to the original
733// message. Returns the MessageID of the sent message on success.
734func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
735	resp, err := bot.Request(config)
736	if err != nil {
737		return MessageID{}, err
738	}
739
740	var messageID MessageID
741	err = json.Unmarshal(resp.Result, &messageID)
742
743	return messageID, err
744}
745
746// AnswerWebAppQuery sets the result of an interaction with a Web App and send a
747// corresponding message on behalf of the user to the chat from which the query originated.
748func (bot *BotAPI) AnswerWebAppQuery(config AnswerWebAppQueryConfig) (SentWebAppMessage, error) {
749	var sentWebAppMessage SentWebAppMessage
750
751	resp, err := bot.Request(config)
752	if err != nil {
753		return sentWebAppMessage, err
754	}
755
756	err = json.Unmarshal(resp.Result, &sentWebAppMessage)
757	return sentWebAppMessage, err
758}
759
760// GetMyDefaultAdministratorRights gets the current default administrator rights of the bot.
761func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministratorRightsConfig) (ChatAdministratorRights, error) {
762	var rights ChatAdministratorRights
763
764	resp, err := bot.Request(config)
765	if err != nil {
766		return rights, err
767	}
768
769	err = json.Unmarshal(resp.Result, &rights)
770	return rights, err
771}
772
773// EscapeText takes an input text and escape Telegram markup symbols.
774// In this way we can send a text without being afraid of having to escape the characters manually.
775// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
776// If there is an error, an empty string will be returned.
777//
778// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
779// text is the input string that will be escaped
780func EscapeText(parseMode string, text string) string {
781	var replacer *strings.Replacer
782
783	if parseMode == ModeHTML {
784		replacer = strings.NewReplacer("<", "&lt;", ">", "&gt;", "&", "&amp;")
785	} else if parseMode == ModeMarkdown {
786		replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
787	} else if parseMode == ModeMarkdownV2 {
788		replacer = strings.NewReplacer(
789			"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
790			"\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
791			"#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
792			"\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
793		)
794	} else {
795		return ""
796	}
797
798	return replacer.Replace(text)
799}