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 %} -
- -
- {{ f[1] }} -
-
- - - - {{ f[2] }} -
-
-
- {% endfor %} + {% for f in files %} +
+ +
+ {{ f[1] }} +
+
+ + + + {{ f[3] }} + {% if f[2] %} + + + + {% endif %} +
+
+
+ {% 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 = $(`