all repos — telegram-bot-api @ 11b1a666629f22c85a2e99330efbc06a319711db

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				log.Println(err)
477				log.Println("Failed to get updates, retrying in 3 seconds...")
478				time.Sleep(time.Second * 3)
479
480				continue
481			}
482
483			for _, update := range updates {
484				if update.UpdateID >= config.Offset {
485					config.Offset = update.UpdateID + 1
486					ch <- update
487				}
488			}
489		}
490	}()
491
492	return ch
493}
494
495// StopReceivingUpdates stops the go routine which receives updates
496func (bot *BotAPI) StopReceivingUpdates() {
497	bot.mu.Lock()
498	defer bot.mu.Unlock()
499
500	if bot.Debug {
501		log.Println("Stopping the update receiver routine...")
502	}
503	for _, stopper := range bot.stoppers {
504		stopper()
505	}
506}
507
508// ListenForWebhook registers a http handler for a webhook.
509func (bot *BotAPI) ListenForWebhook(pattern string) UpdatesChannel {
510	ch := make(chan Update, bot.Buffer)
511
512	http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
513		update, err := bot.HandleUpdate(r)
514		if err != nil {
515			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
516			w.WriteHeader(http.StatusBadRequest)
517			w.Header().Set("Content-Type", "application/json")
518			_, _ = w.Write(errMsg)
519			return
520		}
521
522		ch <- *update
523	})
524
525	return ch
526}
527
528// ListenForWebhookRespReqFormat registers a http handler for a single incoming webhook.
529func (bot *BotAPI) ListenForWebhookRespReqFormat(w http.ResponseWriter, r *http.Request) UpdatesChannel {
530	ch := make(chan Update, bot.Buffer)
531
532	func(w http.ResponseWriter, r *http.Request) {
533		defer close(ch)
534
535		update, err := bot.HandleUpdate(r)
536		if err != nil {
537			errMsg, _ := json.Marshal(map[string]string{"error": err.Error()})
538			w.WriteHeader(http.StatusBadRequest)
539			w.Header().Set("Content-Type", "application/json")
540			_, _ = w.Write(errMsg)
541			return
542		}
543
544		ch <- *update
545	}(w, r)
546
547	return ch
548}
549
550// HandleUpdate parses and returns update received via webhook
551func (bot *BotAPI) HandleUpdate(r *http.Request) (*Update, error) {
552	if r.Method != http.MethodPost {
553		err := errors.New("wrong HTTP method required POST")
554		return nil, err
555	}
556
557	var update Update
558	err := json.NewDecoder(r.Body).Decode(&update)
559	if err != nil {
560		return nil, err
561	}
562
563	return &update, nil
564}
565
566// WriteToHTTPResponse writes the request to the HTTP ResponseWriter.
567//
568// It doesn't support uploading files.
569//
570// See https://core.telegram.org/bots/api#making-requests-when-getting-updates
571// for details.
572func WriteToHTTPResponse(w http.ResponseWriter, c Chattable) error {
573	params, err := c.params()
574	if err != nil {
575		return err
576	}
577
578	if t, ok := c.(Fileable); ok {
579		if hasFilesNeedingUpload(t.files()) {
580			return errors.New("unable to use http response to upload files")
581		}
582	}
583
584	values := buildParams(params)
585	values.Set("method", c.method())
586
587	w.Header().Set("Content-Type", "application/x-www-form-urlencoded")
588	_, err = w.Write([]byte(values.Encode()))
589	return err
590}
591
592// GetChat gets information about a chat.
593func (bot *BotAPI) GetChat(config ChatInfoConfig) (ChatFullInfo, error) {
594	resp, err := bot.Request(config)
595	if err != nil {
596		return ChatFullInfo{}, err
597	}
598
599	var chat ChatFullInfo
600	err = json.Unmarshal(resp.Result, &chat)
601
602	return chat, err
603}
604
605// GetChatAdministrators gets a list of administrators in the chat.
606//
607// If none have been appointed, only the creator will be returned.
608// Bots are not shown, even if they are an administrator.
609func (bot *BotAPI) GetChatAdministrators(config ChatAdministratorsConfig) ([]ChatMember, error) {
610	resp, err := bot.Request(config)
611	if err != nil {
612		return []ChatMember{}, err
613	}
614
615	var members []ChatMember
616	err = json.Unmarshal(resp.Result, &members)
617
618	return members, err
619}
620
621// GetChatMembersCount gets the number of users in a chat.
622func (bot *BotAPI) GetChatMembersCount(config ChatMemberCountConfig) (int, error) {
623	resp, err := bot.Request(config)
624	if err != nil {
625		return -1, err
626	}
627
628	var count int
629	err = json.Unmarshal(resp.Result, &count)
630
631	return count, err
632}
633
634// GetChatMember gets a specific chat member.
635func (bot *BotAPI) GetChatMember(config GetChatMemberConfig) (ChatMember, error) {
636	resp, err := bot.Request(config)
637	if err != nil {
638		return ChatMember{}, err
639	}
640
641	var member ChatMember
642	err = json.Unmarshal(resp.Result, &member)
643
644	return member, err
645}
646
647// GetGameHighScores allows you to get the high scores for a game.
648func (bot *BotAPI) GetGameHighScores(config GetGameHighScoresConfig) ([]GameHighScore, error) {
649	resp, err := bot.Request(config)
650	if err != nil {
651		return []GameHighScore{}, err
652	}
653
654	var highScores []GameHighScore
655	err = json.Unmarshal(resp.Result, &highScores)
656
657	return highScores, err
658}
659
660// GetInviteLink get InviteLink for a chat
661func (bot *BotAPI) GetInviteLink(config ChatInviteLinkConfig) (string, error) {
662	resp, err := bot.Request(config)
663	if err != nil {
664		return "", err
665	}
666
667	var inviteLink string
668	err = json.Unmarshal(resp.Result, &inviteLink)
669
670	return inviteLink, err
671}
672
673// GetStickerSet returns a StickerSet.
674func (bot *BotAPI) GetStickerSet(config GetStickerSetConfig) (StickerSet, error) {
675	resp, err := bot.Request(config)
676	if err != nil {
677		return StickerSet{}, err
678	}
679
680	var stickerSet StickerSet
681	err = json.Unmarshal(resp.Result, &stickerSet)
682
683	return stickerSet, err
684}
685
686// GetCustomEmojiStickers returns a slice of Sticker objects.
687func (bot *BotAPI) GetCustomEmojiStickers(config GetCustomEmojiStickersConfig) ([]Sticker, error) {
688	resp, err := bot.Request(config)
689	if err != nil {
690		return []Sticker{}, err
691	}
692
693	var stickers []Sticker
694	err = json.Unmarshal(resp.Result, &stickers)
695
696	return stickers, err
697}
698
699// StopPoll stops a poll and returns the result.
700func (bot *BotAPI) StopPoll(config StopPollConfig) (Poll, error) {
701	resp, err := bot.Request(config)
702	if err != nil {
703		return Poll{}, err
704	}
705
706	var poll Poll
707	err = json.Unmarshal(resp.Result, &poll)
708
709	return poll, err
710}
711
712// GetMyCommands gets the currently registered commands.
713func (bot *BotAPI) GetMyCommands() ([]BotCommand, error) {
714	return bot.GetMyCommandsWithConfig(GetMyCommandsConfig{})
715}
716
717// GetMyCommandsWithConfig gets the currently registered commands with a config.
718func (bot *BotAPI) GetMyCommandsWithConfig(config GetMyCommandsConfig) ([]BotCommand, error) {
719	resp, err := bot.Request(config)
720	if err != nil {
721		return nil, err
722	}
723
724	var commands []BotCommand
725	err = json.Unmarshal(resp.Result, &commands)
726
727	return commands, err
728}
729
730// CopyMessage copy messages of any kind. The method is analogous to the method
731// forwardMessage, but the copied message doesn't have a link to the original
732// message. Returns the MessageID of the sent message on success.
733func (bot *BotAPI) CopyMessage(config CopyMessageConfig) (MessageID, error) {
734	resp, err := bot.Request(config)
735	if err != nil {
736		return MessageID{}, err
737	}
738
739	var messageID MessageID
740	err = json.Unmarshal(resp.Result, &messageID)
741
742	return messageID, err
743}
744
745// AnswerWebAppQuery sets the result of an interaction with a Web App and send a
746// corresponding message on behalf of the user to the chat from which the query originated.
747func (bot *BotAPI) AnswerWebAppQuery(config AnswerWebAppQueryConfig) (SentWebAppMessage, error) {
748	var sentWebAppMessage SentWebAppMessage
749
750	resp, err := bot.Request(config)
751	if err != nil {
752		return sentWebAppMessage, err
753	}
754
755	err = json.Unmarshal(resp.Result, &sentWebAppMessage)
756	return sentWebAppMessage, err
757}
758
759// GetMyDefaultAdministratorRights gets the current default administrator rights of the bot.
760func (bot *BotAPI) GetMyDefaultAdministratorRights(config GetMyDefaultAdministratorRightsConfig) (ChatAdministratorRights, error) {
761	var rights ChatAdministratorRights
762
763	resp, err := bot.Request(config)
764	if err != nil {
765		return rights, err
766	}
767
768	err = json.Unmarshal(resp.Result, &rights)
769	return rights, err
770}
771
772// EscapeText takes an input text and escape Telegram markup symbols.
773// In this way we can send a text without being afraid of having to escape the characters manually.
774// Note that you don't have to include the formatting style in the input text, or it will be escaped too.
775// If there is an error, an empty string will be returned.
776//
777// parseMode is the text formatting mode (ModeMarkdown, ModeMarkdownV2 or ModeHTML)
778// text is the input string that will be escaped
779func EscapeText(parseMode string, text string) string {
780	var replacer *strings.Replacer
781
782	if parseMode == ModeHTML {
783		replacer = strings.NewReplacer("<", "&lt;", ">", "&gt;", "&", "&amp;")
784	} else if parseMode == ModeMarkdown {
785		replacer = strings.NewReplacer("_", "\\_", "*", "\\*", "`", "\\`", "[", "\\[")
786	} else if parseMode == ModeMarkdownV2 {
787		replacer = strings.NewReplacer(
788			"_", "\\_", "*", "\\*", "[", "\\[", "]", "\\]", "(",
789			"\\(", ")", "\\)", "~", "\\~", "`", "\\`", ">", "\\>",
790			"#", "\\#", "+", "\\+", "-", "\\-", "=", "\\=", "|",
791			"\\|", "{", "\\{", "}", "\\}", ".", "\\.", "!", "\\!",
792		)
793	} else {
794		return ""
795	}
796
797	return replacer.Replace(text)
798}