diff --git a/.github/workflows/docker-release.yml b/.github/workflows/docker-release.yml
index 510e7c7678..dba13f9358 100644
--- a/.github/workflows/docker-release.yml
+++ b/.github/workflows/docker-release.yml
@@ -12,4 +12,4 @@ jobs:
- name: curl
run: |
apk add curl bash
- curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
+ curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'
diff --git a/cypress/fixtures/doctype_with_tab_break.js b/cypress/fixtures/doctype_with_tab_break.js
index bc346e8fb8..74e5e6abba 100644
--- a/cypress/fixtures/doctype_with_tab_break.js
+++ b/cypress/fixtures/doctype_with_tab_break.js
@@ -30,11 +30,6 @@ export default {
"link_doctype": "Contact",
"link_fieldname": "user"
},
- {
- "group": "Profile",
- "link_doctype": "Chat Profile",
- "link_fieldname": "user"
- },
],
modified_by: 'Administrator',
module: 'Custom',
diff --git a/frappe/__init__.py b/frappe/__init__.py
index 43246a7fd6..4218aa113b 100644
--- a/frappe/__init__.py
+++ b/frappe/__init__.py
@@ -41,7 +41,8 @@ class _dict(dict):
"""dict like object that exposes keys as attributes"""
def __getattr__(self, key):
ret = self.get(key)
- if not ret and key.startswith("__"):
+ # "__deepcopy__" exception added to fix frappe#14833 via DFP
+ if not ret and key.startswith("__") and key != "__deepcopy__":
raise AttributeError()
return ret
def __setattr__(self, key, value):
@@ -1797,7 +1798,7 @@ def get_version(doctype, name, limit=None, head=False, raise_err=True):
'limit': limit
}, as_list=1)
- from frappe.chat.util import squashify, dictify, safe_json_loads
+ from frappe.utils import squashify, dictify, safe_json_loads
versions = []
@@ -1855,7 +1856,7 @@ def mock(type, size=1, locale='en'):
data = getattr(fake, type)()
results.append(data)
- from frappe.chat.util import squashify
+ from frappe.utils import squashify
return squashify(results)
def validate_and_sanitize_search_inputs(fn):
diff --git a/frappe/auth.py b/frappe/auth.py
index 2c875c4437..078a6bb165 100644
--- a/frappe/auth.py
+++ b/frappe/auth.py
@@ -128,7 +128,6 @@ class LoginManager:
self.make_session()
self.set_user_info()
- @frappe.whitelist()
def login(self):
# clear cache
frappe.clear_cache(user = frappe.form_dict.get('usr'))
diff --git a/frappe/chat/__init__.py b/frappe/chat/__init__.py
deleted file mode 100644
index 4c9b1c5db7..0000000000
--- a/frappe/chat/__init__.py
+++ /dev/null
@@ -1,23 +0,0 @@
-
-import frappe
-from frappe import _
-
-session = frappe.session
-
-def authenticate(user, raise_err = True):
- if session.user == 'Guest':
- if not frappe.db.exists('Chat Token', user):
- if raise_err:
- frappe.throw(_("Sorry, you're not authorized."))
- else:
- return False
- else:
- return True
- else:
- if user != session.user:
- if raise_err:
- frappe.throw(_("Sorry, you're not authorized."))
- else:
- return False
- else:
- return True
\ No newline at end of file
diff --git a/frappe/chat/doctype/__init__.py b/frappe/chat/doctype/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/chat/doctype/chat_message/__init__.py b/frappe/chat/doctype/chat_message/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/chat/doctype/chat_message/chat_message.js b/frappe/chat/doctype/chat_message/chat_message.js
deleted file mode 100644
index edaad011db..0000000000
--- a/frappe/chat/doctype/chat_message/chat_message.js
+++ /dev/null
@@ -1,10 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Chat Message', {
- onload: function(frm) {
- if(frm.doc.type == 'File') {
- frm.set_df_property('content', 'read_only', 1);
- }
- }
-});
diff --git a/frappe/chat/doctype/chat_message/chat_message.json b/frappe/chat/doctype/chat_message/chat_message.json
deleted file mode 100644
index 9d2d70c5e0..0000000000
--- a/frappe/chat/doctype/chat_message/chat_message.json
+++ /dev/null
@@ -1,91 +0,0 @@
-{
- "beta": 1,
- "creation": "2017-11-10 11:10:40.011099",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "room_type",
- "type",
- "user",
- "room",
- "content",
- "mentions",
- "urls"
- ],
- "fields": [
- {
- "fieldname": "room_type",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Room Type",
- "options": "Direct\nGroup\nVisitor",
- "reqd": 1
- },
- {
- "fieldname": "type",
- "fieldtype": "Data",
- "label": "Type",
- "options": "Content\nFile"
- },
- {
- "fieldname": "user",
- "fieldtype": "Link",
- "hidden": 1,
- "label": "User",
- "options": "User",
- "read_only": 1
- },
- {
- "fieldname": "room",
- "fieldtype": "Link",
- "label": "Room",
- "options": "Chat Room",
- "reqd": 1
- },
- {
- "fieldname": "content",
- "fieldtype": "Text",
- "label": "Content",
- "reqd": 1
- },
- {
- "fieldname": "mentions",
- "fieldtype": "Code",
- "hidden": 1,
- "label": "Mentions"
- },
- {
- "fieldname": "urls",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "URLs"
- }
- ],
- "modified": "2020-09-18 17:26:09.703215",
- "modified_by": "Administrator",
- "module": "Chat",
- "name": "Chat Message",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "search_fields": "content, user",
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "content",
- "track_changes": 1,
- "track_seen": 1
-}
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_message/chat_message.py b/frappe/chat/doctype/chat_message/chat_message.py
deleted file mode 100644
index bc470a5e9c..0000000000
--- a/frappe/chat/doctype/chat_message/chat_message.py
+++ /dev/null
@@ -1,215 +0,0 @@
-# imports - standard imports
-import json
-
-# imports - third-party imports
-import requests
-from bs4 import BeautifulSoup as Soup
-
-# imports - module imports
-from frappe.model.document import Document
-from frappe import _, _dict
-import frappe
-
-# imports - frappe module imports
-from frappe.chat import authenticate
-from frappe.chat.util import (
- get_if_empty,
- check_url,
- dictify,
- get_emojis,
- safe_json_loads,
- get_user_doc,
- squashify
-)
-
-session = frappe.session
-
-class ChatMessage(Document):
- pass
-
-def get_message_urls(content):
- soup = Soup(content, 'html.parser')
- anchors = soup.find_all('a')
- urls = [ ]
-
- for anchor in anchors:
- text = anchor.text
-
- if check_url(text):
- urls.append(text)
-
- return urls
-
-def get_message_mentions(content):
- mentions = [ ]
- tokens = content.split(' ')
-
- for token in tokens:
- if token.startswith('@'):
- what = token[1:]
- if frappe.db.exists('User', what):
- mentions.append(what)
- else:
- if frappe.db.exists('User', token):
- mentions.append(token)
-
- return mentions
-
-def get_message_meta(content):
- '''
- Assumes content to be HTML. Sanitizes the content
- into a dict of metadata values.
- '''
- meta = _dict(
- links = [ ],
- mentions = [ ]
- )
-
- meta.content = content
- meta.urls = get_message_urls(content)
- meta.mentions = get_message_mentions(content)
-
- return meta
-
-def sanitize_message_content(content):
- emojis = get_emojis()
-
- tokens = content.split(' ')
- for token in tokens:
- if token.startswith(':') and token.endswith(':'):
- what = token[1:-1]
-
- # Expensive, I know.
- for emoji in emojis:
- for alias in emoji.aliases:
- if what == alias:
- content = content.replace(token, emoji.emoji)
-
- return content
-
-def get_new_chat_message_doc(user, room, content, type = "Content", link = True):
- user = get_user_doc(user)
- room = frappe.get_doc('Chat Room', room)
-
- meta = get_message_meta(content)
- mess = frappe.new_doc('Chat Message')
- mess.room = room.name
- mess.room_type = room.type
- mess.content = sanitize_message_content(content)
- mess.type = type
- mess.user = user.name
-
- mess.mentions = json.dumps(meta.mentions)
- mess.urls = ','.join(meta.urls)
- mess.save(ignore_permissions = True)
-
- if link:
- room.update(dict(
- last_message = mess.name
- ))
- room.save(ignore_permissions = True)
-
- return mess
-
-def get_new_chat_message(user, room, content, type = "Content"):
- mess = get_new_chat_message_doc(user, room, content, type)
-
- resp = dict(
- name = mess.name,
- user = mess.user,
- room = mess.room,
- room_type = mess.room_type,
- content = json.loads(mess.content) if mess.type in ["File"] else mess.content,
- urls = mess.urls,
- mentions = json.loads(mess.mentions),
- creation = mess.creation,
- seen = json.loads(mess._seen) if mess._seen else [ ],
- )
-
- return resp
-
-@frappe.whitelist(allow_guest = True)
-def send(user, room, content, type = "Content"):
- mess = get_new_chat_message(user, room, content, type)
-
- frappe.publish_realtime('frappe.chat.message:create', mess, room = room,
- after_commit = True)
-
-@frappe.whitelist(allow_guest = True)
-def seen(message, user = None):
- authenticate(user)
-
- has_message = frappe.db.exists('Chat Message', message)
-
- if has_message:
- mess = frappe.get_doc('Chat Message', message)
- mess.add_seen(user)
- mess.load_from_db()
- room = mess.room
- resp = dict(message = message, data = dict(seen = json.loads(mess._seen) if mess._seen else []))
-
- frappe.publish_realtime('frappe.chat.message:update', resp, room = room, after_commit = True)
-
-def history(room, fields = None, limit = 10, start = None, end = None):
- room = frappe.get_doc('Chat Room', room)
- mess = frappe.get_all('Chat Message',
- filters = [
- ('Chat Message', 'room', '=', room.name),
- ('Chat Message', 'room_type', '=', room.type)
- ],
- fields = fields if fields else [
- 'name', 'room_type', 'room', 'content', 'type', 'user', 'mentions', 'urls', 'creation', '_seen'
- ],
- order_by = 'creation'
- )
-
- if not fields or 'seen' in fields:
- for m in mess:
- m['seen'] = json.loads(m._seen) if m._seen else [ ]
- del m['_seen']
- if not fields or 'content' in fields:
- for m in mess:
- m['content'] = json.loads(m.content) if m.type in ["File"] else m.content
-
- frappe.enqueue('frappe.chat.doctype.chat_message.chat_message.mark_messages_as_seen',
- message_names=[m.name for m in mess], user=frappe.session.user)
-
- return mess
-
-def mark_messages_as_seen(message_names, user):
- '''
- Marks chat messages as seen, updates the _seen for each message
- (should be run in background process)
- '''
- for name in message_names:
- seen = frappe.db.get_value('Chat Message', name, '_seen') or '[]'
- seen = json.loads(seen)
- seen.append(user)
- seen = json.dumps(seen)
- frappe.db.set_value('Chat Message', name, '_seen', seen, update_modified=False)
-
- frappe.db.commit()
-
-
-@frappe.whitelist()
-def get(name, rooms = None, fields = None):
- rooms, fields = safe_json_loads(rooms, fields)
-
- has_message = frappe.db.exists('Chat Message', name)
-
- if has_message:
- dmess = frappe.get_doc('Chat Message', name)
- data = dict(
- name = dmess.name,
- user = dmess.user,
- room = dmess.room,
- room_type = dmess.room_type,
- content = json.loads(dmess.content) if dmess.type in ["File"] else dmess.content,
- type = dmess.type,
- urls = dmess.urls,
- mentions = dmess.mentions,
- creation = dmess.creation,
- seen = get_if_empty(dmess._seen, [ ])
- )
-
- return data
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_message/chat_message_list.js b/frappe/chat/doctype/chat_message/chat_message_list.js
deleted file mode 100644
index c5b717048b..0000000000
--- a/frappe/chat/doctype/chat_message/chat_message_list.js
+++ /dev/null
@@ -1,8 +0,0 @@
-frappe.listview_settings['Chat Message'] = {
- filters: [
- ['Chat Message', 'user', '==', frappe.session.user, true]
- // I need an or_filter here.
- // ['Chat Room', 'owner', '==', frappe.session.user, true],
- // ['Chat Room', frappe.session.user, 'in', 'users', true]
- ]
-};
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_profile/__init__.py b/frappe/chat/doctype/chat_profile/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/chat/doctype/chat_profile/chat_profile.js b/frappe/chat/doctype/chat_profile/chat_profile.js
deleted file mode 100644
index b27a98faf5..0000000000
--- a/frappe/chat/doctype/chat_profile/chat_profile.js
+++ /dev/null
@@ -1,10 +0,0 @@
-/* eslint semi: "never" */
-frappe.ui.form.on('Chat Profile', {
- refresh: function (form) {
- if ( form.doc.name !== frappe.session.user ) {
- form.disable_save()
- form.set_read_only(true)
- // There's one more that faris@frappe.io told me to add here. form.refresh_fields()?
- }
- }
-});
diff --git a/frappe/chat/doctype/chat_profile/chat_profile.json b/frappe/chat/doctype/chat_profile/chat_profile.json
deleted file mode 100644
index eb36f803fe..0000000000
--- a/frappe/chat/doctype/chat_profile/chat_profile.json
+++ /dev/null
@@ -1,98 +0,0 @@
-{
- "autoname": "field:user",
- "beta": 1,
- "creation": "2017-11-13 18:26:57.943027",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "user",
- "status",
- "chat_background",
- "notifications",
- "message_preview",
- "notification_tones",
- "conversation_tones",
- "settings",
- "enable_chat"
- ],
- "fields": [
- {
- "fieldname": "user",
- "fieldtype": "Link",
- "label": "User",
- "options": "User",
- "reqd": 1
- },
- {
- "default": "Online",
- "fieldname": "status",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Status",
- "options": "Online\nAway\nBusy\nOffline"
- },
- {
- "fieldname": "chat_background",
- "fieldtype": "Attach Image",
- "label": "Chat Background"
- },
- {
- "fieldname": "notifications",
- "fieldtype": "Section Break",
- "label": "Notifications"
- },
- {
- "default": "1",
- "fieldname": "message_preview",
- "fieldtype": "Check",
- "label": "Message Preview"
- },
- {
- "default": "1",
- "fieldname": "notification_tones",
- "fieldtype": "Check",
- "label": "Notification Tones"
- },
- {
- "default": "1",
- "fieldname": "conversation_tones",
- "fieldtype": "Check",
- "label": "Conversation Tones"
- },
- {
- "fieldname": "settings",
- "fieldtype": "Section Break",
- "label": "Settings"
- },
- {
- "default": "1",
- "fieldname": "enable_chat",
- "fieldtype": "Check",
- "label": "Enable Chat"
- }
- ],
- "in_create": 1,
- "modified": "2019-11-07 13:21:36.414961",
- "modified_by": "Administrator",
- "module": "Chat",
- "name": "Chat Profile",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_profile/chat_profile.py b/frappe/chat/doctype/chat_profile/chat_profile.py
deleted file mode 100644
index da10a836c4..0000000000
--- a/frappe/chat/doctype/chat_profile/chat_profile.py
+++ /dev/null
@@ -1,98 +0,0 @@
-# imports - module imports
-from frappe.model.document import Document
-from frappe import _
-import frappe
-
-# imports - frappe module imports
-from frappe.core.doctype.version.version import get_diff
-from frappe.chat.doctype.chat_room import chat_room
-from frappe.chat.util import (
- safe_json_loads,
- filter_dict,
- dictify
-)
-
-session = frappe.session
-
-class ChatProfile(Document):
- def on_update(self):
- if not self.is_new():
- b, a = self.get_doc_before_save(), self
- diff = dictify(get_diff(a, b))
- if diff:
- user = session.user
-
- fields = [changed[0] for changed in diff.changed]
-
- if 'status' in fields:
- rooms = chat_room.get(user, filters = ['Chat Room', 'type', '=', 'Direct'])
- update = dict(user = user, data = dict(status = self.status))
-
- for room in rooms:
- frappe.publish_realtime('frappe.chat.profile:update', update, room = room.name, after_commit = True)
-
- if 'enable_chat' in fields:
- update = dict(user = user, data = dict(enable_chat = bool(self.enable_chat)))
- frappe.publish_realtime('frappe.chat.profile:update', update, user = user, after_commit = True)
-
-def authenticate(user):
- if user != session.user:
- frappe.throw(_("Sorry, you're not authorized."))
-
-@frappe.whitelist()
-def get(user, fields = None):
- duser = frappe.get_doc('User', user)
-
- if frappe.db.exists('Chat Profile', user):
- dprof = frappe.get_doc('Chat Profile', user)
-
- # If you're adding something here, make sure the client recieves it.
- profile = dict(
- # User
- name = duser.name,
- email = duser.email,
- first_name = duser.first_name,
- last_name = duser.last_name,
- username = duser.username,
- avatar = duser.user_image,
- bio = duser.bio,
- # Chat Profile
- status = dprof.status,
- chat_background = dprof.chat_background,
- message_preview = bool(dprof.message_preview),
- notification_tones = bool(dprof.notification_tones),
- conversation_tones = bool(dprof.conversation_tones),
- enable_chat = bool(dprof.enable_chat)
- )
- profile = filter_dict(profile, fields)
-
- return dictify(profile)
-
-@frappe.whitelist()
-def create(user, exists_ok = False, fields = None):
- authenticate(user)
-
- exists_ok, fields = safe_json_loads(exists_ok, fields)
-
- try:
- dprof = frappe.new_doc('Chat Profile')
- dprof.user = user
- dprof.save(ignore_permissions = True)
- except frappe.DuplicateEntryError:
- frappe.clear_messages()
- if not exists_ok:
- frappe.throw(_('Chat Profile for User {0} exists.').format(user))
-
- profile = get(user, fields = fields)
-
- return profile
-
-@frappe.whitelist()
-def update(user, data):
- authenticate(user)
-
- data = safe_json_loads(data)
-
- dprof = frappe.get_doc('Chat Profile', user)
- dprof.update(data)
- dprof.save(ignore_permissions = True)
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_profile/chat_profile_list.js b/frappe/chat/doctype/chat_profile/chat_profile_list.js
deleted file mode 100644
index 4d97b75e65..0000000000
--- a/frappe/chat/doctype/chat_profile/chat_profile_list.js
+++ /dev/null
@@ -1,11 +0,0 @@
-frappe.listview_settings['Chat Profile'] =
-{
- get_indicator: function (doc)
- {
- const status = frappe.utils.squash(frappe.chat.profile.STATUSES.filter(
- s => s.name === doc.status
- ));
-
- return [__(status.name), status.color, `status,=,${status.name}`]
- }
-};
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_room/__init__.py b/frappe/chat/doctype/chat_room/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/chat/doctype/chat_room/chat_room.js b/frappe/chat/doctype/chat_room/chat_room.js
deleted file mode 100644
index 00b9c8d8f7..0000000000
--- a/frappe/chat/doctype/chat_room/chat_room.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Chat Room', {
- refresh: function (form) {
-
- }
-});
diff --git a/frappe/chat/doctype/chat_room/chat_room.json b/frappe/chat/doctype/chat_room/chat_room.json
deleted file mode 100644
index 1417306c45..0000000000
--- a/frappe/chat/doctype/chat_room/chat_room.json
+++ /dev/null
@@ -1,100 +0,0 @@
-{
- "autoname": "CR.#####",
- "beta": 1,
- "creation": "2017-11-08 15:27:21.156667",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "type",
- "room_name",
- "avatar",
- "last_message",
- "message_count",
- "owner",
- "user_list",
- "users"
- ],
- "fields": [
- {
- "default": "Direct",
- "fieldname": "type",
- "fieldtype": "Select",
- "in_list_view": 1,
- "label": "Type",
- "options": "Direct\nGroup\nVisitor",
- "reqd": 1,
- "set_only_once": 1
- },
- {
- "depends_on": "eval:doc.type==\"Group\"",
- "fieldname": "room_name",
- "fieldtype": "Data",
- "label": "Name"
- },
- {
- "depends_on": "eval:doc.type==\"Group\"",
- "fieldname": "avatar",
- "fieldtype": "Attach Image",
- "hidden": 1,
- "label": "Avatar"
- },
- {
- "fieldname": "last_message",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Last Message"
- },
- {
- "fieldname": "message_count",
- "fieldtype": "Int",
- "hidden": 1,
- "label": "Message Count"
- },
- {
- "fieldname": "owner",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Owner",
- "read_only": 1
- },
- {
- "fieldname": "user_list",
- "fieldtype": "Section Break",
- "label": "Users"
- },
- {
- "fieldname": "users",
- "fieldtype": "Table",
- "label": "Users",
- "options": "Chat Room User"
- }
- ],
- "image_field": "avatar",
- "modified": "2019-11-07 13:20:24.625329",
- "modified_by": "Administrator",
- "module": "Chat",
- "name": "Chat Room",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 1,
- "share": 1,
- "write": 1
- }
- ],
- "search_fields": "room_name",
- "show_name_in_global_search": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "room_name",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_room/chat_room.py b/frappe/chat/doctype/chat_room/chat_room.py
deleted file mode 100644
index bdbee44d7a..0000000000
--- a/frappe/chat/doctype/chat_room/chat_room.py
+++ /dev/null
@@ -1,227 +0,0 @@
-# imports - module imports
-from frappe.model.document import Document
-from frappe import _
-import frappe
-
-# imports - frappe module imports
-from frappe.chat import authenticate
-from frappe.core.doctype.version.version import get_diff
-from frappe.chat.doctype.chat_message import chat_message
-from frappe.chat.util import (
- safe_json_loads,
- dictify,
- listify,
- squashify,
- get_if_empty
-)
-
-session = frappe.session
-
-
-def is_direct(owner, other, bidirectional=False):
- def get_room(owner, other):
- room = frappe.get_all('Chat Room', filters=[
- ['Chat Room', 'type', 'in', ('Direct', 'Visitor')],
- ['Chat Room', 'owner', '=', owner],
- ['Chat Room User', 'user', '=', other]
- ], distinct=True)
-
- return room
-
- exists = len(get_room(owner, other)) == 1
- if bidirectional:
- exists = exists or len(get_room(other, owner)) == 1
-
- return exists
-
-
-def get_chat_room_user_set(users, filter_=None):
- seen, uset = set(), list()
-
- for u in users:
- if filter_(u) and u.user not in seen:
- uset.append(u)
- seen.add(u.user)
-
- return uset
-
-
-class ChatRoom(Document):
- def validate(self):
- if self.is_new():
- users = get_chat_room_user_set(self.users, filter_=lambda u: u.user != session.user)
- self.update(dict(
- users=users
- ))
-
- if self.type == "Direct":
- if len(self.users) != 1:
- frappe.throw(_('{0} room must have atmost one user.').format(self.type))
-
- other = squashify(self.users)
-
- if self.is_new():
- if is_direct(self.owner, other.user, bidirectional=True):
- frappe.throw(_('Direct room with {0} already exists.').format(other.user))
-
- if self.type == "Group" and not self.room_name:
- frappe.throw(_('Group name cannot be empty.'))
-
- def on_update(self):
- if not self.is_new():
- before = self.get_doc_before_save()
- if not before: return
-
- after = self
- diff = dictify(get_diff(before, after))
- if diff:
- update = {}
- for changed in diff.changed:
- field, old, new = changed
-
- if field == 'last_message':
- new = chat_message.get(new)
-
- update.update({field: new})
-
- if diff.added or diff.removed:
- update.update(dict(users=[u.user for u in self.users]))
-
- update = dict(room=self.name, data=update)
-
- frappe.publish_realtime('frappe.chat.room:update', update, room=self.name,
- after_commit=True)
-
-
-@frappe.whitelist(allow_guest=True)
-def get(user=None, token=None, rooms=None, fields=None, filters=None):
- # There is this horrible bug out here.
- # Looks like if frappe.call sends optional arguments (not in right order),
- # the argument turns to an empty string.
- # I'm not even going to think searching for it.
- # Hence, the hack was get_if_empty (previous assign_if_none)
- # - Achilles Rasquinha achilles@frappe.io
- data = user or token
- authenticate(data)
-
- rooms, fields, filters = safe_json_loads(rooms, fields, filters)
-
- rooms = listify(get_if_empty(rooms, []))
- fields = listify(get_if_empty(fields, []))
-
- const = [] # constraints
- if rooms:
- const.append(['Chat Room', 'name', 'in', rooms])
- if filters:
- if isinstance(filters[0], list):
- const = const + filters
- else:
- const.append(filters)
-
- default = ['name', 'type', 'room_name', 'creation', 'owner', 'avatar']
- handle = ['users', 'last_message']
-
- param = [f for f in fields if f not in handle]
-
- rooms = frappe.get_all('Chat Room',
- or_filters=[
- ['Chat Room', 'owner', '=', frappe.session.user],
- ['Chat Room User', 'user', '=', frappe.session.user]
- ],
- filters=const,
- fields=param + ['name'] if param else default,
- distinct=True
- )
-
- if not fields or 'users' in fields:
- for i, r in enumerate(rooms):
- droom = frappe.get_doc('Chat Room', r.name)
- rooms[i]['users'] = []
-
- for duser in droom.users:
- rooms[i]['users'].append(duser.user)
-
- if not fields or 'last_message' in fields:
- for i, r in enumerate(rooms):
- droom = frappe.get_doc('Chat Room', r.name)
- if droom.last_message:
- rooms[i]['last_message'] = chat_message.get(droom.last_message)
- else:
- rooms[i]['last_message'] = None
-
- rooms = squashify(dictify(rooms))
-
- return rooms
-
-
-@frappe.whitelist(allow_guest=True)
-def create(kind, token, users=None, name=None):
- authenticate(token)
-
- users = safe_json_loads(users)
- create = True
-
- if kind == 'Visitor':
- room = squashify(frappe.db.sql("""
- SELECT name
- FROM `tabChat Room`
- WHERE owner=%s
- """, (frappe.session.user), as_dict=True))
-
- if room:
- room = frappe.get_doc('Chat Room', room.name)
- create = False
-
- if create:
- room = frappe.new_doc('Chat Room')
- room.type = kind
- room.owner = frappe.session.user
- room.room_name = name
-
- dusers = []
-
- if kind != 'Visitor':
- if users:
- users = listify(users)
- for user in users:
- duser = frappe.new_doc('Chat Room User')
- duser.user = user
- dusers.append(duser)
-
- room.users = dusers
- else:
- dsettings = frappe.get_single('Website Settings')
- room.room_name = dsettings.chat_room_name
-
- users = [user for user in room.users] if hasattr(room, 'users') else []
-
- for user in dsettings.chat_operators:
- if user.user not in users:
- # appending user to room.users will remove the user from chat_operators
- # this is undesirable, create a new Chat Room User instead
- chat_room_user = {"doctype": "Chat Room User", "user": user.user}
- room.append('users', chat_room_user)
-
- room.save(ignore_permissions=True)
-
- room = get(token=token, rooms=room.name)
- if room:
- users = [room.owner] + [u for u in room.users]
-
- for user in users:
- frappe.publish_realtime('frappe.chat.room:create', room, user=user, after_commit=True)
-
- return room
-
-
-@frappe.whitelist(allow_guest=True)
-def history(room, user, fields=None, limit=10, start=None, end=None):
- if frappe.get_doc('Chat Room', room).type != 'Visitor':
- authenticate(user)
-
- fields = safe_json_loads(fields)
-
- mess = chat_message.history(room, limit=limit, start=start, end=end)
- mess = squashify(mess)
-
- return dictify(mess)
diff --git a/frappe/chat/doctype/chat_room/chat_room_list.js b/frappe/chat/doctype/chat_room/chat_room_list.js
deleted file mode 100644
index 70c708c7bd..0000000000
--- a/frappe/chat/doctype/chat_room/chat_room_list.js
+++ /dev/null
@@ -1,6 +0,0 @@
-frappe.listview_settings['Chat Room'] = {
- filters: [
- ['Chat Room', 'owner', '=', frappe.session.user, true],
- ['Chat Room User', 'user', '=', frappe.session.user, true]
- ]
-};
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_room_user/__init__.py b/frappe/chat/doctype/chat_room_user/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/chat/doctype/chat_room_user/chat_room_user.json b/frappe/chat/doctype/chat_room_user/chat_room_user.json
deleted file mode 100644
index f7bdf6706b..0000000000
--- a/frappe/chat/doctype/chat_room_user/chat_room_user.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "beta": 1,
- "creation": "2017-11-08 15:24:21.029314",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "user",
- "is_admin"
- ],
- "fields": [
- {
- "fieldname": "user",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "User",
- "options": "User",
- "reqd": 1
- },
- {
- "default": "0",
- "fieldname": "is_admin",
- "fieldtype": "Check",
- "label": "Admin"
- }
- ],
- "in_create": 1,
- "istable": 1,
- "modified": "2019-11-07 13:21:05.297337",
- "modified_by": "Administrator",
- "module": "Chat",
- "name": "Chat Room User",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_room_user/chat_room_user.py b/frappe/chat/doctype/chat_room_user/chat_room_user.py
deleted file mode 100644
index f6dbdc7659..0000000000
--- a/frappe/chat/doctype/chat_room_user/chat_room_user.py
+++ /dev/null
@@ -1,8 +0,0 @@
-# imports - module imports
-from frappe.model.document import Document
-import frappe
-
-session = frappe.session
-
-class ChatRoomUser(Document):
- pass
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_token/__init__.py b/frappe/chat/doctype/chat_token/__init__.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/frappe/chat/doctype/chat_token/chat_token.js b/frappe/chat/doctype/chat_token/chat_token.js
deleted file mode 100644
index 78f03026ec..0000000000
--- a/frappe/chat/doctype/chat_token/chat_token.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2018, Frappe Technologies and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Chat Token', {
- refresh: function(frm) {
-
- }
-});
diff --git a/frappe/chat/doctype/chat_token/chat_token.json b/frappe/chat/doctype/chat_token/chat_token.json
deleted file mode 100644
index b73505ac2c..0000000000
--- a/frappe/chat/doctype/chat_token/chat_token.json
+++ /dev/null
@@ -1,57 +0,0 @@
-{
- "autoname": "field:token",
- "beta": 1,
- "creation": "2018-03-26 18:20:13.825652",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "token",
- "ip_address",
- "country"
- ],
- "fields": [
- {
- "fieldname": "token",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Token",
- "reqd": 1
- },
- {
- "fieldname": "ip_address",
- "fieldtype": "Data",
- "label": "IP Address"
- },
- {
- "fieldname": "country",
- "fieldtype": "Data",
- "label": "Country"
- }
- ],
- "in_create": 1,
- "modified": "2019-11-07 13:21:24.514558",
- "modified_by": "Administrator",
- "module": "Chat",
- "name": "Chat Token",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/frappe/chat/doctype/chat_token/chat_token.py b/frappe/chat/doctype/chat_token/chat_token.py
deleted file mode 100644
index 0be51b6081..0000000000
--- a/frappe/chat/doctype/chat_token/chat_token.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies and contributors
-# License: MIT. See LICENSE
-
-import frappe
-from frappe.model.document import Document
-
-class ChatToken(Document):
- pass
diff --git a/frappe/chat/util/__init__.py b/frappe/chat/util/__init__.py
deleted file mode 100644
index 383df581cd..0000000000
--- a/frappe/chat/util/__init__.py
+++ /dev/null
@@ -1,13 +0,0 @@
-# imports - module imports
-from frappe.chat.util.util import (
- get_user_doc,
- squashify,
- safe_json_loads,
- filter_dict,
- get_if_empty,
- listify,
- dictify,
- check_url,
- create_test_user,
- get_emojis
-)
\ No newline at end of file
diff --git a/frappe/chat/util/test_util.py b/frappe/chat/util/test_util.py
deleted file mode 100644
index e2d05a4024..0000000000
--- a/frappe/chat/util/test_util.py
+++ /dev/null
@@ -1,35 +0,0 @@
-# imports - standard imports
-import unittest
-
-# imports - module imports
-from frappe.chat.util import (
- get_user_doc,
- safe_json_loads
-)
-import frappe
-
-class TestChatUtil(unittest.TestCase):
- def test_safe_json_loads(self):
- number = safe_json_loads("1")
- self.assertEqual(type(number), int)
-
- number = safe_json_loads("1.0")
- self.assertEqual(type(number), float)
-
- string = safe_json_loads("foobar")
- self.assertEqual(type(string), str)
-
- array = safe_json_loads('[{ "foo": "bar" }]')
- self.assertEqual(type(array), list)
-
- objekt = safe_json_loads('{ "foo": "bar" }')
- self.assertEqual(type(objekt), dict)
-
- true, null = safe_json_loads("true", "null")
- self.assertEqual(true, True)
- self.assertEqual(null, None)
-
- def test_get_user_doc(self):
- # Needs more test cases.
- user = get_user_doc()
- self.assertEqual(user.name, frappe.session.user)
\ No newline at end of file
diff --git a/frappe/chat/util/util.py b/frappe/chat/util/util.py
deleted file mode 100644
index b7e7991c2b..0000000000
--- a/frappe/chat/util/util.py
+++ /dev/null
@@ -1,108 +0,0 @@
-# imports - standard imports
-import json
-from collections.abc import MutableMapping, MutableSequence, Sequence
-
-# imports - third-party imports
-import requests
-from urllib.parse import urlparse
-
-# imports - module imports
-import frappe
-from frappe.exceptions import DuplicateEntryError
-from frappe.model.document import Document
-
-session = frappe.session
-
-
-def get_user_doc(user = None):
- if isinstance(user, Document):
- return user
-
- user = user or session.user
- user = frappe.get_doc('User', user)
-
- return user
-
-def squashify(what):
- if isinstance(what, Sequence) and len(what) == 1:
- return what[0]
-
- return what
-
-def safe_json_loads(*args):
- results = []
-
- for arg in args:
- try:
- arg = json.loads(arg)
- except Exception:
- pass
-
- results.append(arg)
-
- return squashify(results)
-
-def filter_dict(what, keys, ignore = False):
- copy = dict()
-
- if keys:
- for k in keys:
- if k not in what and not ignore:
- raise KeyError('{key} not in dict.'.format(key = k))
- else:
- copy.update({
- k: what[k]
- })
- else:
- copy = what.copy()
-
- return copy
-
-def get_if_empty(a, b):
- if not a:
- a = b
- return a
-
-def listify(arg):
- if not isinstance(arg, list):
- arg = [arg]
- return arg
-
-def dictify(arg):
- if isinstance(arg, MutableSequence):
- for i, a in enumerate(arg):
- arg[i] = dictify(a)
- elif isinstance(arg, MutableMapping):
- arg = frappe._dict(arg)
-
- return arg
-
-def check_url(what, raise_err = False):
- if not urlparse(what).scheme:
- if raise_err:
- raise ValueError('{what} not a valid URL.')
- else:
- return False
-
- return True
-
-def create_test_user(module):
- try:
- test_user = frappe.new_doc('User')
- test_user.first_name = '{module}'.format(module = module)
- test_user.email = 'testuser.{module}@example.com'.format(module = module)
- test_user.save()
- except DuplicateEntryError:
- frappe.log('Test User Chat Profile exists.')
-
-def get_emojis():
- redis = frappe.cache()
- emojis = redis.hget('frappe_emojis', 'emojis')
-
- if not emojis:
- resp = requests.get('http://git.io/frappe-emoji')
- if resp.ok:
- emojis = resp.json()
- redis.hset('frappe_emojis', 'emojis', emojis)
-
- return dictify(emojis)
diff --git a/frappe/chat/website/__init__.py b/frappe/chat/website/__init__.py
deleted file mode 100644
index 12affd2782..0000000000
--- a/frappe/chat/website/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-
-import frappe
-from frappe.chat.util import filter_dict, safe_json_loads
-
-from frappe.sessions import get_geo_ip_country
-
-@frappe.whitelist(allow_guest = True)
-def settings(fields = None):
- fields = safe_json_loads(fields)
-
- dsettings = frappe.get_single('Website Settings')
- response = dict(
- socketio = dict(
- port = frappe.conf.socketio_port
- ),
- enable = bool(dsettings.chat_enable),
- enable_from = dsettings.chat_enable_from,
- enable_to = dsettings.chat_enable_to,
- room_name = dsettings.chat_room_name,
- welcome_message = dsettings.chat_welcome_message,
- operators = [
- duser.user for duser in dsettings.chat_operators
- ]
- )
-
- if fields:
- response = filter_dict(response, fields)
-
- return response
-
-@frappe.whitelist(allow_guest = True)
-def token():
- dtoken = frappe.new_doc('Chat Token')
-
- dtoken.token = frappe.generate_hash()
- dtoken.ip_address = frappe.local.request_ip
- country = get_geo_ip_country(dtoken.ip_address)
- if country:
- dtoken.country = country['iso_code']
- dtoken.save(ignore_permissions = True)
-
- return dtoken.token
\ No newline at end of file
diff --git a/frappe/commands/site.py b/frappe/commands/site.py
index 27a9e86078..c5f78e2680 100755
--- a/frappe/commands/site.py
+++ b/frappe/commands/site.py
@@ -55,8 +55,11 @@ def new_site(site, mariadb_root_username=None, mariadb_root_password=None, admin
@click.option('--with-public-files', help='Restores the public files of the site, given path to its tar file')
@click.option('--with-private-files', help='Restores the private files of the site, given path to its tar file')
@click.option('--force', is_flag=True, default=False, help='Ignore the validations and downgrade warnings. This action is not recommended')
+@click.option('--encryption-key', help='Backup encryption key')
@pass_context
-def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_password=None, db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None, with_private_files=None):
+def restore(context, sql_file_path, encryption_key=None, mariadb_root_username=None, mariadb_root_password=None,
+ db_name=None, verbose=None, install_app=None, admin_password=None, force=None, with_public_files=None,
+ with_private_files=None):
"Restore site database from an sql file"
from frappe.installer import (
_new_site,
@@ -66,26 +69,74 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
is_partial,
validate_database_sql
)
+ from frappe.utils.backups import Backup
+ if not os.path.exists(sql_file_path):
+ print("Invalid path", sql_file_path)
+ sys.exit(1)
+
+ _backup = Backup(sql_file_path)
site = get_site(context)
frappe.init(site=site)
-
force = context.force or force
- decompressed_file_name = extract_sql_from_archive(sql_file_path)
- # check if partial backup
- if is_partial(decompressed_file_name):
- click.secho(
- "Partial Backup file detected. You cannot use a partial file to restore a Frappe Site.",
- fg="red"
- )
- click.secho(
- "Use `bench partial-restore` to restore a partial backup to an existing site.",
- fg="yellow"
- )
- sys.exit(1)
+ try:
+ decompressed_file_name = extract_sql_from_archive(sql_file_path)
+ if is_partial(decompressed_file_name):
+ click.secho(
+ "Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
+ fg="red"
+ )
+ click.secho(
+ "Use `bench partial-restore` to restore a partial backup to an existing site.",
+ fg="yellow"
+ )
+ _backup.decryption_rollback()
+ sys.exit(1)
+
+ except UnicodeDecodeError:
+ _backup.decryption_rollback()
+ if encryption_key:
+ click.secho(
+ "Encrypted backup file detected. Decrypting using provided key.",
+ fg="yellow"
+ )
+ _backup.backup_decryption(encryption_key)
+
+ else:
+ click.secho(
+ "Encrypted backup file detected. Decrypting using site config.",
+ fg="yellow"
+ )
+ encryption_key = frappe.get_site_config().encryption_key
+ _backup.backup_decryption(encryption_key)
+
+ # Rollback on unsuccessful decryrption
+ if not os.path.exists(sql_file_path):
+ click.secho(
+ "Decryption failed. Please provide a valid key and try again.",
+ fg="red"
+ )
+
+ _backup.decryption_rollback()
+ sys.exit(1)
+
+ decompressed_file_name = extract_sql_from_archive(sql_file_path)
+
+ if is_partial(decompressed_file_name):
+ click.secho(
+ "Partial Backup file detected. You cannot use a partial file to restore a Frappe site.",
+ fg="red"
+ )
+ click.secho(
+ "Use `bench partial-restore` to restore a partial backup to an existing site.",
+ fg="yellow"
+ )
+ _backup.decryption_rollback()
+ sys.exit(1)
+
+
- # check if valid SQL file
validate_database_sql(decompressed_file_name, _raise=not force)
# dont allow downgrading to older versions of frappe without force
@@ -96,23 +147,51 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
)
click.confirm(warn_message, abort=True)
- _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
- mariadb_root_password=mariadb_root_password, admin_password=admin_password,
- verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
- force=True, db_type=frappe.conf.db_type)
- # Extract public and/or private files to the restored site, if user has given the path
- if with_public_files:
- public = extract_files(site, with_public_files)
- os.remove(public)
- if with_private_files:
- private = extract_files(site, with_private_files)
- os.remove(private)
+ try:
+ _new_site(frappe.conf.db_name, site, mariadb_root_username=mariadb_root_username,
+ mariadb_root_password=mariadb_root_password, admin_password=admin_password,
+ verbose=context.verbose, install_apps=install_app, source_sql=decompressed_file_name,
+ force=True, db_type=frappe.conf.db_type)
+
+ except Exception as err:
+ print(err.args[1])
+ _backup.decryption_rollback()
+ sys.exit(1)
# Removing temporarily created file
if decompressed_file_name != sql_file_path:
os.remove(decompressed_file_name)
+ _backup.decryption_rollback()
+
+ # Extract public and/or private files to the restored site, if user has given the path
+ if with_public_files:
+ # Decrypt data if there is a Key
+ if encryption_key:
+ _backup = Backup(with_public_files)
+ _backup.backup_decryption(encryption_key)
+ if not os.path.exists(with_public_files):
+ _backup.decryption_rollback()
+ public = extract_files(site, with_public_files)
+
+ # Removing temporarily created file
+ os.remove(public)
+ _backup.decryption_rollback()
+
+
+ if with_private_files:
+ # Decrypt data if there is a Key
+ if encryption_key:
+ _backup = Backup(with_private_files)
+ _backup.backup_decryption(encryption_key)
+ if not os.path.exists(with_private_files):
+ _backup.decryption_rollback()
+ private = extract_files(site, with_private_files)
+
+ # Removing temporarily created file
+ os.remove(private)
+ _backup.decryption_rollback()
success_message = "Site {0} has been restored{1}".format(
site,
@@ -120,19 +199,92 @@ def restore(context, sql_file_path, mariadb_root_username=None, mariadb_root_pas
)
click.secho(success_message, fg="green")
-
@click.command('partial-restore')
@click.argument('sql-file-path')
@click.option("--verbose", "-v", is_flag=True)
+@click.option('--encryption-key', help='Backup encryption key')
@pass_context
-def partial_restore(context, sql_file_path, verbose):
- from frappe.installer import partial_restore
- verbose = context.verbose or verbose
+def partial_restore(context, sql_file_path, verbose, encryption_key=None):
+ from frappe.installer import partial_restore, extract_sql_from_archive
+ from frappe.utils.backups import Backup
+
+ if not os.path.exists(sql_file_path):
+ print("Invalid path", sql_file_path)
+ sys.exit(1)
site = get_site(context)
frappe.init(site=site)
+
+ _backup = Backup(sql_file_path)
+
+ verbose = context.verbose or verbose
+
frappe.connect(site=site)
+ try:
+ decompressed_file_name = extract_sql_from_archive(sql_file_path)
+
+ with open(decompressed_file_name) as f:
+ header = " ".join(f.readline() for _ in range(5))
+
+ #Check for full backup file
+ if "Partial Backup" not in header:
+ click.secho(
+ "Full backup file detected.Use `bench restore` to restore a Frappe Site.",
+ fg="red"
+ )
+ _backup.decryption_rollback()
+ sys.exit(1)
+
+
+ except UnicodeDecodeError:
+ _backup.decryption_rollback()
+ if encryption_key:
+ click.secho(
+ "Encrypted backup file detected. Decrypting using provided key.",
+ fg="yellow"
+ )
+ key = encryption_key
+
+ else:
+ click.secho(
+ "Encrypted backup file detected. Decrypting using site config.",
+ fg="yellow"
+ )
+ key = frappe.get_site_config().encryption_key
+
+ _backup.backup_decryption(key)
+
+ # Rollback on unsuccessful decryrption
+ if not os.path.exists(sql_file_path):
+ click.secho(
+ "Decryption failed. Please provide a valid key and try again.",
+ fg="red"
+ )
+ _backup.decryption_rollback()
+ sys.exit(1)
+
+ decompressed_file_name = extract_sql_from_archive(sql_file_path)
+
+ with open(decompressed_file_name) as f:
+ header = " ".join(f.readline() for _ in range(5))
+
+ #Check for Full backup file.
+ if "Partial Backup" not in header:
+ click.secho(
+ "Full Backup file detected.Use `bench restore` to restore a Frappe Site.",
+ fg="red"
+ )
+ _backup.decryption_rollback()
+ sys.exit(1)
+
+
partial_restore(sql_file_path, verbose)
+
+ # Removing temporarily created file
+ _backup.decryption_rollback()
+ if os.path.exists(sql_file_path.rstrip(".gz")):
+ os.remove(sql_file_path.rstrip(".gz"))
+
frappe.destroy()
@@ -418,6 +570,7 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
backup_path_private_files=None, backup_path_conf=None, ignore_backup_conf=False, verbose=False,
compress=False, include="", exclude=""):
"Backup"
+
from frappe.utils.backups import scheduled_backup
verbose = verbose or context.verbose
exit_code = 0
@@ -441,14 +594,25 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
force=True
)
except Exception:
- click.secho("Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site), fg="red")
+ click.secho(
+ "Backup failed for Site {0}. Database or site_config.json may be corrupted".format(site),
+ fg="red"
+ )
if verbose:
print(frappe.get_traceback())
exit_code = 1
continue
+ if frappe.get_system_settings("encrypt_backup") and frappe.get_site_config().encryption_key:
+ click.secho(
+ "Backup encryption is turned on. Please note the backup encryption key.",
+ fg="yellow"
+ )
odb.print_summary()
- click.secho("Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""), fg="green")
+ click.secho(
+ "Backup for Site {0} has been successfully completed{1}".format(site, " with files" if with_files else ""),
+ fg="green"
+ )
frappe.destroy()
if not context.sites:
@@ -456,6 +620,7 @@ def backup(context, with_files=False, backup_path=None, backup_path_db=None, bac
sys.exit(exit_code)
+
@click.command('remove-from-installed-apps')
@click.argument('app')
@pass_context
diff --git a/frappe/core/doctype/role/role.json b/frappe/core/doctype/role/role.json
index 0135cbf9e8..ba82e023a9 100644
--- a/frappe/core/doctype/role/role.json
+++ b/frappe/core/doctype/role/role.json
@@ -17,7 +17,6 @@
"navigation_settings_section",
"search_bar",
"notifications",
- "chat",
"list_settings_section",
"list_sidebar",
"bulk_actions",
@@ -85,12 +84,6 @@
"fieldtype": "Check",
"label": "Search Bar"
},
- {
- "default": "1",
- "fieldname": "chat",
- "fieldtype": "Check",
- "label": "Chat"
- },
{
"fieldname": "list_settings_section",
"fieldtype": "Section Break",
@@ -155,10 +148,11 @@
"idx": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-27 10:35:37.638350",
+ "modified": "2021-10-08 14:06:55.729364",
"modified_by": "Administrator",
"module": "Core",
"name": "Role",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
diff --git a/frappe/core/doctype/role/role.py b/frappe/core/doctype/role/role.py
index f4fa855ea1..98d2d72fc2 100644
--- a/frappe/core/doctype/role/role.py
+++ b/frappe/core/doctype/role/role.py
@@ -5,7 +5,7 @@ import frappe
from frappe.model.document import Document
-desk_properties = ("search_bar", "notifications", "chat", "list_sidebar",
+desk_properties = ("search_bar", "notifications", "list_sidebar",
"bulk_actions", "view_switcher", "form_sidebar", "timeline", "dashboard")
class Role(Document):
@@ -82,4 +82,4 @@ def role_query(doctype, txt, searchfield, start, page_len, filters):
report_filters.extend(filters)
return frappe.get_all('Role', limit_start=start, limit_page_length=page_len,
- filters=report_filters, as_list=1)
\ No newline at end of file
+ filters=report_filters, as_list=1)
diff --git a/frappe/core/doctype/system_settings/system_settings.json b/frappe/core/doctype/system_settings/system_settings.json
index 4b53983702..82e88d2477 100644
--- a/frappe/core/doctype/system_settings/system_settings.json
+++ b/frappe/core/doctype/system_settings/system_settings.json
@@ -23,6 +23,7 @@
"currency_precision",
"sec_backup_limit",
"backup_limit",
+ "encrypt_backup",
"background_workers",
"enable_scheduler",
"dormant_days",
@@ -65,9 +66,7 @@
"attach_view_link",
"prepared_report_section",
"enable_prepared_report_auto_deletion",
- "prepared_report_expiry_period",
- "chat",
- "enable_chat"
+ "prepared_report_expiry_period"
],
"fields": [
{
@@ -381,18 +380,6 @@
"fieldtype": "Check",
"label": "Hide footer in auto email reports"
},
- {
- "collapsible": 1,
- "fieldname": "chat",
- "fieldtype": "Section Break",
- "label": "Chat"
- },
- {
- "default": "1",
- "fieldname": "enable_chat",
- "fieldtype": "Check",
- "label": "Enable Chat"
- },
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
@@ -469,12 +456,18 @@
"fieldname": "strip_exif_metadata_from_uploaded_images",
"fieldtype": "Check",
"label": "Strip EXIF tags from uploaded images"
+ },
+ {
+ "default": "0",
+ "fieldname": "encrypt_backup",
+ "fieldtype": "Check",
+ "label": "Encrypt Backups"
}
],
"icon": "fa fa-cog",
"issingle": 1,
"links": [],
- "modified": "2021-03-30 11:47:47.330437",
+ "modified": "2021-10-21 19:24:15.232430",
"modified_by": "Administrator",
"module": "Core",
"name": "System Settings",
@@ -492,4 +485,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/frappe/core/doctype/transaction_log/transaction_log.py b/frappe/core/doctype/transaction_log/transaction_log.py
index e2e75b130c..6dc4340277 100644
--- a/frappe/core/doctype/transaction_log/transaction_log.py
+++ b/frappe/core/doctype/transaction_log/transaction_log.py
@@ -1,12 +1,13 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
+import hashlib
+
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder import DocType
from frappe.utils import cint, now_datetime
-import hashlib
class TransactionLog(Document):
def before_insert(self):
@@ -44,10 +45,14 @@ class TransactionLog(Document):
def get_current_index():
- current = frappe.db.sql("""SELECT `current`
- FROM `tabSeries`
- WHERE `name` = 'TRANSACTLOG'
- FOR UPDATE""")
+ series = DocType("Series")
+ current = (
+ frappe.qb.from_(series)
+ .where(series.name == "TRANSACTLOG")
+ .for_update()
+ .select("current")
+ ).run()
+
if current and current[0][0] is not None:
current = current[0][0]
diff --git a/frappe/core/doctype/user/user.js b/frappe/core/doctype/user/user.js
index 96726d875c..2ce7413aa7 100644
--- a/frappe/core/doctype/user/user.js
+++ b/frappe/core/doctype/user/user.js
@@ -263,6 +263,7 @@ frappe.ui.form.on('User', {
callback: function(r) {
if (r.message) {
frappe.msgprint(__("Save API Secret: {0}", [r.message.api_secret]));
+ frm.reload_doc();
}
}
});
diff --git a/frappe/core/doctype/user/user.json b/frappe/core/doctype/user/user.json
index cd7dcd6a34..ea31e76a57 100644
--- a/frappe/core/doctype/user/user.json
+++ b/frappe/core/doctype/user/user.json
@@ -555,20 +555,22 @@
"collapsible": 1,
"fieldname": "api_access",
"fieldtype": "Section Break",
- "label": "Api Access"
+ "label": "API Access"
},
{
- "description": "API Key cannot be regenerated",
+ "description": "API Key cannot be regenerated",
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key",
+ "permlevel": 1,
"read_only": 1,
"unique": 1
},
{
"fieldname": "generate_keys",
"fieldtype": "Button",
- "label": "Generate Keys"
+ "label": "Generate Keys",
+ "permlevel": 1
},
{
"fieldname": "column_break_65",
@@ -578,6 +580,7 @@
"fieldname": "api_secret",
"fieldtype": "Password",
"label": "API Secret",
+ "permlevel": 1,
"read_only": 1
},
{
@@ -614,11 +617,6 @@
"link_doctype": "Contact",
"link_fieldname": "user"
},
- {
- "group": "Profile",
- "link_doctype": "Chat Profile",
- "link_fieldname": "user"
- },
{
"group": "Profile",
"link_doctype": "Blogger",
@@ -671,7 +669,7 @@
}
],
"max_attachments": 5,
- "modified": "2021-10-18 16:56:05.578379",
+ "modified": "2021-10-27 17:17:16.098457",
"modified_by": "Administrator",
"module": "Core",
"name": "User",
@@ -706,4 +704,4 @@
"sort_order": "DESC",
"title_field": "full_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py
index 45f7d47a27..86fd1cb4a6 100644
--- a/frappe/core/doctype/user/user.py
+++ b/frappe/core/doctype/user/user.py
@@ -421,9 +421,6 @@ class User(Document):
WHERE `%s` = %s""" %
(tab, field, '%s', field, '%s'), (new_name, old_name))
- if frappe.db.exists("Chat Profile", old_name):
- frappe.rename_doc("Chat Profile", old_name, new_name, force=True, show_alert=False)
-
if frappe.db.exists("Notification Settings", old_name):
frappe.rename_doc("Notification Settings", old_name, new_name, force=True, show_alert=False)
@@ -717,8 +714,10 @@ def ask_pass_update():
# update the sys defaults as to awaiting users
from frappe.utils import set_default
- users = frappe.db.sql("""SELECT DISTINCT(parent) as user FROM `tabUser Email`
- WHERE awaiting_password = 1""", as_dict=True)
+ doctype = DocType("User Email")
+ users = frappe.qb.from_(doctype).where(doctype.awaiting_password == 1).select(
+ doctype.parent.as_("user")
+ ).distinct().run(as_dict=True)
password_list = [ user.get("user") for user in users ]
set_default("email_user_password", u','.join(password_list))
@@ -1055,4 +1054,4 @@ def get_enabled_users():
enabled_users = frappe.get_all("User", filters={"enabled": "1"}, pluck="name")
return enabled_users
- return frappe.cache().get_value("enabled_users", _get_enabled_users)
\ No newline at end of file
+ return frappe.cache().get_value("enabled_users", _get_enabled_users)
diff --git a/frappe/core/doctype/user_permission/test_user_permission.py b/frappe/core/doctype/user_permission/test_user_permission.py
index 85db846982..cf905c2ce2 100644
--- a/frappe/core/doctype/user_permission/test_user_permission.py
+++ b/frappe/core/doctype/user_permission/test_user_permission.py
@@ -73,7 +73,7 @@ class TestUserPermission(unittest.TestCase):
def test_for_applicable_on_update_from_apply_to_all(self):
''' Update User Permission from all to some applicable Doctypes'''
user = create_user('test_bulk_creation_update@example.com')
- param = get_params(user,'User', user.name, applicable = ["Chat Room", "Chat Message"])
+ param = get_params(user,'User', user.name, applicable = ["Comment", "Contact"])
# Initially create User Permission document with apply_to_all checked
is_created = add_user_permissions(get_params(user, 'User', user.name))
@@ -84,8 +84,8 @@ class TestUserPermission(unittest.TestCase):
frappe.db.commit()
removed_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
- is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
- is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
+ is_created_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Comment"))
+ is_created_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Contact"))
# Check that apply_to_all is removed
self.assertIsNone(removed_apply_to_all)
@@ -101,14 +101,14 @@ class TestUserPermission(unittest.TestCase):
param = get_params(user, 'User', user.name)
# create User permissions that with applicable
- is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Chat Room", "Chat Message"]))
+ is_created = add_user_permissions(get_params(user, 'User', user.name, applicable = ["Comment", "Contact"]))
self.assertEqual(is_created, 1)
is_created = add_user_permissions(param)
is_created_apply_to_all = frappe.db.exists("User Permission", get_exists_param(user))
- removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Room"))
- removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Chat Message"))
+ removed_applicable_first = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Comment"))
+ removed_applicable_second = frappe.db.exists("User Permission", get_exists_param(user, applicable = "Contact"))
# To check that a User permission with apply_to_all exists
self.assertIsNotNone(is_created_apply_to_all)
diff --git a/frappe/core/report/transaction_log_report/transaction_log_report.py b/frappe/core/report/transaction_log_report/transaction_log_report.py
index 0a74ece322..e9c68cb0c7 100644
--- a/frappe/core/report/transaction_log_report/transaction_log_report.py
+++ b/frappe/core/report/transaction_log_report/transaction_log_report.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2019, Frappe Technologies and contributors
+# Copyright (c) 2021, Frappe Technologies and contributors
# License: MIT. See LICENSE
import frappe
@@ -12,13 +12,17 @@ def execute(filters=None):
return columns, data
def get_data(filters=None):
-
- logs = frappe.db.sql("SELECT * FROM `tabTransaction Log` order by creation desc ", as_dict=1)
result = []
+ logs = frappe.get_all("Transaction Log", fields=["*"], order_by="creation desc")
+
for l in logs:
row_index = int(l.row_index)
if row_index > 1:
- previous_hash = frappe.db.sql("SELECT chaining_hash FROM `tabTransaction Log` WHERE row_index = {0}".format(row_index - 1))
+ previous_hash = frappe.get_all(
+ "Transaction Log",
+ fields=["chaining_hash"],
+ filters={"row_index": row_index - 1},
+ )
if not previous_hash:
integrity = False
else:
diff --git a/frappe/custom/doctype/property_setter/property_setter.py b/frappe/custom/doctype/property_setter/property_setter.py
index d71b7b0021..7f40be9725 100644
--- a/frappe/custom/doctype/property_setter/property_setter.py
+++ b/frappe/custom/doctype/property_setter/property_setter.py
@@ -43,20 +43,28 @@ class PropertySetter(Document):
def get_setup_data(self):
return {
- 'doctypes': [d[0] for d in frappe.db.sql("select name from tabDocType")],
+ 'doctypes': frappe.get_all("DocType", pluck="name"),
'dt_properties': self.get_property_list('DocType'),
'df_properties': self.get_property_list('DocField')
}
def get_field_ids(self):
- return frappe.db.sql("select name, fieldtype, label, fieldname from tabDocField where parent=%s", self.doc_type, as_dict = 1)
+ return frappe.db.get_values(
+ "DocField",
+ filters={"parent": self.doc_type},
+ fieldname=["name", "fieldtype", "label", "fieldname"],
+ as_dict=True,
+ )
def get_defaults(self):
if not self.field_name:
- return frappe.db.sql("select * from `tabDocType` where name=%s", self.doc_type, as_dict = 1)[0]
+ return frappe.get_all("DocType", filters={"name": self.doc_type}, fields="*")[0]
else:
- return frappe.db.sql("select * from `tabDocField` where fieldname=%s and parent=%s",
- (self.field_name, self.doc_type), as_dict = 1)[0]
+ return frappe.db.get_values(
+ "DocField",
+ filters={"fieldname": self.field_name, "parent": self.doc_type},
+ fieldname="*",
+ )[0]
def on_update(self):
if frappe.flags.in_patch:
diff --git a/frappe/database/database.py b/frappe/database/database.py
index c0d377fd42..a7dd9b6b66 100644
--- a/frappe/database/database.py
+++ b/frappe/database/database.py
@@ -20,7 +20,7 @@ from frappe.query_builder.functions import Count
from frappe.query_builder.functions import Min, Max, Avg, Sum
from frappe.query_builder.utils import Column
from .query import Query
-from pypika.terms import PseudoColumn
+from pypika.terms import Criterion, PseudoColumn
class Database(object):
@@ -543,18 +543,22 @@ class Database(object):
update=None, for_update=False, run=True):
field_objects = []
- for field in fields:
- if "(" in field or " as " in field:
- field_objects.append(PseudoColumn(field))
- else:
- field_objects.append(field)
+ if not isinstance(fields, Criterion):
+ for field in fields:
+ if "(" in field or " as " in field:
+ field_objects.append(PseudoColumn(field))
+ else:
+ field_objects.append(field)
criterion = self.query.build_conditions(
table=doctype, filters=filters, orderby=order_by, for_update=for_update
)
-
if isinstance(fields, (list, tuple)):
query = criterion.select(*field_objects)
+
+ elif isinstance(fields, Criterion):
+ query = criterion.select(fields)
+
else:
if fields=="*":
query = criterion.select(fields)
diff --git a/frappe/desk/doctype/tag/tag.py b/frappe/desk/doctype/tag/tag.py
index aff1bd6973..381c24a765 100644
--- a/frappe/desk/doctype/tag/tag.py
+++ b/frappe/desk/doctype/tag/tag.py
@@ -4,6 +4,7 @@
import frappe
from frappe.model.document import Document
from frappe.utils import unique
+from frappe.query_builder import DocType
class Tag(Document):
pass
@@ -11,7 +12,8 @@ class Tag(Document):
def check_user_tags(dt):
"if the user does not have a tags column, then it creates one"
try:
- frappe.db.sql("select `_user_tags` from `tab%s` limit 1" % dt)
+ doctype = DocType(dt)
+ frappe.qb.from_(doctype).select(doctype._user_tags).limit(1).run()
except Exception as e:
if frappe.db.is_column_missing(e):
DocTags(dt).setup()
@@ -42,10 +44,12 @@ def remove_tag(tag, dt, dn):
@frappe.whitelist()
def get_tagged_docs(doctype, tag):
frappe.has_permission(doctype, throw=True)
-
- return frappe.db.sql("""SELECT name
- FROM `tab{0}`
- WHERE _user_tags LIKE '%{1}%'""".format(doctype, tag))
+ doctype = DocType(doctype)
+ return (
+ frappe.qb.from_(doctype)
+ .where(doctype._user_tags.like(tag))
+ .select(doctype.name)
+ ).run()
@frappe.whitelist()
def get_tags(doctype, txt):
diff --git a/frappe/desk/page/backups/backups.html b/frappe/desk/page/backups/backups.html
index e63481487c..ff10f1bd06 100644
--- a/frappe/desk/page/backups/backups.html
+++ b/frappe/desk/page/backups/backups.html
@@ -1,20 +1,27 @@
- {% for f in files %}
-
- {% endfor %}
+ {% for f in files %}
+
+ {% endfor %}
\ No newline at end of file
diff --git a/frappe/desk/page/backups/backups.js b/frappe/desk/page/backups/backups.js
index 337ad33f43..d6cab750f0 100644
--- a/frappe/desk/page/backups/backups.js
+++ b/frappe/desk/page/backups/backups.js
@@ -1,4 +1,4 @@
-frappe.pages['backups'].on_page_load = function(wrapper) {
+frappe.pages['backups'].on_page_load = function (wrapper) {
var page = frappe.ui.make_app_page({
parent: wrapper,
title: __('Download Backups'),
@@ -11,12 +11,35 @@ frappe.pages['backups'].on_page_load = function(wrapper) {
page.add_inner_button(__("Download Files Backup"), function () {
frappe.call({
- method:"frappe.desk.page.backups.backups.schedule_files_backup",
- args: {"user_email": frappe.session.user_email}
+ method: "frappe.desk.page.backups.backups.schedule_files_backup",
+ args: { "user_email": frappe.session.user_email }
});
});
+ page.add_inner_button(__("Get Backup Encryption Key"), function () {
+ if (frappe.user.has_role("System Manager")) {
+ frappe.verify_password(function () {
+ frappe.call({
+ method: "frappe.utils.backups.get_backup_encryption_key",
+ callback: function (r) {
+ frappe.msgprint({
+ title: __('Backup Encryption Key'),
+ message: __(r.message),
+ indicator: 'blue'
+ });
+ }
+ });
+ });
+ } else {
+ frappe.msgprint({
+ title: __('Error'),
+ message: __('System Manager privileges required.'),
+ indicator: 'red'
+ });
+ }
+ });
+
frappe.breadcrumbs.add("Setup");
$(frappe.render_template("backups")).appendTo(page.body.addClass("no-border"));
-}
+};
diff --git a/frappe/desk/page/backups/backups.py b/frappe/desk/page/backups/backups.py
index 2229a6d89e..14ed025e08 100644
--- a/frappe/desk/page/backups/backups.py
+++ b/frappe/desk/page/backups/backups.py
@@ -11,6 +11,10 @@ def get_context(context):
dt = os.path.getmtime(path)
return convert_utc_to_user_timezone(datetime.datetime.utcfromtimestamp(dt)).strftime('%a %b %d %H:%M %Y')
+ def get_encrytion_status(path):
+ if "-enc" in path:
+ return True
+
def get_size(path):
size = os.path.getsize(path)
if size > 1048576:
@@ -26,8 +30,9 @@ def get_context(context):
cleanup_old_backups(path, files, backup_limit)
files = [('/backups/' + _file,
- get_time(os.path.join(path, _file)),
- get_size(os.path.join(path, _file))) for _file in files if _file.endswith('sql.gz')]
+ get_time(os.path.join(path, _file)),
+ get_encrytion_status(os.path.join(path, _file)),
+ get_size(os.path.join(path, _file))) for _file in files if _file.endswith('sql.gz')]
files.sort(key=lambda x: x[1], reverse=True)
return {"files": files[:backup_limit]}
diff --git a/frappe/desk/query_report.py b/frappe/desk/query_report.py
index 1e8298269f..97bceeb725 100644
--- a/frappe/desk/query_report.py
+++ b/frappe/desk/query_report.py
@@ -72,6 +72,7 @@ def get_report_result(report, filters):
return res
+@frappe.read_only()
def generate_report_result(report, filters=None, user=None, custom_columns=None):
user = user or frappe.session.user
filters = filters or []
@@ -405,7 +406,7 @@ def build_xlsx_data(columns, data, visible_idx, include_indentation, ignore_visi
for column in data.columns:
if column.get("hidden"):
continue
- result[0].append(column["label"])
+ result[0].append(column.get("label"))
column_width = cint(column.get('width', 0))
# to convert into scale accepted by openpyxl
column_width /= 10
diff --git a/frappe/email/doctype/auto_email_report/auto_email_report.py b/frappe/email/doctype/auto_email_report/auto_email_report.py
index 7081a84e7a..34728375cd 100644
--- a/frappe/email/doctype/auto_email_report/auto_email_report.py
+++ b/frappe/email/doctype/auto_email_report/auto_email_report.py
@@ -242,13 +242,16 @@ def make_links(columns, data):
for row in data:
doc_name = row.get('name')
for col in columns:
- if col.fieldtype == "Link" and col.options != "Currency":
- if col.options and row.get(col.fieldname):
+ if not row.get(col.fieldname):
+ continue
+
+ if col.fieldtype == "Link":
+ if col.options and col.options != "Currency":
row[col.fieldname] = get_link_to_form(col.options, row[col.fieldname])
elif col.fieldtype == "Dynamic Link":
- if col.options and row.get(col.fieldname) and row.get(col.options):
+ if col.options and row.get(col.options):
row[col.fieldname] = get_link_to_form(row[col.options], row[col.fieldname])
- elif col.fieldtype == "Currency" and row.get(col.fieldname):
+ elif col.fieldtype == "Currency":
doc = frappe.get_doc(col.parent, doc_name) if doc_name and col.parent else None
# Pass the Document to get the currency based on docfield option
row[col.fieldname] = frappe.format_value(row[col.fieldname], col, doc=doc)
diff --git a/frappe/handler.py b/frappe/handler.py
index ea654517c3..42c17261b4 100755
--- a/frappe/handler.py
+++ b/frappe/handler.py
@@ -27,7 +27,7 @@ def handle():
cmd = frappe.local.form_dict.cmd
data = None
- if cmd!='login':
+ if cmd != 'login':
data = execute_cmd(cmd)
# data can be an empty string or list which are valid responses
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 2ae5a59066..8bca5c066c 100644
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -76,8 +76,6 @@ before_tests = "frappe.utils.install.before_tests"
email_append_to = ["Event", "ToDo", "Communication"]
-get_rooms = 'frappe.chat.doctype.chat_room.chat_room.get_rooms'
-
calendars = ["Event"]
leaderboards = "frappe.desk.leaderboard.get_leaderboards"
@@ -281,11 +279,6 @@ sounds = [
{"name": "error", "src": "/assets/frappe/sounds/error.mp3", "volume": 0.1},
{"name": "alert", "src": "/assets/frappe/sounds/alert.mp3", "volume": 0.2},
# {"name": "chime", "src": "/assets/frappe/sounds/chime.mp3"},
-
- # frappe.chat sounds
- { "name": "chat-message", "src": "/assets/frappe/sounds/chat-message.mp3", "volume": 0.1 },
- { "name": "chat-notification", "src": "/assets/frappe/sounds/chat-notification.mp3", "volume": 0.1 }
- # frappe.chat sounds
]
bot_parsers = [
diff --git a/frappe/integrations/doctype/social_login_key/social_login_key.py b/frappe/integrations/doctype/social_login_key/social_login_key.py
index d6f55e5758..195d6800be 100644
--- a/frappe/integrations/doctype/social_login_key/social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/social_login_key.py
@@ -80,7 +80,9 @@ class SocialLoginKey(Document):
"redirect_url":"/api/method/frappe.www.login.login_via_github",
"api_endpoint":"user",
"api_endpoint_args":None,
- "auth_url_data":None
+ "auth_url_data": json.dumps({
+ "scope": "user:email"
+ })
}
providers["Google"] = {
diff --git a/frappe/integrations/doctype/social_login_key/test_social_login_key.py b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
index 880f1ee99c..73e6a072cb 100644
--- a/frappe/integrations/doctype/social_login_key/test_social_login_key.py
+++ b/frappe/integrations/doctype/social_login_key/test_social_login_key.py
@@ -4,6 +4,12 @@
import frappe
from frappe.integrations.doctype.social_login_key.social_login_key import BaseUrlNotSetError
import unittest
+from frappe.utils.oauth import login_via_oauth2
+from unittest.mock import patch, MagicMock
+from rauth import OAuth2Service
+from frappe.auth import LoginManager, CookieManager
+from frappe.utils import set_request
+
class TestSocialLoginKey(unittest.TestCase):
def test_adding_frappe_social_login_provider(self):
@@ -14,6 +20,41 @@ class TestSocialLoginKey(unittest.TestCase):
social_login_key.get_social_login_provider(provider_name, initialize=True)
self.assertRaises(BaseUrlNotSetError, social_login_key.insert)
+ def test_github_login_with_private_email(self):
+ github_social_login_setup()
+
+ mock_session = MagicMock()
+ mock_session.get.side_effect = github_response_for_private_email
+
+ with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
+ login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token
+
+ def test_github_login_with_public_email(self):
+ github_social_login_setup()
+
+ mock_session = MagicMock()
+ mock_session.get.side_effect = github_response_for_public_email
+
+ with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
+ login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"}) # Dummy code and state token
+
+ def test_normal_signup_and_github_login(self):
+ github_social_login_setup()
+
+ if not frappe.db.exists("User", "githublogin@example.com"):
+ user = frappe.get_doc({
+ "doctype": "User",
+ "email": "githublogin@example.com",
+ "first_name": "GitHub Login"
+ })
+ user.save(ignore_permissions=True)
+
+ mock_session = MagicMock()
+ mock_session.get.side_effect = github_response_for_login
+
+ with patch.object(OAuth2Service, "get_auth_session", return_value=mock_session):
+ login_via_oauth2("github", "iwriu", {"token": "ewrwerwer"})
+
def make_social_login_key(**kwargs):
kwargs["doctype"] = "Social Login Key"
if not "provider_name" in kwargs:
@@ -34,3 +75,48 @@ def create_or_update_social_login_key():
frappe.db.commit()
return social_login_key
+
+def create_github_social_login_key():
+ if frappe.db.exists("Social Login Key", "github"):
+ return frappe.get_doc("Social Login Key", "github")
+ else:
+ provider_name = "GitHub"
+ social_login_key = make_social_login_key(
+ social_login_provider=provider_name
+ )
+ social_login_key.get_social_login_provider(provider_name, initialize=True)
+
+ # Dummy client_id and client_secret
+ social_login_key.client_id = "h6htd6q"
+ social_login_key.client_secret = "keoererk988ekkhf8w9e8ewrjhhkjer9889"
+ social_login_key.insert(ignore_permissions=True)
+ return social_login_key
+
+def github_response_for_private_email(url, *args, **kwargs):
+ if url == "user":
+ return_value = {"login": "dummy_username", "id": "223342", "email": None, "first_name": "Github Private"}
+ else:
+ return_value = [{"email": "github@example.com", "primary": True, "verified": True}]
+
+ return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
+
+def github_response_for_public_email(url, *args, **kwargs):
+ if url == "user":
+ return_value = {"login": "dummy_username", "id": "223343", "email": "github_public@example.com", "first_name": "Github Public"}
+
+ return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
+
+def github_response_for_login(url, *args, **kwargs):
+ if url == "user":
+ return_value = {"login": "dummy_username", "id": "223346", "email": None, "first_name": "Github Login"}
+ else:
+ return_value = [{"email": "githublogin@example.com", "primary": True, "verified": True}]
+
+ return MagicMock(status_code=200, json=MagicMock(return_value=return_value))
+
+def github_social_login_setup():
+ set_request(path="/random")
+ frappe.local.cookie_manager = CookieManager()
+ frappe.local.login_manager = LoginManager()
+
+ create_github_social_login_key()
diff --git a/frappe/model/base_document.py b/frappe/model/base_document.py
index 066085a27c..1826cca9a3 100644
--- a/frappe/model/base_document.py
+++ b/frappe/model/base_document.py
@@ -81,9 +81,6 @@ class BaseDocument(object):
if hasattr(self, "__setup__"):
self.__setup__()
- def __getitem__(self, key):
- return self.get(key) if hasattr(self, key) else frappe.throw(msg=key, exc=KeyError)
-
@property
def meta(self):
if not getattr(self, "_meta", None):
diff --git a/frappe/model/naming.py b/frappe/model/naming.py
index 71ff281642..deea6698b3 100644
--- a/frappe/model/naming.py
+++ b/frappe/model/naming.py
@@ -17,6 +17,7 @@ from frappe import _
from frappe.utils import now_datetime, cint, cstr
import re
from frappe.model import log_types
+from frappe.query_builder import DocType
def set_new_name(doc):
@@ -194,7 +195,15 @@ def parse_naming_series(parts, doctype='', doc=''):
def getseries(key, digits):
# series created ?
- current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (key,))
+ # Using frappe.qb as frappe.get_values does not allow order_by=None
+ series = DocType("Series")
+ current = (
+ frappe.qb.from_(series)
+ .where(series.name == key)
+ .for_update()
+ .select("current")
+ ).run()
+
if current and current[0][0] is not None:
current = current[0][0]
# yes, update it
@@ -260,7 +269,13 @@ def revert_series_if_last(key, name, doc=None):
prefix = parse_naming_series(prefix.split('.'), doc=doc)
count = cint(name.replace(prefix, ""))
- current = frappe.db.sql("SELECT `current` FROM `tabSeries` WHERE `name`=%s FOR UPDATE", (prefix,))
+ series = DocType("Series")
+ current = (
+ frappe.qb.from_(series)
+ .where(series.name == prefix)
+ .for_update()
+ .select("current")
+ ).run()
if current and current[0][0]==count:
frappe.db.sql("UPDATE `tabSeries` SET `current` = `current` - 1 WHERE `name`=%s", prefix)
diff --git a/frappe/model/rename_doc.py b/frappe/model/rename_doc.py
index de83b24cd8..ee9044b73e 100644
--- a/frappe/model/rename_doc.py
+++ b/frappe/model/rename_doc.py
@@ -7,6 +7,7 @@ from frappe.model.naming import validate_name
from frappe.model.utils.user_settings import sync_user_settings, update_user_settings_data
from frappe.utils import cint
from frappe.utils.password import rename_password
+from frappe.query_builder import Field
@frappe.whitelist()
@@ -191,8 +192,14 @@ def update_autoname_field(doctype, new, meta):
def validate_rename(doctype, new, meta, merge, force, ignore_permissions):
# using for update so that it gets locked and someone else cannot edit it while this rename is going on!
- exists = frappe.db.sql("select name from `tab{doctype}` where name=%s for update".format(doctype=doctype), new)
- exists = exists[0][0] if exists else None
+ exists = (
+ frappe.qb.from_(doctype)
+ .where(Field("name") == new)
+ .for_update()
+ .select("name")
+ .run(pluck=True)
+ )
+ exists = exists[0] if exists else None
if merge and not exists:
frappe.msgprint(_("{0} {1} does not exist, select a new target to merge").format(doctype, new), raise_exception=1)
diff --git a/frappe/modules.txt b/frappe/modules.txt
index 1229116a2e..a707ca853e 100644
--- a/frappe/modules.txt
+++ b/frappe/modules.txt
@@ -9,7 +9,6 @@ Integrations
Printing
Contacts
Data Migration
-Chat
Social
Automation
-Event Streaming
+Event Streaming
\ No newline at end of file
diff --git a/frappe/patches.txt b/frappe/patches.txt
index abfd2ee31c..b230c336b4 100644
--- a/frappe/patches.txt
+++ b/frappe/patches.txt
@@ -183,5 +183,6 @@ frappe.patches.v13_0.jinja_hook
frappe.patches.v13_0.update_notification_channel_if_empty
frappe.patches.v14_0.drop_data_import_legacy
frappe.patches.v14_0.rename_cancelled_documents
-frappe.patches.v14_0.update_workspace2 # 25.08.2021
-frappe.patches.v14_0.copy_mail_data #08.03.21
\ No newline at end of file
+frappe.patches.v14_0.copy_mail_data #08.03.21
+frappe.patches.v14_0.update_workspace2 # 20.09.2021
+frappe.patches.v14_0.update_github_endpoints #08-11-2021
diff --git a/frappe/patches/v14_0/update_github_endpoints.py b/frappe/patches/v14_0/update_github_endpoints.py
new file mode 100644
index 0000000000..8f9a06a043
--- /dev/null
+++ b/frappe/patches/v14_0/update_github_endpoints.py
@@ -0,0 +1,10 @@
+import frappe
+import json
+
+def execute():
+ if frappe.db.exists("Social Login Key", "github"):
+ frappe.db.set_value("Social Login Key", "github", "auth_url_data",
+ json.dumps({
+ "scope": "user:email"
+ })
+ )
diff --git a/frappe/public/js/chat.bundle.js b/frappe/public/js/chat.bundle.js
deleted file mode 100644
index 5f9a91ebb7..0000000000
--- a/frappe/public/js/chat.bundle.js
+++ /dev/null
@@ -1 +0,0 @@
-import "./frappe/chat";
diff --git a/frappe/public/js/desk.bundle.js b/frappe/public/js/desk.bundle.js
index 338ddda601..50947bd9bc 100644
--- a/frappe/public/js/desk.bundle.js
+++ b/frappe/public/js/desk.bundle.js
@@ -99,7 +99,6 @@ import "./frappe/query_string.js";
// import "./frappe/ui/comment.js";
-import "./frappe/chat.js";
import "./frappe/utils/energy_point_utils.js";
import "./frappe/utils/dashboard_utils.js";
import "./frappe/ui/chart.js";
diff --git a/frappe/public/js/frappe/chat.js b/frappe/public/js/frappe/chat.js
deleted file mode 100644
index fd440dcbbc..0000000000
--- a/frappe/public/js/frappe/chat.js
+++ /dev/null
@@ -1,2788 +0,0 @@
-// Frappe Chat
-// Author - Achilles Rasquinha
-
-import Fuse from 'fuse.js'
-import hyper from '../lib/hyper.min'
-
-import './socketio_client'
-
-import './ui/dialog'
-import './ui/capture'
-
-import './utils/user'
-
-/* eslint semi: "never" */
-// Fuck semicolons - https://mislav.net/2010/05/semicolons
-
-// frappe extensions
-
-/**
- * @description The base class for all Frappe Errors.
- *
- * @example
- * try
- * throw new frappe.Error("foobar")
- * catch (e)
- * console.log(e.name)
- * // returns "FrappeError"
- *
- * @see https://stackoverflow.com/a/32749533
- * @todo Requires "transform-builtin-extend" for Babel 6
- */
-frappe.Error = Error
-// class extends Error {
-// constructor (message) {
-// super (message)
-
-// this.name = 'FrappeError'
-
-// if ( typeof Error.captureStackTrace === 'function' )
-// Error.captureStackTrace(this, this.constructor)
-// else
-// this.stack = (new Error(message)).stack
-// }
-// }
-
-/**
- * @description TypeError
- */
-frappe.TypeError = TypeError
-// class extends frappe.Error {
-// constructor (message) {
-// super (message)
-
-// this.name = this.constructor.name
-// }
-// }
-
-/**
- * @description ValueError
- */
-frappe.ValueError = Error
-// class extends frappe.Error {
-// constructor (message) {
-// super (message)
-
-// this.name = this.constructor.name
-// }
-// }
-
-/**
- * @description ImportError
- */
-frappe.ImportError = Error
-// class extends frappe.Error {
-// constructor (message) {
-// super (message)
-
-// this.name = this.constructor.name
-// }
-// }
-
-// frappe.datetime
-frappe.provide('frappe.datetime')
-
-/**
- * @description Frappe's datetime object. (Inspired by Python's datetime object).
- *
- * @example
- * const datetime = new frappe.datetime.datetime()
- */
-frappe.datetime.datetime = class {
- /**
- * @description Frappe's datetime Class's constructor.
- */
- constructor (instance, format = null) {
- if ( typeof moment === 'undefined' )
- throw new frappe.ImportError(`Moment.js not installed.`)
-
- this.moment = instance ? moment(instance, format) : moment()
- }
-
- /**
- * @description Returns a formatted string of the datetime object.
- */
- format (format = null) {
- const formatted = this.moment.format(format)
- return formatted
- }
-}
-
-/**
- * @description Frappe's daterange object.
- *
- * @example
- * const range = new frappe.datetime.range(frappe.datetime.now(), frappe.datetime.now())
- * range.contains(frappe.datetime.now())
- */
-frappe.datetime.range = class {
- constructor (start, end) {
- if ( typeof moment === undefined )
- throw new frappe.ImportError(`Moment.js not installed.`)
-
- this.start = start
- this.end = end
- }
-
- contains (datetime) {
- const contains = datetime.moment.isBetween(this.start.moment, this.end.moment)
- return contains
- }
-}
-
-/**
- * @description Returns the current datetime.
- *
- * @example
- * const datetime = new frappe.datetime.now()
- */
-frappe.datetime.now = () => new frappe.datetime.datetime()
-
-frappe.datetime.equal = (a, b, type) => {
- a = a.moment
- b = b.moment
-
- const equal = a.isSame(b, type)
-
- return equal
-}
-
-/**
- * @description Compares two frappe.datetime.datetime objects.
- *
- * @param {frappe.datetime.datetime} a - A frappe.datetime.datetime/moment object.
- * @param {frappe.datetime.datetime} b - A frappe.datetime.datetime/moment object.
- *
- * @returns {number} 0 (if a and b are equal), 1 (if a is before b), -1 (if a is after b).
- *
- * @example
- * frappe.datetime.compare(frappe.datetime.now(), frappe.datetime.now())
- * // returns 0
- * const then = frappe.datetime.now()
- *
- * frappe.datetime.compare(then, frappe.datetime.now())
- * // returns 1
- */
-frappe.datetime.compare = (a, b) => {
- a = a.moment
- b = b.moment
-
- if ( a.isBefore(b) )
- return 1
- else
- if ( b.isBefore(a) )
- return -1
- else
- return 0
-}
-
-// frappe.quick_edit
-frappe.quick_edit = (doctype, docname, fn) => {
- return new Promise(resolve => {
- frappe.model.with_doctype(doctype, () => {
- frappe.db.get_doc(doctype, docname).then(doc => {
- const meta = frappe.get_meta(doctype)
- const fields = meta.fields
- const required = fields.filter(f => f.reqd || f.bold && !f.read_only)
-
- required.map(f => {
- if(f.fieldname == 'content' && doc.type == 'File') {
- f['read_only'] = 1;
- }
- })
-
- const dialog = new frappe.ui.Dialog({
- title: __('Edit') + `${doctype} (${docname})`,
- fields: required,
- action: {
- primary: {
- label: __("Save"),
- onsubmit: (values) => {
- frappe.call('frappe.client.save',
- { doc: { doctype: doctype, docname: docname, ...doc, ...values } })
- .then(r => {
- if ( fn )
- fn(r.message)
-
- resolve(r.message)
- })
-
- dialog.hide()
- }
- },
- secondary: {
- label: __("Discard")
- }
- }
- })
- dialog.set_values(doc)
-
- const $element = $(dialog.body)
- $element.append(`
-
-
-
- `)
- $element.find('.qe-fp').click(() => {
- dialog.hide()
- frappe.set_route('Form', doctype, docname)
- })
-
- dialog.show()
- })
- })
- })
-}
-
-// frappe._
-// frappe's utility namespace.
-frappe.provide('frappe._')
-
-// String Utilities
-
-/**
- * @description Python-inspired format extension for string objects.
- *
- * @param {string} string - A string with placeholders.
- * @param {object} object - An object with placeholder, value pairs.
- *
- * @return {string} - The formatted string.
- *
- * @example
- * frappe._.format('{foo} {bar}', { bar: 'foo', foo: 'bar' })
- * // returns "bar foo"
- */
-frappe._.format = (string, object) => {
- for (const key in object)
- string = string.replace(`{${key}}`, object[key])
-
- return string
-}
-
-/**
- * @description Fuzzy Search a given query within a dataset.
- *
- * @param {string} query - A query string.
- * @param {array} dataset - A dataset to search within, can contain singletons or objects.
- * @param {object} options - Options as per fuze.js
- *
- * @return {array} - The fuzzy matched index/object within the dataset.
- *
- * @example
- * frappe._.fuzzy_search("foobar", ["foobar", "bartender"])
- * // returns [0, 1]
- *
- * @see http://fusejs.io
- */
-frappe._.fuzzy_search = (query, dataset, options) => {
- const DEFAULT = {
- shouldSort: true,
- threshold: 0.6,
- location: 0,
- distance: 100,
- minMatchCharLength: 1,
- maxPatternLength: 32
- }
- options = { ...DEFAULT, ...options }
-
- const fuse = new Fuse(dataset, options)
- const result = fuse.search(query)
-
- return result
-}
-
-/**
- * @description Pluralizes a given word.
- *
- * @param {string} word - The word to be pluralized.
- * @param {number} count - The count.
- *
- * @return {string} - The pluralized string.
- *
- * @example
- * frappe._.pluralize('member', 1)
- * // returns "member"
- * frappe._.pluralize('members', 0)
- * // returns "members"
- *
- * @todo Handle more edge cases.
- */
-frappe._.pluralize = (word, count = 0, suffix = 's') => `${word}${count === 1 ? '' : suffix}`
-
-/**
- * @description Captializes a given string.
- *
- * @param {word} - The word to be capitalized.
- *
- * @return {string} - The capitalized word.
- *
- * @example
- * frappe._.capitalize('foobar')
- * // returns "Foobar"
- */
-frappe._.capitalize = word => `${word.charAt(0).toUpperCase()}${word.slice(1)}`
-
-// Array Utilities
-
-/**
- * @description Returns the first element of an array.
- *
- * @param {array} array - The array.
- *
- * @returns - The first element of an array, undefined elsewise.
- *
- * @example
- * frappe._.head([1, 2, 3])
- * // returns 1
- * frappe._.head([])
- * // returns undefined
- */
-frappe._.head = arr => frappe._.is_empty(arr) ? undefined : arr[0]
-
-/**
- * @description Returns a copy of the given array (shallow).
- *
- * @param {array} array - The array to be copied.
- *
- * @returns {array} - The copied array.
- *
- * @example
- * frappe._.copy_array(["foobar", "barfoo"])
- * // returns ["foobar", "barfoo"]
- *
- * @todo Add optional deep copy.
- */
-frappe._.copy_array = array => {
- if ( Array.isArray(array) )
- return array.slice()
- else
- throw frappe.TypeError(`Expected Array, recieved ${typeof array} instead.`)
-}
-
-/**
- * @description Check whether an array|string|object|jQuery is empty.
- *
- * @param {any} value - The value to be checked on.
- *
- * @returns {boolean} - Returns if the object is empty.
- *
- * @example
- * frappe._.is_empty([]) // returns true
- * frappe._.is_empty(["foo"]) // returns false
- *
- * frappe._.is_empty("") // returns true
- * frappe._.is_empty("foo") // returns false
- *
- * frappe._.is_empty({ }) // returns true
- * frappe._.is_empty({ foo: "bar" }) // returns false
- *
- * frappe._.is_empty($('.papito')) // returns false
- *
- * @todo Handle other cases.
- */
-frappe._.is_empty = value => {
- let empty = false
-
- if ( value === undefined || value === null )
- empty = true
- else
- if ( Array.isArray(value) || typeof value === 'string' || value instanceof $ )
- empty = value.length === 0
- else
- if ( typeof value === 'object' )
- empty = Object.keys(value).length === 0
-
- return empty
-}
-
-/**
- * @description Converts a singleton to an array, if required.
- *
- * @param {object} item - An object
- *
- * @example
- * frappe._.as_array("foo")
- * // returns ["foo"]
- *
- * frappe._.as_array(["foo"])
- * // returns ["foo"]
- *
- * @see https://docs.oracle.com/javase/8/docs/api/java/util/Arrays.html#asList-T...-
- */
-frappe._.as_array = item => Array.isArray(item) ? item : [item]
-
-/**
- * @description Return a singleton if array contains a single element.
- *
- * @param {array} list - An array to squash.
- *
- * @returns {array|object} - Returns an array if there's more than 1 object else the first object itself.
- *
- * @example
- * frappe._.squash(["foo"])
- * // returns "foo"
- *
- * frappe._.squash(["foo", "bar"])
- * // returns ["foo", "bar"]
- */
-frappe._.squash = list => Array.isArray(list) && list.length === 1 ? list[0] : list
-
-/**
- * @description Returns true, if the current device is a mobile device.
- *
- * @example
- * frappe._.is_mobile()
- * // returns true|false
- *
- * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
- */
-frappe._.is_mobile = () => {
- const regex = new RegExp("Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini", "i")
- const agent = navigator.userAgent
- const mobile = regex.test(agent)
-
- return mobile
-}
-
-/**
- * @description Removes falsey values from an array.
- *
- * @example
- * frappe._.compact([1, 2, false, NaN, ''])
- * // returns [1, 2]
- */
-frappe._.compact = array => array.filter(Boolean)
-
-// extend utils to base.
-frappe.utils = { ...frappe.utils, ...frappe._ }
-
-// frappe extensions
-
-// frappe.user extensions
-/**
- * @description Returns the first name of a User.
- *
- * @param {string} user - User
- *
- * @returns The first name of the user.
- *
- * @example
- * frappe.user.first_name("Rahul Malhotra")
- * // returns "Rahul"
- */
-frappe.provide('frappe.user')
-frappe.user.first_name = user => frappe._.head(frappe.user.full_name(user).split(" "))
-
-frappe.provide('frappe.ui.keycode')
-frappe.ui.keycode = { RETURN: 13 }
-
-/**
- * @description Frappe's Store Class
- */
- // frappe.stores - A registry for frappe stores.
-frappe.provide('frappe.stores')
-frappe.stores = [ ]
-frappe.Store = class
-{
- /**
- * @description Frappe's Store Class's constructor.
- *
- * @param {string} name - Name of the logger.
- */
- constructor (name) {
- if ( typeof name !== 'string' )
- throw new frappe.TypeError(`Expected string for name, got ${typeof name} instead.`)
- this.name = name
- }
-
- /**
- * @description Get instance of frappe.Store (return registered one if declared).
- *
- * @param {string} name - Name of the store.
- */
- static get (name) {
- if ( !(name in frappe.stores) )
- frappe.stores[name] = new frappe.Store(name)
- return frappe.stores[name]
- }
-
- set (key, value) { localStorage.setItem(`${this.name}:${key}`, value) }
- get (key, value) { return localStorage.getItem(`${this.name}:${key}`) }
-}
-
-// frappe.loggers - A registry for frappe loggers.
-frappe.provide('frappe.loggers')
-/**
- * @description Frappe's Logger Class
- *
- * @example
- * frappe.log = frappe.Logger.get('foobar')
- * frappe.log.level = frappe.Logger.DEBUG
- *
- * frappe.log.info('foobar')
- * // prints '[timestamp] foobar: foobar'
- */
-frappe.Logger = class {
- /**
- * @description Frappe's Logger Class's constructor.
- *
- * @param {string} name - Name of the logger.
- */
- constructor (name, level) {
- if ( typeof name !== 'string' )
- throw new frappe.TypeError(`Expected string for name, got ${typeof name} instead.`)
-
- this.name = name
- this.level = level
-
- if ( !this.level ) {
- if ( frappe.boot.developer_mode )
- this.level = frappe.Logger.ERROR
- else
- this.level = frappe.Logger.NOTSET
- }
- this.format = frappe.Logger.FORMAT
- }
-
- /**
- * @description Get instance of frappe.Logger (return registered one if declared).
- *
- * @param {string} name - Name of the logger.
- */
- static get (name, level) {
- if ( !(name in frappe.loggers) )
- frappe.loggers[name] = new frappe.Logger(name, level)
- return frappe.loggers[name]
- }
-
- debug (message) { this.log(message, frappe.Logger.DEBUG) }
- info (message) { this.log(message, frappe.Logger.INFO) }
- warn (message) { this.log(message, frappe.Logger.WARN) }
- error (message) { this.log(message, frappe.Logger.ERROR) }
-
- log (message, level) {
- const timestamp = frappe.datetime.now()
-
- if ( level.value <= this.level.value ) {
- const format = frappe._.format(this.format, {
- time: timestamp.format('HH:mm:ss'),
- name: this.name
- })
- console.log(`%c ${format}:`, `color: ${level.color}`, message)
- }
- }
-}
-
-frappe.Logger.DEBUG = { value: 10, color: '#616161', name: 'DEBUG' }
-frappe.Logger.INFO = { value: 20, color: '#2196F3', name: 'INFO' }
-frappe.Logger.WARN = { value: 30, color: '#FFC107', name: 'WARN' }
-frappe.Logger.ERROR = { value: 40, color: '#F44336', name: 'ERROR' }
-frappe.Logger.NOTSET = { value: 0, name: 'NOTSET' }
-
-frappe.Logger.FORMAT = '{time} {name}'
-
-// frappe.chat
-frappe.provide('frappe.chat')
-
-frappe.log = frappe.Logger.get('frappe.chat', frappe.Logger.NOTSET)
-
-// frappe.chat.profile
-frappe.provide('frappe.chat.profile')
-
-/**
- * @description Create a Chat Profile.
- *
- * @param {string|array} fields - (Optional) fields to be retrieved after creating a Chat Profile.
- * @param {function} fn - (Optional) callback with the returned Chat Profile.
- *
- * @returns {Promise}
- *
- * @example
- * frappe.chat.profile.create(console.log)
- *
- * frappe.chat.profile.create("status").then(console.log) // { status: "Online" }
- */
-frappe.chat.profile.create = (fields, fn) => {
- if ( typeof fields === "function" ) {
- fn = fields
- fields = null
- } else
- if ( typeof fields === "string" )
- fields = frappe._.as_array(fields)
-
- return new Promise(resolve => {
- frappe.call("frappe.chat.doctype.chat_profile.chat_profile.create",
- { user: frappe.session.user, exists_ok: true, fields: fields },
- response => {
- if ( fn )
- fn(response.message)
-
- resolve(response.message)
- })
- })
-}
-
-/**
- * @description Updates a Chat Profile.
- *
- * @param {string} user - (Optional) Chat Profile User, defaults to session user.
- * @param {object} update - (Required) Updates to be dispatched.
- *
- * @example
- * frappe.chat.profile.update(frappe.session.user, { "status": "Offline" })
- */
-frappe.chat.profile.update = (user, update, fn) => {
- return new Promise(resolve => {
- frappe.call("frappe.chat.doctype.chat_profile.chat_profile.update",
- { user: user || frappe.session.user, data: update },
- response => {
- if ( fn )
- fn(response.message)
-
- resolve(response.message)
- })
- })
-}
-
-// frappe.chat.profile.on
-frappe.provide('frappe.chat.profile.on')
-
-/**
- * @description Triggers on a Chat Profile update of a user (Only if there's a one-on-one conversation).
- *
- * @param {function} fn - (Optional) callback with the User and the Chat Profile update.
- *
- * @returns {Promise}
- *
- * @example
- * frappe.chat.profile.on.update(function (user, update)
- * {
- * // do stuff
- * })
- */
-frappe.chat.profile.on.update = function (fn) {
- frappe.realtime.on("frappe.chat.profile:update", r => fn(r.user, r.data))
-}
-frappe.chat.profile.STATUSES
-=
-[
- {
- name: "Online",
- color: "green"
- },
- {
- name: "Away",
- color: "yellow"
- },
- {
- name: "Busy",
- color: "red"
- },
- {
- name: "Offline",
- color: "gray"
- }
-]
-
-// frappe.chat.room
-frappe.provide('frappe.chat.room')
-
-/**
- * @description Creates a Chat Room.
- *
- * @param {string} kind - (Required) "Direct", "Group" or "Visitor".
- * @param {string} owner - (Optional) Chat Room owner (defaults to current user).
- * @param {string|array} users - (Required for "Direct" and "Visitor", Optional for "Group") User(s) within Chat Room.
- * @param {string} name - Chat Room name.
- * @param {function} fn - callback with created Chat Room.
- *
- * @returns {Promise}
- *
- * @example
- * frappe.chat.room.create("Direct", frappe.session.user, "foo@bar.com", function (room) {
- * // do stuff
- * })
- * frappe.chat.room.create("Group", frappe.session.user, ["santa@gmail.com", "banta@gmail.com"], "Santa and Banta", function (room) {
- * // do stuff
- * })
- */
-frappe.chat.room.create = function (kind, owner, users, name, fn) {
- if ( typeof name === "function" ) {
- fn = name
- name = null
- }
-
- users = frappe._.as_array(users)
-
- return new Promise(resolve => {
- frappe.call("frappe.chat.doctype.chat_room.chat_room.create",
- { kind: kind, token: owner || frappe.session.user, users: users, name: name },
- r => {
- let room = r.message
- room = { ...room, creation: new frappe.datetime.datetime(room.creation) }
-
- if ( fn )
- fn(room)
-
- resolve(room)
- })
- })
-}
-
-/**
- * @description Returns Chat Room(s).
- *
- * @param {string|array} names - (Optional) Chat Room(s) to retrieve.
- * @param {string|array} fields - (Optional) fields to be retrieved for each Chat Room.
- * @param {function} fn - (Optional) callback with the returned Chat Room(s).
- *
- * @returns {Promise}
- *
- * @example
- * frappe.chat.room.get(function (rooms) {
- * // do stuff
- * })
- * frappe.chat.room.get().then(function (rooms) {
- * // do stuff
- * })
- *
- * frappe.chat.room.get(null, ["room_name", "avatar"], function (rooms) {
- * // do stuff
- * })
- *
- * frappe.chat.room.get("CR00001", "room_name", function (room) {
- * // do stuff
- * })
- *
- * frappe.chat.room.get(["CR00001", "CR00002"], ["room_name", "last_message"], function (rooms) {
- *
- * })
- */
-frappe.chat.room.get = function (names, fields, fn) {
- if ( typeof names === "function" ) {
- fn = names
- names = null
- fields = null
- }
- else
- if ( typeof names === "string" ) {
- names = frappe._.as_array(names)
-
- if ( typeof fields === "function" ) {
- fn = fields
- fields = null
- }
- else
- if ( typeof fields === "string" )
- fields = frappe._.as_array(fields)
- }
-
- return new Promise(resolve => {
- frappe.call("frappe.chat.doctype.chat_room.chat_room.get",
- { user: frappe.session.user, rooms: names, fields: fields },
- response => {
- let rooms = response.message
- if ( rooms ) { // frappe.api BOGZ! (emtpy arrays are falsified, not good design).
- rooms = frappe._.as_array(rooms)
- rooms = rooms.map(room => {
- return { ...room, creation: new frappe.datetime.datetime(room.creation),
- last_message: room.last_message ? {
- ...room.last_message,
- creation: new frappe.datetime.datetime(room.last_message.creation)
- } : null
- }
- })
- rooms = frappe._.squash(rooms)
- }
- else
- rooms = [ ]
-
- if ( fn )
- fn(rooms)
-
- resolve(rooms)
- })
- })
-}
-
-/**
- * @description Subscribe current user to said Chat Room(s).
- *
- * @param {string|array} rooms - Chat Room(s).
- *
- * @example
- * frappe.chat.room.subscribe("CR00001")
- */
-frappe.chat.room.subscribe = function (rooms) {
- frappe.realtime.publish("frappe.chat.room:subscribe", rooms)
-}
-
-/**
- * @description Get Chat Room history.
- *
- * @param {string} name - Chat Room name
- *
- * @returns {Promise} - Chat Message(s)
- *
- * @example
- * frappe.chat.room.history(function (messages)
- * {
- * // do stuff.
- * })
- */
-frappe.chat.room.history = function (name, fn) {
- return new Promise(resolve => {
- frappe.call("frappe.chat.doctype.chat_room.chat_room.history",
- { room: name, user: frappe.session.user },
- r => {
- let messages = r.message ? frappe._.as_array(r.message) : [ ] // frappe.api BOGZ! (emtpy arrays are falsified, not good design).
- messages = messages.map(m => {
- return { ...m,
- creation: new frappe.datetime.datetime(m.creation)
- }
- })
-
- if ( fn )
- fn(messages)
-
- resolve(messages)
- })
- })
-}
-
-/**
- * @description Searches Rooms based on a query.
- *
- * @param {string} query - The query string.
- * @param {array} rooms - A list of Chat Rooms.
- *
- * @returns {array} - A fuzzy searched list of rooms.
- */
-frappe.chat.room.search = function (query, rooms) {
- const dataset = rooms.map(r => {
- if ( r.room_name )
- return r.room_name
- else
- if ( r.owner === frappe.session.user )
- return frappe.user.full_name(frappe._.squash(r.users))
- else
- return frappe.user.full_name(r.owner)
- })
- const results = frappe._.fuzzy_search(query, dataset)
- rooms = results.map(i => rooms[i])
-
- return rooms
-}
-
-/**
- * @description Sort Chat Room(s) based on Last Message Timestamp or Creation Date.
- *
- * @param {array} - A list of Chat Room(s)
- * @param {compare} - (Optional) a comparision function.
- */
-frappe.chat.room.sort = function (rooms, compare = null) {
- compare = compare || function (a, b) {
- if ( a.last_message && b.last_message )
- return frappe.datetime.compare(a.last_message.creation, b.last_message.creation)
- else
- if ( a.last_message )
- return frappe.datetime.compare(a.last_message.creation, b.creation)
- else
- if ( b.last_message )
- return frappe.datetime.compare(a.creation, b.last_message.creation)
- else
- return frappe.datetime.compare(a.creation, b.creation)
- }
- rooms.sort(compare)
-
- return rooms
-}
-
-// frappe.chat.room.on
-frappe.provide('frappe.chat.room.on')
-
-/**
- * @description Triggers on Chat Room updated.
- *
- * @param {function} fn - callback with the Chat Room and Update.
- */
-frappe.chat.room.on.update = function (fn) {
- frappe.realtime.on("frappe.chat.room:update", r => {
- if ( r.data.last_message )
- // creation to frappe.datetime.datetime (easier to manipulate).
- r.data = { ...r.data, last_message: { ...r.data.last_message, creation: new frappe.datetime.datetime(r.data.last_message.creation) } }
-
- fn(r.room, r.data)
- })
-}
-
-/**
- * @description Triggers on Chat Room created.
- *
- * @param {function} fn - callback with the created Chat Room.
- */
-frappe.chat.room.on.create = function (fn) {
- frappe.realtime.on("frappe.chat.room:create", r =>
- fn({ ...r, creation: new frappe.datetime.datetime(r.creation) })
- )
-}
-
-/**
- * @description Triggers when a User is typing in a Chat Room.
- *
- * @param {function} fn - callback with the typing User within the Chat Room.
- */
-frappe.chat.room.on.typing = function (fn) {
- frappe.realtime.on("frappe.chat.room:typing", r => fn(r.room, r.user))
-}
-
-// frappe.chat.message
-frappe.provide('frappe.chat.message')
-
-frappe.chat.message.typing = function (room, user) {
- frappe.realtime.publish("frappe.chat.message:typing", { user: user || frappe.session.user, room: room })
-}
-
-frappe.chat.message.send = function (room, message, type = "Content") {
- frappe.call("frappe.chat.doctype.chat_message.chat_message.send",
- { user: frappe.session.user, room: room, content: message, type: type })
-}
-
-frappe.chat.message.update = function (message, update, fn) {
- return new Promise(resolve => {
- frappe.call('frappe.chat.doctype.chat_message.chat_message.update',
- { user: frappe.session.user, message: message, update: update },
- r => {
- if ( fn )
- fn(response.message)
-
- resolve(response.message)
- })
- })
-}
-
-frappe.chat.message.sort = (messages) => {
- if ( !frappe._.is_empty(messages) )
- messages.sort((a, b) => frappe.datetime.compare(b.creation, a.creation))
-
- return messages
-}
-
-/**
- * @description Add user to seen (defaults to session.user)
- */
-frappe.chat.message.seen = (mess, user) => {
- frappe.call('frappe.chat.doctype.chat_message.chat_message.seen',
- { message: mess, user: user || frappe.session.user })
-}
-
-frappe.provide('frappe.chat.message.on')
-frappe.chat.message.on.create = function (fn) {
- frappe.realtime.on("frappe.chat.message:create", r =>
- fn({ ...r, creation: new frappe.datetime.datetime(r.creation) })
- )
-}
-
-frappe.chat.message.on.update = function (fn) {
- frappe.realtime.on("frappe.chat.message:update", r => fn(r.message, r.data))
-}
-
-frappe.chat.pretty_datetime = function (date) {
- const today = moment()
- const instance = date.moment
-
- if ( today.isSame(instance, "d") )
- return instance.format("hh:mm A")
- else
- if ( today.isSame(instance, "week") )
- return instance.format("dddd")
- else
- return instance.format("DD/MM/YYYY")
-}
-
-// frappe.chat.sound
-frappe.provide('frappe.chat.sound')
-
-/**
- * @description Plays a given registered sound.
- *
- * @param {value} - The name of the registered sound.
- *
- * @example
- * frappe.chat.sound.play("message")
- */
-frappe.chat.sound.play = function (name, volume = 0.1) {
- // frappe._.play_sound(`chat-${name}`)
- const $audio = $(``)
- $audio.attr('volume', volume)
-
- if ( frappe._.is_empty($audio) )
- $(document).append($audio)
-
- if ( !$audio.paused ) {
- frappe.log.info('Stopping sound playing.')
- $audio[0].pause()
- $audio.attr('currentTime', 0)
- }
-
- frappe.log.info('Playing sound.')
- $audio.attr('src', `${frappe.chat.sound.PATH}/chat-${name}.mp3`)
- $audio[0].play()
-}
-frappe.chat.sound.PATH = '/assets/frappe/sounds'
-
-// frappe.chat.emoji
-frappe.chat.emojis = [ ]
-frappe.chat.emoji = function (fn) {
- return new Promise(resolve => {
- if ( !frappe._.is_empty(frappe.chat.emojis) ) {
- if ( fn )
- fn(frappe.chat.emojis)
-
- resolve(frappe.chat.emojis)
- }
- else
- $.get('https://cdn.rawgit.com/frappe/emoji/master/emoji', (data) => {
- frappe.chat.emojis = JSON.parse(data)
-
- if ( fn )
- fn(frappe.chat.emojis)
-
- resolve(frappe.chat.emojis)
- })
- })
-}
-
-// Website Settings
-frappe.provide('frappe.chat.website.settings')
-frappe.chat.website.settings = (fields, fn) =>
-{
- if ( typeof fields === "function" ) {
- fn = fields
- fields = null
- } else
- if ( typeof fields === "string" )
- fields = frappe._.as_array(fields)
-
- return new Promise(resolve => {
- frappe.call("frappe.chat.website.settings",
- { fields: fields })
- .then(response => {
- var message = response.message
-
- if ( message.enable_from )
- message = { ...message, enable_from: new frappe.datetime.datetime(message.enable_from, 'HH:mm:ss') }
- if ( message.enable_to )
- message = { ...message, enable_to: new frappe.datetime.datetime(message.enable_to, 'HH:mm:ss') }
-
- if ( fn )
- fn(message)
-
- resolve(message)
- })
- })
-}
-
-frappe.chat.website.token = (fn) =>
-{
- return new Promise(resolve => {
- frappe.call("frappe.chat.website.token")
- .then(response => {
- if ( fn )
- fn(response.message)
-
- resolve(response.message)
- })
- })
-}
-
-const { h, Component } = hyper
-
-// frappe.components
-// frappe's component namespace.
-frappe.provide('frappe.components')
-
-frappe.provide('frappe.chat.component')
-
-/**
- * @description Button Component
- *
- * @prop {string} type - (Optional) "default", "primary", "info", "success", "warning", "danger" (defaults to "default")
- * @prop {boolean} block - (Optional) Render a button block (defaults to false).
- */
-frappe.components.Button
-=
-class extends Component {
- render ( ) {
- const { props } = this
- const size = frappe.components.Button.SIZE[props.size]
-
- return (
- h("button", { ...props, class: `btn ${size && size.class} btn-${props.type} ${props.block ? "btn-block" : ""} ${props.class ? props.class : ""}` },
- props.children
- )
- )
- }
-}
-frappe.components.Button.SIZE
-=
-{
- small: {
- class: "btn-sm"
- },
- large: {
- class: "btn-lg"
- }
-}
-frappe.components.Button.defaultProps
-=
-{
- type: "default",
- block: false
-}
-
-/**
- * @description FAB Component
- *
- * @extends frappe.components.Button
- */
-frappe.components.FAB
-=
-class extends frappe.components.Button {
- render ( ) {
- const { props } = this
- const size = frappe.components.FAB.SIZE[props.size]
-
- return (
- h(frappe.components.Button, { ...props, class: `${props.class} ${size && size.class}`},
- h("i", { class: props.icon })
- )
- )
- }
-}
-frappe.components.FAB.defaultProps
-=
-{
- icon: "octicon octicon-plus"
-}
-frappe.components.FAB.SIZE
-=
-{
- small:
- {
- class: "frappe-fab-sm"
- },
- large:
- {
- class: "frappe-fab-lg"
- }
-}
-
-/**
- * @description Octicon Component
- *
- * @prop color - (Required) color for the indicator
- */
-frappe.components.Indicator
-=
-class extends Component {
- render ( ) {
- const { props } = this
-
- return props.color ? h("span", { ...props, class: `indicator ${props.color}` }) : null
- }
-}
-
-/**
- * @description FontAwesome Component
- */
-frappe.components.FontAwesome
-=
-class extends Component {
- render ( ) {
- const { props } = this
-
- return props.type ? h("i", { ...props, class: `fa ${props.fixed ? "fa-fw" : ""} fa-${props.type} ${props.class}` }) : null
- }
-}
-frappe.components.FontAwesome.defaultProps
-=
-{
- fixed: false
-}
-
-/**
- * @description Octicon Component
- *
- * @extends frappe.Component
- */
-frappe.components.Octicon
-=
-class extends Component {
- render ( ) {
- const { props } = this
-
- return props.type ? h("i", { ...props, class: `octicon octicon-${props.type}` }) : null
- }
-}
-
-/**
- * @description Avatar Component
- *
- * @prop {string} title - (Optional) title for the avatar.
- * @prop {string} abbr - (Optional) abbreviation for the avatar, defaults to the first letter of the title.
- * @prop {string} size - (Optional) size of the avatar to be displayed.
- * @prop {image} image - (Optional) image for the avatar, defaults to the first letter of the title.
- */
-frappe.components.Avatar
-=
-class extends Component {
- render ( ) {
- const { props } = this
- const abbr = props.abbr || props.title.substr(0, 1)
- const size = frappe.components.Avatar.SIZE[props.size] || frappe.components.Avatar.SIZE.medium
-
- return (
- h("span", { class: `avatar ${size.class} ${props.class ? props.class : ""}` },
- props.image ?
- h("img", { class: "media-object", src: props.image })
- :
- h("div", { class: "standard-image" }, abbr)
- )
- )
- }
-}
-frappe.components.Avatar.SIZE
-=
-{
- small:
- {
- class: "avatar-small"
- },
- large:
- {
- class: "avatar-large"
- },
- medium:
- {
- class: "avatar-medium"
- }
-}
-
-/**
- * @description Frappe Chat Object.
- *
- * @example
- * const chat = new frappe.Chat(options) // appends to "body"
- * chat.render()
- * const chat = new frappe.Chat(".selector", options)
- * chat.render()
- *
- * const chat = new frappe.Chat()
- * chat.set_wrapper('.selector')
- * .set_options(options)
- * .render()
- */
-frappe.Chat
-=
-class {
- /**
- * @description Frappe Chat Object.
- *
- * @param {string} selector - A query selector, HTML Element or jQuery object.
- * @param {object} options - Optional configurations.
- */
- constructor (selector, options) {
- if ( !(typeof selector === "string" || selector instanceof $ || selector instanceof HTMLElement) ) {
- options = selector
- selector = null
- }
-
- this.options = frappe.Chat.OPTIONS
-
- this.set_wrapper(selector ? selector : "body")
- this.set_options(options)
-
- }
-
- /**
- * Set the container on which the chat widget is mounted on.
- * @param {string|HTMLElement} selector - A query selector, HTML Element or jQuery object.
- *
- * @returns {frappe.Chat} - The instance.
- *
- * @example
- * const chat = new frappe.Chat()
- * chat.set_wrapper(".selector")
- */
- set_wrapper (selector) {
- this.$wrapper = $(selector)
-
- return this
- }
-
- /**
- * Set the configurations for the chat interface.
- * @param {object} options - Optional Configurations.
- *
- * @returns {frappe.Chat} - The instance.
- *
- * @example
- * const chat = new frappe.Chat()
- * chat.set_options({ layout: frappe.Chat.Layout.PAGE })
- */
- set_options (options) {
- this.options = { ...this.options, ...options }
-
- return this
- }
-
- /**
- * @description Destory the chat widget.
- *
- * @returns {frappe.Chat} - The instance.
- *
- * @example
- * const chat = new frappe.Chat()
- * chat.render()
- * .destroy()
- */
- destroy ( ) {
- const $wrapper = this.$wrapper
- $wrapper.remove(".frappe-chat")
-
- return this
- }
-
- /**
- * @description Render the chat widget component onto destined wrapper.
- *
- * @returns {frappe.Chat} - The instance.
- *
- * @example
- * const chat = new frappe.Chat()
- * chat.render()
- */
- render (props = { }) {
- this.destroy()
-
- const $wrapper = this.$wrapper
- const options = this.options
-
- const component = h(frappe.Chat.Widget, {
- layout: options.layout,
- target: options.target,
- ...props
- })
-
- hyper.render(component, $wrapper[0])
-
- return this
- }
-}
-frappe.Chat.Layout
-=
-{
- PAGE: "page", POPPER: "popper"
-}
-frappe.Chat.OPTIONS
-=
-{
- layout: frappe.Chat.Layout.POPPER
-}
-
-/**
- * @description The base Component for Frappe Chat
- */
-frappe.Chat.Widget
-=
-class extends Component {
- constructor (props) {
- super (props)
-
- this.setup(props)
- this.make()
- }
-
- setup (props) {
- // room actions
- this.room = { }
- this.room.add = rooms => {
- rooms = frappe._.as_array(rooms)
- const names = rooms.map(r => r.name)
-
- frappe.log.info(`Subscribing ${frappe.session.user} to Chat Rooms ${names.join(", ")}.`)
- frappe.chat.room.subscribe(names)
-
- const state = [ ]
-
- for (const room of rooms)
- if ( ["Group", "Visitor"].includes(room.type) || room.owner === frappe.session.user || room.last_message || room.users.includes(frappe.session.user)) {
- frappe.log.info(`Adding ${room.name} to component.`)
- state.push(room)
- }
-
- this.set_state({ rooms: [ ...this.state.rooms, ...state ] })
- }
- this.room.update = (room, update) => {
- const { state } = this
- var exists = false
- const rooms = state.rooms.map(r => {
- if ( r.name === room ) {
- exists = true
- if ( update.typing ) {
- if ( !frappe._.is_empty(r.typing) ) {
- const usr = update.typing
- if ( !r.typing.includes(usr) ) {
- update.typing = frappe._.copy_array(r.typing)
- update.typing.push(usr)
- }
- }
- else
- update.typing = frappe._.as_array(update.typing)
- }
-
- return { ...r, ...update }
- }
-
- return r
- })
-
- if ( frappe.session.user !== 'Guest' ) {
- if ( !exists )
- frappe.chat.room.get(room, (room) => this.room.add(room))
- else
- this.set_state({ rooms })
- }
-
- if ( state.room.name === room ) {
- if ( update.typing ) {
- if ( !frappe._.is_empty(state.room.typing) ) {
- const usr = update.typing
- if ( !state.room.typing.includes(usr) ) {
- update.typing = frappe._.copy_array(state.room.typing)
- update.typing.push(usr)
- }
- } else
- update.typing = frappe._.as_array(update.typing)
- }
-
- const room = { ...state.room, ...update }
-
- this.set_state({ room })
- }
- }
- this.room.select = (name) => {
- frappe.chat.room.history(name, (messages) => {
- const { state } = this
- const room = state.rooms.find(r => r.name === name)
-
- this.set_state({
- room: { ...state.room, ...room, messages: messages }
- })
- })
- }
-
- this.state = { ...frappe.Chat.Widget.defaultState, ...props }
- }
-
- make ( ) {
- if ( frappe.session.user !== 'Guest' ) {
- frappe.chat.profile.create([
- "status", "message_preview", "notification_tones", "conversation_tones"
- ]).then(profile => {
- this.set_state({ profile })
-
- frappe.chat.room.get(rooms => {
- rooms = frappe._.as_array(rooms)
- frappe.log.info(`User ${frappe.session.user} is subscribed to ${rooms.length} ${frappe._.pluralize('room', rooms.length)}.`)
-
- if ( !frappe._.is_empty(rooms) )
- this.room.add(rooms)
- })
-
- this.bind()
- })
- } else {
- this.bind()
- }
- }
-
- bind ( ) {
- frappe.chat.profile.on.update((user, update) => {
- frappe.log.warn(`TRIGGER: Chat Profile update ${JSON.stringify(update)} of User ${user}.`)
-
- if ( 'status' in update ) {
- if ( user === frappe.session.user ) {
- this.set_state({
- profile: { ...this.state.profile, status: update.status }
- })
- } else {
- const status = frappe.chat.profile.STATUSES.find(s => s.name === update.status)
- const color = status.color
-
- const alert = ` ${frappe.user.full_name(user)} is currently ${update.status}`
- frappe.show_alert(alert, 3)
- }
- }
- })
-
- frappe.chat.room.on.create((room) => {
- frappe.log.warn(`TRIGGER: Chat Room ${room.name} created.`)
- this.room.add(room)
- })
-
- frappe.chat.room.on.update((room, update) => {
- frappe.log.warn(`TRIGGER: Chat Room ${room} update ${JSON.stringify(update)} recieved.`)
- this.room.update(room, update)
- })
-
- frappe.chat.room.on.typing((room, user) => {
- if ( user !== frappe.session.user ) {
- frappe.log.warn(`User ${user} typing in Chat Room ${room}.`)
- this.room.update(room, { typing: user })
-
- setTimeout(() => this.room.update(room, { typing: null }), 5000)
- }
- })
-
- frappe.chat.message.on.create((r) => {
- const { state } = this
-
- // play sound.
- if ( state.room.name )
- state.profile.conversation_tones && frappe.chat.sound.play('message')
- else
- state.profile.notification_tones && frappe.chat.sound.play('notification')
-
- if ( r.user !== frappe.session.user && state.profile.message_preview && !state.toggle ) {
- const $element = $('body').find('.frappe-chat-alert')
- $element.remove()
-
- const alert = // TODO: ellipses content
- `
-
-
-
-
-
- ${frappe.user.first_name(r.user)}: ${r.content}
-
- `
- frappe.show_alert(alert, 15, {
- "show-message": function (r) {
- this.room.select(r.room)
- this.base.firstChild._component.toggle()
- }.bind(this, r)
- })
- frappe.notify(`${frappe.user.first_name(r.user)}`, {
- body: r.content,
- icon: frappe.user.image(r.user),
- tag: r.user
- })
- }
-
- if ( r.room === state.room.name ) {
- const mess = frappe._.copy_array(state.room.messages)
- mess.push(r)
-
- this.set_state({ room: { ...state.room, messages: mess } })
- }
- })
-
- frappe.chat.message.on.update((message, update) => {
- frappe.log.warn(`TRIGGER: Chat Message ${message} update ${JSON.stringify(update)} recieved.`)
- })
- }
-
- render ( ) {
- const { props, state } = this
- const me = this
-
- const ActionBar = h(frappe.Chat.Widget.ActionBar, {
- placeholder: __("Search or Create a New Chat"),
- class: "level",
- layout: props.layout,
- actions:
- frappe._.compact([
- {
- label: __("New"),
- onclick: function ( ) {
- const dialog = new frappe.ui.Dialog({
- title: __("New Chat"),
- fields: [
- {
- label: __("Chat Type"),
- fieldname: "type",
- fieldtype: "Select",
- options: ["Group", "Direct Chat"],
- default: "Group",
- onchange: () => {
- const type = dialog.get_value("type")
- const is_group = type === "Group"
-
- dialog.set_df_property("group_name", "reqd", is_group)
- dialog.set_df_property("user", "reqd", !is_group)
- }
- },
- {
- label: __("Group Name"),
- fieldname: "group_name",
- fieldtype: "Data",
- reqd: true,
- depends_on: "eval:doc.type == 'Group'"
- },
- {
- label: __("Users"),
- fieldname: "users",
- fieldtype: "MultiSelect",
- options: frappe.user.get_emails(),
- depends_on: "eval:doc.type == 'Group'"
- },
- {
- label: __("User"),
- fieldname: "user",
- fieldtype: "Link",
- options: "User",
- depends_on: "eval:doc.type == 'Direct Chat'"
- }
- ],
- action: {
- primary: {
- label: __('Create'),
- onsubmit: (values) => {
- if ( values.type === "Group" ) {
- if ( !frappe._.is_empty(values.users) ) {
- const name = values.group_name
- const users = dialog.fields_dict.users.get_values()
-
- frappe.chat.room.create("Group", null, users, name)
- }
- } else {
- const user = values.user
-
- frappe.chat.room.create("Direct", null, user)
- }
- dialog.hide()
- }
- }
- }
- })
- dialog.show()
- }
- },
- frappe._.is_mobile() && {
- icon: "octicon octicon-x",
- class: "frappe-chat-close",
- onclick: () => this.set_state({ toggle: false })
- }
- ], Boolean),
- change: query => { me.set_state({ query }) },
- span: span => { me.set_state({ span }) },
- })
-
- var contacts = [ ]
- if ( 'user_info' in frappe.boot ) {
- const emails = frappe.user.get_emails()
- for (const email of emails) {
- var exists = false
-
- for (const room of state.rooms) {
- if ( room.type === 'Direct' ) {
- if ( room.owner === email || frappe._.squash(room.users) === email )
- exists = true
- }
- }
-
- if ( !exists )
- contacts.push({ owner: frappe.session.user, users: [email] })
- }
- }
- const rooms = state.query ? frappe.chat.room.search(state.query, state.rooms.concat(contacts)) : frappe.chat.room.sort(state.rooms)
-
- const layout = state.span ? frappe.Chat.Layout.PAGE : frappe.Chat.Layout.POPPER
-
- const RoomList = frappe._.is_empty(rooms) && !state.query ?
- h("div", { class: "vcenter" },
- h("div", { class: "text-center text-extra-muted" },
- h("p","",__("You don't have any messages yet."))
- )
- )
- :
- h(frappe.Chat.Widget.RoomList, { rooms: rooms, click: room => {
- if ( room.name )
- this.room.select(room.name)
- else
- frappe.chat.room.create("Direct", room.owner, frappe._.squash(room.users), ({ name }) => this.room.select(name))
- }})
- const Room = h(frappe.Chat.Widget.Room, { ...state.room, layout: layout, destroy: () => {
- this.set_state({
- room: { name: null, messages: [ ] }
- })
- }})
-
- const component = layout === frappe.Chat.Layout.POPPER ?
- h(frappe.Chat.Widget.Popper, { heading: ActionBar, page: state.room.name && Room, target: props.target,
- toggle: (t) => this.set_state({ toggle: t }) },
- RoomList
- )
- :
- h("div", { class: "frappe-chat-popper" },
- h("div", { class: "frappe-chat-popper-collapse" },
- h("div", { class: "panel panel-default panel-span", style: { width: "25%" } },
- h("div", { class: "panel-heading" },
- ActionBar
- ),
- RoomList
- ),
- Room
- )
- )
-
- return (
- h("div", { class: "frappe-chat" },
- component
- )
- )
- }
-}
-frappe.Chat.Widget.defaultState = {
- query: "",
- profile: { },
- rooms: [ ],
- room: { name: null, messages: [ ], typing: [ ] },
- toggle: false,
- span: false
-}
-frappe.Chat.Widget.defaultProps = {
- layout: frappe.Chat.Layout.POPPER
-}
-
-/**
- * @description Chat Widget Popper HOC.
- */
-frappe.Chat.Widget.Popper
-=
-class extends Component {
- constructor (props) {
- super (props)
-
- this.setup(props);
- }
-
- setup (props) {
- this.toggle = this.toggle.bind(this)
-
- this.state = frappe.Chat.Widget.Popper.defaultState
-
- if ( props.target )
- $(props.target).click(() => this.toggle())
-
- frappe.chat.widget = this
- }
-
- toggle (active) {
- let toggle
- if ( arguments.length === 1 )
- toggle = active
- else
- toggle = this.state.active ? false : true
-
- this.set_state({ active: toggle })
-
- this.props.toggle(toggle)
- }
-
- on_mounted ( ) {
- $(document.body).on('click', '.page-container, .frappe-chat-close', ({ currentTarget }) => {
- this.toggle(false)
- })
- }
-
- render ( ) {
- const { props, state } = this
-
- return !state.destroy ?
- (
- h("div", { class: "frappe-chat-popper", style: !props.target ? { "margin-bottom": "80px" } : null },
- !props.target ?
- h(frappe.components.FAB, {
- class: "frappe-fab",
- icon: state.active ? "fa fa-fw fa-times" : "font-heavy octicon octicon-comment",
- size: frappe._.is_mobile() ? null : "large",
- type: "primary",
- onclick: () => this.toggle(),
- }) : null,
- state.active ?
- h("div", { class: "frappe-chat-popper-collapse" },
- props.page ? props.page : (
- h("div", { class: `panel panel-default ${frappe._.is_mobile() ? "panel-span" : ""}` },
- h("div", { class: "panel-heading" },
- props.heading
- ),
- props.children
- )
- )
- ) : null
- )
- ) : null
- }
-}
-frappe.Chat.Widget.Popper.defaultState
-=
-{
- active: false,
- destroy: false
-}
-
-/**
- * @description frappe.Chat.Widget ActionBar Component
- */
-frappe.Chat.Widget.ActionBar
-=
-class extends Component {
- constructor (props) {
- super (props)
-
- this.change = this.change.bind(this)
- this.submit = this.submit.bind(this)
-
- this.state = frappe.Chat.Widget.ActionBar.defaultState
- }
-
- change (e) {
- const { props, state } = this
-
- this.set_state({
- [e.target.name]: e.target.value
- })
-
- props.change(state.query)
- }
-
- submit (e) {
- const { props, state } = this
-
- e.preventDefault()
-
- props.submit(state.query)
- }
-
- render ( ) {
- const me = this
- const { props, state } = this
- const { actions } = props
-
- return (
- h("div", { class: `frappe-chat-action-bar ${props.class ? props.class : ""}` },
- h("form", { oninput: this.change, onsubmit: this.submit },
- h("input", { autocomplete: "off", class: "form-control input-sm", name: "query", value: state.query, placeholder: props.placeholder || "Search" }),
- ),
- !frappe._.is_empty(actions) ?
- actions.map(action => h(frappe.Chat.Widget.ActionBar.Action, { ...action })) : null,
- !frappe._.is_mobile() ?
- h(frappe.Chat.Widget.ActionBar.Action, {
- icon: `octicon octicon-screen-${state.span ? "normal" : "full"}`,
- onclick: () => {
- const span = !state.span
- me.set_state({ span })
- props.span(span)
- }
- })
- :
- null
- )
- )
- }
-}
-frappe.Chat.Widget.ActionBar.defaultState
-=
-{
- query: null,
- span: false
-}
-
-/**
- * @description frappe.Chat.Widget ActionBar's Action Component.
- */
-frappe.Chat.Widget.ActionBar.Action
-=
-class extends Component {
- render ( ) {
- const { props } = this
-
- return (
- h(frappe.components.Button, { size: "small", class: "btn-action", ...props },
- props.icon ? h("i", { class: props.icon }) : null,
- `${props.icon ? " " : ""}${props.label ? props.label : ""}`
- )
- )
- }
-}
-
-/**
- * @description frappe.Chat.Widget RoomList Component
- */
-frappe.Chat.Widget.RoomList
-=
-class extends Component {
- render ( ) {
- const { props } = this
- const rooms = props.rooms
-
- return !frappe._.is_empty(rooms) ? (
- h("ul", { class: "frappe-chat-room-list nav nav-pills nav-stacked" },
- rooms.map(room => h(frappe.Chat.Widget.RoomList.Item, { ...room, click: props.click }))
- )
- ) : null
- }
-}
-
-/**
- * @description frappe.Chat.Widget RoomList's Item Component
- */
-frappe.Chat.Widget.RoomList.Item
-=
-class extends Component {
- render ( ) {
- const { props } = this
- const item = { }
-
- if ( props.type === "Group" ) {
- item.title = props.room_name
- item.image = props.avatar
-
- if ( !frappe._.is_empty(props.typing) ) {
- props.typing = frappe._.as_array(props.typing) // HACK: (BUG) why does typing return a string?
- const names = props.typing.map(user => frappe.user.first_name(user))
- item.subtitle = `${names.join(", ")} typing...`
- } else
- if ( props.last_message ) {
- const message = props.last_message
- const content = message.content
-
- if ( message.type === "File" ) {
- item.subtitle = `📁 ${content.name}`
- } else {
- item.subtitle = props.last_message.content
- }
- }
- } else {
- const user = props.owner === frappe.session.user ? frappe._.squash(props.users) : props.owner
-
- item.title = frappe.user.full_name(user)
- item.image = frappe.user.image(user)
- item.abbr = frappe.user.abbr(user)
-
- if ( !frappe._.is_empty(props.typing) )
- item.subtitle = 'typing...'
- else
- if ( props.last_message ) {
- const message = props.last_message
- const content = message.content
-
- if ( message.type === "File" ) {
- item.subtitle = `📁 ${content.name}`
- } else {
- item.subtitle = props.last_message.content
- }
- }
- }
-
- let is_unread = false
- if ( props.last_message ) {
- item.timestamp = frappe.chat.pretty_datetime(props.last_message.creation)
- is_unread = !props.last_message.seen.includes(frappe.session.user)
- }
-
- return (
- h("li", null,
- h("a", { class: props.active ? "active": "", onclick: () => {
- if (props.last_message) {
- frappe.chat.message.seen(props.last_message.name);
- }
- props.click(props)
- } },
- h("div", { class: "row" },
- h("div", { class: "col-xs-9" },
- h(frappe.Chat.Widget.MediaProfile, { ...item })
- ),
- h("div", { class: "col-xs-3 text-right" },
- [
- h("div", { class: "text-muted", style: { "font-size": "9px" } }, item.timestamp),
- is_unread ? h("span", { class: "indicator red" }) : null
- ]
- ),
- )
- )
- )
- )
- }
-}
-
-/**
- * @description frappe.Chat.Widget's MediProfile Component.
- */
-frappe.Chat.Widget.MediaProfile
-=
-class extends Component {
- render ( ) {
- const { props } = this
- const position = frappe.Chat.Widget.MediaProfile.POSITION[props.position || "left"]
- const avatar = (
- h("div", { class: `${position.class} media-middle` },
- h(frappe.components.Avatar, { ...props,
- title: props.title,
- image: props.image,
- size: props.size,
- abbr: props.abbr
- })
- )
- )
-
- return (
- h("div", { class: "media", style: position.class === "media-right" ? { "text-align": "right" } : null },
- position.class === "media-left" ? avatar : null,
- h("div", { class: "media-body" },
- h("div", { class: "media-heading ellipsis small", style: `max-width: ${props.width_title || "100%"} display: inline-block` }, props.title),
- props.content ? h("div","",h("small","",props.content)) : null,
- props.subtitle ? h("div",{ class: "media-subtitle small" },h("small", { class: "text-muted" }, props.subtitle)) : null
- ),
- position.class === "media-right" ? avatar : null
- )
- )
- }
-}
-frappe.Chat.Widget.MediaProfile.POSITION
-=
-{
- left: { class: "media-left" }, right: { class: "media-right" }
-}
-
-/**
- * @description frappe.Chat.Widget Room Component
- */
-frappe.Chat.Widget.Room
-=
-class extends Component {
- render ( ) {
- const { props, state } = this
- const hints =
- [
- {
- match: /@(\w*)$/,
- search: function (keyword, callback) {
- if ( props.type === 'Group' ) {
- const query = keyword.slice(1)
- const users = [].concat(frappe._.as_array(props.owner), props.users)
- const grep = users.filter(user => user !== frappe.session.user && user.indexOf(query) === 0)
-
- callback(grep)
- }
- },
- component: function (item) {
- return (
- h(frappe.Chat.Widget.MediaProfile, {
- title: frappe.user.full_name(item),
- image: frappe.user.image(item),
- size: "small"
- })
- )
- }
- },
- {
- match: /:([a-z]*)$/,
- search: function (keyword, callback) {
- frappe.chat.emoji(function (emojis) {
- const query = keyword.slice(1)
- const items = [ ]
- for (const emoji of emojis)
- for (const alias of emoji.aliases)
- if ( alias.indexOf(query) === 0 )
- items.push({ name: alias, value: emoji.emoji })
-
- callback(items)
- })
- },
- content: (item) => item.value,
- component: function (item) {
- return (
- h(frappe.Chat.Widget.MediaProfile, {
- title: item.name,
- abbr: item.value,
- size: "small"
- })
- )
- }
- }
- ]
-
- const actions = frappe._.compact([
- !frappe._.is_mobile() && {
- icon: "camera",
- label: "Camera",
- onclick: ( ) => {
- const capture = new frappe.ui.Capture({
- animate: false,
- error: true
- })
- capture.show()
-
- capture.submit(data_url => {
- // data_url
- })
- }
- },
- {
- icon: "file",
- label: "File",
- onclick: ( ) => {
- new frappe.ui.FileUploader({
- doctype: "Chat Room",
- docname: props.name,
- on_success(file_doc) {
- const { file_url, filename } = file_doc
- frappe.chat.message.send(props.name, { path: file_url, name: filename }, "File")
- }
- })
- }
- }
- ])
-
- if ( frappe.session.user !== 'Guest' ) {
- if (props.messages) {
- props.messages = frappe._.as_array(props.messages)
- for (const message of props.messages)
- if ( !message.seen.includes(frappe.session.user) )
- frappe.chat.message.seen(message.name)
- else
- break
- }
- }
-
- return (
- h("div", { class: `panel panel-default
- ${props.name ? "panel-bg" : ""}
- ${props.layout === frappe.Chat.Layout.PAGE || frappe._.is_mobile() ? "panel-span" : ""}`,
- style: props.layout === frappe.Chat.Layout.PAGE && { width: "75%", left: "25%", "box-shadow": "none" } },
- props.name && h(frappe.Chat.Widget.Room.Header, { ...props, on_back: props.destroy }),
- props.name ?
- !frappe._.is_empty(props.messages) ?
- h(frappe.chat.component.ChatList, {
- messages: props.messages
- })
- :
- h("div", { class: "panel-body", style: { "height": "100%" } },
- h("div", { class: "vcenter" },
- h("div", { class: "text-center text-extra-muted" },
- h(frappe.components.Octicon, { type: "comment-discussion", style: "font-size: 48px" }),
- h("p","",__("Start a conversation."))
- )
- )
- )
- :
- h("div", { class: "panel-body", style: { "height": "100%" } },
- h("div", { class: "vcenter" },
- h("div", { class: "text-center text-extra-muted" },
- h(frappe.components.Octicon, { type: "comment-discussion", style: "font-size: 125px" }),
- h("p","",__("Select a chat to start messaging."))
- )
- )
- ),
- props.name ?
- h("div", { class: "chat-room-footer" },
- h(frappe.chat.component.ChatForm, { actions: actions,
- onchange: () => {
- frappe.chat.message.typing(props.name)
- },
- onsubmit: (message) => {
- frappe.chat.message.send(props.name, message)
- },
- hint: hints
- })
- )
- :
- null
- )
- )
- }
-}
-
-frappe.Chat.Widget.Room.Header
-=
-class extends Component {
- render ( ) {
- const { props } = this
-
- const item = { }
-
- if ( ["Group", "Visitor"].includes(props.type) ) {
- item.route = `chat-room/${props.name}`
-
- item.title = props.room_name
- item.image = props.avatar
-
- if ( !frappe._.is_empty(props.typing) ) {
- props.typing = frappe._.as_array(props.typing) // HACK: (BUG) why does typing return as a string?
- const users = props.typing.map(user => frappe.user.first_name(user))
- item.subtitle = `${users.join(", ")} typing...`
- } else
- item.subtitle = props.type === "Group" ?
- `${props.users.length} ${frappe._.pluralize('member', props.users.length)}`
- : ""
- }
- else {
- const user = props.owner === frappe.session.user ? frappe._.squash(props.users) : props.owner
-
- item.route = `user/${user}`
-
- item.title = frappe.user.full_name(user)
- item.image = frappe.user.image(user)
-
- if ( !frappe._.is_empty(props.typing) )
- item.subtitle = 'typing...'
- }
-
- const popper = props.layout === frappe.Chat.Layout.POPPER || frappe._.is_mobile()
-
- return (
- h("div", { class: "panel-heading", style: { "height": "50px" } }, // sorry. :(
- h("div", { class: "level" },
- popper && frappe.session.user !== "Guest" ?
- h(frappe.components.Button,{class:"btn-back",onclick:props.on_back},
- h(frappe.components.Octicon, { type: "chevron-left" })
- ) : null,
- h("div","",
- h("div", { class: "panel-title" },
- h("div", { class: "cursor-pointer", onclick: () => {
- frappe.session.user !== "Guest" ?
- frappe.set_route(item.route) : null;
- }},
- h(frappe.Chat.Widget.MediaProfile, { ...item })
- )
- )
- ),
- h("div", { class: popper ? "col-xs-2" : "col-xs-3" },
- h("div", { class: "text-right" },
- frappe._.is_mobile() && h(frappe.components.Button, { class: "frappe-chat-close", onclick: props.toggle },
- h(frappe.components.Octicon, { type: "x" })
- )
- )
- )
- )
- )
- )
- }
-}
-
-/**
- * @description ChatList Component
- *
- * @prop {array} messages - ChatMessage(s)
- */
-frappe.chat.component.ChatList
-=
-class extends Component {
- on_mounted ( ) {
- this.$element = $('.frappe-chat').find('.chat-list')
- this.$element.scrollTop(this.$element[0].scrollHeight)
- }
-
- on_updated ( ) {
- this.$element.scrollTop(this.$element[0].scrollHeight)
- }
-
- render ( ) {
- var messages = [ ]
- for (var i = 0 ; i < this.props.messages.length ; ++i) {
- var message = this.props.messages[i]
- const me = message.user === frappe.session.user
-
- if ( i === 0 || !frappe.datetime.equal(message.creation, this.props.messages[i - 1].creation, 'day') )
- messages.push({ type: "Notification", content: message.creation.format('MMMM DD') })
-
- messages.push(message)
- }
-
- return (
- h("div",{class:"chat-list list-group"},
- !frappe._.is_empty(messages) ?
- messages.map(m => h(frappe.chat.component.ChatList.Item, {...m})) : null
- )
- )
- }
-}
-
-/**
- * @description ChatList.Item Component
- *
- * @prop {string} name - ChatMessage name
- * @prop {string} user - ChatMessage user
- * @prop {string} room - ChatMessage room
- * @prop {string} room_type - ChatMessage room_type ("Direct", "Group" or "Visitor")
- * @prop {string} content - ChatMessage content
- * @prop {frappe.datetime.datetime} creation - ChatMessage creation
- *
- * @prop {boolean} groupable - Whether the ChatMessage is groupable.
- */
-frappe.chat.component.ChatList.Item
-=
-class extends Component {
- render ( ) {
- const { props } = this
-
- const me = props.user === frappe.session.user
- const content = props.content
-
- return (
- h("div",{class: "chat-list-item list-group-item"},
- props.type === "Notification" ?
- h("div",{class:"chat-list-notification"},
- h("div",{class:"chat-list-notification-content"},
- content
- )
- )
- :
- h("div",{class:`${me ? "text-right" : ""}`},
- props.room_type === "Group" && !me ?
- h(frappe.components.Avatar, {
- title: frappe.user.full_name(props.user),
- image: frappe.user.image(props.user)
- }) : null,
- h(frappe.chat.component.ChatBubble, props)
- )
- )
- )
- }
-}
-
-/**
- * @description ChatBubble Component
- *
- * @prop {string} name - ChatMessage name
- * @prop {string} user - ChatMessage user
- * @prop {string} room - ChatMessage room
- * @prop {string} room_type - ChatMessage room_type ("Direct", "Group" or "Visitor")
- * @prop {string} content - ChatMessage content
- * @prop {frappe.datetime.datetime} creation - ChatMessage creation
- *
- * @prop {boolean} groupable - Whether the ChatMessage is groupable.
- */
-frappe.chat.component.ChatBubble
-=
-class extends Component {
- constructor (props) {
- super (props)
-
- this.onclick = this.onclick.bind(this)
- }
-
- onclick ( ) {
- const { props } = this
- if ( props.user === frappe.session.user ) {
- frappe.quick_edit("Chat Message", props.name, (values) => {
-
- })
- }
- }
-
- render ( ) {
- const { props } = this
- const creation = props.creation.format('hh:mm A')
-
- const me = props.user === frappe.session.user
- const read = !frappe._.is_empty(props.seen) && !props.seen.includes(frappe.session.user)
-
- const content = props.content
-
- return (
- h("div",{class:`chat-bubble ${props.groupable ? "chat-groupable" : ""} chat-bubble-${me ? "r" : "l"}`,
- onclick: this.onclick},
- props.room_type === "Group" && !me ?
- h("div",{class:"chat-bubble-author"},
- h("a", { onclick: () => { frappe.set_route('Form', 'User', props.user) } },
- frappe.user.full_name(props.user)
- )
- ) : null,
- h("div",{class:"chat-bubble-content"},
- h("small","",
- props.type === "File" ?
- h("a", { class: "no-decoration", href: content.path, target: "_blank" },
- h(frappe.components.FontAwesome, { type: "file", fixed: true }), ` ${content.name}`
- )
- :
- content
- )
- ),
- h("div",{class:"chat-bubble-meta"},
- h("span",{class:"chat-bubble-creation"},creation),
- me && read ?
- h("span",{class:"chat-bubble-check"},
- h(frappe.components.Octicon,{type:"check"})
- ) : null
- )
- )
- )
- }
-}
-
-/**
- * @description ChatForm Component
- */
-frappe.chat.component.ChatForm
-=
-class extends Component {
- constructor (props) {
- super (props)
-
- this.onchange = this.onchange.bind(this)
- this.onsubmit = this.onsubmit.bind(this)
-
- this.hint = this.hint.bind(this)
-
- this.state = frappe.chat.component.ChatForm.defaultState
- }
-
- onchange (e) {
- const { props, state } = this
- const value = e.target.value
-
- this.set_state({
- [e.target.name]: value
- })
-
- props.onchange(state)
-
- this.hint(value)
- }
-
- hint (value) {
- const { props, state } = this
-
- if ( props.hint ) {
- const tokens = value.split(" ")
- const sliced = tokens.slice(0, tokens.length - 1)
-
- const token = tokens[tokens.length - 1]
-
- if ( token ) {
- props.hint = frappe._.as_array(props.hint)
- const hint = props.hint.find(hint => hint.match.test(token))
-
- if ( hint ) {
- hint.search(token, items => {
- const hints = items.map(item => {
- // You should stop writing one-liners! >_>
- const replace = token.replace(hint.match, hint.content ? hint.content(item) : item)
- const content = `${sliced.join(" ")} ${replace}`.trim()
- item = { component: hint.component(item), content: content }
-
- return item
- }).slice(0, hint.max || 5)
-
- this.set_state({ hints })
- })
- }
- else
- this.set_state({ hints: [ ] })
- } else
- this.set_state({ hints: [ ] })
- }
- }
-
- onsubmit (e) {
- e.preventDefault()
-
- if ( this.state.content ) {
- this.props.onsubmit(this.state.content)
-
- this.set_state({ content: null })
- }
- }
-
- render ( ) {
- const { props, state } = this
-
- return (
- h("div",{class:"chat-form"},
- state.hints.length ?
- h("ul", { class: "hint-list list-group" },
- state.hints.map((item) => {
- return (
- h("li", { class: "hint-list-item list-group-item" },
- h("a", { href: "javascript:void(0)", onclick: () => {
- this.set_state({ content: item.content, hints: [ ] })
- }},
- item.component
- )
- )
- )
- })
- ) : null,
- h("form", { oninput: this.onchange, onsubmit: this.onsubmit },
- h("div",{class:"input-group input-group-lg"},
- !frappe._.is_empty(props.actions) ?
- h("div",{class:"input-group-btn dropup"},
- h(frappe.components.Button,{ class: (frappe.session.user === "Guest" ? "disabled" : "dropdown-toggle"), "data-toggle": "dropdown"},
- h(frappe.components.FontAwesome, { class: "text-muted", type: "paperclip", fixed: true })
- ),
- h("div",{ class:"dropdown-menu dropdown-menu-left", onclick: e => e.stopPropagation() },
- !frappe._.is_empty(props.actions) && props.actions.map((action) => {
- return (
- h("li", null,
- h("a",{onclick:action.onclick},
- h(frappe.components.FontAwesome,{type:action.icon,fixed:true}), ` ${action.label}`,
- )
- )
- )
- })
- )
- ) : null,
- h("textarea", {
- class: "form-control",
- name: "content",
- value: state.content,
- placeholder: "Type a message",
- autofocus: true,
- onkeypress: (e) => {
- if ( e.which === frappe.ui.keycode.RETURN && !e.shiftKey )
- this.onsubmit(e)
- }
- }),
- h("div",{class:"input-group-btn"},
- h(frappe.components.Button, { onclick: this.onsubmit },
- h(frappe.components.FontAwesome, { class: !frappe._.is_empty(state.content) ? "text-primary" : "text-muted", type: "send", fixed: true })
- ),
- )
- )
- )
- )
- )
- }
-}
-frappe.chat.component.ChatForm.defaultState
-=
-{
- content: null,
- hints: [ ],
-}
-
-/**
- * @description EmojiPicker Component
- *
- * @todo Under Development
- */
-frappe.chat.component.EmojiPicker
-=
-class extends Component {
- render ( ) {
- const { props } = this
-
- return (
- h("div", { class: `frappe-chat-emoji dropup ${props.class}` },
- h(frappe.components.Button, { type: "primary", class: "dropdown-toggle", "data-toggle": "dropdown" },
- h(frappe.components.FontAwesome, { type: "smile-o", fixed: true })
- ),
- h("div", { class: "dropdown-menu dropdown-menu-right", onclick: e => e.stopPropagation() },
- h("div", { class: "panel panel-default" },
- h(frappe.chat.component.EmojiPicker.List)
- )
- )
- )
- )
- }
-}
-frappe.chat.component.EmojiPicker.List
-=
-class extends Component {
- render ( ) {
- const { props } = this
-
- return (
- h("div", { class: "list-group" },
-
- )
- )
- }
-}
-
-/**
- * @description Python equivalent to sys.platform
- */
-frappe.provide('frappe._')
-frappe._.platform = () => {
- const string = navigator.appVersion
-
- if ( string.includes("Win") ) return "Windows"
- if ( string.includes("Mac") ) return "Darwin"
- if ( string.includes("X11") ) return "UNIX"
- if ( string.includes("Linux") ) return "Linux"
-
- return undefined
-}
-
-/**
- * @description Frappe's Asset Helper
- */
-frappe.provide('frappe.assets')
-frappe.assets.image = (image, app = 'frappe') => {
- const path = `/assets/${app}/images/${image}`
- return path
-}
-
-/**
- * @description Notify using Web Push Notifications
- */
-frappe.provide('frappe.boot')
-frappe.provide('frappe.browser')
-frappe.browser.Notification = 'Notification' in window
-
-frappe.notify = (string, options) => {
- frappe.log = frappe.Logger.get('frappe.notify')
-
- const OPTIONS = {
- icon: frappe.assets.image('favicon.png', 'frappe'),
- lang: frappe.boot.lang || "en"
- }
- options = Object.assign({ }, OPTIONS, options)
-
- if ( !frappe.browser.Notification )
- frappe.log.error('ERROR: This browser does not support desktop notifications.')
-
- Notification.requestPermission(status => {
- if ( status === "granted" ) {
- const notification = new Notification(string, options)
- }
- })
-}
-
-frappe.chat.render = (render = true, force = false) =>
-{
- frappe.log.info(`${render ? "Enable" : "Disable"} Chat for User.`)
-
- const desk = 'desk' in frappe
- if ( desk ) {
- // With the assumption, that there's only one navbar.
- const $placeholder = $('.navbar .frappe-chat-dropdown')
-
- if ( render ) {
- $placeholder.removeClass('hidden')
- } else {
- $placeholder.addClass('hidden')
- }
- }
-
- // Avoid re-renders. Once is enough.
- if ( !frappe.chatter || force ) {
- frappe.chatter = new frappe.Chat({
- target: desk ? '.frappe-chat-toggle' : null
- })
-
- if ( render ) {
- if ( frappe.session.user === 'Guest' && !desk ) {
- frappe.store = frappe.Store.get('frappe.chat')
- var token = frappe.store.get('guest_token')
-
- frappe.log.info(`Local Guest Token - ${token}`)
-
- const setup_room = (token) =>
- {
- return new Promise(resolve => {
- frappe.chat.room.create("Visitor", token).then(room => {
- frappe.log.info(`Visitor Room Created: ${room.name}`)
- frappe.chat.room.subscribe(room.name)
-
- var reference = room
-
- frappe.chat.room.history(room.name).then(messages => {
- const room = { ...reference, messages: messages }
- return room
- }).then(room => {
- resolve(room)
- })
- })
- })
- }
-
- if ( !token ) {
- frappe.chat.website.token().then(token => {
- frappe.log.info(`Generated Guest Token - ${token}`)
- frappe.store.set('guest_token', token)
-
- setup_room(token).then(room => {
- frappe.chatter.render({ room })
- })
- })
- } else {
- setup_room(token).then(room => {
- frappe.chatter.render({ room })
- })
- }
- } else {
- frappe.chatter.render()
- }
- }
- }
-}
-
-frappe.chat.setup = () => {
- frappe.log = frappe.Logger.get('frappe.chat')
-
- frappe.log.info('Setting up frappe.chat')
- frappe.log.warn('TODO: frappe.chat.