github.com/line/line-bot-sdk-go/v7@v7.21.0/examples/kitchensink/server.go (about)

     1  // Copyright 2016 LINE Corporation
     2  //
     3  // LINE Corporation licenses this file to you under the Apache License,
     4  // version 2.0 (the "License"); you may not use this file except in compliance
     5  // with the License. You may obtain a copy of the License at:
     6  //
     7  //   http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
    11  // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
    12  // License for the specific language governing permissions and limitations
    13  // under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"fmt"
    19  	"io"
    20  	"log"
    21  	"net/http"
    22  	"os"
    23  	"os/exec"
    24  	"path/filepath"
    25  
    26  	"github.com/line/line-bot-sdk-go/v7/linebot"
    27  )
    28  
    29  func main() {
    30  	app, err := NewKitchenSink(
    31  		os.Getenv("CHANNEL_SECRET"),
    32  		os.Getenv("CHANNEL_TOKEN"),
    33  		os.Getenv("APP_BASE_URL"),
    34  	)
    35  	if err != nil {
    36  		log.Fatal(err)
    37  	}
    38  
    39  	// serve /static/** files
    40  	staticFileServer := http.FileServer(http.Dir("static"))
    41  	http.HandleFunc("/static/", http.StripPrefix("/static/", staticFileServer).ServeHTTP)
    42  	// serve /downloaded/** files
    43  	downloadedFileServer := http.FileServer(http.Dir(app.downloadDir))
    44  	http.HandleFunc("/downloaded/", http.StripPrefix("/downloaded/", downloadedFileServer).ServeHTTP)
    45  
    46  	http.HandleFunc("/callback", app.Callback)
    47  	// This is just a sample code.
    48  	// For actually use, you must support HTTPS by using `ListenAndServeTLS`, reverse proxy or etc.
    49  	if err := http.ListenAndServe(":"+os.Getenv("PORT"), nil); err != nil {
    50  		log.Fatal(err)
    51  	}
    52  }
    53  
    54  // KitchenSink app
    55  type KitchenSink struct {
    56  	bot         *linebot.Client
    57  	appBaseURL  string
    58  	downloadDir string
    59  }
    60  
    61  // NewKitchenSink function
    62  func NewKitchenSink(channelSecret, channelToken, appBaseURL string) (*KitchenSink, error) {
    63  	apiEndpointBase := os.Getenv("ENDPOINT_BASE")
    64  	if apiEndpointBase == "" {
    65  		apiEndpointBase = linebot.APIEndpointBase
    66  	}
    67  	bot, err := linebot.New(
    68  		channelSecret,
    69  		channelToken,
    70  		linebot.WithEndpointBase(apiEndpointBase), // Usually you omit this.
    71  	)
    72  	if err != nil {
    73  		return nil, err
    74  	}
    75  	downloadDir := filepath.Join(filepath.Dir(os.Args[0]), "line-bot")
    76  	_, err = os.Stat(downloadDir)
    77  	if err != nil {
    78  		if err := os.Mkdir(downloadDir, 0777); err != nil {
    79  			return nil, err
    80  		}
    81  	}
    82  	return &KitchenSink{
    83  		bot:         bot,
    84  		appBaseURL:  appBaseURL,
    85  		downloadDir: downloadDir,
    86  	}, nil
    87  }
    88  
    89  // Callback function for http server
    90  func (app *KitchenSink) Callback(w http.ResponseWriter, r *http.Request) {
    91  	events, err := app.bot.ParseRequest(r)
    92  	if err != nil {
    93  		if err == linebot.ErrInvalidSignature {
    94  			w.WriteHeader(400)
    95  		} else {
    96  			w.WriteHeader(500)
    97  		}
    98  		return
    99  	}
   100  	for _, event := range events {
   101  		log.Printf("Got event %v", event)
   102  		switch event.Type {
   103  		case linebot.EventTypeMessage:
   104  			switch message := event.Message.(type) {
   105  			case *linebot.TextMessage:
   106  				if err := app.handleText(message, event.ReplyToken, event.Source); err != nil {
   107  					log.Print(err)
   108  				}
   109  			case *linebot.ImageMessage:
   110  				if err := app.handleImage(message, event.ReplyToken); err != nil {
   111  					log.Print(err)
   112  				}
   113  			case *linebot.VideoMessage:
   114  				if err := app.handleVideo(message, event.ReplyToken); err != nil {
   115  					log.Print(err)
   116  				}
   117  			case *linebot.AudioMessage:
   118  				if err := app.handleAudio(message, event.ReplyToken); err != nil {
   119  					log.Print(err)
   120  				}
   121  			case *linebot.FileMessage:
   122  				if err := app.handleFile(message, event.ReplyToken); err != nil {
   123  					log.Print(err)
   124  				}
   125  			case *linebot.LocationMessage:
   126  				if err := app.handleLocation(message, event.ReplyToken); err != nil {
   127  					log.Print(err)
   128  				}
   129  			case *linebot.StickerMessage:
   130  				if err := app.handleSticker(message, event.ReplyToken); err != nil {
   131  					log.Print(err)
   132  				}
   133  			default:
   134  				log.Printf("Unknown message: %v", message)
   135  			}
   136  		case linebot.EventTypeFollow:
   137  			if err := app.replyText(event.ReplyToken, "Got followed event"); err != nil {
   138  				log.Print(err)
   139  			}
   140  		case linebot.EventTypeUnfollow:
   141  			log.Printf("Unfollowed this bot: %v", event)
   142  		case linebot.EventTypeJoin:
   143  			if err := app.replyText(event.ReplyToken, "Joined "+string(event.Source.Type)); err != nil {
   144  				log.Print(err)
   145  			}
   146  		case linebot.EventTypeLeave:
   147  			log.Printf("Left: %v", event)
   148  		case linebot.EventTypePostback:
   149  			data := event.Postback.Data
   150  			if data == "DATE" || data == "TIME" || data == "DATETIME" {
   151  				data += fmt.Sprintf("(%v)", *event.Postback.Params)
   152  			}
   153  			if err := app.replyText(event.ReplyToken, "Got postback: "+data); err != nil {
   154  				log.Print(err)
   155  			}
   156  		case linebot.EventTypeBeacon:
   157  			if err := app.replyText(event.ReplyToken, "Got beacon: "+event.Beacon.Hwid); err != nil {
   158  				log.Print(err)
   159  			}
   160  		default:
   161  			log.Printf("Unknown event: %v", event)
   162  		}
   163  	}
   164  }
   165  
   166  func (app *KitchenSink) handleText(message *linebot.TextMessage, replyToken string, source *linebot.EventSource) error {
   167  	switch message.Text {
   168  	case "profile":
   169  		if source.UserID != "" {
   170  			profile, err := app.bot.GetProfile(source.UserID).Do()
   171  			if err != nil {
   172  				return app.replyText(replyToken, err.Error())
   173  			}
   174  			if _, err := app.bot.ReplyMessage(
   175  				replyToken,
   176  				linebot.NewTextMessage("Display name: "+profile.DisplayName),
   177  				linebot.NewTextMessage("Status message: "+profile.StatusMessage),
   178  			).Do(); err != nil {
   179  				return err
   180  			}
   181  		} else {
   182  			return app.replyText(replyToken, "Bot can't use profile API without user ID")
   183  		}
   184  	case "buttons":
   185  		imageURL := app.appBaseURL + "/static/buttons/1040.jpg"
   186  		template := linebot.NewButtonsTemplate(
   187  			imageURL, "My button sample", "Hello, my button",
   188  			linebot.NewURIAction("Go to line.me", "https://line.me"),
   189  			linebot.NewPostbackAction("Say hello1", "hello こんにちは", "", "hello こんにちは", "", ""),
   190  			linebot.NewPostbackAction("言 hello2", "hello こんにちは", "hello こんにちは", "", "", ""),
   191  			linebot.NewMessageAction("Say message", "Rice=米"),
   192  		)
   193  		if _, err := app.bot.ReplyMessage(
   194  			replyToken,
   195  			linebot.NewTemplateMessage("Buttons alt text", template),
   196  		).Do(); err != nil {
   197  			return err
   198  		}
   199  	case "confirm":
   200  		template := linebot.NewConfirmTemplate(
   201  			"Do it?",
   202  			linebot.NewMessageAction("Yes", "Yes!"),
   203  			linebot.NewMessageAction("No", "No!"),
   204  		)
   205  		if _, err := app.bot.ReplyMessage(
   206  			replyToken,
   207  			linebot.NewTemplateMessage("Confirm alt text", template),
   208  		).Do(); err != nil {
   209  			return err
   210  		}
   211  	case "carousel":
   212  		imageURL := app.appBaseURL + "/static/buttons/1040.jpg"
   213  		template := linebot.NewCarouselTemplate(
   214  			linebot.NewCarouselColumn(
   215  				imageURL, "hoge", "fuga",
   216  				linebot.NewURIAction("Go to line.me", "https://line.me"),
   217  				linebot.NewPostbackAction("Say hello1", "hello こんにちは", "", "", "", ""),
   218  			),
   219  			linebot.NewCarouselColumn(
   220  				imageURL, "hoge", "fuga",
   221  				linebot.NewPostbackAction("言 hello2", "hello こんにちは", "hello こんにちは", "", "", ""),
   222  				linebot.NewMessageAction("Say message", "Rice=米"),
   223  			),
   224  		)
   225  		if _, err := app.bot.ReplyMessage(
   226  			replyToken,
   227  			linebot.NewTemplateMessage("Carousel alt text", template),
   228  		).Do(); err != nil {
   229  			return err
   230  		}
   231  	case "image carousel":
   232  		imageURL := app.appBaseURL + "/static/buttons/1040.jpg"
   233  		template := linebot.NewImageCarouselTemplate(
   234  			linebot.NewImageCarouselColumn(
   235  				imageURL,
   236  				linebot.NewURIAction("Go to LINE", "https://line.me"),
   237  			),
   238  			linebot.NewImageCarouselColumn(
   239  				imageURL,
   240  				linebot.NewPostbackAction("Say hello1", "hello こんにちは", "", "", "", ""),
   241  			),
   242  			linebot.NewImageCarouselColumn(
   243  				imageURL,
   244  				linebot.NewMessageAction("Say message", "Rice=米"),
   245  			),
   246  			linebot.NewImageCarouselColumn(
   247  				imageURL,
   248  				linebot.NewDatetimePickerAction("datetime", "DATETIME", "datetime", "", "", ""),
   249  			),
   250  		)
   251  		if _, err := app.bot.ReplyMessage(
   252  			replyToken,
   253  			linebot.NewTemplateMessage("Image carousel alt text", template),
   254  		).Do(); err != nil {
   255  			return err
   256  		}
   257  	case "datetime":
   258  		template := linebot.NewButtonsTemplate(
   259  			"", "", "Select date / time !",
   260  			linebot.NewDatetimePickerAction("date", "DATE", "date", "", "", ""),
   261  			linebot.NewDatetimePickerAction("time", "TIME", "time", "", "", ""),
   262  			linebot.NewDatetimePickerAction("datetime", "DATETIME", "datetime", "", "", ""),
   263  		)
   264  		if _, err := app.bot.ReplyMessage(
   265  			replyToken,
   266  			linebot.NewTemplateMessage("Datetime pickers alt text", template),
   267  		).Do(); err != nil {
   268  			return err
   269  		}
   270  	case "flex":
   271  		// {
   272  		//   "type": "bubble",
   273  		//   "body": {
   274  		//     "type": "box",
   275  		//     "layout": "horizontal",
   276  		//     "contents": [
   277  		//       {
   278  		//         "type": "text",
   279  		//         "text": "Hello,"
   280  		//       },
   281  		//       {
   282  		//         "type": "text",
   283  		//         "text": "World!"
   284  		//       }
   285  		//     ]
   286  		//   }
   287  		// }
   288  		contents := &linebot.BubbleContainer{
   289  			Type: linebot.FlexContainerTypeBubble,
   290  			Body: &linebot.BoxComponent{
   291  				Type:   linebot.FlexComponentTypeBox,
   292  				Layout: linebot.FlexBoxLayoutTypeHorizontal,
   293  				Contents: []linebot.FlexComponent{
   294  					&linebot.TextComponent{
   295  						Type: linebot.FlexComponentTypeText,
   296  						Text: "Hello,",
   297  					},
   298  					&linebot.TextComponent{
   299  						Type: linebot.FlexComponentTypeText,
   300  						Text: "World!",
   301  					},
   302  				},
   303  			},
   304  		}
   305  		if _, err := app.bot.ReplyMessage(
   306  			replyToken,
   307  			linebot.NewFlexMessage("Flex message alt text", contents),
   308  		).Do(); err != nil {
   309  			return err
   310  		}
   311  	case "flex carousel":
   312  		// {
   313  		//   "type": "carousel",
   314  		//   "contents": [
   315  		//     {
   316  		//       "type": "bubble",
   317  		//       "body": {
   318  		//         "type": "box",
   319  		//         "layout": "vertical",
   320  		//         "contents": [
   321  		//           {
   322  		//             "type": "text",
   323  		//             "text": "First bubble"
   324  		//           }
   325  		//         ]
   326  		//       }
   327  		//     },
   328  		//     {
   329  		//       "type": "bubble",
   330  		//       "body": {
   331  		//         "type": "box",
   332  		//         "layout": "vertical",
   333  		//         "contents": [
   334  		//           {
   335  		//             "type": "text",
   336  		//             "text": "Second bubble"
   337  		//           }
   338  		//         ]
   339  		//       }
   340  		//     }
   341  		//   ]
   342  		// }
   343  		contents := &linebot.CarouselContainer{
   344  			Type: linebot.FlexContainerTypeCarousel,
   345  			Contents: []*linebot.BubbleContainer{
   346  				{
   347  					Type: linebot.FlexContainerTypeBubble,
   348  					Body: &linebot.BoxComponent{
   349  						Type:   linebot.FlexComponentTypeBox,
   350  						Layout: linebot.FlexBoxLayoutTypeVertical,
   351  						Contents: []linebot.FlexComponent{
   352  							&linebot.TextComponent{
   353  								Type: linebot.FlexComponentTypeText,
   354  								Text: "First bubble",
   355  							},
   356  						},
   357  					},
   358  				},
   359  				{
   360  					Type: linebot.FlexContainerTypeBubble,
   361  					Body: &linebot.BoxComponent{
   362  						Type:   linebot.FlexComponentTypeBox,
   363  						Layout: linebot.FlexBoxLayoutTypeVertical,
   364  						Contents: []linebot.FlexComponent{
   365  							&linebot.TextComponent{
   366  								Type: linebot.FlexComponentTypeText,
   367  								Text: "Second bubble",
   368  							},
   369  						},
   370  					},
   371  				},
   372  			},
   373  		}
   374  		if _, err := app.bot.ReplyMessage(
   375  			replyToken,
   376  			linebot.NewFlexMessage("Flex message alt text", contents),
   377  		).Do(); err != nil {
   378  			return err
   379  		}
   380  	case "flex json":
   381  		jsonString := `{
   382    "type": "bubble",
   383    "hero": {
   384      "type": "image",
   385      "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/01_1_cafe.png",
   386      "size": "full",
   387      "aspectRatio": "20:13",
   388      "aspectMode": "cover",
   389      "action": {
   390        "type": "uri",
   391        "uri": "http://linecorp.com/"
   392      }
   393    },
   394    "body": {
   395      "type": "box",
   396      "layout": "vertical",
   397      "contents": [
   398        {
   399          "type": "text",
   400          "text": "Brown Cafe",
   401          "weight": "bold",
   402          "size": "xl"
   403        },
   404        {
   405          "type": "box",
   406          "layout": "baseline",
   407          "margin": "md",
   408          "contents": [
   409            {
   410              "type": "icon",
   411              "size": "sm",
   412              "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
   413            },
   414            {
   415              "type": "icon",
   416              "size": "sm",
   417              "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
   418            },
   419            {
   420              "type": "icon",
   421              "size": "sm",
   422              "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
   423            },
   424            {
   425              "type": "icon",
   426              "size": "sm",
   427              "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
   428            },
   429            {
   430              "type": "icon",
   431              "size": "sm",
   432              "url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
   433            },
   434            {
   435              "type": "text",
   436              "text": "4.0",
   437              "size": "sm",
   438              "color": "#999999",
   439              "margin": "md",
   440              "flex": 0
   441            }
   442          ]
   443        },
   444        {
   445          "type": "box",
   446          "layout": "vertical",
   447          "margin": "lg",
   448          "spacing": "sm",
   449          "contents": [
   450            {
   451              "type": "box",
   452              "layout": "baseline",
   453              "spacing": "sm",
   454              "contents": [
   455                {
   456                  "type": "text",
   457                  "text": "Place",
   458                  "color": "#aaaaaa",
   459                  "size": "sm",
   460                  "flex": 1
   461                },
   462                {
   463                  "type": "text",
   464                  "text": "Miraina Tower, 4-1-6 Shinjuku, Tokyo",
   465                  "wrap": true,
   466                  "color": "#666666",
   467                  "size": "sm",
   468                  "flex": 5
   469                }
   470              ]
   471            },
   472            {
   473              "type": "box",
   474              "layout": "baseline",
   475              "spacing": "sm",
   476              "contents": [
   477                {
   478                  "type": "text",
   479                  "text": "Time",
   480                  "color": "#aaaaaa",
   481                  "size": "sm",
   482                  "flex": 1
   483                },
   484                {
   485                  "type": "text",
   486                  "text": "10:00 - 23:00",
   487                  "wrap": true,
   488                  "color": "#666666",
   489                  "size": "sm",
   490                  "flex": 5
   491                }
   492              ]
   493            }
   494          ]
   495        }
   496      ]
   497    },
   498    "footer": {
   499      "type": "box",
   500      "layout": "vertical",
   501      "spacing": "sm",
   502      "contents": [
   503        {
   504          "type": "button",
   505          "style": "link",
   506          "height": "sm",
   507          "action": {
   508            "type": "uri",
   509            "label": "CALL",
   510            "uri": "https://linecorp.com"
   511          }
   512        },
   513        {
   514          "type": "button",
   515          "style": "link",
   516          "height": "sm",
   517          "action": {
   518            "type": "uri",
   519            "label": "WEBSITE",
   520            "uri": "https://linecorp.com",
   521            "altUri": {
   522              "desktop": "https://line.me/ja/download"
   523            }
   524          }
   525        },
   526        {
   527          "type": "spacer",
   528          "size": "sm"
   529        }
   530      ],
   531      "flex": 0
   532    }
   533  }`
   534  		contents, err := linebot.UnmarshalFlexMessageJSON([]byte(jsonString))
   535  		if err != nil {
   536  			return err
   537  		}
   538  		if _, err := app.bot.ReplyMessage(
   539  			replyToken,
   540  			linebot.NewFlexMessage("Flex message alt text", contents),
   541  		).Do(); err != nil {
   542  			return err
   543  		}
   544  	case "imagemap":
   545  		if _, err := app.bot.ReplyMessage(
   546  			replyToken,
   547  			linebot.NewImagemapMessage(
   548  				app.appBaseURL+"/static/rich",
   549  				"Imagemap alt text",
   550  				linebot.ImagemapBaseSize{Width: 1040, Height: 1040},
   551  				linebot.NewURIImagemapAction("LINE Store Manga", "https://store.line.me/family/manga/en", linebot.ImagemapArea{X: 0, Y: 0, Width: 520, Height: 520}),
   552  				linebot.NewURIImagemapAction("LINE Store Music", "https://store.line.me/family/music/en", linebot.ImagemapArea{X: 520, Y: 0, Width: 520, Height: 520}),
   553  				linebot.NewURIImagemapAction("LINE Store Play", "https://store.line.me/family/play/en", linebot.ImagemapArea{X: 0, Y: 520, Width: 520, Height: 520}),
   554  				linebot.NewMessageImagemapAction("URANAI!", "URANAI!", linebot.ImagemapArea{X: 520, Y: 520, Width: 520, Height: 520}),
   555  			),
   556  		).Do(); err != nil {
   557  			return err
   558  		}
   559  	case "imagemap video":
   560  		if _, err := app.bot.ReplyMessage(
   561  			replyToken,
   562  			linebot.NewImagemapMessage(
   563  				app.appBaseURL+"/static/rich",
   564  				"Imagemap with video alt text",
   565  				linebot.ImagemapBaseSize{Width: 1040, Height: 1040},
   566  				linebot.NewURIImagemapAction("LINE Store Manga", "https://store.line.me/family/manga/en", linebot.ImagemapArea{X: 0, Y: 0, Width: 520, Height: 520}),
   567  				linebot.NewURIImagemapAction("LINE Store Music", "https://store.line.me/family/music/en", linebot.ImagemapArea{X: 520, Y: 0, Width: 520, Height: 520}),
   568  				linebot.NewURIImagemapAction("LINE Store Play", "https://store.line.me/family/play/en", linebot.ImagemapArea{X: 0, Y: 520, Width: 520, Height: 520}),
   569  				linebot.NewMessageImagemapAction("URANAI!", "URANAI!", linebot.ImagemapArea{X: 520, Y: 520, Width: 520, Height: 520}),
   570  			).WithVideo(&linebot.ImagemapVideo{
   571  				OriginalContentURL: app.appBaseURL + "/static/imagemap/video.mp4",
   572  				PreviewImageURL:    app.appBaseURL + "/static/imagemap/preview.jpg",
   573  				Area:               linebot.ImagemapArea{X: 280, Y: 385, Width: 480, Height: 270},
   574  				ExternalLink:       &linebot.ImagemapVideoExternalLink{LinkURI: "https://line.me", Label: "LINE"},
   575  			}),
   576  		).Do(); err != nil {
   577  			return err
   578  		}
   579  	case "quick":
   580  		if _, err := app.bot.ReplyMessage(
   581  			replyToken,
   582  			linebot.NewTextMessage("Select your favorite food category or send me your location!").
   583  				WithQuickReplies(linebot.NewQuickReplyItems(
   584  					linebot.NewQuickReplyButton(
   585  						app.appBaseURL+"/static/quick/sushi.png",
   586  						linebot.NewMessageAction("Sushi", "Sushi")),
   587  					linebot.NewQuickReplyButton(
   588  						app.appBaseURL+"/static/quick/tempura.png",
   589  						linebot.NewMessageAction("Tempura", "Tempura")),
   590  					linebot.NewQuickReplyButton(
   591  						"",
   592  						linebot.NewLocationAction("Send location")),
   593  					linebot.NewQuickReplyButton(
   594  						"",
   595  						linebot.NewURIAction("LINE Developer", "https://developers.line.biz/")),
   596  				)),
   597  		).Do(); err != nil {
   598  			return err
   599  		}
   600  	case "bye":
   601  		switch source.Type {
   602  		case linebot.EventSourceTypeUser:
   603  			return app.replyText(replyToken, "Bot can't leave from 1:1 chat")
   604  		case linebot.EventSourceTypeGroup:
   605  			if err := app.replyText(replyToken, "Leaving group"); err != nil {
   606  				return err
   607  			}
   608  			if _, err := app.bot.LeaveGroup(source.GroupID).Do(); err != nil {
   609  				return app.replyText(replyToken, err.Error())
   610  			}
   611  		case linebot.EventSourceTypeRoom:
   612  			if err := app.replyText(replyToken, "Leaving room"); err != nil {
   613  				return err
   614  			}
   615  			if _, err := app.bot.LeaveRoom(source.RoomID).Do(); err != nil {
   616  				return app.replyText(replyToken, err.Error())
   617  			}
   618  		}
   619  	default:
   620  		log.Printf("Echo message to %s: %s", replyToken, message.Text)
   621  		if _, err := app.bot.ReplyMessage(
   622  			replyToken,
   623  			linebot.NewTextMessage(message.Text),
   624  		).Do(); err != nil {
   625  			return err
   626  		}
   627  	}
   628  	return nil
   629  }
   630  
   631  func (app *KitchenSink) handleImage(message *linebot.ImageMessage, replyToken string) error {
   632  	return app.handleHeavyContent(message.ID, func(originalContent *os.File) error {
   633  		// You need to install ImageMagick.
   634  		// And you should consider about security and scalability.
   635  		previewImagePath := originalContent.Name() + "-preview"
   636  		_, err := exec.Command("convert", "-resize", "240x", "jpeg:"+originalContent.Name(), "jpeg:"+previewImagePath).Output()
   637  		if err != nil {
   638  			return err
   639  		}
   640  
   641  		originalContentURL := app.appBaseURL + "/downloaded/" + filepath.Base(originalContent.Name())
   642  		previewImageURL := app.appBaseURL + "/downloaded/" + filepath.Base(previewImagePath)
   643  		if _, err := app.bot.ReplyMessage(
   644  			replyToken,
   645  			linebot.NewImageMessage(originalContentURL, previewImageURL),
   646  		).Do(); err != nil {
   647  			return err
   648  		}
   649  		return nil
   650  	})
   651  }
   652  
   653  func (app *KitchenSink) handleVideo(message *linebot.VideoMessage, replyToken string) error {
   654  	return app.handleHeavyContent(message.ID, func(originalContent *os.File) error {
   655  		// You need to install FFmpeg and ImageMagick.
   656  		// And you should consider about security and scalability.
   657  		previewImagePath := originalContent.Name() + "-preview"
   658  		_, err := exec.Command("convert", "mp4:"+originalContent.Name()+"[0]", "jpeg:"+previewImagePath).Output()
   659  		if err != nil {
   660  			return err
   661  		}
   662  
   663  		originalContentURL := app.appBaseURL + "/downloaded/" + filepath.Base(originalContent.Name())
   664  		previewImageURL := app.appBaseURL + "/downloaded/" + filepath.Base(previewImagePath)
   665  		if _, err := app.bot.ReplyMessage(
   666  			replyToken,
   667  			linebot.NewVideoMessage(originalContentURL, previewImageURL),
   668  		).Do(); err != nil {
   669  			return err
   670  		}
   671  		return nil
   672  	})
   673  }
   674  
   675  func (app *KitchenSink) handleAudio(message *linebot.AudioMessage, replyToken string) error {
   676  	return app.handleHeavyContent(message.ID, func(originalContent *os.File) error {
   677  		originalContentURL := app.appBaseURL + "/downloaded/" + filepath.Base(originalContent.Name())
   678  		if _, err := app.bot.ReplyMessage(
   679  			replyToken,
   680  			linebot.NewAudioMessage(originalContentURL, 100),
   681  		).Do(); err != nil {
   682  			return err
   683  		}
   684  		return nil
   685  	})
   686  }
   687  
   688  func (app *KitchenSink) handleFile(message *linebot.FileMessage, replyToken string) error {
   689  	return app.replyText(replyToken, fmt.Sprintf("File `%s` (%d bytes) received.", message.FileName, message.FileSize))
   690  }
   691  
   692  func (app *KitchenSink) handleLocation(message *linebot.LocationMessage, replyToken string) error {
   693  	if _, err := app.bot.ReplyMessage(
   694  		replyToken,
   695  		linebot.NewLocationMessage(message.Title, message.Address, message.Latitude, message.Longitude),
   696  	).Do(); err != nil {
   697  		return err
   698  	}
   699  	return nil
   700  }
   701  
   702  func (app *KitchenSink) handleSticker(message *linebot.StickerMessage, replyToken string) error {
   703  	if _, err := app.bot.ReplyMessage(
   704  		replyToken,
   705  		linebot.NewStickerMessage(message.PackageID, message.StickerID),
   706  	).Do(); err != nil {
   707  		return err
   708  	}
   709  	return nil
   710  }
   711  
   712  func (app *KitchenSink) replyText(replyToken, text string) error {
   713  	if _, err := app.bot.ReplyMessage(
   714  		replyToken,
   715  		linebot.NewTextMessage(text),
   716  	).Do(); err != nil {
   717  		return err
   718  	}
   719  	return nil
   720  }
   721  
   722  func (app *KitchenSink) handleHeavyContent(messageID string, callback func(*os.File) error) error {
   723  	content, err := app.bot.GetMessageContent(messageID).Do()
   724  	if err != nil {
   725  		return err
   726  	}
   727  	defer content.Content.Close()
   728  	log.Printf("Got file: %s", content.ContentType)
   729  	originalContent, err := app.saveContent(content.Content)
   730  	if err != nil {
   731  		return err
   732  	}
   733  	return callback(originalContent)
   734  }
   735  
   736  func (app *KitchenSink) saveContent(content io.ReadCloser) (*os.File, error) {
   737  	file, err := os.CreateTemp(app.downloadDir, "")
   738  	if err != nil {
   739  		return nil, err
   740  	}
   741  	defer file.Close()
   742  
   743  	_, err = io.Copy(file, content)
   744  	if err != nil {
   745  		return nil, err
   746  	}
   747  	log.Printf("Saved %s", file.Name())
   748  	return file, nil
   749  }