Skip to content

Commit 1a6442a

Browse files
authored
Merge pull request #342 from Seluj78/feature/chatbot
2 parents 79e9a03 + e755e7f commit 1a6442a

File tree

14 files changed

+348
-23
lines changed

14 files changed

+348
-23
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,5 @@ www/
133133
*.db
134134
.env.ci
135135
.env.dev
136-
.env.docker
136+
.env.docker
137+
backend/sentence_tokenizer.pickle

backend/PyMatcha/models/user.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ class User(Model):
6666
is_confirmed = Field(bool)
6767
confirmed_on = Field(datetime.datetime, fmt="%Y-%m-%d %H:%M:%S")
6868
previous_reset_token = Field(str)
69-
skip_recommendations = Field(bool)
69+
is_bot = Field(bool)
7070
superlikes_counter = Field(int)
7171
superlikes_reset_dt = Field(datetime.datetime, fmt="%Y-%m-%d %H:%M:%S")
7272

@@ -89,7 +89,7 @@ def create(
8989
is_profile_completed: bool = False,
9090
is_confirmed: bool = False,
9191
confirmed_on: datetime.datetime = None,
92-
skip_recommendations: bool = False,
92+
is_bot: bool = False,
9393
superlikes_counter: int = 5,
9494
superlikes_reset_dt: Optional[datetime.datetime] = None,
9595
) -> User:
@@ -151,7 +151,7 @@ def create(
151151
is_confirmed=is_confirmed,
152152
confirmed_on=confirmed_on,
153153
previous_reset_token=None,
154-
skip_recommendations=skip_recommendations,
154+
is_bot=is_bot,
155155
superlikes_counter=superlikes_counter,
156156
superlikes_reset_dt=superlikes_reset_dt,
157157
)
@@ -193,7 +193,7 @@ def register(email: str, username: str, password: str, first_name: str, last_nam
193193
is_confirmed=False,
194194
confirmed_on=None,
195195
previous_reset_token=None,
196-
skip_recommendations=False,
196+
is_bot=False,
197197
superlikes_counter=5,
198198
superlikes_reset_dt=None,
199199
)

backend/PyMatcha/routes/api/messages.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from PyMatcha.utils.success import Success
3333
from PyMatcha.utils.success import SuccessOutput
3434
from PyMatcha.utils.success import SuccessOutputMessage
35+
from PyMatcha.utils.tasks import bot_respond_to_message
3536
from timeago import format as timeago_format
3637

3738

@@ -77,7 +78,7 @@ def send_message():
7778
current_user.send_message(to_id=to_user.id, content=content)
7879
current_app.logger.debug("/messages -> Message successfully sent to {}.".format(to_uid))
7980

80-
new_message = Message.get_multis(to_id=to_user.id, content=content, from_id=current_user.id)[-1]
81+
new_message = Message.get_multis(to_id=to_user.id, from_id=current_user.id)[-1]
8182

8283
Notification.create(
8384
trigger_id=current_user.id,
@@ -87,6 +88,12 @@ def send_message():
8788
link_to=f"conversation/{current_user.id}",
8889
)
8990

91+
if to_user.is_bot:
92+
new_message.is_seen = True
93+
new_message.seen_timestamp = datetime.datetime.utcnow()
94+
new_message.save()
95+
bot_respond_to_message.delay(bot_id=to_user.id, from_id=current_user.id, message_content=content)
96+
9097
return SuccessOutputMessage("new_message", new_message.to_dict(), "Message successfully sent to {}.".format(to_uid))
9198

9299

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import json
2+
import logging
3+
from random import choice
4+
from random import choices
5+
from random import randrange
6+
7+
from chatterbot import ChatBot
8+
from chatterbot.trainers import ChatterBotCorpusTrainer
9+
from PyMatcha import redis
10+
from PyMatcha.models.like import Like
11+
from PyMatcha.models.match import Match
12+
from PyMatcha.models.message import Message
13+
from PyMatcha.models.user import User
14+
from PyMatcha.models.view import View
15+
from PyMatcha.utils.recommendations import create_user_recommendations
16+
from PyMatcha.utils.static import BACKEND_ROOT
17+
from PyMatcha.utils.static import BOT_CONV_OPENERS
18+
19+
20+
# TODO: DO superlikes
21+
22+
23+
def _prepare_chatbot(bot_name):
24+
logging.debug(f"Starting chatbot with name {bot_name}")
25+
chatbot = ChatBot(
26+
bot_name,
27+
storage_adapter="chatterbot.storage.SQLStorageAdapter",
28+
database_uri=f"sqlite:///{BACKEND_ROOT}/../chatbot_database.sqlite3",
29+
)
30+
31+
trainer = ChatterBotCorpusTrainer(chatbot, show_training_progress=False)
32+
trainer.train(
33+
"chatterbot.corpus.english.conversations",
34+
"chatterbot.corpus.english.emotion",
35+
"chatterbot.corpus.english.greetings",
36+
"chatterbot.corpus.english.humor",
37+
"PyMatcha.utils.dating",
38+
)
39+
return chatbot
40+
41+
42+
def _get_recommendations(bot_user: User, ignore_bots: bool):
43+
recommendations = redis.get(f"user_recommendations:{str(bot_user.id)}")
44+
if not recommendations:
45+
create_user_recommendations(bot_user, ignore_bots)
46+
recommendations = redis.get(f"user_recommendations:{str(bot_user.id)}")
47+
if not recommendations:
48+
raise ValueError("Recommendations could not be calculated")
49+
return json.loads(recommendations)
50+
51+
52+
def _botaction_like(bot_user: User, recommendations):
53+
liked_ids = [like.liked_id for like in bot_user.get_likes_sent()]
54+
for user in recommendations:
55+
if user["id"] in liked_ids:
56+
recommendations.remove(user)
57+
try:
58+
user_to_like = choice(recommendations)
59+
except IndexError:
60+
return
61+
user_to_like = User.get(id=user_to_like["id"])
62+
View.create(profile_id=user_to_like.id, viewer_id=bot_user.id)
63+
Like.create(liker_id=bot_user.id, liked_id=user_to_like.id)
64+
65+
if user_to_like.already_likes(bot_user.id):
66+
Match.create(user_1=bot_user.id, user_2=user_to_like.id)
67+
68+
69+
def botaction_unlike(bot_user: User):
70+
liked_ids = [like.liked_id for like in bot_user.get_likes_sent()]
71+
try:
72+
id_to_unlike = choice(liked_ids)
73+
except IndexError:
74+
return
75+
Like.get_multi(liker_id=bot_user.id, liked_id=id_to_unlike).delete()
76+
77+
78+
def _botaction_view(bot_user: User, recommendations):
79+
try:
80+
user_to_view = choice(recommendations)
81+
except IndexError:
82+
return
83+
View.create(profile_id=user_to_view["id"], viewer_id=bot_user.id)
84+
85+
86+
def _botaction_message_new_conversation(bot_user: User):
87+
matches = bot_user.get_matches()
88+
unopened_matches = []
89+
for match in matches:
90+
msg_1 = Message.get_multi(from_id=match.user_1, to_id=match.user_2)
91+
msg_2 = Message.get_multi(from_id=match.user_2, to_id=match.user_1)
92+
if not msg_1 and not msg_2:
93+
unopened_matches.append(match)
94+
95+
try:
96+
match_to_open_conv = choice(unopened_matches)
97+
except IndexError:
98+
return
99+
100+
if match_to_open_conv.user_1 == bot_user.id:
101+
other_user = match_to_open_conv.user_2
102+
else:
103+
other_user = match_to_open_conv.user_1
104+
105+
bot_user.send_message(to_id=other_user, content=choice(BOT_CONV_OPENERS))
106+
107+
108+
def _botaction_respond_to_unread(bot_user: User, chatbot):
109+
last_message_list = bot_user.get_conversation_list()
110+
111+
unread_last_messages = []
112+
for last_message in last_message_list:
113+
if not last_message.is_seen and last_message.to_id == bot_user.id:
114+
unread_last_messages.append(last_message)
115+
try:
116+
message_to_reply = choice(unread_last_messages)
117+
except IndexError:
118+
return
119+
bot_reply = chatbot.get_response(message_to_reply.content)
120+
bot_user.send_message(to_id=message_to_reply.from_id, content=bot_reply.text)
121+
122+
123+
def _botaction_send_message_over_old_one(bot_user: User, chatbot):
124+
last_message_list = bot_user.get_conversation_list()
125+
try:
126+
message_to_reply = choice(last_message_list)
127+
except IndexError:
128+
return
129+
if message_to_reply.to_id == bot_user.id:
130+
other_user = message_to_reply.from_id
131+
else:
132+
other_user = message_to_reply.to_id
133+
134+
bot_reply = chatbot.get_response(".")
135+
bot_user.send_message(to_id=other_user, content=bot_reply.text)
136+
137+
138+
def decide_bot_action(bot_user: User):
139+
recommendations = _get_recommendations(bot_user, ignore_bots=True)
140+
141+
# The bot will first view 0 to 3 profiles
142+
for _ in range(0, randrange(0, 3)):
143+
_botaction_view(bot_user, recommendations)
144+
145+
# The bot will then like 0 to 2 profiles
146+
for _ in range(0, randrange(0, 2)):
147+
_botaction_like(bot_user, recommendations)
148+
149+
matches = bot_user.get_matches()
150+
if not matches:
151+
# No matches so far, no more actions to be done
152+
# TODO: Add unlike
153+
return
154+
155+
for match in matches:
156+
chatbot = _prepare_chatbot(bot_user.username)
157+
message_actions = [
158+
_botaction_respond_to_unread,
159+
_botaction_message_new_conversation,
160+
_botaction_send_message_over_old_one,
161+
]
162+
selected_action = choices(message_actions, weights=[10, 9, 5], k=1)
163+
if selected_action[0]:
164+
# Has to have the [0] because random.choices return a list
165+
selected_action[0](bot_user, chatbot)

backend/PyMatcha/utils/dating.yml

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
categories:
2+
- dating
3+
conversations:
4+
- - Sex?
5+
- Sure
6+
- Hmm, idk
7+
- why not?
8+
- when where how?
9+
- Fuck you
10+
- ok
11+
- How's no ?
12+
- Depends on your dick size
13+
- How tall are you?
14+
- No, you'll stay a virgin.
15+
- We can do it in the butt
16+
- Let's gooo
17+
- yessss
18+
- You're cute, so ok.
19+
- err that's forward
20+
- does that pickup line often work for you ?
21+
- - Hello pretty
22+
- Hi sexy
23+
- Hello cutie
24+
- Hi babe
25+
- Hello baby
26+
- fuck you
27+
- Does that pickup line ever work for you ?
28+
- - Height ?
29+
- Surely you must know you're nowhere near attractive enough to make those demands, right ?
30+
- Fuck you why do you need that information ?
31+
- ...
32+
- - How much does a polar bear weigh ?
33+
- Enough to break the ice
34+
- Fucking back pickup line
35+
- - Are you a microwave? Cause mmmmmmmmmmmmmmmmmmmmmmmmm
36+
- Are you cold food ? Cause I want to shove you in me and warm you up
37+
- - That's weird you really look updog in that picture
38+
- What's updog?
39+
- - what's you major ?
40+
- how about a more interesting question ?
41+
- - Which vegetables give you anxiety?
42+
- Kiwis, had a bad experience with one as a child
43+
- - Top or bottom?
44+
- Middle, sometimes jungle
45+
- - Wanna hear a joke about paper?
46+
- It's probably going to be tearable
47+
- - If I was naked in front of you right now, what would you do?
48+
- Ask for consent
49+
- - Are you a bot?
50+
- are you ?
51+
- - Hey caramel woman
52+
- Hey mayonnaise man

backend/PyMatcha/utils/orm/_model.py

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import logging
2+
import threading
23
from copy import deepcopy
34
from typing import List
45

@@ -15,7 +16,13 @@ class Model(object):
1516
Base Model class, all other Models will inherit from this
1617
"""
1718

18-
db = connection
19+
_conn = threading.local()
20+
21+
@property
22+
def db(self):
23+
if not hasattr(self._conn, "db"):
24+
self._conn.db = pymysql.connect(**database_config)
25+
return self._conn.db
1926

2027
# Every model should override this with the correct table name
2128
table_name = None
@@ -391,6 +398,63 @@ def select_random(cls, count):
391398
for item in data:
392399
yield cls(item)
393400

401+
@classmethod
402+
def select_random_multis(cls, count, **kwargs):
403+
"""
404+
Get models from the database, using multiple keyword argument as a filter.
405+
406+
Class method allows you to use without instanciation eg.
407+
408+
model = Model.get(username="test", email="test@example.org")
409+
410+
Returns list of instances on success and raises an error if the row count was 0
411+
"""
412+
413+
keys = []
414+
values = []
415+
for key, value in kwargs.items():
416+
keys.append(key)
417+
values.append(value)
418+
419+
where = ""
420+
length = len(keys)
421+
for index, (key, value) in enumerate(zip(keys, values)):
422+
if isinstance(value, str):
423+
if index == length - 1:
424+
where = where + f"{key}='{value}'"
425+
else:
426+
where = where + f"{key}='{value}' and "
427+
else:
428+
if index == length - 1:
429+
where = where + f"{key}={value}"
430+
else:
431+
where = where + f"{key}={value} and "
432+
temp = cls()
433+
with temp.db.cursor() as c:
434+
c.execute(
435+
"""
436+
SELECT
437+
{fields}
438+
FROM
439+
{table}
440+
WHERE {where}
441+
ORDER BY RAND()
442+
LIMIT {count}
443+
""".format(
444+
fields=", ".join(temp.fields.keys()), table=cls.table_name, where=where, count=count
445+
)
446+
)
447+
448+
data = c.fetchall()
449+
c.close()
450+
if data:
451+
ret_list = []
452+
for i in data:
453+
ret_list.append(cls(i))
454+
return ret_list
455+
else:
456+
return []
457+
394458
@classmethod
395459
def drop_table(cls):
396460
logging.warning("Dropping table {}".format(cls.table_name))

backend/PyMatcha/utils/recommendations.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ def default_date_converter(o):
1313
return o.__str__()
1414

1515

16-
def create_user_recommendations(user_to_update):
16+
def create_user_recommendations(user_to_update, ignore_bots: bool = False):
1717
today = datetime.datetime.utcnow()
1818
user_to_update_recommendations = []
1919
if not user_to_update.birthdate:
@@ -38,6 +38,8 @@ def create_user_recommendations(user_to_update):
3838
blocked_ids = [u.blocked_id for u in user_to_update.get_blocks()]
3939

4040
for user in query:
41+
if user.is_bot and ignore_bots:
42+
continue
4143
if user.id == user_to_update.id:
4244
continue
4345
if user.id in matches_id or user.id in likes_sent_user_ids:

0 commit comments

Comments
 (0)