Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bot language generalization #12

Merged
merged 6 commits into from
Aug 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions application/bot/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from src.broker.amqp_connection import AmqpConnection
from src.broker.amqp_connection_pool import AmqpConnectionPool
from src.broker.handlers import on_message
from src.i18n import Translator


def setup_logger():
Expand Down Expand Up @@ -58,6 +59,10 @@ def main():
except:
logger.warning("Missing locale nb_NO.utf8 on server")

# set up translator
translator = Translator(language_folder="./src/lang")
injector.binder.bind(Translator, to=translator, scope=singleton)

# Set up rabbitmq
setup_connection_pool()
setup_consumption_queue_listener()
Expand Down
54 changes: 28 additions & 26 deletions application/bot/src/api/bot_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from src.broker.broker_client import BrokerClient
from src.injector import injector
import logging
from src.i18n import Translator

class BotApiConfiguration:
def __init__(self, timezone):
Expand All @@ -22,6 +23,7 @@ def __init__(self, config: BotApiConfiguration, logger: logging.Logger):
self.HOURS_BETWEEN_REMINDERS = int(os.environ["HOURS_BETWEEN_REMINDERS"])
self.timezone = config.timezone
self.logger = logger
self.translator = injector.get(Translator)

def __enter__(self):
self.client = injector.get(BrokerClient)
Expand All @@ -34,7 +36,7 @@ def welcome(self, slack_client, team_id):
channel_id = self.join_channel(slack_client=slack_client, team_id=team_id)
self.send_slack_message(
channel_id=channel_id,
text="Hei! Jeg er pizzabot. Hvis dere vil endre hvilke kanal jeg bruker så kan dere gå inn i riktig kanal og bruke kommandoen '/set-pizza-channel'. Hvis kanalen er privat må dere legge meg til først.",
text=self.translator.translate("botWelcome"),
slack_client=slack_client
)

Expand Down Expand Up @@ -130,7 +132,7 @@ def send_reminders(self):
if invitation['reminded_at'] < remind_timestamp:
slack_client.send_slack_message(
channel_id=invitation['slack_id'],
text="Hei du! Jeg hørte ikke noe mer? Er du gira?"
text=self.translator.translate("eventReminder")
)
was_updated = self.client.update_invitation(
slack_id=invitation['slack_id'],
Expand Down Expand Up @@ -158,7 +160,7 @@ def send_event_finalized(self, timestamp, restaurant_name, slack_ids, channel_id
# Send the finalization Slack message
slack_client.send_slack_message(
channel_id=channel_id,
text="Halloi! %s! Dere skal spise 🍕 på %s, %s. %s booker bord, og %s legger ut for maten. Blank betaler!" % (ids_string, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M"), booker, payer)
text=self.translator.translate("eventFinalized", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B kl %H:%M"), booker=booker, payer=payer)
)

def send_event_unfinalized(self, timestamp, restaurant_name, slack_ids, channel_id, slack_client):
Expand All @@ -171,7 +173,7 @@ def send_event_unfinalized(self, timestamp, restaurant_name, slack_ids, channel_
# Send message that the event unfinalized
slack_client.send_slack_message(
channel_id=channel_id,
text="Halloi! %s! Hvis den som meldte seg av besøket til %s %s skulle betale eller booke så må nesten en av dere andre sørge for det. I mellomtiden letes det etter en erstatter." % (ids_string, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M"))
text=self.translator.translate("eventUnfinalized", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B kl %H:%M"))
)
# Invite more users for the event
self.invite_multiple_if_needed()
Expand All @@ -181,7 +183,7 @@ def send_user_withdrew_after_finalization(self, user_id, timestamp, restaurant_n
# Send message that the user withdrew
slack_client.send_slack_message(
channel_id=channel_id,
text="Halloi! <@%s> meldte seg nettopp av besøket til %s %s." % (user_id, restaurant_name, timestamp.strftime("%A %d. %B kl %H:%M"))
text=self.translator.translate("userWithdrawAfterFinalization", user_id=user, restaurant_name=restaurant_name, time_stamp=timestamp.strftime("%A %d. %B kl %H:%M"))
)
# Invite more users for the event
self.invite_multiple_if_needed()
Expand Down Expand Up @@ -219,7 +221,7 @@ def auto_reply(self):
# Send the user a message that the invite expired
slack_client.send_slack_message(
channel_id=invitation['slack_id'],
text="Neivel, da antar jeg du ikke kan/gidder. Håper du blir med neste gang! 🤞"
JacobTheisen marked this conversation as resolved.
Show resolved Hide resolved
text=self.translator.translate("autoReplyNoAttending")
)
self.logger.info("%s didn't answer. Setting RSVP to not attending." % invitation['slack_id'])
else:
Expand Down Expand Up @@ -310,7 +312,7 @@ def inform_users_unfinalized_event_got_cancelled(self, time, restaurant_name, sl
# Send the user a message that the event has been cancelled
slack_client.send_slack_message(
channel_id=slack_id,
text="Halloi! Besøket til %s, %s har blitt kansellert. Sorry!" % (restaurant_name, time.strftime("%A %d. %B kl %H:%M"))
text=self.translator.translate("unfinalizedEventCancelled", restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B kl %H:%M"))
)
self.logger.info("Informed user: %s" % slack_id)

Expand All @@ -322,7 +324,7 @@ def inform_users_finalized_event_got_cancelled(self, time, restaurant_name, slac
self.logger.info("finalized event got cancelled for users %s" % ", ".join(slack_user_ids))
slack_client.send_slack_message(
channel_id=channel_id,
text="Halloi! %s! Besøket til %s, %s har blitt kansellert. Sorry!" % (ids_string, restaurant_name, time.strftime("%A %d. %B kl %H:%M"),)
text=self.translator.translate("finalizedEventCancelled", user_ids=ids_string, restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B kl %H:%M"))
)
# Update invitation message - remove buttons and tell user it has been cancelled
for slack_user_data in slack_data:
Expand All @@ -342,7 +344,7 @@ def inform_users_unfinalized_event_got_updated(self, old_time, time, old_restaur
for slack_id in slack_ids:
slack_client.send_slack_message(
channel_id=slack_id,
text="Halloi! Besøket til %s, %s har blit endret til %s, %s." % (old_restaurant_name, old_time.strftime("%A %d. %B kl %H:%M"), restaurant_name, time.strftime("%A %d. %B kl %H:%M"))
text=self.translator.translate("unfinalizedEventUpdate", old_restaurant_name=old_restaurant_name, old_time_stamp=old_time.strftime("%A %d. %B kl %H:%M"),restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B kl %H:%M"))
)
self.logger.info("Informed user: %s" % slack_id)

Expand All @@ -352,7 +354,7 @@ def inform_users_finalized_event_got_updated(self, old_time, time, old_restauran
self.logger.info("finalized event got updated for users %s" % ", ".join(slack_ids))
slack_client.send_slack_message(
channel_id=channel_id,
text="Halloi! %s! Besøket til %s, %s har blit endret til %s, %s." % (ids_string, old_restaurant_name, old_time.strftime("%A %d. %B kl %H:%M"), restaurant_name, time.strftime("%A %d. %B kl %H:%M"))
text=self.translator.translate("finalizedEventUpdate", user_ids=ids_string, old_restaurant_name=old_restaurant_name, old_time_stamp=old_time.strftime("%A %d. %B kl %H:%M"),restaurant_name=restaurant_name, time_stamp=time.strftime("%A %d. %B kl %H:%M"))
)

def send_slack_message(self, channel_id, text, slack_client, blocks=None, thread_ts=None):
Expand All @@ -362,20 +364,20 @@ def update_slack_message(self, channel_id, ts, slack_client, text=None, blocks=N
return slack_client.update_slack_message(channel_id, ts, text, blocks)

def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, slack_client):
top_level_title_text = f"Pizzainvitasjon: {place}, {datetime}"
top_level_title_text = self.translator.translate("topLevelPizzaInvitation", restaurant_name=place, time_stamp=datetime)
blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Pizzainvitasjon"
"text": self.translator.translate("pizzaInvitationHeader")
}
},
{
"type": "section",
"text": {
"type": "plain_text",
"text": f"Du er invitert til :pizza: på {place}, {datetime}. Pls svar innen {deadline} timer :pray:. Kan du?"
"text": self.translator.translate("pizzaInvitationBody", restaurant_name=place, time_stamp=datetime, deadline=deadline)
}
},
{
Expand All @@ -388,7 +390,7 @@ def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, sla
"type": "button",
"text": {
"type": "plain_text",
"text": "Hells yesss!!! 🍕🍕🍕"
"text": self.translator.translate("pizzaInvitationAttendButton")
},
"value": event_id,
"action_id": "rsvp_yes",
Expand All @@ -397,7 +399,7 @@ def send_pizza_invite(self, channel_id, event_id, place, datetime, deadline, sla
"type": "button",
"text": {
"type": "plain_text",
"text": "Nah ☹️"
"text": self.translator.translate("pizzaInvitationNoAttendButton")
},
"value": event_id,
"action_id": "rsvp_no",
Expand All @@ -421,7 +423,7 @@ def send_pizza_invite_loading(self, channel_id, ts, old_blocks, event_id, slack_
"type": "section",
"text": {
"type": "mrkdwn",
"text": ":hourglass_flowing_sand: Behandler forespørselen din...",
"text": self.translator.translate("inviteLoading")
}
}
]
Expand All @@ -435,7 +437,7 @@ def send_pizza_invite_not_among_invited_users(self, channel_id, ts, old_blocks,
"type": "section",
"text": {
"type": "plain_text",
"text": "Kunne ikke oppdatere invitasjonen. Du var ikke blant de inviterte.",
"text": self.translator.translate("inviteNotAmongUsers")
}
}
]
Expand Down Expand Up @@ -480,7 +482,7 @@ def send_update_pizza_invite_unanswered(self, channel_id, ts, event_id, slack_cl
"type": "button",
"text": {
"type": "plain_text",
"text": "Hells yesss!!! 🍕🍕🍕"
"text": self.translator.translate("pizzaInvitationAttendButton")
},
"value": str(event_id),
"action_id": "rsvp_yes",
Expand All @@ -489,7 +491,7 @@ def send_update_pizza_invite_unanswered(self, channel_id, ts, event_id, slack_cl
"type": "button",
"text": {
"type": "plain_text",
"text": "Nah ☹️"
"text": self.translator.translate("pizzaInvitationNoAttendButton")
},
"value": str(event_id),
"action_id": "rsvp_no",
Expand All @@ -506,7 +508,7 @@ def send_pizza_invite_answered(self, channel_id, ts, event_id, old_blocks, atten
"type": "section",
"text": {
"type": "plain_text",
"text": f"Du har takket {'ja. Sweet! 🤙' if attending else 'nei. Ok 😕'}",
"text": self.translator.translate("pizzaInviteAnswerAttend") if attending else self.translator.translate("pizzaInviteAnswerNoAttend") ,
}
}
]
Expand All @@ -518,13 +520,13 @@ def send_pizza_invite_answered(self, channel_id, ts, event_id, old_blocks, atten
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Hvis noe skulle skje så kan du melde deg av ved å klikke på knappen!"
"text": self.translator.translate("unsubscribeBody")
},
"accessory": {
"type": "button",
"text": {
"type": "plain_text",
"text": "Meld meg av"
"text": self.translator.translate("unsubscribeButton")
},
"value": str(event_id),
"action_id": "rsvp_withdraw"
Expand All @@ -543,7 +545,7 @@ def send_invitation_invalidated_event_cancelled(self, channel_id, ts, old_blocks
"type": "section",
"text": {
"type": "plain_text",
"text": "Arrangementet har blitt avlyst.",
"text": self.translator.translate("invalidatedEventCancelled")
}
}
]
Expand All @@ -557,7 +559,7 @@ def send_invitation_expired(self, channel_id, ts, old_blocks, slack_client):
"type": "section",
"text": {
"type": "plain_text",
"text": "Invitasjonen er utløpt.",
"text": self.translator.translate("invitationExpired")
}
}
]
Expand All @@ -571,7 +573,7 @@ def send_pizza_invite_withdraw(self, channel_id, ts, old_blocks, slack_client):
"type": "section",
"text": {
"type": "plain_text",
"text": "Du har meldt deg av. Ok 😕",
"text": self.translator.translate("inviteWithdrawn")
}
}
]
Expand All @@ -585,7 +587,7 @@ def send_pizza_invite_withdraw_failure(self, channel_id, ts, old_blocks, slack_c
"type": "section",
"text": {
"type": "plain_text",
"text": "Pizza arrangementet er over. Avmelding er ikke mulig.",
"text": self.translator.translate("inviteWithdrawnFailure")
}
}
]
Expand Down
38 changes: 38 additions & 0 deletions application/bot/src/i18n.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import json
import os
import glob
from src.injector import injector
import logging
from string import Template



supported_format = ["json"]

class Translator:
def __init__(self, language_folder="./lang", default_locale="en") -> None:
self.data= {}
self.locale = default_locale
self.logger = injector.get(logging.Logger)


for filename in glob.glob(os.path.join(language_folder, '*.json')):
loc = os.path.splitext(os.path.basename(filename))[0]
with open(filename, encoding="utf-8", mode="r") as f:
self.data[loc] = json.load(f)

def set_locale(self, locale):
if locale in self.data:
self.locale = locale
else:
self.logger.warn(f"Unvalid locale: {locale}, fallback to default locale: {self.locale}")

def translate(self, key, **kwargs):
if key in self.data[self.locale]:
text = self.data[self.locale][key]
return Template(text).safe_substitute(**kwargs)
else:
self.logger.warn(f"The key '{key}' does not match any text. Defaults text to key")
return key


31 changes: 31 additions & 0 deletions application/bot/src/lang/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"thanksForFile": "Thanks for the file! 🤙",
"pizzaChannelError": "Something went wrong. Couldn't set Pizza channel.",
"pizzaChannelConfirm": "Pizza channel has now been set to <#$channel_id>",
"botWelcome": "Hello! I'm the pizza bot. If you want to change the channel I use, you can go to the appropriate channel and use the command '/set-pizza-channel'. If the channel is private, you need to add me first.",
"eventReminder": "Hey there! I didn't hear any more from you. Are you comming?",
"eventFinalized": "Hello there! $user_ids! You're going to have 🍕 at $restaurant_name, $time_stamp. $booker is booking the table!",
"eventUnfinalized": "Hello there! $user_ids! If the one who withdrew from the visit to $restaurant_name $time_stamp was supposed to book, then one of you others will have to take care of that. Meanwhile, a replacement is being sought.",
"userWithdrawAfterFinalization": "Hello there! <@$user_id> just withdrew from the visit to $restaurant_name $time_stamp.",
"autoReplyNoAttending": "Alright, I guess you can't or don't want to. Hope you'll join next time! 🤞",
"unfinalizedEventCancelled": "Hello there! The visit to $restaurant_name, $time_stamp has been canceled. Sorry!",
"finalizedEventCancelled": "Hello there! $user_ids! The visit to $restaurant_name, $time_stamp has been canceled. Sorry!",
"unfinalizedEventUpdate": "Hello there! The visit to $old_restaurant_name, $old_time_stamp has been updated to $restaurant_name, $time_stamp.",
"finalizedEventUpdate": "Hello there! $user_ids! The visit to $old_restaurant_name, $old_time_stamp has been updated to $restaurant_name, $time_stamp.",
"topLevelPizzaInvitation": "Pizza Invitation: $restaurant_name, $time_stamp",
"pizzaInvitationHeader": "Pizza Invitation",
"pizzaInvitationBody": "You're invited to have :pizza: at $restaurant_name, $time_stamp. Please respond within $deadline hours :pray:. Will you join in?",
"inviteLoading": ":hourglass_flowing_sand: Processing your request...",
"inviteNotAmongUsers": "Couldn't update the invitation. You were not among the invited.",
"pizzaInvitationAttendButton": "Hells yesss!!! 🍕🍕🍕",
"pizzaInvitationNoAttendButton": "Nah ☹️",
"pizzaInviteAnswerAttend": "You've accepted. Sweet! 🤙",
"pizzaInviteAnswerNoAttend": "You've declined. Ok 😕",
"unsubscribeBody": "If something comes up, you can unsubscribe by clicking the button!",
"unsubscribeButton": "Unsubscribe me",
"invalidatedEventCancelled": "The event has been canceled.",
"invitationExpired": "The invitation has expired.",
"inviteWithdrawn": "You've withdrawn. Ok 😕",
"inviteWithdrawnFailure": "The pizza event is over. Withdrawal is not possible."
}

Loading