line / main.py
solnone's picture
Update
607aa51
raw
history blame
36.6 kB
# -*- coding: utf-8 -*-
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import errno
import os
import sys
import logging
import tempfile
from argparse import ArgumentParser
from flask import Flask, request, abort, send_from_directory
import google.generativeai as genai
import markdown
from bs4 import BeautifulSoup
from linebot.v3 import (
WebhookHandler
)
from linebot.v3.models import (
UnknownEvent
)
from linebot.v3.exceptions import (
InvalidSignatureError
)
from linebot.v3.webhooks import (
MessageEvent,
TextMessageContent,
LocationMessageContent,
StickerMessageContent,
ImageMessageContent,
VideoMessageContent,
AudioMessageContent,
FileMessageContent,
UserSource,
RoomSource,
GroupSource,
FollowEvent,
UnfollowEvent,
JoinEvent,
LeaveEvent,
PostbackEvent,
BeaconEvent,
MemberJoinedEvent,
MemberLeftEvent,
)
from linebot.v3.messaging import (
Configuration,
ApiClient,
MessagingApi,
MessagingApiBlob,
ReplyMessageRequest,
PushMessageRequest,
MulticastRequest,
BroadcastRequest,
TextMessage,
ApiException,
LocationMessage,
StickerMessage,
ImageMessage,
TemplateMessage,
FlexMessage,
Emoji,
QuickReply,
QuickReplyItem,
ConfirmTemplate,
ButtonsTemplate,
CarouselTemplate,
CarouselColumn,
ImageCarouselTemplate,
ImageCarouselColumn,
FlexBubble,
FlexImage,
FlexBox,
FlexText,
FlexIcon,
FlexButton,
FlexSeparator,
FlexContainer,
MessageAction,
URIAction,
PostbackAction,
DatetimePickerAction,
CameraAction,
CameraRollAction,
LocationAction,
ErrorResponse
)
from linebot.v3.insight import (
ApiClient as InsightClient,
Insight
)
app = Flask(__name__)
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
app.logger.setLevel(logging.INFO)
# get channel_secret and channel_access_token from your environment variable
channel_secret = os.getenv('LINE_CHANNEL_SECRET', None)
channel_access_token = os.getenv('LINE_CHANNEL_ACCESS_TOKEN', None)
if channel_secret is None or channel_access_token is None:
print('Specify LINE_CHANNEL_SECRET and LINE_CHANNEL_ACCESS_TOKEN as environment variables.')
sys.exit(1)
handler = WebhookHandler(channel_secret)
static_tmp_path = os.path.join(os.path.dirname(__file__), 'static', 'tmp')
configuration = Configuration(
access_token=channel_access_token
)
genai.configure(api_key=os.getenv('GOOGLE_API_KEY', None))
model = genai.GenerativeModel('gemini-pro')
chats: dict[str, genai.ChatSession] = {}
def get_chat(user_id: str) -> genai.ChatSession:
if user_id in chats:
return chats.get(user_id)
else:
chat = model.start_chat()
chats[user_id] = chat
return chat
@app.route("/")
def home():
return {"message": "Line Webhook Server"}
# function for create tmp dir for download content
def make_static_tmp_dir():
try:
os.makedirs(static_tmp_path)
except OSError as exc:
if exc.errno == errno.EEXIST and os.path.isdir(static_tmp_path):
pass
else:
raise
@app.route("/callback", methods=['POST'])
def callback():
# get X-Line-Signature header value
signature = request.headers['X-Line-Signature']
# get request body as text
body = request.get_data(as_text=True)
app.logger.info("Request body: " + body)
# handle webhook body
try:
handler.handle(body, signature)
except ApiException as e:
app.logger.warn("Got exception from LINE Messaging API: %s\n" % e.body)
except InvalidSignatureError:
abort(400)
return 'OK'
@handler.add(MessageEvent, message=TextMessageContent)
def handle_text_message(event):
text = event.message.text
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
if text == 'profile':
if isinstance(event.source, UserSource):
profile = line_bot_api.get_profile(user_id=event.source.user_id)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[
TextMessage(text='Display name: ' + profile.display_name),
TextMessage(text='Status message: ' + str(profile.status_message))
]
)
)
else:
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text="Bot can't use profile API without user ID")]
)
)
elif text == 'emojis':
emojis = [Emoji(index=0, product_id="5ac1bfd5040ab15980c9b435", emoji_id="001"),
Emoji(index=13, product_id="5ac1bfd5040ab15980c9b435", emoji_id="002")]
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text='$ LINE emoji $', emojis=emojis)]
)
)
elif text == 'quota':
quota = line_bot_api.get_message_quota()
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[
TextMessage(text='type: ' + quota.type),
TextMessage(text='value: ' + str(quota.value))
]
)
)
elif text == 'quota_consumption':
quota_consumption = line_bot_api.get_message_quota_consumption()
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[
TextMessage(text='total usage: ' + str(quota_consumption.total_usage))
]
)
)
elif text == 'push':
line_bot_api.push_message(
PushMessageRequest(
to=event.source.user_id,
messages=[TextMessage(text='PUSH!')]
)
)
elif text == 'multicast':
line_bot_api.multicast(
MulticastRequest(
to=[event.source.user_id],
messages=[TextMessage(text="THIS IS A MULTICAST MESSAGE, but it's slower than PUSH.")]
)
)
elif text == 'broadcast':
line_bot_api.broadcast(
BroadcastRequest(
messages=[TextMessage(text='THIS IS A BROADCAST MESSAGE')]
)
)
elif text.startswith('broadcast '): # broadcast 20190505
date = text.split(' ')[1]
app.logger.info("Getting broadcast result: " + date)
result = line_bot_api.get_number_of_sent_broadcast_messages(var_date=date)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[
TextMessage(text='Number of sent broadcast messages: ' + date),
TextMessage(text='status: ' + str(result.status)),
TextMessage(text='success: ' + str(result.success)),
]
)
)
elif text == 'bye':
if isinstance(event.source, GroupSource):
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text="Leaving group")]
)
)
line_bot_api.leave_group(event.source.group_id)
elif isinstance(event.source, RoomSource):
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text="Leaving room")]
)
)
line_bot_api.leave_room(room_id=event.source.room_id)
else:
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[
TextMessage(text="Bot can't leave from 1:1 chat")
]
)
)
elif text == 'image':
url = 'https://placehold.co/400'
app.logger.info("url=" + url)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[
ImageMessage(original_content_url=url, preview_image_url=url)
]
)
)
elif text == 'confirm':
confirm_template = ConfirmTemplate(
text='Do it?',
actions=[
MessageAction(label='Yes', text='Yes!'),
MessageAction(label='No', text='No!')
]
)
template_message = TemplateMessage(
alt_text='Confirm alt text',
template=confirm_template
)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[template_message]
)
)
elif text == 'buttons':
buttons_template = ButtonsTemplate(
title='My buttons sample',
text='Hello, my buttons',
actions=[
URIAction(label='Go to line.me', uri='https://line.me'),
PostbackAction(label='ping', data='ping'),
PostbackAction(label='ping with text', data='ping', text='ping'),
MessageAction(label='Translate Rice', text='米')
])
template_message = TemplateMessage(
alt_text='Buttons alt text',
template=buttons_template
)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[template_message]
)
)
elif text == 'carousel':
carousel_template = CarouselTemplate(
columns=[
CarouselColumn(
text='hoge1',
title='fuga1',
actions=[
URIAction(label='Go to line.me', uri='https://line.me'),
PostbackAction(label='ping', data='ping')
]
),
CarouselColumn(
text='hoge2',
title='fuga2',
actions=[
PostbackAction(label='ping with text', data='ping', text='ping'),
MessageAction(label='Translate Rice', text='米')
]
)
]
)
template_message = TemplateMessage(
alt_text='Carousel alt text', template=carousel_template)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[template_message]
)
)
elif text == 'image_carousel':
image_carousel_template = ImageCarouselTemplate(columns=[
ImageCarouselColumn(image_url='https://via.placeholder.com/1024x1024',
action=DatetimePickerAction(label='datetime',
data='datetime_postback',
mode='datetime')),
ImageCarouselColumn(image_url='https://via.placeholder.com/1024x1024',
action=DatetimePickerAction(label='date',
data='date_postback',
mode='date'))
])
template_message = TemplateMessage(
alt_text='ImageCarousel alt text', template=image_carousel_template)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[template_message]
)
)
elif text == 'imagemap':
pass
elif text == 'flex':
bubble = FlexBubble(
direction='ltr',
hero=FlexImage(
url='https://example.com/cafe.jpg',
size='full',
aspect_ratio='20:13',
aspect_mode='cover',
action=URIAction(uri='http://example.com', label='label')
),
body=FlexBox(
layout='vertical',
contents=[
# title
FlexText(text='Brown Cafe', weight='bold', size='xl'),
# review
FlexBox(
layout='baseline',
margin='md',
contents=[
FlexIcon(size='sm', url='https://example.com/gold_star.png'),
FlexIcon(size='sm', url='https://example.com/grey_star.png'),
FlexIcon(size='sm', url='https://example.com/gold_star.png'),
FlexIcon(size='sm', url='https://example.com/gold_star.png'),
FlexIcon(size='sm', url='https://example.com/grey_star.png'),
FlexText(text='4.0', size='sm', color='#999999', margin='md', flex=0)
]
),
# info
FlexBox(
layout='vertical',
margin='lg',
spacing='sm',
contents=[
FlexBox(
layout='baseline',
spacing='sm',
contents=[
FlexText(
text='Place',
color='#aaaaaa',
size='sm',
flex=1
),
FlexText(
text='Shinjuku, Tokyo',
wrap=True,
color='#666666',
size='sm',
flex=5
)
],
),
FlexBox(
layout='baseline',
spacing='sm',
contents=[
FlexText(
text='Time',
color='#aaaaaa',
size='sm',
flex=1
),
FlexText(
text="10:00 - 23:00",
wrap=True,
color='#666666',
size='sm',
flex=5,
),
],
),
],
)
],
),
footer=FlexBox(
layout='vertical',
spacing='sm',
contents=[
# callAction
FlexButton(
style='link',
height='sm',
action=URIAction(label='CALL', uri='tel:000000'),
),
# separator
FlexSeparator(),
# websiteAction
FlexButton(
style='link',
height='sm',
action=URIAction(label='WEBSITE', uri="https://example.com")
)
]
),
)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[FlexMessage(alt_text="hello", contents=bubble)]
)
)
elif text == 'flex_update_1':
bubble_string = """
{
"type": "bubble",
"body": {
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "image",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/flexsnapshot/clip/clip3.jpg",
"position": "relative",
"size": "full",
"aspectMode": "cover",
"aspectRatio": "1:1",
"gravity": "center"
},
{
"type": "box",
"layout": "horizontal",
"contents": [
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "Brown Hotel",
"weight": "bold",
"size": "xl",
"color": "#ffffff"
},
{
"type": "box",
"layout": "baseline",
"margin": "md",
"contents": [
{
"type": "icon",
"size": "sm",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "sm",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "sm",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "sm",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gold_star_28.png"
},
{
"type": "icon",
"size": "sm",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
},
{
"type": "text",
"text": "4.0",
"size": "sm",
"color": "#d6d6d6",
"margin": "md",
"flex": 0
}
]
}
]
},
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "¥62,000",
"color": "#a9a9a9",
"decoration": "line-through",
"align": "end"
},
{
"type": "text",
"text": "¥42,000",
"color": "#ebebeb",
"size": "xl",
"align": "end"
}
]
}
],
"position": "absolute",
"offsetBottom": "0px",
"offsetStart": "0px",
"offsetEnd": "0px",
"backgroundColor": "#00000099",
"paddingAll": "20px"
},
{
"type": "box",
"layout": "vertical",
"contents": [
{
"type": "text",
"text": "SALE",
"color": "#ffffff"
}
],
"position": "absolute",
"backgroundColor": "#ff2600",
"cornerRadius": "20px",
"paddingAll": "5px",
"offsetTop": "10px",
"offsetEnd": "10px",
"paddingStart": "10px",
"paddingEnd": "10px"
}
],
"paddingAll": "0px"
}
}
"""
message = FlexMessage(alt_text="hello", contents=FlexContainer.from_json(bubble_string))
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[message]
)
)
elif text == 'quick_reply':
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(
text='Quick reply',
quick_reply=QuickReply(
items=[
QuickReplyItem(
action=PostbackAction(label="label1", data="data1")
),
QuickReplyItem(
action=MessageAction(label="label2", text="text2")
),
QuickReplyItem(
action=DatetimePickerAction(label="label3",
data="data3",
mode="date")
),
QuickReplyItem(
action=CameraAction(label="label4")
),
QuickReplyItem(
action=CameraRollAction(label="label5")
),
QuickReplyItem(
action=LocationAction(label="label6")
),
]
)
)]
)
)
elif text == 'link_token' and isinstance(event.source, UserSource):
link_token_response = line_bot_api.issue_link_token(user_id=event.source.user_id)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text='link_token: ' + link_token_response.link_token)]
)
)
elif text == 'insight_message_delivery':
with InsightClient(configuration) as api_client:
line_bot_insight_api = Insight(api_client)
today = datetime.date.today().strftime("%Y%m%d")
response = line_bot_insight_api.get_number_of_message_deliveries(var_date=today)
if response.status == 'ready':
messages = [
TextMessage(text='broadcast: ' + str(response.broadcast)),
TextMessage(text='targeting: ' + str(response.targeting)),
]
else:
messages = [TextMessage(text='status: ' + response.status)]
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=messages
)
)
elif text == 'insight_followers':
with InsightClient(configuration) as api_client:
line_bot_insight_api = Insight(api_client)
today = datetime.date.today().strftime("%Y%m%d")
response = line_bot_insight_api.get_number_of_followers(var_date=today)
if response.status == 'ready':
messages = [
TextMessage(text='followers: ' + str(response.followers)),
TextMessage(text='targetedReaches: ' + str(response.targeted_reaches)),
TextMessage(text='blocks: ' + str(response.blocks)),
]
else:
messages = [TextMessage(text='status: ' + response.status)]
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=messages
)
)
elif text == 'insight_demographic':
with InsightClient(configuration) as api_client:
line_bot_insight_api = Insight(api_client)
response = line_bot_insight_api.get_friends_demographics()
if response.available:
messages = ["{gender}: {percentage}".format(gender=it.gender, percentage=it.percentage)
for it in response.genders]
else:
messages = [TextMessage(text='available: false')]
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=messages
)
)
elif text == 'with http info':
response = line_bot_api.reply_message_with_http_info(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text='see application log')]
)
)
app.logger.info("Got response with http status code: " + str(response.status_code))
app.logger.info("Got x-line-request-id: " + response.headers['x-line-request-id'])
app.logger.info("Got response with http body: " + str(response.data))
elif text == 'with http info error':
try:
line_bot_api.reply_message_with_http_info(
ReplyMessageRequest(
reply_token='invalid-reply-token',
messages=[TextMessage(text='see application log')]
)
)
except ApiException as e:
app.logger.info("Got response with http status code: " + str(e.status))
app.logger.info("Got x-line-request-id: " + e.headers['x-line-request-id'])
app.logger.info("Got response with http body: " + str(ErrorResponse.from_json(e.body)))
else:
chat = get_chat(event.source.user_id)
response = chat.send_message(text)
html_msg = markdown.markdown(response.text)
soup = BeautifulSoup(html_msg, 'html.parser')
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text=soup.get_text())]
)
)
@handler.add(MessageEvent, message=LocationMessageContent)
def handle_location_message(event):
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[LocationMessage(
title='Location',
address=event.message.address,
latitude=event.message.latitude,
longitude=event.message.longitude
)]
)
)
@handler.add(MessageEvent, message=StickerMessageContent)
def handle_sticker_message(event):
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[StickerMessage(
package_id=event.message.package_id,
sticker_id=event.message.sticker_id)
]
)
)
# Other Message Type
@handler.add(MessageEvent, message=(ImageMessageContent,
VideoMessageContent,
AudioMessageContent))
def handle_content_message(event):
if isinstance(event.message, ImageMessageContent):
ext = 'jpg'
elif isinstance(event.message, VideoMessageContent):
ext = 'mp4'
elif isinstance(event.message, AudioMessageContent):
ext = 'm4a'
else:
return
with ApiClient(configuration) as api_client:
line_bot_blob_api = MessagingApiBlob(api_client)
message_content = line_bot_blob_api.get_message_content(message_id=event.message.id)
with tempfile.NamedTemporaryFile(dir=static_tmp_path, prefix=ext + '-', delete=False) as tf:
tf.write(message_content)
tempfile_path = tf.name
dist_path = tempfile_path + '.' + ext
dist_name = os.path.basename(dist_path)
os.rename(tempfile_path, dist_path)
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[
TextMessage(text='Save content.'),
TextMessage(text=request.host_url + os.path.join('static', 'tmp', dist_name))
]
)
)
@handler.add(MessageEvent, message=FileMessageContent)
def handle_file_message(event):
with ApiClient(configuration) as api_client:
line_bot_blob_api = MessagingApiBlob(api_client)
message_content = line_bot_blob_api.get_message_content(message_id=event.message.id)
with tempfile.NamedTemporaryFile(dir=static_tmp_path, prefix='file-', delete=False) as tf:
tf.write(message_content)
tempfile_path = tf.name
dist_path = tempfile_path + '-' + event.message.file_name
dist_name = os.path.basename(dist_path)
os.rename(tempfile_path, dist_path)
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[
TextMessage(text='Save file.'),
TextMessage(text=request.host_url + os.path.join('static', 'tmp', dist_name))
]
)
)
@handler.add(FollowEvent)
def handle_follow(event):
app.logger.info("Got Follow event:" + event.source.user_id)
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text='Got follow event')]
)
)
@handler.add(UnfollowEvent)
def handle_unfollow(event):
app.logger.info("Got Unfollow event:" + event.source.user_id)
@handler.add(JoinEvent)
def handle_join(event):
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text='Joined this ' + event.source.type)]
)
)
@handler.add(LeaveEvent)
def handle_leave():
app.logger.info("Got leave event")
@handler.add(PostbackEvent)
def handle_postback(event: PostbackEvent):
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
if event.postback.data == 'ping':
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text='pong')]
)
)
elif event.postback.data == 'datetime_postback':
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text=event.postback.params['datetime'])]
)
)
elif event.postback.data == 'date_postback':
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text=event.postback.params['date'])]
)
)
@handler.add(BeaconEvent)
def handle_beacon(event: BeaconEvent):
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text='Got beacon event. hwid={}, device_message(hex string)={}'.format(
event.beacon.hwid, event.beacon.dm))]
)
)
@handler.add(MemberJoinedEvent)
def handle_member_joined(event):
with ApiClient(configuration) as api_client:
line_bot_api = MessagingApi(api_client)
line_bot_api.reply_message(
ReplyMessageRequest(
reply_token=event.reply_token,
messages=[TextMessage(text='Got memberJoined event. event={}'.format(event))]
)
)
@handler.add(MemberLeftEvent)
def handle_member_left(event):
app.logger.info("Got memberLeft event")
@handler.add(UnknownEvent)
def handle_unknown_left(event):
app.logger.info(f"unknown event {event}")
@app.route('/static/<path:path>')
def send_static_content(path):
return send_from_directory('static', path)